Java併發容器篇

做者:湯圓java

我的博客:javalover.cc編程

前言

斷斷續續一個多月,也寫了十幾篇原創文章,感受真的很不同;安全

不能說技術有很大的進步,可是想法確實跟之前有所不一樣;多線程

還沒開始的時候,想着要學的東西太多,總以爲無從下手;併發

可是當你真正下定決心去作了幾天後,就會發現 原來路真的是一步步走出來的;dom

若是老是原地踏步東張西望,對本身不會有幫助;高併發

好了,下面開始今天的話題,併發容器篇工具

簡介

前面咱們介紹了同步容器,它的很大一個缺點就是在高併發下的環境下,性能差;性能

針對這個,因而就有了專門爲高併發設計的併發容器類;測試

由於併發容器類都位於java.util.concurrent下,因此咱們也習慣把併發容器簡稱爲JUC容器;

相對應的還有JUC原子類、JUC鎖、JUC工具類等等(這些後面再介紹)

今天就讓咱們簡單來了解下JUC中併發容器的相關知識點

文章若是有問題,歡迎你們批評指正,在此謝過啦

目錄

  1. 什麼是併發容器
  2. 爲何會有併發容器
  3. 併發容器、同步容器、普通容器的區別

正文

1. 什麼是併發容器

併發容器是針對高併發專門設計的一些類,用來替代性能較低的同步容器

常見的併發容器類以下所示:

這節咱們主要以第一個ConcurrentHashMap爲例子來介紹併發容器

其餘的之後有空會單獨開篇分析

2. 爲何會有併發容器

其實跟同步容器的出現的道理是同樣的:

同步容器是爲了讓咱們在編寫多線程代碼時,不用本身手動去同步加鎖,爲咱們解放了雙手,去作更多有意義的事情(有意義?雙手?);

而併發容器則又是爲了提升同步容器的性能,至關於同步容器的升級版;

這也是爲何Java一直在被人唱衰,卻又一直沒有衰退的緣由(大佬們也很焦慮啊!!!);

不過話說回來,大佬們焦慮地有點過頭了;不敢想Java如今都升到16級了,而咱們始終還在8級徘徊。

3. 併發容器、同步容器、普通容器的區別

這裏的普通容器,指的是沒有同步和併發的容器類,好比HashMap

三個對比着來介紹,這樣會更加清晰一點

下面咱們分別以HashMap, HashTable, ConcurrentHashMap爲例來介紹

性能分析

下面咱們來分析下他們三個之間的性能區別:

注:這裏普通容器用的是單線程來測試的,由於多線程不安全,因此咱們就不考慮了

有的朋友可能會說,你這不公平啊,但是沒辦法呀,誰讓她多線程不安全呢。

若是非要讓我在安全和性能之間選一個的話,那我選 ConcurrentHashMap(我都要)

他們三個之間的關係,以下圖

(紅色表示堵的厲害,橙色表示堵的通常,綠色表示暢通)

能夠看到:

  • 單線程中操做普通容器時,代碼都是串行執行的,同一時刻只能put或get一個數據到容器中
  • 多線程中操做同步容器時,能夠多個線程排隊去執行,同一時刻也是隻能put或get一個數據到同步容器中
  • 多線程中操做併發容器時,能夠多個線程同時去執行,也就是說同一時刻能夠有多個線程去put或get多個數據到併發容器中(可同時讀讀,可同時讀寫,可同時寫寫-有可能會阻塞,這裏是以ConcurrentHashMap爲參考)

下面咱們用代碼來複現下上面圖中所示的效果(慢-中-快)

  1. HashMap 測試方法
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);
}
  1. HashTable 測試方法
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);
}
  1. concurrentHashMap 測試方法
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);
}
  1. main 方法分別執行上面的三個測試
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中,多個線程可同時讀,多個線程可同時寫,多個線程同時讀和寫

總結

  1. 什麼是併發容器:併發容器是針對高併發專門設計的一些類,用來替代性能較低的同步容器
  2. 爲何會有併發容器:爲了提升同步容器的性能
  3. 併發容器、同步容器、普通容器的區別:

    • 性能:高 - 中 - 低
    • 鎖:粒度小 - 粒度大 - 無
    • 場景:高併發 - 中併發 - 單線程

參考內容:

  • 《Java併發編程實戰》
  • 《實戰Java高併發》
  • 《深刻理解Java虛擬機》

後記

我這裏介紹的都是比較淺的東西,其實併發容器的知識深刻起來有不少;

可是由於這節是併發系列的比較靠前的,還有不少東西沒涉及到,因此就分析地比較淺;

等到併發系列的內容都涉及地差很少了,再回過頭來深刻分析。

寫在最後:

願你的意中人亦是中意你之人。