先看Collections結構
面試常問問題:
1、ArrayBlockQueue和LinkedBlockingQueue有什么區別
答:二者都是通過reentrantLock進行加鎖的,但是區別在于ArrayBlockQueue是讀寫不分離的,也就是說要么進行讀操作,要么進行寫操作,而且因為用的是ReentrantLock,所以一個線程是可以重復寫或者讀的;LinkedBlockingQueue則是讀寫分離,可以同時讀或者寫,但是因為用的是ReentrantLock,所以每次只能有一個線程讀和一個寫。這個需要專門注意下讀和寫的并發操作。
2、PriorityBlockingQueue是無界隊列,基于數組,數據結構為二叉堆,數組的第一個節點也是樹的根節點總是最小值
一、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、復制的原因是為了讀寫分離,為了在寫的時候操作不阻塞也能不出現問題
未讀寫分離時,如果不阻塞讀操作,由于讀和寫操作都不是原子操作,它們可能會交替執行,如:
當一個線程執行到len = len + 1 的時候,另外一個線程來進行讀操作了,比如執行getLast()方法,即執行get(len - 1),但是現在這個位置是沒有賦值上的,這就出現了問題。但是如果修改時是賦值一份來修改,另外一個線程讀的時候就沒有任何影響。這也就是為什么在沒有這個容器之前,只能坐到讀讀不加鎖,讀寫、寫讀、寫寫都要加鎖。有了這個容器,就可以只在寫寫的時候加鎖了。
4、寫操作的源碼(通過ReentrantLock加鎖實現)
三、ConcurrentMap
1、ConcurrentHashMap
a、JDK1.7之前ConcurrentHashMap采用鎖分段機制
如圖所示,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。下面看看部分用法:
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進行同步