学习ConcurrentHashMap1.7分段锁原理
1. 概述
接上一篇 学习 ConcurrentHashMap1.8 并发写机制, 本文主要学习 Segment分段锁
的实现原理。
虽然 JDK1.7
在生产环境已逐渐被 JDK1.8
替代,然而一些好的思想还是需要进行学习的。比方说位图中寻找 bit
位的思路是不是和 ConcurrentHashMap1.7
有点相似?
接下来,本文基于 OpenJDK7
来做源码解析。
2. ConcurrentHashMap1.7 初认识
ConcurrentHashMap 中 put()是线程安全的。但是很多时候, 由于业务需求, 需要先 get()
操作再 put()
操作,这 2 个操作无法保证原子性,这样就会产生线程安全问题了。大家在开发中一定要注意。
ConcurrentHashMap 的结构示意图如下:
在进行数据的定位时,会首先找到 segment
, 然后在 segment
中定位 bucket
。如果多线程操作同一个 segment
, 就会触发 segment
的锁 ReentrantLock
, 这就是分段锁的基本实现原理。
3. 源码分析
3.1 HashEntry
HashEntry
是 ConcurrentHashMap
的基础单元(节点),是实际数据的载体。
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
/**
* Sets next field with volatile write semantics. (See above
* about use of putOrderedObject.)
*/
final void setNext(HashEntry<K,V> n) {
UNSAFE.putOrderedObject(this, nextOffset, n);
}
// Unsafe mechanics
static final sun.misc.Unsafe UNSAFE;
static final long nextOffset;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class k = HashEntry.class;
nextOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("next"));
} catch (Exception e) {
throw new Error(e);
}
}
}
3.2 Segment
Segment
继承 ReentrantLock
锁,用于存放数组 HashEntry[]
。在这里可以看出, 无论 1.7 还是 1.8 版本, ConcurrentHashMap
底层并不是对 HashMap
的扩展, 而是同样从底层基于数组+链表进行功能实现。
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;
Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
this.loadFactor = lf;
this.threshold = threshold;
this.table = tab;
}
// 具体方法不在这里讨论...
}
3.3 构造方法
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
// 对于concurrencyLevel的理解, 可以理解为segments数组的长度,即理论上多线程并发数(分段锁), 默认16
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
// Find power-of-two sizes best matching arguments
int sshift = 0;
int ssize = 1;
// 默认concurrencyLevel = 16, 所以ssize在默认情况下也是16,此时 sshift = 4
// ssize = 2^sshift 即 ssize = 1 << sshift
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
// 段偏移量,32是因为hash是int值,int值32位,默认值情况下此时segmentShift = 28
this.segmentShift = 32 - sshift;
// 散列算法的掩码,默认值情况下segmentMask = 15, 定位segment的时候需要根据segment[]长度取模, 即hash(key)&(ssize - 1)
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 计算每个segment中table的容量, 初始容量=16, 并发数=16, 则segment中的Entry[]长度为1。
int c = initialCapacity / ssize;
// 处理无法整除的情况,取上限
if (c * ssize < initialCapacity)
++c;
// MIN_SEGMENT_TABLE_CAPACITY默认时2,cap是2的n次方
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
// create segments and segments[0]
// 创建segments并初始化第一个segment数组,其余的segment延迟初始化
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
(HashEntry<K,V>[])new HashEntry[cap]);
// 默认并发数=16
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
由图和源码可知,当用默认构造函数时,最大并发数是 16,即最大允许 16 个线程同步写操作,且无法扩展。所以如果我们的场景数据量比较大时,应该设置合适的并发数,避免频繁锁冲突。
3.4 put()操作
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
// 根据key的hash再次进行hash运算
int hash = hash(key.hashCode());
// 基于hash定位segment数组的索引。
// hash值是int值,32bits。segmentShift=28,无符号右移28位,剩下高4位,其余补0。
// segmentMask=15,二进制低4位全部是1,所以j相当于hash右移后的低4位。
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
// 找到对应segment
s = ensureSegment(j);
// 将新节点插入segment中
return s.put(key, hash, value, false);
}
找出对应 segment,如果不存在就创建并初始化
@SuppressWarnings("unchecked")
private Segment<K,V> ensureSegment(int k) {
// 当前的segments数组
final Segment<K,V>[] ss = this.segments;
// 计算原始偏移量,在segments数组的位置
long u = (k << SSHIFT) + SBASE; // raw offset
Segment<K,V> seg;
// 判断没有被初始化
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
// 获取第一个segment ss[0]作为原型
Segment<K,V> proto = ss[0]; // use segment 0 as prototype
int cap = proto.table.length; // 容量
float lf = proto.loadFactor; // 负载因子
int threshold = (int)(cap * lf); // 阈值
// 初始化ss[k] 内部的tab数组 // recheck
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
// 再次检查这个ss[k] 有没有被初始化
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) { // recheck
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
// 自旋。getObjectVolatile 保证了读的可见性,所以一旦有一个线程初始化了,那么就结束自旋
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}
3.5 segment 插入节点
上一步找到 segment 位置后计算节点在 segment 中的位置。
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 是否获取锁,失败自旋获取锁(直到成功)
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value); // 失败了才会scanAndLockForPut
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
// 获取到bucket位置的第一个节点
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
// hash冲突
if (e != null) {
K k;
// key相等则覆盖
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
// 不相等则遍历链表
e = e.next;
}
else {
if (node != null)
// 将新节点插入链表作为表头
node.setNext(first);
else
// 创建新节点并插入表头
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
// 判断元素个数是否超过了阈值或者segment中数组的长度超过了MAXIMUM_CAPACITY,如果满足条件则rehash扩容!
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
// 扩容
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
// 解锁
unlock();
}
return oldValue;
}
如果加锁失败则先走 scanAndLockForPut()
方法。
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
// 根据hash获取头结点
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
int retries = -1; // negative while locating node
// 尝试获取锁,成功就返回,失败就开始自旋
while (!tryLock()) {
HashEntry<K,V> f; // to recheck first below
if (retries < 0) {
// 如果头结点不存在
if (e == null) {
if (node == null) // speculatively create node
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
// 和头结点key相等
else if (key.equals(e.key))
retries = 0;
else
// 下一个节点 直到为null
e = e.next;
}
// 达到自旋的最大次数
else if (++retries > MAX_SCAN_RETRIES) {
// lock()是阻塞方法。进入加锁方法,失败进入队列,阻塞当前线程
lock();
break;
}
// TODO (retries & 1) == 0 没理解
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
// 头结点变化,需要重新遍历,说明有新的节点加入或者移除
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}
(retries & 1) == 0 没理解是在做什么,有小伙伴看明白了请赐教。
最后
本文到此结束,主要是学习分段锁是如何工作的。谢谢大家的观看。
学习ConcurrentHashMap1.7分段锁原理的更多相关文章
- 关于分布式锁原理的一些学习与思考-redis分布式锁,zookeeper分布式锁
首先分布式锁和我们平常讲到的锁原理基本一样,目的就是确保,在多个线程并发时,只有一个线程在同一刻操作这个业务或者说方法.变量. 在一个进程中,也就是一个jvm 或者说应用中,我们很容易去处理控制,在j ...
- 并发编程学习笔记(6)----公平锁和ReentrantReadWriteLock使用及原理
(一)公平锁 1.什么是公平锁? 公平锁指的是在某个线程释放锁之后,等待的线程获取锁的策略是以请求获取锁的时间为标准的,即使先请求获取锁的线程先拿到锁. 2.在java中的实现? 在java的并发包中 ...
- AQS学习(二) AQS互斥模式与ReenterLock可重入锁原理解析
1. MyAQS介绍 在这个系列博客中,我们会参考着jdk的AbstractQueuedLongSynchronizer,从零开始自己动手实现一个AQS(MyAQS).通过模仿,自己造轮子来学习 ...
- JDK8的 CHM 为何放弃分段锁
概述 我们知道, 在 Java 5 之后,JDK 引入了 java.util.concurrent 并发包 ,其中最常用的就是 ConcurrentHashMap 了, 它的原理是引用了内部的 Seg ...
- Zookeeper--0300--java操作Zookeeper,临时节点实现分布式锁原理
删除Zookeeper的java客户端有 : 1,Zookeeper官方提供的原生API, 2,zkClient,在原生api上进行扩展的开源java客户端 3, 一.Zookeeper原生API ...
- 集成学习之Boosting —— Gradient Boosting原理
集成学习之Boosting -- AdaBoost原理 集成学习之Boosting -- AdaBoost实现 集成学习之Boosting -- Gradient Boosting原理 集成学习之Bo ...
- 手写一个线程池,带你学习ThreadPoolExecutor线程池实现原理
摘要:从手写线程池开始,逐步的分析这些代码在Java的线程池中是如何实现的. 本文分享自华为云社区<手写线程池,对照学习ThreadPoolExecutor线程池实现原理!>,作者:小傅哥 ...
- 利用多写Redis实现分布式锁原理与实现分析(转)
利用多写Redis实现分布式锁原理与实现分析 一.关于分布式锁 关于分布式锁,可能绝大部分人都会或多或少涉及到. 我举二个例子:场景一:从前端界面发起一笔支付请求,如果前端没有做防重处理,那么可能 ...
- Java IO学习笔记:概念与原理
Java IO学习笔记:概念与原理 一.概念 Java中对文件的操作是以流的方式进行的.流是Java内存中的一组有序数据序列.Java将数据从源(文件.内存.键盘.网络)读入到内存 中,形成了 ...
随机推荐
- OpenCV Mat - 基本图像容器
Mat 在2001年刚刚出现的时候,OpenCV基于 C 语言接口而建.为了在内存(memory)中存放图像,当时采用名为 IplImage 的C语言结构体,时至今日这仍出现在大多数的旧版教程和教学材 ...
- Netflix拒上戛纳电影节,能给国内视频产业什么启示?
当新事物诞生时,总是会遭到质疑,甚至是排斥!因为新事物的活力.潜力,都对保守的传统事物产生了极大的冲击.就像有声电影刚刚诞生时,一代"默片大师"卓别林就对其进行了激烈的反对.他认为 ...
- python开发时小问题之端口占用
昨天开发时遇到个小问题: 在使用pycharm编写tornado代码时: 直接用这种方式开启了服务,当我想修改代码时发现端口已经被占用代码提交不上去 所以现在该关闭进程: 步骤一: 打开CMD 步骤二 ...
- 使用JavaServer Faces技术的Web模块:hello1 example
该hello1应用程序是一个Web模块,它使用JavaServer Faces技术来显示问候语和响应.您可以使用文本编辑器查看应用程序文件,也可以使用NetBeans IDE. 此应用程序的源代码位于 ...
- Data Visualization and D3.js 笔记(1)
课程地址: https://classroom.udacity.com/courses/ud507 什么是数据可视化? 高效传达一个故事/概念,探索数据的pattern 通过颜色.尺寸.形式在视觉上表 ...
- 多线程的lock功能
import threading def job1(): global A, lock lock.acquire() for i in range(10): A += 1 print('job1', ...
- 网络编程01 · 艺
Web Socket和Socket 套接字,实际就是传输层的接口.用于抽象传输层,隐藏细节.一对套接字可以进行通信. Web Socket,是基于TCP协议的.类似于,http. 为什么需要Web S ...
- JAVA如何判断两个字符串是否相等
==比较引用,equals 比较值 1.java中字符串的比较:== 我们经常习惯性的写上if(str1==str2),这种写法在java中可能会带来问题 example1: String a=&qu ...
- Redis(1)——5种基本数据结构
一.Redis 简介 "Redis is an open source (BSD licensed), in-memory data structure store, used as a d ...
- 【教程向】配置属于自己的vim
新建Vim配置文件 Linux mkdir -/.vimrc 配置 常用设置 配置 功能 set number 设置行号 set systax on 高亮 colorscheme{主题} 设置颜色主题 ...