## ConcurrentHashMap
[TOC]
### 1\. 概述
眾所周知,哈希表是中非常高效,復雜度為 O(1) 的數據結構,在 Java 開發中,我們最常見到最頻繁使用的就是 HashMap 和 HashTable,但是在線程競爭激烈的并發場景中使用都不夠合理。
**HashMap**:先說 HashMap,HashMap 是**線程不安全**的,在并發環境下,可能會形成**環狀鏈表**(擴容時可能造成),導致 get 操作時,cpu 空轉,所以,在并發環境中使 用HashMap 是非常危險的。
**HashTable**: HashTable 和 HashMap的實現原理幾乎一樣,差別無非是:(1)HashTable不允許key和value為null;(2)HashTable是線程安全的。
但是 HashTable 線程安全的策略實現代價卻太大了,簡單粗暴,get/put 所有相關操作都是 synchronized 的,這相當于給整個哈希表加了一把大鎖,多線程訪問時候,只要有一個線程訪問或操作該對象,那其他線程只能阻塞,相當于將所有的操作串行化,在競爭激烈的并發場景中性能就會非常差。
:-: 
HashTable 性能差主要是由于所有操作需要競爭同一把鎖,而如果容器中有多把鎖,每一把鎖鎖一段數據,這樣在多線程訪問時不同段的數據時,就不會存在鎖競爭了,這樣便可以有效地提高并發效率。這就是ConcurrentHashMap 所采用的 "**分段鎖**" 思想。

### 2\. 存儲結構
ConcurrentHashMap 采用了非常精妙的"分段鎖"策略,ConcurrentHashMap 的主干是個 Segment 數組。
~~~java
final Segment<K,V>[] segments;
~~~
Segment 繼承了 ReentrantLock,所以它就是一種可重入鎖(ReentrantLock)。在 ConcurrentHashMap,一個 Segment 就是一個子哈希表,Segment 里維護了一個 HashEntry 數組,并發環境下,對于不同 Segment 的數據進行操作是不用考慮鎖競爭的。(就按默認的 ConcurrentLeve 為16來講,理論上就允許 16 個線程并發執行,有木有很酷)
**所以,對于同一個 Segment 的操作才需考慮線程同步,不同的 Segment 則無需考慮。**
Segment 類似于 HashMap,一個 Segment 維護著一個 HashEntry 數組
~~~java
transient volatile HashEntry<K,V>[] table;
~~~
HashEntry 是目前我們提到的最小的邏輯處理單元了。一個 ConcurrentHashMap 維護一個 Segment 數組,一個 Segment 維護一個 HashEntry 數組。
~~~java
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
}
~~~
ConcurrentHashMap 和 HashMap 實現上類似,最主要的差別是 ConcurrentHashMap 采用了分段鎖(Segment),每個分段鎖維護著幾個桶(HashEntry),多個線程可以同時訪問不同分段鎖上的桶,從而使其并發度更高(并發度就是 Segment 的個數)。
Segment 繼承自**ReentrantLock**。
~~~java
static final class Segment<K,V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
static final int MAX_SCAN_RETRIES =
Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
transient volatile HashEntry<K,V>[] table;
transient int count;
transient int modCount;
transient int threshold;
final float loadFactor;
}
~~~
~~~java
final Segment<K,V>[] segments;
~~~
默認的并發級別為 16,也就是說默認創建 16 個 Segment。
~~~java
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
~~~
### 2\. size 操作
每個 Segment 維護了一個 count 變量來統計該 Segment 中的鍵值對個數。
~~~java
/**
* The number of elements. Accessed only either within locks
* or among other volatile reads that maintain visibility.
*/
transient int count;
~~~
在執行 size 操作時,需要遍歷所有 Segment 然后把 count 累計起來。
ConcurrentHashMap 在執行 size 操作時先嘗試不加鎖,如果連續兩次不加鎖操作得到的結果一致,那么可以認為這個結果是正確的。
嘗試次數使用 RETRIES\_BEFORE\_LOCK 定義,該值為 2,retries 初始值為 -1,因此嘗試次數為 3。
如果嘗試的次數超過 3 次,就需要對每個 Segment 加鎖。
~~~java
/**
* Number of unsynchronized retries in size and containsValue
* methods before resorting to locking. This is used to avoid
* unbounded retries if tables undergo continuous modification
* which would make it impossible to obtain an accurate result.
*/
static final int RETRIES_BEFORE_LOCK = 2;
public int size() {
// Try a few times to get accurate count. On failure due to
// continuous async changes in table, resort to locking.
final Segment<K,V>[] segments = this.segments;
int size;
boolean overflow; // true if size overflows 32 bits
long sum; // sum of modCounts
long last = 0L; // previous sum
int retries = -1; // first iteration isn't retry
try {
for (;;) {
// 超過嘗試次數,則對每個 Segment 加鎖
if (retries++ == RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
ensureSegment(j).lock(); // force creation
}
sum = 0L;
size = 0;
overflow = false;
for (int j = 0; j < segments.length; ++j) {
Segment<K,V> seg = segmentAt(segments, j);
if (seg != null) {
sum += seg.modCount;
int c = seg.count;
if (c < 0 || (size += c) < 0)
overflow = true;
}
}
// 連續兩次得到的結果一致,則認為這個結果是正確的
if (sum == last)
break;
last = sum;
}
} finally {
if (retries > RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
segmentAt(segments, j).unlock();
}
}
return overflow ? Integer.MAX_VALUE : size;
}
~~~
### 3\. 同步方式
Segment 繼承自 ReentrantLock,所以我們可以很方便的對每一個 Segment 上鎖。
對于讀操作,獲取 Key 所在的 Segment 時,需要保證可見性。具體實現上可以使用 volatile 關鍵字,也可使用鎖。但使用鎖開銷太大,而使用 volatile 時每次寫操作都會讓所有 CPU 內緩存無效,也有一定開銷。ConcurrentHashMap 使用如下方法保證可見性,取得最新的 Segment。
~~~java
Segment<K,V> s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)
~~~
獲取 Segment 中的 HashEntry 時也使用了類似方法
~~~java
HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE)
~~~
對于寫操作,并不要求同時獲取所有 Segment 的鎖,因為那樣相當于鎖住了整個 Map。它會先獲取該 Key-Value 對所在的 Segment 的鎖,獲取成功后就可以像操作一個普通的 HashMap 一樣操作該 Segment,并保證該Segment 的安全性。 同時由于其它 Segment 的鎖并未被獲取,因此理論上可支持 concurrencyLevel(等于 Segment 的個數)個線程安全的并發讀寫。
獲取鎖時,并不直接使用 lock 來獲取,因為該方法獲取鎖失敗時會掛起。事實上,它使用了自旋鎖,如果 tryLock 獲取鎖失敗,說明鎖被其它線程占用,此時通過循環再次以 tryLock 的方式申請鎖。如果在循環過程中該 Key 所對應的鏈表頭被修改,則重置 retry 次數。如果 retry 次數超過一定值,則使用 lock 方法申請鎖。
這里使用自旋鎖是因為自旋鎖的效率比較高,但是它消耗 CPU 資源比較多,因此在自旋次數超過閾值時切換為互斥鎖。
### 4\. JDK 1.8 的改動
* JDK 1.7 使用分段鎖機制來實現并發更新操作,核心類為 Segment,它繼承自重入鎖 ReentrantLock,并發程度與 Segment 數量相等。
* JDK 1.8 使用了 CAS 操作來支持更高的并發度,在 CAS 操作失敗時使用內置鎖 synchronized。
* 并且 JDK 1.8 的實現也在鏈表過長時會轉換為紅黑樹。
參考資料:
* [ConcurrentHashMap演進從Java7到Java8](http://www.jasongj.com/java/concurrenthashmap/)
* [ConcurrentHashMap實現原理及源碼分析 - dreamcatcher-cx - 博客園](https://www.cnblogs.com/chengxiao/p/6842045.html)
- 一.JVM
- 1.1 java代碼是怎么運行的
- 1.2 JVM的內存區域
- 1.3 JVM運行時內存
- 1.4 JVM內存分配策略
- 1.5 JVM類加載機制與對象的生命周期
- 1.6 常用的垃圾回收算法
- 1.7 JVM垃圾收集器
- 1.8 CMS垃圾收集器
- 1.9 G1垃圾收集器
- 2.面試相關文章
- 2.1 可能是把Java內存區域講得最清楚的一篇文章
- 2.0 GC調優參數
- 2.1GC排查系列
- 2.2 內存泄漏和內存溢出
- 2.2.3 深入理解JVM-hotspot虛擬機對象探秘
- 1.10 并發的可達性分析相關問題
- 二.Java集合架構
- 1.ArrayList深入源碼分析
- 2.Vector深入源碼分析
- 3.LinkedList深入源碼分析
- 4.HashMap深入源碼分析
- 5.ConcurrentHashMap深入源碼分析
- 6.HashSet,LinkedHashSet 和 LinkedHashMap
- 7.容器中的設計模式
- 8.集合架構之面試指南
- 9.TreeSet和TreeMap
- 三.Java基礎
- 1.基礎概念
- 1.1 Java程序初始化的順序是怎么樣的
- 1.2 Java和C++的區別
- 1.3 反射
- 1.4 注解
- 1.5 泛型
- 1.6 字節與字符的區別以及訪問修飾符
- 1.7 深拷貝與淺拷貝
- 1.8 字符串常量池
- 2.面向對象
- 3.關鍵字
- 4.基本數據類型與運算
- 5.字符串與數組
- 6.異常處理
- 7.Object 通用方法
- 8.Java8
- 8.1 Java 8 Tutorial
- 8.2 Java 8 數據流(Stream)
- 8.3 Java 8 并發教程:線程和執行器
- 8.4 Java 8 并發教程:同步和鎖
- 8.5 Java 8 并發教程:原子變量和 ConcurrentMap
- 8.6 Java 8 API 示例:字符串、數值、算術和文件
- 8.7 在 Java 8 中避免 Null 檢查
- 8.8 使用 Intellij IDEA 解決 Java 8 的數據流問題
- 四.Java 并發編程
- 1.線程的實現/創建
- 2.線程生命周期/狀態轉換
- 3.線程池
- 4.線程中的協作、中斷
- 5.Java鎖
- 5.1 樂觀鎖、悲觀鎖和自旋鎖
- 5.2 Synchronized
- 5.3 ReentrantLock
- 5.4 公平鎖和非公平鎖
- 5.3.1 說說ReentrantLock的實現原理,以及ReentrantLock的核心源碼是如何實現的?
- 5.5 鎖優化和升級
- 6.多線程的上下文切換
- 7.死鎖的產生和解決
- 8.J.U.C(java.util.concurrent)
- 0.簡化版(快速復習用)
- 9.鎖優化
- 10.Java 內存模型(JMM)
- 11.ThreadLocal詳解
- 12 CAS
- 13.AQS
- 0.ArrayBlockingQueue和LinkedBlockingQueue的實現原理
- 1.DelayQueue的實現原理
- 14.Thread.join()實現原理
- 15.PriorityQueue 的特性和原理
- 16.CyclicBarrier的實際使用場景
- 五.Java I/O NIO
- 1.I/O模型簡述
- 2.Java NIO之緩沖區
- 3.JAVA NIO之文件通道
- 4.Java NIO之套接字通道
- 5.Java NIO之選擇器
- 6.基于 Java NIO 實現簡單的 HTTP 服務器
- 7.BIO-NIO-AIO
- 8.netty(一)
- 9.NIO面試題
- 六.Java設計模式
- 1.單例模式
- 2.策略模式
- 3.模板方法
- 4.適配器模式
- 5.簡單工廠
- 6.門面模式
- 7.代理模式
- 七.數據結構和算法
- 1.什么是紅黑樹
- 2.二叉樹
- 2.1 二叉樹的前序、中序、后序遍歷
- 3.排序算法匯總
- 4.java實現鏈表及鏈表的重用操作
- 4.1算法題-鏈表反轉
- 5.圖的概述
- 6.常見的幾道字符串算法題
- 7.幾道常見的鏈表算法題
- 8.leetcode常見算法題1
- 9.LRU緩存策略
- 10.二進制及位運算
- 10.1.二進制和十進制轉換
- 10.2.位運算
- 11.常見鏈表算法題
- 12.算法好文推薦
- 13.跳表
- 八.Spring 全家桶
- 1.Spring IOC
- 2.Spring AOP
- 3.Spring 事務管理
- 4.SpringMVC 運行流程和手動實現
- 0.Spring 核心技術
- 5.spring如何解決循環依賴問題
- 6.springboot自動裝配原理
- 7.Spring中的循環依賴解決機制中,為什么要三級緩存,用二級緩存不夠嗎
- 8.beanFactory和factoryBean有什么區別
- 九.數據庫
- 1.mybatis
- 1.1 MyBatis-# 與 $ 區別以及 sql 預編譯
- Mybatis系列1-Configuration
- Mybatis系列2-SQL執行過程
- Mybatis系列3-之SqlSession
- Mybatis系列4-之Executor
- Mybatis系列5-StatementHandler
- Mybatis系列6-MappedStatement
- Mybatis系列7-參數設置揭秘(ParameterHandler)
- Mybatis系列8-緩存機制
- 2.淺談聚簇索引和非聚簇索引的區別
- 3.mysql 證明為什么用limit時,offset很大會影響性能
- 4.MySQL中的索引
- 5.數據庫索引2
- 6.面試題收集
- 7.MySQL行鎖、表鎖、間隙鎖詳解
- 8.數據庫MVCC詳解
- 9.一條SQL查詢語句是如何執行的
- 10.MySQL 的 crash-safe 原理解析
- 11.MySQL 性能優化神器 Explain 使用分析
- 12.mysql中,一條update語句執行的過程是怎么樣的?期間用到了mysql的哪些log,分別有什么作用
- 十.Redis
- 0.快速復習回顧Redis
- 1.通俗易懂的Redis數據結構基礎教程
- 2.分布式鎖(一)
- 3.分布式鎖(二)
- 4.延時隊列
- 5.位圖Bitmaps
- 6.Bitmaps(位圖)的使用
- 7.Scan
- 8.redis緩存雪崩、緩存擊穿、緩存穿透
- 9.Redis為什么是單線程、及高并發快的3大原因詳解
- 10.布隆過濾器你值得擁有的開發利器
- 11.Redis哨兵、復制、集群的設計原理與區別
- 12.redis的IO多路復用
- 13.相關redis面試題
- 14.redis集群
- 十一.中間件
- 1.RabbitMQ
- 1.1 RabbitMQ實戰,hello world
- 1.2 RabbitMQ 實戰,工作隊列
- 1.3 RabbitMQ 實戰, 發布訂閱
- 1.4 RabbitMQ 實戰,路由
- 1.5 RabbitMQ 實戰,主題
- 1.6 Spring AMQP 的 AMQP 抽象
- 1.7 Spring AMQP 實戰 – 整合 RabbitMQ 發送郵件
- 1.8 RabbitMQ 的消息持久化與 Spring AMQP 的實現剖析
- 1.9 RabbitMQ必備核心知識
- 2.RocketMQ 的幾個簡單問題與答案
- 2.Kafka
- 2.1 kafka 基礎概念和術語
- 2.2 Kafka的重平衡(Rebalance)
- 2.3.kafka日志機制
- 2.4 kafka是pull還是push的方式傳遞消息的?
- 2.5 Kafka的數據處理流程
- 2.6 Kafka的腦裂預防和處理機制
- 2.7 Kafka中partition副本的Leader選舉機制
- 2.8 如果Leader掛了的時候,follower沒來得及同步,是否會出現數據不一致
- 2.9 kafka的partition副本是否會出現腦裂情況
- 十二.Zookeeper
- 0.什么是Zookeeper(漫畫)
- 1.使用docker安裝Zookeeper偽集群
- 3.ZooKeeper-Plus
- 4.zk實現分布式鎖
- 5.ZooKeeper之Watcher機制
- 6.Zookeeper之選舉及數據一致性
- 十三.計算機網絡
- 1.進制轉換:二進制、八進制、十六進制、十進制之間的轉換
- 2.位運算
- 3.計算機網絡面試題匯總1
- 十四.Docker
- 100.面試題收集合集
- 1.美團面試常見問題總結
- 2.b站部分面試題
- 3.比心面試題
- 4.騰訊面試題
- 5.哈羅部分面試
- 6.筆記
- 十五.Storm
- 1.Storm和流處理簡介
- 2.Storm 核心概念詳解
- 3.Storm 單機版本環境搭建
- 4.Storm 集群環境搭建
- 5.Storm 編程模型詳解
- 6.Storm 項目三種打包方式對比分析
- 7.Storm 集成 Redis 詳解
- 8.Storm 集成 HDFS 和 HBase
- 9.Storm 集成 Kafka
- 十六.Elasticsearch
- 1.初識ElasticSearch
- 2.文檔基本CRUD、集群健康檢查
- 3.shard&replica
- 4.document核心元數據解析及ES的并發控制
- 5.document的批量操作及數據路由原理
- 6.倒排索引
- 十七.分布式相關
- 1.分布式事務解決方案一網打盡
- 2.關于xxx怎么保證高可用的問題
- 3.一致性hash原理與實現
- 4.微服務注冊中心 Nacos 比 Eureka的優勢
- 5.Raft 協議算法
- 6.為什么微服務架構中需要網關
- 0.CAP與BASE理論
- 十八.Dubbo
- 1.快速掌握Dubbo常規應用
- 2.Dubbo應用進階
- 3.Dubbo調用模塊詳解
- 4.Dubbo調用模塊源碼分析
- 6.Dubbo協議模塊