Java之ConcurrentHashMap

ConcurrentHashMap

继承自AbstractMap,实现ConcurrentMap接口


保证同步

  • table nextTable sizeCtl baseCount等变量均用volatile修饰
  • 获取元素用Unsafe类的getObjectVolatile方法
  • 设置数组元素用compareAndSwapObject方法
  • 用for循环for (Node<K,V>[] tab = table;;)
  • 如果其他线程正在修改tab,那么尝试就会失败

JDK8中的实现

  • 摒弃了Segment(锁段)的概念
  • 利用CAS算法 + Synchronized
  • 底层采用数组+链表+红黑树的存储结构

并发度(Concurrency Level)

  • 程序运行时能够同时更新ConccurentHashMap且不产生锁竞争的最大线程数
  • 如果并发度设置的过小,会带来严重的锁竞争问题;
  • 如果并发度设置的过大,CPU cache命中率会下降

重要对象

  • table:默认为null,初始化发生在第一次插入操作,默认大小为16的数组,用来存储Node节点数据,扩容时大小总是2的幂次方。
  • nextTable:默认为null,扩容时新生成的数组,其大小为原数组的两倍。

  • sizeCtl:默认为0,用来控制table的初始化和扩容操作
    负数代表正在进行初始化或扩容操作
    -1代表正在初始化
    -N 表示有N-1个线程正在进行扩容操作
    正数或0代表hash表还没有被初始化
    这个数值表示初始化或下一次进行扩容的大小

  • CELLSBUSY:自旋锁

  • DEFAULT_CONCURRENCY_LEVEL:默认的并发度为16
  • LOAD_FACTOR:0.75f
  • TREEIFY_THRESHOLD:8
  • UNTREEIFY_THRESHOLD:6
  • MIN_TREEIFY_CAPACITY:64

重要的类

  • Node
  • TreeNode
  • TreeBin
  • ForwardingNode:一个特殊的Node节点,hash值为-1,其中存储nextTable的引用
  • CAS
    CAS算法实现无锁化的修改值的操作,他可以大大降低锁代理的性能消耗
  • Unsafe
    unsafe代码块控制了一些属性的修改工作,比如最常用的SIZECTL 。

核心方法

实例初始化

实例化ConcurrentHashMap时带参数时,会根据参数调整table的大小,假设参数为100,最终会调整成256,确保table的大小总是2的幂次方

ConcurrentHashMap在构造函数中只会初始化sizeCtl值
并不会直接初始化table,而是延缓到第一次put操作

table初始化

put是可以并发执行的,如何实现table只初始化一次?
sizeCtl默认为0,执行第一次put操作的线程会执行Unsafe.compareAndSwapInt方法修改sizeCtl为-1

如果初始失败 调用yeild 自旋

    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

put()方法

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
            ...
            ...
    }
    addCount(1L, binCount);
    return null;
}

hash算法

static final int spread(int h) {return (h ^ (h >>> 16)) & HASH_BITS;}

定位索引位置并获取索引处元素

if ((f = tabAt(tab, i = (n - 1) & hash)) == null)

取出table
用for循环for (Node<K,V>[] tab = table;;)
如果其他线程正在修改tab,那么尝试就会失败

尝试成功后

  • 如果tab为空或长度为0,初始化table
  • 否则说明table有元素

取出索引位置元素

如果f为null说明table中这个位置第一次插入元素
利用Unsafe.compareAndSwapObject方法插入Node节点
退出循环,addCount(1L, binCount)方法会检查当前容量是否需要进行扩容。

casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null))
  • 如果CAS成功,说明Node节点已经插入
  • 如果CAS失败,自旋重新尝试在这个位置插入节点。

如果f不为null
如果f的hash值为-1,说明当前f是ForwardingNode节点,意味有其它线程正在扩容,则一起进行扩容操作。

其余情况把新的Node节点按链表或红黑树的方式插入到合适的位置,这个过程采用同步内置锁实现并发

put操作类似HashMap
区别是

  • 操作放在for循环里,如果其他线程正在修改tab,那么尝试就会失败
  • 针对首个节点进行加锁操作,而不是segment,进一步减少线程冲突

get()方法

public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        int h = spread(key.hashCode());
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            if ((eh = e.hash) == h) {
                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) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

table扩容

当table容量不足的时候,即table的元素数量达到容量阈值sizeCtl,需要对table进行扩容。
构建一个nextTable,大小为table的两倍。
把table的数据复制到nextTable中。

private final void addCount(long x, int check) {
    ... 省略部分代码
    if (check >= 0) {
        Node<K,V>[] tab, nt; int n, sc;
        while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
               (n = tab.length) < MAXIMUM_CAPACITY) {
            int rs = resizeStamp(n);
            if (sc < 0) {
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                    transferIndex <= 0)
                    break;
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null);
            s = sumCount();
        }
    }
}

通过Unsafe.compareAndSwapInt修改sizeCtl值,保证只有一个线程能够初始化nextTable,
扩容后的数组长度为原来的两倍,但是容量是原来的1.5。


tabAt

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

casTabAt

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

在ConcurrentHashMap(1.8)中,内部使用一个volatile的数组table保存数据,细心的同学可以发现,Doug Lea每次在获取数组的元素时,采用Unsafe类的getObjectVolatile方法,在设置数组元素时,采用compareAndSwapObject方法,而不是直接通过下标去操作,这是为什么?

volatile修饰的变量如果是对象或数组之类的,其含义是对象或数组的地址具有可见性,但是数组或对象内部成员改变不具备可见性

这个是因为Java数组在元素层面的元数据设计上的缺失,无法表达元素是final、volatile等语义,所以使用getObjectVolatile用来补上无法表达元素是volatile的坑,@Stable用来补上final的坑,

数组元素就跟没有标volatile的成员字段一样,无法保证线程之间可见性。

只有触发happens before关系的操作,才能保证线程之间的可见性,比如使用table[0] = new Object()直接赋值,这个赋值不会触发任何happens before关系的操作,相当于对一个无volatile变量进行赋值一样。

猜你喜欢

转载自blog.csdn.net/moni_mm/article/details/80106906