【Java基礎】多線程從入門到掌握

文章目錄

一.多線程基礎

1.多任務概念

  • 現代操作系統(Windows,macOS,Linux)都可以執行多任務多任務就是同時運行多個任務,例如:
    同時打開ie瀏覽器/QQ/QQ音樂

  • CPU執行代碼都是一條一條順序執行的,即使是單核cpu,也可以同時運行多個任務。 因爲操作系統執行多任務實際上就是讓CPU對多個任務輪流交替執行

    例如,假設我們有語文、數學、英語3門作業要做,每個作業需要30分鐘。我們把這3門作業看成是3個任務,可以做1分鐘語文作業,再做1分鐘數學作業,再做1分鐘英語作業:

在這裏插入圖片描述
這樣輪流做下去,在某些人眼裏看來,做作業的速度就非常快,看上去就像同時在做3門作業一樣
在這裏插入圖片描述

類似的,操作系統輪流讓多個任務交替執行,例如,讓瀏覽器執行0.001秒,讓QQ執行0.001秒,再讓QQ音樂執行0.001秒,在人看來,CPU就是在同時執行多個任務。 即使是多核CPU,因爲通常任務的數量遠遠多於CPU的核數,所以任務也是交替執行的。

2.進程和線程的概念

  • 在計算機中,我們把一個任務稱爲一個進程,瀏覽器就是一個進程,視頻播放器是另一個進程,類似的,音樂播放器和Word都是進程。

  • 某些進程內部還需要同時執行多個子任務 例如,我們在使用Word時,Word可以讓我們一邊打字,一邊進行拼寫檢查,同時還可以在後臺進行打印,我們把子任務稱爲線程。

  • 進程和線程的關係就是:一個進程可以包含一個或多個線程,但至少會有一個線程。

    在這裏插入圖片描述

  • 操作系統調度的最小任務單位是線程 常用的Windows、Linux等操作系統都採用搶佔式多任務如何調度線程完全由操作系統決定,程序自己不能決定什麼時候執行,以及執行多長時間。


因爲同一個應用程序,既可以有多個進程,也可以有多個線程
因此,實現多任務的方法,有以下幾種:

  1. 多進程模式(每個進程只有一個線程):
    在這裏插入圖片描述
  2. 多線程模式(一個進程有多個線程)
    在這裏插入圖片描述
  3. 多進程+多線程模式(複雜度最高)
    在這裏插入圖片描述

3.進程 vs 線程

  • 進程和線程是包含關係,但是多任務既可以由多進程實現,也可以由單進程內的多線程實現,還可以混合多進程+多線程。具體採用哪種方式,要考慮到進程和線程的特點。

  • 和多線程相比,多進程的缺點在於:

  1. 創建進程比創建線程開銷大,尤其是在Windows系統上;
  2. 進程間通信比線程間通信要慢,因爲線程間通信就是讀寫同一個變量,速度很快。
  • 而多進程的優點在於:
  1. 多進程穩定性比多線程高,因爲在多進程的情況下,一個進程崩潰不會影響其他進程,而在多線程的情況下,任何一個線程崩潰會直接導致整個進程崩潰。

4.Java多線程

Java語言內置了多線程支持:一個Java程序實際上是一個JVM進程,JVM進程用一個主線程來執行main()方法,在main()方法內部,我們又可以啓動多個線程 此外,JVM還有負責垃圾回收的其他工作線程(守護線程)等。

  • 因此,對於大多數Java程序來說,我們說多任務,實際上是說如何使用多線程實現多任務。

  • 和單線程相比,多線程編程的特點在於:多線程經常需要讀寫共享數據,並且需要同步

    • 例如,播放電影時,就必須由一個線程播放視頻,另一個線程播放音頻,兩個線程需要協調運行,否則畫面和聲音就不同步。因此,多線程編程的複雜度高,調試更困難。
  • Java多線程編程的特點又在於:

    • 多線程模型是Java程序最基本的併發模型;
    • 後續讀寫網絡、數據庫、Web開發等都依賴Java多線程模型。

二.創建線程

  • Java語言內置了多線程支持。當Java程序啓動的時候,實際上是啓動了一個JVM進程,然後,JVM啓動主線程來執行main()方法。在main()方法中,我們又可以啓動其他線程。

要創建一個新線程非常容易,我們需要實例化一個Thread實例,然後調用它的start()方法:

  • 方法一:繼承Thread類重寫run方法:
public class Main {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start(); // 啓動新線程
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("start new thread!");
    }
}

  • 方法二:實現Runnable接口重寫run方法,創建Thread實例時,傳入一個Runnable實例:
public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.start(); // 啓動新線程
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("start new thread!");
    }
}

或者用Java8引入的lambda語法進一步簡寫爲:

public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("start new thread!");
        });
        t.start(); // 啓動新線程
    }
}

1.線程執行順序

public class Main {
    public static void main(String[] args) {
        System.out.println("main start...");
        Thread t = new Thread() {
            public void run() {
                System.out.println("thread run...");
                System.out.println("thread end.");
            }
        };
        t.start();
        System.out.println("main end...");
    }
}

main線程執行的代碼有4行,首先打印main start,然後創建Thread對象,緊接着調用start()啓動新線程。當start()方法被調用時,JVM就創建了一個新線程,我們通過實例變量t來表示這個新線程對象,並開始執行。

接着,main線程繼續執行打印main end語句,而t線程main線程執行的同時會併發執行,打印thread runthread end語句。

當run()方法結束時,新線程就結束了。而main()方法結束時,主線程也結束了。

我們再來看線程的執行順序:

  1. main線程肯定是先打印main start,再打印main end;
    2.t線程肯定是先打印thread run,再打印thread end
  2. 但是,除了可以肯定,main start會先打印外main end打印在thread run之前、thread end之後或者之間,都無法確定。因爲從t線程開始運行以後,兩個線程就開始同時運行了,並且由操作系統調度程序本身無法確定線程的調度順序。

2.線程的優先級

可以對線程設定優先級,設定優先級的方法是:

	Thread.setPriority(int n) // 1~10, 默認值5

優先級高的線程被操作系統調度的優先級較高,操作系統對高優先級線程可能調度更頻繁,但不能保證優先級高的線程一定會先執行。線程調度由操作系統決定,程序本身無法決定調度順序;

三.線程的狀態

在Java程序中,一個線程對象只能調用一次start()方法啓動新線程,並在新線程中執行run()方法。一旦run()方法執行完畢,線程就結束了。因此,Java線程的狀態有以下幾種:

  • New:新創建的線程,尚未執行;
  • Runnable:運行中的線程,正在執行run()方法的Java代碼;
  • Blocked:運行中的線程,因爲某些操作被阻塞而掛起;
  • Waiting:運行中的線程,因爲某些操作在等待中;
  • Timed Waiting:運行中的線程,因爲執行sleep()方法正在計時等待;
  • Terminated:線程已終止,因爲run()方法執行完畢。
    -在這裏插入圖片描述
    當線程啓動後,它可以在Runnable、Blocked、Waiting和Timed Waiting這幾個狀態之間切換,直到最後變成Terminated狀態,線程終止。

線程終止的原因有:

  • 線程正常終止:run()方法執行到return語句返回;
  • 線程意外終止:run()方法因爲未捕獲的異常導致線程終止;
  • 調用線程實例的stop()方法強制終止(強烈不推薦使用)

四.線程禮讓

  1. 通過對另一個線程對象調用join()方法可以等待其執行結束;
  2. 可以指定等待時間,超過等待時間線程仍然沒有結束就不再等待;
  3. 對已經運行結束的線程調用join()方法會立刻返回。
  4. join()方法誰的線程體中哪個線程就會等待當前join()方法對應實例線程執行結束在執行
public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            System.out.println("hello");
        });
        System.out.println("start");
        t.start();
        t.join();
        System.out.println("end");
    }
}

main線程線程t調用join()方法時,主線程將等待線程t運行結束在繼續執行,即join就是指等待該線程結束,然後才繼續往下執行自身線程。 所以,上述代碼打印順序可以肯定是main線程先打印start,t線程再打印hello,main線程最後再打印end。

如果線程t已經結束,對實例t調用join()會立刻返回。

  • 此外,join(long)的重載方法也可以指定一個等待時間,超過等待時間後就不再繼續等待

五.中斷線程

  • 如果線程需要執行一個長時間任務,就可能需要能中斷線程。中斷線程就是其他線程給該線程發一個信號,該線程收到信號後結束執行run()方法,使得自身線程能立刻結束運行。

    我們舉個栗子:假設從網絡下載一個100M的文件,如果網速很慢,用戶等得不耐煩,就可能在下載過程中點“取消”,這時,程序就需要中斷下載線程的執行。

中斷一個線程非常簡單,只需要在其他線程中對目標線程調用interrupt()(默認值爲false,需要取反判斷) 方法,目標線程循環調用interrupted()方法判斷自身狀態,如果是,就立刻結束運行。

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new MyThread();
        t.start();
        Thread.sleep(1); // 暫停1毫秒
        t.interrupt(); // 中斷t線程
        t.join(); // 等待t線程結束
        System.out.println("end");
    }
}

class MyThread extends Thread {
    public void run() {
        int n = 0;
        while (! isInterrupted()) {
            n ++;
            System.out.println(n + " hello!");
        }
    }
}

上述代碼 main線程 通過調用·線程t.interrupt()·方法中斷t線程,但是要注意,interrupt()方法僅僅向t線程發出了“中斷請求”,至於t線程是否能立刻響應,要看具體代碼。而線程t的while循環會檢測isInterrupted(),所以上述代碼能正確響應interrupt()請求,使得自身立刻結束運行run()方法。


如果線程處於等待狀態,調用當前線程的interrupt()會拋出InterruptedException,例如,t.join()會讓main線程進入等待狀態,此時,如果對main線程調用interrupt(),join()方法會立刻拋出InterruptedException,因此,目標線程只要捕獲到join()方法拋出的InterruptedException`,就說明有其他線程對其調用了interrupt()方法,通常情況下該線程應該立刻結束運行。

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new MyThread();
        t.start();
        Thread.sleep(1000);
        t.interrupt(); // 中斷t線程
        t.join(); // 等待t線程結束
        System.out.println("end");
    }
}

class MyThread extends Thread {
    public void run() {
        Thread hello = new HelloThread();
        hello.start(); // 啓動hello線程
        try {
            hello.join(); // 等待hello線程結束
        } catch (InterruptedException e) {
            System.out.println("interrupted!");
        }
        hello.interrupt();
    }
}

class HelloThread extends Thread {
    public void run() {
        int n = 0;
        while (!isInterrupted()) {
            n++;
            System.out.println(n + " hello!");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                break;
            }
        }
    }
}

main線程通過調用t.interrupt()從而通知t線程中斷,而此時線程t正在等待hello線程執行結束此方法會立刻結束等待並拋出InterruptedException。由於我們在線程t中捕獲了InterruptedException,因此,就可以準備結束該線程。在t線程結束前,對hello線程也進行了interrupt()調用通知其中斷。如果去掉這一行代碼,可以發現hello線程仍然會繼續運行,且JVM不會退出。

1.設置標誌位中斷線程

常用的中斷線程的方法是設置標誌位。我們通常會用一個boolean 類型的標記位來標識線程是否應該繼續運行,在外部線程中,通過把它置爲false,就可以讓線程結束:

public class Main {
    public static void main(String[] args)  throws InterruptedException {
        HelloThread t = new HelloThread();
        t.start();
        Thread.sleep(1);
        t.running = false; // 標誌位置爲false
    }
}

class HelloThread extends Thread {
    public volatile boolean running = true;
    public void run() {
        int n = 0;
        while (running) {
            n ++;
            System.out.println(n + " hello!");
        }
        System.out.println("end!");
    }
}

注意到HelloThread的標誌位boolean running是一個線程間共享的變量

  • 線程間共享變量需要使用volatile關鍵字標記,確保每個線程都能讀取到更新後的變量值。

2.volatile

爲什麼要對線程間共享的變量用關鍵字volatile聲明?

  • 這涉及到Java的內存模型。在Java虛擬機中,變量的值保存在主內存中,當線程訪問變量時,它會先獲取一個副本,並保存在自己的工作內存中如果線程修改了變量的值,虛擬機會在某個時刻把修改後的值回寫到主內存,但這個時間是不確定的!
    在這裏插入圖片描述
    這會導致一個線程更新了某個變量,另一個線程讀取的值可能還是更新前的。
    例如,主內存變量a = true,線程1執行a = false時,它在此刻僅僅是把變量a的副本變成了false主內存的變量a還是true在JVM把修改後的a回寫到主內存之前,其他線程讀取到的a的值仍然是true 這 就造成了多線程之間共享的變量不一致。

volatile關鍵字的目的是告訴虛擬機:

  • 每次訪問變量時,總是獲取主內存的最新值;
  • 每次修改變量後,立刻回寫到主內存。

volatile關鍵字解決的是共享變量在線程間的可見性問題:當一個線程修改了某個共享變量的值,其他線程能夠立刻看到修改後的值。

如果我們去掉volatile關鍵字,運行上述程序,發現效果和帶volatile差不多,這是因爲在x86的架構下,JVM回寫主內存的速度非常快,但是,換成ARM的架構,就會有顯著的延遲。

五.守護線程

  • Java程序入口就是由JVM啓動main線程,main線程又可以啓動其他線程。當所有線程都運行結束時,JVM退出,進程結束

  • 如果有一個線程沒有退出,JVM進程就不會退出。所以,必須保證所有線程都能及時結束。


但是有一種線程的目的就是無限循環,例如,一個定時觸發任務的線程

class TimerThread extends Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println(LocalTime.now());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                break;
            }
        }
    }
}

如果這個線程不結束,JVM進程就無法結束。問題是,由誰負責結束這個線程?
然而這類線程經常沒有負責人來負責結束它們。但是,當其他線程結束時,JVM進程又必須要結束,怎麼辦?
答案是使用守護線程(Daemon Thread)


守護線程是指爲其他線程服務的線程。在JVM中,所有非守護線程都執行完畢後,無論有沒有守護線程,虛擬機都會自動退出。 因此,JVM退出時,不必關心守護線程是否已結束。

如何創建守護線程

  • 在調用start()方法前,調用setDaemon(true)把該線程標記爲守護線程:
Thread t = new MyThread();
t.setDaemon(true);
t.start();

在守護線程中,編寫代碼要注意:守護線程不能持有任何需要關閉的資源,例如打開文件等,因爲虛擬機退出時,守護線程沒有任何機會來關閉文件,這會導致數據丟失。

六.線程同步

當多個線程同時運行時,線程的調度由操作系統決定,程序本身無法決定。因此,任何一個線程都有可能在任何指令處被操作系統暫停,然後在某個時間段後繼續執行。

這個時候,有個單線程模型下不存在的問題就來了:如果多個線程時讀寫共享變量,會出現數據不一致的問題。


1.線程同步問題產生

public class Main {
    public static void main(String[] args) throws Exception {
        AddThread add = new AddThread();
        DecThread dec = new DecThread();
        add.start();
        dec.start();
        add.join();
        dec.join();
        System.out.println(Counter.count);
    }
}

class Counter {
    public static int count = 0;
}

class AddThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) { Counter.count += 1; }
    }
}

class DecThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) { Counter.count -= 1; }
    }
}

上面的代碼兩個線程同時對一個int變量進行操作,一個加10000次,一個減10000次,最後結果應該是0,但是,每次運行,結果實際上都是不一樣的。

連續執行三次結果
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
這是因爲對變量進行讀取和寫入時,結果要正確,必須保證是原子操作。原子操作是指不能被中斷的一個或一系列操作。


實際上執行n = n + 1並不是一個原子操作,它的執行過程如下:
1. 從主存中讀取變量x副本到工作內存
2. 給x加1
3. 將x加1後的值寫回主存

我們假設n的值是100,如果兩個線程同時執行n = n + 1,得到的結果很可能不是102,而是101
原因在於:多個線程執行時,CPU對線程的調度是隨機的,我們不知道當前程序被執行到哪步就切換到了下一個線程

  • 如果線程1在從主內存將n=100的值同步到工作內存時,此時cpu切換到線程2,線程2也將n=100的值同步到工作內存
  • 線程1 n+=1 = 101,然後同步到主內存此時主內存爲101
  • 線程2 n-=1 = 99,然後同步到主內存此時主內存爲99
  • 顯然由於執行順序的不同n最終的結果可能爲101也可能爲99

這說明多線程模型下,要保證邏輯正確,即某一個線程對共享變量進行讀寫時,其他線程必須等待


2.Synchronized

  • 通過加鎖和解鎖的操作,就能保證在一個線程執行期間,不會有其他線程會進入此代碼塊。

  • 即使在執行期線程被操作系統中斷執行,其他線程也會因爲無法獲得鎖導致無法進入此代碼塊。只有執行線程將鎖釋放後,其他線程纔有機會獲得鎖並執行。這種加鎖和解鎖之間的代碼塊我們稱之爲臨界區(Critical Section)任何時候臨界區最多隻有一個線程能執行。

  • 保證一段代碼的原子性就是通過加鎖和解鎖實現的。Java程序使用synchronized關鍵字對一個對象進行加鎖

synchronized(lock) {
    n = n + 1;
}

synchronized保證了代碼塊在任意時刻最多隻有一個線程能執行。

public class TestSynchronized {
    public static void main(String[] args) throws Exception {
        AddThread add = new AddThread();
        DecThread dec = new DecThread();
        add.start();
        dec.start();
        add.join();
        dec.join();
        System.out.println(Counter.count);
    }
}
//計數器
class Counter {
    public static final Object lock = new Object();
    public static int count = 0;
}
//新增線程
class AddThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) {    synchronized(Counter.lock) { Counter.count += 1;} }
    }
}
//減少線程
class DecThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) {  synchronized(Counter.lock){ Counter.count -= 1;} }
    }
}

執行結果
在這裏插入圖片描述
代碼

 synchronized(Counter.lock) {//獲取鎖
  }//釋放鎖
  • 它表示用Counter.lock實例作爲鎖,兩個線程在執行各自的synchronized(Counter.lock) { ... }代碼塊時,必須先獲得鎖,才能進入代碼塊進行。 執行結束後,在synchronized語句塊結束會自動釋放鎖。 這將會導致對Counter.count變量進行讀寫就不能同時進行。論運行多少次,最終結果都是0。

synchronized解決了多線程同步訪問共享變量的有序性問題。 但它的缺點是帶來了性能下降。因爲synchronized代碼塊無法併發執行。此外加鎖和解鎖需要消耗一定的時間,所以,synchronized會降低程序的執行效率。


如何使用Synchronized

  1. 找出修改共享變量的線程代碼塊;
  2. 選擇一個共享實例作爲鎖;
  3. 使用synchronized(lockObject) { }.
  • 在使用synchronized的時候,不必擔心拋出異常因爲無論是否有異常,都會在synchronized結束處正確釋放鎖:
public void add(int m) {
    synchronized (obj) {
        if (m < 0) {
            throw new RuntimeException();
        }
        this.value += m;
    } // 無論有無異常,都會在此釋放鎖
}

3.錯誤使用Synchronized例子1

public class Main {
    public static void main(String[] args) throws Exception {
        AddThread add = new AddThread();
        DecThread dec = new DecThread();
        add.start();
        dec.start();
        add.join();
        dec.join();
        System.out.println(Counter.count);
    }
}

class Counter {
    public static final Object lock1 = new Object();
    public static final Object lock2 = new Object();
    public static int count = 0;
}

class AddThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) {
            synchronized(Counter.lock1) {
                Counter.count += 1;
            }
        }
    }
}

class DecThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) {
            synchronized(Counter.lock2) {
                Counter.count -= 1;
            }
        }
    }
}

執行結果
在這裏插入圖片描述
結果並不是0,這是因爲兩個線程各自的synchronized鎖住的不是同一個對象! 這使得兩個線程各自都可以同時獲得鎖:因爲JVM只保證同一個鎖在任意時刻只能被一個線程獲取,但兩個不同的鎖在同一時刻可以被兩個線程分別獲取 使用synchronized的時候,獲取到的是哪個鎖非常重要。鎖對象如果不對,代碼邏輯就不對。

4.錯誤使用Synchronized例子2

public class Main {
    public static void main(String[] args) throws Exception {
        Thread [] ts = new Thread[] { new AddStudentThread(), new DecStudentThread(), new AddTeacherThread(), new DecTeacherThread() };
        for (Thread t : ts) {
            t.start();
        }
        for (Thread t : ts) {
            t.join();
        }
        System.out.println(Counter.studentCount);
        System.out.println(Counter.teacherCount);
    }
}

class Counter {
    public static final Object lock = new Object();
    public static int studentCount = 0;
    public static int teacherCount = 0;
}

class AddStudentThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) {
            synchronized(Counter.lock) {
                Counter.studentCount += 1;
            }
        }
    }
}

class DecStudentThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) {
            synchronized(Counter.lock) {
                Counter.studentCount -= 1;
            }
        }
    }
}

class AddTeacherThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) {
            synchronized(Counter.lock) {
                Counter.teacherCount += 1;
            }
        }
    }
}

class DecTeacherThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) {
            synchronized(Counter.lock) {
                Counter.teacherCount -= 1;
            }
        }
    }
}

執行結果
在這裏插入圖片描述

  • 上面4個線程兩個共享變量分別進行讀寫操作,但是使用的鎖都是Counter.lock對象,這就造成了原本可以併發執行的Counter.studentCount += 1和Counter.teacherCount += 1無法併發執行了執行效率大大降低
  • 實際上,需要同步的線程可以分成兩組:AddStudentThread和DecStudentThreadAddTeacherThread和DecTeacherThread,組之間不存在競爭,因此,應該使用兩個不同的鎖
public class TestSynchronizedMulti {
    public static void main(String[] args) throws Exception {
        //創建線程
        Thread[] ts = new Thread[]{new AddStudentThread(), new DecStudentThread(), new AddTeacherThread(), new DecTeacherThread()};
        //啓動線程
        for (Thread t : ts) {
            t.start();
        }
        //優先子線程先執行
        for (Thread t : ts) {
            t.join();
        }
        //最後打印執行結果
        System.out.println(Counter.studentCount);
        System.out.println(Counter.teacherCount);
    }
}

//計數器
class Counter {
    public static final Object lockTeacher = new Object();//學生線程鎖對象
    public static final Object lockStudent = new Object();//老師線程鎖對象
    public static int studentCount = 0;
    public static int teacherCount = 0;
}

//增加學生數量線程
class AddStudentThread extends Thread {
    public void run() {
        for (int i = 0; i < 10000; i++) {
            synchronized (Counter.lockStudent) {
                Counter.studentCount += 1;
            }
        }
    }
}

//減少學生數量線程
class DecStudentThread extends Thread {
    public void run() {
        for (int i = 0; i < 10000; i++) {
            synchronized (Counter.lockStudent) {
                Counter.studentCount -= 1;
            }
        }
    }
}

//增加老師數量線程
class AddTeacherThread extends Thread {
    public void run() {
        for (int i = 0; i < 10000; i++) {
            synchronized (Counter.lockTeacher) {
                Counter.teacherCount += 1;
            }
        }
    }
}

//減少老師數量線程
class DecTeacherThread extends Thread {
    public void run() {
        for (int i = 0; i < 10000; i++) {
            synchronized (Counter.lockTeacher) {
                Counter.teacherCount -= 1;
            }
        }
    }
}

執行結果
在這裏插入圖片描述

5.不需要synchronized的操作

JVM規範定義了幾種原子操作:

  • 基本類型(long和double除外)賦值,例如:int n = m;
  • 引用類型賦值,例如:List list = anotherList。
    • long和double是64位數據,JVM沒有明確規定64位賦值操作是不是一個原子操作,不過在x64平臺的JVM是把long和double的賦值作爲原子操作實現的。

  1. 單條原子操作的語句不需要同步。例如:
public void set(int m) {
    synchronized(lock) {
        this.value = m;
    }
}

就不需要同步

//引用類型賦值
public void set(String s) {
    this.value = s;
}
  1. 如果是多行賦值語句,就必須保證是同步操作
class Pair {
    int first;
    int last;
    public void set(int first, int last) {
        synchronized(this) {
            this.first = first;
            this.last = last;
        }
    }
}

有些時候,通過一些巧妙的轉換,可以把非原子操作變爲原子操作。例如,上述代碼如果改造成:

class Pair {
    int[] pair;
    public void set(int first, int last) {
        int[] ps = new int[] { first, last };
        this.pair = ps;
    }
}

就不再需要同步,因爲this.pair = ps引用賦值的原子操作。而語句:int[] ps = new int[] { first, last };,這裏的ps是方法內部定義的局部變量每個線程都會有各自的局部變量,互不影響,並且互不可見,並不需要同步。

6.小結

  1. 多線程同時讀寫共享變量時,會造成邏輯錯誤,因此需要通過synchronized同步;
  2. 同步的本質就是給指定對象加鎖,加鎖後才能繼續執行後續代碼;
  3. 注意加鎖對象必須是同一個實例
  4. 對JVM定義的單個原子操作不需要同步

七.同步方法

Java程序依靠synchronized對線程進行同步,使用synchronized的時候,鎖住的是哪個對象非常重要。
讓線程自己選擇鎖對象往往會使得代碼邏輯混亂,也不利於封裝。更好的方法是把synchronized邏輯封裝起來。


例如,我們編寫一個計數器

//計數器
public class Counter {
    private int count = 0;

    public synchronized  void add(int n) {
            count += n;
    }

    public synchronized  void dec(int n) {
            count -= n;
    }

    public int get() {
        return count;
    }
}

//測試方法
class Main {
    public static void main(String[] args) throws InterruptedException {
        Counter c1 = new Counter();
        Counter c2  = new Counter();

		// 對c1進行操作的線程:
        new Thread(() -> {
            c1.add(1);
        }).start();
        new Thread(() -> {
            c1.dec(1);
        }).start();


		// 對c2進行操作的線程:
        new Thread(() -> {
            c2.add(1);
        }).start();
        new Thread(() -> {
            c2.dec(1);
        }).start();

        //主線程休眠20毫秒
        Thread.sleep(20);
        System.out.println(c1.get());
        System.out.println(c2.get());
    }

線程調用 add()dec() 方法時不必關心同步邏輯,因爲synchronized代碼塊add()dec()方法內部。並且synchronized鎖住的對象是this即當前實例這使得創建多個Counter實例的時候,它們之間互不影響,可以併發執行

  • 使用synchronized修飾方法,表示整個方法都必須用this實例加鎖

    public synchronized void add(int n) { // 鎖住this
     count += n;
    } // 
    
  • 使用synchronized修飾靜態方法,鎖住的是該類的class實例
    static方法沒有this實例的,因爲static方法是針對類而不是實例。任何一個類都有一個由JVM自動創建的Class實例對static方法添加synchronized鎖住的是該類的class實例

    public class Counter {
     public static void test(int n) {
        	synchronized(Counter.class) {
          	//  ...
        	}
     	}
    }
    

Java中沒有特殊說明時,一個類默認是非線程安全的

八.死鎖

1.什麼是可重入鎖

Java的線程鎖是可重入的鎖

  1. 什麼是可重入的鎖?
    public synchronized void method1(){
        System.out.println("sysn method1");
        method2();
    }

    private synchronized void method2() {
        System.out.println("syn method2");
    }

如果一旦線程執行到add()方法內部,說明它已經獲取了當前實例的this鎖。如果傳入的n < 0,將在add()方法內部調用dec()方法。由於dec()方法也需要獲取this鎖,現在問題來了:

對同一個線程,能否在獲取到鎖以後繼續獲取同一個鎖?

  • 答案是肯定的。JVM允許同一個線程重複獲取同一個鎖,這種能被同一個線程反覆獲取的鎖,就叫做可重入鎖

  • 廣義上的可重入鎖指的是可重複可遞歸調用的鎖在外層使用鎖之後,在內層仍然可以使用,並且不發生死鎖(前提得是同一個對象或者class),這樣的鎖就叫做可重入鎖.

  • 可重入鎖(也叫遞歸鎖):指的是同一線程外層方法獲得鎖之後,內層遞歸方法仍然可以獲取該鎖的代碼,在同一線程在外層方法獲取鎖的時候+1,在進入內層方法會自動獲取鎖。也就是說,線程可以進入任何一個它已經擁有的鎖所同步着的代碼塊

  • 重入鎖以線程爲單位,當一個線程獲取對象鎖之後,這個線程可以再次獲取本對象上的鎖,而其他的線程是不可以的。ReentrantLock和synchronized都是可重入鎖

  • 可重入鎖的意義便在於防止死鎖!!!

  • 由於Java的線程鎖是可重入鎖,所以獲取鎖的時,不但要判斷是否是第一次獲取,還要記錄這是第幾次獲取。每獲取一次鎖,記錄+1,每退出synchronized塊,記錄-1,減到0的時候,纔會真正釋放鎖。

  • 實現原理是通過爲每個鎖關聯一個請求計數器和一個佔有它的線程。當計數爲0時,認爲鎖是未被佔有的;線程請求一個未被佔有的鎖時,JVM將記錄鎖的佔有者,並且將請求計數器置爲1 。

    • 如果同一個線程再次請求這個鎖,計數將遞增;
    • 每次佔用線程退出同步塊,計數器值將遞減。直到計數器爲0,鎖被釋放。

java之可重入鎖和遞歸鎖理論知識

2.不可重入鎖

所謂不可重入鎖,即指的是同一線程在外層方法獲得鎖之後,那麼在內層遞歸方法嘗試再次獲取鎖時,就會獲取不到被阻塞

  • 不可重入鎖,與可重入鎖相反,不可遞歸調用,遞歸調用就發生死鎖

public class Lock{
    private boolean isLocked = false;
    public synchronized void lock() throws InterruptedException{
        while(isLocked){    
            wait();
        }
        isLocked = true;
    }
    public synchronized void unlock(){
        isLocked = false;
        notify();
    }
}


public class Count{
    Lock lock = new Lock();
    public void print(){
        lock.lock();
        doAdd();
        lock.unlock();
    }
    public void doAdd(){
        lock.lock();
        //do something
        lock.unlock();
    }
}

當前線程執行print()方法首先獲取lock,接下來執行doAdd()方法就無法執行doAdd()中的邏輯,必須先釋放鎖。

Java不可重入鎖和可重入鎖理解

3.死鎖

  1. 什麼是死鎖
    死鎖產生的條件是多線程各自持有不同的鎖,並互相試圖獲取對方已持有的鎖,導致無限等待
public void add(int m) {
    synchronized(lockA) { // 獲得lockA的鎖
        this.value += m;
        synchronized(lockB) { // 獲得lockB的鎖
            this.another += m;
        } // 釋放lockB的鎖
    } // 釋放lockA的鎖
}

public void dec(int m) {
    synchronized(lockB) { // 獲得lockB的鎖
        this.another -= m;
        synchronized(lockA) { // 獲得lockA的鎖
            this.value -= m;
        } // 釋放lockA的鎖
    } // 釋放lockB的鎖
}

在獲取多個鎖的時候,不同線程獲取多個不同對象的鎖可能導致死鎖

線程1和線程2如果分別執行add()和dec()方法時:

  • 線程1:進入add(),獲得lockA;
  • 線程2:進入dec(),獲得lockB。

隨後:

  • 線程1:準備獲得lockB,失敗,等待中;
  • 線程2:準備獲得lockA,失敗,等待中。

此時兩個線程各自持有不同的鎖,然後各自試圖獲取對方手裏的鎖,造成了雙方無限等待下去,這就是死鎖

  • 死鎖發生後,沒有任何機制能解除死鎖,只能強制結束JVM進程。在編寫多線程應用時,要特別注意防止死鎖。因爲死鎖一旦形成,就只能強制結束進程。
  1. 如何避免死鎖?
    線程獲取鎖的順序要一致。即嚴格按照先獲取鎖A,再獲取鎖B的順序,或者先獲取鎖B,再獲取鎖A的順序

將上面代碼中的dec方法獲取鎖的順序改成和add()一樣即可

public void dec(int m) {
    synchronized(lockA) { // 獲得lockA的鎖
        this.value -= m;
        synchronized(lockB) { // 獲得lockB的鎖
            this.another -= m;
        } // 釋放lockB的鎖
    } // 釋放lockA的鎖
}
  1. 避免死鎖的3中辦法
  • 加鎖順序
    當多個線程需要相同的一些鎖,但是按照不同的順序加鎖,死鎖就很容易發生。如果能確保所有的線程都是按照相同的順序獲得鎖,那麼死鎖就不會發生。

  • 加鎖時限
    另外一個可以避免死鎖的方法是在嘗試獲取鎖的時候加一個超時時間,這也就意味着在嘗試獲取鎖的過程中若超過了這個時限該線程則放棄對該鎖請求。若一個線程沒有在給定的時限內成功獲得所有需要的鎖,則會進行回退並釋放所有已經獲得的鎖,然後等待一段隨機的時間再重試。這段隨機的等待時間讓其它線程有機會嘗試獲取相同的這些鎖,並且讓該應用在沒有獲得鎖的時候可以繼續運行(注:加鎖超時後可以先繼續運行乾點其它事情,再回頭來重複之前加鎖的邏輯)。

  • 死鎖檢測
    死鎖檢測是一個更好的死鎖預防機制,它主要是針對那些不可能實現按序加鎖並且鎖超時也不可行的場景。

    • 每當一個線程獲得了鎖,會在線程和鎖相關的數據結構中(map、graph等等)將其記下。除此之外,每當有線程請求鎖,也需要記錄在這個數據結構中。

    • 當一個線程請求鎖失敗時,這個線程可以遍歷鎖的關係圖看看是否有死鎖發生。例如,線程A請求鎖7,但是鎖7這個時候被線程B持有,這時線程A就可以檢查一下線程B是否已經請求了線程A當前所持有的鎖。如果線程B確實有這樣的請求,那麼就是發生了死鎖(線程A擁有鎖1,請求鎖7;線程B擁有鎖7,請求鎖1)。

    • 當然,死鎖一般要比兩個線程互相持有對方的鎖這種情況要複雜的多。線程A等待線程B,線程B等待線程C,線程C等待線程D,線程D又在等待線程A。線程A爲了檢測死鎖,它需要遞進地檢測所有被B請求的鎖。從線程B所請求的鎖開始,線程A找到了線程C,然後又找到了線程D,發現線程D請求的鎖被線程A自己持有着。這是它就知道發生了死鎖。

關於四個線程(A,B,C和D)之間鎖佔有和請求的關係圖。像這樣的數據結構就可以被用來檢測死鎖。
在這裏插入圖片描述
那麼當檢測出死鎖時,這些線程該做些什麼呢?

  • 一個可行的做法是釋放所有鎖,回退,並且等待一段隨機的時間後重試。這個和簡單的加鎖超時類似,不一樣的是隻有死鎖已經發生了纔回退,而不會是因爲加鎖的請求超時了。雖然有回退和等待,但是如果有大量的線程競爭同一批鎖,它們還是會重複地死鎖(編者注:原因同超時類似,不能從根本上減輕競爭)。

  • 一個更好的方案是給這些線程設置優先級,讓一個(或幾個)線程回退,剩下的線程就像沒發生死鎖一樣繼續保持着它們需要的鎖。如果賦予這些線程的優先級是固定不變的,同一批線程總是會擁有更高的優先級。爲避免這個問題,可以在死鎖發生的時候設置隨機的優先級。

九.使用wait和notify

1.什麼是多線程協調?

synchronized解決了多線程競爭的問題。例如,對於一個任務管理器,多個線程同時往隊列中添加任務,可以用synchronized加鎖:

class TaskQueue {
    Queue<String> queue = new LinkedList<>();

    public synchronized void addTask(String s) {
        this.queue.add(s);
    }
}

但是synchronized並沒有解決多線程協調的問題。

class TaskQueue {
    Queue<String> queue = new LinkedList<>();

    public synchronized void addTask(String s) {
        this.queue.add(s);
    }

    public synchronized String getTask() {
        while (queue.isEmpty()) {
        }
        return queue.remove();
    }
}

上述代碼看上去沒有問題:getTask()內部先判斷隊列是否爲空,如果爲空就循環等待,直到另一個線程往隊列中放入了一個任務,while()循環退出,就可以返回隊列的元素了。

  • 但實際上while()循環永遠不會退出。因爲線程在執行while()循環時,已經在getTask()入口獲取了this鎖,其他線程因爲addTask()執行條件也是獲取this鎖,根本無法調用addTask(),線程會在getTask()中因爲死循環而100%佔用CPU資源

而我們想要的執行效果是:

  • 線程1可以調用addTask()不斷往隊列中添加任務;
  • 線程2可以調用getTask()從隊列中獲取任務。如果隊列爲空,則getTask()應該等待,直到隊列中至少有一個任務時再返回。

多線程協調運行的原則就是:當條件不滿足時,線程進入等待狀態;當條件滿足時,線程被喚醒,已喚醒的線程還需要重新獲得鎖後才能繼續執行任務。

2.使用wait()和notify()解決多線程協調?

對於上述TaskQueue,我們先改造getTask()方法,在條件不滿足時,線程進入等待狀態:

    public synchronized String getTask() throws InterruptedException {
        while (queue.isEmpty()) {
         // 釋放this鎖:
        	this.wait();
        // 重新獲取this鎖
        }
        return queue.remove();
    }

當一個線程執行到getTask()方法內部的while循環時,它必定已經獲取到了this鎖,此時,線程執行while條件判斷,如果條件成立(隊列爲空),線程將執行this.wait(),進入等待狀態,且釋放當前佔用鎖

  • 這裏的關鍵是:wait()方法必須在當前獲取的鎖對象上調用,這裏獲取的是this鎖,因此調用this.wait()

  • 調用wait()方法後,線程進入等待狀態wait()方法不會返回,直到將來某個時刻,線程從等待狀態被其他線程喚醒後,wait()方法纔會返回,然後,繼續執行下一條語句。

  • 定義在Object類的一個native方法,也就是由JVM的C代碼實現的。其次,必須在synchronized塊中才能調用wait()方法,因爲wait()方法調用時,會釋放線程獲得的鎖,wait()方法返回後,線程又會重新試圖獲得鎖

當一個線程在this.wait()等待時,它就會釋放this鎖,從而使得其他線程能夠在addTask()方法獲得this鎖。


如何讓等待的線程被重新喚醒,然後從wait()方法返回? 答案是在相同的鎖對象上調用notify()方法。我們修改addTask()如下:

    public synchronized void addTask(String s) {
        this.queue.add(s);// 喚醒在this鎖等待的線程
        this.notify();
    }

注意到在往隊列中添加任務後,線程立刻對this鎖對象調用notify()方法,這個方法會喚醒一個正在等待this鎖的線程(就是在getTask()中位於this.wait()的線程),從而使得等待線程從this.wait()方法返回。


  • wait()、notify()方法屬於Object中的方法;對於Object中的方法,每個對象都擁有。

  • wait()方法:使當前線程進入等待狀態並釋放鎖,讓其他線程可以有機會運行,直到接到通知或者被中斷打斷爲止。在調用wait()方法之前,線程必須要獲得該對象的對象級鎖;換句話說就是該方法只能在同步方法或者同步塊中調用,如果沒有持有合適的鎖的話,線程將會拋出異常IllegalArgumentException如果調用成功後,當前線程則釋放鎖。

  • notify()方法:用來喚醒處於等待狀態獲取對象鎖的其他線程。如果有多個線程則線程調度器任意選出一個線程進行喚醒,使其去競爭獲取對象鎖,但調用notify()的線程並不會馬上就釋放該對象鎖,wait()所在的線程也不能馬上獲取該對象鎖,要程序退出同步塊或者同步方法之後,當前線程纔會釋放鎖,wait()所在的線程纔可以獲取該對象鎖。

  • wait()和notify()持有同一把鎖 ,wait()方法是釋放鎖的;notify()方法不釋放鎖,必須等到所在線程把代碼執行完。

3.完整例子

public class TaskQueueMain {
    public static void main(String[] args) throws InterruptedException {
        TaskQueue taskQueue = new TaskQueue();
        List<Thread> threadList = new ArrayList<Thread>();

        //創建5個線程用於從隊列中不斷取任務,如果隊列爲空,getTask()就會釋放當前this鎖,進入等待喚醒狀態
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread() {
                @Override
                public void run() {
                    // 執行task:
                    while (true) {
                        try {
                            String s = taskQueue.getTask();
                            System.out.println("execute task: " + s);
                        } catch (InterruptedException e) {
                            System.out.println(Thread.currentThread().getName()+":InterruptedException");
                            return;
                        }
                    }
                }
            };
            //啓動線程
            thread.start();
            //添加當前線程實例帶list中
            threadList.add(thread);
        }


        //創建一個線程循環添加10個任務到隊列中,每次添加都會喚醒處理等待狀態的任意一個線程
        Thread add = new Thread() {
            @Override
            public void run() {
                // 執行task:
                for (int i = 0; i < 10; i++) {
                    // 放入task:
                    String str = "t-" + Math.random();
                    System.out.println("add task: " + str);
                    taskQueue.addTask(str);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                    }
                }
            }
        };
        //啓動線程
        add.start();
        //阻塞main,必須等待當前線程執行完
        add.join();

        //休眠100毫秒
        Thread.sleep(100);

        //中斷處於等待狀態的線程
        //1.如果線程處於等待狀態,調用當前線程的interrupt()會拋出InterruptedExceptio
        // 2.因此,目標線程只要捕獲到拋出的InterruptedException,就說明有其他線程對其調用了interrupt()方法,通常情況下該線程應該立刻結束運行。
        for (Thread thread : threadList) {
            thread.interrupt();
        }
    }
}

class TaskQueue {
    Queue<String> queue = new LinkedList<>();

    public synchronized void addTask(String s) {
        this.queue.add(s);
        //喚醒所有等待 this鎖共同競爭this鎖
        this.notifyAll();
    }

    public synchronized String getTask() throws InterruptedException {
        while (queue.isEmpty()) {
            //釋放鎖,進入等待狀態
            this.wait();
            // 獲取鎖,繼續執行
        }
        return queue.remove();
    }
}

執行結果:
在這裏插入圖片描述

重點關注addTask()方法,內部調用了this.notifyAll()而不是this.notify()使用notifyAll()將喚醒所有當前正在this鎖等待的線程,而notify()只會喚醒其中一個(具體哪個依賴操作系統,有一定的隨機性)。

  • 這是因爲可能有多個線程正在getTask()方法內部的wait()中等待,使用notifyAll()將一次性全部喚醒。通常來說,notifyAll()更安全有些時候,如果我們的代碼邏輯考慮不周,用notify()會導致只喚醒了一個線程,而其他線程可能永遠等待下去醒不過來了。
  • 注意到wait()方法返回時需要重新獲得this鎖。假設當前有3個線程被喚醒,喚醒後,首先要等待執行addTask()的線程結束此方法後,才能釋放this鎖,隨後,這3個線程中只能有一個獲取到this鎖,剩下兩個將繼續等待。

再注意到我們在while()循環中調用wait(),而不是if語句

 public synchronized String getTask() throws InterruptedException {
        while (queue.isEmpty()) {
            this.wait();
        }
        return queue.remove();
    }

如果使用if語句實際上是錯誤的,因爲線程被喚醒時,需要再次獲取this鎖多個線程被喚醒後,只有一個線程能獲取this鎖,此時,該線程執行queue.remove()可以獲取到隊列的元素,然而,剩下的線程如果獲取this鎖後執行queue.remove(),此刻隊列可能已經沒有任何元素了。 所以,要始終在while循環中wait(),並且每次被喚醒後拿到this鎖就必須再次判斷隊列是否爲空,如果爲空則調用this.wait(),釋放鎖進入等待狀態

4.小結

  • wait和notify用於多線程協調運行:

  • synchronized內部可以調用wait()使線程進入等待狀態

  • 必須在已獲得的鎖對象上調用wait()方法;

  • synchronized內部可以調用notify()或notifyAll()喚醒其他等待線程;

  • 必須在已獲得的鎖對象上調用notify()或notifyAll()方法;

  • 已喚醒的線程還需要重新獲得鎖後才能繼續執行

十.使用ReentrantLock

1.什麼是ReentrantLock?

  • Java 5開始,引入了一個高級的處理併發的java.util.concurrent,它提供了大量更高級的併發功能,能大大簡化多線程程序的編寫。

  • Java語言直接提供了synchronized關鍵字用於加鎖,但這種鎖一是很重,二是獲取時必須一直等待,沒有額外的嘗試機制。

  • java.util.concurrent.locks包提供的ReentrantLock用於替代synchronized加鎖,

2.java中使用ReentrantLock

  1. 傳統的synchronized
public class Counter {
    private int count;

    public void add(int n) {
        synchronized(this) {
            count += n;
        }
    }
}
  1. 如果用ReentrantLock替代,可以把代碼改造爲:
public class Counter {
    private final Lock lock = new ReentrantLock();
    private int count;

    public void add(int n) {
        lock.lock();
        try {
            count += n;
        } finally {
            lock.unlock();
        }
    }
}

因爲synchronized是Java語言層面提供的語法,所以我們不需要考慮異常,而ReentrantLockJava代碼實現的鎖,我們就必須先獲取鎖,然後在finally中正確釋放鎖。

  • 顧名思義,ReentrantLock是可重入鎖,它和synchronized一樣,一個線程可以多次獲取同一個鎖。

和synchronized不同的是,ReentrantLock可以嘗試獲取鎖

if (lock.tryLock(1, TimeUnit.SECONDS)) {
    try {
       // ...
    } finally {
        lock.unlock();
    }
}

上述代碼在嘗試獲取鎖的時候,最多等待1秒。如果1秒後仍未獲取到鎖,tryLock()返回false,程序就可以做一些額外處理,而不是無限等待下去。

  • 所以,使用ReentrantLock比直接使用synchronized更安全線程在tryLock()失敗的時候不會導致死鎖。

ReentrantLock的lock(), tryLock(), tryLock(long timeout, TimeUnit unit), lockInterruptibly() 及使用場景示例

3.完整代碼

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TestReentrantLock {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread add = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    counter.add(1);
                }
            }
        };
        add.start();
        add.join();

        Thread dec = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    counter.dec(1);
                }
            }
        };

        dec.start();
        dec.join();

        Thread.sleep(100);
        System.out.println(counter.getCount());

    }
}

class Counter {
    private final Lock lock = new ReentrantLock();
    private int count;

    public void add(int n) {
        lock.lock();
        try {
            count += n;
        } finally {
            lock.unlock();
        }
    }

    public void dec(int n) {
            lock.lock();
            try {
                count -= n;
            } finally {
                lock.unlock();
        }
    }

    public int getCount() {
        return count;
    }
}

4.小結

  • ReentrantLock可以替代synchronized進行同步;

  • ReentrantLock獲取鎖更安全

    • 必須先獲取到鎖,再進入try {...}代碼塊,最後使用finally保證釋放鎖;
    • 可以使用tryLock()嘗試獲取鎖。

十一.使用ReentrantLock + Condition對象來實現wait和notify的功能

1.如何使用ReentrantLock + Condition對象來實現wait和notify的功能

使用ReentrantLock比直接使用synchronized安全,可以替代synchronized進行線程同步

synchronized可以配合wait和notify實現線程在條件不滿足時等待,條件滿足時喚醒,用ReentrantLock我們怎麼編寫wait和notify的功能呢?

  • 答案是使用Condition對象來實現wait和notify的功能。

我們仍然以TaskQueue爲例,把前面用synchronized實現的功能通過ReentrantLock和Condition來實現:

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TestReentrantLockConditionMain {
    public static void main(String[] args) throws InterruptedException {
        TaskQueue taskQueue = new TaskQueue();// 聲明任務隊列
        List<Thread> threadList = new ArrayList<>();// 聲明線程集合

        //創建5個線程用於從隊列中不斷取任務,如果隊列爲空,getTask()就會釋放當前this鎖,進入等待喚醒狀態
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread() {
                @Override
                public void run() {
                    while (true) {
                        String str = null;
                        try {
                            str = taskQueue.getTask();
                            System.out.println("execute task: " + str);
                        } catch (InterruptedException e) {
                            System.out.println(Thread.currentThread().getName() + ":InterruptedException");
                            return;
                        }

                    }
                }
            };

            //啓動線程
            thread.start();
            //添加當前線程實例帶list中
            threadList.add(thread);
        }


        //創建一個線程循環添加10個任務到隊列中,每次添加都會喚醒處理等待狀態的任意一個線程
        Thread add = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    // 放入task:
                    String str = "t-" + Math.random();
                    System.out.println("add task: " + str);
                     taskQueue.addTask(str);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) { }
                }
            }
        };
        //啓動線程
        add.start();
        //阻塞main,必須等待當前線程執行完
        add.join();


        //休眠100毫秒
        Thread.sleep(100);

        //中斷處於等待狀態的線程
        //1.如果線程處於等待狀態,調用當前線程的interrupt()會拋出InterruptedExceptio
        // 2.因此,目標線程只要捕獲到拋出的InterruptedException,就說明有其他線程對其調用了interrupt()方法,通常情況下該線程應該立刻結束運行。
        for (Thread thread : threadList) {
            thread.interrupt();
        }

    }
}

class TaskQueue {
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    private Queue<String> queue = new LinkedList<>();

    public void addTask(String str) {
        lock.lock();
        try {
            queue.add(str);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public String getTask() throws InterruptedException {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                condition.await();
            }
            return queue.remove();
        } finally {
            lock.unlock();
        }
    }
}

執行結果
在這裏插入圖片描述
使用Condition時,引用的Condition對象必須從Lock實例的newCondition()返回,這樣才能獲得一個綁定了Lock實例的Condition實例。

  • Condition提供的await()、signal()、signalAll()原理和synchronized鎖對象wait()、notify()、notifyAll()是一致的,並且其行爲也是一樣的:
    • await()會釋放當前鎖,進入等待狀態;
    • signal()會喚醒某個等待線程;
    • signalAll()會喚醒所有等待線程;
      • 喚醒線程從await()返回後需要重新獲得鎖。
  • tryLock()類似,await()可以在等待指定時間後,如果還沒有被其他線程通過signal()signalAll()喚醒可以自己醒來:
if (condition.await(1, TimeUnit.SECOND)) {
    // 被其他線程喚醒
} else {
    // 指定時間內沒有被其他線程喚醒
}

可見,使用Condition配合Lock,我們可以實現更靈活的線程同步

2.小結

  1. Condition可以替代synchronized + wait和notify實現線程同步;

  2. Condition對象必須從Lock對象獲取。

十二.使用ReadWriteLock

1.什麼是ReadWriteLock?

前面講到的ReentrantLock保證了只有一個線程可以執行臨界區代碼:

public class Counter {
    private final Lock lock = new ReentrantLock();
    private int[] counts = new int[10];

    public void inc(int index) {
        lock.lock();
        try {
            counts[index] += 1;
        } finally {
            lock.unlock();
        }
    }

    public int[] get() {
        lock.lock();
        try {
            return Arrays.copyOf(counts, counts.length);
        } finally {
            lock.unlock();
        }
    }
}

但是有些時候,這種保護有點過頭。 因爲我們發現,任何時刻,只允許一個線程修改,也就是調用inc()方法必須獲取鎖,但是,get()方法只讀取數據,不修改數據它實際上允許多個線程同時調用。

實際上我們想要的是:允許多個線程同時讀,但只要有一個線程在寫,其他線程就必須等待:

允許 不允許
不允許 不允許

使用ReadWriteLock可以解決這個問題,它保證:

  • 只允許一個線程寫入(其他線程既不能寫入也不能讀取);
  • 沒有寫入時,多個線程允許同時讀(提高性能)

2.Java中實現ReadWriteLock?

ReadWriteLock實現這個功能十分容易。我們需要創建一個ReadWriteLock實例,然後分別獲取讀鎖寫鎖

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class TestReadWriteLockMain {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();// 聲明任務隊列
        List<Thread> threadList = new ArrayList<>();// 聲明線程集合

        //創建5個線程用於從隊列中不斷取任務,如果隊列爲空,getTask()就會釋放當前this鎖,進入等待喚醒狀態
        for (int i = 0; i < 5; i++) {
            Thread readThread = new Thread() {
                @Override
                public void run() {
                while (true) {
                        counter.get();
                        System.out.println(Thread.currentThread().getName() + ":get("+ Arrays.toString(counter.get())+")");
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            System.out.println(Thread.currentThread().getName() + ":InterruptedException");
                            return;
                        }
                    }
                }
            };

            //啓動線程
            readThread.start();
            //添加當前線程實例帶list中
            threadList.add(readThread);
        }


        //創建一個線程循環添加10個任務到隊列中,每次添加都會喚醒處理等待狀態的任意一個線程
        Thread incThread = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    counter.inc(i);
                    System.out.println(Thread.currentThread().getName() + ":inc("+i+")");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }


            }
        };
        //啓動線程
        incThread.start();
        //阻塞main,必須等待當前線程執行完
        incThread.join();


        //休眠100毫秒
      Thread.sleep(100);

        //中斷處於等待狀態的線程
        //1.如果線程處於等待狀態,調用當前線程的interrupt()會拋出InterruptedExceptio
        // 2.因此,目標線程只要捕獲到拋出的InterruptedException,就說明有其他線程對其調用了interrupt()方法,通常情況下該線程應該立刻結束運行。
        for (Thread thread : threadList) {
            thread.interrupt();
        }
    }
}


class Counter {
    private final ReadWriteLock rwlock = new ReentrantReadWriteLock();
    private final Lock rlock = rwlock.readLock();
    private final Lock wlock = rwlock.writeLock();
    private int[] counts = new int[10];

    public void inc(int index) {
        wlock.lock(); // 加寫鎖
        try {
            counts[index] += 1;
        } finally {
            wlock.unlock(); // 釋放寫鎖
        }
    }

    public int[] get() {
        rlock.lock(); // 加讀鎖
        try {
            return Arrays.copyOf(counts, counts.length);
        } finally {
            rlock.unlock(); // 釋放讀鎖
        }
    }
}

執行結果

Thread-0:get([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
Thread-2:get([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
Thread-4:get([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
Thread-1:get([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
Thread-3:get([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
Thread-5:inc(0)
Thread-2:get([1, 0, 0, 0, 0, 0, 0, 0, 0, 0])
Thread-4:get([1, 0, 0, 0, 0, 0, 0, 0, 0, 0])
Thread-3:get([1, 0, 0, 0, 0, 0, 0, 0, 0, 0])
Thread-5:inc(1)
Thread-1:get([1, 0, 0, 0, 0, 0, 0, 0, 0, 0])
Thread-0:get([1, 0, 0, 0, 0, 0, 0, 0, 0, 0])
Thread-5:inc(2)
Thread-3:get([1, 1, 1, 0, 0, 0, 0, 0, 0, 0])
Thread-1:get([1, 1, 1, 0, 0, 0, 0, 0, 0, 0])
Thread-4:get([1, 1, 1, 0, 0, 0, 0, 0, 0, 0])
Thread-0:get([1, 1, 1, 0, 0, 0, 0, 0, 0, 0])
Thread-2:get([1, 1, 1, 0, 0, 0, 0, 0, 0, 0])
Thread-5:inc(3)
Thread-3:get([1, 1, 1, 1, 0, 0, 0, 0, 0, 0])
Thread-4:get([1, 1, 1, 1, 0, 0, 0, 0, 0, 0])
Thread-2:get([1, 1, 1, 1, 0, 0, 0, 0, 0, 0])
Thread-1:get([1, 1, 1, 1, 0, 0, 0, 0, 0, 0])
Thread-0:get([1, 1, 1, 1, 0, 0, 0, 0, 0, 0])
Thread-5:inc(4)
Thread-3:get([1, 1, 1, 1, 1, 0, 0, 0, 0, 0])
Thread-1:get([1, 1, 1, 1, 1, 0, 0, 0, 0, 0])
Thread-2:get([1, 1, 1, 1, 1, 0, 0, 0, 0, 0])
Thread-0:get([1, 1, 1, 1, 1, 0, 0, 0, 0, 0])
Thread-4:get([1, 1, 1, 1, 1, 0, 0, 0, 0, 0])
Thread-5:inc(5)
Thread-1:get([1, 1, 1, 1, 1, 1, 0, 0, 0, 0])
Thread-2:get([1, 1, 1, 1, 1, 1, 0, 0, 0, 0])
Thread-0:get([1, 1, 1, 1, 1, 1, 0, 0, 0, 0])
Thread-4:get([1, 1, 1, 1, 1, 1, 0, 0, 0, 0])
Thread-3:get([1, 1, 1, 1, 1, 1, 0, 0, 0, 0])
Thread-1:get([1, 1, 1, 1, 1, 1, 0, 0, 0, 0])
Thread-3:get([1, 1, 1, 1, 1, 1, 0, 0, 0, 0])
Thread-5:inc(6)
Thread-4:get([1, 1, 1, 1, 1, 1, 0, 0, 0, 0])
Thread-2:get([1, 1, 1, 1, 1, 1, 0, 0, 0, 0])
Thread-0:get([1, 1, 1, 1, 1, 1, 1, 0, 0, 0])
Thread-5:inc(7)
Thread-1:get([1, 1, 1, 1, 1, 1, 1, 1, 0, 0])
Thread-0:get([1, 1, 1, 1, 1, 1, 1, 1, 0, 0])
Thread-2:get([1, 1, 1, 1, 1, 1, 1, 1, 0, 0])
Thread-3:get([1, 1, 1, 1, 1, 1, 1, 1, 0, 0])
Thread-4:get([1, 1, 1, 1, 1, 1, 1, 1, 0, 0])
Thread-5:inc(8)
Thread-2:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 0])
Thread-0:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 0])
Thread-4:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 0])
Thread-1:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 0])
Thread-3:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 0])
Thread-5:inc(9)
Thread-1:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
Thread-3:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
Thread-0:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
Thread-2:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
Thread-4:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
Thread-4:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
Thread-1:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
Thread-0:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
Thread-3:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
Thread-2:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
Thread-1:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
Thread-4:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
Thread-2:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
Thread-0:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
Thread-3:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
Thread-4:InterruptedException
Thread-2:InterruptedException
Thread-0:InterruptedException
Thread-1:InterruptedException
Thread-3:InterruptedException

  • 讀寫操作分別用讀鎖寫鎖來加鎖,在讀取時 多個線程可以同時獲得讀鎖,這樣就大大提高了併發讀的執行效率。

  • 使用ReadWriteLock時,適用條件是同一個數據,有大量線程讀取,但僅有少數線程修改。

    例如: 一個論壇的帖子,回覆可以看做寫入操作,它是不頻繁的,但是,瀏覽可以看做讀取操作,是非常頻繁的,這種情況就可以使用ReadWriteLock。

3.小結

使用ReadWriteLock可以提高讀取效率:

  • ReadWriteLock只允許一個線程寫入

  • ReadWriteLock允許多個線程在沒有寫入時同時讀取

  • ReadWriteLock適合讀多寫少的場景

十三.樂觀鎖和悲觀鎖

1.什麼是悲觀鎖

總是假設最壞的情況,每次去拿數據的時候都認爲別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖(共享資源每次只給一個線程使用,其它線程阻塞,用完後再把資源轉讓給其它線程)。傳統的關係型數據庫裏邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。Java中synchronizedReentrantLock等獨佔鎖就是悲觀鎖思想的實現。

2.什麼是樂觀鎖

總是假設最好的情況,每次去拿數據的時候都認爲別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號機制CAS算法實現。樂觀鎖適用於多讀的應用類型,這樣可以提高吞吐量,像數據庫提供的類似於write_condition機制,其實都是提供的樂觀鎖。在Java中java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS實現的。

3.使用場景:

  • 悲觀鎖適合寫操作多的場景,先加鎖可以保證寫操作時數據正確。
  • 樂觀鎖適合讀操作多的場景,不加鎖的特點能夠使其讀操作的性能大幅提升。

4.什麼是CAS

CAS(compare and swap):當多個線程使用CAS獲取鎖,只能有一個成功,其他線程返回失敗,繼續嘗試獲取鎖;

  • CAS操作中包含三個參數:V(需讀寫的內存位置)+A(準備用來比較的參數)+B(準備寫入的新值):若A的參數與V的對應的值相匹配,就寫入值B;若不匹配,就寫入這個不匹配的值而非B;

5.樂觀鎖常見的兩種實現方式

樂觀鎖一般會使用版本號機制或CAS(Compare-and-Swap,即比較並替換)算法實現。

1.版本號機制

一般是在數據表中加上一個數據版本號version字段,表示數據被修改的次數,當數據被修改時,version值會加一。當線程A要更新數據值時,在讀取數據的同時也會讀取version值,在提交更新時,若剛纔讀取到的version值爲當前數據庫中的version值相等時才更新,否則重試更新操作,直到更新成功。

舉一個簡單的例子: 假設數據庫中帳戶信息表中有一個version字段,當前值爲 1 ;而當前帳戶餘額字段(balance )爲 $100

  1. 操作員 A 此時將其讀出( version=1 ),並從其帳戶餘額中扣除$50( $100-$50 )
  2. 在操作員 A 操作的過程中,操作員B 也讀入此用戶信息( version=1 ),並從其帳戶餘額中扣除$20 ( $100-$20 )
  3. 操作員 A 完成了修改工作,將數據版本號加1( version=2 ),連同帳戶扣除後餘額( balance=$50 ),提交至數據庫更新,此時由於提交數據版本大於數據庫記錄當前版本,數據被更新,數據庫記錄 version更新爲 2 。
  4. 操作員 B 完成了操作,也將版本號加1 ( version=2 )試圖向數據庫提交數據( balance=$80 ),但此時比對數據庫記錄版本時發現,操作員 B 提交的數據版本號爲 2 ,數據庫記錄當前版本也爲 2 ,不滿足 “ 提交版本必須大於記錄當前版本才能執行更新 “ 的樂觀鎖策略,因此,操作員 B 的提交被駁回。
    這樣,就避免了操作員 B 用基於 version=1的舊數據修改的結果覆蓋操作員A 的操作結果的可能。

2.CAS算法

compare and swap(比較與交換),是一種有名的無鎖算法。無鎖編程,即不使用鎖的情況下實現多線程之間的變量同步,也就是在沒有線程被阻塞的情況下實現變量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三個操作數

  • 需要讀寫的內存值 V

  • 進行比較的值 A

  • 擬寫入的新值 B

  • 當且僅當 V 的值等於 A時,CAS通過原子方式用新值B來更新V的值,否則不會執行任何操作(比較和替換是一個原子操作)。一般情況下是一個自旋操作(自旋鎖),即不斷的重試。

Java 多線程之悲觀鎖與樂觀鎖

十四.使用StampedLock

1.什麼是StampedLock

第十二章節講了 ReadWriteLock可以解決多線程同時讀 ,但只有一個線程能寫 的問題

  • 但是ReadWriteLock有個潛在的問題如果有線程正在讀,寫線程需要等待讀線程釋放鎖後才能獲取寫鎖,即讀的過程中不允許寫,這是一種悲觀的讀鎖。

要進一步提升併發執行效率,Java 8引入了新的讀寫鎖StampedLock

  • StampedLock和ReadWriteLock相比,改進之處在於:讀的過程中也允許獲取寫鎖後寫入! 這樣一來,我們讀的數據就可能不一致,所以需要編寫一點額外的代碼來判斷讀的過程中是否有寫入,這種讀鎖是一種樂觀鎖

  • 樂觀鎖的意思: 就是樂觀地估計讀的過程中大概率不會有寫入,因此被稱爲樂觀鎖。反過來,悲觀鎖則是讀的過程中拒絕有寫入,也就是寫入必須等待。 顯然樂觀鎖的併發效率更高但一旦有小概率的寫入導致讀取的數據不一致,需要能檢測出來,再讀一遍就行。

2.Java中使用StampedLock

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.StampedLock;

public class TestStampedLockMain {
    public static void main(String[] args) throws InterruptedException {
        Point point = new Point();
        List<Thread> threadList = new ArrayList<>();// 聲明線程集合

        //創建5個線程用於不斷從point讀
        for (int i = 0; i < 5; i++) {
            Thread readThread = new Thread() {
                @Override
                public void run() {
                    while (true) {
                        System.out.println(Thread.currentThread().getName() + ":get("+ point.distanceFromOrigin()+")");
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            System.out.println(Thread.currentThread().getName() + ":InterruptedException");
                            return;
                        }
                    }
                }
            };

            //啓動線程
            readThread.start();
            //添加當前線程實例帶list中
            threadList.add(readThread);
        }


        //創建一個線程不斷往point寫
        Thread writeThread = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    point.move(2,2);
                    System.out.println(Thread.currentThread().getName() + ":move(2,2)=>"+i);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

            }
        };
        //啓動線程
        writeThread.start();
        //阻塞main,必須等待當前線程執行完
        writeThread.join();


        //休眠100毫秒
        Thread.sleep(100);

        //中斷處於等待狀態的線程
        //1.如果線程處於等待狀態,調用當前線程的interrupt()會拋出InterruptedExceptio
        // 2.因此,目標線程只要捕獲到拋出的InterruptedException,就說明有其他線程對其調用了interrupt()方法,通常情況下該線程應該立刻結束運行。
        for (Thread thread : threadList) {
            thread.interrupt();
        }
    }
}



class Point {
    private final StampedLock stampedLock = new StampedLock();

    private double x;
    private double y;

    public void move(double deltaX, double deltaY) {
        long stamp = stampedLock.writeLock(); // 獲取寫鎖
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            stampedLock.unlockWrite(stamp); // 釋放寫鎖
        }
    }

    public double distanceFromOrigin() {
        long stamp = stampedLock.tryOptimisticRead(); // 獲得一個樂觀讀鎖,並返回版本號
        // 注意下面兩行代碼不是原子操作
        // 假設x,y = (100,200)
        double currentX = x;
        // 此處已讀取到x=100,但x,y可能被寫線程修改爲(300,400)
        double currentY = y;
        // 此處已讀取到y,如果沒有寫入,讀取是正確的(100,200)
        // 如果有寫入,讀取是錯誤的(100,400)
        if (!stampedLock.validate(stamp)) { // 檢查樂觀讀鎖後是否有其他寫鎖發生,即校驗版本號是否一致,不一致重新讀取x,y的值
            stamp = stampedLock.readLock(); // 獲取一個悲觀讀鎖
            try {
                currentX = x;
                currentY = y;
            } finally {
                stampedLock.unlockRead(stamp); // 釋放悲觀讀鎖
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }
}

執行結果:

Thread-0:get(0.0)
Thread-2:get(0.0)
Thread-1:get(0.0)
Thread-3:get(0.0)
Thread-4:get(0.0)
Thread-5:move(2,2)=>0
Thread-4:get(2.8284271247461903)
Thread-5:move(2,2)=>1
Thread-1:get(2.8284271247461903)
Thread-2:get(2.8284271247461903)
Thread-3:get(2.8284271247461903)
Thread-0:get(2.8284271247461903)
Thread-5:move(2,2)=>2
Thread-4:get(8.48528137423857)
Thread-3:get(8.48528137423857)
Thread-0:get(8.48528137423857)
Thread-2:get(8.48528137423857)
Thread-1:get(8.48528137423857)
Thread-5:move(2,2)=>3
Thread-4:get(11.313708498984761)
Thread-2:get(11.313708498984761)
Thread-0:get(11.313708498984761)
Thread-1:get(11.313708498984761)
Thread-3:get(11.313708498984761)
Thread-5:move(2,2)=>4
Thread-0:get(14.142135623730951)
Thread-3:get(14.142135623730951)
Thread-4:get(14.142135623730951)
Thread-2:get(14.142135623730951)
Thread-1:get(14.142135623730951)
Thread-5:move(2,2)=>5
Thread-4:get(16.97056274847714)
Thread-3:get(16.97056274847714)
Thread-2:get(16.97056274847714)
Thread-0:get(16.97056274847714)
Thread-1:get(16.97056274847714)
Thread-5:move(2,2)=>6
Thread-4:get(19.79898987322333)
Thread-0:get(19.79898987322333)
Thread-1:get(19.79898987322333)
Thread-3:get(19.79898987322333)
Thread-2:get(19.79898987322333)
Thread-5:move(2,2)=>7
Thread-2:get(22.627416997969522)
Thread-0:get(22.627416997969522)
Thread-3:get(22.627416997969522)
Thread-4:get(22.627416997969522)
Thread-1:get(22.627416997969522)
Thread-5:move(2,2)=>8
Thread-3:get(25.45584412271571)
Thread-1:get(25.45584412271571)
Thread-0:get(25.45584412271571)
Thread-4:get(25.45584412271571)
Thread-2:get(25.45584412271571)
Thread-5:move(2,2)=>9
Thread-3:get(28.284271247461902)
Thread-0:get(28.284271247461902)
Thread-1:get(28.284271247461902)
Thread-2:get(28.284271247461902)
Thread-4:get(28.284271247461902)
Thread-4:get(28.284271247461902)
Thread-2:get(28.284271247461902)
Thread-3:get(28.284271247461902)
Thread-0:get(28.284271247461902)
Thread-1:get(28.284271247461902)
Thread-1:get(28.284271247461902)
Thread-4:get(28.284271247461902)
Thread-3:get(28.284271247461902)
Thread-2:get(28.284271247461902)
Thread-0:get(28.284271247461902)
Thread-2:InterruptedException
Thread-1:InterruptedException
Thread-3:InterruptedException
Thread-4:InterruptedException
Thread-0:InterruptedException

  • 和ReadWriteLock相比,寫入的加鎖是完全一樣的不同的是讀取。

  • 注意到首先我們通過tryOptimisticRead()獲取一個樂觀讀鎖,並返回版本號。接着進行讀取,讀取完成後,我們通過validate()驗證版本號,如果在讀取過程中沒有寫入版本號不變驗證成功,我們就可以放心地繼續後續操作。如果在讀取過程中有寫入版本號會發生變化驗證將失敗。在失敗的時候,我們再通過 獲取悲觀讀鎖再次讀取由於寫入的概率不高,程序在絕大部分情況下可以通過樂觀讀鎖獲取數據,極少數情況下使用悲觀讀鎖獲取數據

    • 可見,StampedLock把讀鎖細分爲樂觀讀悲觀讀,能進一步提升併發效率。
    • 但這也是有代價的:一是代碼更加複雜,二是StampedLock是不可重入鎖不能在一個線程中反覆獲取同一個鎖

StampedLock還提供了更復雜的將悲觀讀鎖升級爲寫鎖的功能,它主要使用在if-then-update的場景:即先讀,如果讀的數據滿足條件,就返回,如果讀的數據不滿足條件,再嘗試寫。

3.小結

  • StampedLock提供了樂觀讀鎖可取代ReadWriteLock以進一步提升併發性能;

  • StampedLock是不可重入鎖。

十五.使用Concurrent集合

1.java.util.concurrent下的併發集合

前面十一章已經通過ReentrantLock和Condition實現了一個BlockingQueue

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TestReentrantLockConditionMain {
    public static void main(String[] args) throws InterruptedException {
        TaskQueue taskQueue = new TaskQueue();// 聲明任務隊列
        List<Thread> threadList = new ArrayList<>();// 聲明線程集合

        //創建5個線程用於從隊列中不斷取任務,如果隊列爲空,getTask()就會釋放當前this鎖,進入等待喚醒狀態
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread() {
                @Override
                public void run() {
                    while (true) {
                        String str = null;
                        try {
                            str = taskQueue.getTask();
                            System.out.println("execute task: " + str);
                        } catch (InterruptedException e) {
                            System.out.println(Thread.currentThread().getName() + ":InterruptedException");
                            return;
                        }

                    }
                }
            };

            //啓動線程
            thread.start();
            //添加當前線程實例帶list中
            threadList.add(thread);
        }


        //創建一個線程循環添加10個任務到隊列中,每次添加都會喚醒處理等待狀態的任意一個線程
        Thread add = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    // 放入task:
                    String str = "t-" + Math.random();
                    System.out.println("add task: " + str);
                     taskQueue.addTask(str);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) { }
                }
            }
        };
        //啓動線程
        add.start();
        //阻塞main,必須等待當前線程執行完
        add.join();


        //休眠100毫秒
        Thread.sleep(100);

        //中斷處於等待狀態的線程
        //1.如果線程處於等待狀態,調用當前線程的interrupt()會拋出InterruptedExceptio
        // 2.因此,目標線程只要捕獲到拋出的InterruptedException,就說明有其他線程對其調用了interrupt()方法,通常情況下該線程應該立刻結束運行。
        for (Thread thread : threadList) {
            thread.interrupt();
        }

    }
}

class TaskQueue {
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    private Queue<String> queue = new LinkedList<>();

    public void addTask(String str) {
        lock.lock();
        try {
            queue.add(str);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public String getTask() throws InterruptedException {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                condition.await();
            }
            return queue.remove();
        } finally {
            lock.unlock();
        }
    }
}

執行結果:
在這裏插入圖片描述

BlockingQueue(阻塞隊列)的意思就是說:當一個線程調用這個TaskQueuegetTask()方法時,該方法內部可能會讓線程變成等待狀態直到隊列條件滿足不爲空線程被喚醒後,getTask()方法纔會返回

  • 因爲BlockingQueue非常有用,所以我們不必自己編寫,可 直接使用Java標準庫的java.util.concurrent包提供的線程安全的集合ArrayBlockingQueue
interface non-thread-safe thread-safe
List ArrayList CopyOnWriteArrayList
Map HashMap ConcurrentHashMap
Set HashSet / TreeSet CopyOnWriteArraySet
Queue ArrayDeque / LinkedList ArrayBlockingQueue / LinkedBlockingQueue
Deque ArrayDeque / LinkedList LinkedBlockingDeque

使用這些併發集合與使用非線程安全的集合類完全相同。我們以ConcurrentHashMap爲例:

Map<String, String> map = ConcurrentHashMap<>();
// 在不同的線程讀寫:
map.put("A", "1");
map.put("B", "2");
map.get("A", "1");

因爲所有的同步和加鎖的邏輯都在集合內部實現,對外部調用者來說,只需要正常按接口引用,其他代碼和原來的非線程安全代碼完全一樣。即當我們需要多線程訪問時,把:

Map<String, String> map = HashMap<>();
//改爲
Map<String, String> map = ConcurrentHashMap<>();

java.util.Collections工具類還提供了一箇舊的線程安全集合轉換器處理List/Set/Map

語法爲 Collections.synchronizedXXX(Collection c)

Map unsafeMap = new HashMap();
Map threadSafeMap = Collections.synchronizedMap(unsafeMap);
  • 它實際上是用一個包裝類包裝了非線程安全的Map然後對所有讀寫方法都用synchronized加鎖,這樣獲得的線程安全集合的性能java.util.concurrent集合要低很多,所以不推薦使用。

2.小結

  • 使用java.util.concurrent包提供的線程安全的併發集合可以大大簡化多線程編程:

  • 多線程同時讀寫併發集合是安全的;

  • 儘量使用Java標準庫提供的併發集合,避免自己編寫同步代碼。

Java併發集合類
Java併發集合類

十六.使用Atomic(原子類)

1.什麼是原子類?

Atomic 翻譯成中文是原子的意思。在化學上,我們知道原子是構成一般物質的最小單位,在化學反應中是不可分割的。在我們這裏 Atomic是指一個操作是不可中斷的。即使是在多個線程一起執行的時候,一個操作一旦開始,就不會被其他線程干擾。 所以,所謂原子類說簡單點就是具有原子/原子操作特徵的類。


Java的java.util.concurrent包除了提供底層鎖併發集合外,還提供了一組原子操作的封裝類,它們位於java.util.concurrent.atomic包。

我們以AtomicInteger爲例,它提供的主要操作有:

  • 增加值並返回新值:int addAndGet(int delta)
  • 加1後返回新值:int incrementAndGet()
  • 獲取當前值:int get()
  • 用CAS方式設置:int compareAndSet(int expect, int update)

Atomic類是通過無鎖(lock-free)的方式實現的線程安全(thread-safe)訪問。它的主要原理是利用了CAS:Compare and Set。

如果我們自己通過CAS編寫incrementAndGet(),它大概長這樣:

public int incrementAndGet(AtomicInteger var) {
    int prev, next;
    do {
        prev = var.get();
        next = prev + 1;
    } while ( ! var.compareAndSet(prev, next));
    return prev;
}

CAS是指,在這個操作中,如果AtomicInteger的當前值是prev,那麼就更新爲next,返回true。如果AtomicInteger的當前值不是prev,就什麼也不幹,返回false。通過CAS操作配合do ... while循環即使其他線程修改了AtomicInteger的值,最終的結果也是正確的。

我們利用AtomicLong可以編寫一個多線程安全的全局唯一ID生成器:

class IdGenerator {
    AtomicLong var = new AtomicLong(0);

    public long getNextId() {
        return var.incrementAndGet();
    }
}
  • 通常情況下,我們並不需要直接用do ... while循環調用compareAndSet實現複雜的併發操作,而是用incrementAndGet()這樣的封裝好的方法,因此,使用起來非常簡單。

  • 在高度競爭的情況下,還可以使用Java 8提供的LongAdder和LongAccumulator

2.關於原子類個數說明

JDK7包括7之前java原子類有12個,圖片如下,有些資料說有13個,多出來的是AtomicBooleanArray類,可是我在JDK8之前的源碼裏並沒有發現有這個類,當然我也沒去8以上的版本去看,所以這裏不確定這個類到底在哪個版本中存在。
在這裏插入圖片描述
在JDK8時出現了4個原子操作類,分別是如下圖片所示
在這裏插入圖片描述

3.原子類的分類:

  • 原子更新基本數據類型
  • 原子更新數組類型
  • 原子更新抽象數據類型
  • 原子更新字段

併發包java.util.concurrent 的原子類都存放在java.util.concurrent.atomic下,如下圖所示。
在這裏插入圖片描述

1.原子更新基本類型類

  • AtomicBoolean: 原子更新布爾類型。
  • AtomicInteger: 原子更新整型。
  • AtomicLong: 原子更新長整型。

以上3個類提供的方法幾乎一模一樣,以AtomicInteger爲例進行詳解,AtomicIngeter的常用方法如下:

  • int addAndGet(int delta): 以原子的方式將輸入的數值與實例中的值相加,並返回結果。
  • boolean compareAndSet(int expect, int update): 如果輸入的值等於預期值,則以原子方式將該值設置爲輸入的值。
  • int getAndIncrement(): 以原子的方式將當前值加 1,注意,這裏返回的是自增前的值,也就是舊值。
  • void lazySet(int newValue): 最終會設置成newValue,使用lazySet設置值後,可能導致其他線程在之後的一小段時間內還是可以讀到舊的值。
  • int getAndSet(int newValue): 以原子的方式設置爲newValue,並返回舊值。

代碼示例

	static AtomicInteger ai =new AtomicInteger(1);
	public static void main(String[] args) {
 
		System.out.println(ai.getAndIncrement());
		System.out.println(ai.get());
     }

2.原子更新數組

  • AtomicIntegerArray: 原子更新整型數組裏的元素。
  • AtomicLongArray: 原子更新長整型數組裏的元素。
  • AtomicReferenceArray: 原子更新引用類型數組裏的元素。

三個類的最常用的方法是如下兩個方法:

  • get(int index):獲取索引爲index的元素值。
  • compareAndSet(int i, int expect, int update): 如果當前值等於預期值,則以原子方式將數組位置 i 的元素設置爲update值。

代碼示例

//下面以 AtomicReferenceArray 舉例如下
	static int[] value =new int[]{1,2};
	static AtomicIntegerArray ai =new AtomicIntegerArray(value);
	public static void main(String[] args) {
 
		ai.getAndSet(0,2);
		System.out.println(ai.get(0));
		System.out.println(value[0]);
    }

3.原子更新引用類型

原子更新基本類型的AtomicInteger,只能更新一個值,如果更新多個值,比如更新一個對象裏的值,那麼就要用原子更新引用類型提供的類,Atomic包提供了以下三個類:

  • AtomicReference: 原子更新引用類型。
  • AtomicReferenceFieldUpdater: 原子更新引用類型的字段。
  • AtomicMarkableReferce: 原子更新帶有標記位的引用類型,可以使用構造方法更新一個布爾類型的標記位和引用類型。

代碼示例

  public static AtomicReference<User> ai = new AtomicReference<User>();
 
    public static void main(String[] args) {
 
        User u1 = new User("pangHu", 18);
        ai.set(u1);
        User u2 = new User("piKaQiu", 15);
        ai.compareAndSet(u1, u2);
        System.out.println(ai.get().getAge() + ai.get().getName());
 
    }
 
 
static class User {
        private String name;
        private int age;
 
        public User(String name, int age) {
            this.name = name;
            this.age = age;
        }
 
        public String getName() {
            return name;
        }
 
        public void setName(String name) {
            this.name = name;
        }
 
        public int getAge() {
            return age;
        }
 
        public void setAge(int age) {
            this.age = age;
        }
    }
 
 //輸出結果:piKaQiu 15

4.原子更新字段類

如果需要原子的更新類裏某個字段時,需要用到原子更新字段類,Atomic包提供了3個類進行原子字段更新:

  • AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。
  • AtomicLongFieldUpdater: 原子更新長整型字段的更新器。
  • AtomicStampedFieldUpdater: 原子更新帶有版本號的引用類型。

代碼示例

 //創建原子更新器,並設置需要更新的對象類和對象的屬性
    private static AtomicIntegerFieldUpdater<User> ai = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
 
    public static void main(String[] args) {
 
        User u1 = new User("pangHu", 18);
        //原子更新年齡,+1
        System.out.println(ai.getAndIncrement(u1));
        System.out.println(u1.getAge());
    }
 
 
 
static class User {
        private String name;
        public volatile int age;
 
        public User(String name, int age) {
            this.name = name;
            this.age = age;
        }
 
        public String getName() {
            return name;
        }
 
        public void setName(String name) {
            this.name = name;
        }
 
        public int getAge() {
            return age;
        }
 
        public void setAge(int age) {
            this.age = age;
        }
    }
//輸出結果
//18
//19

要想原子地更新字段類需要兩步。

  • 第一步,因爲原子更新字段類都是抽象類,每次使用的時候必須使用靜態方法newUpdater()創建一個更新器,並且需要設置想要更新的類和屬性
  • 第二步,更新類的字段必須使用public volatile 修飾。

5.JDK8新增原子類簡介

  • DoubleAccumulator
  • LongAccumulator
  • DoubleAdder
  • LongAdder

下面以 LongAdder 爲例介紹一下,並列出使用注意事項

這些類對應把AtomicLong等類的改進。比如LongAccumulator 高併發環境下比 AtomicLong 更高效。

  • Atomic、Adder在低併發環境下,兩者性能很相似。但在高併發環境下,Adder 有着明顯更高的吞吐量,但是有着更高的空間複雜度。

  • LongAdder其實是LongAccumulator的一個特例,調用LongAdder相當使用下面的方式調LongAccumulator。

  • sum() 方法在沒有併發的情況下調用,如果在併發情況下使用會存在計數不準,下面有代碼爲例。

  • LongAdder不可以代替AtomicLong雖然 LongAdder 的 add() 方法可以原子性操作,但是並沒有使用 Unsafe 的CAS算法,只是使用了CAS的思想。

  • LongAccumulator,LongAccumulator提供了比LongAdder更強大的功能,構造函數其中accumulatorFunction一個雙目運算器接口,根據輸入的兩個參數返回一個計算值,identity則是LongAccumulator累加器的初始

在這裏插入圖片描述

如圖LongAdder則是內部維護多個變量,每個變量初始化都0,在同等併發量的情況下,爭奪單個變量的線程量會減少這是變相的減少了爭奪共享資源的併發量,另外多個線程在爭奪同一個原子變量時候如果失敗並不是自旋CAS重試,而是嘗試獲取其他原子變量的鎖,最後獲取當前值時候是把所有變量的值累加後返回的。

//構造函數
LongAdder()
    //創建初始和爲零的新加法器。
 
//方法摘要
void    add(long x)    //添加給定的值。
void    decrement()    //相當於add(-1)。
double  doubleValue() //在擴展原始轉換之後返回sum()as double。
float   floatValue()  //在擴展原始轉換之後返回sum()as float。
void    increment()  //相當於add(1)。
int intValue()      //返回sum()作爲int一個基本收縮轉換之後。
long    longValue() //相當於sum()。
void    reset()    //重置將總和保持爲零的變量。
long    sum()     //返回當前的總和。
long    sumThenReset()  //等同於sum()後面的效果reset()。
String  toString()   //返回。的字符串表示形式sum()。

4.使用java.util.concurrent.atomic提供的原子操作可以簡化多線程編程:

  • 原子操作實現了無鎖的線程安全;

  • 適用於計數器,累加器等

十七.使用線程池

1.什麼是線程池?

Java語言雖然內置了多線程支持,啓動一個新線程非常方便,但創建線程需要操作系統資源(線程資源,棧空間等),頻繁創建和銷燬大量線程需要消耗大量時間。

如果可以複用一組線程:
在這裏插入圖片描述

那麼我們就可以把很多小任務讓一組線程來執行而不是一個任務對應一個新線程。這種能接收大量小任務並進行分發處理的就是線程池

  • 簡單地說,線程池內部維護了若干個線程沒有任務的時候,這些線程都處於等待狀態。 **如果有新任務,就分配一個空閒線程執行如果所有線程都處於忙碌狀態新任務要麼放入隊列等待,要麼增加一個新線程進行處理

2.Java中使用線程池

Java標準庫提供了ExecutorService接口表示線程池,它的典型用法如下:

// 創建固定大小的線程池:
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交任務:
executor.submit(task1);
executor.submit(task2);
executor.submit(task3);
executor.submit(task4);
executor.submit(task5);

因爲ExecutorService只是接口,Java標準庫提供的幾個常用實現類有:

  • FixedThreadPool:線程數固定的線程池;
  • CachedThreadPool:線程數根據任務動態調整的線程池;
  • SingleThreadExecutor:僅單線程執行的線程池。

FixedThreadPool
創建這些線程池的方法都被封裝到Executors這個類中。我們以FixedThreadPool爲例,看看線程池的執行邏輯:

import java.util.concurrent.*;

public class Main {
    public static void main(String[] args) {
        // 創建一個固定大小的線程池:
        ExecutorService es = Executors.newFixedThreadPool(4);
        for (int i = 0; i < 6; i++) {
            es.submit(new Task("" + i));
        }
        // 關閉線程池:
        es.shutdown();
    }
}

class Task implements Runnable {
    private final String name;

    public Task(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        System.out.println("start task " + name);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }
        System.out.println("end task " + name);
    }
}

一次性放入6個任務,由於線程池只有固定的4個線程,因此,前4個任務會同時執行等到有線程空閒後,纔會執行後面的兩個任務。

線程池在程序結束的時候要關閉

  • shutdown()方法關閉線程池的時候,它會等待正在執行的任務先完成,然後再關閉
  • shutdownNow()會立刻停止正在執行的任務
    -awaitTermination()則會等待指定的時間讓線程池關閉。

CachedThreadPool
如果我們把線程池改爲CachedThreadPool,由於這個線程池的實現會根據任務數量動態調整線程池的大小,所以6個任務可一次性全部同時執行

如果我們想把線程池的大小限制在4~10個之間動態調整怎麼辦?我們看Executors.newCachedThreadPool()方法的源碼

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                    60L, TimeUnit.SECONDS,
                                    new SynchronousQueue<Runnable>());
}

想創建指定動態範圍的線程池可以這麼寫:

int min = 4;
int max = 10;
ExecutorService es = new ThreadPoolExecutor(min, max,60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());

ScheduledThreadPool
還有一種任務需要 定期反覆執行例如:每秒刷新證券價格。
這種任務本身固定,需要反覆執行的,可以使用ScheduledThreadPool。放入ScheduledThreadPool的任務可以定期反覆執行。

創建一個ScheduledThreadPool仍然是通過Executors類:

ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);

我們可以提交一次性任務,它會在指定延遲後只執行一次:

// 1秒後執行一次性任務:
ses.schedule(new Task("one-time"), 1, TimeUnit.SECONDS);

如果任務以固定的每3秒執行,我們可以這樣寫:

// 5秒後開始執行定時任務,每3秒執行一次:
ses.scheduleAtFixedRate(new Task("fixed-rate"), 5, 3, TimeUnit.SECONDS);

如果任務以固定的3秒爲間隔執行,我們可以這樣寫:

// 3秒後開始執行定時任務,以3秒爲間隔執行:
ses.scheduleWithFixedDelay(new Task("fixed-delay"), 2, 3, TimeUnit.SECONDS);

注意FixedRateFixedDelay的區別

  • FixedRate是指任務總是以固定時間間隔觸發,不管任務執行多長時間
    在這裏插入圖片描述

  • FixedDelay是指,上一次任務執行完畢後,等待固定的時間間隔,再執行下一次任務
    在這裏插入圖片描述

因此,使用ScheduledThreadPool時,我們要根據需要選擇執行一次FixedRate執行還是FixedDelay執行

細心的童鞋還可以思考下面的問題:

  • FixedRate模式下,假設每秒觸發,如果某次任務執行時間超過1秒,後續任務會不會併發執行
  • 如果任務拋出了異常,後續任務是否繼續執行

Java標準庫還提供了一個java.util.Timer類,這個類也可以定期執行任務,但是,一個Timer會對應一個Thread,所以,一個Timer只能定期執行一個任務多個定時任務必須啓動多個Timer

  • 一個ScheduledThreadPool就可以調度多個定時任務,所以,我們完全可以用ScheduledThreadPool取代舊的Timer。

3.小結

JDK提供了ExecutorService實現了線程池功能:

  • 線程池內部維護一組線程,可以高效執行大量小任務

-Executors提供了靜態方法創建不同類型的ExecutorService

  • 必須調用shutdown()關閉ExecutorService

  • ScheduledThreadPool可以定期調度多個任務

Java線程學習體系
Java 中的幾種線程池,你之前用對了嗎
java中的線程池有哪些,分別有什麼作用?
最詳細的Java線程池原理解析
java線程池,阿里爲什麼不允許使用Executors?
java線程池詳解
Java線程池詳解

十八.使用Future

1.什麼是Future?

class Task implements Runnable {
    public String result;

    public void run() {
        this.result = longTimeCalculation(); 
    }
}

Runnable接口有個問題,它的方法沒有返回值如果任務需要一個返回結果,那麼只能保存到變量,還要提供額外的方法讀取,非常不便。所以,Java標準庫還提供了一個Callable接口,和Runnable接口比,它多了一個返回值:並且Callable接口是一個泛型接口,可以指定返回類型的結果

class Task implements Callable<String> {
    public String call() throws Exception {
        return longTimeCalculation(); 
    }
}

2.Java中使用Future?

現在的問題是,如何獲得異步執行的結果?

如果仔細看ExecutorService.submit()方法,可以看到,它返回了一個Future類型一個Future類型的實例代表一個未來能獲取結果的對象

ExecutorService executor = Executors.newFixedThreadPool(4); 
// 定義任務:
Callable<String> task = new Task();
// 提交任務並獲得Future:
Future<String> future = executor.submit(task);
// 從Future獲取異步執行返回的結果:
String result = future.get(); // 可能阻塞
  • 當我們提交一個Callable任務後,我們會同時獲得一個Future對象,然後,我們在主線程某個時刻調用Future對象的get()方法,就可以獲得異步執行的結果
  • 調用get()時,如果異步任務已經完成,我們就直接獲得結果。如果異步任務還沒有完成,那麼get()會阻塞,直到任務完成後才返回結果。

一個Future<V>接口表示一個未來可能會返回的結果,它定義的方法有:

  • get():獲取結果(可能會等待)
  • get(long timeout, TimeUnit unit):獲取結果,但只等待指定的時間;
  • cancel(boolean mayInterruptIfRunning):取消當前任務;
    -isDone():判斷任務是否已完成。
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Main {
	public static void TestFutureMain(String[] args) throws Exception {
		ExecutorService es = Executors.newFixedThreadPool(4);
		Future<BigDecimal> future = es.submit(new Task("601857"));
		System.out.println(future.get());
		es.shutdown();
	}
}

class Task implements Callable<BigDecimal> {

	public Task(String code) {
	}

	@Override
	public BigDecimal call() throws Exception {
		Thread.sleep(1000);
		double d = 5 + Math.random() * 20;
		return new BigDecimal(d).setScale(2, RoundingMode.DOWN);
	}
}

3.小結

  • 線程池提交一個Callable任務,可以獲得一個Future對象

  • 可以用Future在將來某個時刻獲取結果

十九. 使用CompletableFuture

1.什麼是CompletableFuture?

  • 使用Future獲得異步執行結果時,要麼調用阻塞方法get(),要麼輪詢看isDone()是否爲true,這兩種方法都不是很好,因爲主線程也會被迫等待

  • Java 8開始引入了CompletableFuture,它針對Future做了改進,可以傳入回調對象,當異步任務完成或者發生異常時自動調用回調對象的回調方法。

以獲取股票價格爲例,看看如何使用CompletableFuture

import java.util.concurrent.CompletableFuture;

public class TestCompletableFutureMain1{
    public static void main(String[] args) throws Exception {
        // 創建異步執行任務:
        CompletableFuture<Double> cf = CompletableFuture.supplyAsync(TestCompletableFutureMain1::fetchPrice);
        // 如果執行成功:
        cf.thenAccept((result) -> {
            System.out.println("price: " + result);
        });
        // 如果執行異常:
        cf.exceptionally((e) -> {
            e.printStackTrace();
            return null;
        });
        // 主線程不要立刻結束,否則CompletableFuture默認使用的線程池會立刻關閉:
        Thread.sleep(2000);
    }

    static Double fetchPrice() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }
        if (Math.random() < 0.3) {
            throw new RuntimeException("fetch price failed!");
        }
        return 5 + Math.random() * 20;
    }
}

2.Java中使用CompletableFuture?

  1. 創建一個CompletableFuture是通過CompletableFuture.supplyAsync()實現的,它需要一個實現了Supplier接口的對象
public interface Supplier<T> {
    T get();
}

這裏我們用lambda語法簡化了一下,直接傳入TestCompletableFutureMain1::fetchPrice,因爲TestCompletableFutureMain1.fetchPrice()靜態方法的簽名符合Supplier接口的定義(除了方法名外)。

  1. 緊接着,CompletableFuture已經被提交給默認的線程池執行了,我們需要定義的是CompletableFuture完成時和異常時需要回調的實例

完成時CompletableFuture調用Consumer對象

public interface Consumer<T> {
    void accept(T t);
}

異常時CompletableFuture會調用Function對象

public interface Function<T, R> {
    R apply(T t);
}

!!! 這裏我們都用lambda語法簡化了代碼。

*可見CompletableFuture的優點是:

  • 異步任務結束時,會自動回調某個對象的方法;
  • 異步任務出錯時,會自動回調某個對象的方法;
  • 主線程設置好回調後不再關心異步任務的執行

3.CompletableFuture相比Future的優勢

如果只是實現了異步回調機制,我們還看不出CompletableFuture相比Future的優勢。CompletableFuture更強大的功能是,多個CompletableFuture可以串行執行

例如: 定義兩個CompletableFuture,
第一個CompletableFuture根據證券名稱查詢證券代碼,
第二個CompletableFuture根據證券代碼查詢證券價格,這兩個CompletableFuture實現串行操作如下:

import java.util.concurrent.CompletableFuture;

public class TestCompletableFutureMain2 {
    public static void main(String[] args) throws Exception {
        // 第一個任務:
        CompletableFuture<String> cfQuery = CompletableFuture.supplyAsync(() -> {
            return queryCode("中國石油");
        });
        // cfQuery成功後繼續執行下一個任務:
        CompletableFuture<Double> cfFetch = cfQuery.thenApplyAsync((code) -> {
            return fetchPrice(code);
        });
        // cfFetch成功後打印結果:
        cfFetch.thenAccept((result) -> {
            System.out.println("price: " + result);
        });
        // 主線程不要立刻結束,否則CompletableFuture默認使用的線程池會立刻關閉:
        Thread.sleep(2000);
    }

    static String queryCode(String name) {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
        }
        return "601857";
    }

    static Double fetchPrice(String code) {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
        }
        return 5 + Math.random() * 20;
    }
}

除了串行執行外,多個CompletableFuture還可以並行執行。例如,我們考慮這樣的場景:

同時從新浪和網易查詢證券代碼,只要任意一個返回結果,就進行下一步查詢價格查詢價格也同時從新浪和網易查詢,只要任意一個返回結果,就完成操作

import java.util.concurrent.CompletableFuture;

public class TestCompletableFutureMain3 {
    public static void main(String[] args) throws Exception {
        // 兩個CompletableFuture執行異步查詢:
        CompletableFuture<String> cfQueryFromSina = CompletableFuture.supplyAsync(() -> {
            return queryCode("中國石油", "https://finance.sina.com.cn/code/");
        });
        CompletableFuture<String> cfQueryFrom163 = CompletableFuture.supplyAsync(() -> {
            return queryCode("中國石油", "https://money.163.com/code/");
        });

        // 用anyOf合併爲一個新的CompletableFuture:
        CompletableFuture<Object> cfQuery = CompletableFuture.anyOf(cfQueryFromSina, cfQueryFrom163);

        // 兩個CompletableFuture執行異步查詢:
        CompletableFuture<Double> cfFetchFromSina = cfQuery.thenApplyAsync((code) -> {
            return fetchPrice((String) code, "https://finance.sina.com.cn/price/");
        });
        CompletableFuture<Double> cfFetchFrom163 = cfQuery.thenApplyAsync((code) -> {
            return fetchPrice((String) code, "https://money.163.com/price/");
        });

        // 用anyOf合併爲一個新的CompletableFuture:
        CompletableFuture<Object> cfFetch = CompletableFuture.anyOf(cfFetchFromSina, cfFetchFrom163);

        // 最終結果:
        cfFetch.thenAccept((result) -> {
            System.out.println("price: " + result);
        });
        // 主線程不要立刻結束,否則CompletableFuture默認使用的線程池會立刻關閉:
        Thread.sleep(2000);
    }

    static String queryCode(String name, String url) {
        System.out.println("query code from " + url + "...");
        try {
            Thread.sleep((long) (Math.random() * 1000));
        } catch (InterruptedException e) {
        }
        return "601857";
    }

    static Double fetchPrice(String code, String url) {
        System.out.println("query price from " + url + "...");
        try {
            Thread.sleep((long) (Math.random() * 1000));
        } catch (InterruptedException e) {
        }
        return 5 + Math.random() * 20;
    }
}

上述邏輯實現的異步查詢規則實際上是:

在這裏插入圖片描述

除了 anyOf()可以實現“任意個CompletableFuture只要一個成功”allOf()可以實現“所有CompletableFuture都必須成功”,這些組合操作可以實現非常複雜的異步流程控制。

最後我們注意CompletableFuture的命名規則

  • xxx():表示該方法將繼續在已有的線程中執行;
  • xxxAsync():表示將異步在線程池中執行。

4.小結

CompletableFuture可以指定異步處理流程

  • thenAccept()處理正常結果;
  • exceptional()處理異常結果;
  • thenApplyAsync()用於串行化另一個CompletableFuture;
  • anyOf()和allOf()用於並行化多個CompletableFuture。

二十.使用ForkJoin

1.什麼是ForkJoin

Java 7開始引入了一種新的Fork/Join線程池,它可以執行一種特殊的任務:把一個大任務拆成多個小任務並行執行

我們舉個例子:如果要計算一個超大數組的和,最簡單的做法是 用一個循環在一個線程內完成
在這裏插入圖片描述

還有一種方法,可以把數組拆成兩部分,分別計算,最後加起來就是最終結果,這樣可以用兩個線程並行執行:
在這裏插入圖片描述
如果拆成兩部分還是很大,我們還可以繼續拆,用4個線程並行執行

在這裏插入圖片描述

這就是Fork/Join任務的原理:判斷一個任務是否足夠小,如果就直接計算,否則就分拆成幾個小任務分別計算。這個過程可以反覆“裂變”成一系列小任務。

2.Java中使用Fork/Join

使用Fork/Join對大數據進行並行求和:

import java.util.Random;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;

public class TestForkJoinMain {
    static Random random = new Random(0);

    public static void main(String[] args) throws Exception {
        // 創建2000個隨機數組成的數組:
        long[] array = new long[2000];
        long expectedSum = 0;
        for (int i = 0; i < array.length; i++) {
            array[i] = random();
            expectedSum += array[i];
        }
        System.out.println("Expected sum: " + expectedSum);
        // fork/join:
        ForkJoinTask<Long> task = new SumTask(array, 0, array.length);
        long startTime = System.currentTimeMillis();
        Long result = ForkJoinPool.commonPool().invoke(task);
        long endTime = System.currentTimeMillis();
        System.out.println("Fork/join sum: " + result + " in " + (endTime - startTime) + " ms.");
    }

    static long random() {
        return random.nextInt(10000);
    }
}

class SumTask extends RecursiveTask<Long> {
    static final int THRESHOLD = 500;
    private static final long serialVersionUID = 3426594844235690937L;

    long[] array;
    int start;
    int end;

    SumTask(long[] array, int start, int end) {
        this.array = array;
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        if (end - start <= THRESHOLD) {
            // 如果任務足夠小,直接計算:
            long sum = 0;
            for (int i = start; i < end; i++) {
                sum += this.array[i];
                // 故意放慢計算速度:
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                }
            }
            return sum;
        }
        // 任務太大,一分爲二:
        int middle = (end + start) / 2;
        System.out.println(String.format("split %d~%d ==> %d~%d, %d~%d", start, end, start, middle, middle, end));
        SumTask subtask1 = new SumTask(this.array, start, middle);
        SumTask subtask2 = new SumTask(this.array, middle, end);
        invokeAll(subtask1, subtask2);
        Long subresult1 = subtask1.join();
        Long subresult2 = subtask2.join();
        Long result = subresult1 + subresult2;
        System.out.println("result = " + subresult1 + " + " + subresult2 + " ==> " + result);
        return result;
    }
}

執行結果
在這裏插入圖片描述
觀察上述代碼的執行過程,一個大的計算任務0~2000首先分裂爲兩個小任務0~10001000~2000,這兩個小任務仍然太大,繼續分裂爲更小的0~500500~10001000~15001500~2000,最後,計算結果被依次合併,得到最終結果。

因此,核心代碼SumTask繼承自RecursiveTask,在compute()方法中,關鍵是如何“分裂”出子任務並且提交子任務:

class SumTask extends RecursiveTask<Long> {
    protected Long compute() {
        // “分裂”子任務:
        SumTask subtask1 = new SumTask(...);
        SumTask subtask2 = new SumTask(...);
        // invokeAll會並行運行兩個子任務:
        invokeAll(subtask1, subtask2);
        // 獲得子任務的結果:
        Long result1 = fork1.join();
        Long result2 = fork2.join();
        // 彙總結果:
        return result1 + result2;
    }
}

Fork/Join線程池在Java標準庫中就有應用。Java標準庫提供的java.util.Arrays.parallelSort(array)可以進行並行排序,它的原理就是內部通過Fork/Join對大數組分拆進行並行排序,在多核CPU上就可以大大提高排序的速度。

3.小結

  • Fork/Join是一種基於“分治”的算法:通過分解任務,並行執行,最後合併結果得到最終結果。

  • ForkJoinPool線程池可以把一個大任務分拆成小任務並行執行任務類必須繼承RecursiveTask或RecursiveAction

  • 使用Fork/Join模式可以進行並行計算以提高效率。

二十一.使用ThreadLocal

1.什麼是ThreadLocal?

多線程是Java實現多任務的基礎,Thread對象代表一個線程,我們可以在代碼中調用Thread.currentThread()獲取當前線程。例如,打印日誌時,可以同時打印出當前線程的名字:

public class Main {
    public static void main(String[] args) throws Exception {
        log("start main...");
        new Thread(() -> {
            log("run task...");
        }).start();
        new Thread(() -> {
            log("print...");
        }).start();
        log("end main.");
    }

    static void log(String s) {
        System.out.println(Thread.currentThread().getName() + ": " + s);
    }
}

對於多任務,Java標準庫提供的線程池可以方便地執行這些任務,同時複用線程。Web應用程序就是典型的多任務應用每個用戶請求頁面時,我們都會創建一個任務,類似:

public void process(User user) {
    checkPermission();
    doWork();
    saveStatus();
    sendResponse();
}

然後,通過線程池去執行這些任務。

觀察process()方法,它內部需要調用若干其他方法,同時,我們遇到一個問題:如何在一個線程內傳遞狀態?

  • process()方法需要傳遞的狀態就是User實例。有的童鞋會想,簡單地傳入User就可以了:
public void process(User user) {
    checkPermission(user);
    doWork(user);
    saveStatus(user);
    sendResponse(user);
}

但是往往一個方法又會調用其他很多方法,這樣會導致User傳遞到所有地方:

void doWork(User user) {
    queryStatus(user);
    checkStatus();
    setNewStatus(user);
    log();
}

這種在一個線程中,橫跨若干方法調用,需要傳遞的對象,我們通常稱之爲上下文(Context),它是一種狀態,可以是用戶身份任務信息等。

  • 每個方法增加一個context參數非常麻煩,而且有些時候,如果調用鏈有無法修改源碼的第三方庫,User對象就傳不進去了。

Java標準庫提供了一個特殊的 ThreadLocal它可以在一個線程中傳遞同一個對象。

2.Java中使用ThreadLocal

  • ThreadLocal實例通常總是以靜態字段初始化如下:
static ThreadLocal<String> threadLocalUser = new ThreadLocal<>();

它的典型使用方式如下:

void processUser(user) {
    try {
        threadLocalUser.set(user);
        step1();
        step2();
    } finally {
        threadLocalUser.remove();
    }
}

通過設置一個User實例關聯到ThreadLocal中, 在 移除之前所有方法都可以隨時獲取到該User實例

void step1() {
    User u = threadLocalUser.get();
    log();
    printUser();
}

void log() {
    User u = threadLocalUser.get();
    println(u.name);
}

void step2() {
    User u = threadLocalUser.get();
    checkUser(u.id);
}
  • 注意到 普通的方法調用一定是同一個線程執行的,所以,step1()step2()以及log()方法內,threadLocalUser.get()獲取的User對象是同一個實例。

實際上,可以把ThreadLocal看成一個全局Map<Thread, Object>每個線程獲取ThreadLocal變量時,總是使用Thread自身作爲key

Object threadLocalValue = threadLocalMap.get(Thread.currentThread());
  • 因此,ThreadLocal相當於給每個線程都開闢了一個獨立的存儲空間,各個線程的ThreadLocal關聯的實例互不干擾。

最後,特別注意ThreadLocal一定要在finally中清除:

try {
    threadLocalUser.set(user);
    ...
} finally {
    threadLocalUser.remove();
}
  • 這是因爲 當前線程執行完相關代碼後很可能會被重新放入線程池中,如果ThreadLocal沒有被清除,該線程執行其他代碼時,會把上一次的狀態帶進去

爲了保證能釋放ThreadLocal關聯的實例,我們可以通過AutoCloseable接口配合try-catch-resource讓編譯器自動爲我們關閉

  • 例如,一個保存了當前用戶名的ThreadLocal可以封裝爲一個UserContext對象:
public class UserContext implements AutoCloseable {

    static final ThreadLocal<String> ctx = new ThreadLocal<>();

    public UserContext(String user) {
        ctx.set(user);
    }

    public static String currentUser() {
        return ctx.get();
    }

    @Override
    public void close() {
        ctx.remove();
    }
}

使用的時候,我們藉助try (resource) {...}結構,可以這麼寫:

try (UserContext ctx = new UserContext("Bob")) {
    // 可任意調用UserContext.currentUser():
    String currentUser = UserContext.currentUser();
} // 在此自動調用UserContext.close()方法釋放ThreadLocal關聯對象

這樣就在UserContext中完全封裝了ThreadLocal,外部代碼在try (resource) {...}內部可以隨時調用UserContext.currentUser()獲取當前線程綁定的用戶名

實例代碼


import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class TestThreadLocalMain {
    public static void main(String[] args) throws Exception {
        ExecutorService es = Executors.newFixedThreadPool(3);
        String[] users = new String[]{"Bob", "Alice", "Tim", "Mike", "Lily", "Jack", "Bush"};
        for (String user : users) {
            es.submit(new Task(user));
        }
        es.awaitTermination(3, TimeUnit.SECONDS);
        es.shutdown();
    }
}

class UserContext implements AutoCloseable {
    private static final ThreadLocal<String> userThreadLocal = new ThreadLocal<>();

    public UserContext(String name) {
        userThreadLocal.set(name);
        System.out.printf("[%s] init user %s...\n", Thread.currentThread().getName(), UserContext.getCurrentUser());
    }

    public static String getCurrentUser() {
        return userThreadLocal.get();
    }

    @Override
    public void close() {
        System.out.printf("[%s] cleanup for user %s...\n", Thread.currentThread().getName(),
                UserContext.getCurrentUser());
        userThreadLocal.remove();
    }
}

class Task implements Runnable {

    final String username;

    public Task(String username) {
        this.username = username;
    }

    @Override
    public void run() {
        try (UserContext ctx = new UserContext(this.username)) {
            new Task1().process();
            new Task2().process();
            new Task3().process();
        }
    }
}

class Task1 {
    public void process() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
        System.out.printf("[%s] check user %s...\n", Thread.currentThread().getName(), UserContext.getCurrentUser());
    }
}

class Task2 {
    public void process() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
        System.out.printf("[%s] %s registered ok.\n", Thread.currentThread().getName(), UserContext.getCurrentUser());
    }
}

class Task3 {
    public void process() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
        System.out.printf("[%s] work of %s has done.\n", Thread.currentThread().getName(),
                UserContext.getCurrentUser());
    }
}

執行結果

[pool-1-thread-1] init user Bob…
[pool-1-thread-3] init user Tim…
[pool-1-thread-2] init user Alice…
[pool-1-thread-3] check user Tim…
[pool-1-thread-2] check user Alice…
[pool-1-thread-1] check user Bob…
[pool-1-thread-2] Alice registered ok.
[pool-1-thread-3] Tim registered ok.
[pool-1-thread-1] Bob registered ok.
[pool-1-thread-1] work of Bob has done.
[pool-1-thread-2] work of Alice has done.
[pool-1-thread-3] work of Tim has done.
[pool-1-thread-2] cleanup for user Alice…
[pool-1-thread-1] cleanup for user Bob…
[pool-1-thread-2] init user Mike…
[pool-1-thread-3] cleanup for user Tim…
[pool-1-thread-1] init user Lily…
[pool-1-thread-3] init user Jack…
[pool-1-thread-2] check user Mike…
[pool-1-thread-3] check user Jack…
[pool-1-thread-1] check user Lily…
[pool-1-thread-2] Mike registered ok.
[pool-1-thread-3] Jack registered ok.
[pool-1-thread-1] Lily registered ok.
[pool-1-thread-2] work of Mike has done.
[pool-1-thread-2] cleanup for user Mike…
[pool-1-thread-2] init user Bush…
[pool-1-thread-1] work of Lily has done.
[pool-1-thread-3] work of Jack has done.
[pool-1-thread-1] cleanup for user Lily…
[pool-1-thread-3] cleanup for user Jack…
[pool-1-thread-2] check user Bush…
[pool-1-thread-2] Bush registered ok.
[pool-1-thread-2] work of Bush has done.
[pool-1-thread-2] cleanup for user Bush…

3.小結

  • ThreadLocal表示線程的“局部變量”,它確保每個線程的ThreadLocal變量都是各自獨立的;

  • ThreadLocal適合在一個線程的處理流程中保持上下文(避免了同一參數在所有方法中傳遞);

  • 使用ThreadLocal要用try ... finally結構,並在finally中清除,或者通過AutoCloseable接口配合try-catch-resource ,讓編譯器自動爲我們關閉

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章