ConcurrentHashMap

374 阅读7分钟

1.线程不安全的HashMap 当使用多线程向HashMap中添加数据时,并发执行put操作会引起死循环,多线程会导致HashMap的Entry链表形成环形数据结构,这就会导致Entry的next节点永不为空,在获取Entry时就会进入死循环

2.效率低下的HashTable HashTable容器使用synchronized来保证线程安全,在线程竞争激烈的情况下HashTable的效率非常地下。当一个线程访问同步方法时,其他线程会阻塞。

3.ConcurrentHashMap的锁分段技术可以有效提升并发访问率 锁分段技术:容器中有多把锁,每一把锁用于锁容器其中一部分数据 。当多线程访问的是不同数据段的数据,线程间就不会存在锁竞争。 Map中的多个锁使用Segment数组来实现

使用Node数组来存储,数组与单链表的组合结构存储数据

transient volatile Node<K,V>[] table;

初始化容器

public ConcurrentHashMap(int initialCapacity,
                         float loadFactor, int concurrencyLevel) {
    if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    if (initialCapacity < concurrencyLevel)   // Use at least as many bins
        initialCapacity = concurrencyLevel;   // as estimated threads
    long size = (long)(1.0 + (long)initialCapacity / loadFactor);
    int cap = (size >= (long)MAXIMUM_CAPACITY) ?
        MAXIMUM_CAPACITY : tableSizeFor((int)size);
    this.sizeCtl = cap;
}

concurrencyLevel表示并发数(锁的个数),即Segment数组的长度。为了能按位与散列运算定位segment数组的索引,必须保证segments数组的长度是2的N次方

segmentShift和segmentMask

int sshift = 0;
int ssize = 1;
while (ssize < DEFAULT_CONCURRENCY_LEVEL) {
    ++sshift;
    ssize <<= 1;
}
int segmentShift = 32 - sshift;
int segmentMask = ssize - 1;
@SuppressWarnings("unchecked")
Segment<K,V>[] segments = (Segment<K,V>[])
    new Segment<?,?>[DEFAULT_CONCURRENCY_LEVEL];

sshift表示ssize从1向左移动的次数,默认concurrencyLevel等于16,需要向左移动4次,则sshift = 4。 segmentShift 用于定位参与散列运算的位数, 32-4 = 28 segmentMask 是散列运算的掩码, 2^16 -1= 65536-1 = 65535. 二进制16位,每位都是1

ConcurrentHashMap使用分段锁Segment来保护不同段的数据 在插入和获取元素的时候,必须先通过散列算法定位到Segment。

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
        }。。。。。。。。。。。。。。。。
static final int spread(int h) {
    return (h ^ (h >>> 16)) & HASH_BITS;
}

对 hashCode值进行再散列,使高位也能参与散列。如果高位不同,低位相同,散列值很大可能一样

ConcurrentHashMap操作 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;
}

整个get过程不需要加锁,对get方法里要用到的共享变量都定义成volatile类型

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;
    volatile Node<K,V> next;

即使两个线程同时修改和获取volatile变量,由于happen before原则,volatile字段的写操作先于读操作。get方法也能拿到最新的值 这是用volatile替换锁的经典应用场景

put put方法需要对共享变量进行写入操作,为了线程安全,在操作共享变量时必须加锁。

V oldVal = null;
synchronized (f) {
    if (tabAt(tab, i) == f) {
        if (fh >= 0) {
            binCount = 1;
            for (Node<K,V> e = f;; ++binCount) {
                K ek;
                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) {
                    pred.next = new Node<K,V>(hash, key,
                                              value, null);
                    break;
                }
            }
        }
        else if (f instanceof TreeBin) {
            Node<K,V> p;
            binCount = 2;
            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                           value)) != null) {
                oldVal = p.val;
                if (!onlyIfAbsent)
                    p.val = value;
            }
        }
    }
}

size 要统计ConcurrentHashMap里元素的大小,就必须统计所有Segment里元素的大小。

final long sumCount() {
    CounterCell[] as = counterCells; CounterCell a;
    long sum = baseCount;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}
   虽然相加时可以获取每个Segment的count的最新值,但是累加时,可能之前加过的count发生了变化,这就导致结果不准确。最安全的做法是在统计size时,将所有Segment的put,remove,clean方法都锁住,但是这种做法效率低下
   由于累加过程中,之前累加过的count发生变化的几率非常小,所以ConcurrentHashMap是先尝试2次不锁住Segment方式进行统计,如果统计过程中count发生了变化,则再采用加锁的方式来统计所有Segment的大小。

ConcurrentLinkedQueue 线程安全队列 两种实现方式:1.使用阻塞算法,入队和出队各用一把锁 2.非阻塞算法,使用循环CAS操作来实现

ConcurrentLinkedQueue是一个基于链表实现的FIFO队列,采用"wait-free" 算法(CAS)来实现
该队列由 head节点和tail节点组成,每个节点由 节点元素和指向下一个节点的引用组成。
private static class Node<E> {
    volatile E item;
    volatile Node<E> next;

该队列由head和tail节点组成,默认head节点存储的元素为空

offer() :入队列

public boolean offer(E e) {
    checkNotNull(e);
    final Node<E> newNode = new Node<E>(e);

    for (Node<E> t = tail, p = t;;) {
        Node<E> q = p.next;
        if (q == null) { 
            // p 是尾部节点
            if (p.casNext(null, newNode)) {//CAS操作在尾部插入新节点
               //如果p与tail引用不一致,表示tail并没有指向最新的尾部节点,更新队列tail指向新的尾节点
                if (p != t) // 同一时刻有两个线程添加了节点
                    casTail(t, newNode);  // CAS更新尾部节点
                return true;
            }
            // Lost CAS race to another thread; re-read next
        }
        else if (p == q)  //p和p.next()都为null,表示队列刚初始化,返回head节点
            p = (t != (t = tail)) ? t : head;
        else
            //其他线程更新了p的next节点,导致P不是尾部节点,更新p指向尾部节点
            p = (p != t && t != (t = tail)) ? t : q;
    }
}

整个入队过程主要做两件事情:一,定位出尾节点。第二是使用CAS算法将入队节点设置成尾节点,如果不成功则重试(循环处理直到成功为止)。

定位尾节点 tail节点并不总是尾节点,每次入队都必须先通过tail节点来找到尾节点。尾节点可能是tail节点也可能是tail节点的next节点

p.casNext(null,n):用于将入队节点设置为当前队列尾节点的next节点,如果q==null则表示当前p节点是尾部节点,否则表示其他线程更新了尾部节点,需要重新获取尾节点

poll()

public E poll() {
    restartFromHead:
    for (;;) {
        for (Node<E> h = head, p = h, q;;) {
            E item = p.item;    //获取p节点元素

            if (item != null && p.casItem(item, null)) { //成功取出节点元素
                // Successful CAS is the linearization point
                // for item to be removed from this queue.
                if (p != h) //同一时刻取出了多个节点,更新head指向新的头节点
                    updateHead(h, ((q = p.next) != null) ? q : p); //将P节点的下一个节点设置成head节点
                return item;
            }
            else if ((q = p.next) == null) { //如果下一个节点为null,说明队列空了
                updateHead(h, p);
                return null;
            }
            else if (p == q)
                continue restartFromHead;
            else         //当前节点没取到(被其他线程获取),则接着获取下一个节点的元素
                p = q;
        }
    }
}
   首先获取头节点元素,如果为null表示另一个线程已经进行了一次出队操作取出了该节点元素(循环接着获取下一个节点的元素),否则通过CAS获取头节点元素,获取成功就返回元素,如果获取不成功就接着获取下一个节点的元素直到获取成功或是队列为null才返回。

其中,当成功获取节点元素后,会判断当前节点是否是头节点引用,如果不是,就会更新头节点引用指向新的头节点。