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);
    }

从代码中我们可以看到 tabAtcasTabAtsetTabAt 方法都是通过 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
基址比例变址寻址

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值