併發進階

1. synchronized關鍵字

1.1 對synchronized的理解

synchronized解決的是多線程訪問資源的同步性,synchronized可以保證它所修飾的方法或代碼塊在任意時刻只能有一個線程運行。

synchronized 是一個重量級操作,需要調用操作系統相關接口,性能是低效的,有可能給線程加鎖消耗的時間比有用操作消耗的時間更多。
Synchronized 是通過對象內部的一個叫做監視器鎖(monitor)來實現的。但是監視器鎖本質又
是依賴於底層的操作系統的 Mutex Lock 來實現的。而操作系統實現線程之間的切換這就需要從用
戶態轉換到核心態,這個成本非常高,狀態之間的轉換需要相對比較長的時間,這就是爲什麼
Synchronized 效率低的原因。因此,這種依賴於操作系統 Mutex Lock 所實現的鎖我們稱之爲
「重量級鎖」。JDK 中對 Synchronized 做的種種優化,其核心都是爲了減少這種重量級鎖的使用。
Java1.6,synchronized 進行了很多的優化,有適應自旋、鎖消除、鎖粗化、輕量級鎖及偏向
鎖等,效率有了本質上的提高。在之後推出的 Java1.7 與 1.8 中,均對該關鍵字的實現機理做
了優化。引入了偏向鎖和輕量級鎖。都是在對象頭中有標記位,不需要經過操作系統加鎖。鎖可以從偏向鎖升級到輕量級鎖,再升級到重量級鎖。這種升級過程叫做鎖膨脹;

1.2 怎麼使用synchronized

  1. 作用於方法時,鎖住的是對象的實例(this);
  2. 當作用於靜態方法時,鎖住的是Class實例,又因爲Class的相關數據存儲在永久帶PermGen
    (jdk1.8 則是 metaspace),永久帶是全局共享的,因此靜態方法鎖相當於類的一個全局鎖,會鎖所有調用該方法的線程;
  3. synchronized 作用於一個對象實例時,鎖住的是所有以該對象爲鎖的代碼塊。它有多個隊列,當多個線程一起訪問某個對象監視器的時候,對象監視器會將這些線程存儲在不同的容器中。

synchronized關鍵字修飾靜態方法和class上都是給類上鎖,儘量不要使用synchronized(String)因爲在JVM中,字符串常量具有緩衝功能。

1.3 JDK1.6之後synchronized做了哪些優化?

Java1.6,synchronized 進行了很多的優化,有適應自旋、鎖消除、鎖粗化、輕量級鎖及偏向鎖等,效率有了本質上的提高。在之後推出的 Java1.7 與 1.8 中,均對該關鍵字的實現機理做了優化。引入了偏向鎖和輕量級鎖。都是在對象頭中有標記位,不需要經過操作系統加鎖。

鎖的狀態總共有四種:無鎖狀態、偏向鎖、輕量級鎖和重量級鎖
隨着鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖(但是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級)

這幾種優化的詳細信息:

1.3.1 偏向鎖

Hotspot 的作者經過以往的研究發現大多數情況下鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得。偏向鎖的目的是在某個線程獲得鎖之後,消除這個線程鎖重入(CAS)的開銷,看起來讓這個線程得到了偏護。引入偏向鎖是爲了在無多線程競爭的情況下儘量減少不必要的輕量級鎖執行路徑,因爲輕量級鎖的獲取及釋放依賴多次 CAS 原子指令,而偏向鎖只需要在置換ThreadID 的時候依賴一次 CAS 原子指令(由於一旦出現多線程競爭的情況就必須撤銷偏向鎖,所以偏向鎖的撤銷操作的性能損耗必須小於節省下來的 CAS 原子指令的性能消耗)。輕量級鎖是爲了在線程交替執行同步塊時提高性能,而偏向鎖則是在只有一個線程執行同步塊時進一步提高性能。

引入偏向鎖的目的和引入輕量級鎖的目的很像,他們都是爲了沒有多線程競爭的前提下,減少傳統的重量級鎖使用操作系統互斥量產生的性能消耗。但是不同是:輕量級鎖在無競爭的情況下使用 CAS 操作去代替使用互斥量。而偏向鎖在無競爭的情況下會把整個同步都消除掉。

1.3.2 輕量級鎖

隨着鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖(但是鎖的升級是單向的,
也就是說只能從低到高升級,不會出現鎖的降級)。
「輕量級」是相對於使用操作系統互斥量來實現的傳統鎖而言的。但是,首先需要強調一點的是,
輕量級鎖並不是用來代替重量級鎖的,**它的本意是在沒有多線程競爭的前提下,減少傳統的重量
級鎖使用產生的性能消耗。**在解釋輕量級鎖的執行過程之前,先明白一點,輕量級鎖所適應的場
景是線程交替執行同步塊的情況,如果存在同一時間訪問同一鎖的情況,就會導致輕量級鎖膨脹
爲重量級鎖。

輕量級鎖能夠提升程序同步性能的依據是「對於絕大部分鎖,在整個同步週期內都是不存在競爭的」,這是一個經驗數據。如果沒有競爭,輕量級鎖使用 CAS 操作避免了使用互斥操作的開銷。但如果存在鎖競爭,除了互斥量開銷外,還會額外發生CAS操作,因此在有鎖競爭的情況下,輕量級鎖比傳統的重量級鎖更慢!如果鎖競爭激烈,那麼輕量級將很快膨脹爲重量級鎖!

1.3.3 鎖消除

鎖消除是在編譯器級別的事情。在即時編譯器時,如果發現不可能被共享的對象,則可以消除這些對象的鎖操作,多數是因爲程序員編碼不規範引起。

1.3.4 鎖粗化

通常情況下,爲了保證多線程間的有效併發,會要求每個線程持有鎖的時間儘量短,即在使用完
公共資源後,應該立即釋放鎖。但是,凡事都有一個度,如果對同一個鎖不停的進行請求、同步
和釋放,其本身也會消耗系統寶貴的資源,反而不利於性能的優化 。

1.3.5 自旋鎖和自適應自旋

自旋鎖原理非常簡單,如果持有鎖的線程能在很短時間內釋放鎖資源,那麼那些等待競爭鎖
的線程就不需要做內核態和用戶態之間的切換進入阻塞掛起狀態,它們只需要等一等(自旋),
等持有鎖的線程釋放鎖後即可立即獲取鎖,這樣就避免用戶線程和內核的切換的消耗。線程自旋是需要消耗 cup 的,說白了就是讓 cup 在做無用功,如果一直獲取不到鎖,那線程也不能一直佔用 cup 自旋做無用功,所以需要設定一個自旋等待的最大時間。
如果持有鎖的線程執行的時間超過自旋等待的最大時間扔沒有釋放鎖,就會導致其它爭用鎖的線程在最大等待時間內還是獲取不到鎖,這時爭用線程會停止自旋進入阻塞狀態。

JDK1.6 中引入了自適應的自旋鎖。自適應的自旋鎖帶來的改進就是:自旋的時間不在固定了,而是和前一次同一個鎖上的自旋時間以及鎖的擁有者的狀態來決定.

1.4 synchronized 和 ReentrantLock 的區別

1.4.1 兩者的共同點

  1. 都是用來協調多線程對共享對象、變量的訪問

  2. 都是可重入鎖,同一線程可以多次獲得同一個鎖
    可重入鎖,也叫做遞歸鎖,指的是同一線程外層函數獲得鎖之後 ,內層遞歸函數仍然有獲取該鎖的代碼,但不受影響。在 JAVA 環境下 ReentrantLock 和 synchronized 都是 可重入鎖。
    可重入鎖」概念是:自己可以再次獲取自己的內部鎖。比如一個線程獲得了某個對象的鎖,此時這個對象鎖還沒有釋放,當其再次想要獲取這個對象的鎖的時候還是可以獲取的,如果不可鎖重入的話,就會造成死鎖。同一個線程每次獲取鎖,鎖的計數器都自增1,所以要等到鎖的計數器下降爲0時才能釋放鎖。

  3. 都保證了可見性和互斥性

1.4 .2 兩者的不同點

  1. ReentrantLock 是 API 級別的,synchronized 是 JVM 級別的
    synchronized 是依賴於 JVM 實現的, 虛擬機團隊在 JDK1.6 爲 synchronized 關鍵字進行了很多優化,但是這些優化都是在虛擬機層面實現的,並沒有直接暴露給我們。ReentrantLock 是 JDK 層面實現的(也就是 API 層面,需要 lock() 和 unlock() 方法配合 try/finally 語句塊來完成),所以我們可以通過查看它的源代碼,來看它是如何實現的。

2.ReentrantLock 比 synchronized 增加了一些高級功能
相比synchronized,ReentrantLock增加了一些高級功能。主要來說主要有三點:①等待可中斷;②可實現公平鎖;③可實現選擇性通知(鎖可以綁定多個條件)

2.1 ReentrantLock 可響應中斷、可輪迴,synchronized 是不可以響應中斷的,爲處理鎖的不可用性提供了更高的靈活性。
ReentrantLock提供了一種能夠中斷等待鎖的線程的機制,通過lock.lockInterruptibly()來實現這個機制。也就是說正在等待的線程可以選擇放棄等待,改爲處理其他事情。
使用 synchronized 時,等待的線程會一直等待下去,不能夠響應中斷。

2.2 ReentrantLock可以指定是公平鎖還是非公平鎖。默認是非公平鎖。而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的線程先獲得鎖。 ReentrantLock默認情況是非公平的,可以通過 ReentrantLock類的ReentrantLock(boolean fair)構造方法來制定是否是公平的。

公平鎖(Fair)
加鎖前檢查是否有排隊等待的線程,優先排隊等待的線程,先來先得
非公平鎖(Nonfair)
加鎖時不考慮排隊等待問題,直接嘗試獲取鎖,獲取不到自動到隊尾等待

2.3 synchronized關鍵字與wait()和notify()/notifyAll()方法相結合可以實現等待/通知機制,ReentrantLock類當然也可以實現,但是需要藉助於Condition接口與newCondition() 方法。

2. volatile關鍵字

2.1 Java內存模型

在 JDK1.2 之前,Java的內存模型實現總是從主存(即共享內存)讀取變量,是不需要進行特別的注意的。而在當前的 Java 內存模型下,線程可以把變量保存本地內存比如機器的寄存器)中,而不是直接在主存中進行讀寫。這就可能造成一個線程在主存中修改了一個變量的值,而另外一個線程還繼續使用它在寄存器中的變量值的拷貝,造成數據的不一致。

要解決這個問題,就需要把變量聲明爲volatile,這就指示 JVM,這個變量是不穩定的,每次使用它都到主存中進行讀取。

volatile有兩種特性:

  1. 變量可見性
    其一是保證該變量對所有線程可見,這裏的可見性指的是當一個線程修改了變量的值,那麼新的
    值對於其他線程是可以立即獲取的。

2.** 禁止重排序**
volatile 禁止了指令重排。

2.2 volatile和synchronized的區別

  1. volatile關鍵字是線程同步的輕量級體現,性能比synchronized要好,但是volatile只能用於變量,synchronized可以修飾代碼塊和方法
  2. 多線程訪問volatile不會發生堵塞,而synchronized可能會發生堵塞
  3. volatile關鍵字可以保證數據的可見性,但不能數據原子性。synchronized都可以保證
  4. volatile解決變量在多線程之間的可見性,synchronized解決的是多線程直接的資源同步性。

3.ThreadLocal

3.1 ThreadLocal介紹

ThreadLocal,很多地方叫做線程本地變量,也有些地方叫做線程本地存儲,ThreadLocal 的作用
是提供線程內的局部變量,這種變量在線程的生命週期內起作用,減少同一個線程內多個函數或
者組件之間一些公共變量的傳遞的複雜度。

3.2 ThreadLocal的原理

最終的變量是放在了當前線程的 ThreadLocalMap 中,並不是存在 ThreadLocal 上,ThreadLocal 可以理解爲只是ThreadLocalMap的封裝,傳遞了變量值。

每個Thread中都具備一個ThreadLocalMap,而ThreadLocalMap可以存儲以ThreadLocal爲key的鍵值對。這裏解釋了爲什麼每個線程訪問同一個ThreadLocal,得到的確是不同的數值。另外,ThreadLocal 是 map結構是爲了讓每個線程可以關聯多個 ThreadLocal變量。

ThreadLocalMap是ThreadLocal的靜態內部類。

在這裏插入圖片描述

4. 線程池

4.1 爲什麼使用線程池

線程池提供了一種限制和管理資源(包括執行一個任務)。 每個線程池還維護一些基本統計信息,例如已完成任務的數量。

使用線程池的好處:

  1. 降低資源消耗。 通過重複利用已創建的線程降低線程創建和銷燬造成的消耗。
  2. 提高響應速度。 當任務到達時,任務可以不需要的等到線程創建就能立即執行。
  3. 提高線程的可管理性。 線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的分配,調優和監控。

4.2 Runnable接口和Callable區別

有返回值的任務必須實現 Callable 接口,類似的,無返回值的任務必須 Runnable 接口。執行
Callable 任務後,可以獲取一個 Future 的對象,在該對象上調用 get 就可以獲取到 Callable 任務
返回的 Object 了,再結合線程池接口 ExecutorService 就可以實現傳說中有返回結果的多線程
了。

4.3 執行execute()方法和submit()方法的區別是什麼呢?

  1. execute() 方法用於提交不需要返回值的任務,所以無法判斷任務是否被線程池執行成功與否;

  2. submit() 方法用於提交需要返回值的任務。線程池會返回一個Future類型的對象,通過這個Future對象可以判斷任務是否執行成功

4.4 創建線程池

《阿里巴巴Java開發手冊》中強制線程池不允許使用 Executors 去創建,而是ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險

Java 裏面線程池的頂級接口是 Executor,但是嚴格意義上講 Executor 並不是一個線程池,而
只是一個執行線程的工具。真正的線程池接口是 ExecutorService。

在這裏插入圖片描述

方式一:通過構造方法實現
在這裏插入圖片描述

**方式2:通過Executor 框架的工具類Executors來實現 **

  1. newCachedThreadPool:創建一個可根據需要創建新線程的線程池,但是在以前構造的線程可用時將重用它們。對於執行很多短期異步任務的程序而言,這些線程池通常可提高程序性能。調用 execute 將重用以前構造的線程(如果線程可用)。如果現有線程沒有可用的,則創建一個新線程並添加到池中。終止並從緩存中移除那些已有 60 秒鐘未被使用的線程。因此,長時間保持空閒的線程池不會使用任何資源。

  2. newScheduledThreadPool:創建一個線程池,它可安排在給定延遲後運行命令或者定期地執行。

  3. newSingleThreadExecutor:Executors.newSingleThreadExecutor()返回一個線程池(這個線程池只有一個線程),這個線程池可以在線程死後(或發生異常時)重新啓動一個線程來替代原來的線程繼續執行下去!

5.Atomic 原子類

5.1. 介紹一下Atomic 原子類

Atomic 是指一個操作是不可中斷的。即使是在多個線程一起執行的時候,一個操作一旦開始,就不會被其他線程干擾。
所以,所謂原子類說簡單點就是具有原子/原子操作特徵的類

5.2. JUC 包中的原子類是哪4類?

基本類型

使用原子的方式更新基本類型

AtomicInteger:整形原子類
AtomicLong:長整型原子類
AtomicBoolean:布爾型原子類

數組類型

使用原子的方式更新數組裏的某個元素

AtomicIntegerArray:整形數組原子類
AtomicLongArray:長整形數組原子類
AtomicReferenceArray:引用類型數組原子類

引用類型

AtomicReference:引用類型原子類
AtomicStampedReference:原子更新引用類型裏的字段原子類
AtomicMarkableReference :原子更新帶有標記位的引用類型

對象的屬性修改類型

AtomicIntegerFieldUpdater:原子更新整形字段的更新器
AtomicLongFieldUpdater:原子更新長整形字段的更新器
AtomicStampedReference:原子更新帶有版本號的引用類型。該類將整數值與引用關聯起來,可用於解決原子的更新數據和數據的版本號,可以解決使用 CAS 進行原子更新時可能出現的 ABA 問題。

6 . AQS

6.1 什麼是AQS

AbstractQueuedSynchronizer 類如其名,抽象的隊列式的同步器,AQS 定義了一套多線程訪問共享資源的同步器框架,許多同步類實現都依賴於它,如常用的ReentrantLock/Semaphore/CountDownLatch。

6.2 AQS原理

6.2.1 AQS原理介紹

AQS核心思想是,如果被請求的共享資源空閒,則將當前請求資源的線程設置爲有效的工作線程,並且將共享資源設置爲鎖定狀態。如果被請求的共享資源被佔用,那麼就需要一套線程阻塞等待以及被喚醒時鎖分配的機制,這個機制AQS是用CLH隊列鎖實現的,即將暫時獲取不到鎖的線程加入到隊列中。

AQS原理圖:
在這裏插入圖片描述

它維護了一個 volatile int state(代表共享資源)和一個 FIFO 線程等待隊列(多線程爭用資源被
阻塞時會進入此隊列)。

6.2.2 AQS共享資源的方式

AQS 定義兩種資源共享方式

Exclusive 獨佔資源-ReentrantLock
Exclusive(獨佔,只有一個線程能執行,如 ReentrantLock)

Share 共享資源-Semaphore/CountDownLatch
Share(共享,多個線程可同時執行,如 Semaphore/CountDownLatch)。‘’

6.2.3. AQS底層使用了模板方法模式

AQS 只是一個框架,具體資源的獲取/釋放方式交由自定義同步器去實現,AQS 這裏只定義了一個
接口,具體資源的獲取交由自定義同步器去實現了(通過 state 的 get/set/CAS)之所以沒有定義成
abstract ,是因爲獨佔模式下只用實現 tryAcquire-tryRelease ,而共享模式下只用實現
tryAcquireShared-tryReleaseShared。如果都定義成abstract,那麼每個模式也要去實現另一模
式下的接口。不同的自定義同步器爭用共享資源的方式也不同。自定義同步器在實現時只需要實
現共享資源 state 的獲取與釋放方式即可,至於具體線程等待隊列的維護(如獲取資源失敗入隊/
喚醒出隊等),AQS 已經在頂層實現好了。自定義同步器實現時主要實現以下幾種方法:

1.isHeldExclusively():該線程是否正在獨佔資源。只有用到 condition 才需要去實現它。
2.tryAcquire(int):獨佔方式。嘗試獲取資源,成功則返回 true,失敗則返回 false。
3.tryRelease(int):獨佔方式。嘗試釋放資源,成功則返回 true,失敗則返回 false。
4.tryAcquireShared(int):共享方式。嘗試獲取資源。負數表示失敗;0 表示成功,但沒有剩餘
可用資源;正數表示成功,且有剩餘資源。
5.tryReleaseShared(int):共享方式。嘗試釋放資源,如果釋放後允許喚醒後續等待結點返回
true,否則返回 false。

同步器的實現是 ABS 核心(state 資源狀態計數)

同步器的實現是 ABS 核心,以 ReentrantLock 爲例,state 初始化爲 0,表示未鎖定狀態。A 線程lock()時,會調用 tryAcquire()獨佔該鎖並將 state+1。此後,其他線程再 tryAcquire()時就會失
敗,直到 A 線程 unlock()到 state=0(即釋放鎖)爲止,其它線程纔有機會獲取該鎖。當然,釋放
鎖之前,A 線程自己是可以重複獲取此鎖的(state 會累加),這就是可重入的概念。但要注意,
獲取多少次就要釋放多麼次,這樣才能保證 state 是能回到零態的。

以 CountDownLatch 以例,任務分爲 N 個子線程去執行,state 也初始化爲 N(注意 N 要與
線程個數一致)。這 N 個子線程是並行執行的,每個子線程執行完後 countDown()一次,state
會 CAS 減 1。等到所有子線程都執行完後(即 state=0),會 unpark()主調用線程,然後主調用線程
就會從 await()函數返回,繼續後餘動作。

ReentrantReadWriteLock 實現獨佔和共享兩種方式 一般來說,自定義同步器要麼是獨佔方法,要麼是共享方式,他們也只需實現 tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared 中的一種即可。但 AQS 也支持自定義同步器同時實現獨佔和共享兩種方式,如 ReentrantReadWriteLock。