JUC之Collections面經整理

先看Collections結構

Collections目錄結構

面試常問問題:

1、ArrayBlockQueue和LinkedBlockingQueue有什么區別

答:二者都是通過reentrantLock進行加鎖的,但是區別在于ArrayBlockQueue是讀寫不分離的,也就是說要么進行讀操作,要么進行寫操作,而且因為用的是ReentrantLock,所以一個線程是可以重復寫或者讀的;LinkedBlockingQueue則是讀寫分離,可以同時讀或者寫,但是因為用的是ReentrantLock,所以每次只能有一個線程讀和一個寫。這個需要專門注意下讀和寫的并發操作。

2、PriorityBlockingQueue是無界隊列,基于數組,數據結構為二叉堆,數組的第一個節點也是樹的根節點總是最小值



一、BlockingQueue

參見解讀java并發隊列BlockingQueue

1、BlockingQueue是雙緩沖隊列。BlockingQueue內部使用兩條隊列,允許兩個線程同時向隊列一個存儲,一個取出操作。在保證并發安全的同時,提高了隊列的存取效率。

2、常用的幾種BlockingQueue:

(1)ArrayBlockingQueue(int i):規定大小的BlockingQueue,其構造必須指定大小。其所含的對象是FIFO順序排序的。

(2)LinkedBlockingQueue()或者(int i):大小不固定的BlockingQueue,若其構造時指定大小,生成的BlockingQueue有大小限制,不指定大小,其大小由Integer.MAX_VALUE來決定。其所含的對象是FIFO順序排序的。

(3)PriorityBlockingQueue()或者(int i):類似于LinkedBlockingQueue,但是其所含對象的排序不是FIFO,而是依據對象的自然順序或者構造函數的Comparator決定。

(4)SynchronizedQueue():特殊的BlockingQueue,對其的操作必須是放和取交替完成。

3、常用方法

常用方法

4、應用

(1)生產者消費者模式:有了阻塞隊列,我們實現生產者消費者模式只需要調用接口就行了,不用自己實現線程的阻塞和喚醒,也就是說阻塞隊列接口的實現方式就是生產者消費者模式的實現。

(2)線程池:線程池的底層也用到了阻塞隊列,其實它的本質也是生產者消費者模式,只不過這里的資源是線程。

(3)消息中間件:RabbitMQ等消息隊列的底層數據結構就是用到了阻塞隊列,因為它的模型也是生產者消費者模式。


二、CopyOnWriteArrayList(線程安全,解決iterator遍歷的java.util.ConcurrentModificationException)

1、原理:寫操作并不是直接修改原本的array,而是復制一份進行修改,同時也加了鎖,在多線程的情況下不會復制出多個副本,其讀操作沒有任何鎖,直接返回原本的array。

2、CopyOnWriteArrayList,對它的操作可以做到寫寫互斥、其他三個操作不互斥。

3、復制的原因是為了讀寫分離,為了在寫的時候操作不阻塞也能不出現問題

未讀寫分離時,如果不阻塞讀操作,由于讀和寫操作都不是原子操作,它們可能會交替執行,如:

例1

當一個線程執行到len = len + 1 的時候,另外一個線程來進行讀操作了,比如執行getLast()方法,即執行get(len - 1),但是現在這個位置是沒有賦值上的,這就出現了問題。但是如果修改時是賦值一份來修改,另外一個線程讀的時候就沒有任何影響。這也就是為什么在沒有這個容器之前,只能坐到讀讀不加鎖,讀寫、寫讀、寫寫都要加鎖。有了這個容器,就可以只在寫寫的時候加鎖了。

4、寫操作的源碼(通過ReentrantLock加鎖實現)

寫操作的源碼

三、ConcurrentMap

1、ConcurrentHashMap

a、JDK1.7之前ConcurrentHashMap采用鎖分段機制

ConcurrentHashMapJDK1.7

如圖所示,ConcurrentHashMap默認分成了16個segment,每個segment都對應一個Hash表,且都由獨立的鎖。所以這樣就每個線程訪問一個Segment,就可以并行訪問了,從而提高了效率,這就是鎖分段。

JDK1.8之后采用的是和HashMap一樣的結構:數組+鏈表/紅黑樹

問:JDK1.8ConcurrentHashMap怎樣保證線程安全?

使用Synchronized + CAS 的方法來實現線程安全。在用hash定位到數組的頭結點時,如果沒有發生沖突就用CAS來實現寫操作的線程安全,如果發生了沖突,則鎖住這個頭節點。

java.util.concurrent包還提供了設計用于多線程上下文中的 Collection 實現: ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、CopyOnWriteArrayList 和 CopyOnWriteArraySet。當期望許多線程訪問一個給 定 collection 時,ConcurrentHashMap 通常優于同步的 HashMap,ConcurrentSkipListMap 通常優于同步的 TreeMap。當期望的讀數和遍歷遠遠 大于列表的更新數時,CopyOnWriteArrayList 優于同步的 ArrayList。下面看看部分用法:

例2

10個線程并發訪問這個集合,讀取集合數據的同時再往集合中添加數據,運行這段代碼會報錯,并發修改異常

(1)出現并發修改異常的原因及解決辦法

原因:當線程1添加了元素時,modCount+1,并會修改其他線程的expectedModCount+1,而此時線程1中的expectedModCount值為0,雖然modCount不是volatile變量,不保證線程1一定看得到線程2修改后的modCount的值,但是也有可能看得到線程2對modCount的修改,這樣就有可能導致線程1中比較expectedModCount和modCount不等,而拋出異常。

解決:

1、將集合創建方式改成:

private static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();

這樣就不會有并發異常了,因為這個是寫入并復制,每次生成新的,所以如果添加操作比較多的話,開銷非常大,適合迭代操作比較多的時候使用。

2、在使用iterator迭代的時候使用synchronized或Lock進行同步

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。