ConcurrentHashMap源码

文章目录

  • ConcurrentHashMap源码
    • 1. JDK1.7版本
      • 1. 基本结构
      • 2. size操作
      • 3. put操作
      • 3. rehash操作
      • 4. get操作
      • 5. remove操作
    • 2. JDK1.8版本
      • 1. 基本结构
      • 2. put操作
      • 3. treeifyBin操作
      • 4. transfer操作
      • 5. get操作
    • 3. ConcurrentHashMap1.7与1.8的区别
    • 4. 总结

ConcurrentHashMap源码

1. JDK1.7版本

1. 基本结构

ConcurrentHashMap与HashMap在实现上差距不大,最主要的区别在于ConcurrentHashMap采用了分段锁(Segment),每个分段锁维护着几个桶(HashEntry),多个线程可以同时访问不同分段中的桶,从而提高并发度,并发度就是Segment的个数。

segment的定位是按照偏移量来定位的,底层使用了Unsafe类的相关方法。

HashEntry

static final class HashEntry<K,V> {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry<K,V> next;

        HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
}

Segment

static final class Segment<K,V> extends ReentrantLock implements Serializable {

        private static final long serialVersionUID = 2249069246763182397L;

        static final int MAX_SCAN_RETRIES =
            Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;

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

        transient int count;

        transient int modCount;

        transient int threshold;

        final float loadFactor;
}
// 默认的并发级别是16,创建16个segment
static final int DEFAULT_CONCURRENCY_LEVEL = 16;

2. size操作

每个Segment维护了一个count变量来统计该Segment中键值对的个数。

/**
 * The number of elements. Accessed only either within locks
 * or among other volatile reads that maintain visibility.
 */
 transient int count;

在执行size操作的时候,需要遍历所有Segment然后把count累计起来

ConcurrentHashMap在执行size操作时先尝试不加锁,如果连续两次尝试不加锁操作得到的结果一致,就认为这个结果是正确的。否则的话,就需要对每个Segment执行加锁,然后执行size操作。

这里加锁操作主要是因为ConcurrentHashMap是并发操作,你在获取size的同时可能还在插入元素,会导致获取到的结果出现问题。第一种方法是计算前后两次获取到的值是否相等,相等的话,表明没有元素插入,结果准确。否则一旦超过三次表明结果不准备需要给当前的segment加锁执行size操作。

public int size() {
        // Try a few times to get accurate count. On failure due to
        // continuous async changes in table, resort to locking.
        final Segment<K,V>[] segments = this.segments;
        int size;
        boolean overflow; // true if size overflows 32 bits
        long sum;         // sum of modCounts
        long last = 0L;   // previous sum
        int retries = -1; // first iteration isn't retry
        try {
            for (;;) {
                // 如果retries==2,执行++之后为3,此时加锁
                if (retries++ == RETRIES_BEFORE_LOCK) {
                    for (int j = 0; j < segments.length; ++j)
                        ensureSegment(j).lock(); // force creation
                }
                // sum size置0操作,主要是为了验证前后是否等。
                // last是不清零的,所以只要两次对等,表明没有数据插入。
                sum = 0L;
                size = 0;
                overflow = false;
                for (int j = 0; j < segments.length; ++j) {
                    Segment<K,V> seg = segmentAt(segments, j);
                    if (seg != null) {
                        sum += seg.modCount;
                        int c = seg.count;
                        if (c < 0 || (size += c) < 0)
                            overflow = true;
                    }
                } // 表名没有数据插入,直接break,可以返回size
                if (sum == last)
                    break;
                last = sum;
            }
        } finally {
            if (retries > RETRIES_BEFORE_LOCK) {
                for (int j = 0; j < segments.length; ++j)
                    segmentAt(segments, j).unlock();
            }
        }
        return overflow ? Integer.MAX_VALUE : size;
    }

3. put操作

put操作中涉及到两个参数segmentShift\segmentMask

this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;

这两个参数定义在ConcurrentHashMap构造器中:

下面大致构造即使构造出segmentShift的大小以及segmentMask大小。至于初始的c这个主要是用来辅助构建有多少个Segment的。从下面可以看到MIN_SEGMENT_TABLE_CAPACITY=2,这个为2,表示最少每个Segment中的数组需要由两个坑位。至于cap与c之间的关系,可以看到,如果c为3的话此时cap需要从2翻倍到4,ssize是segment的个数。

public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        if (concurrencyLevel > MAX_SEGMENTS)
            concurrencyLevel = MAX_SEGMENTS;
        // Find power-of-two sizes best matching arguments
        int sshift = 0;
        int ssize = 1;
    	// 从这里可以看出ssize=2^sshift次方
        while (ssize < concurrencyLevel) {
            ++sshift;
            ssize <<= 1;
        }// 这里主要是为了获取取多少位的问题!
        this.segmentShift = 32 - sshift;
        this.segmentMask = ssize - 1;
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        int c = initialCapacity / ssize;
        if (c * ssize < initialCapacity)
            ++c;
        int cap = MIN_SEGMENT_TABLE_CAPACITY;
        while (cap < c)
            cap <<= 1;
        // create segments and segments[0] 其余的延迟初始化
        Segment<K,V> s0 =
            new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                             (HashEntry<K,V>[])new HashEntry[cap]);
        Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
        UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
        this.segments = ss;
    }

put操作:

int j = (hash >>> segmentShift) & segmentMask;
// 这一句操作其实看完上面的也很好理解了,为什么需要左移segmentShift位呢
// 因为你的segmentMask是根据ssize-1来的,而根据ssize = 2^sshift可以知道
// sshift比ssize少32-sshift位(segmentShift)

所以的话此时如果左移segmentShift位的话,再与的话正好取到高位相与的值就是下标

public V put(K key, V value) {
        Segment<K,V> s;
        if (value == null)
            throw new NullPointerException();
        int hash = hash(key);
    	// 求下标
        int j = (hash >>> segmentShift) & segmentMask;
    	// 如果偏移量不能定位的话,调用ensureSegment来定位
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            s = ensureSegment(j);
        return s.put(key, hash, value, false);
    }

put真正的实现,实质是在Segment中完成put操作的,因为分段锁的缘故。

put操作主要是因为加了锁!tryLock()方法一旦获取成功则意味着lock(),而scanAndLockForPut明确了最后推出只能lock()之后break退出。无论是否找到,出口只有lock();break;return node;

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    // 尝试是否能够拿到锁,拿到则取null值,否则通过scanAndLockForPut自旋获取node
    HashEntry<K,V> node = tryLock() ? null :
    scanAndLockForPut(key, hash, value);
    V oldValue;
    try {
        // Segment对应的HashEntry
        HashEntry<K,V>[] tab = table;
        // 获取在第几个hashEntry中
        int index = (tab.length - 1) & hash;
        // 获取到tab的第一个值
        HashEntry<K,V> first = entryAt(tab, index);
        for (HashEntry<K,V> e = first;;) {
            if (e != null) {
                K k; // 如果找到了的话,此时替换掉值即可
                if ((k = e.key) == key ||
                    (e.hash == hash && key.equals(k))) {
                    oldValue = e.value; 
                    // 管你有没有,我都直接覆盖
                    if (!onlyIfAbsent) {
                        e.value = value;
                        ++modCount;
                    }
                    break;
                } // 没找到则一直遍历下去
                e = e.next;
            } // 如果遇到了tab中为null的元素。
            else { // 看看node是否为null,不为null的话,表明已经新建了节点
                // 只需要把自己的next设置为first头即可。
                if (node != null) // 因为first==null
                    node.setNext(first);
                else 
               // 相当于直接将node这个节点作为当前HashEntry的第一个节点,下一个节点为null(first)
                    node = new HashEntry<K,V>(hash, key, value, first);
                int c = count + 1; // count是记录元素个数的
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                    rehash(node); // 元素个数过多执行扩容
                else // 一切顺利,将此node放入到当前的HashEntry中
                    setEntryAt(tab, index, node);
                ++modCount; // 修改次数++
                count = c; // 元素个数为c
                oldValue = null; // 老的值记录为null,因为需要返回
                break;
            }
        }
    } finally { // 释放锁
        unlock();
    }
    return oldValue;
}

scanAndLockForPut其中使用自旋获取锁。这一段的逻辑操作为:首先获取当前key-hash=hash(key)对应的节点链的头节点,然后持续遍历该链,如果节点中不存在该节点,则预创建一个新的节点,retries=0,意味着增加1,直到retries操作达到了最大值,进入阻塞等待状态,终止!否则的话在自旋过程中如果发现头节点发生了变化,此时头节点也相应的跟随变化,并将retries置为-1,意味着从头开始。

private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
    HashEntry<K,V> first = entryForHash(this, hash);
    HashEntry<K,V> e = first;
    HashEntry<K,V> node = null;
    int retries = -1; // negative while locating node
    while (!tryLock()) { // 自旋获取锁
        HashEntry<K,V> f; // to recheck first below
        if (retries < 0) {
            if (e == null) { // 预创建节点
                if (node == null) // speculatively create node
                    node = new HashEntry<K,V>(hash, key, value, null);
                retries = 0;
            } // 表示找到了,retries置为0,下一轮必结束,break,返回node即可
            else if (key.equals(e.key))
                retries = 0;
            else
                e = e.next; // 即不为空也不等,证明在其中则一直下去。
        }// 尝试次数如果大于1了 采用阻塞锁获取
        else if (++retries > MAX_SCAN_RETRIES) {
            lock();
            break;
        }
        else if ((retries & 1) == 0 && // 只要链表头不改变,执行下去,否则同步链表头,重新再来
                 (f = entryForHash(this, hash)) != first) {
            e = first = f; // re-traverse if entry changed
            retries = -1;
        }
    }
    return node;
}

3. rehash操作

rehash操作的话是对Segment中存储元素的扩容,也就是对table的扩容。

private void rehash(HashEntry<K,V> node) {
    HashEntry<K,V>[] oldTable = table;
    int oldCapacity = oldTable.length;
    // 直接扩容一倍
    int newCapacity = oldCapacity << 1;
    threshold = (int)(newCapacity * loadFactor);
    // 直接构建新的table,这是为了使得并发get拿到值是没有问题的
    HashEntry<K,V>[] newTable =
        (HashEntry<K,V>[]) new HashEntry[newCapacity];
    // 使用新的掩码
    int sizeMask = newCapacity - 1;
    // 由于是2的倍数,基本上之前的位置上的索引hash值是不会变的
    for (int i = 0; i < oldCapacity ; i++) {
        HashEntry<K,V> e = oldTable[i]; // 取值
        if (e != null) {
            HashEntry<K,V> next = e.next;
            int idx = e.hash & sizeMask; 
            if (next == null)   //  Single node on list
                newTable[idx] = e; // 如果是一个节点的话直接存,这是Doug Lea大神的一个优化点
            else { // Reuse consecutive sequence at same slot
                HashEntry<K,V> lastRun = e;// 如果是多个节点的话
                // 表示在当前槽位上
                int lastIdx = idx;
                // 遍历找到可重用的那个点,一般来说也不会发生变化。除非还有新的操作
                for (HashEntry<K,V> last = next;
                     last != null;
                     last = last.next) {
                    int k = last.hash & sizeMask;
                    if (k != lastIdx) {
                        lastIdx = k;
                        lastRun = last;
                    }
                }// lastRun之后的左右的节点都是可重用的。如果一开始就没变的话那就直接重用
                newTable[lastIdx] = lastRun;
                // Clone remaining nodes 
                for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
                    V v = p.value;
                    int h = p.hash;
                    int k = h & sizeMask;
                    HashEntry<K,V> n = newTable[k];
                    // 保证所有不可重用点都是new出来的,不会对原有结构进行改变
                    newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
                }
            }
        }
    }
    int nodeIndex = node.hash & sizeMask; // add the new node
    node.setNext(newTable[nodeIndex]);
    newTable[nodeIndex] = node;
    table = newTable;
}

4. get操作

get操作比较简单,由于在存入或者put操作的时候都是新创建一个新的表,所以获取只需要定位然后根据key值以及hash值判断即可,相等就取出。

public V get(Object key) {
    Segment<K,V> s; // manually integrate access methods to reduce overhead
    HashEntry<K,V>[] tab;
    int h = hash(key);
    long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
    if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
        (tab = s.table) != null) { // 定位segment,然后获取到HashEntry的位置
        for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
             (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
             e != null; e = e.next) {
            K k;
            if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                return e.value;
        }
    }
    return null;
}

5. remove操作

final V remove(Object key, int hash, Object value) {
    if (!tryLock()) // 加锁失败,则采取自旋获取锁lock()
        scanAndLock(key, hash);
    V oldValue = null; // 移除之后需要返回的值
    try {
        // 获取到当前segment的table
        HashEntry<K,V>[] tab = table;
        // 获取当前删除元素的下标位置
        int index = (tab.length - 1) & hash;
        // 找到当前下标的头节点
        HashEntry<K,V> e = entryAt(tab, index);
        HashEntry<K,V> pred = null;// 记录前一个节点
        while (e != null) {
            K k; // 三个节点 pred 当前节点 next
            HashEntry<K,V> next = e.next;
            if ((k = e.key) == key ||
                (e.hash == hash && key.equals(k))) {
                V v = e.value; // hash与key都相等,取出值value判断是否相等
                if (value == null || value == v || value.equals(v)) {
                    if (pred == null)// 如果pred为null,正好是首节点,就将next设置为首节点
                        setEntryAt(tab, index, next);
                    else // 否则直接跨过当前节点即可
                        pred.setNext(next);
                    ++modCount;
                    --count;
                    oldValue = v;
                }
                break;
            } // 不然大家一起前进啊
            pred = e;
            e = next;
        }
    } finally {
        unlock();
    }
    return oldValue;
}

2. JDK1.8版本

1. 基本结构

ConcurrentHashMap第一个的区别就是在于使用了Node来代替Segment存储数据,使用Node代替了HashEntry,从Node的结构上我们可以看到,现在的粒度更细,之前Segment锁住的是所有的HashEntry[],现在锁住的仅仅是一个Node,粒度更细,并发度更高。

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

    Node(int hash, K key, V val, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.val = val;
        this.next = next;
    }
}

其中几个比较重要的方法:后面用得着

// 获取当前数组以及指定下标的节点的原子操作
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操作,比较并交换,在特定位置设置值
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);
}

其中有个比较中要的参数:

/**
 * 用来控制初始化和扩容的,默认值为0,初始化的时候如果制定了大小,则这个值会保存在sizeCtl中
 * 大小为数组长度的0.75,当为负的时候,说明表正在初始化或在扩张,
 * -1 表示初始化
 * -(1+n):其中n表示正在活动的扩张线程
 */
private transient volatile int sizeCtl;

构造方法:空构造就不看了

// 实例化的时候如果指定了容量的话,此处就将sizeCtl=cap
public ConcurrentHashMap(int initialCapacity) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException();
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
               MAXIMUM_CAPACITY :
               tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
    this.sizeCtl = cap;
}

初始化方法:初始化table,使用sizeCtl中记录的大小。初始化方法主要用于第一次put操作时使用

 private final Node<K,V>[] initTable() {
     Node<K,V>[] tab; int sc;
     while ((tab = table) == null || tab.length == 0) {
         // 小于0的话表示别的线程正在初始化,自己进行线程礼让
         if ((sc = sizeCtl) < 0)
             Thread.yield(); // lost initialization race; just spin
         // 否则的话比较 SIZECTL:表示当前对象的内存偏移量
         // 期望值sc,符合就将sc替换为-1
         else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
             try {
                 if ((tab = table) == null || tab.length == 0) {
                     // 指定了大小的话就按照指定大小来,否则默认16
                     int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                     @SuppressWarnings("unchecked")
                     Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                     table = tab = nt;
                     // n-n/4=3n/4
                     sc = n - (n >>> 2);
                 }
             } finally {
                 // 长度为3/4,即0.75
                 sizeCtl = sc;
             }
             break;
         }
     }
     return tab;
 }

2. put操作

传入key-value,实际调用putVal方法执行

 public V put(K key, V value) {
     // 第三个参数onlyIfAbscent: false的话表示value一定会被设置
     // true:表示value为null时才会被设置
     return putVal(key, value, false);
 }

putVal()方法,从第一句if (key == null || value == null) throw new NullPointerException();可以看出,ConcurrentHashMap键值都不能为空。

总结put操作的过程

首先判断键值是否为null,不为null获取当前key的hash值,获取当前table,如果当前tab没有被初始化,则先调用initTable()方法先初始化表(顺便初始化sizeCtl);已经初始化的话,此时判断当前位置的元素是否为null,是的话,利用CAS操作以key创建新节点Node插入当前位置即可。否则的话,判断当前的hash值是否为-1,看看是否有其他线程在执行扩容的复制过程,是的话自己要去帮忙,然后再回来处理put过程。都不是的话,此时可以知道当前位置是存在元素的,此时利用synchronized关键字加锁,由于允许并发操作,为防止之前取出的头节点发生变化,再次取出比较,如果还相等,证明当前Node没有插入新的元素,否则表明已经插入,直接返回null即可。

如果头节点没有发生变化,判断当前的hash值是否大于0,(-2是树化的hash值),此时就遍历Node,binCount=1,依次累加,如果找到了元素,直接覆盖。如果之前树化了,则利用putTreeVal的方法添加节点。最后判断bitCount的数量是否超过了树化的门限,超过则调用treeifyBin方法判断是扩容还是树化。

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    // 获取key的hash值
    int hash = spread(key.hashCode());
    // 记录当前桶的元素个数(可以理解为坑位)
    int binCount = 0; 
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        // 如果tab没有初始化,则执行initTable()方法初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        // 否则获取到当下下标处的Node节点,如果为null的话
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 通过CAS将当前新创建的节点放入即可
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                // 跳出循环,结束put操作
                break;                   // no lock when adding to empty bin
        } // MOVED:-1 用于转发节点的hash
        // 如果是MOVED状态的话,表示正在进行数组扩张的数据复制阶段。
        // 当前线程也会参与复制,通过允许多线程复制的功能,以此来减少数组的复制所带来的性能损失。
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f); // fh如果在这里没有命中,往后走只有>=0 以及 -2可取
        else {
            // 如果当前位置有元素的话,采用synchronized加锁
            V oldVal = null;
            synchronized (f) {
                // 再次取出值与之前取出的对比
                if (tabAt(tab, i) == f) {
                    // 取出的hash值大于0,如果转化为树则为-2
                    if (fh >= 0) {// 桶个数置为1,表示从1开始
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            // 如果找到了,则直接值覆盖,put成功
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            // 否则将之前的节点定为pred,看看后面是否为null
                            Node<K,V> pred = e;
                            // 是的话,直接创建新的Node节点拼接在pred.next处
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }// 小于0的话。表示已经树化,此时判断是否为树的类型
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        // 调用putTreeVal方法,存入值
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                              value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }// binCount发生变化之后,判断是否大于树化的门限值8,是的话,树化。
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    } // binCount执行+1操作,返回null
    addCount(1L, binCount);
    return null;
}

3. treeifyBin操作

Replaces all linked nodes in bin at given index unless table is too small, in which case resizes instead.

将其中的给定了index的节点替换为树中节点(链表转为树),超过64就要转,除非表太小,这种情况就参与扩容操作。

private final void treeifyBin(Node<K,V>[] tab, int index) {
    Node<K,V> b; int n, sc;
    if (tab != null) {// 如果表不为null且大小小于64,执行扩容操作。
        if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
            tryPresize(n << 1); // 直接扩成2倍即可
        // 否则的话取出当前index下的Node节点并在hash值大于0的情况下
        else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
            synchronized (b) {
                // 使用synchronized加锁,取出再次比较
                if (tabAt(tab, index) == b) {
                    TreeNode<K,V> hd = null, tl = null;
                    for (Node<K,V> e = b; e != null; e = e.next) {
                        TreeNode<K,V> p =
                            new TreeNode<K,V>(e.hash, e.key, e.val,
                                              null, null);
                        // 只有一个节点的话,则head=p,当前节点为头结点
                        if ((p.prev = tl) == null)
                            hd = p;
                        else // 否则将其放在数的最后一个节点next处
                            tl.next = p; 
                        tl = p; // tl变到下面。
                    }// 将当前表放入TreeBin中
                    setTabAt(tab, index, new TreeBin<K,V>(hd));
                }
            }
        }
    }
}

扩容实现方法:tryPresize

private final void tryPresize(int size) {
    // 如果size大于1 << 30的一半,则直接使用最大的。否则的话调用tableSizeFor来扩容
    int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
    tableSizeFor(size + (size >>> 1) + 1); // 无论怎么扩都要是2的整次幂
    int sc;
    // 假如之前是16的容量需要扩到32,那么sizeCtl就需要从12扩到24
    while ((sc = sizeCtl) >= 0) {
        Node<K,V>[] tab = table; int n;
        // 如果没有被初始化,则此时创建一个n大小为sc与c中的更大值的Node[]
        if (tab == null || (n = tab.length) == 0) {
            n = (sc > c) ? sc : c;
            // 比较置为-1.
            if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {// 确认其他线程没有对此表进行修改
                    if (table == tab) {
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = nt;
                        // 完成之后还是设置为3/4大小
                        sc = n - (n >>> 2);
                    }
                } finally {// 3/4大小
                    sizeCtl = sc;
                }
            }
        }// 扩容到容量小于等于sizeCtl大小,或者n大于最大容量,终止扩容
        else if (c <= sc || n >= MAXIMUM_CAPACITY)
            break;
        // 否则获取到tab
        else if (tab == table) {
            int rs = resizeStamp(n);
            // 如果也在初始化扩容的话,则帮助扩容,否则开始新的扩容
            if (sc < 0) {
                Node<K,V>[] nt;
                // 第一个判断是判断右移这么多位之后与之前的n是否是同一个容量下进行扩容
                // 第二个和第三个判断是判断当前扩容数是否到达最大的限制
                // 第四和第五个判断是确保transfer()方法初始化完毕
                // 如果都不符合,直接判定break;退出扩容
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                    transferIndex <= 0)
                    break;
                // 线程数+1,sc在扩容的时候表示transfer的工作线程数
                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);
        }
    }
}

4. transfer操作

这一节的代码解释可以参看:并发编程——ConcurrentHashMap#transfer() 扩容逐行分析

  • 扩容这里Doug Lea有点天秀的感觉。(先Mark,等以后能力足了再来补充!)
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
    // 设定线程处理桶的个数NCPU获取的CPU核心数
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
    if (nextTab == null) {            // initiating
        try {// 初始化 扩容两倍
            @SuppressWarnings("unchecked")
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {      // try to cope with OOME
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        nextTable = nextTab;
        transferIndex = n;
    }
    int nextn = nextTab.length;// 占位节点
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    // 为true表示可以推进
    boolean advance = true;
    // 保证完成状态
    boolean finishing = false; // to ensure sweep before committing nextTab
    // transferIndex为转移时的下标,扩容前的tab的length
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        while (advance) {// 向前推进
            int nextIndex, nextBound;
            if (--i >= bound || finishing)
                advance = false;
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
            else if (U.compareAndSwapInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {
                bound = nextBound;
                i = nextIndex - 1;
                advance = false;
            }
        }
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            if (finishing) {
                nextTable = null;
                table = nextTab;
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;
                finishing = advance = true;
                i = n; // recheck before commit
            }
        }
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
        else if ((fh = f.hash) == MOVED)
            advance = true; // already processed
        else {
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    Node<K,V> ln, hn;
                    if (fh >= 0) {
                        int runBit = fh & n;
                        Node<K,V> lastRun = f;
                        for (Node<K,V> p = f.next; p != null; p = p.next) {
                            int b = p.hash & n;
                            if (b != runBit) {
                                runBit = b;
                                lastRun = p;
                            }
                        }
                        if (runBit == 0) {
                            ln = lastRun;
                            hn = null;
                        }
                        else {
                            hn = lastRun;
                            ln = null;
                        }
                        for (Node<K,V> p = f; p != lastRun; p = p.next) {
                            int ph = p.hash; K pk = p.key; V pv = p.val;
                            if ((ph & n) == 0)
                                ln = new Node<K,V>(ph, pk, pv, ln);
                            else
                                hn = new Node<K,V>(ph, pk, pv, hn);
                        }
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                    else if (f instanceof TreeBin) {
                        TreeBin<K,V> t = (TreeBin<K,V>)f;
                        TreeNode<K,V> lo = null, loTail = null;
                        TreeNode<K,V> hi = null, hiTail = null;
                        int lc = 0, hc = 0;
                        for (Node<K,V> e = t.first; e != null; e = e.next) {
                            int h = e.hash;
                            TreeNode<K,V> p = new TreeNode<K,V>
                                (h, e.key, e.val, null, null);
                            if ((h & n) == 0) {
                                if ((p.prev = loTail) == null)
                                    lo = p;
                                else
                                    loTail.next = p;
                                loTail = p;
                                ++lc;
                            }
                            else {
                                if ((p.prev = hiTail) == null)
                                    hi = p;
                                else
                                    hiTail.next = p;
                                hiTail = p;
                                ++hc;
                            }
                        }
                        ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                        (hc != 0) ? new TreeBin<K,V>(lo) : t;
                        hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                        (lc != 0) ? new TreeBin<K,V>(hi) : t;
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                }
            }
        }
    }
}

5. get操作

get操作和jdk1.7基本一样。

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 &&
        // 拿到当前的Node节点
        (e = tabAt(tab, (n - 1) & h)) != null) {
        // 如果当前key的哈希值与当前获取到的节点哈希值相等
        if ((eh = e.hash) == h) { // 判断值是否相等,相等的话就返回
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        else if (eh < 0) // 如果小于0的话,此时遍历寻找,不存在返回null
            return (p = e.find(h, key)) != null ? p.val : null;
        // 既不等于头结点的hash值,也不小于0,遍历查找
        while ((e = e.next) != null) {
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }// 没找到返回null
    return null;
}

3. ConcurrentHashMap1.7与1.8的区别

  • JDK1.7中采用分段锁来实现并发更新操作,核心类为Segment,它继承自ReentrantLock,并发度为Segment的数量,采用的是数组+链表
  • JDK1.8使用了CAS操作以此支持更高的并发度,在CAS操作失败之后使用内置锁synchronized,JDK1.8的实现在链表过长时也会转化为红黑树。底层是数组+链表+红黑树。

4. 总结

ConcurrentHashMap支持并发操作,从get方法中我们可以看出,没有任何任何的同步机制,所以读操作支持并发操作。

写操作的话一般是put操作,放入元素时发生写的操作,这个时候会进行相应的加锁过程(jdk1.7),在jdk1.8中主要是利用synchronized以及cas+Unsafe底层的操作来实现的同步操作。

多个线程同步处理的过程通过synchronized和unsafe两种方式来实现的。

  • 在取得sizeCtl、Node节点的时候,使用的都是Unsafe方法实现,以达到并发安全的目的。
  • 当需要在某个位置设置节点的时候,一般是值覆盖的时候,会使用synchronized同步机制来锁定该位置的节点
  • 在数组扩容的时候,则通过处理的步长和fwd节点来控制达到并发安全的目的,通过设置hash值为MOVED
  • 当把某个位置的节点复制到扩张后的table的时候,也是通过synchronized同步机制来实现

参考

你可能感兴趣的:(面试复习指南)