ConcurrentHashMap原理解析

JDK1.8

初始化方法

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);
 // tableSizeFor 仍然是保证计算的大小是 2^n, 即 16,32,64 ... 
 int cap = (size >= (long)MAXIMUM_CAPACITY) ?
 MAXIMUM_CAPACITY : tableSizeFor((int)size);
 this.sizeCtl = cap;
}

懒惰初始化,在构造方法中仅仅计算了 table 的大小,以后在第一次使用时才会真正创建

/**
 * Initializes table, using the size recorded in sizeCtl.
 */
private final Node[] initTable() {
    Node[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        // 如果 sizeCtl < 0 ,说明另外的线程执行CAS 成功,正在进行初始化。
        if ((sc = sizeCtl) < 0)
            // 让出 CPU 使用权
            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[] nt = (Node[])new Node[n];
                    table = tab = nt;
                    sc = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

在初始状态下,table为null,sizeCtl为0。当第一个元素被插入时,会根据并发级别(Concurrency Level)计算出数组的长度,并使用CAS操作将数组初始化为对应长度的桶

  • 根据 key 计算出 hashcode 。

  • 判断是否需要进行初始化。

  • 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。

  • 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。

  • 如果都不满足,则利用 synchronized 锁写入数据。

  • 如果数量大于 TREEIFY_THRESHOLD 则要执行树化方法,在 treeifyBin 中会首先判断当前数组长度 ≥64 时才会将链表转换为红黑树

put方法

  • 根据 key 计算出 hashcode 。

  • 判断是否需要进行初始化。

  • 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。

  • 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。

  • 如果都不满足,则利用 synchronized 锁写入数据。

  • 如果数量大于 TREEIFY_THRESHOLD 则要执行树化方法,在 treeifyBin 中会首先判断当前数组长度 ≥64 时才会将链表转换为红黑树。

  • put方法中有扩容方法,需要对 bin 进行 synchronized。当 table.length < 64 时,先尝试扩容,超过 64 时,并且 bin.length > 8 时,会将链表树化,树化过程 会用 synchronized 锁住链表头

Get方法

  1. 根据 hash 值计算位置。
  2. 查找到指定位置,如果头节点就是要找的,直接返回它的 value.
  3. 如果头节点 hash 值小于 0 ,说明正在扩容或者是红黑树,查找之。
  4. 如果是链表,遍历查找之。

Size方法

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;
 // 将 baseCount 计数与所有 cell 计数累加
 long sum = baseCount;
 if (as != null) {
 for (int i = 0; i < as.length; ++i) {
 if ((a = as[i]) != null)
 sum += a.value;
 }
 }
 return sum;
}

元素个数保存在 baseCount 中,并发时的个数变动保存在 CounterCell[] 当中。最后统计数量时累加 即可。

JDK1.7

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

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

static final class Segment extends ReentrantLock implements Serializable {
    static final float LOAD_FACTOR = 0.75f;

    transient volatile HashEntry[] table;
    transient int count;
    transient int modCount;
    transient int threshold;
    final float loadFactor;
}

每个Segment都是一个继承自ReentrantLock的可重入锁,具备独立的线程安全性。table是Segment内部的HashEntry数组,用于存储键值对。count表示当前Segment中的元素数量,modCount用于记录修改次数,threshold表示扩容的阈值,loadFactor表示加载因子。

初始化方法

@SuppressWarnings("unchecked")
public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel) {
    // 参数校验
    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    // 校验并发级别大小,大于 1<<16,重置为 65536
    if (concurrencyLevel > MAX_SEGMENTS)
        concurrencyLevel = MAX_SEGMENTS;
    // Find power-of-two sizes best matching arguments
    // 2的多少次方
    int sshift = 0;
    int ssize = 1;
    // 这个循环可以找到 concurrencyLevel 之上最近的 2的次方值
    while (ssize < concurrencyLevel) {
        ++sshift;
        ssize <<= 1;
    }
    // 记录段偏移量
    this.segmentShift = 32 - sshift;
    // 记录段掩码
    this.segmentMask = ssize - 1;
    // 设置容量
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    // c = 容量 / ssize ,默认 16 / 16 = 1,这里是计算每个 Segment 中的类似于 HashMap 的容量
    int c = initialCapacity / ssize;
    if (c * ssize < initialCapacity)
        ++c;
    int cap = MIN_SEGMENT_TABLE_CAPACITY;
    //Segment 中的类似于 HashMap 的容量至少是2或者2的倍数
    while (cap < c)
        cap <<= 1;
    // create segments and segments[0]
    // 创建 Segment 数组,设置 segments[0]
    Segment s0 = new Segment(loadFactor, (int)(cap * loadFactor),
                         (HashEntry[])new HashEntry[cap]);
    Segment[] ss = (Segment[])new Segment[ssize];
    UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
    this.segments = ss;
}
  1. 必要参数校验。
  2. 校验并发级别 concurrencyLevel 大小,如果大于最大值,重置为最大值。无参构造默认值是 16.
  3. 寻找并发级别 concurrencyLevel 之上最近的 2 的幂次方值,作为初始化容量大小,默认是 16
  4. 记录 segmentShift 偏移量,这个值为【容量 = 2 的 N 次方】中的 N,在后面 Put 时计算位置时会用到。默认是 32 - sshift = 28.
  5. 记录 segmentMask,默认是 ssize - 1 = 16 -1 = 15.
  6. 初始化 segments[0]默认大小为 2负载因子 0.75扩容阀值是 2*0.75=1.5,插入第二个值时才会进行扩容。

put方法

public V put(K key, V value) {
    if (value == null)
        throw new NullPointerException();
    int hash = hash(key.hashCode());
    int segmentIndex = getSegmentIndex(hash);
    return segments[segmentIndex].put(key, hash, value, false);
}

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    lock(); // 获取当前Segment的锁
    try {
        int c = count;
        if (c++ > threshold) // 判断是否需要扩容
            rehash();
        HashEntry[] tab = table;
        int index = hash & (tab.length - 1);
        HashEntry first = tab[index];
        HashEntry e = first;

        while (e != null && (e.hash != hash || !key.equals(e.key))) 
            e = e.next;

        V oldValue;
        if (e != null) { // 键存在,更新值
            oldValue = e.value;
            if (!onlyIfAbsent)
                e.value = value;
        } else { // 键不存在,创建新节点并添加到链表头部
            oldValue = null;
            ++modCount;
            tab[index] = new HashEntry(hash, key, value, first);
            count = c; // 更新元素数量
        }
        return oldValue;
    } finally {
        unlock(); // 释放当前Segment的锁
    }
}

在put操作中,首先通过hash函数计算键的散列值hash,然后根据散列值获取对应的Segment。接着,通过Segment的锁保证了当前操作的线程安全。

在获取到Segment的锁之后,首先判断当前Segment中的元素数量count是否超过了阈值threshold,如果超过了则进行扩容。然后通过散列值和数组长度计算出键对应的索引位置index,并从对应的链表开始遍历,寻找是否存在相同的键。

如果找到了相同的键,则更新对应的值;如果没有找到相同的键,则创建一个新的HashEntry节点,并将其添加到链表的头部。在完成操作后,释放Segment的锁。

get 流程

public V get(Object key) {
 Segment s; // manually integrate access methods to reduce overhead
 HashEntry[] tab;
 int h = hash(key);
 // u 为 segment 对象在数组中的偏移量
 long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
 // s 即为 segment
 if ((s = (Segment)UNSAFE.getObjectVolatile(segments, u)) != null &&
 (tab = s.table) != null) {
 for (HashEntry e = (HashEntry) 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;
}

get 时并未加锁,用了 UNSAFE 方法保证了可见性,扩容过程中,get 先发生就从旧表取内容,get 后发生就从新 表取内容。

  1. 计算得到 key 的存放位置。
  2. 遍历指定位置查找相同 key 的 value 值。

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[] 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 (;;) {
 if (retries++ == RETRIES_BEFORE_LOCK) {
 // 超过重试次数, 需要创建所有 segment 并加锁
 for (int j = 0; j < segments.length; ++j)
 ensureSegment(j).lock(); // force creation
 }
 sum = 0L;
 size = 0;
 overflow = false;
 for (int j = 0; j < segments.length; ++j) {
 Segment seg = segmentAt(segments, j);
 if (seg != null) {
 sum += seg.modCount;
 int c = seg.count;
 if (c < 0 || (size += c) < 0)
 overflow = true;
 }
 }
 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,将所有 segment 锁住,重新计算个数返回

 摘自 黑马程序员,JavaGuide,结合底层源码介绍ConcurrentHashMap如何保证线程安全,佬会爱上这篇文章嘛-CSDN博客

你可能感兴趣的:(Java集合,java,开发语言)