做者:湯圓java
我的博客:javalover.cc編程
斷斷續續一個多月,也寫了十幾篇原創文章,感受真的很不同;安全
不能說技術有很大的進步,可是想法確實跟之前有所不一樣;多線程
還沒開始的時候,想着要學的東西太多,總以爲無從下手;併發
可是當你真正下定決心去作了幾天後,就會發現 原來路真的是一步步走出來的;dom
若是老是原地踏步東張西望,對本身不會有幫助;高併發
好了,下面開始今天的話題,併發容器篇工具
前面咱們介紹了同步容器,它的很大一個缺點就是在高併發下的環境下,性能差;性能
針對這個,因而就有了專門爲高併發設計的併發容器類;測試
由於併發容器類都位於java.util.concurrent
下,因此咱們也習慣把併發容器簡稱爲JUC容器;
相對應的還有JUC原子類、JUC鎖、JUC工具類等等(這些後面再介紹)
今天就讓咱們簡單來了解下JUC中併發容器的相關知識點
文章若是有問題,歡迎你們批評指正,在此謝過啦
併發容器是針對高併發專門設計的一些類,用來替代性能較低的同步容器
常見的併發容器類以下所示:
這節咱們主要以第一個ConcurrentHashMap
爲例子來介紹併發容器
其餘的之後有空會單獨開篇分析
其實跟同步容器的出現的道理是同樣的:
同步容器是爲了讓咱們在編寫多線程代碼時,不用本身手動去同步加鎖,爲咱們解放了雙手,去作更多有意義的事情(有意義?雙手?);
而併發容器則又是爲了提升同步容器的性能,至關於同步容器的升級版;
這也是爲何Java一直在被人唱衰,卻又一直沒有衰退的緣由(大佬們也很焦慮啊!!!);
不過話說回來,大佬們焦慮地有點過頭了;不敢想Java如今都升到16級了,而咱們始終還在8級徘徊。
這裏的普通容器,指的是沒有同步和併發的容器類,好比HashMap
三個對比着來介紹,這樣會更加清晰一點
下面咱們分別以HashMap
, HashTable
, ConcurrentHashMap
爲例來介紹
下面咱們來分析下他們三個之間的性能區別:
注:這裏普通容器用的是單線程來測試的,由於多線程不安全,因此咱們就不考慮了
有的朋友可能會說,你這不公平啊,但是沒辦法呀,誰讓她多線程不安全呢。
若是非要讓我在安全和性能之間選一個的話,那我選 ConcurrentHashMap(我都要)
他們三個之間的關係,以下圖
(紅色表示堵的厲害,橙色表示堵的通常,綠色表示暢通)
能夠看到:
下面咱們用代碼來複現下上面圖中所示的效果(慢-中-快)
public static void hashMapTest(){ Map<String, String> map = new HashMap<>(); long start = System.nanoTime(); // 建立10萬條數據 單線程 for (int i = 0; i < 100_000; i++) { // 用UUID做爲key,保證key的惟一 map.put(UUID.randomUUID().toString(), String.valueOf(i)); map.get(UUID.randomUUID().toString()); } long end = System.nanoTime(); System.out.println("hashMap耗時:"); System.out.println(end - start); }
public static void hashTableTest(){ Map<String, String> map = new Hashtable<>(); long start = System.nanoTime(); // 建立10個線程 - 多線程 for (int i = 0; i < 10; i++) { new Thread(()->{ // 每一個線程建立1萬條數據 for (int j = 0; j < 10000; j++) { // UUID保證key的惟一性 map.put(UUID.randomUUID().toString(), String.valueOf(j)); map.get(UUID.randomUUID().toString()); } }).start(); } // 這裏是爲了等待上面的線程執行結束,之因此判斷>2,是由於在IDEA中除了main thread,還有一個monitor thread while (Thread.activeCount()>2){ Thread.yield(); } long end = System.nanoTime(); System.out.println("hashTable耗時:"); System.out.println(end - start); }
public static void concurrentHashMapTest(){ Map<String, String> map = new ConcurrentHashMap<>(); long start = System.nanoTime(); // 建立10個線程 - 多線程 for (int i = 0; i < 10; i++) { new Thread(()->{ // 每一個線程建立1萬條數據 for (int j = 0; j < 10000; j++) { // UUID做爲key,保證惟一性 map.put(UUID.randomUUID().toString(), String.valueOf(j)); map.get(UUID.randomUUID().toString()); } }).start(); } // 這裏是爲了等待上面的線程執行結束,之因此判斷>2,是由於在IDEA中除了main thread,還有一個monitor thread while (Thread.activeCount()>2){ Thread.yield(); } long end = System.nanoTime(); System.out.println("concurrentHashMap耗時:"); System.out.println(end - start); }
public static void main(String[] args) { hashMapTest(); hashTableTest(); while (Thread.activeCount()>2){ Thread.yield(); } concurrentHashMapTest(); }
運行能夠看到,以下結果(運行屢次,數值可能會變好,可是規律基本一致)
hashMap耗時: 754699874 (慢) hashTable耗時: 609160132(中) concurrentHashMap耗時: 261617133(快)
結論就是,正常狀況下的速度:普通容器 < 同步容器 < 併發容器
可是也不那麼絕對,由於這裏插入的key都是惟一的,因此看起來正常一點
那若是咱們不正常一點呢?好比極端到BT的那種
下面咱們就不停地插入同一條數據,上面的全部put/get都改成下面的代碼:
map.put("a", "a"); map.get("a");
運行後,你會發現,又是另一個結論(你們感興趣的能夠敲出來試試)
不過結論不結論的,意義不是很大;
普通容器沒鎖
同步容器中鎖的都是方法級別,也就是說鎖的是整個容器,咱們先來看下HashTable的鎖
public synchronized V put(K key, V value) {} public synchronized V remove(Object key) {}
能夠看到:由於鎖是內置鎖,鎖住的是整個容器
因此咱們在put的時候,其餘線程都不能put/get
而咱們在get的時候,其餘線程也都不能put/get
因此同步容器的效率會比較低
併發容器,咱們以1.7的ConcurrentHashMap爲例來講下(之因此選1.7,是由於它裏面涉及的內容都是前面章節介紹過的)
它的鎖粒度很小,它不會給整個容器上鎖,而是分段上鎖;
分段的依據就是key.hash,根據不一樣的hash值映射到不一樣的段(默認16個段),而後插入數據時,根據這個hash值去給對應的段上鎖,此時其餘段仍是能夠被其餘線程讀寫的;
因此這就是文章開頭所說的,爲啥ConcurrentHashMap會支持多個線程同時寫(由於只要插入的key的hashCode不會映射到同一個段裏,那就不會衝突,此時就能夠同時寫)
讀由於沒有上鎖,因此固然也支持同時讀
若是讀操做沒有鎖,那麼它怎麼保證數據的一致性呢?
答案就是之前介紹過的volatile(保證可見性、禁止重排序),它修飾在節點Node和值val上,保證了你get的值永遠是最新的
下面是ConcurrentHashMap部分源碼,能夠看到val和net節點都是volatile類型
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; volatile V val; volatile Node<K,V> next; }
總結下來就是:併發容器ConcurrentHashMap中,多個線程可同時讀,多個線程可同時寫,多個線程同時讀和寫
併發容器、同步容器、普通容器的區別:
參考內容:
我這裏介紹的都是比較淺的東西,其實併發容器的知識深刻起來有不少;
可是由於這節是併發系列的比較靠前的,還有不少東西沒涉及到,因此就分析地比較淺;
等到併發系列的內容都涉及地差很少了,再回過頭來深刻分析。
寫在最後:
願你的意中人亦是中意你之人。