ConcurrentHashMap
HashTable
实现了同步操作,但是由于其实现的是使用了synchronized关键字对put等操作进行加锁,而synchronized关键字加锁是对整个对象进行加锁,也就是说在进行put等修改Hash表的操作时,锁住了整个Hash表,从而使得其表现的效率低下。
JDK 1.7
Java使用了分段锁机制实现ConcurrentHashMap
ConcurrentHashMap在对象中保存了一个Segment数组,即将整个Hash表划分为多个分段;而每个Segment元素,即每个分段则类似于一个Hashtable;这样,在执行put操作时首先根据hash算法定位到元素属于哪个Segment,然后对该Segment加锁即可。因此,ConcurrentHashMap在多线程并发编程中可是实现多线程put操作。
concurrencyLevel
: 并行级别、并发数、Segment 数,默认是 16,也就是说 ConcurrentHashMap 有 16 个 Segments,所以理论上,这个时候,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的。
-
initialCapacity: 初始容量,这个值指的是整个 ConcurrentHashMap 的初始容量,实际操作的时候需要平均分给每个 Segment。
-
loadFactor: 负载因子,之前我们说了,Segment 数组不可以扩容,所以这个负载因子是给每个 Segment 内部使用的。
-
用 new ConcurrentHashMap() 无参构造函数进行初始化的,那么初始化完成后:
- Segment 数组长度为 16,不可以扩容
- Segment[i] 的默认大小为 2,负载因子是 0.75,得出初始阈值为 1.5,也就是以后插入第一个元素不会触发扩容,插入第二个会进行第一次扩容
- 这里初始化了 segment[0],其他位置还是 null,初始化segment[0]是为了使用当前 segment[0] 处的数组长度和负载因子来初始化 segment[k]。ensureSegment(int k) 用于初始化槽,对于并发操作使用 CAS(Compare and Swap ,即 比较和替换) 进行控制。
- 当前 segmentShift 的值为 32 - 4 = 28,segmentMask 为 16 - 1 = 15,姑且把它们简单翻译为移位数和掩码
-
获取写入锁: scanAndLockForPut:一个是 tryLock() 成功了,循环终止,另一个就是重试次数超过了 MAX_SCAN_RETRIES,进到 lock() 方法,此方法会阻塞等待,直到成功拿到独占锁。---->获取该 segment 的独占锁
-
get:计算 hash 值,找到 segment 数组中的具体位置,到这里是链表了,顺着链表进行查找即可
CAS
CAS
全称compare and swap
——比较并替换,它是并发条件下修改数据的一种机制,包含三个操作数:
- 需要修改的数据的内存地址(V);
- 对这个数据的旧预期值(A);
- 需要将它修改为的值(B);
CAS的操作步骤如下:
- 修改前记录数据的内存地址V;
- 读取数据的当前的值,记录为A;
- 计算要修改为的值B;
- 查看地址V下的值是否仍然为A,若为A,则用B替换它;若地址V下的值不为A,表示在自己修改的过程中,其他的线程对数据进行了修改,则不更新变量的值,而是重新从步骤2开始执行,这被称为自旋;
参考链接:https://pdai.tech/md/java/thread/java-thread-x-juc-collection-ConcurrentHashMap.html