ConcurrentHashMap在jdk/java 1.8/8种的Unsafe与CAS操作

来源:互联网 发布:linux 新建log文件 编辑:程序博客网 时间:2024/09/21 09:25

导语:JDK8中的实现


ConcurrentHashMap在JDK8中进行了巨大改动,很需要通过源码来再次学习下Doug Lea的实现方法。

它摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现,利用CAS算法。它沿用了与它同时期的HashMap版本的思想,底层依然由“数组”+链表+红黑树的方式思想(JDK7与JDK8中HashMap的实现),但是为了做到并发,又增加了很多辅助的类,例如TreeBin,Traverser等对象内部类。

Unsafe与CAS

在ConcurrentHashMap中,随处可以看到U, 大量使用了U.compareAndSwapXXX的方法,这个方法是利用一个CAS算法实现无锁化的修改值的操作,他可以大大降低锁代理的性能消耗。这个算法的基本思想就是不断地去比较当前内存中的变量值与你指定的一个变量值是否相等,如果相等,则接受你指定的修改的值,否则拒绝你的操作。因为当前线程中的值已经不是最新的值,你的修改很可能会覆盖掉其他线程修改的结果。这一点与乐观锁,SVN的思想是比较类似的。

1 unsafe静态块

unsafe代码块控制了一些属性的修改工作,比如最常用的SIZECTL 。在这一版本的concurrentHashMap中,大量应用来的CAS方法进行变量、属性的修改工作。利用CAS进行无锁操作,可以大大提高性能。

private static final sun.misc.Unsafe U;   private static final long SIZECTL;   private static final long TRANSFERINDEX;   private static final long BASECOUNT;   private static final long CELLSBUSY;   private static final long CELLVALUE;   private static final long ABASE;   private static final int ASHIFT;    static {       try {           U = sun.misc.Unsafe.getUnsafe();           Class<?> k = ConcurrentHashMap.class;           SIZECTL = U.objectFieldOffset               (k.getDeclaredField("sizeCtl"));           TRANSFERINDEX = U.objectFieldOffset               (k.getDeclaredField("transferIndex"));           BASECOUNT = U.objectFieldOffset               (k.getDeclaredField("baseCount"));           CELLSBUSY = U.objectFieldOffset               (k.getDeclaredField("cellsBusy"));           Class<?> ck = CounterCell.class;           CELLVALUE = U.objectFieldOffset               (ck.getDeclaredField("value"));           Class<?> ak = Node[].class;           ABASE = U.arrayBaseOffset(ak);           int scale = U.arrayIndexScale(ak);           if ((scale & (scale - 1)) != 0)               throw new Error("data type scale not a power of two");           ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);       }catch (Exception e) {           throw new Error(e);       }   }


2 三个核心方法

ConcurrentHashMap定义了三个原子操作,用于对指定位置的节点进行操作。正是这些原子操作保证了ConcurrentHashMap的线程安全。

//获得在i位置上的Node节点    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);    }        //利用CAS算法设置i位置上的Node节点。之所以能实现并发是因为他指定了原来这个节点的值是多少        //在CAS算法中,会比较内存中的值与你指定的这个值是否相等,如果相等才接受你的修改,否则拒绝你的修改        //因此当前线程中的值并不是最新的值,这种修改可能会覆盖掉其他线程的修改结果  有点类似于SVN    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);    }        //利用volatile方法设置节点位置的值    static final <K,V>void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {        U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);    }



3.
我们可以发现JDK8中的实现也是锁分离的思想,只是锁住的是一个Node,而不是JDK7中的Segment,而锁住Node之前的操作是无锁的并且也是线程安全的,建立在之前提到的3个原子操作上。

4.总结

jdk7中ConcurrentHashmap中,当长度过长碰撞会很频繁,链表的增改删查操作都会消耗很长的时间,影响性能,所以jdk8 中完全重写了concurrentHashmap,代码量从原来的1000多行变成了 6000多 行,实现上也和原来的分段式存储有很大的区别。

主要设计上的变化有以下几点:

  1. 不采用segment而采用node,锁住node来实现减小锁粒度。
  2. 设计了MOVED状态 当resize的中过程中 线程2还在put数据,线程2会帮助resize。
  3. 使用3个CAS操作来确保node的一些操作的原子性,这种方式代替了锁。
  4. sizeCtl的不同值来代表不同含义,起到了控制的作用。

至于为什么JDK8中使用synchronized而不是ReentrantLock,我猜是因为JDK8中对synchronized有了足够的优化吧。