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

前言

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

面試知識點複習手冊

全複習手冊文章導航git

點擊公衆號下方技術推文——面試衝刺程序員

已發佈知識點複習手冊github

Java基礎知識點面試手冊(上)面試

Java基礎知識點面試手冊(下)算法

Java容器(List、Set、Map)知識點快速複習手冊(上)數據庫

Java容器(List、Set、Map)知識點快速複習手冊(中)segmentfault

Java容器(List、Set、Map)知識點快速複習手冊(下)數組

Redis基礎知識點快速複習手冊(上)安全

Redis基礎知識點快速複習手冊(下)

雙非碩士的春招秋招經驗總結——對校招,複習以及面試心態的理解

參考

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

https://github.com/CyC2018/CS-Notes/

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

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

線程狀態轉換

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

新建(New)

建立後還沒有啓動。

可運行(Runnable)

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

包含了操做系統線程狀態中的 Running 和 Ready。

阻塞(Blocking)

等待獲取一個排它鎖,若是其線程釋放了鎖就會結束此狀態。

無限期等待(Waiting)

等待其它線程顯式地喚醒,不然不會被分配 CPU 時間片。

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

限期等待(Timed Waiting)

無需等待其它線程顯式地喚醒,在必定時間以後會被系統自動喚醒。

調用 Thread.sleep() 方法使線程進入限期等待狀態時,經常用「使一個線程睡眠」進行描述。

調用 Object.wait() 方法使線程進入限期等待或者無限期等待時,經常用「掛起一個線程」進行描述。

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

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

而等待是主動的,經過調用 Thread.sleep() 和 Object.wait() 等方法進入。

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

死亡(Terminated)

能夠是線程結束任務以後本身結束,或者產生了異常而結束。

使用線程

有三種使用線程的方法:

  • 實現 Runnable 接口
  • 實現 Callable 接口
  • 繼承 Thread 類

實現 Runnable 和 Callable 接口的類只能當作一個能夠在線程中運行的任務,不是真正意義上的線程,所以最後還須要經過 Thread 來調用。能夠說任務是經過線程驅動從而執行的。

實現 Runnable 接口

須要實現 run() 方法。

經過 Thread 調用 start() 方法來啓動線程。

1public class MyRunnable implements Runnable {
2    public void run() {
3        // ...
4    }
5}
1public static void main(String[] args) {
2    MyRunnable instance = new MyRunnable();
3    Thread thread = new Thread(instance);
4    thread.start();
5}

實現 Callable 接口

Callable就是Runnable的擴展。

與 Runnable 相比,Callable 能夠有返回值,返回值經過 FutureTask 進行封裝。

1public class MyCallable implements Callable<Integer> {
2    public Integer call() {
3        return 123;
4    }
5}
1public static void main(String[] args) throws ExecutionException, InterruptedException {
2    MyCallable mc = new MyCallable();
3    FutureTask<Integer> ft = new FutureTask<>(mc);
4    Thread thread = new Thread(ft);
5    thread.start();
6    System.out.println(ft.get());
7}

繼承 Thread 類

一樣也是須要實現 run() 方法,而且最後也是調用 start() 方法來啓動線程。

1public class MyThread extends Thread {
2    public void run() {
3        // ...
4    }
5}
1public static void main(String[] args) {
2    MyThread mt = new MyThread();
3    mt.start();
4}

其餘方法

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

  • 匿名內部類
  • 線程池

實現接口 VS 繼承 Thread

實現接口會更好一些,由於:

  • Java 不支持多重繼承,所以繼承了 Thread 類就沒法繼承其它類,可是能夠實現多個接口;
    類可能只要求可執行就行,繼承整個 Thread 類開銷過大。
    代碼能夠被多線程共享,數據獨立,很容易實現資源共享

start和run有什麼區別?

詳細解釋:https://blog.csdn.net/lai_li/article/details/53070141?locationNum=13&fps=1

start方法:

  • 經過該方法啓動線程的同時也建立了一個線程,真正實現了多線程。無需等待run()方法中的代碼執行完畢,就能夠接着執行下面的代碼。

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

run方法:

  • 經過run方法啓動線程其實就是調用一個類中的方法,看成普通的方法的方式調用。並無建立一個線程,程序中依舊只有一個主線程,必須等到run()方法裏面的代碼執行完畢,纔會繼續執行下面的代碼,這樣就沒有達到寫線程的目的。

線程代碼示例

1package cn.thread.test;
 2
 3/*
 4 * 設計4個線程,其中兩個線程每次對j增長1,另外兩個線程對j每次減小1。寫出程序。
 5 */
 6public class ThreadTest1 {
 7
 8    private int j;
 9
10    public static void main(String[] args) {
11        ThreadTest1 tt = new ThreadTest1();
12        Inc inc = tt.new Inc();
13        Dec dec = tt.new Dec();
14
15
16        Thread t1 = new Thread(inc);
17        Thread t2 = new Thread(dec);
18        Thread t3 = new Thread(inc);
19        Thread t4 = new Thread(dec);
20        t1.start();
21        t2.start();
22        t3.start();
23        t4.start();
24
25    }
26
27    private synchronized void inc() {
28        j++;
29        System.out.println(Thread.currentThread().getName()+"inc:"+j);
30    }
31
32    private synchronized void dec() {
33        j--;
34        System.out.println(Thread.currentThread().getName()+"dec:"+j);
35    }
36
37    class Inc implements Runnable {
38        @Override
39        public void run() {
40            for (int i = 0; i < 100; i++) {
41                inc();
42            }
43        }
44    }
45
46    class Dec extends Thread {
47        @Override
48        public void run() {
49            for (int i = 0; i < 100; i++) {
50                dec();
51            }
52        }
53    }
54}

基礎線程機制

Executor線程池

http://www.javashuo.com/article/p-gmlygbhe-mw.html

Executor 管理多個異步任務的執行,而無需程序員顯式地管理線程的生命週期。異步是指多個任務的執行互不干擾,不須要進行同步操做。

  • 當前線程池大小 :表示線程池中實際工做者線程的數量;

  • 最大線程池大小 (maxinumPoolSize):表示線程池中容許存在的工做者線程的數量上限;

  • 核心線程大小 (corePoolSize ):表示一個不大於最大線程池大小的工做者線程數量上限。

若是運行的線程少於 corePoolSize,則 Executor 始終首選添加新的線程,而不進行排隊;

若是運行的線程等於或者多於 corePoolSize,則 Executor 始終首選將請求加入隊列,而不是添加新線程;

若是沒法將請求加入隊列,即隊列已經滿了,則建立新的線程,除非建立此線程超出 maxinumPoolSize, 在這種狀況下,任務將被拒絕。

不用線程池的弊端

  • 線程生命週期的開銷很是高。每一個線程都有本身的生命週期,建立和銷燬線程所花費的時間和資源可能比處理客戶端的任務花費的時間和資源更多,而且還會有某些空閒線程也會佔用資源。
  • 程序的穩定性和健壯性會降低,每一個請求開一個線程。若是受到了惡意***或者請求過多(內存不足),程序很容易就奔潰掉了。

ThreadPoolExecutor類

實現了Executor接口,是用的最多的線程池,下面是已經默認實現的三種:

  • newCachedThreadPool:一個任務建立一個線程;

很是有彈性的線程池,對於新的任務,若是此時線程池裏沒有空閒線程,線程池會絕不猶豫的建立一條新的線程去處理這個任務。

1public static void main(String[] args) {
2    ExecutorService executorService = Executors.newCachedThreadPool();
3    for (int i = 0; i < 5; i++) {
4        executorService.execute(new MyRunnable());
5    }
6    executorService.shutdown();
7}
  • newFixedThreadPool:全部任務只能使用固定大小的線程;

一個固定線程數的線程池,它將返回一個corePoolSize和maximumPoolSize相等的線程池。

  • SingleThreadExecutor:至關於大小爲 1 的 FixedThreadPool。
    ThreadPoolExecutor提供了shutdown()和shutdownNow()兩個方法來關閉線程池

區別:

  • 調用shutdown()後,線程池狀態馬上變爲SHUTDOWN,而調用shutdownNow(),線程池狀態馬上變爲STOP。
  • shutdown()等待任務執行完才中斷線程,而shutdownNow()不等任務執行完就中斷了線程。
    ScheduledThreadPoolExecutor類

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

Daemon 守護線程

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

當全部非守護線程結束時,程序也就終止,同時會殺死全部守護線程。

main() 屬於非守護線程,垃圾回收是守護線程。

使用 setDaemon() 方法將一個線程設置爲守護線程。

1public static void main(String[] args) {
2    Thread thread = new Thread(new MyRunnable());
3    thread.setDaemon(true);
4}
  • 使用守護線程不要訪問共享資源(數據庫、文件等),由於它可能會在任什麼時候候就掛掉了。
  • 守護線程中產生的新線程也是守護線程

sleep()

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

sleep() 可能會拋出 InterruptedException,由於異常不能跨線程傳播回 main() 中,所以必須在本地進行處理。線程中拋出的其它異常也一樣須要在本地進行處理。

1public void run() {
2    try {
3        Thread.sleep(3000);
4    } catch (InterruptedException e) {
5        e.printStackTrace();
6    }
7}

yield()

對靜態方法 Thread.yield() 的調用聲明瞭當前線程已經完成了生命週期中最重要的部分,能夠切換給其它線程來執行。該方法只是對線程調度器的一個建議,並且也只是建議具備相同優先級的其它線程能夠運行。

1public void run() {
2    Thread.yield();
3}

中斷

一個線程執行完畢以後會自動結束,若是在運行過程當中發生異常也會提早結束。

如今已經沒有強制線程終止的方法了!。

Stop方法太暴力了,不安全,因此被設置過期了。

interrupt():報出InterruptedException

http://www.javashuo.com/article/p-hwobvksy-nc.html

要注意的是:interrupt不會真正中止一個線程,它僅僅是給這個線程發了一個信號告訴它,它應該要結束了(明白這一點很是重要!)

調用interrupt()並非要真正終止掉當前線程,僅僅是設置了一箇中斷標誌。這個中斷標誌能夠給咱們用來判斷何時該幹什麼活!何時中斷由咱們本身來決定,這樣就能夠安全地終止線程了!

經過調用一個線程的 interrupt() 來中斷該線程,能夠中斷處於:

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

那麼就會拋出 InterruptedException,從而提早結束該線程。

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

對於如下代碼,在 main() 中啓動一個線程以後再中斷它,因爲線程中調用了 Thread.sleep() 方法,所以會拋出一個 InterruptedException,從而提早結束線程,不執行以後的語句。

1public class InterruptExample {
 2
 3    private static class MyThread1 extends Thread {
 4        @Override
 5        public void run() {
 6            try {
 7                Thread.sleep(2000);
 8                System.out.println("Thread run");
 9            } catch (InterruptedException e) {
10                e.printStackTrace();
11            }
12        }
13    }
14}
1public static void main(String[] args) throws InterruptedException {
2    Thread thread1 = new MyThread1();
3    thread1.start();
4    thread1.interrupt();
5    System.out.println("Main run");
6}
1Main run
2java.lang.InterruptedException: sleep interrupted
3    at java.lang.Thread.sleep(Native Method)
4    at InterruptExample.lambda$main$0(InterruptExample.java:5)
5    at InterruptExample$$Lambda$1/713338599.run(Unknown Source)
6    at java.lang.Thread.run(Thread.java:745)

interrupted()和isInterrupted()

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

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

若是一個線程的 run() 方法執行一個無限循環(不屬於阻塞、限期等待、非限期等待),例如while(True),而且沒有執行 sleep() 等會拋出 InterruptedException 的操做,那麼調用線程的 interrupt() 方法就沒法使線程提早結束。

然而,

可是調用 interrupt() 方法會設置線程的中斷標記,此時調用 interrupted() 方法會返回 true。所以能夠在循環體中使用 interrupted() 方法來判斷線程是否處於中斷狀態,從而提早結束線程。

1Thread t1 = new Thread( new Runnable(){
 2    public void run(){
 3        // 若未發生中斷,就正常執行任務
 4        while(!Thread.currentThread.isInterrupted()){
 5            // 正常任務代碼……
 6        }
 7        // 中斷的處理代碼……
 8        doSomething();
 9    }
10} ).start();

Executor線程池的中斷操做

  • 調用 Executor 的 shutdown() 方法會等待線程都執行完畢以後再關閉
  • 可是若是調用的是 shutdownNow() 方法,則至關於調用每一個線程的 interrupt() 方法。

如下使用 Lambda 建立線程,至關於建立了一個匿名內部線程。

1public static void main(String[] args) {
 2    ExecutorService executorService = Executors.newCachedThreadPool();
 3    executorService.execute(() -> {
 4        try {
 5            Thread.sleep(2000);
 6            System.out.println("Thread run");
 7        } catch (InterruptedException e) {
 8            e.printStackTrace();
 9        }
10    });
11    executorService.shutdownNow();
12    System.out.println("Main run");
13}
1Main run
2java.lang.InterruptedException: sleep interrupted
3    at java.lang.Thread.sleep(Native Method)
4    at ExecutorInterruptExample.lambda$main$0(ExecutorInterruptExample.java:9)
5    at ExecutorInterruptExample$$Lambda$1/1160460865.run(Unknown Source)
6    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
7    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
8    at java.lang.Thread.run(Thread.java:745)

若是隻想中斷 Executor 中的一個線程,能夠經過使用 submit() 方法來提交一個線程,它會返回一個 Future 對象,經過調用該對象的 cancel(true) 方法就能夠中斷線程。

1Future<?> future = executorService.submit(() -> {
2    // ..
3});
4future.cancel(true);

互斥同步

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

可重入與不可重入鎖

https://blog.csdn.net/u012545728/article/details/80843595

不可重入鎖

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

1public class Count{
 2    Lock lock = new Lock();
 3    public void print(){
 4        lock.lock();
 5        doAdd();
 6        lock.unlock();
 7    }
 8    public void doAdd(){
 9        lock.lock();
10        //do something
11        lock.unlock();
12    }
13}

可重入鎖

所謂可重入,意味着線程能夠進入它已經擁有的鎖的同步代碼塊兒

咱們設計兩個線程調用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. 同步一個代碼塊
1public void func () {
2    synchronized (this) {
3        // ...
4    }
5}

它只做用於同一個對象,若是調用兩個不一樣對象上的同步代碼塊,就不會進行同步。

  1. 同步一個方法
1public synchronized void func () {
2    // ...
3}

它和同步代碼塊同樣,只做用於同一個對象。

  1. 同步一個類
1public void func() {
2    synchronized (SynchronizedExample.class) {
3        // ...
4    }
5}

做用於整個類,也就是說兩個線程調用同一個類的不一樣對象上的這種同步語句,也須要進行同步。

  1. 同步一個靜態方法
1public synchronized static void fun() {
2    // ...
3}

做用於整個類。

釋放鎖的時機

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

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

Lock

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

1public class LockExample {
 2
 3    private Lock lock = new ReentrantLock();
 4
 5    public void func() {
 6        lock.lock();
 7        try {
 8            for (int i = 0; i < 10; i++) {
 9                System.out.print(i + " ");
10            }
11        } finally {
12            lock.unlock(); // 確保釋放鎖,從而避免發生死鎖。
13        }
14    }
15}
1public static void main(String[] args) {
2    LockExample lockExample = new LockExample();
3    ExecutorService executorService = Executors.newCachedThreadPool();
4    executorService.execute(() -> lockExample.func());
5    executorService.execute(() -> lockExample.func());
6}
10 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. 等待可中斷

當持有鎖的線程長期不釋放鎖的時候,正在等待的線程能夠選擇放棄等待,改成處理其餘事情,可中斷特性對處理執行時間很是長的同步塊頗有幫助。

  1. 可實現公平鎖

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

  • synchronized 中的鎖是非公平的
  • ReentrantLock 默認狀況下也是非公平的,但能夠經過帶布爾值的構造函數要求使用公平鎖。

    1. 鎖綁定多個條件
  • synchronized 中,鎖對象的 wait() 和 notify() 或 notifyAll() 方法能夠實現一個隱含的條件,若是要和多於一個的條件關聯的時候,就不得不額外地添加一個鎖
  • 而 ReentrantLock 則無須這樣作,只須要屢次調用 newCondition() 方法便可。

ReentrantReadWriteLock

咱們知道synchronized內置鎖和ReentrantLock都是互斥鎖(一次只能有一個線程進入到臨界區(被鎖定的區域)

ReentrantReadWriteLock優勢:

  • 在讀取數據的時候,能夠多個線程同時進入到到臨界區(被鎖定的區域)
  • 在寫數據的時候,不管是讀線程仍是寫線程都是互斥的
  • 若是讀的線程比寫的線程要多不少的話,那能夠考慮使用它。它使用state的變量高16位是讀鎖,低16位是寫鎖
  • 寫鎖能夠降級爲讀鎖,讀鎖不能升級爲寫鎖

synchronized 和 ReentrantLock 比較

  1. 鎖的實現

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

  1. 性能

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

  1. 等待可中斷

ReentrantLock 可中斷,而 synchronized 不行。

  1. 公平鎖
  • 公平鎖能保證:老的線程(234)排隊使用鎖,新線程仍然排隊使用鎖(2345)。
  • 非公平鎖保證:老的線程(234)排隊使用鎖;可是沒法保證新線程5搶佔已經在排隊的線程的鎖(正好在1釋放鎖的時候搶佔到了鎖,沒有進入排隊隊列)。
    synchronized 中的鎖是非公平的,ReentrantLock 默認狀況下也是非公平的,可是也能夠是公平的。
  1. 鎖綁定多個條件

一個 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 對象。

1public class AwaitSignalExample {
 2    private Lock lock = new ReentrantLock();
 3    private Condition condition = lock.newCondition();
 4
 5    public void before() {
 6        lock.lock();
 7        try {
 8            System.out.println("before");
 9            condition.signalAll();
10        } finally {
11            lock.unlock();
12        }
13    }
14
15    public void after() {
16        lock.lock();
17        try {
18            condition.await();
19            System.out.println("after");
20        } catch (InterruptedException e) {
21            e.printStackTrace();
22        } finally {
23            lock.unlock();
24        }
25    }
26}
1public static void main(String[] args) {
2    ExecutorService executorService = Executors.newCachedThreadPool();
3    AwaitSignalExample example = new AwaitSignalExample();
4    executorService.execute(() -> example.after());
5    executorService.execute(() -> example.before());
6}
1before
2after

J.U.C - AQS

從總體來看,concurrent包的實現示意圖以下:
Java併發知識點快速複習手冊(上)
在這裏插入圖片描述

http://www.javashuo.com/article/p-gvvwqcdj-nc.html

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了(其餘線程的任務都完成了),那就能夠繼續執行。
    Java併發知識點快速複習手冊(上)
    在這裏插入圖片描述
1public class CountdownLatchExample {
 2    public static void main(String[] args) throws InterruptedException {
 3        final int totalThread = 10;
 4        CountDownLatch countDownLatch = new CountDownLatch(totalThread);
 5        ExecutorService executorService = Executors.newCachedThreadPool();
 6        for (int i = 0; i < totalThread; i++) {
 7            executorService.execute(() -> {
 8                System.out.print("run..");
 9                countDownLatch.countDown();
10            });
11        }
12        countDownLatch.await();
13        System.out.println("end");
14        executorService.shutdown();
15    }
16}
17
18run..run..run..run..run..run..run..run..run..run..end

CyclicBarrier

CyclicBarrier 的字面意思是可循環使用(Cyclic)的屏障(Barrier)。它要作的事情是,讓一組線程到達一個屏障(也能夠叫同步點)時被阻塞,直到最後一個線程到達屏障時,屏障纔會開門,全部被屏障攔截的線程纔會繼續幹活。

和 CountdownLatch類似,都是經過維護計數器來實現的。可是它的計數器是遞增的,每次執行 await() 方法以後,計數器會加 1,直到計數器的值和設置的值相等,等待的全部線程纔會繼續執行。

CyclicBarrier能夠被重用(對比於CountDownLatch是不能重用的),CyclicBarrier 的計數器經過調用 reset() 方法能夠循環使用,因此它才叫作循環屏障。
Java併發知識點快速複習手冊(上)
在這裏插入圖片描述

1public CyclicBarrier(int parties, Runnable barrierAction) {
 2    if (parties <= 0) throw new IllegalArgumentException();
 3    this.parties = parties;
 4    this.count = parties;
 5    this.barrierCommand = barrierAction;
 6}
 7
 8public CyclicBarrier(int parties) {
 9    this(parties, null);
10}
 1public class CyclicBarrierExample {
 2    public static void main(String[] args) {
 3        final int totalThread = 10;
 4        CyclicBarrier cyclicBarrier = new CyclicBarrier(totalThread);
 5        ExecutorService executorService = Executors.newCachedThreadPool();
 6        for (int i = 0; i < totalThread; i++) {
 7            executorService.execute(() -> {
 8                System.out.print("before..");
 9                try {
10                    cyclicBarrier.await();
11                } catch (InterruptedException | BrokenBarrierException e) {
12                    e.printStackTrace();
13                }
14                System.out.print("after..");
15            });
16        }
17        executorService.shutdown();
18    }
19}
20before..before..before..before..before..before..before..before..before..before..after..after..after..after..after..after..after..after..after..after..

Semaphore

Semaphore 就是操做系統中的信號量,能夠控制對互斥資源的訪問線程數。

  • 當調用acquire()方法時,會消費一個許可證。若是沒有許可證了,會阻塞起來
  • 當調用release()方法時,會添加一個許可證。
  • 這些"許可證"的個數其實就是一個count變量罷了~
    Java併發知識點快速複習手冊(上)
    在這裏插入圖片描述
1public class SemaphoreExample {
 2    public static void main(String[] args) {
 3        final int clientCount = 3;
 4        final int totalRequestCount = 10;
 5        Semaphore semaphore = new Semaphore(clientCount);
 6        ExecutorService executorService = Executors.newCachedThreadPool();
 7        for (int i = 0; i < totalRequestCount; i++) {
 8            executorService.execute(()->{
 9                try {
10                    semaphore.acquire();
11                    System.out.print(semaphore.availablePermits() + " ");
12                } catch (InterruptedException e) {
13                    e.printStackTrace();
14                } finally {
15                    semaphore.release();
16                }
17            });
18        }
19        executorService.shutdown();
20    }
21}

J.U.C - 其它組件

FutureTask

在介紹 Callable 時咱們知道它能夠有返回值,返回值經過 Future進行封裝。

FutureTask 實現了 RunnableFuture 接口,該接口繼承自 Runnable 和 Future接口,這使得 FutureTask 既能夠當作一個任務執行,也能夠有返回值。

1public class FutureTask<V> implements RunnableFuture<V>
2
1public interface RunnableFuture<V> extends Runnable, Future<V>
2

當一個計算任務須要執行很長時間,那麼就能夠用 FutureTask 來封裝這個任務,用一個線程去執行該任務,而後其它線程繼續執行其它任務。當須要該任務的計算結果時,再經過 FutureTask 的 get() 方法獲取。

1public class FutureTaskExample {
 2    public static void main(String[] args) throws ExecutionException, InterruptedException {
 3        FutureTask<Integer> futureTask = new FutureTask<Integer>(new Callable<Integer>() {
 4            @Override
 5            public Integer call() throws Exception {
 6                int result = 0;
 7                for (int i = 0; i < 100; i++) {
 8                    Thread.sleep(10);
 9                    result += i;
10                }
11                return result;
12            }
13        });
14
15        Thread computeThread = new Thread(futureTask);
16        computeThread.start();
17
18        Thread otherThread = new Thread(() -> {
19            System.out.println("other task is running...");
20            try {
21                Thread.sleep(1000);
22            } catch (InterruptedException e) {
23                e.printStackTrace();
24            }
25        });
26        otherThread.start();
27        System.out.println(futureTask.get());
28    }
29}
1other task is running...
24950

BlockingQueue

java.util.concurrent.BlockingQueue 接口有如下阻塞隊列的實現:

  • FIFO 隊列 :LinkedBlockingQueue、ArrayListBlockingQueue(固定長度)
  • 優先級隊列(每一個元素都有優先級) :PriorityBlockingQueue
    提供了阻塞的 take() 和 put() 方法:若是隊列爲空 take() 將阻塞,直到隊列中有內容;若是隊列爲滿 put() 將阻塞,指到隊列有空閒位置。

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

1public class ProducerConsumer {
 2
 3    private static BlockingQueue<String> queue = new ArrayBlockingQueue<>(5);
 4
 5    private static class Producer extends Thread {
 6        @Override
 7        public void run() {
 8            try {
 9                queue.put("product");
10            } catch (InterruptedException e) {
11                e.printStackTrace();
12            }
13            System.out.print("produce..");
14        }
15    }
16
17    private static class Consumer extends Thread {
18
19        @Override
20        public void run() {
21            try {
22                String product = queue.take();
23            } catch (InterruptedException e) {
24                e.printStackTrace();
25            }
26            System.out.print("consume..");
27        }
28    }
29}
 1public static void main(String[] args) {
 2    for (int i = 0; i < 2; i++) {
 3        Producer producer = new Producer();
 4        producer.start();
 5    }
 6    for (int i = 0; i < 5; i++) {
 7        Consumer consumer = new Consumer();
 8        consumer.start();
 9    }
10    for (int i = 0; i < 3; i++) {
11        Producer producer = new Producer();
12        producer.start();
13    }
14}
1produce..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 原理相似,都是把大的計算任務拆分紅多個小任務並行計算。

1public class ForkJoinExample extends RecursiveTask<Integer> {
 2    private final int threhold = 5;
 3    private int first;
 4    private int last;
 5
 6    public ForkJoinExample(int first, int last) {
 7        this.first = first;
 8        this.last = last;
 9    }
10
11    @Override
12    protected Integer compute() {
13        int result = 0;
14        if (last - first <= threhold) {
15            // 任務足夠小則直接計算
16            for (int i = first; i <= last; i++) {
17                result += i;
18            }
19        } else {
20            // 拆分紅小任務
21            int middle = first + (last - first) / 2;
22            ForkJoinExample leftTask = new ForkJoinExample(first, middle);
23            ForkJoinExample rightTask = new ForkJoinExample(middle + 1, last);
24            leftTask.fork();
25            rightTask.fork();
26            result = leftTask.join() + rightTask.join();
27        }
28        return result;
29    }
30}
1public static void main(String[] args) throws ExecutionException, InterruptedException {
2    ForkJoinExample example = new ForkJoinExample(1, 10000);
3    ForkJoinPool forkJoinPool = new ForkJoinPool();
4    Future result = forkJoinPool.submit(example);
5    System.out.println(result.get());
6}

ForkJoin 使用 ForkJoinPool 來啓動,它是一個特殊的線程池,線程數量取決於 CPU 核數。

1public class ForkJoinPool extends AbstractExecutorService
2

ForkJoinPool 實現了工做竊取算法來提升 CPU 的利用率。每一個線程都維護了一個雙端隊列,用來存儲須要執行的任務。工做竊取算法容許空閒的線程從其它線程的雙端隊列中竊取一個任務來執行。竊取的任務必須是最晚的任務,避免和隊列所屬線程發生競爭。例以下圖中,Thread2 從 Thread1 的隊列中拿出最晚的 Task1 任務,Thread1 會拿出 Task2 來執行,這樣就避免發生競爭。可是若是隊列中只有一個任務時仍是會發生競爭。
Java併發知識點快速複習手冊(上)
在這裏插入圖片描述

關注我

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

來微信和我聊聊:yangzd1102

Github:https://github.com/qqxx6661

原創博客主要內容

  • 筆試面試複習知識點手冊
  • Leetcode算法題解析(前150題)
  • 劍指offer算法題解析
  • Python爬蟲相關實戰
  • 後臺開發相關實戰
    同步更新如下博客
  1. Csdn

http://blog.csdn.net/qqxx6661

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

  1. 知乎

https://www.zhihu.com/people/yang-zhen-dong-1/

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

  1. 掘金

https://juejin.im/user/5b48015ce51d45191462ba55

  1. 簡書

https://www.jianshu.com/u/b5f225ca2376

我的公衆號:Rude3Knife

Java併發知識點快速複習手冊(上)
我的公衆號:Rude3Knife

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

相關文章
相關標籤/搜索