写在前面的话
有所坚持才会有所得,相信行动的力量。
继承关系
继承AbstractMap,实现ConcurrentMap和Serializable接口。
具备map的基本属性,可序列化。
ConcurrentMap接口,是一个能够支持并发访问的java.util.map集合,
在map的基础上提供了4个接口
//插入元素
V putIfAbsent(K key, V value);
//移除元素
boolean remove(Object key, Object value);
//替换元素
boolean replace(K key, V oldValue, V newValue);
//替换元素
V replace(K key, V value);
底层结构
ConcurrentHashMap在初始化时会要求初始化concurrencyLevel作为segment数组长度,即并发度,
代表最多有多少个线程可以同时操作ConcurrentHashMap,默认是16,
每个segment片段里面含有键值对HashEntry数组,
是真正存放键值对的地方。这就是ConcurrentHashMap的数据结构。
数组+链表
具体说来:
分段锁数组+HashEntry数组+链表组成
HashEntry–> Segment–> ConcurrentHashMap
基本属性
//默认的初始容量
static final int DEFAULT_INITIAL_CAPACITY = 16;
//默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//默认的并发度,也就是默认的Segment数组长度
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
//最大容量,ConcurrentMap最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//每个segment中table数组的长度,必须是2^n,最小为2
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;
//允许最大segment数量,用于限定concurrencyLevel的边界,必须是2^n
static final int MAX_SEGMENTS = 1 << 16; // slightly conservative
//非锁定情况下调用size和contains方法的重试次数,避免由于table连续被修改导致无限重试
static final int RETRIES_BEFORE_LOCK = 2;
//计算segment位置的掩码值
final int segmentMask;
//用于计算算segment位置时,hash参与运算的位数
final int segmentShift;
//segmentMask 和 segmentShift作用主要是根据key的hash值做计算定位在哪个Segment片段。
//Segment数组
final Segment<K,V>[] segments;
构造方法
传入初始容量,负载因子和并发读的构造方法
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
//参数校验
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
// Find power-of-two sizes best matching arguments
//找到一个大于等于传入的concurrencyLevel的2^n数,且与concurrencyLevel最接近
//ssize作为Segment数组,必须使2的幂
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
// 默认值,concurrencyLevel 为 16,sshift 为 4
// 那么计算出 segmentShift 为 28,segmentMask 为 15,后面会用到这两个值
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 计算每个segment中table的容量
// 这里根据 initialCapacity 计算 Segment 数组中每个位置可以分到的大小
// 如 initialCapacity 为 64,那么每个 Segment 或称之为"槽"可以分到 4 个
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
// 默认 MIN_SEGMENT_TABLE_CAPACITY 是 2,这个值也是有讲究的,因为这样的话,对于具体的槽上,
// 插入一个元素不至于扩容,插入第二个的时候才会扩容
int cap = MIN_SEGMENT_TABLE_CAPACITY;
// 确保cap是2^n
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]);
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
// 往数组写入 segment[0]
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
可以看出Segment的数组大小必须使2的幂,最小为2,里面的tableEntity数组也必须使2的幂,最小也为2.
只初始化了segment数组0上的值,其他位置仍然是 null。
无参构造方法
public ConcurrentHashMap(){}
默认容量为16,负载因子为0.75,并发度为16.
Segment介绍
分段锁
继承于重入锁ReentrantLock,要想访问Segment片段,线程必须获得同步锁
static final class Segment<K,V> extends ReentrantLock implements Serializable {
//尝试获取锁的最多尝试次数,即自旋次数
static final int MAX_SCAN_RETRIES =
Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
//HashEntry数组,也就是键值对数组,volatile修饰,线程可见性
transient volatile HashEntry<K, V>[] table;
//元素的个数
transient int count;
//segment中发生改变元素的操作的次数,如put/remove
transient int modCount;
//当table大小超过阈值时,对table进行扩容,值为capacity *loadFactor
transient int threshold;
//加载因子
final float loadFactor;
Segment(float lf, int threshold, HashEntry<K, V>[] tab) {
this.loadFactor = lf;
this.threshold = threshold;
this.table = tab;
}
}
HashEntity介绍
源码
static final class HashEntry<K,V> {
//hash值
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;
}
}
键值对HashEntry是ConcurrentHashMap的基本数据结构,多个HashEntry可以形成链表用于解决hash冲突。
和hashmap的entry是一样的结构。
常用方法
put
1.根据键的值定位键值对在哪个segment片段
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
//计算键的hash值
int hash = hash(key);
//通过hash值运算把键值对定位到segment[j]片段上
int j = (hash >>> segmentShift) & segmentMask;
//检查segment[j]是否已经初始化了,没有的话调用ensureSegment初始化segment[j]
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
//向片段中插入键值对
return s.put(key, hash, value, false);
}
详细步骤
-
计算 key 的 hash 值
-
根据 hash 值找到 Segment 数组中的位置 j
-
插入新值到 槽 s 中
ensureSegment(int k)方法源码
Segment初始化的时候只初始化了0位置上的数据,其余Segment用到了再进行初始化,通过延时加载的策略,而延迟加载调用的就是ensureSegment方法
private Segment<K,V> ensureSegment(int k) {
//获取当前的segments数组
final Segment<K,V>[] ss = this.segments;
long u = (k << SSHIFT) + SBASE; // raw offset
Segment<K,V> seg;
//按照segment[0]的HashEntry数组长度和加载因子初始化Segment[k]
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
// 使用当前 segment[0] 处的数组长度和负载因子来初始化 segment[k],这就是之前要初始化 segment[0] 的原因。
// 为什么要用 " 当前 ",因为 segment[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);
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
//再次检查一遍该槽是否被其他线程初始化。
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))== null) { // recheck
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
//unsafe保障内存可见性
// 使用 while 循环,内部用 CAS,当前线程成功设值或其他线程成功设值后,退出
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))== null) {
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))//cas操作,原子性
break;
}
}
}
return seg;
}
该过程会初始化用到的segment,使用UNSAFE的属性进行线程安全的值获取。
2.调用Segment的put方法插入键值对到Segment的HashEntry数组
源码
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
//Segment继承ReentrantLock,在向segment写数据前,尝试获取独占锁,获取失败的话就尝试获取自旋锁,实现都在scanAndLockForPut中
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
//表示获取到锁后
HashEntry<K,V>[] tab = table;
//定位键值对在HashEntry数组上的位置
int index = (tab.length - 1) & hash;
//获取这个位置的第一个键值对
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {//此处有链表结构,一直循环到e==null
K k;
//存在与待插入键值对相同的键,则替换value
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {//onlyIfAbsent默认为false
e.value = value;
++modCount;
}
break;
}
// 继续顺着链表走
e = e.next;
}
else {
//node不为null,设置node的next为first,node为当前链表的头节点(头插法)
if (node != null)
node.setNext(first);
//node为null,创建头节点,指定next为first,node为当前链表的头节点
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
//扩容条件 (1)entry数量大于阈值 (2) 当前数组tab长度小于最大容量。满足以上条件就扩容
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
//扩容
rehash(node);
else
// 没有达到阈值,将 node 放到数组 tab 的 index 位置,
// 将新的结点设置成原链表的表头
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
//解锁
unlock();
}
return oldValue;
}
scanAndLockForPut(K key, int hash, V value)
获取锁的方法源码 在不超过最大重试次数MAX_SCAN_RETRIES通过CAS尝试获取锁
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
//first,e:键值对的hash值定位到数组tab的第一个键值对
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
int retries = -1; // negative while locating node
// 循环获取锁,通过cas获取锁
while (!tryLock()) {
HashEntry<K,V> f; // to recheck first below
if (retries < 0) {
if (e == null) {
if (node == null) // speculatively create node
// 进到这里说明数组该位置的链表是空的,没有任何元素
// 当然,进到这里的另一个原因是 tryLock() 失败,所以该槽存在并发,不一定是该位置
//初始化键值对,next指向null
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
else if (key.equals(e.key))
retries = 0;
else
// 顺着链表往下走
e = e.next;
}
//超过最大自旋次数,阻塞
// 重试次数如果超过 MAX_SCAN_RETRIES(单核 1 次多核 64 次),进入到阻塞队列等待锁
// lock() 是阻塞方法,直到获取锁后返回
else if (++retries > MAX_SCAN_RETRIES) {
lock();
break;
}
//头节点发生变化,重新遍历
else if ((retries & 1) == 0 &&
// 进入这里,说明有新的元素进到了链表,并且成为了新的表头
// 这边的策略是,重新执行 scanAndLockForPut 方法
(f = entryForHash(this, hash)) != first) {
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}
1.7put方法流程总结
1.根据键的值定位键值对在哪个segment片段,同时检查对应的segment是否初始化,没有初始化,会调用ensureSegment方法对用到的segment进行初始化。
2.在这个过程中会调用segment的put方法,在segment的put方法中会先调用scanAndLockForPut方法获取锁。
3.将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。
4.遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
5.不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容,扩容就调用rehash方法
6.最后再解除在第 2 步中所获取当前 Segment 的锁。
扩容方法
rehash()
segment 数组不能扩容,是对 segment 数组某个位置内部的数组 HashEntry数组进行扩容,扩容后容量为原来的 2 倍,
该方法不需要考虑并发,因为执行该方法之前已经获取了锁。
// 方法参数上的 node 是这次扩容后,需要添加到新的数组中的数据。
private void rehash(HashEntry<K,V> node) {
//扩容前的旧tab数组
HashEntry<K,V>[] oldTable = table;
//扩容前数组长度
int oldCapacity = oldTable.length;
//扩容后数组长度(扩容前两倍)
int newCapacity = oldCapacity << 1;
//计算新的阈值
threshold = (int)(newCapacity * loadFactor);
//新的tab数组
HashEntry<K,V>[] newTable =
(HashEntry<K,V>[]) new HashEntry[newCapacity];
//新的掩码,如从 16 扩容到 32,那么 sizeMask 为 31,对应二进制 ‘000...00011111’
int sizeMask = newCapacity - 1;
//遍历旧的数组,将原数组位置 i 处的链表拆分到 新数组位置 i 和 i+oldCap 两个位置
for (int i = 0; i < oldCapacity ; i++) {
//遍历数组的每一个元素
HashEntry<K,V> e = oldTable[i];
if (e != null) {
//元素e指向的下一个节点,如果存在hash冲突那么e不为空
HashEntry<K,V> next = e.next;
//计算元素在新数组的索引
//假设原数组长度为 16,e 在 oldTable[4] 处,那么 idx 只可能是 4 或者是 4 + 16 = 20
int idx = e.hash & sizeMask;
// 桶中只有一个元素,把当前的e设置给新的table
if (next == null) // Single node on list
newTable[idx] = e;
//桶中有布置一个元素的链表
else { // Reuse consecutive sequence at same slot
// e 是链表表头
HashEntry<K,V> lastRun = e;
// idx 是当前链表的头结点 e 的新位置
int lastIdx = idx;
//for 循环找到一个 lastRun 结点,这个结点之后的所有元素是将要放到一起的
for (HashEntry<K,V> last = next;
last != null;
last = last.next) {
//k是单链表元素在新数组的位置
int k = last.hash & sizeMask;
//lastRun是最后一个扩容后不在原桶处的Entry
if (k != lastIdx) {
lastIdx = k;
lastRun = last;
}
}
//lastRun以及它后面的元素都在一个桶中
newTable[lastIdx] = lastRun;
// Clone remaining nodes
//下面的操作是处理 lastRun 之前的结点,
//这些结点可能分配在另一个链表中,也可能分配到上面的那个链表中
for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
V v = p.value;
int h = p.hash;
int k = h & sizeMask;
HashEntry<K,V> n = newTable[k];
newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
}
}
}
}
//处理引起扩容的那个待添加的节点
// 将新来的 node 放到新数组中刚刚的 两个链表之一 的 头部
int nodeIndex = node.hash & sizeMask; // add the new node
node.setNext(newTable[nodeIndex]);
newTable[nodeIndex] = node;
//把Segment的table指向扩容后的table
table = newTable;
}
get方法
get获取元素不需要加锁,效率高,
获取key定位到的segment片段还是遍历table数组的HashEntry元素时
使用了UNSAFE.getObjectVolatile保证了能够无锁且获取到最新的volatile变量的值。
public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
//计算key的hash值
int h = hash(key);
//根据hash值计算key在哪个segment片段
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
//获取segments[u]的table数组
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
//遍历table中的HashEntry元素
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
//找到相同的key,返回value
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
size方法
要统计整个 ConcurrentHashMap 里元素的大小,就必须统计所有 Segment 里元素的大小后求和。
Segment 里的全局变量 count 是一个 volatile 变量。
ConcurrentHashMap 的做法是先尝试 2 次通过不锁住 Segment 的方式统计各个 Segment 大小,如果统计的过程中,容器的 count 发生了变化,则再采用加锁的方式来统计所有 Segment 的大小。
使用 modCount 变量,在 put、remove 和 clean 方法里操作元素前都会将变量 modCount 进行加 1,在统计 size 前后比较 modCount 是否发生变化,从而得知容器的大小是否发生变化。
源码
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// 存储本次循环过程中计算得到的modCount的值
long last = 0L; // previous sum// 存储上一次遍历过程中计算得到的modCount的和
int retries = -1; // first iteration isn't retry
try {
for (;;) {
//无限for循环,结束条件就是任意前后两次遍历过程中modcount值的和是一样的,说明第二次遍历没有做任何变化
//这里就是前面介绍的为了防止由于有线程不断在更新map而导致每次遍历过程一直发现modCount和上一次不一样
//从而导致线程一直进行遍历验证前后两次modCount,为了防止这种情况发生,加了一个最多重复的次数限制,
//超过这个次数则直接强制对所有的segment进行加锁,不过这里需要注意如果出现这种情况,会导致本来要延迟创建的所有segment
//均在这个过程中被创建
//达到RETRIES_BEFORE_LOCK=2,也就是三次
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);
//遍历计算segment的modCount和count的和
if (seg != null) {
sum += seg.modCount;
int c = seg.count;
//是否溢出int范围
if (c < 0 || (size += c) < 0)
overflow = true;
}
}
//last是上一次的sum值,相等跳出循环
if (sum == last)
break;
last = sum;
}
} finally {
//解锁
//由于只有在retries等于RETRIES_BEFORE_LOCK时才会执行强制加锁,并且由于是用的retries++,
//所以强制加锁完毕后,retries的值是一定会大于RETRIES_BEFORE_LOCK的,
//这样就防止正常遍历而没进行加锁时进行锁释放的情况
if (retries > RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
segmentAt(segments, j).unlock();
}
}
return overflow ? Integer.MAX_VALUE : size;
}
remove方法
1.首先会调用Segment的remove方法。
public V remove(Object key) {
//计算hash值
int hash = hash(key);
//确定是哪个segment锁
Segment<K,V> s = segmentForHash(hash);
//调用Segment的remove方法
return s == null ? null : s.remove(key, hash, null);
}
2.获取同步锁,移除指定的键值对
final V remove(Object key, int hash, Object value) {
//获取同步锁
if (!tryLock())
scanAndLock(key, hash);
V oldValue = null;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> e = entryAt(tab, index);
//遍历链表用来保存当前链表节点的前一个节点
HashEntry<K,V> pred = null;
while (e != null) {
K k;
HashEntry<K,V> next = e.next;
//找到key对应的键值对
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
V v = e.value;
//键值对的值与传入的value相等
if (value == null || value == v || value.equals(v)) {
//当前元素为头节点,把当前元素的下一个节点设为头节点
if (pred == null)
setEntryAt(tab, index, next);
//不是头节点,把当前链表节点的前一个节点的next指向当前节点的下一个节点
else
pred.setNext(next);
++modCount;
--count;
oldValue = v;
}
break;
}
pred = e;
e = next;
}
} finally {
//解锁
unlock();
}
return oldValue;
}
scanAndLock获取同步锁的方法解读
扫描是否含有指定的key并且获取同步锁,当方法执行完毕也就是跳出循环肯定成功获取到同步锁,
跳出循环有两种方式:
1.tryLock方法尝试获取独占锁成功
2.尝试获取超过最大自旋次数MAX_SCAN_RETRIES线程堵塞,当线程从等待队列中被唤醒获取到锁跳出循环。
private void scanAndLock(Object key, int hash) {
// similar to but simpler than scanAndLockForPut
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
int retries = -1;
while (!tryLock()) {
HashEntry<K,V> f;
if (retries < 0) {
if (e == null || key.equals(e.key))
retries = 0;
else
e = e.next;
}
else if (++retries > MAX_SCAN_RETRIES) {
lock();
break;
}
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
e = first = f;
retries = -1;
}
}
}
remove流程总结
1.计算hash值,确定是哪个分段锁,调用分段锁的删除方法
2.在分段锁删除方法中先获取同步锁,获取同步锁的方式是调用scanAndLock方法
3.找到对应的具体的hashentry,遍历当前hashentry下的链表。
4.移除指定的键值对,将前链表节点的前一个节点的next指向当前节点的下一个节点。
如果是头结点,就将下一个节点设置为头节点。
isEmpty()方法源码
和size方法类似,也是不加锁操作。
两次遍历:
1.确定每个segment是否为0,其中任何一个segment的count不为0,就返回,都为0,就累加modCount为sum.
2.第一个循环执行完还没有推出,map可能为空,再做一次遍历,如果在这个过程中任何一个segment的count不为0返回false,同时sum减去每个segment的modCount,
若循环执行完程序还没有退出,比较sum是否为0,为0表示两次检查没有元素插入,map确实为空,否则map不为空。
public boolean isEmpty() {
//累计segment的modCount值
long sum = 0L;
final Segment<K,V>[] segments = this.segments;
for (int j = 0; j < segments.length; ++j) {
Segment<K,V> seg = segmentAt(segments, j);
if (seg != null) {
if (seg.count != 0)
return false;
sum += seg.modCount;
}
}
//再次检查
if (sum != 0L) { // recheck unless no modifications
for (int j = 0; j < segments.length; ++j) {
Segment<K,V> seg = segmentAt(segments, j);
if (seg != null) {
if (seg.count != 0)
return false;
sum -= seg.modCount;
}
}
if (sum != 0L)
return false;
}
return true;
}
Unsafe类介绍
Unsafe是Java中一个底层类,包含了很多基础的操作,
比如数组操作、对象操作、内存操作、CAS操作、线程(park)操作、栅栏(Fence)操作,JUC包、一些三方框架都使用Unsafe类来保证并发安全。
在这里主要使用了它保障线程安全。
其中又特别使用到getObjectVolatile、compareAndSwapObject、Unsafe.putObjectVolatile(obj,long,obj2)、 Unsafe.putOrderedObject等方法 源码
void sun::misc::Unsafe::putObjectVolatile (jobject obj, jlong offset, jobject value)
{
write_barrier ();
volatile jobject *addr = (jobject *) ((char *) obj + offset);
*addr = value;
}
void sun::misc::Unsafe::putObject (jobject obj, jlong offset, jobject value)
{
jobject *addr = (jobject *) ((char *) obj + offset);
*addr = value;
}//用于和putObjectVolatile进行对比
jobject sun::misc::Unsafe::getObjectVolatile (jobject obj, jlong offset)
{
volatile jobject *addr = (jobject *) ((char *) obj + offset);
jobject result = *addr;
read_barrier ();
return result;
}
void sun::misc::Unsafe::putOrderedObject (jobject obj, jlong offset, jobject value)
{
volatile jobject *addr = (jobject *) ((char *) obj + offset);
*addr = value;
}
可以看到有write_barrier和read_barrier这两个内存屏障,
这两个就是对应的硬件中的写屏障和读屏障,
java内存模型中使用的所谓的LoadLoad、LoadStore、StoreStore、StoreLoad这几个屏障就是基于这两个屏障实现的。
写屏障的作用就是禁止了指令的重排序,并且配合C语言中的volatile关键字(C中的volatile关键字只能保证可见性不能保证有序性),
就是通过添加内存屏障+C中的Volatile实现了类似Java中的Volatile关键字语义,
putObjectVolatile
putObjectVolatile方法中通过内存屏障保证了有序性,
再通过volatile保证将对指定地址的操作是马上写入到共享的主存中而不是线程自身的本地工作内存中,
这样配合下面的getObjectVolatile方法,就可以确保每次读取到的就是最新的数据。
getObjectVolatile
它在返回前加了read_barrier,
这个读屏障的作用就是强制去读取主存中的数据而不是线程自己的本地工作内存,
这样就确保了读取到的一定是最新的数据。
putOrderedObject
这个方法和putObjectVolatile的区别源码中在于没有加write_barrier,
方法只保证了更新数据的可见性,但是无法保证有序性,因为没有添加屏障可能会导致最终生成的汇编指令被重排序优化,
不过在ConcurrentHashMap中使用到这个方法的地方主要是在put方法更新数据的时候用到了,
而关于put是加锁了的,所以个人理解的是在依据加锁过的代码区域,
用putOrderedObject比putObjectVolatile好在不需要添加屏障,
因为只会有一个线程进行操作,从而允许进行指令优化重排序,从而性能会更好。
compareAndSwapObject
源码解读:
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapObject(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jobject e_h, jobject x_h))
UnsafeWrapper("Unsafe_CompareAndSwapObject");
oop x = JNIHandles::resolve(x_h); // 新值
oop e = JNIHandles::resolve(e_h); // 预期值
oop p = JNIHandles::resolve(obj);
HeapWord* addr = (HeapWord *)index_oop_from_field_offset_long(p, offset);// 在内存中的具体位置
oop res = oopDesc::atomic_compare_exchange_oop(x, addr, e, true);// 调用了另一个方法
jboolean success = (res == e); // 如果返回的res等于e,则判定满足compare条件(说明res应该为内存中的当前值),但实际上会有ABA的问题
if (success) // success为true时,说明此时已经交换成功(调用的是最底层的cmpxchg指令)
update_barrier_set((void*)addr, x); // 每次Reference类型数据写操作时,都会产生一个Write Barrier暂时中断操作,配合垃圾收集器
return success;
UNSAFE_END
原子性操作。
不能为null值的根本原因
1.为了成功地从哈希表存储和检索对象,用作键的对象必须实现hashCode方法和equals方法。
由于null不是对象,因此您无法对其进行调用.equals()或调用.hashCode(),因此Hashtable无法计算哈希值以将其用作键。
2.Hashtable和ConcurrentHashMap不允许空键或值的主要原因是因为期望它们将在多线程环境中使用。
让我们假设允许空值。在这种情况下,哈希表的“ get”方法具有不明确的行为。
如果在映射中找不到键,则可以返回null;如果找到键且其值为null,则可以返回null。
当代码期望空值时,通常会检查映射中是否存在键,以便它可以知道键是否不存在或键是否存在,但value为空。现在,此代码在多线程环境中中断。让我们看下面的代码:
if (map.contains(key)) {
return map.get(key);
} else {
throw new KeyNotFoundException;
}
在上面的代码中,假设线程t1调用contains方法并找到键,并且假定键存在并且可以返回值(无论它是否为null)。
现在,在调用map.get之前,另一个线程t2从地图中删除了该键。
现在t1恢复并返回null。但是根据代码,t1的正确答案是KeyNotFoundException,因为密钥已被删除。但是仍然返回null,因此预期的行为被破坏了。
写在后面的话
今年成都的汛期雨水有点多呀,到处都在看海。
希望大家都没事。
尽管天灾不可预测,但是我们可以尽人事。