1. 程式人生 > 其它 >併發程式設計從零開始(一)

併發程式設計從零開始(一)

併發程式設計從零開始(一)

簡介

java是一個支援多執行緒的開發語言。多執行緒可以在包含多個CPU核心的機器上同時處理多個不同的任務,優化資源的使用率,提升程式的效率。在一些對效能要求比較高場合,多執行緒是java程式調優的重要方面。

Java併發程式設計主要涉及以下幾個部分:

  • 併發程式設計三要素:原子性:即一個不可再被分割的顆粒。在Java中原子性指的是一個或多個操作要麼全部執行成功要麼全部執行失敗。

    有序性:程式執行的順序按照程式碼的先後順序執行。(處理器可能會對指令進行重排序)

    可見性:當多個執行緒訪問同一個變數時,如果其中一個執行緒對其作了修改,其他執行緒能立即獲取到最新的值。

  • 執行緒的五大狀態:

    建立狀態:當用 new 操作符建立一個執行緒的時候

    就緒狀態:呼叫 start 方法,處於就緒狀態的執行緒並不一定馬上就會執行 run 方法,還需要等待CPU的排程

    執行狀態:CPU 開始排程執行緒,並開始執行 run 方法

    阻塞狀態:執行緒的執行過程中由於一些原因進入阻塞狀態比如:呼叫 sleep 方法、嘗試去得到一個鎖等等

    死亡狀態:run 方法執行完 或者 執行過程中遇到了一個異常

  • 悲觀鎖與樂觀鎖:

    悲觀鎖:每次操作都會加鎖,會造成執行緒阻塞。

    樂觀鎖:每次操作不加鎖而是假設沒有衝突而去完成某項操作,如果因為衝突失敗就重試,直到成功為止,不會造成執行緒阻塞。

  • 執行緒之間的協作:

    執行緒間的協作有:wait/notify/notifyAll等 。

  • synchronized 關鍵字:

    synchronized是Java中的關鍵字,是一種同步鎖。它修飾的物件有以下幾種:

    1. 修飾一個程式碼塊:被修飾的程式碼塊稱為同步語句塊,其作用的範圍是大括號{}括起來的程式碼,作用的物件是呼叫這個程式碼塊的物件
    2. 修飾一個方法:被修飾的方法稱為同步方法,其作用的範圍是整個方法,作用的物件是呼叫這個方法的物件
    3. 修飾一個靜態的方法:其作用的範圍是整個靜態方法,作用的物件是這個類的所有物件
    4. 修飾一個類:其作用的範圍是synchronized後面括號括起來的部分,作用主的物件是這個類的所有物件。
  • CAS:

    CAS全稱是Compare And Swap,即比較替換,是實現併發應用到的一種技術。操作包含三個運算元—記憶體位置(V)、預期原值(A)和新值(B)。 如果記憶體位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新為新值 。否則,處理器不做任何操作。CAS存在三大問題:ABA問題,迴圈時間長開銷大,以及只能保證一個共享變數的原子操作。

  • 執行緒池:

    如果我們使用執行緒的時候就去建立一個執行緒,雖然簡單,但是存在很大的問題。如果併發的執行緒數量很多,並且每個執行緒都是執行一個時間很短的任務就結束了,這樣頻繁建立執行緒就會大大降低系統的效率,因為頻繁建立執行緒和銷燬執行緒需要時間。執行緒池通過複用可以大大減少執行緒頻繁建立與銷燬帶來的效能上的損耗。


第一部分:多執行緒&併發設計原理

1. 多執行緒

1.1 Thread 和 Runnable

1.1.1 java中的執行緒

建立執行執行緒的兩種方法:

  • 擴充套件Thread 類。繼承Thread類實現多執行緒,覆蓋run()方法。

    public class ThreadCreatingByThread extends Thread{
        @Override
        public void run() {
            while(true){
                System.out.println(Thread.currentThread().getName()+" is running");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

    在Main中呼叫測試:

    public class Main {
        public static void main(String[] args) {
            //呼叫執行緒建立類建立執行緒
    
            //通過繼承Thread類,重寫run()方法的方式建立執行緒
            ThreadCreatingByThread thread = new ThreadCreatingByThread();
            thread.start();
        }
    }
    
  • 實現Runnable 介面。實現run()方法。

    public class ThreadCreatingByRunnable implements Runnable{
        @Override
        public void run() {
            while(true){
                System.out.println(Thread.currentThread().getName()+" is running");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

    在Main中呼叫測試:

    public class Main {
        public static void main(String[] args) {
            //呼叫執行緒建立類建立執行緒
    
            //通過繼承Runnable介面,實現run()方法的方式建立執行緒
            Thread thread = new Thread(new ThreadCreatingByRunnable());
            thread.start();
        }
    }
    
  • 覆寫Callable介面實現多執行緒(jdk1.5),實現call()方法,有返回值。

    public class ThreadCreatingByCallable implements Callable<String> {
        @Override
        public String call() throws Exception {
            Thread.sleep(3000);
            return "hello world call() invoked!";
        }
    }
    

    在Main中進行測試:

    public class Main {
        public static void main(String[] args) throws InterruptedException, ExecutionException {
            //呼叫執行緒建立類建立執行緒
            //覆寫Callable介面實現多執行緒(jdk1.5),實現call()方法,有返回值。
            ThreadCreatingByCallable threadCreatingByCallable = new ThreadCreatingByCallable();
            //定義FutureTask代表了一個由Callable定義的未來的工作
            // 設定Callable物件,泛型表示Callable的返回型別
            FutureTask<String> futureTask = new FutureTask<>(threadCreatingByCallable);
            // 啟動處理執行緒
            new Thread(futureTask).start();
            // 同步等待執行緒執行的結果
            String result = futureTask.get();
            System.out.println(result);
    
        }
    }
    
  • 通過執行緒池啟動多執行緒。

1.1.2 Java中的執行緒: 特徵和狀態
  1. 所有的Java 程式,不論併發與否,都有一個名為主執行緒的Thread 物件。執行該程式時, Java虛擬機器( JVM )將建立一個新Thread 並在該執行緒中執行main()方法。這是非併發應用程式中唯一的執行緒,也是併發應用程式中的第一個執行緒。

  2. Java中的執行緒共享應用程式中的所有資源,包括記憶體和開啟的檔案,快速而簡單地共享資訊。但是必須使用同步避免資料競爭

  3. Java中的所有執行緒都有一個優先順序,這個整數值介於Thread.MIN_PRIORITY(1)和Thread.MAX_PRIORITY(10)之間,預設優先順序是Thread.NORM_PRIORITY(5)。執行緒的執行順序並沒有保證,通常,較高優先順序的執行緒將在較低優先順序的錢程之前執行。

  4. 在Java 中,可以建立兩種執行緒:

    • 守護執行緒。
    • 非守護執行緒。

    區別在於它們如何影響程式的結束。

    Java程式結束執行過程的情形:

    • 程式執行Runtime類的exit()方法, 而且使用者有權執行該方法。
    • 應用程式的所有非守護執行緒均已結束執行,無論是否有正在執行的守護執行緒

    守護執行緒通常用在作為垃圾收集器或快取管理器的應用程式中,執行輔助任務。線上程start之前呼叫isDaemon()方法檢查執行緒是否為守護執行緒,也可以使用setDaemon()方法將某個執行緒確立為守護執行緒。

  5. Thread.States類中定義執行緒的狀態如下:

    • NEW:Thread物件已經建立,但是還沒有開始執行。
    • RUNNABLE:Thread物件正在Java虛擬機器中執行。
    • BLOCKED : Thread物件正在等待鎖定。
    • WAITING:Thread 物件正在等待另一個執行緒的動作。
    • TIME_WAITING:Thread物件正在等待另一個執行緒的操作,但是有時間限制。
    • TERMINATED:Thread物件已經完成了執行。

    getState()方法獲取Thread物件的狀態,可以直接更改執行緒的狀態。

    在給定時間內, 執行緒只能處於一個狀態。這些狀態是JVM使用的狀態,不能對映到作業系統的執行緒狀態。

1.1.3 Thread類和Runnable介面

Runnable介面只定義了一種方法:run()方法。這是每個執行緒的主方法。當執行start()方法啟動新執行緒時,它將呼叫run()方法。

Thread類其他常用方法:

  • 獲取和設定Thread物件資訊的方法。
    • getId():該方法返回Thread物件的識別符號。該識別符號是在錢程建立時分配的一個正整數。線上程的整個生命週期中是唯一且無法改變的。
    • getName()/setName():這兩種方法允許你獲取或設定Thread物件的名稱。這個名稱是一個String物件,也可以在Thread類的建構函式中建立。
    • getPriority()/setPriority():你可以使用這兩種方法來獲取或設定Thread物件的優先順序。
    • isDaemon()/setDaemon():這兩種方法允許你獲取或建立Thread物件的守護條件。
    • getState():該方法返回Thread物件的狀態
  • interrupt():中斷目標執行緒,給目標執行緒傳送一箇中斷訊號,執行緒被打上中斷標記。
  • interrupted():判斷目標執行緒是否被中斷,但是將清除執行緒的中斷標記。
  • isinterrupted():判斷目標執行緒是否被中斷,不會清除中斷標記。
  • sleep(long ms):該方法將執行緒的執行暫停ms時間。
  • join():暫停執行緒的執行,直到呼叫該方法的執行緒執行結束為止。可以使用該方法等待另一個Thread物件結束。也可以理解為:當我們呼叫某個執行緒的這個方法時,這個方法會掛起呼叫執行緒,直到被呼叫執行緒結束執行,呼叫執行緒才會繼續執行。
  • setUncaughtExceptionHandler():當執行緒執行出現未校驗異常時,該方法用於建立未校驗異常的控制器。
  • currentThread():Thread類的靜態方法,返回實際執行該程式碼的Thread物件。

Thread類常用方法以及join()方法示例:

public class ThreadCreatingForJoinByThread extends Thread{
    @Override
    public void run() {
        for (int i = 1 ; i<=10 ; i++ ){
            System.out.println(Thread.currentThread().getName()+i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Main {
    public static void main(String[] args) throws InterruptedException {
        //呼叫執行緒建立類建立執行緒

        //thread物件常用方法
        ThreadCreatingForJoinByThread thread = new ThreadCreatingForJoinByThread();
        System.out.println(thread.getName());
        System.out.println(thread.getId());
        System.out.println(thread.getContextClassLoader());
        System.out.println(thread.getPriority());
        System.out.println(thread.getState());
        System.out.println(thread.isDaemon());
        thread.start();
        System.out.println(thread.getState());
        //join()方法呼叫
        thread.join();
        System.out.println("main running finished");
    }
}
1.1.4 Callable

Callable 介面是一個與Runnable 介面非常相似的介面。一般用於Future模式。Callable 介面的主要特徵如下:

  • 介面。有簡單型別引數,與call()方法的返回型別相對應。

  • 聲明瞭call()方法。執行器執行任務時,該方法會被執行器執行。它必須返回宣告中指定型別的物件。

  • call()方法可以丟擲任何一種校驗異常。可以實現自己的執行器並重載afterExecute()方法來處理這些異常。

    ThreadCreatingByCallable threadCreatingByCallable = new ThreadCreatingByCallable();
            ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                    5,5,1, TimeUnit.SECONDS,new ArrayBlockingQueue<>(10)
            ){
                //如果call方法執行過程中存在異常,則可以在此處處理
                @Override
                protected void afterExecute(Runnable r, Throwable t) {
                    System.out.println("mission has been execute successfully:"+t);
                }
            };
    
            Future<String> future = threadPoolExecutor.submit(threadCreatingByCallable);
            String result = future.get();
            System.out.println(result);
            //關閉執行緒池
            threadPoolExecutor.shutdown();
    

1.2 synchronized 關鍵字

1.2.1 鎖的物件

synchronized關鍵字“給某個物件加鎖”,示例程式碼:

public class MyClass {
    //例項方法 (兩個方法相互等價)
    public synchronized void method1(){
        // ...
    }

    public void method2(){
        synchronized (this){
            // ...
        }
    }
    //靜態方法 (兩個方法相互等價)
    public static synchronized void method3(){
        // ...
    }
    
    public static void method4(){
        synchronized (MyClass.class){
            // ...
        }
    }
}

例項方法的鎖加在物件myClass上;靜態方法的鎖加在MyClass.class上。

1.2.2 鎖的本質

如果一份資源需要多個執行緒同時訪問,需要給該資源加鎖。加鎖之後,可以保證同一時間只能有一個執行緒訪問該資源。資源可以是一個變數、一個物件或一個檔案等。

鎖是一個“物件”,作用如下:

  1. 這個物件內部得有一個標誌位(state變數),記錄自己有沒有被某個執行緒佔用。最簡單的情況是這個state有0、1兩個取值,0表示沒有執行緒佔用這個鎖,1表示有某個執行緒佔用了這個鎖。
  2. 如果這個物件被某個執行緒佔用,記錄這個執行緒的thread ID。
  3. 這個物件維護一個thread id list,記錄其他所有阻塞的、等待獲取拿這個鎖的執行緒。在當前執行緒釋放鎖之後從這個thread id list裡面取一個執行緒喚醒。

要訪問的共享資源本身也是一個物件,例如前面的物件myClass,這兩個物件可以合成一個物件。程式碼就變成synchronized(this) {…},要訪問的共享資源是物件a,鎖加在物件a上。當然,也可以另外新建一個物件,程式碼變成synchronized(obj1) {…}。這個時候,訪問的共享資源是物件a,而鎖加在新建的物件obj1上。

資源和鎖合二為一,使得在Java裡面,synchronized關鍵字可以加在任何物件的成員上面。這意味著,這個物件既是共享資源,同時也具備“鎖”的功能!

1.2.3 實現原理

修飾物件:在物件頭裡,有一塊資料叫Mark Word。在64位機器上,Mark Word是8位元組(64位)的,這64位中有2個重要欄位:鎖標誌位和佔用該鎖的thread ID。因為不同版本的JVM實現,物件頭的資料結構會有各種差異。

修飾同步程式碼塊:中是在物件頭中有一個monitor物件,對應著monitorenter和monitorexit指令,當執行enter指令時嘗試獲取monitor的持有權,獲取成功將計數器從0設為1,如果獲取失敗就阻塞等待別的執行緒釋放。

修飾方法:的話是ACC_SYNCHRONIZED標識,標明是一個同步方法。JVM通過這個標識才執行相應的同步呼叫。

1.2.4 優化

在jdk 1.6後,對synchronized鎖進行了優化。

偏向鎖:JVM認為只有某個執行緒才會執行同步程式碼(沒有競爭環境),所以在MarkWord會直接記錄執行緒ID,只要執行緒來執行程式碼就會對比執行緒ID是否相等,相等則直接獲取到鎖,不相等就CAS來嘗試修改當前的執行緒ID,如果CAS修改成功就繼續,如果失敗說明有競爭環境,升級為輕量級鎖。簡單來說就是:如果存在競爭環境,則升級為輕量級鎖。

輕量級鎖:輕量級鎖是相對於重量級鎖而言,輕量級鎖不需要申請互斥量,只需要將markwork中的部分位元組CAS更新指向執行緒的id,如果更新成功則表示已經成功的獲取了鎖,否則說明已經有執行緒獲取了輕量級鎖,發生了鎖競爭,輕量級鎖開始自旋。

在jdk1.6之前,設定了自旋鎖自旋次數為10次。1.6及之後,優化為自適應自旋鎖。可以根據加鎖的程式碼來決定要自選幾次. 如果自旋超過一定次數,或者此時有第三個執行緒來競爭該鎖時,鎖膨脹為重量級鎖。

重量級鎖:Jvm每次從佇列中取出一個執行緒來用於鎖競爭候選者即競爭執行緒.但是併發情況下,尾部list會被大量的併發執行緒的訪問為了降低競爭,提高獲取執行緒的速度,JVM將競爭的list拆為了兩份,獲取競爭執行緒時只從頭部獲取,而新進入的競爭執行緒則被放到尾部.提高了競爭時的效率.當Owner執行緒在unlock時會將尾部執行緒的部分執行緒遷移到頭部執行緒中,並且制定頭部執行緒的某一個執行緒作為競爭執行緒,但是並沒有直接將鎖交給競爭執行緒,而是讓競爭執行緒自己來獲取鎖,這樣做雖然會犧牲公平性,但是會極大的提升系統的吞吐量。

synchronized是非公平鎖.當執行緒在進入尾部佇列之前,會嘗試著先自旋獲取鎖,如果獲取失敗才選擇進入尾部佇列。之後的操作參考重量級鎖。

公平鎖底層為將執行緒放入一個先進先出的佇列中,按照順序一次獲取資源。


1.3 wait與notify

wait方法會讓執行緒進入等待佇列,若要執行wait方法,執行緒必須持有鎖,但如果執行緒進入等待佇列,便會釋放例項的鎖。

notify()方法會將等待佇列中的一個執行緒取出。那麼在等待佇列中的那個執行緒便會被選中喚醒,然後退出等待佇列。這裡需要注意的是,在執行notify喚醒的執行緒並不會在執行notify的一瞬間重新執行。因為在執行notify的那一瞬間,執行notify的執行緒還持著鎖,所以其他執行緒還無法獲取這個例項的鎖。

notifyAll()方法會將等待佇列中的所有執行緒都取出來,所有等待的執行緒都會被喚醒。有意思的是,在執行notifyAll()方法時,誰持著鎖呢?當然是執行notifyAll()的執行緒正持著鎖,因此,喚醒的執行緒雖然都退出了等待佇列,但都在等待獲取鎖,處於阻塞狀態,只有在執行notifyAll之後的執行緒釋放鎖以後,其中的一個幸運兒才能夠實際執行。

在呼叫之前,先判定該執行緒是否持有該鎖。

1.3.1 生產者-消費者模型

一個記憶體佇列,多個生產者執行緒往記憶體佇列中放資料;多個消費者執行緒從記憶體佇列中取資料。要實現這樣一個程式設計模型,需要做下面幾件事情:

  • 記憶體佇列本身要加鎖,才能實現執行緒安全。
  • 阻塞。當記憶體佇列滿了,生產者放不進去時,會被阻塞;當記憶體佇列是空的時候,消費者無事可做,會被阻塞。
  • 雙向通知。消費者被阻塞之後,生產者放入新資料,要notify()消費者;反之,生產者被阻塞之後,消費者消費了資料,要notify()生產者。

第1件事情必須要做,第2件和第3件事情不一定要做。例如,可以採取一個簡單的辦法,生產者放不進去之後,睡眠幾百毫秒再重試,消費者取不到資料之後,睡眠幾百毫秒再重試。但這個辦法效率低下,也不實時。所以,我們只討論如何阻塞、如何通知的問題。

如何阻塞?

辦法1:執行緒自己阻塞自己,也就是生產者、消費者執行緒各自呼叫wait()和notify()。

辦法2:用一個阻塞佇列,當取不到或者放不進去資料的時候,入隊/出隊函式本身就是阻塞的。

如何雙向通知?

辦法1:wait()與notify()機制。

辦法2:Condition機制。

單個生產者單個消費者執行緒的情形:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        MessageQueue messageQueue = new MessageQueue();
        new ProducerThread(messageQueue).start();
        new ConsumerThread(messageQueue).start();
        Thread.sleep(2000);
        System.exit(0);
    }
}
public class ProducerThread extends Thread{
    private final MessageQueue messageQueue;
    private final Random random = new Random();
    private int index = 0;
    public ProducerThread(MessageQueue messageQueue) {
        this.messageQueue = messageQueue;
    }


    @Override
    public void run() {
        while (true){
            String message = String.valueOf(index++);
            messageQueue.put(message);
            System.out.println("生產資料: "+message);
            try {
                Thread.sleep(random.nextInt(100));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}
public class ConsumerThread extends Thread{
    private final MessageQueue messageQueue;
    private final Random random = new Random();
    public ConsumerThread(MessageQueue messageQueue) {
        this.messageQueue = messageQueue;
    }

    @Override
    public void run() {
        while (true){
            String result = null;
            try {
                result = messageQueue.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("consumer data is "+result);
            try {
                Thread.sleep(random.nextInt(100));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}
@SuppressWarnings("all")
public class MessageQueue {
    private String[] data = new String[10];
    //下一條儲存記錄的下標
    private int putIndex = 0;
    //下一條要獲取的記錄的下標
    private int getIndex = 0;
    //data中元素的個數
    private int size = 0;

    public synchronized void put(String element){
        if (size == data.length){
            try{
                //阻塞,等待
                wait();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
        data[putIndex++] = element;
        //喚醒消費者
        notify();
        size++;
        if (putIndex == data.length){
            putIndex = 0;
        }
    }

    public synchronized String get() throws InterruptedException {
        if (size == 0){
            wait();
        }
        String result = data[getIndex++];
        if(getIndex == data.length) getIndex=0;
        size--;
        //喚醒生產者
        notify();
        return result;
    }
}

多個生產者多個消費者的情形:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        //多個生產者和多個消費者
        MessageQueue2 messageQueue = new MessageQueue2();
        for (int i = 0 ; i < 3 ; i++ ){
            new ConsumerThread(messageQueue).start();
        }
        for (int i = 0 ; i<5 ; i++ ){
            new ProducerThread(messageQueue).start();
        }
    }
}
@SuppressWarnings("all")
public class MessageQueue2 extends MessageQueue{
    private String[] data = new String[10];
    //下一條儲存記錄的下標
    private int putIndex = 0;
    //下一條要獲取的記錄的下標
    private int getIndex = 0;
    //data中元素的個數
    private int size = 0;

    private void commonPut(String element){
        data[putIndex++] = element;
        //喚醒消費者
        notify();
        size++;
        if (putIndex == data.length){
            putIndex = 0;
        }
    }

    @Override
    public synchronized void put(String element) {
        if (size == data.length){
            try{
                //阻塞,等待
                wait();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            //利用迭代,重新獲取共享鎖
            put(element);
        }else {
            commonPut(element);
        }

    }

    private String commonGet(){
        String result = data[getIndex++];
        if(getIndex == data.length) getIndex=0;
        size--;
        //喚醒生產者
        notify();
        return result;
    }

    @Override
    public synchronized String get() throws InterruptedException {
        if (size == 0){
            wait();
            //利用迭代,重新獲取共享鎖
            return get();
        }else{
            return commonGet();
        }

    }
}
1.3.2 為什麼必須要和synchronized一起使用

在Java裡面,wait()和notify()是Object的成員函式,是基礎中的基礎。為什麼Java要把wait()和notify()放在如此基礎的類裡面,而不是作為像Thread一類的成員函式,或者其他類的成員函式呢?

兩個執行緒之間要通訊,對於同一個物件來說,一個執行緒呼叫該物件的wait(),另一個執行緒呼叫該物件的notify(),這兩個操作需要協調,所以該物件本身就需要同步!所以,在呼叫wait()、notify()之前,要先通過synchronized關鍵字同步給物件,也就是給該物件加鎖。並且呼叫這些方法之前需要確定是否獲得了該鎖,所以需要和synchronized關鍵字一起使用。

synchronized關鍵字可以加在任何物件的例項方法上面,任何物件都可能成為鎖。因此,wait()和notify()只能放在Object裡面了。

1.3.3 為什麼wait()的時候必須要釋放鎖

當執行緒A進入synchronized(obj1)中之後,也就是對obj1上了鎖。此時,呼叫wait()進入阻塞狀態,一直不能退出synchronized程式碼塊;那麼,執行緒B永遠無法進入synchronized(obj1)同步塊裡,永遠沒有機會呼叫notify(),發生死鎖。

在wait()的內部,會先釋放鎖obj1,然後進入阻塞狀態,之後,它被另外一個執行緒用notify()喚醒,重新獲取鎖!其次,wait()呼叫完成後,執行後面的業務邏輯程式碼,然後退出synchronized同步塊,再次釋放鎖。

wait(){
	//釋放鎖
    //阻塞,等待被其他執行緒notify
    //重新獲取鎖
}

如此則可以避免死鎖。

1.3.4 wait()與notify()的問題

生產者在通知消費者的同時,也通知了其他的生產者;消費者在通知生產者的同時,也通知了其他消費者。原因在於wait()和notify()所作用的物件和synchronized所作用的物件是同一個,只能有一個物件,無法區分佇列空和列隊滿兩個條件。這正是Condition要解決的問題


1.4 InterruptedException和interrupt()方法

1.4.1 Interrupted異常

什麼情況下會丟擲Interrupted異常

只有那些聲明瞭會丟擲InterruptedException的函式才會丟擲異常,也就是下面這些常用的函式:

public static native void sleep(long millis) throws InterruptedException {...} 
public final void wait() throws InterruptedException {...} public final void join() throws InterruptedException {...}
1.4.2 輕量級鎖阻塞與重量級阻塞

能夠被中斷的阻塞稱為輕量級阻塞,對應的執行緒狀態是WAITING或者TIMED_WAITING;而像synchronized 這種不能被中斷的阻塞稱為重量級阻塞,對應的狀態是 BLOCKED。如圖所示:呼叫不同的方法後,一個執行緒的狀態遷移過程。

初始執行緒處於NEW狀態,呼叫start()開始執行後,進入RUNNING或者READY狀態。如果沒有呼叫任何的阻塞函式,執行緒只會在RUNNING和READY之間切換,也就是系統的時間片排程。這兩種狀態的切換是作業系統完成的,除非手動呼叫yield()函式,放棄對CPU的佔用。

一旦呼叫了圖中的任何阻塞函式,執行緒就會進入WAITING或TIMED_WAITING狀態,兩者的區別只是前者為無限期阻塞,後者則傳入了一個時間引數,阻塞一個有限的時間。如果使用了synchronized關鍵字或者synchronized塊,則會進入BLOCKED狀態。

不太常見的阻塞/喚醒函式,LockSupport.park()/unpark()。這對函式非常關鍵,Concurrent包中Lock的實現即依賴這一對操作原語。

因此thread.interrupted()的精確含義是“喚醒輕量級阻塞”,而不是字面意思“中斷一個執行緒”。

thread.isInterrupted()Thread.interrupted()的區別

因為 thread.interrupted()相當於給執行緒傳送了一個喚醒的訊號,所以如果執行緒此時恰好處於WAITING或者TIMED_WAITING狀態,就會丟擲一個InterruptedException,並且執行緒被喚醒。而如果執行緒此時並沒有被阻塞,則執行緒什麼都不會做。但在後續,執行緒可以判斷自己是否收到過其他執行緒發來的中斷訊號,然後做一些對應的處理。

這兩個方法都是執行緒用來判斷自己是否收到過中斷訊號的,前者是例項方法,後者是靜態方法。二者的區別在於,前者只是讀取中斷狀態,不修改狀態;後者不僅讀取中斷狀態,還會重置中斷標誌位。


1.5 執行緒的優雅關閉

1.5.1 stop與destory函式

執行緒是“一段執行中的程式碼”,一個執行中的方法。執行到一半的執行緒能否強制殺死?

不能。在Java中,有stop()、destory()等方法,但這些方法官方明確不建議使用,並在jdk 11中廢除。原因很簡單,如果強制殺死執行緒,則執行緒中所使用的資源,例如檔案描述符、網路連線等無法正常關閉。

因此,一個執行緒一旦執行起來,不要強行關閉,合理的做法是讓其執行完(也就是方法執行完畢),乾淨地釋放掉所有資源,然後退出。如果是一個不斷迴圈執行的執行緒,就需要用到執行緒間的通訊機制,讓主執行緒通知其退出。

1.5.2 守護執行緒
public class Main {
    public static void main(String[] args) {
        MyDaemonThread myDaemonThread = new MyDaemonThread();
        myDaemonThread.setDaemon(true);
        myDaemonThread.start();
        new MyThread().start();

    }
}
public class MyDaemonThread extends Thread{
    @Override
    public void run() {
        while (true){
            try{
                System.out.println("waking ...");
                Thread.sleep(500);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}
public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("normal thread running");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

對於上面的程式,在thread.start()前面加一行程式碼thread.setDaemon(true)。當main(...)函式退出後,執行緒thread就會退出,整個程序也會退出。

當在一個JVM程序裡面開多個執行緒時,這些執行緒被分成兩類:守護執行緒和非守護執行緒。預設都是非守護執行緒。

在Java中有一個規定:當所有的非守護執行緒退出後,整個JVM程序就會退出。意思就是守護執行緒“不算作數”,守護執行緒不影響整個 JVM 程序的退出。

例如,垃圾回收執行緒就是守護執行緒,它們在後臺默默工作,當開發者的所有前臺執行緒(非守護執行緒)都退出之後,整個JVM程序就退出了。

1.5.3 設定關閉的標誌位

開發中一般通過設定標誌位的方式,停止迴圈執行的執行緒。

public class Main {
    public static void main(String[] args) throws InterruptedException {
        StopFlagThread stopFlagThread = new StopFlagThread();
        stopFlagThread.start();
        Thread.sleep(500);
        stopFlagThread.changeRunning();
        stopFlagThread.join();
    }
}
public class StopFlagThread extends Thread{
    private boolean running = true;

    @Override
    public void run() {
        while(running){
            System.out.println("thread is running");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void changeRunning(){
        this.running = false;
    }
}

但上面的程式碼有一個問題:如果MyThread t在while迴圈中阻塞在某個地方,例如裡面呼叫了object.wait()函式,那它可能永遠沒有機會再執行 while( !stopped)程式碼,也就一直無法退出迴圈。

此時,就要用到InterruptedException()與interrupt()函式。