Java併發知識點快速複習手冊(上)

qqxx6661發表於2019-01-31

在這裡插入圖片描述

前言

本文快速回顧了常考的的知識點,用作面試複習,事半功倍。

面試知識點複習手冊

已釋出知識點複習手冊

參考

本文內容參考自CyC2018的Github倉庫:CS-Notes

github.com/CyC2018/CS-…

有刪減,修改,補充額外增加內容

知識共享許可協議
本作品採用知識共享署名-非商業性使用 4.0 國際許可協議進行許可。

執行緒狀態轉換

在這裡插入圖片描述

新建(New)

建立後尚未啟動。

可執行(Runnable)

可能正在執行,也可能正在等待 CPU 時間片。

包含了作業系統執行緒狀態中的 Running 和 Ready。

阻塞(Blocking)

等待獲取一個排它鎖,如果其執行緒釋放了鎖就會結束此狀態。

無限期等待(Waiting)

等待其它執行緒顯式地喚醒,否則不會被分配 CPU 時間片。

進入方法 退出方法
沒有設定 Timeout 引數的 Object.wait() 方法 Object.notify() / Object.notifyAll()
沒有設定 Timeout 引數的 Thread.join() 方法 被呼叫的執行緒執行完畢
LockSupport.park() 方法 -

限期等待(Timed Waiting)

無需等待其它執行緒顯式地喚醒,在一定時間之後會被系統自動喚醒。

呼叫 Thread.sleep() 方法使執行緒進入限期等待狀態時,常常用“使一個執行緒睡眠”進行描述。

呼叫 Object.wait() 方法使執行緒進入限期等待或者無限期等待時,常常用“掛起一個執行緒”進行描述。

睡眠和掛起是用來描述行為,而阻塞和等待用來描述狀態。

阻塞和等待的區別在於,阻塞是被動的,它是在等待獲取一個排它鎖

而等待是主動的,通過呼叫 Thread.sleep() 和 Object.wait() 等方法進入。

進入方法 退出方法
Thread.sleep() 方法 時間結束
設定了 Timeout 引數的 Object.wait() 方法 時間結束 / Object.notify() / Object.notifyAll()
設定了 Timeout 引數的 Thread.join() 方法 時間結束 / 被呼叫的執行緒執行完畢
LockSupport.parkNanos() 方法 -
LockSupport.parkUntil() 方法 -

死亡(Terminated)

可以是執行緒結束任務之後自己結束,或者產生了異常而結束。

使用執行緒

有三種使用執行緒的方法:

  • 實現 Runnable 介面
  • 實現 Callable 介面
  • 繼承 Thread 類

實現 Runnable 和 Callable 介面的類只能當做一個可以線上程中執行的任務,不是真正意義上的執行緒,因此最後還需要通過 Thread 來呼叫。可以說任務是通過執行緒驅動從而執行的。

實現 Runnable 介面

需要實現 run() 方法。

通過 Thread 呼叫 start() 方法來啟動執行緒。

public class MyRunnable implements Runnable {
    public void run() {
        // ...
    }
}
複製程式碼
public static void main(String[] args) {
    MyRunnable instance = new MyRunnable();
    Thread thread = new Thread(instance);
    thread.start();
}
複製程式碼

實現 Callable 介面

Callable就是Runnable的擴充套件。

與 Runnable 相比,Callable 可以有返回值,返回值通過 FutureTask 進行封裝。

public class MyCallable implements Callable<Integer> {
    public Integer call() {
        return 123;
    }
}
複製程式碼
public static void main(String[] args) throws ExecutionException, InterruptedException {
    MyCallable mc = new MyCallable();
    FutureTask<Integer> ft = new FutureTask<>(mc);
    Thread thread = new Thread(ft);
    thread.start();
    System.out.println(ft.get());
}
複製程式碼

繼承 Thread 類

同樣也是需要實現 run() 方法,並且最後也是呼叫 start() 方法來啟動執行緒。

public class MyThread extends Thread {
    public void run() {
        // ...
    }
}
複製程式碼
public static void main(String[] args) {
    MyThread mt = new MyThread();
    mt.start();
}
複製程式碼

其他方法

嚴格說不能算方法,只能算實現方式:

  • 匿名內部類
  • 執行緒池

實現介面 VS 繼承 Thread

實現介面會更好一些,因為:

  • Java 不支援多重繼承,因此繼承了 Thread 類就無法繼承其它類,但是可以實現多個介面;
  • 類可能只要求可執行就行,繼承整個 Thread 類開銷過大。
  • 程式碼可以被多執行緒共享,資料獨立,很容易實現資源共享

start和run有什麼區別?

詳細解釋:blog.csdn.net/lai_li/arti…

start方法:

  • 通過該方法啟動執行緒的同時也建立了一個執行緒,真正實現了多執行緒。無需等待run()方法中的程式碼執行完畢,就可以接著執行下面的程式碼

  • 此時start()的這個執行緒處於就緒狀態,當得到CPU的時間片後就會執行其中的run()方法。這個run()方法包含了要執行的這個執行緒的內容,run()方法執行結束,此執行緒也就終止了。

run方法:

  • 通過run方法啟動執行緒其實就是呼叫一個類中的方法,當作普通的方法的方式呼叫。並沒有建立一個執行緒,程式中依舊只有一個主執行緒,必須等到run()方法裡面的程式碼執行完畢,才會繼續執行下面的程式碼,這樣就沒有達到寫執行緒的目的。

執行緒程式碼示例

package cn.thread.test;
 
/*
 * 設計4個執行緒,其中兩個執行緒每次對j增加1,另外兩個執行緒對j每次減少1。寫出程式。
 */
public class ThreadTest1 {
 
	private int j;
	
	public static void main(String[] args) {
		ThreadTest1 tt = new ThreadTest1();
		Inc inc = tt.new Inc();
		Dec dec = tt.new Dec();
		
		
		Thread t1 = new Thread(inc);
		Thread t2 = new Thread(dec);
		Thread t3 = new Thread(inc);
		Thread t4 = new Thread(dec);
		t1.start();
		t2.start();
		t3.start();
		t4.start();
		
	}
	
	private synchronized void inc() {
		j++;
		System.out.println(Thread.currentThread().getName()+"inc:"+j);
	}
	
	private synchronized void dec() {
		j--;
		System.out.println(Thread.currentThread().getName()+"dec:"+j);
	}
	
	class Inc implements Runnable {
		@Override
		public void run() {
			for (int i = 0; i < 100; i++) {
				inc();
			}
		}
	}
	
	class Dec extends Thread {
		@Override
		public void run() {
			for (int i = 0; i < 100; i++) {
				dec();
			}
		}
	}
}
複製程式碼

基礎執行緒機制

Executor執行緒池

segmentfault.com/a/119000001…

Executor 管理多個非同步任務的執行,而無需程式設計師顯式地管理執行緒的生命週期。非同步是指多個任務的執行互不干擾,不需要進行同步操作。

  • 當前執行緒池大小 :表示執行緒池中實際工作者執行緒的數量;

  • 最大執行緒池大小 (maxinumPoolSize):表示執行緒池中允許存在的工作者執行緒的數量上限;

  • 核心執行緒大小 (corePoolSize ):表示一個不大於最大執行緒池大小的工作者執行緒數量上限。

如果執行的執行緒少於 corePoolSize,則 Executor 始終首選新增新的執行緒,而不進行排隊;

如果執行的執行緒等於或者多於 corePoolSize,則 Executor 始終首選將請求加入佇列,而不是新增新執行緒;

如果無法將請求加入佇列,即佇列已經滿了,則建立新的執行緒,除非建立此執行緒超出 maxinumPoolSize, 在這種情況下,任務將被拒絕。

不用執行緒池的弊端

  • 執行緒生命週期的開銷非常高。每個執行緒都有自己的生命週期,建立和銷燬執行緒所花費的時間和資源可能比處理客戶端的任務花費的時間和資源更多,並且還會有某些空閒執行緒也會佔用資源。
  • 程式的穩定性和健壯性會下降,每個請求開一個執行緒。如果受到了惡意攻擊或者請求過多(記憶體不足),程式很容易就奔潰掉了。

ThreadPoolExecutor類

實現了Executor介面,是用的最多的執行緒池,下面是已經預設實現的三種:

  • newCachedThreadPool:一個任務建立一個執行緒;

非常有彈性的執行緒池,對於新的任務,如果此時執行緒池裡沒有空閒執行緒,執行緒池會毫不猶豫的建立一條新的執行緒去處理這個任務。

public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < 5; i++) {
        executorService.execute(new MyRunnable());
    }
    executorService.shutdown();
}
複製程式碼
  • newFixedThreadPool:所有任務只能使用固定大小的執行緒;

一個固定執行緒數的執行緒池,它將返回一個corePoolSize和maximumPoolSize相等的執行緒池。

  • SingleThreadExecutor:相當於大小為 1 的 FixedThreadPool。

ThreadPoolExecutor提供了shutdown()和shutdownNow()兩個方法來關閉執行緒池

區別:

  • 呼叫shutdown()後,執行緒池狀態立刻變為SHUTDOWN,而呼叫shutdownNow(),執行緒池狀態立刻變為STOP。
  • shutdown()等待任務執行完才中斷執行緒,而shutdownNow()不等任務執行完就中斷了執行緒。

ScheduledThreadPoolExecutor類

相當於提供了延遲和週期執行功能的ThreadPoolExecutor類

Daemon 守護執行緒

守護執行緒是程式執行時在後臺提供服務的執行緒,不屬於程式中不可或缺的部分。

當所有非守護執行緒結束時,程式也就終止,同時會殺死所有守護執行緒。

main() 屬於非守護執行緒,垃圾回收是守護執行緒。

使用 setDaemon() 方法將一個執行緒設定為守護執行緒。

public static void main(String[] args) {
    Thread thread = new Thread(new MyRunnable());
    thread.setDaemon(true);
}
複製程式碼
  • 使用守護執行緒不要訪問共享資源(資料庫、檔案等),因為它可能會在任何時候就掛掉了。
  • 守護執行緒中產生的新執行緒也是守護執行緒

sleep()

Thread.sleep(millisec) 方法會休眠當前正在執行的執行緒,millisec 單位為毫秒。

sleep() 可能會丟擲 InterruptedException,因為異常不能跨執行緒傳播回 main() 中,因此必須在本地進行處理。執行緒中丟擲的其它異常也同樣需要在本地進行處理。

public void run() {
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
複製程式碼

yield()

對靜態方法 Thread.yield() 的呼叫宣告瞭當前執行緒已經完成了生命週期中最重要的部分,可以切換給其它執行緒來執行。該方法只是對執行緒排程器的一個建議,而且也只是建議具有相同優先順序的其它執行緒可以執行。

public void run() {
    Thread.yield();
}
複製程式碼

中斷

一個執行緒執行完畢之後會自動結束,如果在執行過程中發生異常也會提前結束。

現在已經沒有強制執行緒終止的方法了!

Stop方法太暴力了,不安全,所以被設定過時了。

interrupt():報出InterruptedException

segmentfault.com/a/119000001…

要注意的是:interrupt不會真正停止一個執行緒,它僅僅是給這個執行緒發了一個訊號告訴它,它應該要結束了(明白這一點非常重要!)

呼叫interrupt()並不是要真正終止掉當前執行緒,僅僅是設定了一箇中斷標誌。這個中斷標誌可以給我們用來判斷什麼時候該幹什麼活!什麼時候中斷由我們自己來決定,這樣就可以安全地終止執行緒了!

通過呼叫一個執行緒的 interrupt() 來中斷該執行緒,可以中斷處於:

  • 阻塞
  • 限期等待
  • 無限期等待狀態

那麼就會丟擲 InterruptedException,從而提前結束該執行緒。

但是不能中斷 I/O 阻塞和 synchronized 鎖阻塞。

對於以下程式碼,在 main() 中啟動一個執行緒之後再中斷它,由於執行緒中呼叫了 Thread.sleep() 方法,因此會丟擲一個 InterruptedException,從而提前結束執行緒,不執行之後的語句。

public class InterruptExample {

    private static class MyThread1 extends Thread {
        @Override
        public void run() {
            try {
                Thread.sleep(2000);
                System.out.println("Thread run");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
複製程式碼
public static void main(String[] args) throws InterruptedException {
    Thread thread1 = new MyThread1();
    thread1.start();
    thread1.interrupt();
    System.out.println("Main run");
}
複製程式碼
Main run
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at InterruptExample.lambda$main$0(InterruptExample.java:5)
    at InterruptExample$$Lambda$1/713338599.run(Unknown Source)
    at java.lang.Thread.run(Thread.java:745)
複製程式碼

interrupted()和isInterrupted()

interrupt執行緒中斷還有另外兩個方法(檢查該執行緒是否被中斷):

  • 靜態方法interrupted()-->會清除中斷標誌位
  • 例項方法isInterrupted()-->不會清除中斷標誌位

如果一個執行緒的 run() 方法執行一個無限迴圈(不屬於阻塞、限期等待、非限期等待),例如while(True),並且沒有執行 sleep() 等會丟擲 InterruptedException 的操作,那麼呼叫執行緒的 interrupt() 方法就無法使執行緒提前結束

然而,

但是呼叫 interrupt() 方法會設定執行緒的中斷標記,此時呼叫 interrupted() 方法會返回 true。因此可以在迴圈體中使用 interrupted() 方法來判斷執行緒是否處於中斷狀態,從而提前結束執行緒。

Thread t1 = new Thread( new Runnable(){
    public void run(){
        // 若未發生中斷,就正常執行任務
        while(!Thread.currentThread.isInterrupted()){
            // 正常任務程式碼……
        }
        // 中斷的處理程式碼……
        doSomething();
    }
} ).start();
複製程式碼

Executor執行緒池的中斷操作

  • 呼叫 Executor 的 shutdown() 方法會等待執行緒都執行完畢之後再關閉
  • 但是如果呼叫的是 shutdownNow() 方法,則相當於呼叫每個執行緒的 interrupt() 方法。

以下使用 Lambda 建立執行緒,相當於建立了一個匿名內部執行緒。

public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> {
        try {
            Thread.sleep(2000);
            System.out.println("Thread run");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    executorService.shutdownNow();
    System.out.println("Main run");
}
複製程式碼
Main run
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at ExecutorInterruptExample.lambda$main$0(ExecutorInterruptExample.java:9)
    at ExecutorInterruptExample$$Lambda$1/1160460865.run(Unknown Source)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)
複製程式碼

如果只想中斷 Executor 中的一個執行緒,可以通過使用 submit() 方法來提交一個執行緒,它會返回一個 Future<?> 物件,通過呼叫該物件的 cancel(true) 方法就可以中斷執行緒。

Future<?> future = executorService.submit(() -> {
    // ..
});
future.cancel(true);
複製程式碼

互斥同步

  • JVM 實現的 synchronized
  • JDK 實現的 ReentrantLock。

可重入與不可重入鎖

blog.csdn.net/u012545728/…

不可重入鎖

所謂不可重入鎖,即若當前執行緒執行某個方法已經獲取了該鎖,那麼在方法中嘗試再次獲取鎖時,就會獲取不到被阻塞。

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()方法,第一個執行緒呼叫print()方法獲取鎖,進入lock()方法,由於初始lockedBy是null,所以不會進入while而掛起當前執行緒,而是是增量lockedCount並記錄lockBy為第一個執行緒。接著第一個執行緒進入doAdd()方法,由於同一程式,所以不會進入while而掛起,接著增量lockedCount,當第二個執行緒嘗試lock,由於isLocked=true,所以他不會獲取該鎖,直到第一個執行緒呼叫兩次unlock()將lockCount遞減為0,才將標記為isLocked設定為false。

可重入鎖的概念和設計思想大體如此,Java中的可重入鎖ReentrantLock設計思路也是這樣

synchronized和ReentrantLock都是可重入鎖

synchronized

1. 同步一個程式碼塊

public void func () {
    synchronized (this) {
        // ...
    }
}
複製程式碼

它只作用於同一個物件,如果呼叫兩個不同物件上的同步程式碼塊,就不會進行同步。

2. 同步一個方法

public synchronized void func () {
    // ...
}
複製程式碼

它和同步程式碼塊一樣,只作用於同一個物件。

3. 同步一個類

public void func() {
    synchronized (SynchronizedExample.class) {
        // ...
    }
}
複製程式碼

作用於整個類,也就是說兩個執行緒呼叫同一個類的不同物件上的這種同步語句,也需要進行同步。

4. 同步一個靜態方法

public synchronized static void fun() {
    // ...
}
複製程式碼

作用於整個類。

釋放鎖的時機

  • 當方法(程式碼塊)執行完畢後會自動釋放鎖,不需要做任何的操作。

  • 當一個執行緒執行的程式碼出現異常時,其所持有的鎖會自動釋放。

Lock

有ReentrantLock和ReentrantReadWriteLock,後者分為讀鎖和寫鎖,讀鎖允許併發訪問共享資源。

public class LockExample {

    private Lock lock = new ReentrantLock();

    public void func() {
        lock.lock();
        try {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        } finally {
            lock.unlock(); // 確保釋放鎖,從而避免發生死鎖。
        }
    }
}
複製程式碼
public static void main(String[] args) {
    LockExample lockExample = new LockExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> lockExample.func());
    executorService.execute(() -> lockExample.func());
}
複製程式碼
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
複製程式碼

ReentrantLock 是 java.util.concurrent(J.U.C)包中的鎖,相比於 synchronized,它多了以下高階功能:

1. 等待可中斷

當持有鎖的執行緒長期不釋放鎖的時候,正在等待的執行緒可以選擇放棄等待,改為處理其他事情,可中斷特性對處理執行時間非常長的同步塊很有幫助。

2. 可實現公平鎖

公平鎖是指多個執行緒在等待同一個鎖時,必須按照申請鎖的時間順序來依次獲得鎖;而非公平鎖則不保證這一點,在鎖被釋放時,任何一個等待鎖的執行緒都有機會獲得鎖。

  • synchronized 中的鎖是非公平的
  • ReentrantLock 預設情況下也是非公平的,但可以通過帶布林值的建構函式要求使用公平鎖。

3. 鎖繫結多個條件

  • synchronized 中,鎖物件的 wait() 和 notify() 或 notifyAll() 方法可以實現一個隱含的條件,如果要和多於一個的條件關聯的時候,就不得不額外地新增一個鎖
  • 而 ReentrantLock 則無須這樣做,只需要多次呼叫 newCondition() 方法即可。

ReentrantReadWriteLock

我們知道synchronized內建鎖和ReentrantLock都是互斥鎖(一次只能有一個執行緒進入到臨界區(被鎖定的區域)

ReentrantReadWriteLock優點:

  • 讀取資料的時候,可以多個執行緒同時進入到到臨界區(被鎖定的區域)
  • 在寫資料的時候,無論是讀執行緒還是寫執行緒都是互斥的
  • 如果讀的執行緒比寫的執行緒要多很多的話,那可以考慮使用它。它使用state的變數高16位是讀鎖,低16位是寫鎖
  • 寫鎖可以降級為讀鎖,讀鎖不能升級為寫鎖

synchronized 和 ReentrantLock 比較

1. 鎖的實現

synchronized 是 JVM 實現的,而 ReentrantLock 是 JDK 實現的。

2. 效能

新版本 Java 對 synchronized 進行了很多優化,例如自旋鎖等,synchronized 與 ReentrantLock 大致相同。

3. 等待可中斷

ReentrantLock 可中斷,而 synchronized 不行。

4. 公平鎖

  • 公平鎖能保證:老的執行緒(234)排隊使用鎖,新執行緒仍然排隊使用鎖(2345)。
  • 非公平鎖保證:老的執行緒(234)排隊使用鎖;但是無法保證新執行緒5搶佔已經在排隊的執行緒的鎖(正好在1釋放鎖的時候搶佔到了鎖,沒有進入排隊佇列)

synchronized 中的鎖是非公平的,ReentrantLock 預設情況下也是非公平的,但是也可以是公平的。

5. 鎖繫結多個條件

一個 ReentrantLock 可以同時繫結多個 Condition 物件。

使用選擇

除非需要使用 ReentrantLock 的高階功能,否則優先使用 synchronized。

  • synchronized好用,簡單,效能不差
  • 沒有使用到Lock顯式鎖的特性就不要使用Lock鎖了。
  • synchronized 是 JVM 實現的一種鎖機制,JVM 原生地支援它,而 ReentrantLock 不是所有的 JDK 版本都支援。
  • 並且使用 synchronized 不用擔心沒有釋放鎖而導致死鎖問題,因為JVM 會確保鎖的釋放

執行緒之間的協作

當多個執行緒可以一起工作去解決某個問題時,如果某些部分必須在其它部分之前完成,那麼就需要對執行緒進行協調。

join()

線上程中呼叫另一個執行緒的 join() 方法,會將當前執行緒掛起,而不是忙等待, 直到目標執行緒結束。

對於以下程式碼,雖然 b 執行緒先啟動,但是因為在 b 執行緒中呼叫了 a 執行緒的 join() 方法,因此 b 執行緒會等待 a 執行緒結束才繼續執行,因此最後能夠保證 a 執行緒的輸出先與 b 執行緒的輸出。

wait() notify() notifyAll()

呼叫 wait() 使得執行緒等待某個條件滿足,執行緒在等待時會被掛起,當其他執行緒的執行使得這個條件滿足時,其它執行緒會呼叫 notify() 或者 notifyAll() 來喚醒掛起的執行緒。

它們都屬於 Object 的一部分,而不屬於 Thread。

只能用在同步方法或者同步控制塊中使用,否則會在執行時丟擲 IllegalMonitorStateExeception。

使用 wait() 掛起期間,執行緒會釋放鎖。這是因為,如果沒有釋放鎖,那麼其它執行緒就無法進入物件的同步方法或者同步控制塊中,那麼就無法執行 notify() 或者 notifyAll() 來喚醒掛起的執行緒,造成死鎖。

wait() 和 sleep() 的區別

  1. wait() 是 Object 的方法,而 sleep() 是 Thread 的靜態方法;
  2. wait() 會釋放鎖,sleep() 不會。

await() signal() signalAll()

java.util.concurrent 類庫中提供了 Condition 類來實現執行緒之間的協調,可以在 Condition 上呼叫 await() 方法使執行緒等待,其它執行緒呼叫 signal() 或 signalAll() 方法喚醒等待的執行緒。

相比於 wait() 這種等待方式,await() 可以指定等待的條件,因此更加靈活。

使用 Lock 來獲取一個 Condition 物件。

public class AwaitSignalExample {
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void before() {
        lock.lock();
        try {
            System.out.println("before");
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public void after() {
        lock.lock();
        try {
            condition.await();
            System.out.println("after");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
複製程式碼
public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    AwaitSignalExample example = new AwaitSignalExample();
    executorService.execute(() -> example.after());
    executorService.execute(() -> example.before());
}
複製程式碼
before
after
複製程式碼

J.U.C - AQS

從整體來看,concurrent包的實現示意圖如下:

在這裡插入圖片描述

segmentfault.com/a/119000001…

java.util.concurrent(J.U.C)大大提高了併發效能,AQS 被認為是 J.U.C 的核心。

AbstractQueuedSynchronizer簡稱為AQS:AQS定義了一套多執行緒訪問共享資源的同步器框架,許多同步類實現都依賴於它,我們Lock之類的兩個常見的鎖都是基於它來實現的。

  • AQS可以給我們實現鎖的框架
  • 內部實現的關鍵是:先進先出的佇列、state狀態
  • 定義了內部類ConditionObject
  • 擁有兩種執行緒模式:
    • 獨佔模式
    • 共享模式
  • 在LOCK包中的相關鎖(常用的有ReentrantLock、 ReadWriteLock)都是基於AQS來構建
  • 一般我們叫AQS為同步器

AQS實現特點

  • 同步狀態
    • 使用volatile修飾實現執行緒可見性
    • 修改state狀態值時使用CAS演算法來實現
  • 先進先出佇列

CountdownLatch

維護了一個計數器 cnt,每次呼叫 countDown() 方法會讓計數器的值減 1,減到 0 的時候,那些因為呼叫 await() 方法而在等待的執行緒就會被喚醒。

使用說明:

  • count初始化CountDownLatch,然後需要等待的執行緒呼叫await方法。await方法會一直受阻塞直到count=0。
  • 而其它執行緒完成自己的操作後,呼叫countDown()使計數器count減1。
  • 當count減到0時,所有在等待的執行緒均會被釋放

說白了就是通過count變數來控制等待,如果count值為0了(其他執行緒的任務都完成了),那就可以繼續執行。

在這裡插入圖片描述

public class CountdownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        final int totalThread = 10;
        CountDownLatch countDownLatch = new CountDownLatch(totalThread);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < totalThread; i++) {
            executorService.execute(() -> {
                System.out.print("run..");
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        System.out.println("end");
        executorService.shutdown();
    }
}

run..run..run..run..run..run..run..run..run..run..end
複製程式碼

CyclicBarrier

CyclicBarrier 的字面意思是可迴圈使用(Cyclic)的屏障(Barrier)。它要做的事情是,讓一組執行緒到達一個屏障(也可以叫同步點)時被阻塞,直到最後一個執行緒到達屏障時,屏障才會開門,所有被屏障攔截的執行緒才會繼續幹活。

和 CountdownLatch相似,都是通過維護計數器來實現的。但是它的計數器是遞增的,每次執行 await() 方法之後,計數器會加 1,直到計數器的值和設定的值相等,等待的所有執行緒才會繼續執行。

CyclicBarrier可以被重用(對比於CountDownLatch是不能重用的),CyclicBarrier 的計數器通過呼叫 reset() 方法可以迴圈使用,所以它才叫做迴圈屏障。

在這裡插入圖片描述

public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) throw new IllegalArgumentException();
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
}

public CyclicBarrier(int parties) {
    this(parties, null);
}
複製程式碼
public class CyclicBarrierExample {
    public static void main(String[] args) {
        final int totalThread = 10;
        CyclicBarrier cyclicBarrier = new CyclicBarrier(totalThread);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < totalThread; i++) {
            executorService.execute(() -> {
                System.out.print("before..");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
                System.out.print("after..");
            });
        }
        executorService.shutdown();
    }
}
before..before..before..before..before..before..before..before..before..before..after..after..after..after..after..after..after..after..after..after..
複製程式碼

Semaphore

Semaphore 就是作業系統中的訊號量,可以控制對互斥資源的訪問執行緒數。

  • 當呼叫acquire()方法時,會消費一個許可證。如果沒有許可證了,會阻塞起來
  • 當呼叫release()方法時,會新增一個許可證。
  • 這些"許可證"的個數其實就是一個count變數罷了~

在這裡插入圖片描述

public class SemaphoreExample {
    public static void main(String[] args) {
        final int clientCount = 3;
        final int totalRequestCount = 10;
        Semaphore semaphore = new Semaphore(clientCount);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < totalRequestCount; i++) {
            executorService.execute(()->{
                try {
                    semaphore.acquire();
                    System.out.print(semaphore.availablePermits() + " ");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                }
            });
        }
        executorService.shutdown();
    }
}
複製程式碼

J.U.C - 其它元件

FutureTask

在介紹 Callable 時我們知道它可以有返回值,返回值通過 Future 進行封裝。

FutureTask 實現了 RunnableFuture 介面,該介面繼承自 Runnable 和 Future 介面,這使得 FutureTask 既可以當做一個任務執行,也可以有返回值。

public class FutureTask<V> implements RunnableFuture<V>
複製程式碼
public interface RunnableFuture<V> extends Runnable, Future<V>
複製程式碼

當一個計算任務需要執行很長時間,那麼就可以用 FutureTask 來封裝這個任務,用一個執行緒去執行該任務,然後其它執行緒繼續執行其它任務。當需要該任務的計算結果時,再通過 FutureTask 的 get() 方法獲取。

public class FutureTaskExample {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> futureTask = new FutureTask<Integer>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int result = 0;
                for (int i = 0; i < 100; i++) {
                    Thread.sleep(10);
                    result += i;
                }
                return result;
            }
        });

        Thread computeThread = new Thread(futureTask);
        computeThread.start();

        Thread otherThread = new Thread(() -> {
            System.out.println("other task is running...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        otherThread.start();
        System.out.println(futureTask.get());
    }
}
複製程式碼
other task is running...
4950
複製程式碼

BlockingQueue

java.util.concurrent.BlockingQueue 介面有以下阻塞佇列的實現:

  • FIFO 佇列 :LinkedBlockingQueue、ArrayListBlockingQueue(固定長度)
  • 優先順序佇列(每個元素都有優先順序) :PriorityBlockingQueue

提供了阻塞的 take() 和 put() 方法:如果佇列為空 take() 將阻塞,直到佇列中有內容;如果佇列為滿 put() 將阻塞,指到佇列有空閒位置。

使用 BlockingQueue 實現生產者消費者問題

public class ProducerConsumer {

    private static BlockingQueue<String> queue = new ArrayBlockingQueue<>(5);

    private static class Producer extends Thread {
        @Override
        public void run() {
            try {
                queue.put("product");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.print("produce..");
        }
    }

    private static class Consumer extends Thread {

        @Override
        public void run() {
            try {
                String product = queue.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.print("consume..");
        }
    }
}
複製程式碼
public static void main(String[] args) {
    for (int i = 0; i < 2; i++) {
        Producer producer = new Producer();
        producer.start();
    }
    for (int i = 0; i < 5; i++) {
        Consumer consumer = new Consumer();
        consumer.start();
    }
    for (int i = 0; i < 3; i++) {
        Producer producer = new Producer();
        producer.start();
    }
}
複製程式碼
produce..produce..consume..consume..produce..consume..produce..consume..produce..consume..
複製程式碼

ArrayBlockingQueue, LinkedBlockingQueue, ConcurrentLinkedQueue

都是執行緒安全的,不然叫什麼併發類呢

ArrayBlockingQueue, LinkedBlockingQueue 繼承自 BlockingQueue, 他們的特點就是 Blocking, Blocking 特有的方法就是 take() 和 put(), 這兩個方法是阻塞方法, 每當佇列容量滿的時候, put() 方法就會進入wait, 直到佇列空出來, 而每當佇列為空時, take() 就會進入等待, 直到佇列有元素可以 take()

ArrayBlockingQueue, LinkedBlockingQueue 區別在於:

連結串列和陣列性質決定的

  • ArrayBlockingQueue 必須指定容量,
  • 公平讀取: ArrayBlockingQueue可以指定 fair 變數, 如果 fair 為 true, 則會保持 take() 或者 put() 操作時執行緒的 block 順序, 先 block 的執行緒先 take() 或 put(), fair 由內部變數 ReentrantLock 保證

ConcurrentLinkedQueue 通過 CAS 操作實現了無鎖的 poll() 和 offer(),

  • 他的容量是動態的,

  • 由於無鎖, 所以在 poll() 或者 offer() 的時候 head 與 tail 可能會改變,所以它會持續的判斷 head 與 tail 是否改變來保證操作正確性, 如果改變, 則會重新選擇 head 與 tail.

  • 而由於無鎖的特性, 他的元素更新與 size 變數更新無法做到原子 (實際上它沒有 size 變數), 所以他的 size() 是通過遍歷 queue 來獲得的, 在效率上是 O(n), 而且無法保證準確性, 因為遍歷的時候有可能 queue size 發生了改變。

ForkJoin

除了ScheduledThreadPoolExecutor和ThreadPoolExecutor類執行緒池以外,還有一個是JDK1.7新增的執行緒池:ForkJoinPool執行緒池

主要用於平行計算中,和 MapReduce 原理類似,都是把大的計算任務拆分成多個小任務平行計算。

public class ForkJoinExample extends RecursiveTask<Integer> {
    private final int threhold = 5;
    private int first;
    private int last;

    public ForkJoinExample(int first, int last) {
        this.first = first;
        this.last = last;
    }

    @Override
    protected Integer compute() {
        int result = 0;
        if (last - first <= threhold) {
            // 任務足夠小則直接計算
            for (int i = first; i <= last; i++) {
                result += i;
            }
        } else {
            // 拆分成小任務
            int middle = first + (last - first) / 2;
            ForkJoinExample leftTask = new ForkJoinExample(first, middle);
            ForkJoinExample rightTask = new ForkJoinExample(middle + 1, last);
            leftTask.fork();
            rightTask.fork();
            result = leftTask.join() + rightTask.join();
        }
        return result;
    }
}
複製程式碼
public static void main(String[] args) throws ExecutionException, InterruptedException {
    ForkJoinExample example = new ForkJoinExample(1, 10000);
    ForkJoinPool forkJoinPool = new ForkJoinPool();
    Future result = forkJoinPool.submit(example);
    System.out.println(result.get());
}
複製程式碼

ForkJoin 使用 ForkJoinPool 來啟動,它是一個特殊的執行緒池,執行緒數量取決於 CPU 核數。

public class ForkJoinPool extends AbstractExecutorService
複製程式碼

ForkJoinPool 實現了工作竊取演算法來提高 CPU 的利用率。每個執行緒都維護了一個雙端佇列,用來儲存需要執行的任務。工作竊取演算法允許空閒的執行緒從其它執行緒的雙端佇列中竊取一個任務來執行。竊取的任務必須是最晚的任務,避免和佇列所屬執行緒發生競爭。例如下圖中,Thread2 從 Thread1 的佇列中拿出最晚的 Task1 任務,Thread1 會拿出 Task2 來執行,這樣就避免發生競爭。但是如果佇列中只有一個任務時還是會發生競爭。

在這裡插入圖片描述

關注我

我是蠻三刀把刀,目前為後臺開發工程師。主要關注後臺開發,網路安全,Python爬蟲等技術。

來微信和我聊聊:yangzd1102

Github:github.com/qqxx6661

原創部落格主要內容

  • 筆試面試複習知識點手冊
  • Leetcode演算法題解析(前150題)
  • 劍指offer演算法題解析
  • Python爬蟲相關實戰
  • 後臺開發相關實戰

同步更新以下部落格

1. Csdn

blog.csdn.net/qqxx6661

擁有專欄:Leetcode題解(Java/Python)、Python爬蟲開發

2. 知乎

www.zhihu.com/people/yang…

擁有專欄:碼農面試助攻手冊

3. 掘金

juejin.im/user/5b4801…

4. 簡書

www.jianshu.com/u/b5f225ca2…

個人專案:電商價格監控網站

本人長期維護的個人專案,完全免費,請大家多多支援。

實現功能

  • 京東商品監控:設定商品ID和預期價格,當商品價格【低於】設定的預期價格後自動傳送郵件提醒使用者。(一小時以內)
  • 京東品類商品監控:使用者訂閱特定品類後,該類降價幅度大於7折的【自營商品】會被選出併傳送郵件提醒使用者。
  • 品類商品瀏覽,商品歷史價格曲線,商品歷史最高最低價
  • 持續更新中...

網站地址

pricemonitor.online/

個人公眾號:Rude3Knife

個人公眾號:Rude3Knife

如果文章對你有幫助,不妨收藏起來並轉發給您的朋友們~

相關文章