JUC併發包與容器類 - 面試題(一網打淨,持續更新)


JUC 高併發工具類(3文章)與高併發容器類(N文章) :

內存可見性、指令有序性 理論

Java內存模型

重排序與數據依賴性

爲何代碼會重排序?

在執行程序時,爲了提供性能,處理器和編譯器經常會對指令進行重排序,可是不能隨意重排序,不是你想怎麼排序就怎麼排序,它須要知足如下兩個條件:面試

  • 在單線程環境下不能改變程序運行的結果;
  • 存在數據依賴關係的不容許重排序

須要注意的是:重排序不會影響單線程環境的執行結果,可是會破壞多線程的執行語義。算法

as-if-serial規則和happens-before規則的區別

  • as-if-serial語義保證單線程內程序的執行結果不被改變,happens-before關係保證正確同步的多線程程序的執行結果不被改變。
  • as-if-serial語義給編寫單線程程序的程序員創造了一個幻境:單線程程序是按程序的順序來執行的。happens-before關係給編寫正確同步的多線程程序的程序員創造了一個幻境:正確同步的多線程程序是按happens-before指定的順序來執行的。
  • as-if-serial語義和happens-before這麼作的目的,都是爲了在不改變程序執行結果的前提下,儘量地提升程序執行的並行度。

volatile 內存可見性

volatile 關鍵字的做用

對於可見性,Java 提供了 volatile 關鍵字來保證可見性和禁止指令重排。 volatile 提供 happens-before 的保證,確保一個線程的修改能對其餘線程是可見的。當一個共享變量被 volatile 修飾時,它會保證修改的值會當即被更新到主存,當有其餘線程須要讀取時,它會去內存中讀取新值。數據庫

從實踐角度而言,volatile 的一個重要做用就是和 CAS 結合,保證了原子性,詳細的能夠參見 java.util.concurrent.atomic 包下的類,好比 AtomicInteger。編程

volatile 經常使用於多線程環境下的單次操做(單次讀或者單次寫)。api

Java 中能建立 volatile 數組嗎?

能,Java 中能夠建立 volatile 類型數組,不過只是一個指向數組的引用,而不是整個數組。意思是,若是改變引用指向的數組,將會受到 volatile 的保護,可是若是多個線程同時改變數組的元素,volatile 標示符就不能起到以前的保護做用了。數組

volatile 變量和 atomic 變量有什麼不一樣?

volatile 變量能夠確保先行關係,即寫操做會發生在後續的讀操做以前, 但它並不能保證原子性。例如用 volatile 修飾 count 變量,那麼 count++ 操做就不是原子性的。緩存

而 AtomicInteger 類提供的 atomic 方法可讓這種操做具備原子性如getAndIncrement()方法會原子性的進行增量操做把當前值加一,其它數據類型和引用變量也能夠進行類似操做。

volatile 能使得一個非原子操做變成原子操做嗎?

關鍵字volatile的主要做用是使變量在多個線程間可見,但沒法保證原子性,對於多個線程訪問同一個實例變量須要加鎖進行同步。

雖然volatile只能保證可見性不能保證原子性,但用volatile修飾long和double能夠保證其操做原子性。

因此從Oracle Java Spec裏面能夠看到:

  • 對於64位的long和double,若是沒有被volatile修飾,那麼對其操做能夠不是原子的。在操做的時候,能夠分紅兩步,每次對32位操做。
  • 若是使用volatile修飾long和double,那麼其讀寫都是原子操做
  • 對於64位的引用地址的讀寫,都是原子操做
  • 在實現JVM時,能夠自由選擇是否把讀寫long和double做爲原子操做
  • 推薦JVM實現爲原子操做

volatile 修飾符的有過什麼實踐?

單例模式

是否 Lazy 初始化:是

是否多線程安全:是

實現難度:較複雜

描述:對於Double-Check這種可能出現的問題(固然這種機率已經很是小了,但畢竟仍是有的嘛~),解決方案是:只須要給instance的聲明加上volatile關鍵字便可volatile關鍵字的一個做用是禁止指令重排,把instance聲明爲volatile以後,對它的寫操做就會有一個內存屏障(什麼是內存屏障?),這樣,在它的賦值完成以前,就不用會調用讀操做。注意:volatile阻止的不是singleton = newSingleton()這句話內部[1-2-3]的指令重排,而是保證了在一個寫操做([1-2-3])完成以前,不會調用讀操做(if (instance == null))。

public class Singleton7 {

    private static volatile Singleton7 instance = null;

    private Singleton7() {}

    public static Singleton7 getInstance() {
        if (instance == null) {
            synchronized (Singleton7.class) {
                if (instance == null) {
                    instance = new Singleton7();
                }
            }
        }

        return instance;
    }

}
12345678910111213141516171819

synchronized 和 volatile 的區別是什麼?

synchronized 表示只有一個線程能夠獲取做用對象的鎖,執行代碼,阻塞其餘線程。

volatile 表示變量在 CPU 的寄存器中是不肯定的,必須從主存中讀取。保證多線程環境下變量的可見性;禁止指令重排序。

區別

  • volatile 是變量修飾符;synchronized 能夠修飾類、方法、變量。
  • volatile 僅能實現變量的修改可見性,不能保證原子性;而 synchronized 則能夠保證變量的修改可見性和原子性。
  • volatile 不會形成線程的阻塞;synchronized 可能會形成線程的阻塞。
  • volatile標記的變量不會被編譯器優化;synchronized標記的變量能夠被編譯器優化。
  • volatile關鍵字是線程同步的輕量級實現,因此volatile性能確定比synchronized關鍵字要好。可是volatile關鍵字只能用於變量而synchronized關鍵字能夠修飾方法以及代碼塊。synchronized關鍵字在JavaSE1.6以後進行了主要包括爲了減小得到鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖以及其它各類優化以後執行效率有了顯著提高,實際開發中使用 synchronized 關鍵字的場景仍是更多一些

final

什麼是不可變對象,它對寫併發應用有什麼幫助?

不可變對象(Immutable Objects)即對象一旦被建立它的狀態(對象的數據,也即對象屬性值)就不能改變,反之即爲可變對象(Mutable Objects)。

不可變對象的類即爲不可變類(Immutable Class)。Java 平臺類庫中包含許多不可變類,如 String、基本類型的包裝類、BigInteger 和 BigDecimal 等。

只有知足以下狀態,一個對象纔是不可變的;

  • 它的狀態不能在建立後再被修改;
  • 全部域都是 final 類型;而且,它被正確建立(建立期間沒有發生 this 引用的逸出)。

不可變對象保證了對象的內存可見性,對不可變對象的讀取不須要進行額外的同步手段,提高了代碼執行效率。

GC

Java中垃圾回收有什麼目的?何時進行垃圾回收?

垃圾回收是在內存中存在沒有引用的對象或超過做用域的對象時進行的。

垃圾回收的目的是識別而且丟棄應用再也不使用的對象來釋放和重用資源。

若是對象的引用被置爲null,垃圾收集器是否會當即釋放對象佔用的內存?

不會,在下一個垃圾回調週期中,這個對象將是被可回收的。

也就是說並不會當即被垃圾收集器馬上回收,而是在下一次垃圾回收時纔會釋放其佔用的內存。

finalize()方法何時被調用?析構函數(finalization)的目的是什麼?

1)垃圾回收器(garbage colector)決定回收某對象時,就會運行該對象的finalize()方法;
finalize是Object類的一個方法,該方法在Object類中的聲明protected void finalize() throws Throwable { }
在垃圾回收器執行時會調用被回收對象的finalize()方法,能夠覆蓋此方法來實現對其資源的回收。注意:一旦垃圾回收器準備釋放對象佔用的內存,將首先調用該對象的finalize()方法,而且下一次垃圾回收動做發生時,才真正回收對象佔用的內存空間

2)GC原本就是內存回收了,應用還須要在finalization作什麼呢? 答案是大部分時候,什麼都不用作(也就是不須要重載)。只有在某些很特殊的狀況下,好比你調用了一些native的方法(通常是C寫的),能夠要在finaliztion裏去調用C的釋放函數。

CAS原子操做

什麼是 CAS

CAS 是 compare and swap 的縮寫,即咱們所說的比較交換。

cas 是一種基於鎖的操做,並且是樂觀鎖。在 java 中鎖分爲樂觀鎖和悲觀鎖。悲觀鎖是將資源鎖住,等一個以前得到鎖的線程釋放鎖以後,下一個線程才能夠訪問。而樂觀鎖採起了一種寬泛的態度,經過某種方式不加鎖來處理資源,好比經過給記錄加 version 來獲取數據,性能較悲觀鎖有很大的提升。

CAS 操做包含三個操做數 —— 內存位置(V)、預期原值(A)和新值(B)。若是內存地址裏面的值和 A 的值是同樣的,那麼就將內存裏面的值更新成 B。CAS是經過無限循環來獲取數據的,若果在第一輪循環中,a 線程獲取地址裏面的值被b 線程修改了,那麼 a 線程須要自旋,到下次循環纔有可能機會執行。

java.util.concurrent.atomic 包下的類大可能是使用 CAS 操做來實現的(AtomicInteger,AtomicBoolean,AtomicLong)。

CAS 的會產生什麼問題?

一、ABA 問題:

好比說一個線程 one 從內存位置 V 中取出 A,這時候另外一個線程 two 也從內存中取出 A,而且 two 進行了一些操做變成了 B,而後 two 又將 V 位置的數據變成 A,這時候線程 one 進行 CAS 操做發現內存中仍然是 A,而後 one 操做成功。儘管線程 one 的 CAS 操做成功,但可能存在潛藏的問題。從 Java1.5 開始 JDK 的 atomic包裏提供了一個類 AtomicStampedReference 來解決 ABA 問題。

二、循環時間長開銷大:

對於資源競爭嚴重(線程衝突嚴重)的狀況,CAS 自旋的機率會比較大,從而浪費更多的 CPU 資源,效率低於 synchronized。

三、只能保證一個共享變量的原子操做:

當對一個共享變量執行操做時,咱們可使用循環 CAS 的方式來保證原子操做,可是對多個共享變量操做時,循環 CAS 就沒法保證操做的原子性,這個時候就能夠用鎖。

Lock顯示鎖

Lock 接口(Lock interface)是什麼?對比同步它有什麼優點?

Lock 接口比同步方法和同步塊提供了更具擴展性的鎖操做。他們容許更靈活的結構,能夠具備徹底不一樣的性質,而且能夠支持多個相關類的條件對象。

它的優點有:

(1)可使鎖更公平

(2)可使線程在等待鎖的時候響應中斷

(3)可讓線程嘗試獲取鎖,並在沒法獲取鎖的時候當即返回或者等待一段時間

(4)能夠在不一樣的範圍,以不一樣的順序獲取和釋放鎖

總體上來講 Lock 是 synchronized 的擴展版,Lock 提供了無條件的、可輪詢的(tryLock 方法)、定時的(tryLock 帶參方法)、可中斷的(lockInterruptibly)、可多條件隊列的(newCondition 方法)鎖操做。另外 Lock 的實現類基本都支持非公平鎖(默認)和公平鎖,synchronized 只支持非公平鎖,固然,在大部分狀況下,非公平鎖是高效的選擇。

樂觀鎖和悲觀鎖的理解及如何實現,有哪些實現方式?

悲觀鎖:老是假設最壞的狀況,每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖。傳統的關係型數據庫裏邊就用到了不少這種鎖機制,好比行鎖,表鎖等,讀鎖,寫鎖等,都是在作操做以前先上鎖。再好比 Java 裏面的同步原語 synchronized 關鍵字的實現也是悲觀鎖。

樂觀鎖:顧名思義,就是很樂觀,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可使用版本號等機制。樂觀鎖適用於多讀的應用類型,這樣能夠提升吞吐量,像數據庫提供的相似於 write_condition 機制,其實都是提供的樂觀鎖。在 Java中 java.util.concurrent.atomic 包下面的原子變量類就是使用了樂觀鎖的一種實現方式 CAS 實現的。

樂觀鎖的實現方式:

一、使用版本標識來肯定讀到的數據與提交時的數據是否一致。提交後修改版本標識,不一致時能夠採起丟棄和再次嘗試的策略。

二、java 中的 Compare and Swap 即 CAS ,當多個線程嘗試使用 CAS 同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程並不會被掛起,而是被告知此次競爭中失敗,並能夠再次嘗試。 CAS 操做中包含三個操做數 —— 須要讀寫的內存位置(V)、進行比較的預期原值(A)和擬寫入的新值(B)。若是內存位置 V 的值與預期原值 A 相匹配,那麼處理器會自動將該位置值更新爲新值 B。不然處理器不作任何操做。

ReentrantLock(重入鎖)實現原理與公平鎖非公平鎖區別

什麼是可重入鎖(ReentrantLock)?

ReentrantLock重入鎖,是實現Lock接口的一個類,也是在實際編程中使用頻率很高的一個鎖,支持重入性,表示可以對共享資源可以重複加鎖,即當前線程獲取該鎖再次獲取不會被阻塞。

在java關鍵字synchronized隱式支持重入性,synchronized經過獲取自增,釋放自減的方式實現重入。與此同時,ReentrantLock還支持公平鎖和非公平鎖兩種方式。那麼,要想完徹底全的弄懂ReentrantLock的話,主要也就是ReentrantLock同步語義的學習:1. 重入性的實現原理;2. 公平鎖和非公平鎖。

重入性的實現原理

要想支持重入性,就要解決兩個問題:1. 在線程獲取鎖的時候,若是已經獲取鎖的線程是當前線程的話則直接再次獲取成功;2. 因爲鎖會被獲取n次,那麼只有鎖在被釋放一樣的n次以後,該鎖纔算是徹底釋放成功

ReentrantLock支持兩種鎖:公平鎖非公平鎖何謂公平性,是針對獲取鎖而言的,若是一個鎖是公平的,那麼鎖的獲取順序就應該符合請求上的絕對時間順序,知足FIFO

讀寫鎖ReentrantReadWriteLock源碼分析

ReadWriteLock 是什麼

首先明確一下,不是說 ReentrantLock 很差,只是 ReentrantLock 某些時候有侷限。若是使用 ReentrantLock,可能自己是爲了防止線程 A 在寫數據、線程 B 在讀數據形成的數據不一致,但這樣,若是線程 C 在讀數據、線程 D 也在讀數據,讀數據是不會改變數據的,沒有必要加鎖,可是仍是加鎖了,下降了程序的性能。由於這個,才誕生了讀寫鎖 ReadWriteLock。

ReadWriteLock 是一個讀寫鎖接口,讀寫鎖是用來提高併發程序性能的鎖分離技術,ReentrantReadWriteLock 是 ReadWriteLock 接口的一個具體實現,實現了讀寫的分離,讀鎖是共享的,寫鎖是獨佔的,讀和讀之間不會互斥,讀和寫、寫和讀、寫和寫之間纔會互斥,提高了讀寫的性能。

而讀寫鎖有如下三個重要的特性:

(1)公平選擇性:支持非公平(默認)和公平的鎖獲取方式,吞吐量仍是非公平優於公平。

(2)重進入:讀鎖和寫鎖都支持線程重進入。

(3)鎖降級:遵循獲取寫鎖、獲取讀鎖再釋放寫鎖的次序,寫鎖可以降級成爲讀鎖。

AQS抽象同步隊列

AQS 介紹

AQS的全稱爲(AbstractQueuedSynchronizer),這個類在java.util.concurrent.locks包下面。

AQS類

AQS是一個用來構建鎖和同步器的框架,使用AQS能簡單且高效地構造出應用普遍的大量的同步器,好比咱們提到的ReentrantLock,Semaphore,其餘的諸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基於AQS的。固然,咱們本身也能利用AQS很是輕鬆容易地構造出符合咱們本身需求的同步器。

AQS 原理分析

下面大部份內容其實在AQS類註釋上已經給出了,不過是英語看着比較吃力一點,感興趣的話能夠看看源碼。

AQS 原理概覽

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

CLH(Craig,Landin,and Hagersten)隊列是一個虛擬的雙向隊列(虛擬的雙向隊列即不存在隊列實例,僅存在結點之間的關聯關係)。AQS是將每條請求共享資源的線程封裝成一個CLH鎖隊列的一個結點(Node)來實現鎖的分配。

看個AQS(AbstractQueuedSynchronizer)原理圖:

AQS原理圖

AQS使用一個int成員變量來表示同步狀態,經過內置的FIFO隊列來完成獲取資源線程的排隊工做。AQS使用CAS對該同步狀態進行原子操做實現對其值的修改。

private volatile int state;//共享變量,使用volatile修飾保證線程可見性
1

狀態信息經過protected類型的getState,setState,compareAndSetState進行操做

//返回同步狀態的當前值
protected final int getState() {  
        return state;
}
 // 設置同步狀態的值
protected final void setState(int newState) { 
        state = newState;
}
//原子地(CAS操做)將同步狀態值設置爲給定值update若是當前同步狀態的值等於expect(指望值)
protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
123456789101112

AQS 對資源的共享方式

AQS定義兩種資源共享方式

  • Exclusive(獨佔):只有一個線程能執行,如ReentrantLock。又可分爲公平鎖和非公平鎖:
    • 公平鎖:按照線程在隊列中的排隊順序,先到者先拿到鎖
    • 非公平鎖:當線程要獲取鎖時,無視隊列順序直接去搶鎖,誰搶到就是誰的
  • Share(共享):多個線程可同時執行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 咱們都會在後面講到。

ReentrantReadWriteLock 能夠當作是組合式,由於ReentrantReadWriteLock也就是讀寫鎖容許多個線程同時對某一資源進行讀。

不一樣的自定義同步器爭用共享資源的方式也不一樣。自定義同步器在實現時只須要實現共享資源 state 的獲取與釋放方式便可,至於具體線程等待隊列的維護(如獲取資源失敗入隊/喚醒出隊等),AQS已經在頂層實現好了。

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

同步器的設計是基於模板方法模式的,若是須要自定義同步器通常的方式是這樣(模板方法模式很經典的一個應用):

  1. 使用者繼承AbstractQueuedSynchronizer並重寫指定的方法。(這些重寫方法很簡單,無非是對於共享資源state的獲取和釋放)
  2. 將AQS組合在自定義同步組件的實現中,並調用其模板方法,而這些模板方法會調用使用者重寫的方法。

這和咱們以往經過實現接口的方式有很大區別,這是模板方法模式很經典的一個運用。

AQS使用了模板方法模式,自定義同步器時須要重寫下面幾個AQS提供的模板方法:

isHeldExclusively()//該線程是否正在獨佔資源。只有用到condition才須要去實現它。
tryAcquire(int)//獨佔方式。嘗試獲取資源,成功則返回true,失敗則返回false。
tryRelease(int)//獨佔方式。嘗試釋放資源,成功則返回true,失敗則返回false。
tryAcquireShared(int)//共享方式。嘗試獲取資源。負數表示失敗;0表示成功,但沒有剩餘可用資源;正數表示成功,且有剩餘資源。
tryReleaseShared(int)//共享方式。嘗試釋放資源,成功則返回true,失敗則返回false。

123456

默認狀況下,每一個方法都拋出 UnsupportedOperationException。 這些方法的實現必須是內部線程安全的,而且一般應該簡短而不是阻塞。AQS類中的其餘方法都是final ,因此沒法被其餘類使用,只有這幾個方法能夠被其餘類使用。

以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(Compare and Swap)減1。等到全部子線程都執行完後(即state=0),會unpark()主調用線程,而後主調用線程就會從await()函數返回,繼續後餘動做。

通常來講,自定義同步器要麼是獨佔方法,要麼是共享方式,他們也只需實現tryAcquire-tryReleasetryAcquireShared-tryReleaseShared中的一種便可。但AQS也支持自定義同步器同時實現獨佔和共享兩種方式,如ReentrantReadWriteLock

高併發容器

併發容器之ConcurrentHashMap詳解(JDK1.8版本)與源碼分析

什麼是ConcurrentHashMap?

ConcurrentHashMap是Java中的一個線程安全且高效的HashMap實現。平時涉及高併發若是要用map結構,那第一時間想到的就是它。相對於hashmap來講,ConcurrentHashMap就是線程安全的map,其中利用了鎖分段的思想提升了併發度。

那麼它究竟是如何實現線程安全的?

JDK 1.6版本關鍵要素:

  • segment繼承了ReentrantLock充當鎖的角色,爲每個segment提供了線程安全的保障;
  • segment維護了哈希散列表的若干個桶,每一個桶由HashEntry構成的鏈表。

JDK1.8後,ConcurrentHashMap拋棄了原有的Segment 分段鎖,而採用了 CAS + synchronized 來保證併發安全性

Java 中 ConcurrentHashMap 的併發度是什麼?

ConcurrentHashMap 把實際 map 劃分紅若干部分來實現它的可擴展性和線程安全。這種劃分是使用併發度得到的,它是 ConcurrentHashMap 類構造函數的一個可選參數,默認值爲 16,這樣在多線程狀況下就能避免爭用。

在 JDK8 後,它摒棄了 Segment(鎖段)的概念,而是啓用了一種全新的方式實現,利用 CAS 算法。同時加入了更多的輔助變量來提升併發度,具體內容仍是查看源碼吧。

什麼是併發容器的實現?

何爲同步容器:能夠簡單地理解爲經過 synchronized 來實現同步的容器,若是有多個線程調用同步容器的方法,它們將會串行執行。好比 Vector,Hashtable,以及 Collections.synchronizedSet,synchronizedList 等方法返回的容器。能夠經過查看 Vector,Hashtable 等這些同步容器的實現代碼,能夠看到這些容器實現線程安全的方式就是將它們的狀態封裝起來,並在須要同步的方法上加上關鍵字 synchronized。

併發容器使用了與同步容器徹底不一樣的加鎖策略來提供更高的併發性和伸縮性,例如在 ConcurrentHashMap 中採用了一種粒度更細的加鎖機制,能夠稱爲分段鎖,在這種鎖機制下,容許任意數量的讀線程併發地訪問 map,而且執行讀操做的線程和寫操做的線程也能夠併發的訪問 map,同時容許必定數量的寫操做線程併發地修改 map,因此它能夠在併發環境下實現更高的吞吐量。

Java 中的同步集合與併發集合有什麼區別?

同步集合與併發集合都爲多線程和併發提供了合適的線程安全的集合,不過併發集合的可擴展性更高。在 Java1.5 以前程序員們只有同步集合來用且在多線程併發的時候會致使爭用,阻礙了系統的擴展性。Java5 介紹了併發集合像ConcurrentHashMap,不只提供線程安全還用鎖分離和內部分區等現代技術提升了可擴展性。

SynchronizedMap 和 ConcurrentHashMap 有什麼區別?

SynchronizedMap 一次鎖住整張表來保證線程安全,因此每次只能有一個線程來訪爲 map。

ConcurrentHashMap 使用分段鎖來保證在多線程下的性能。

ConcurrentHashMap 中則是一次鎖住一個桶。ConcurrentHashMap 默認將hash 表分爲 16 個桶,諸如 get,put,remove 等經常使用操做只鎖當前須要用到的桶。

這樣,原來只能一個線程進入,如今卻能同時有 16 個寫線程執行,併發性能的提高是顯而易見的。

另外 ConcurrentHashMap 使用了一種不一樣的迭代方式。在這種迭代方式中,當iterator 被建立後集合再發生改變就再也不是拋出ConcurrentModificationException,取而代之的是在改變時 new 新的數據從而不影響原有的數據,iterator 完成後再將頭指針替換爲新的數據 ,這樣 iterator線程可使用原來老的數據,而寫線程也能夠併發的完成改變。

併發容器之CopyOnWriteArrayList詳解

CopyOnWriteArrayList 是什麼,能夠用於什麼應用場景?有哪些優缺點?

CopyOnWriteArrayList 是一個併發容器。有不少人稱它是線程安全的,我認爲這句話不嚴謹,缺乏一個前提條件,那就是非複合場景下操做它是線程安全的。

CopyOnWriteArrayList(免鎖容器)的好處之一是當多個迭代器同時遍歷和修改這個列表時,不會拋出 ConcurrentModificationException。在CopyOnWriteArrayList 中,寫入將致使建立整個底層數組的副本,而源數組將保留在原地,使得複製的數組在被修改時,讀取操做能夠安全地執行。

CopyOnWriteArrayList 的使用場景

經過源碼分析,咱們看出它的優缺點比較明顯,因此使用場景也就比較明顯。就是合適讀多寫少的場景。

CopyOnWriteArrayList 的缺點

  1. 因爲寫操做的時候,須要拷貝數組,會消耗內存,若是原數組的內容比較多的狀況下,可能致使 young gc 或者 full gc。
  2. 不能用於實時讀的場景,像拷貝數組、新增元素都須要時間,因此調用一個 set 操做後,讀取到數據可能仍是舊的,雖然CopyOnWriteArrayList 能作到最終一致性,可是仍是無法知足實時性要求。
  3. 因爲實際使用中可能無法保證 CopyOnWriteArrayList 到底要放置多少數據,萬一數據稍微有點多,每次 add/set 都要從新複製數組,這個代價實在過高昂了。在高性能的互聯網應用中,這種操做分分鐘引發故障。

CopyOnWriteArrayList 的設計思想

  1. 讀寫分離,讀和寫分開
  2. 最終一致性
  3. 使用另外開闢空間的思路,來解決併發衝突

併發容器之BlockingQueue詳解

什麼是阻塞隊列?阻塞隊列的實現原理是什麼?如何使用阻塞隊列來實現生產者-消費者模型?

阻塞隊列(BlockingQueue)是一個支持兩個附加操做的隊列。

這兩個附加的操做是:在隊列爲空時,獲取元素的線程會等待隊列變爲非空。當隊列滿時,存儲元素的線程會等待隊列可用。

阻塞隊列經常使用於生產者和消費者的場景,生產者是往隊列裏添加元素的線程,消費者是從隊列裏拿元素的線程。阻塞隊列就是生產者存放元素的容器,而消費者也只從容器裏拿元素。

JDK7 提供了 7 個阻塞隊列。分別是:

ArrayBlockingQueue :一個由數組結構組成的有界阻塞隊列。

LinkedBlockingQueue :一個由鏈表結構組成的有界阻塞隊列。

PriorityBlockingQueue :一個支持優先級排序的無界阻塞隊列。

DelayQueue:一個使用優先級隊列實現的無界阻塞隊列。

SynchronousQueue:一個不存儲元素的阻塞隊列。

LinkedTransferQueue:一個由鏈表結構組成的無界阻塞隊列。

LinkedBlockingDeque:一個由鏈表結構組成的雙向阻塞隊列。

Java 5 以前實現同步存取時,可使用普通的一個集合,而後在使用線程的協做和線程同步能夠實現生產者,消費者模式,主要的技術就是用好,wait,notify,notifyAll,sychronized 這些關鍵字。而在 java 5 以後,可使用阻塞隊列來實現,此方式大大簡少了代碼量,使得多線程編程更加容易,安全方面也有保障。

BlockingQueue 接口是 Queue 的子接口,它的主要用途並非做爲容器,而是做爲線程同步的的工具,所以他具備一個很明顯的特性,當生產者線程試圖向 BlockingQueue 放入元素時,若是隊列已滿,則線程被阻塞,當消費者線程試圖從中取出一個元素時,若是隊列爲空,則該線程會被阻塞,正是由於它所具備這個特性,因此在程序中多個線程交替向 BlockingQueue 中放入元素,取出元素,它能夠很好的控制線程之間的通訊。

阻塞隊列使用最經典的場景就是 socket 客戶端數據的讀取和解析,讀取數據的線程不斷將數據放入隊列,而後解析線程不斷從隊列取數據解析。

併發容器之ConcurrentLinkedQueue詳解

ConcurrentLinkedQueue非阻塞無界鏈表隊列

ConcurrentLinkedQueue是一個線程安全的隊列,基於鏈表結構實現,是一個無界隊列,理論上來講隊列的長度能夠無限擴大。

與其餘隊列相同,ConcurrentLinkedQueue也採用的是先進先出(FIFO)入隊規則,對元素進行排序。 (推薦學習:java面試題目)

當咱們向隊列中添加元素時,新插入的元素會插入到隊列的尾部;而當咱們獲取一個元素時,它會從隊列的頭部中取出。

由於ConcurrentLinkedQueue是鏈表結構,因此當入隊時,插入的元素依次向後延伸,造成鏈表;而出隊時,則從鏈表的第一個元素開始獲取,依次遞增;

值得注意的是,在使用ConcurrentLinkedQueue時,若是涉及到隊列是否爲空的判斷,切記不可以使用size()==0的作法,由於在size()方法中,是經過遍歷整個鏈表來實現的,在隊列元素不少的時候,size()方法十分消耗性能和時間,只是單純的判斷隊列爲空使用isEmpty()便可。

BlockingQueue拯救了生產者、消費者模型的控制邏輯

經典的「生產者」和「消費者」模型中,在concurrent包發佈之前,在多線程環境下,咱們每一個程序員都必須去本身控制這些細節,尤爲還要兼顧效率和線程安全,而這會給咱們的程序帶來不小的複雜度。好在此時,強大的concurrent包橫空出世了,而他也給咱們帶來了強大的BlockingQueue。(在多線程領域:所謂阻塞,在某些狀況下會掛起線程(即阻塞),一旦條件知足,被掛起的線程又會自動被喚醒)

BlockingQueue的成員介紹

由於它隸屬於集合家族,本身又是個接口。因此是有不少成員的,下面簡單介紹一下

1. ArrayBlockingQueue

基於數組的阻塞隊列實現,在ArrayBlockingQueue內部,維護了一個定長數組,以便緩存隊列中的數據對象,這是一個經常使用的阻塞隊列,除了一個定長數組外,ArrayBlockingQueue內部還保存着兩個整形變量,分別標識着隊列的頭部和尾部在數組中的位置。 ArrayBlockingQueue在生產者放入數據和消費者獲取數據,都是共用同一個鎖對象,由此也意味着二者沒法真正並行運行,這點尤爲不一樣於LinkedBlockingQueue;按照實現原理來分析,ArrayBlockingQueue徹底能夠採用分離鎖,從而實現生產者和消費者操做的徹底並行運行。Doug Lea之因此沒這樣去作,也許是由於ArrayBlockingQueue的數據寫入和獲取操做已經足夠輕巧,以致於引入獨立的鎖機制,除了給代碼帶來額外的複雜性外,其在性能上徹底佔不到任何便宜。 ArrayBlockingQueue和LinkedBlockingQueue間還有一個明顯的不一樣之處在於,前者在插入或刪除元素時不會產生或銷燬任何額外的對象實例,然後者則會生成一個額外的Node對象。這在長時間內須要高效併發地處理大批量數據的系統中,其對於GC的影響仍是存在必定的區別。而在建立ArrayBlockingQueue時,咱們還能夠控制對象的內部鎖是否採用公平鎖,默認採用非公平鎖。

2.LinkedBlockingQueue

基於鏈表的阻塞隊列,同ArrayListBlockingQueue相似,其內部也維持着一個數據緩衝隊列(該隊列由一個鏈表構成),當生產者往隊列中放入一個數據時,隊列會從生產者手中獲取數據,並緩存在隊列內部,而生產者當即返回;只有當隊列緩衝區達到最大值緩存容量時(LinkedBlockingQueue能夠經過構造函數指定該值),纔會阻塞生產者隊列,直到消費者從隊列中消費掉一份數據,生產者線程會被喚醒,反之對於消費者這端的處理也基於一樣的原理。而LinkedBlockingQueue之因此可以高效的處理併發數據,還由於其對於生產者端和消費者端分別採用了獨立的鎖來控制數據同步,這也意味着在高併發的狀況下生產者和消費者能夠並行地操做隊列中的數據,以此來提升整個隊列的併發性能。 做爲開發者,咱們須要注意的是,若是構造一個LinkedBlockingQueue對象,而沒有指定其容量大小,LinkedBlockingQueue會默認一個相似無限大小的容量(Integer.MAX_VALUE),這樣的話,若是生產者的速度一旦大於消費者的速度,也許尚未等到隊列滿阻塞產生,系統內存就有可能已被消耗殆盡了。

3. DelayQueue 延遲隊列

DelayQueue中的元素只有當其指定的延遲時間到了,纔可以從隊列中獲取到該元素。DelayQueue是一個沒有大小限制的隊列,所以往隊列中插入數據的操做(生產者)永遠不會被阻塞,而只有獲取數據的操做(消費者)纔會被阻塞,因此必定要注意內存的使用。 使用場景:   DelayQueue使用場景較少,但都至關巧妙,常見的例子好比使用一個DelayQueue來管理一個超時未響應的鏈接隊列。

4. PriorityBlockingQueue

基於優先級的阻塞隊列(優先級的判斷經過構造函數傳入的Compator對象來決定),但須要注意的是PriorityBlockingQueue並不會阻塞數據生產者,而只會在沒有可消費的數據時,阻塞數據的消費者。所以使用的時候要特別注意,生產者生產數據的速度絕對不能快於消費者消費數據的速度,不然時間一長,會最終耗盡全部的可用堆內存空間。在實現PriorityBlockingQueue時,內部控制線程同步的鎖採用的是公平鎖。

5. SynchronousQueue

一種無緩衝的等待隊列,相似於無中介的直接交易,有點像原始社會中的生產者和消費者,生產者拿着產品去集市銷售給產品的最終消費者,而消費者必須親自去集市找到所要商品的直接生產者,若是一方沒有找到合適的目標,那麼對不起,你們都在集市等待。相對於有緩衝的BlockingQueue來講,少了一箇中間經銷商的環節(緩衝區),若是有經銷商,生產者直接把產品批發給經銷商,而無需在乎經銷商最終會將這些產品賣給那些消費者,因爲經銷商能夠庫存一部分商品,所以相對於直接交易模式,整體來講採用中間經銷商的模式會吞吐量高一些(能夠批量買賣);但另外一方面,又由於經銷商的引入,使得產品從生產者到消費者中間增長了額外的交易環節,單個產品的及時響應性能可能會下降。

小結

BlockingQueue不光實現了一個完整隊列所具備的基本功能,同時在多線程環境下,他還自動管理了多線間的自動等待於喚醒功能,從而使得程序員能夠忽略這些細節,關注更高級的功能。

原子操做類

什麼是原子操做?

原子操做(atomic operation)意爲」不可被中斷的一個或一系列操做」 。

處理器使用基於對緩存加鎖或總線加鎖的方式來實現多處理器之間的原子操做。在 Java 中能夠經過鎖和循環 CAS 的方式來實現原子操做。 CAS 操做——Compare & Set,或是 Compare & Swap,如今幾乎全部的 CPU 指令都支持 CAS 的原子操做。

原子操做是指一個不受其餘操做影響的操做任務單元。原子操做是在多線程環境下避免數據不一致必須的手段。

int++並非一個原子操做,因此當一個線程讀取它的值並加 1 時,另一個線程有可能會讀到以前的值,這就會引起錯誤。

爲了解決這個問題,必須保證增長操做是原子的,在 JDK1.5 以前咱們可使用同步技術來作到這一點。到 JDK1.5,java.util.concurrent.atomic 包提供了 int 和long 類型的原子包裝類,它們能夠自動的保證對於他們的操做是原子的而且不須要使用同步。

在 Java Concurrency API 中有哪些原子類(atomic classes)?

java.util.concurrent 這個包裏面提供了一組原子類。其基本的特性就是在多線程環境下,當有多個線程同時執行這些類的實例包含的方法時,具備排他性,即當某個線程進入方法,執行其中的指令時,不會被其餘線程打斷,而別的線程就像自旋鎖同樣,一直等到該方法執行完成,才由 JVM 從等待隊列中選擇另外一個線程進入,這只是一種邏輯上的理解。

原子類:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference

原子數組:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

原子屬性更新器:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater

解決 ABA 問題的原子類:AtomicMarkableReference(經過引入一個 boolean來反映中間有沒有變過),AtomicStampedReference(經過引入一個 int 來累加來反映中間有沒有變過)

說一下 atomic 的原理?

Atomic包中的類基本的特性就是在多線程環境下,當有多個線程同時對單個(包括基本類型及引用類型)變量進行操做時,具備排他性,即當多個線程同時對該變量的值進行更新時,僅有一個線程能成功,而未成功的線程能夠向自旋鎖同樣,繼續嘗試,一直等到執行成功。

AtomicInteger 類的部分源碼:

// setup to use Unsafe.compareAndSwapInt for updates(更新操做時提供「比較並替換」的做用)
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
	try {
		valueOffset = unsafe.objectFieldOffset
		(AtomicInteger.class.getDeclaredField("value"));
	} catch (Exception ex) { throw new Error(ex); }
}

private volatile int value;
123456789101112

AtomicInteger 類主要利用 CAS (compare and swap) + volatile 和 native 方法來保證原子操做,從而避免 synchronized 的高開銷,執行效率大爲提高。

CAS的原理是拿指望的值和本來的一個值做比較,若是相同則更新成新的值。UnSafe 類的 objectFieldOffset() 方法是一個本地方法,這個方法是用來拿到「原來的值」的內存地址,返回值是 valueOffset。另外 value 是一個volatile變量,在內存中可見,所以 JVM 能夠保證任什麼時候刻任何線程總能拿到該變量的最新值。

同步工具類

併發工具之CountDownLatch與CyclicBarrier

經常使用的併發工具類有哪些?

  • Semaphore(信號量)-容許多個線程同時訪問: synchronized 和 ReentrantLock 都是一次只容許一個線程訪問某個資源,Semaphore(信號量)能夠指定多個線程同時訪問某個資源。
  • CountDownLatch(倒計時器): CountDownLatch是一個同步工具類,用來協調多個線程之間的同步。這個工具一般用來控制線程等待,它可讓某一個線程等待直到倒計時結束,再開始執行。
  • CyclicBarrier(循環柵欄): CyclicBarrier 和 CountDownLatch 很是相似,它也能夠實現線程間的技術等待,可是它的功能比 CountDownLatch 更加複雜和強大。主要應用場景和 CountDownLatch 相似。CyclicBarrier 的字面意思是可循環使用(Cyclic)的屏障(Barrier)。它要作的事情是,讓一組線程到達一個屏障(也能夠叫同步點)時被阻塞,直到最後一個線程到達屏障時,屏障纔會開門,全部被屏障攔截的線程纔會繼續幹活。CyclicBarrier默認的構造方法是 CyclicBarrier(int parties),其參數表示屏障攔截的線程數量,每一個線程調用await()方法告訴 CyclicBarrier 我已經到達了屏障,而後當前線程被阻塞。

在 Java 中 CycliBarriar 和 CountdownLatch 有什麼區別?

CountDownLatch與CyclicBarrier都是用於控制併發的工具類,均可以理解成維護的就是一個計數器,可是這二者仍是各有不一樣側重點的:

  • CountDownLatch通常用於某個線程A等待若干個其餘線程執行完任務以後,它才執行;而CyclicBarrier通常用於一組線程互相等待至某個狀態,而後這一組線程再同時執行;CountDownLatch強調一個線程等多個線程完成某件事情。CyclicBarrier是多個線程互等,等你們都完成,再攜手共進。
  • 調用CountDownLatch的countDown方法後,當前線程並不會阻塞,會繼續往下執行;而調用CyclicBarrier的await方法,會阻塞當前線程,直到CyclicBarrier指定的線程所有都到達了指定點的時候,才能繼續往下執行;
  • CountDownLatch方法比較少,操做比較簡單,而CyclicBarrier提供的方法更多,好比可以經過getNumberWaiting(),isBroken()這些方法獲取當前多個線程的狀態,而且CyclicBarrier的構造方法能夠傳入barrierAction,指定當全部線程都到達時執行的業務功能;
  • CountDownLatch是不能複用的,而CyclicLatch是能夠複用的。

併發工具之Semaphore與Exchanger

Semaphore 有什麼做用

Semaphore 就是一個信號量,它的做用是限制某段代碼塊的併發數。Semaphore有一個構造函數,能夠傳入一個 int 型整數 n,表示某段代碼最多隻有 n 個線程能夠訪問,若是超出了 n,那麼請等待,等到某個線程執行完畢這段代碼塊,下一個線程再進入。由此能夠看出若是 Semaphore 構造函數中傳入的 int 型整數 n=1,至關於變成了一個 synchronized 了。

Semaphore(信號量)-容許多個線程同時訪問: synchronized 和 ReentrantLock 都是一次只容許一個線程訪問某個資源,Semaphore(信號量)能夠指定多個線程同時訪問某個資源。

什麼是線程間交換數據的工具Exchanger

Exchanger是一個用於線程間協做的工具類,用於兩個線程間交換數據。它提供了一個交換的同步點,在這個同步點兩個線程可以交換數據。交換數據是經過exchange方法來實現的,若是一個線程先執行exchange方法,那麼它會同步等待另外一個線程也執行exchange方法,這個時候兩個線程就都達到了同步點,兩個線程就能夠交換數據。

瘋狂創客圈 經典圖書 : 《Netty Zookeeper Redis 高併發實戰》 面試必備 + 面試必備 + 面試必備


回到◀瘋狂創客圈

瘋狂創客圈 - Java高併發研習社羣,爲你們開啓大廠之門

相關文章
相關標籤/搜索