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


文章很長,建議收藏起來,慢慢讀! Java 高並發 發燒友社群:瘋狂創客圈 奉上以下珍貴的學習資源:


推薦: 瘋狂創客圈 高質量 博文

高並發 必讀 的精彩博文
nacos 實戰(史上最全) sentinel (史上最全+入門教程)
Zookeeper 分布式鎖 (圖解+秒懂+史上最全) Webflux(史上最全)
SpringCloud gateway (史上最全) TCP/IP(圖解+秒懂+史上最全)
10分鍾看懂, Java NIO 底層原理 Feign原理 (圖解)
更多精彩博文 ..... 請參見【 瘋狂創客圈 高並發 總目錄

史上最全 Java 面試題 30 專題 總目錄

精心梳理、吐血推薦、史上最強、建議收藏 阿里、京東、美團、頭條.... 隨意挑、橫着走!!!
1.Java算法面試題(史上最強、持續更新、吐血推薦) 2.Java基礎面試題(史上最全、持續更新、吐血推薦)
3.JVM面試題(史上最強、持續更新、吐血推薦) 4、架構設計面試題 (史上最全、持續更新、吐血推薦)
5、Spring面試題 專題 6、SpringMVC面試題 專題
7.SpringBoot - 面試題(史上最強、持續更新) 8、Tomcat面試題 專題部分
9.網絡協議面試題(史上最全、持續更新、吐血推薦) 10、TCP/IP協議(圖解+秒懂+史上最全)
11.JUC並發包與容器 - 面試題(史上最強、持續更新) 12、設計模式面試題 (史上最全、持續更新、吐血推薦)
13.死鎖面試題(史上最強、持續更新) 15.Zookeeper 分布式鎖 (圖解+秒懂+史上最全)
14、Redis 面試題 - 收藏版(史上最強、持續更新) 16、Zookeeper 面試題(史上最強、持續更新)
17、分布式事務面試題 (史上最全、持續更新、吐血推薦) 18、一致性協議 (史上最全)
19、Zab協議 (史上最全) 20、Paxos 圖解 (秒懂)
21、raft 圖解 (秒懂) 26、消息隊列、RabbitMQ、Kafka、RocketMQ面試題 (史上最全、持續更新)
22.Linux面試題(史上最全、持續更新、吐血推薦) 23、Mysql 面試題(史上最強、持續更新)
24、SpringCloud 面試題 - 收藏版(史上最強、持續更新) 25、Netty 面試題 (史上最強、持續更新)
27、內存泄漏 內存溢出(史上最全) 28、JVM 內存溢出 實戰 (史上最全)
29、多線程面試題(史上最全) 30、HR面經:過五關斬六將后,小心陰溝翻船!(史上最全)

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 常用於多線程環境下的單次操作(單次讀或者單次寫)。

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 的會產生什么問題?

1、ABA 問題:

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

2、循環時間長開銷大:

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

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

當對一個共享變量執行操作時,我們可以使用循環 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 實現的。

樂觀鎖的實現方式:

1、使用版本標識來確定讀到的數據與提交時的數據是否一致。提交后修改版本標識,不一致時可以采取丟棄和再次嘗試的策略。

2、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高並發研習社群,為大家開啟大廠之門


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM