從Java多線程基礎到Java內存模型;從synchronized關鍵字到Java併發工具包JUC。html
咱們不生產知識,咱們只作知識的搬運工!算法
線程與進程的不一樣點:編程
起源不一樣。先有進程後有線程。因爲處理器的速度遠遠大於外設,爲了提高程序的執行效率,才誕生了線程。segmentfault
概念不一樣。進程是具備獨立功能的程序運行起來的一個活動,是操做系統分配資源和調度的一個獨立單位;線程是CPU的基本調度單位。數組
內存共享方式不一樣。不一樣進程之間的內存數據通常是不共享的(除非採用進程間通訊IPC);同一個進程中的不一樣線程每每會共享:緩存
擁有的資源不一樣。線程獨有的內容包括:安全
進程和線程的數量不一樣。數據結構
線程和進程建立的開銷不一樣。多線程
Java中沒有協程的概念,協程每每指程序中的多個線程能夠映射到操做系統級別的幾個線程,Java中的線程數目與操做系統中的線程數目是一一對應的。架構
建立線程只有一種方式就是構造Thread類。實現線程的執行單元有兩種方式:
從3個角度能夠獲得實現Runnable接口來完成多線程編程優於繼承Thread類的完成多線程編程:
同步與異步:
同步是指被調用者不會主動告訴被調用者結果,須要調用者不斷的去查看調用結果
異步是指被調用者會主動告訴被調用者結果,不須要調用者不斷的去查看調用結果
複製代碼
線程的正確啓動與中止:
線程的正確啓動方法是start()而不是run()。start()方法的本質是請求JVM來運行當前的線程,至於當前線程什麼時候真正運行是由線程調度器決定的。start()方法的內部實現主要是包括三個步驟:一是檢查要啓動的新線程的狀態,二是將該線程加入線程組,三是調用線程的native方法start0()。
線程的正確中止方法是:使用interrupt()來通知,而不是強制結束指定線程。
public class JavaDemo implements Runnable {
@Override
public void run() {
while (true) {
if (Thread.currentThread().isInterrupted()) {
break;
}
System.out.println("go");
interrupt();
}
}
public void interrupt() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
System.out.println("出現異常,記錄日誌而且中止");
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new JavaDemoSi());
thread.start();
thread.sleep(1000);
thread.interrupt();
}
}
複製代碼
線程的六種生命週期:
常見方法:
wait()方法:在同步代碼塊synchronized(object){}
中的線程A已經獲取到鎖時,其餘線程不能獲取當前鎖從而會阻塞進入BLOCKED狀態;當線程A執行object.wait()
時,線程A持有的鎖會釋放,此時其餘線程獲取到object鎖;其餘線程代碼中執行了object.notify()
方法時,線程A會從新獲取到object鎖,能夠進行線程的調用。
注意notify()、notifyAll()方法必需要在wait()方法以後調用,若順序改變則程序會進入永久等待。
park()方法:在線程中調用LockSupport.park()進行線程的掛起,在其餘線程中調用LockSupport(已掛起的線程對象)進行線程的喚醒。park()和unpark()是基於許可證的概念存在的,只要調用了unpark()在一次park()中就能夠實現線程的一次喚醒(這裏的一次是指線程只要調用了park()就要調用unpark(),不能實現調用屢次unpark()後面的park()屢次調用就能夠直接實現線程的喚醒),park()和unpark()沒有調用順序的限制。
注意park()、unpark()方法不是基於監視器鎖實現的,與wait()方法不一樣,park()只會掛起當前線程並不會對鎖進行釋放。在線程中使用synchronized關鍵字的內部調用了park()容易致使死鎖。
幾個常見特性: 原子性、內存可見性和重排序。
原子性:
原子(Atomic)操做指相應的操做是單一不可分割的操做。 在多線程中,非原子操做可能會受到其餘線程的干擾,使用關鍵字synchronized
能夠實現操做的原子性。synchronized
的本質是經過該關鍵字所包括的臨界區的排他性保證在任何一個時刻只有一個線程可以執行臨界區中的代碼,從而使的臨界區中的代碼實現了原子操做。
內存可見性:
CPU在執行代碼時,爲了減小變量訪問的時間消耗會將代碼中訪問的變量值緩存到CPU的緩存區中,代碼在訪問某個變量時,相應的值會從緩存中讀取而不是在主內存中讀取;一樣的,代碼對被緩存過的變量的值的修改可能僅僅是寫入緩存區而不是寫回到內存中。這樣就致使一個線程對相同變量的修改沒法同步到其餘線程從而致使了內存的不可見性。
可使用synchronized
或volatile
來解決內存的不可見性問題。二者又有點不一樣。synchronized
仍然是 經過將代碼在臨界區中對變量進行改變,而後使得對稍後執行該臨界區中代碼的線程是可見的。volatile
不一樣之處在於,一個線程對一個採用volatile關鍵字修飾的變量的值的更改對於其餘使用該變量的線程老是可見的,它是經過將變量的更改直接同步到主內存中,同時其餘線程緩存中的對應變量失效,從而實現了變量的每次讀取都是從主內存中讀取。
指令重排序:
在CPU多級緩存場景下,當CPU寫緩存時發現緩存區正在被其餘CPU佔用,爲了提升CPU處理性能,可能將後面的讀緩存命令優先執行。運行時指令重排要遵循as-if-serial語義,即無論怎麼重排序,單線程程序的執行結果不能改變而且編譯器和處理器不會對存在的數據依賴關係的操做作重排序。
指令的重排序致使代碼的執行順序改變,這常常會致使一系列的問題,好比在對象的建立過程當中,指令的重排序使得咱們獲得了一個已經分配好的內存而對象的初始化並未完成,從而致使空指針的異常。volatile
關鍵字能夠禁止指令的重排序從而解決這類問題。
總之,synchronized
能夠保證在多線程中操做的原子性和內存可見性,可是會引發上下文切換;而volatile
關鍵字僅能保證內存可見性,可是能夠禁止指令的重排序,同時不會引發上下文切換。
首先介紹Java內存模型的特性
下面介紹內存模型圖
基於JMM,Java提供了多種除了鎖以外的同步機制來保證線程安全性。Java提供的TreadLocal以及前面概念中提到的volatile就是兩種策略。
下面先介紹volatile關鍵字,ThreadLocal在下文併發工具類中介紹
volatile最主要的就是實現了共享變量的內存可見性,其實現的原理是:volatile變量的值每次都會從高速緩存或者主內存中讀取,對於volatile變量,每個線程再也不會有一個副本變量,全部線程對volatile變量的操做都是對同一個變量的操做。
volatile變量的開銷包括讀變量和寫變量兩個方面。volatile變量的讀、寫操做都不會致使上下文的切換,所以volatile的開銷比鎖小。可是volatile變量的值不會暫存在寄存器中,所以讀取volatile變量的成本要比讀取普通變量的成本更高。
volatile常被稱爲"輕量級鎖"。
互斥同步是指多個線程對共享資源是獨佔的,當一個線程得到共享資源時,其餘全部的線程都將處於等待獲取狀態,不一樣線程之間是敵對的。
根據不一樣的分類標準存在多種鎖類型,對於一種肯定的鎖能夠同時屬於下面的多種類型:
多個線程可否共享一把鎖:能夠實現共享的稱爲共享鎖;不能夠實現共享的稱爲排他鎖。共享鎖又稱爲讀鎖,每個線程均可以獲取到讀鎖,以後能夠查看數據可是沒法修改和刪除數據。
synchronized屬於排他鎖**。 **
ReentrantReadWriteLock`同時具有共享鎖和排他鎖,其中讀鎖是共享鎖,寫鎖是排他鎖。
線程要不要鎖住同步資源:鎖住同步資源的稱爲悲觀鎖(又稱爲互斥同步鎖);不鎖住同步資源的稱爲樂觀鎖(又稱爲非互斥同步鎖)。
優缺點:
悲觀鎖的性能相對較低:當發生長時間鎖等不到釋放或者直接出現死鎖時,等待鎖的線程永遠得不到執行;同時悲觀鎖存在阻塞和喚醒這兩種狀態都是會消耗資源的;此外使用了悲觀鎖,線程的優先級屬性設置將會失效。
相對於悲觀鎖而言,樂觀鎖性能較高,可是若是獲取鎖的線程數量過多,那麼樂觀鎖會產生大量的無用自旋等消耗,性能也會所以而降低
悲觀鎖適用於併發寫入多或者臨界區持鎖時間比較長的情形
樂觀鎖適用於併發寫入少、併發讀取多的情形
synchronized
和Lock
都屬於悲觀鎖。 原子類和併發容器工具都採用了樂觀鎖的思想
樂觀鎖基於CAS算法實現。
CAS算法:
CAS(Compare and Swap),即比較並交換。
CAS有3個操做數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改成B,不然什麼都不作。
固然CAS除了有上面提到的樂觀鎖的缺點外,CAS還容易出現ABA問題。便可能存在其餘線程修改過預期值執行過其餘操做以後又寫會預期值,這樣反而不會被察覺。解決ABA問題的一個好方式就是增長版本號version字段,經過每次更新操做都修改version字段以及每次更新以前都檢查version字段來保證線程執行的安全性
同一個線程是否能夠重複獲取同一把鎖:能夠重複獲取的稱爲可重入鎖;不能夠重複獲取的稱爲不可重入鎖
可重入鎖能夠有效的避免死鎖,當一個線程獲取到鎖時,能夠繼續獲取該鎖,而不會出現當前線程等待當前線程釋放鎖這一狀況的發生。
synchronized
和ReentrantLock
都屬於可重入鎖。
多個線程競爭時根據是否排隊:經過排隊來獲取的稱爲公平鎖;先嚐試插隊,插隊失敗再排隊的稱爲非公平鎖
ReentrantLock既能夠實現公平鎖又能夠實現非公平鎖,經過指定ReentrantLock構造方法中fair的參數值來實現公平與非公平的效果
是否能夠響應中斷:可響應中斷的稱爲可中斷鎖;不可響應中斷的稱爲非可中斷鎖
等鎖的過程不一樣:等鎖的過程當中若是不停的嘗試而非阻塞稱爲自旋鎖;等鎖的過程當中若是阻塞等待稱爲非自旋鎖
方法鎖,即默認鎖對象爲this當前實例對象。同一個實例對象下的實例方法共享同一把鎖,不一樣的實例對象的實例方法鎖不一樣。
class SynchronizedDemo1 {
public synchronized void index1() {
//do something...
}
public synchronized void index2() {
//do something...
}
}
class SynchronizedDemo2 {
public synchronized void index1() {
//do something...
}
public synchronized void index2() {
//do something...
}
}
複製代碼
以上代碼中,SynchronizedDemo1實例對象demo1的方法index1和index2共享同一把鎖,SynchronizedDemo2實例對象demo1的方法index1和index2共享同一把鎖,多個線程訪問同一個對象下的synchronized修飾的方法時是互斥同步的,訪問不一樣對象的synchronized修飾的方法互不干擾
同步代碼塊鎖,即本身指定鎖對象。
class SynchronizedDemo1 {
public synchronized void index() {
synchronized(this){
//do something...
}
}
}
複製代碼
以上代碼中,只有得到了當前對象鎖的線程才能執行同步代碼塊中的代碼,同步代碼塊的出現是爲了減少方法鎖的粒度,提升性能
synchronized修飾靜態的方法。多個線程訪問同一類的不一樣實例對象的靜態方法時,因爲靜態方法是類級別的而不是對象級別的,因此即使是不一樣對象,方法之間的訪問也是互斥同步的
指定的鎖爲Class對象。
class SynchronizedDemo1 {
public synchronized void index() {
synchronized(SynchronizedDemo1.class){
//do something...
}
}
}
複製代碼
以上代碼中,只有得到了當前類的Class對象鎖的線程才能執行同步代碼塊中的代碼,同步代碼塊的出現是爲了減少方法鎖的粒度,提升性能
在jdk1.5以後,併發包中新增了Lock接口(以及相關實現類)用來實現鎖功能,Lock接口提供了與synchronized關鍵字相似的同步功能,但須要在使用時手動獲取鎖和釋放鎖,也正由於如此,基於Lock接口實現的鎖具有更好的可操做性。
Lock接口中的方法:
lock()
: 此方法用於獲取鎖,若是鎖已被其餘線程獲取,那麼線程進入等待狀態,與synchronized不一樣的是:當獲取到鎖而且在執行任務中發生了異常,synchronized會自動釋放鎖而lock()方法獲取到的鎖不會自動釋放。使用lock()必須在try...finally...中手動釋放。tryLock()
:因爲lock()不能被中斷,因此一旦陷入死鎖,lock()就會陷入永久等待中;tryLock()方法是一種更爲優雅的使用方式,tryLock()用來嘗試獲取鎖,若是當前鎖沒有被其餘線程佔用,那麼獲取鎖成功並馬上返回true,不然馬上返回false表示獲取鎖失敗。ReetrantLock 是基於Lock接口最通用的實現,在上文中在介紹鎖分類時也已經屢次提到過ReentrantLock,所以也瞭解過其許多特性,因爲ReentrantLock很是值得深刻探究,在此也不在一文中過多闡述,在此給出一個連接進行參看:
[深刻ReentrantLock]blog.csdn.net/fuyuwei2015…
讀寫鎖是一種改進型的排它鎖。讀寫鎖容許多個線程能夠同時讀取(只讀)共享變量。讀寫鎖是分爲讀鎖和寫鎖兩種角色的,讀線程在訪問共享變量的時候必須持有相應讀寫鎖的讀鎖,並且讀鎖是共享的、多個線程能夠共同持有的;寫鎖是排他的,以一個線程在持有寫鎖的時候,其餘線程沒法得到相應鎖的寫鎖或讀鎖。總之,讀寫鎖經過讀寫鎖的分離從而提升了併發性。 ReadWriteLock接口是對讀寫鎖的抽象,其默認的實現類是ReentrantReadWriteLock。ReadWriteLock定義了兩個方法readLock()和writeLock(),分別用於返回相應讀寫鎖實例的讀鎖和寫鎖。這兩個方法的返回值類型都是Lock。
關於ReentrantReadWriteLock實現,這裏給出一個連接參看: [ReentrantReadWriteLock詳解]www.cnblogs.com/xiaoxi/p/91…
讀寫鎖主要用於讀線程持有鎖的時間比較長的情景下。
非互斥同步指的是不一樣的線程不對共享資源進行獨佔,不一樣的線程均可以訪問共享資源,只不過當多個線程同時對一個共享變量進行修改或刪除時,只有一個線程的操做能成功其餘的都會失敗。
Java中的原子類分爲6種,分別有:
直接使用Java中的原子類進行操做便可在併發狀況下保證變量的線程安全,原子類相較於鎖粒度更小,性能更高。原子類也是基於CAS算法來實現的,其都包括compareAndSet()方法即爲先比較當前值是否等於預期的值而後進行數據的修改從而保證了變量的原子性。
須要注意的是累加器LongAdder是Java8開始引入的,相較於AtomicLong,因爲LongAdder在每一個線程操做的過程當中並不會實時的進行數據同步(因爲上文所提到的JMM,AtomicLong會實時的進行多個線程之間的數據通訊),因此效率更高。而LongAccumulator擴展了LongAdder使得原子變量不只只能進行累加操做也能夠進行其餘指定公式的計算
Java中併發容器由來已久,固然併發容器的種類也很是多。可是其中一部分諸如Vector、Hashtable、Collections.synchronizedList()、Collections.synchronizedMap()等底層是基於synchronized來實現的併發同步,效率會比較低,因此即便這些容器能夠保證線程安全也再也不使用。與之相替代的就是下面的幾種併發容器類,因爲併發容器在實現上也有許多可學習之處,因此這裏再也不在一文中介紹而是會初步引入,並放上我認爲比較不錯的幾個博客連接,這樣能夠更好的深刻理解。
多個線程往HashMap中同時進行put(),若是有幾個線程計算出的鍵的散列值相同,那麼就會出現key丟失的狀況,一樣的,若是此時HashMap容量不夠,多個線層同時擴容,也會只保留一個擴容後的Map,從而致使數據丟失。而ConcurrentHashMap則在底層數據結構的實現上與HashMap又有所區別,避免了HashMap會產生的問題。
關於ConcurrentHashMap的數據結構能夠參看: [ConcurrentHashMap的數據結構]blog.csdn.net/weixin_4446…
爲了保證List的線程安全,又要避免因使用Vector、Collections.synchronized等而產生的鎖粒度過大而形成效率下降的問題,CopyOnWriteArrayList、CopyOnWriteArraySet應運而生,CopyOnWriteArrayList和CopyOnWriteArraySet在實現原理上大致一致,這裏只給出CopyOnWriteArrayList的介紹.
關於CopyOnWriteArrayList的數據結構能夠參看: [CopyOnWriteArrayList的數據結構]www.cnblogs.com/chengxiao/p…
BlockingQueue很好的解決了多線程中,如何高效安全「傳輸」數據的問題。經過這些高效而且線程安全的隊列類,爲咱們快速搭建高質量的多線程程序帶來極大的便利。在Java中,BlockingQueue是一個接口,它的實現類有ArrayBlockingQueue、DelayQueue、LinkedBlockingQueue、PriorityBlockingQueue、SynchronousQueue等,這些阻塞隊列的實如今Java併發編程中常常要用到,其中最經常使用的就是ArrayBlockingQueue和LinkedBlockingQueue
關於BlockingQueue能夠參看: [BlockingQueue相關]segmentfault.com/a/119000001…
關於ArrayBlockingQueue能夠參看: [ArrayBlockingQueue相關]blog.csdn.net/u014799292/…
關於LinkedBlockingQueue能夠參看: [LinkedBlockingQueue相關]blog.csdn.net/tonywu1992/…
ConcurrentLinkedQueue是一個基於連接節點的非阻塞無界線程安全隊列。
關於ConcurrentLinkedQueue的數據結構能夠參看: [ConcurrentLinkedQueue的數據結構]blog.csdn.net/qq_38293564…
ThreadLocal,即線程變量,是一個以ThreadLocal對象爲鍵、任意對象爲值的存儲結構。這個結構被附帶在線程上,也就是說一個線程能夠根據一個ThreadLocal對象查詢到綁定在這個線程上的一個值。
ThreadLocal採用的是上述策略中的第一種設計思想——採用線程的特有對象.採用線程的特有對象,咱們能夠保障每個線程都具備各自的實例,同一個對象不會被多個線程共享,ThreadLocal是維護線程封閉性的一種更加規範的方法,這個類能使線程中的某個值與保存值的對象關聯起來,從而保證了線程特有對象的固有線程安全性。
ThreadLocal類至關於線程訪問其線程特有對象的代理,即各個線程經過這個對象能夠建立並訪問各自的線程特有對象,泛型T指定了相應線程持有對象的類型。一個線程可使用不一樣的ThreadLocal實例來建立並訪問其不一樣的線程持有對象。多個線程使用同一個ThreadLocal實例所訪問到的對象時類型T的不一樣實例。代理的關係圖以下:
ThreadLocal提供了get和set等訪問接口或方法,這些方法爲每個使用該變量的線程都存有一份獨立的副本,所以get老是能返回由當前執行線程在調用set時設置的最新值。其主要使用的方法以下:
public T get(): 獲取與當前線程中ThreadLocal實例關聯的線程特有對象。
public void set(T value):從新關聯當前線程中ThreadLocal實例所對應的線程特有對象。
protected T initValue():若是沒有調用set(),在初始化threadlocal對象的時候,該方法的返回值就是當前線程中與ThreadLocal實例關聯的線程特有對象。
public void remove():刪除當前線程中ThreadLocal和線程特有對象的關係。
複製代碼
那麼ThreadLocal底層是如何實現Thread持有本身的線程特有對象的?查看set()方法的源代碼:
能夠看到,當咱們調用threadlocal的set方法來保存當前線程的特有對象時,threadlocal會取出當前線程關聯的threadlocalmap對象,而後調用ThreadLocalMap對象的set方法來進行當前給定值的保存。每個Thread都會維護一個ThreadLocalMap對象,ThreadLocalMap是一個相似Map的數據結構,可是它沒有實現任何Map的相關接口。ThreadLocalMap是一個Entry數組,每個Entry對象都是一個"key-value"結構,並且Entry對象的key永遠都是ThreadLocal對象。當咱們調用ThreadLocal的set方法時,實際上就是以當前ThreadLocal對象自己做爲key,放入到了ThreadLocalMap中。
可能發生內存泄漏:
經過查看Entry結構可知,Entry屬於WeakReference類型,所以Entry不會阻止被引用的ThreadLocal實例被垃圾回收。當一個ThreadLocal實例沒有對其可達的強引用時,這個實例就能夠被垃圾回收,即其所在的Entry的key會被置爲null,可是若是建立ThreadLocal的線程一直持續運行,那麼這個Entry對象中的value就有可能一直得不到回收,從而發生內存泄露。
解決內存泄漏的最有效方法就是,在使用完ThreadLocal以後,要注意調用threadlocal的remove()方法釋放內存。
傳統的Runnable來實現任務有兩大缺陷,一個是Runnable中的run()沒有返回值,另外一個是Runnable中的run()沒法拋出異常。爲了解決上述問題,Callable應運而生,而Future是爲了更好的操做Callable實現業務邏輯而誕生的。
咱們能夠用Future.get來獲取Callable接口返回的執行結果,還能夠經過Future.isDone()來判斷任務是否已經執行完了以及取消這個任務,限時獲取任務的結果等等。
線程池提供了複用線程的能力,若是不使用線程池,那麼每一個任務都會新開一個線程,上文基石中也已經提到Java代碼中的線程數量對應於操做系統的線程數量,這樣對於線程的建立和銷燬都會帶來很大的開銷,此外系統可建立的線程數量是有限的,使用線程池能夠有效避免OOM等異常。
線程池的建立通常藉助ThreadPoolExecutor
這個類,其中有5個參數比較關鍵,如下說明:
corePoolSize、maxPoolSize、workQueue
:線程池中默認存在的線程數量是corePoolSize,當任務多於corePoolSize時,新來的任務會首先存儲在任務存儲隊列workQueue
中,當任務數量超出了任務存儲隊列的最大長度,線程池纔會擴大其中的線程數量直到maxPoolSize
,當任務數量超出maxPoolSize
,線程池執行定義的拒絕策略handler
。
workQueue的三種經常使用類型:
1.SyncbronousQueue:最簡單的直接交換隊列,這隊列長度爲0不能存儲新的任務,適用與任務不太多的場景,此外因爲隊列不能存儲任務線程池很容易建立新的線程,因此maxPoolSize要設置的大一點,可是若是設置的maxPoolSize過大,線程建立的過多而不能獲得調度從而產生堆積,就會引起OOM。Executors.newCachedThreadPool()、Executors.newScheduledThreadPool()
即爲這種類型,其中Executors.newCachedThreadPool()的maxPoolSize這裏設置的爲Integer.MAX_VALUE,corePoolSize默認爲0,keepAliveTime爲60s
2.LinkedBlockingQueue:無解隊列,這個相較於第一種隊列屬於另外一個極端,能夠存儲任意數量的任務。此類隊列能夠存儲較多數量的任務而且此時maxPoolSize會失效,可是此時也要注意任務過多時會產生堆積出現OOM。Executors.newFixedThreadPool()、Executors.newSingleThreadExecutor()
即爲這種類型
3.ArrayBlockingQueue:有界隊列,能夠設置隊列長度,此時maxPoolSize有效
keepAliveTime
:若是線程池當前的線程數量多餘corePoolSize
,那麼當多餘線程的空閒時間超過keepAliveTime
時,它們將被回收。
ThreadFactory
:線程池中新建立的線程是由ThreadFactory
建立的,默認使用Executors.defaultThreadFactory()
。
線程池應該手動建立,其中:
當任務屬於CPU密集型時,線程池中的線程數量應該設置爲CPU核心數的1-2倍;當任務屬於資源密集型時,線程池中的線程數量通常設置爲cpu核心數的不少倍,計算方法通常爲num=CPU核心數*(1+平均等待時間/平均工做時間)
線程池中止:
shutdown()
:調用此方法後,線程池並不會馬上中止而是拒絕接受新的任務並等待線程池中已在執行的線程任務和隊列中的任務執行完畢 shutdownNow()
:調用此方法後,線程池經過調用terminated()方法來終止正在執行的線程同時將隊列中未被調度的任務以集合的形式返回。
到此爲止,本文要梳理的Java併發相關也告一段落,之因此如此說是由於Java併發相關確實是值得深刻探究的一個領域,本文的定位是基於Java來梳理併發相關的那些事兒,儘量經過一篇文章來概括出Java併發中應該掌握的知識點。 本文仍然有不少不足之處,好比文中沒有介紹Java的併發工具類諸如CountdownLatch、Semaphore等,而關於ReentrantLock這種重要的鎖的實現原理AQS本文也沒有介紹,但願在以後的文章中能對本文略過的點進行深刻的概括總結。