Java並發容器篇


作者:湯圓

個人博客:javalover.cc

前言

斷斷續續一個多月,也寫了十幾篇原創文章,感覺真的很不一樣;

不能說技術有很大的進步,但是想法確實跟以前有所不同;

還沒開始的時候,想着要學的東西太多,總覺得無從下手;

但是當你真正下定決心去做了幾天后,就會發現 原來路真的是一步步走出來的;

如果總是原地踏步東張西望,對自己不會有幫助;

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

簡介

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

針對這個,於是就有了專門為高並發設計的並發容器類;

因為並發容器類都位於java.util.concurrent下,所以我們也習慣把並發容器簡稱為JUC容器;

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

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

文章如果有問題,歡迎大家批評指正,在此謝過啦

目錄

  1. 什么是並發容器
  2. 為什么會有並發容器
  3. 並發容器、同步容器、普通容器的區別

正文

1. 什么是並發容器

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

常見的並發容器類如下所示:

常見的並發容器

這節我們主要以第一個ConcurrentHashMap為例子來介紹並發容器

其他的以后有空會單獨開篇分析

2. 為什么會有並發容器

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

同步容器是為了讓我們在編寫多線程代碼時,不用自己手動去同步加鎖,為我們解放了雙手,去做更多有意義的事情(有意義?雙手?);

而並發容器則又是為了提高同步容器的性能,相當於同步容器的升級版;

這也是為什么Java一直在被人唱衰,卻又一直沒有衰退的原因(大佬們也很焦慮啊!!!);

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

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

這里的普通容器,指的是沒有同步和並發的容器類,比如HashMap

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

下面我們分別以HashMap, HashTable, ConcurrentHashMap為例來介紹

性能分析

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

注:這里普通容器用的是單線程來測試的,因為多線程不安全,所以我們就不考慮了

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

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

他們三個之間的關系,如下圖

image
(紅色表示堵的厲害,橙色表示堵的一般,綠色表示暢通)

可以看到:

  • 單線程中操作普通容器時,代碼都是串行執行的,同一時刻只能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虛擬機》

后記

我這里介紹的都是比較淺的東西,其實並發容器的知識深入起來有很多;

但是因為這節是並發系列的比較靠前的,還有很多東西沒涉及到,所以就分析地比較淺;

等到並發系列的內容都涉及地差不多了,再回過頭來深入分析。

寫在最后:

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


免責聲明!

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



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