ConcurrentHashMap 中的元素访问
背景
众所周知 ConcurrentHashMap
为线程安全的 HashMap
, 那么今天就来研究下 ConcurrentHashMap
是如何安全的访问内部元素的。
首先看下 ConcurrentHashMap
内部访问元素用到的代码
@SuppressWarnings("unchecked")
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
从代码中我们可以看到 tabAt
、casTabAt
、 setTabAt
方法都是通过 U
类方法最终来获取元素。那么这个 U
又是什么东西呢?
// Unsafe mechanics
private static final sun.misc.Unsafe U;
...
U = sun.misc.Unsafe.getUnsafe();
我们可以从上方代码看到注释中写着非安全操作,既然是不安全的为什么要用呢?
我们从 openjdk
找到 Unsafe
类的注释:
/**
* A collection of methods for performing low-level, unsafe operations.
* Although the class and all methods are public, use of this class is
* limited because only trusted code can obtain instances of it.
*
* @author John R. Rose
* @see #getUnsafe
*/
注释表明这个类集合了执行底层操作方法,这些方法是不安全的操作。虽然都是公共方法但是这个类是被限制的只有被信任的代码才能使用。也就是说ConcurrentHashMap
最终通过Unsafe
这个类来调用底层方法访问内部的元素,来达到一个线程安全的目的。
接下来我们看下ConcurrentHashMap
中用到了Unsafe
中的那些方法。
从数组中获取 Node 节点
@SuppressWarnings("unchecked")
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
tabAt
方法主要用于从数组指定某个下标中找到对应元素。像 get
方法获取 Node
节点和 put
方法在存入数据时判断节点位置是否有值都是调用的此方法。
那么为何此方法是线程安全的的呢?为什么不直接通过 tab[i] 的方法获取呢?我们一起看下这个 getObjectVolatile
方法有什么神奇之处。
/**
* Fetches a reference value from a given Java variable, with volatile
* load semantics. Otherwise identical to {@link #getObject(Object, long)}
*/
public native Object getObjectVolatile(Object o, long offset);
getObjectVolatile
的作用是通过volatile
加载语义从指定的java变量中获取到指定偏移量的引用值。简单来说就是强制计算机使用 volatile
加载语义读取变量使其变得多线程访问安全。
接下来我们在看下偏移量的计算方法
((long)i << ASHIFT) + ABASE
分别看下 ASHIFT
ABASE
两个变量代表什么
// 变量申明,省略部分代码
private static final int ASHIFT;
private static final long ABASE;
...
Class<?> ak = Node[].class;
ABASE = U.arrayBaseOffset(ak);
...
int scale = U.arrayIndexScale(ak);
ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);
arrayBaseOffset
可以获取 Node[]
数组的第一个元素偏移量(ABASE
),ASHIFT
表示左移几位。
((long)i << ASHIFT) + ABASE
可以转换为简单公式:有效地址=基址+(变址*比例因子)
这里scale
必定为2的倍数(有判断否则会抛出异常),所以可以直接用位运算计算表达式变址*比例因子
可以转为变址 << 比例因子位数-1
。
numberOfLeadingZeros
可以计算得到Node[]
数组存储分配元素寻址的比例因子数(scale
)高位前面有几个 0。所以直接用 31 - 高位前面几个0
就可以算出 比例因子位数-1
。
从数组中替换 Node 节点
再看下我们替换数组中的节点
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
// Unsafe.java
* Atomically update Java variable to <tt>x</tt> if it is currently
* holding <tt>expected</tt>.
* @return <tt>true</tt> if successful
*/
public final native boolean compareAndSwapObject(Object o, long offset,
Object expected,
Object x);
compareAndSwapObject
在指定地址中的内容为期待的内容时将内容替换为指定内容,也就相当于乐观锁的方式执行替换。所以用这个方法替换内容是是相对安全的。
从数组中存入 Node 节点
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
// Unsafe.java
/**
* Stores a reference value into a given Java variable, with
* volatile store semantics. Otherwise identical to {@link #putObject(Object, long, Object)}
*/
public native void putObjectVolatile(Object o, long offset, Object x);
我们可以看到同样的putObjectVolatile
方法以volatile
语义加载的方式将引用值存入指定位置中。
总结
ConcurrentHashMap
通过Unsafe
类的方法以volatile
语义加载、CAS等方式去更新值加强多线程安全,并以位运算代替乘法提高了运算速度
参考:
openjdk Unsafe.java
Java魔法类:Unsafe应用解析
Synchronization and the Java Memory Model
基址比例变址寻址