Java 8 中的ConcurrentHashMap源码分析

在HashMap的分析中,介绍了hashmap不是线程安全的,其在并发环境使用fail-fast策略来抛出由并发错误导致的异常。

先来看下Hashtable这个线程安全的容器,其虽然是线程安全的,但是其实现并发安全的手段比较粗暴。从下面的三个方法就能看出来,其只是简单的以自身作为对象锁,将相关方法都声明为synchronized,故每次只有一个线程能调用这些同步方法。

//Hashtable源码中的contains、get、put方法:
    public synchronized boolean containsKey(Object key) {
        ... ...
    }
    public synchronized V get(Object key) {
        ... ...
    }
    public synchronized V put(K key, V value) {
        if (value == null) {                   //Hashtable要求value不能为null
            throw new NullPointerException();
        }

        Entry<?,?> tab[] = table;
        int hash = key.hashCode();             //此处调用了key的hashCode方法,若key=null会抛出空指针异常
        int index = (hash & 0x7FFFFFFF) % tab.length;
        ... ...                                //而在HashMap中,当key=null时,会得到一个h=0的h值以计算index,如下:
    }                                          //return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)

此处再补充两点:

1.HashMap与Hashtable的不同之处:
(1)Hashtable 继承自 Dictiionary 而 HashMap继承自AbstractMap
(2)Hashtable 是线程安全的(虽然其同步实现的很粗暴),而HashMap使用fail-fast策略抛出异常
(3)Hashtable 不允许null key或null value(见上面的put方法),而hashMap允许一个null key和任意个null value

2.同步方法与同步块在JVM中的执行:
同步方法在编译之后,在class文件的方法表对改方法的描述中,会有一个标明此方法为synchronized的标志位。当调用此方法时,执行引擎先去方法表中查看此标志位,若是synchronized的,就要求调用线程先成功持有管程(monitor),才能调用此方法。而对于更细化的同步块而言,当用javac编译之后,会在字节码中插入两条字节码指令:monitorenter与monitorexit,以此来保证同步块内代码的原子性。

关于ConcurrentHashMap:
类似于Hashtable,并不支持null key或null value。

在源码中,author在overview中写明ConcurrentHashMap的设计目标:
1. The primary design goal of this hash table is to maintain concurrent readability (typically method get(), but also iterators and related methods) while minimizing update contention.
2. Secondary goals are to keep space consumption about the same or better than java.util.HashMap, and to support high initial insertion rates on an empty table by many threads.

其底层实现形式也是bucket数组的形式,且<key, value>存储在node中。但其比HashMap更加复杂,包括basic Node、TreeNode、ForwardingNode、ReservationNode,后三种一般有其特殊用途。

ConcurrentHashMap使用延迟初始化策略,在第一次insert时,才分配一个2^n大小的bucket数组,一般的bucket为list形式,通过Node中next属性来实现list。

当在一个空bin中insert第一个node时,其使用CAS操作来同步,而对于其他的update操作(insert,delete, and replace)则需要使用锁来同步。而在每一个bucket中,一般使用此bin中第一个node作为这个bin的锁,锁住整个bucket。(因为新放入bin的node总会添加到list的末尾,故除了delete掉第一个节点或resize数组之外,这个节点总是此bin的第一个node,具有稳定性)。锁住整个bucket的策略是合理的,因为在实际使用中,一个bucket中的node不会太多(0.75的装填因子),所以一般锁住整个bin不会造成特别恶劣的性能影响。(同样ConcurrentHashMap也会使用tree化的策略,将过深的bin进行tree化,即使用红黑树来降低bin的深度,将查找时间限制为O(logN))。

private static final int DEFAULT_CAPACITY = 16;            //默认bin数组的大小,必须为2的n次方
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;   //在java 8中已经不再使用这个量了,只是为了向前兼容才保留的
private static final float LOAD_FACTOR = 0.75f;            //默认装填因子

static final int TREEIFY_THRESHOLD = 8;     //当bin中大于8个node时,将此bin由list式转换为tree式
static final int UNTREEIFY_THRESHOLD = 6;   //逆tree化
static final int MIN_TREEIFY_CAPACITY = 64; //大于64个bucket了,才会考虑tree化

static final int NCPU = Runtime.getRuntime().availableProcessors();   //取得CPU个数
transient volatile Node<K,V>[] table;        //与HashMap相比,增加了volatile声明
private transient volatile long baseCount;   //base counter,经由CAS来update

封装<key, value>对的内部类Node:

内部类basic Node:
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;                //注意下面两行,与HashMap不一样:
        volatile V val;             //  V value;
        volatile Node<K,V> next;    //  Node<K,V> next;
                                    //将value与next均设为volatile,为了保证可见性??
        ... ...                     //Node中并无synchronized方法
    }

//调整hash值分布的方法,类似于HashMap中的hash(Object key)方法
    static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS;
    }

构造器constructor:

public ConcurrentHashMap() {     //不是省略,是源码就没实现这个constructor,意思是:
    }                                //Creates a new, empty map with the default initial table size (16).

    public ConcurrentHashMap(int initialCapacity) {
        ... ...
    }

    public ConcurrentHashMap(int initialCapacity, float loadFactor) {
        this(initialCapacity, loadFactor, 1);
    }

    public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {   //注意这个构造器

        if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)             //验证输入参数是否合法
            throw new IllegalArgumentException();

        if (initialCapacity < concurrencyLevel)    //注意此处concurrencyLevel的使用,在以前的老版本中,可能用其限制最多的写线程数量
            initialCapacity = concurrencyLevel;    //而在java 8 中,只是使用其作为bin数组大小的一个建议(hint)

        ... ...
    }

size( )方法:

//size()方法:在HashMap中,size()方法直接返回size变量,且变量size也没有使用volatile或其他同步机制
    public int size() {
        long n = sumCount();
        return ((n < 0L) ? 0 : (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n);
    }

    final long sumCount() {
        CounterCell[] as = counterCells;   //这是什么???
        CounterCell a;
        long sum = baseCount;              //baseCount如上,是volatile的
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
    }

contains与get方法:

//对于contains与get这样的读方法,并未使用synchronized这样的同步限制,应该是能让任意个线程来读map的
    public boolean containsKey(Object key) {
        return get(key) != null;
    }

    public V get(Object key) {
        Node<K,V>[] tab; 
        Node<K,V> e, p; 
        int n, eh; 
        K ek;
        int h = spread(key.hashCode());                         //同hashmap一样,通过位运算来使key分布更均匀
                                                                //且此处调用key.hashCode(),故当key=null时,会抛出空指针异常
                   //table是底层数组,volatile的              //e取得bin中的first node
        if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & h)) != null) {
            if ((eh = e.hash) == h) {                                       //bin中的first node就是所欲查找的node
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;

            while ((e = e.next) != null) {       //bin中的first node不是欲找,就遍历这个bucket来查找
                if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

关键的put( )方法:(多注意其中的同步机制及对同步块的细化)

public V put(K key, V value) {
        return putVal(key, value, false);
    }

   final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null)          //ConcurrentHashMap不允许null key或null value
            throw new NullPointerException();

        int hash = spread(key.hashCode());         //用位运算处理key.hashCode以使key分布更均匀
        int binCount = 0;                          //记录bucket中的node的数量

        for (Node<K,V>[] tab = table;;) {          //赋tab = 底层bin数组
            Node<K,V> f; 
            int n, i, fh;

            if (tab == null || (n = tab.length) == 0)     //延迟初始化,在第一次put的时候,才初始化
                tab = initTable();
                      //f代表这个bucket中的第一个node
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {         //此bucket为空的
                if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
                    break;                       //当向empty bin 中添加node时,并不使用锁,而使用CAS操作来添加
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);      //???

            else {                               //***put操作的核心之处***
                V oldVal = null;
                synchronized (f) {               //以此bucket中的first node作为对象锁,锁住整个bucket
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;                                  //在bucket中找到了同key的node,就替换其value
                                if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {           //遍历到list末尾了,仍未找到key,就新建一个node,添加到list末尾
                                    pred.next = new Node<K,V>(hash, key, value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {
                             ... ...
                        }
                    }
                }

                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)       //当bucket中node过多时,将此bucket转化为tree式的
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);              //里面也用到CAS操作了
        return null;
    }


在put操作中提到,当bucket为empty时,insert并不使用锁,而是使用CAS操作来将 new node方法此bucket作为first node,如下:

if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {                      //当前bucketfirst node = null
     if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))  //使用CAS来insert一个新node
           break;
}
//CAS操作的静态方法:
    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);
    }

分析:casTabAt返回boolean型,若cas操成功了,返回true,然后break出外层的循环,则此put操作已经实现了,只要再修改下binCount的值就可以return了。而若cas操作失败了,返回了false,注意外面的那层无限循环【for (Node<K,V>[] tab = table;;) 】,则会重新进入循环,再次put操作,若此时bucket仍然为empty,它仍会尝试使用CAS进行插入。但若此时bucket已经不再empty了,那这次的put就不再使用CAS了,而是使用同步锁了。

 

posted @ 2015-07-16 14:41  Mr.do  阅读(156)  评论(0编辑  收藏  举报