ConcurrentHashMap原理

多线程高并发编程(10) -- ConcurrentHashMap源码分析   前文讲了HashMap的源码分析,从中可以看到下面的问题:  基于上述问题,都可以使用ConcurrentHashMap进行解决,ConcurrentHashMap使用分段锁技术解决了并发访问效率,在遍历迭代获取时进行修改操作也不会发生并发修改异常等等问题。 initTable:初始化 tabAt:寻找指定数组在内存中i位置的数据 helpTransfer:帮助扩容 transfer:扩容和数据迁移,采用多线程扩容,整个扩容过程,通过cas设置sizeCtl、tr 阅读详情

为什么使用CocurrentHashMap

  1. ConcurrentHashMap解决了在多线程情况下数据存储被覆盖的问题,同时也提高了存储效率。
  2. 相比于HashMap是线程不安全的,在多线程情况下HashMap的put操作变成死循环。
  3. HashTable中使用synchronized来解决多线程情况下的问题,但是随之也导致了效率非常低。

CocurrentHashMap的结构

对于ConcurrentHashMap的结构,jdk1.8多了很多优化,这里我们对1.7和1.8进行一个对比

1.7:

  1. Segments数组 + HashEntry数组 + 链表
  2. 采用分段锁机制
  3. 底层是数组+链表
  4. Segments的意思就是将一个大的table分成多个Segments,每一个Segments就相当于是一个HashMap

1.8:

  1. 取消了Segment
  2. 使用了CAS+synchronized+Node来保证并发安全

CocurrentHashMap初始化

/**
 * 多线程之间,以volatile的方式读取sizeCtl属性,来判断ConcurrentHashMap当前所处的状态。通过cas设置sizeCtl属性,告知其他线程ConcurrentHashMap的状态变更。
 * 不同状态,sizeCtl所代表的含义也有所不同。
 * 未初始化:
 *  sizeCtl=0:表示没有指定初始容量。
 *  sizeCtl>0:表示初始容量。
 * 初始化中:
 *  sizeCtl=-1,标记作用,告知其他线程,正在初始化
 * 正常状态:
 *  sizeCtl=0.75n ,扩容阈值
 * 扩容中:
 *  sizeCtl < 0 : 表示有其他线程正在执行扩容
 *  sizeCtl = (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2 :表示此时只有一个线程在执行扩容
 */
private transient volatile int sizeCtl;

/**
 * 扩容索引,表示已经分配给扩容线程的table数组索引位置。主要用来协调多个线程,并发安全地获取迁移任务(hash桶)。
 * 1 在扩容之前,transferIndex 在数组的最右边 。此时有一个线程发现已经到达扩容阈值,准备开始扩容。
 * 2 扩容线程,在迁移数据之前,首先要将transferIndex左移(以cas的方式修改 transferIndex=transferIndex-stride(要迁移hash桶的个数)),获取迁移任务。
 * 每个扩容线程都会通过for循环+CAS的  方式设置transferIndex,因此可以确保多线程扩容的并发安全。
 */
private transient volatile int transferIndex;



// ConcurrentHashMap的创建有五种方式
// 第一种方式

/**
 * 创建一个空对象,默认初始容量是16
 * Creates a new, empty map with the default initial table size (16).
 */
public ConcurrentHashMap() {
}

// 第二种
/**
 * 创建一个指定初始容量的对象
 * Creates a new, empty map with an initial table size
 * accommodating the specified number of elements without the need
 * to dynamically resize.
 */
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;
}

/**
 * 返回一个最接近指定容量的2的次幂的数
 */
private static final int tableSizeFor(int c) {
    int n = c - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

// 第三种方式
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
    // 设置默认容量
    this.sizeCtl = DEFAULT_CAPACITY;
    // 接受一个map,创建一个新的map
    putAll(m);
}
/**
 * 将传入map中所有的元素循环copy到新的ConcurrentHashMap中
 * @param m mappings to be stored in this map
 */
public void putAll(Map<? extends K, ? extends V> m) {
    // 尝试在插入前扩容
    tryPresize(m.size());
    for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
        putVal(e.getKey(), e.getValue(), false);
}


// 第四种方式

/**
 * 根据指定的初始容量和加载因子创建一个新的map
 */
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
    this(initialCapacity, loadFactor, 1);
}

// 第五种方式
/**
 * 根据指定的初始容量和加载因子和并发级别创建一个新的map
 * 这里想一个问题, concurrencyLevel 是干什么用的,发挥了什么作用,我们后面在分析
 * @param concurrencyLevel the estimated number of concurrently
 * updating threads. The implementation may use this value as
 * a sizing hint.
 */
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;
}

trySize-预扩容

/**
 * Tries to presize table to accommodate the given number of elements.
 *
 * @param size number of elements (doesn't need to be perfectly accurate)
 */
private final void tryPresize(int size) {
    // 如果指定的容量大于等于最大容量的一半则直接设置容量为最大值
    int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
        tableSizeFor(size + (size >>> 1) + 1);
    int sc;
    // 这里sizeCtl默认初始容量16
    while ((sc = sizeCtl) >= 0) {
        Node<K,V>[] tab = table; int n;
        // 当数组为空时
        if (tab == null || (n = tab.length) == 0) {
            // 设置容量,这里sc默认是16,如果传入的对象大小小于16都会设置为16
            n = (sc > c) ? sc : c;
            // SIZECTL = -1 表示加锁
            if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if (table == tab) {
                        // 这里再次判断之后创建一个指定长度的数组并赋个table
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = nt;
                        // 计算下次扩容阈值 n - n / 4 = n * 0.75
                        sc = n - (n >>> 2);
                    }
                } finally {
                    // 将下次扩容阈值赋值给 sizeCtl 
                    sizeCtl = sc;
                }
            }
        }
        else if (c <= sc || n >= MAXIMUM_CAPACITY)
            // 这里说明数组已经初始化过了
            // 如果扩容大小小于等于扩容阈值或者数组长度已经是最大值了,直接结束
            break;
        else if (tab == table) {
            // 这里说明数组还未发生变化
            int rs = resizeStamp(n);
            if (sc < 0) {
                // 表示正在扩容,
                Node<K,V>[] nt;
                 //条件1: true -> 当前线程获取到的扩容唯一标识戳非本次扩容批次,执行方法体
                 //      false -> 当前线程获取到的扩容唯一标识戳是本次扩容批次,进入下一个判断
                 //条件2: jdk1.8有bug 这里应该是 sc == (rs << RESIZE_STAMP_SHIFT) + 1
                 //      true -> 表示所有线程都执行完毕了 线程数量是 = 1+n,执行方法体
                 //      false -> 表示还在扩容中,进入下一个判断
                 //条件3: jdk1.8有bug 这里应该是 sc == (rs << RESIZE_STAMP_SHIFT) + MAX_RESIZERS
                 //      true -> 表示当前参与扩容的线程数量达到最大限度了,不能再参与了,执行方法体
                 //      false -> 表示当前参与扩容的线程数量还未达到最大限度,当前线程可以参与,进入下一个判断
                 //条件4: true -> 表示扩容已完毕,执行方法体
                 //      false -> 表示扩容还在进行,进入下一个判断
                 //条件5: true -> 表示全局范围内的任务已经分配完了,执行方法体
                 //      false -> 表示还有任务可分配,结束判断
                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);
        }
    }
}

put流程

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

/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    // 这一行代码可以看出在ConcurrentHashMap中 key和value都不能为空,这也是跟HashMap的一个区别
    if (key == null || value == null) throw new NullPointerException();
    // 对key值进行二次 hash
    int hash = spread(key.hashCode());
    // binCount 用于链表节点计数,用于后续判断是否转成红黑树
    int binCount = 0;
    // 循环遍历数组,这里使用的是乐观锁的方式
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            // table初始化,初始化完成后进入下一轮循环,后面就不会走到这一步了
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 若果要插入的位置为空,则使用cas的方式插入新元素,防止并发修改,然后跳出循环
            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)
            // 如果要插入的位置不为空,并且节点hash是 MOVE,说明数组正在扩容,调用 helpTransfer 协助扩容并把插入元素插入到新的table中
            tab = helpTransfer(tab, f);
        else {
            // 走到这里说明发生了hash冲突,这里只对要插入的位置中的节点加锁
            V oldVal = null;
            synchronized (f) {
                // 再次判断要插入的位置元素与之前取出来的元素是否一样,防止被修改
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        // 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
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            // 走到这一步说明插入新元素成功,
            if (binCount != 0) {
                // 如果 binCount 大于等于红黑树转换阈值8,并且数组长度达到64,进行转换
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    // 第一个参数表示元素变化值,1表示新增一个元素,-1表示移除一个元素,
    addCount(1L, binCount);
    return null;
}

下面我们来对put方法做一个总结

  1. 在进行插入操作时,首先会进入乐观锁,如果还没初始化就先进行初始化操作
  2. 然后判断要插入的节点是否为空,如果为空则直接通过CAS插入
  3. 如果不为空,先判断数组是否处于扩容状态,如果是正在扩容,帮助扩容
  4. 如果不是扩容状态,找到发生hash冲突的那个节点,循环遍历该节点,然后判断相等则更新,不等则插入

spread-二次hash

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

initTable-数组初始化

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    // 循环检查是否已经初始化
    while ((tab = table) == null || tab.length == 0) {
        // sizeCtl < 0,就是-1表示正在初始化,直接线程阻塞
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // lost initialization race; just spin
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            // 进入该判断说明 sizeCtl >= 0,存在两种情况,一个是未初始化,二是正在扩容sizeCtl为阈值
            // 这里进入初始化代码,现将 SIZECTL 设置为初始化状态
            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;
                    // sc表示扩容阈值
                    sc = n - (n >>> 2);
                }
            } finally {
                // 初始化完成后将阈值赋给 sizeCtl
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

下面我们来分析以下初始化的步骤:

  1. 判断哈希表是否初始化
  2. 如果未初始化,判断sizeCtl的值如果sizeCtl
  3. 如果sizeCtl >= 0,则有两种可能,一是哈希表未初始化;二是有其他线程在已经初始化完成,此时sizeCtl记录的是扩容阈值
  4. 使用CAS将 SIZECTL 值修改为-1。如果修改成功,开始初始化操作
  5. 如果修改失败,进入下一轮循环判断是否初始化
  6. 修改-1成功后再次判断是否初始化,这里为什么要采用双重校验呢?因为在并发情况下,如果其他线程已经完成了初始化,sizeCtl也是 >=0,也会进入这个判断
  7. 如果还未初始化,则开始初始化
  8. 如果顺利初始化完成,sizeCtl记录扩容阈值。

 helpTransfer-协助扩容

/**
 * Helps transfer if a resize is in progress.
 */
 https://blog.youkuaiyun.com/weixin_41392674/article/details/126250297
 https://blog.youkuaiyun.com/u010285974/article/details/106301101/
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
    Node<K,V>[] nextTab; int sc;
    // 前面我们已经知道Node是链表节点,TreeNode是红黑树节点,那么这里的 ForwardingNode 代表说明呢?
    if (tab != null && (f instanceof ForwardingNode) &&
        (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
        int rs = resizeStamp(tab.length);
        while (nextTab == nextTable && table == tab &&
               (sc = sizeCtl) < 0) {
            if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                sc == rs + MAX_RESIZERS || transferIndex <= 0)
                break;
            if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                transfer(tab, nextTab);
                break;
            }
        }
        return nextTab;
    }
    return table;
}


/**
 * A node inserted at head of bins during transfer operations.
 */
static final class ForwardingNode<K,V> extends Node<K,V> {
    final Node<K,V>[] nextTable;
    ForwardingNode(Node<K,V>[] tab) {
        super(MOVED, null, null, null);
        this.nextTable = tab;
    }

    Node<K,V> find(int h, Object k) {
        // loop to avoid arbitrarily deep recursion on forwarding nodes
        outer: for (Node<K,V>[] tab = nextTable;;) {
            Node<K,V> e; int n;
            if (k == null || tab == null || (n = tab.length) == 0 ||
                (e = tabAt(tab, (n - 1) & h)) == null)
                return null;
            for (;;) {
                int eh; K ek;
                if ((eh = e.hash) == h &&
                    ((ek = e.key) == k || (ek != null && k.equals(ek))))
                    return e;
                if (eh < 0) {
                    if (e instanceof ForwardingNode) {
                        tab = ((ForwardingNode<K,V>)e).nextTable;
                        continue outer;
                    }
                    else
                        return e.find(h, k);
                }
                if ((e = e.next) == null)
                    return null;
            }
        }
    }
}

treeifyBin-红黑树转换

// 从put流程中的这段代码我们可以看出当节点链表长度大于等于 8 是会调用红黑树扩容方法 treeifyBin
if (binCount >= TREEIFY_THRESHOLD)
    treeifyBin(tab, i);

/**
 * Replaces all linked nodes in bin at given index unless table is
 * too small, in which case resizes instead.
 */
private final void treeifyBin(Node<K,V>[] tab, int index) {
    Node<K,V> b; int n, sc;
    if (tab != null) {
        // 这里第一步会再判断数组的长度是否大于等于 64 ,如果小于64还是会进行扩容,扩容流程前面我们已经分析过了
        if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
            tryPresize(n << 1);
        else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
            // 走到这里说明数组长度 >= 64 并且链表长度 >= 8
            synchronized (b) {
                // 加锁
                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);
                        if ((p.prev = tl) == null)
                            hd = p;
                        else
                            tl.next = p;
                        tl = p;
                    }
                    // 将红黑树结构元素设置到数组中
                    setTabAt(tab, index, new TreeBin<K,V>(hd));
                }
            }
        }
    }
}

addCount-计数扩容

/**
 * Adds to count, and if table is too small and not already
 * resizing, initiates transfer. If already resizing, helps
 * perform transfer if work is available.  Rechecks occupancy
 * after a transfer to see if another resize is already needed
 * because resizings are lagging additions.
 *
 * @param x the count to add
 * @param check if <0, don't check resize, if <= 1 only check if uncontended
 */
 // 该方法主要有两个作用,1:计数;2:判断是否扩容
 // x : 1表示新增元素,-1表示移除元素
 // check:正常是数组某个hash槽中链表或红黑树元素个数
private final void addCount(long x, int check) {
    CounterCell[] as; long b, s;
    // 第一次执行时 as = counterCells == null
    /* U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x) ,baseCount表示元素个数
     * 1. 在没有竞争的情况下写入成功,第一个if判断返回false
     * 2. 在多线程情况下,如果写入失败,进入第一个if判断
     */ 
    if ((as = counterCells) != null ||
        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        CounterCell a; long v; int m;
        boolean uncontended = true;
        // 1. 如果as == null,说明多线程竞争失败,counterCells 初始化
        // 2. 如果as != null,并且当前线程对应as数组存储对象不为空,直接使用as存储值 +x
        // 3. 如果as != null,当前线程对应as数组中没有数据,调用 fullAddCount 插入
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
            !(uncontended =
              U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
            fullAddCount(x, uncontended);
            return;
        }
        // 表示不需要检查扩容,直接返回
        if (check <= 1)
            return;
        // 返回当前map中数组个数
        s = sumCount();
    }
    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();
        }
    }
}

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) {
        // 如果数组非空,元素并且存在该key,开始查询
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
            // 如果正好在数组内,找到一样的key,hash则直接返回
                return e.val;
        }
        else if (eh < 0)
            // hash<0 这里有两种情况
            // static final int MOVED     = -1; // hash for forwarding nodes
            // static final int TREEBIN   = -2; // hash for roots of trees
            // 当 eh = -1 时,说明数组正在扩容,e 是ForwardingNode
            // 当 eh = -2 时,说明数组中节点是红黑树结构, e 是TreeBin
            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方法中没有加锁的操作,这是为什么呢?

ConcurrentHashMap 读操作为什么不用加锁

1.7:

ConcurrentHashMap 内部采用了一种分段锁的机制,即将整个哈希表分成多个 Segment,每个 Segment 都是一个独立的哈希表,每个 Segment 内部的操作都是线程安全的。当多个线程同时访问 ConcurrentHashMap 时,每个线程会被分配到不同的 Segment,从而避免了锁的竞争。这样就可以在不影响读操作的情况下,同时支持多个线程的并发写操作。

1.8:

ConcurrentHashMap的get操作并没有像put操作一样有CAS和synchronized锁。get操作不需要加锁,因为 Node 的元素 value 和指针 next 是用 volatile 修饰的,所以在多线程的环境下,即便value的值被修改了,在线程之间也是可见的。

JDK1.8中为什么使用synchronized替换可重入锁ReentrantLock

// 在JDK1.7中,就是通过锁定Segment对象实现线程安全的
static class Segment<K,V> extends ReentrantLock implements Serializable {
    private static final long serialVersionUID = 2249069246763182397L;
    final float loadFactor;
    Segment(float lf) { this.loadFactor = lf; }
}
// 从这段代码可以看出 Segment 继承了ReentrantLock 类,说明 Segment 是一个可重入锁

下面我们来分析一下为什么要用 synchronized 代替 ReentrantLock

  1. Segemnt 其实锁住的就是一个 HashEntry 数组,而 synchronized 锁的是发生hash冲突的头节点或者红黑树的节点提供了性能
  2. 从jdk1.6开始,对synchronized进行了很多优化,对其增加了状态转换,锁会从无锁->偏向锁->轻量级锁->重量级锁一步步升级,只要线程可以通过自旋拿到锁就不会升级为重量级锁,从而线程不会被挂起减少了线程的切换消耗,ReentrantLock并没有自旋的功能

ConcurrentHashMap迭代器是强一致性还是弱一致性?HashMap呢?

ConcurrentHashMap的迭代器创建后,就会按照哈希表结构遍历每个元素,但在遍历过程中,内部元素可能会发生变化,如果变化发生在已遍历的部分,迭代器就不会反映出来,而如果变化发生在未遍历过的部分,迭代器就会发现并反映出来,这就是弱一致性。

HashMap迭代器是强一致性

扩容期间在未迁移到的hash桶插入数据会发生什么

只要插入的位置扩容线程还未迁移到,就可以直接插入;

如果当前要插入的位置正在迁移,会帮助其他扩容线程一起扩容直到扩容结束,然后再加synchronized锁执行插入操作。

正在迁移的hash桶遇到get操作会发生什么

因为ln和hn是在原本链表的基础上复制出来的,复制引用。

原链表并不会受到影响,可以正常的访问。

如果数据迁移结束,会在对应的哈希槽上放一个fwd,那么之后的get请求,就会将请求转发到扩容后的数组中。

正在迁移的hash桶遇到put/remove操作会发生什么

协助扩容

ConcurrentHashMap 是如何发现当前槽点正在扩容的

ConcurrentHashMap 新增了一个节点类型,叫做转移节点 ForwardingNode,当我们发现当前槽点是转移节点时(转移节点的 hash 值是 -1),即表示 Map 正在进行扩容。

// remove 方法  
final V replaceNode(Object key, V value, Object cv) {
        int hash = spread(key.hashCode());
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0 ||
                (f = tabAt(tab, i = (n - 1) & hash)) == null)
                break;
            else if ((fh = f.hash) == MOVED)
                // MOVED = -1
                tab = helpTransfer(tab, f);
            else {
            ......
            }
 }

// 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);
            else {
            ......
            }
    }

为什么超过冲突超过8才将链表转为红黑树而不直接用红黑树

因为红黑树需要进行左旋,右旋操作, 而单链表不需要

以下都是单链表与红黑树结构对比。

如果元素小于8个,查询成本高,新增成本低

如果元素大于8个,查询成本低,新增成本高

因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要。链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。 还有选择6和8,中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。

时间和空间的权衡

时间:因为Map中桶的元素初始化是链表保存的,其查找性能是O(n),而树结构能将查找性能提升到O(log(n))。当链表长度很小的时候,即使遍历,速度也非常快,但是当链表长度不断变长,肯定会对查询性能有一定的影响,所以才需要转成树。

空间:因为红黑树需要进行左旋,右旋操作, 而单链表不需要,TreeNodes占用空间是普通Nodes的两倍,所以只有当bin包含足够多的节点时才会转成TreeNodes,当bin中节点数变少时,又会转成普通的bin。并且我们查看源码的时候发现,链表长度达到8就转成红黑树,当长度降到6就转成普通bin。

描述一下 CAS 算法在 ConcurrentHashMap 中的应用

CAS 其实是一种乐观锁,一般有三个值,分别为:赋值对象,原值,新值,在执行的时候,会先判断内存中的值是否和原值相等,相等的话把新值赋值给对象,否则赋值失败,整个过程都是原子性操作,没有线程安全问题。

 putVal() 方法中,有使用到 CAS ,是结合无限 for 循环一起使用的,步骤如下:

  1. 计算出数组索引下标,拿出下标对应的原值;
  2. CAS 覆盖当前下标的值,赋值时,如果发现内存值和 1 拿出来的原值相等,执行赋值,退出循环,否则不赋值,转到 3;
  3. 进行下一次 for 循环,重复执行 1,2,直到成功为止。

可以看到这样做的好处,第一是不会盲目的覆盖原值,第二是一定可以赋值成功。

ConcurrentHashMap 和 Hashtable 的区别

1.底层数据结构:

JDK1.7的ConcurrentHashMap底层采用:Segments数组+HashEntry数组+链表

JDK1.8的ConcurrentHashMap底层采用:Node数据+链表+红黑树

Hashtable底层数据结构采用:数组+链表

2.实现线程安全的方式:

在JDK1.7中ConcurrentHashMap采用分段锁实现线程安全。

在JDK1.8中ConcurrentHashMap采用synchronized和CAS来实现线程安全。

Hashtable采用synchronized来实现线程安全。在方法上加synchronized同步锁。

ConcurrentHashMap 和 HashMap 的区别

HashMap是非线程安全的,这意味着不应该在多线程中对这些Map进行修改操作,否则会产生数据不 一致的问题,甚至还会因为并发插入元素而导致链表成环,这样在查找时就会发生死循环,影响到整个应用程序。

Collections工具类可以将一个Map转换成线程安全的实现,其实也就是通过一个包装类,然后把所有功能都委托给传入的Map,而包装类是基于synchronized关键字来保证线程安全的(Hashtable也是基于synchronized关键字),底层使用的是互斥锁,性能与吞吐量比较低。

ConcurrentHashMap的实现细节远没有这么简单,因此性能也要高上许多。

它没有使用一个全局锁来锁住自己,而是采用了减少锁粒度的方法,尽量减少因为竞争锁而导致的阻塞与冲突,而且ConcurrentHashMap的检索操作是不需要锁的。

Map集合之ConcurrentHashMap ConcurrentHashMap 与 HashTable 与 HashMap 的比较 HashMap 在并发环境下,可能会形成环状链表(扩容时可能造成),导致 get 操作时,cpu 空转,所以,在并发环境中使用 HashMap 是非常危险的。并且它是线程不安全的 HashTable HashTable 是线程安全的。HashTable 线程安全的策略实现代价却比较大,get/put 所有相关操作都是 synchronized 的,这相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作 阅读详情

相关推荐

ConcurrentHashMap 原理

一 概述 我们都知道 HashMap 不是线程安全的,所以在处理并发的时候会出现问题。 而 HashTable 虽然是线程安全的,但是是通过整个方法来加锁的方式,当一个线程在写操作的时候,另外的线程则不能进行读写。 而 ConcurrentHashMap 则可以支持并发的读写。跟 1.7 版本相比,1.8 版本又有了很大的变化,已经抛弃了 Segment 的概念,虽然源码里面还保留了,也只是为了兼容性的考虑。 二 ConcurrentHashMap 原理 在 ConcurrentHashMap 中通过一个

liuwg1226的专栏 5077

ConcurrentHashMap原理详解(太细了)

和HashMap一样,是一个存放键值对的容器。使用hash算法来获取值的地址,因此时间复杂度是O(1)。查询非常快。同时,是线程安全的HashMap。专门用于多线程环境。做插入操作时,首先进入乐观锁,然后,在乐观锁中判断容器是否初始化,如果没初始化则初始化容器,如果已经初始化,则判断该hash位置的节点是否为空,如果为空,则通过CAS操作进行插入。如果该节点不为空,再判断容器是否在扩容中,如果在扩容,则帮助其扩容。如果没有扩容,则进行最后一步,先加锁,然后找到hash。......

qq_42068856的博客 7万+

JDK并发集合之CocurrentHashMap

JDK并发集合之CoucurrentHashMap

weixin_47742705的博客 455

一文讲清楚ConcurrentHashMap原理,值得收藏。

ConcurrentHashMap原理是面试的一个高频知识点,这属于第一阶段的过招。 如果你答上来了,之后面试官可能会顺势追问一下,如何提高 ConcurrentHashMap 的插入效率?于是一个接着一个,连环问就开始了,直到探到你的老底才罢休。 今天我们用一篇文章把ConcurrentHashMap讲清楚,帮你在面试时不慌不忙,从容应对。 1、ConcurrentHashMap 原理概述 ConcurrentHashMap 是一个存储 key/value 对的容器,并且是线程安全的。我们先看 Concu

mukewangguanfang的博客 1648

告别密码明文传输:手把手带你用Python实现SRP-6协议(附完整代码)

本文详细介绍了如何使用Python实现SRP-6协议,确保密码安全传输和存储。通过零知识证明与密钥交换技术,SRP协议在不传输明文密码的情况下完成认证,适用于金融App等高安全需求场景。文章包含完整代码示例和实战技巧,帮助开发者规避常见安全陷阱。

weixin_30689307的博客 412

实验四 基于IDEA+SpringBoot+Maven+Thymeleaf的系统实现实验目的

通过这次实验,我学习了如何成功进行IDEA+SpringBoot+maven的系统实现,搭建好IDEA+SpringBoot+maven的系统环境,成功地安装并配置了maven的运行环境,学会了创建springboot的三种方式,并且能将之前的实验结合起来运用。pom.xml中引入thymeleaf依赖,在classpath:/templates/下新建HTML页面(可采用实验一创建的页面),新建controller进行测试,访问页面后返回success成功数据。同时掌握和thymeleaf的基本语法。

m0_73502929的博客 1379

ConcurrentHashMap原理图

Cain的博客 278

【CAN通讯系列18】- NM在干什么?休眠唤醒

同时针对不同休眠或唤醒场景,网管报文的userdata有所区别,如何做到场景与网管报文内容一一映射,通常会定义另一帧报文,用来表明当前是哪个或哪几个唤醒条件或休眠条件满足。又或者如果用户发现汽车电量不充足了,那么会有充电需求,插枪充电,这时如果插上慢充枪,那么OBC会先被唤醒,识别出插枪行为及其慢充枪的连接状态,然后OBC唤醒BMS和其他相关的控制器进行充电。也就是说,通过对一个又一个整车功能和应用场景的详细分析,最终能够汇总出整车所有的休眠唤醒场景需求,通常会用一个表格来管理,俗称休眠唤醒表。

menghuyouyou的博客 1167

ConcurrentHashMap原理详解

做插入操作时,首先进入乐观锁然后,在乐观锁中判断容器是否初始化,如果没初始化则初始化容器,如果已经初始化,则判断该hash位置的节点是否为空,如果为空,则通过CAS操作进行插入。如果该节点不为空,再判断容器是否在扩容中,如果在扩容,则帮助其扩容。如果没有扩容,则进行最后一步,先加锁,然后找到hash值相同的那个节点(hash冲突)循环判断这个节点上的链表,决定做覆盖操作还是插入操作。循环结束,插入完毕。

ARUNNE的博客 1963

手把手教你用运放和基准源,把±10V信号变成ADC能吃的0-4V(附完整计算过程)

本文详细介绍了如何利用OP07运放和REF5040基准源设计工业级信号调理电路,将±10V传感器信号转换为0-4V ADC可接受范围。通过七步流程,包括需求分析、元器件选型、电路设计、理论计算、PCB布局、实测验证与性能优化,帮助工程师解决信号不匹配问题,提升系统精度与稳定性。

weixin_28731223的博客 467

零件边界线提取处理原理详解

该程序提出了一种高效的点云边界提取方法,通过三阶段处理流程实现零件几何特征的精确提取。首先采用RANSAC算法进行坐标系标准化,包括主平面检测、点云分割和双重旋转对齐。接着使用凹包算法和RANSAC直线拟合提取底面边界。最后通过多方向密度切片分析(Z向100片、X向50片),自动选择高密度切片进行边界提取和直线拟合。算法具有鲁棒性强、处理效率高(支持OpenMP并行计算)和参数可配置等特点,能有效提取零件的几何边界特征,为后续质量检测提供基础。实验结果显示该方法能准确提取零件的多方向边界线。

weixin_45196569的博客 432

ConcurrentHashMap实现原理

哈希表是一种key-value存储数据的结构,根据key即可查到对应的value。如果所有的键是整数,我们可用简单的无序数组来表示,键作为数组索引,值即为对应的值数据结构JDK7采用数组+Segment的段锁的数据结构,JDK8采用数组+链表+红黑树的数据结构线程安全机制JDK7采用Segment分段锁实现线程安全,其中Segment继承ReentrantLock. JDK8采用CAS+synchronized保证线程安全锁的粒度。

算法小生 3435

【制作100个unity游戏之22】3DRPG游戏开发07——其他详细的设计

我们会有有很多单例,如果每个单例都去写会很麻烦,所以我们定义泛型单例模式 Singleton,我们将所有的单例都继承这个泛型单例,就省略很多重复的工作定义一个GameManager类,继承Singleton,控制整个游戏的进度,比如如果主角死了,广播,游戏结束调用。

向宇的博客,专注php/web全栈 unity游戏开发,欢迎大家评论纠错 2577

ConcurrentHashMap的原理

目录 一、ConcurrentHashMap与HashMap等的区别 二、JDK1.7版本的CurrentHashMap的实现原理 1.Segment(分段锁) 2.内部结构 3.该结构的优劣势 三、JDK1.8版本的CurrentHashMap的实现原理 get操作 put操作 由于ConcurrentHashMap 的实现在JDK1.7 和 JDK1.8 有较大不同,因此会分开两个版本讲解。 一、ConcurrentHashMap与HashMap等的区别 1.HashMap..

ZJE 837

NOC总线架构拓扑介绍

在开篇里,我们对芯片PPA有了初步的认识。下面,让我们从访存这个简单的问题开始展开介绍芯片基础概念。CPU是怎样访问内存的?简单的答案是,CPU执行一条访存指令,把读写请求发往内存管理单元。内存管理单元进行虚实转换,把命令发往总线。总线把命令传递给内存控制器,内存控制器再次翻译地址,对相应内存颗粒进行存取。之后,读取的数据或者写入确认按照原路返回。再复杂些,当中插入多级缓存,在每一层缓存都未命中的情况下,访问才会最终达到内存颗粒。

元直的博客 5293

【麒麟桌面系统】企业微信提示当前副设备的企业微信版本较低

银河麒麟系统中登录企业微信时出现"副设备版本较低"的报错,可通过两种方案解决:1)更新最新版企业微信,需替换软件源并刷新软件商店安装8.0-50-4.1.26.6019kylin5版本;2)安装内测5.0版本企业微信,需下载deb包后使用sudo dpkg命令安装。文章还提供了常见问题解决表和长期维护建议,包括系统更新检查、软件源验证等维护项目及具体操作指南。

信创运维工程师 · 银河麒麟 & 容器实战 | 金融生产环境迁移·等保加固 | 142篇原创·17万+阅读 2268

ConcurrentHashMap底层结构和原理详解_concurrenthashmap底层原理

ConcurrentHashMap的get操作跟HashMap类似,只是ConcurrentHashMap第一次需要经过一次hash定位到Segment的位置,然后再hash定位到指定的HashEntry,遍历该HashEntry下的链表进行对比,成功就返回,不成功就返回null。1.将key传入put方法中,先根据key的hashcode的值找到对应的segment段2.再根据segment中的put方法,加锁lock()。3.再次hash确定存放的hashEntry数组中的位置。

2401_86394084的博客 1067

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

ConcurrentHashMap底层原理

分段锁(Segment)机制ConcurrentHashMap 在 JDK 1.7 及以前采用了分段锁的设计。它将整个哈希表分成多个段(Segment),每个段实际上是一个独立的哈希表,并且都有自己的锁。例如,默认情况下会有 16 个段。当对 ConcurrentHashMap 进行操作时,如put或get操作,首先会根据键(key)的哈希值确定它属于哪个段,然后只对这个段进行加锁操作,而不是对整个哈希表加锁。这样就允许多个线程同时访问不同段的数据,大大提高了并发性能。以putHashMap。

weixin_62941961的博客 1129

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

ConcurrentHashMap的原理分析

链式hash表 链式哈希表从根本上说是由一组链表构成。每个链表都可以看做是一个“桶”,我们将所有的元素通过散列的方式放到具体的不同的桶中。插入元素时,首先将其键传入一个哈希函数(该过程称为哈希键),函数通过散列的方式告知元素属于哪个“桶”,然后在相应的链表头插入元素。查找或删除元素时,用同样的方式先找到元素的“桶”,然后遍历相应的链表,直到发现我们想要的元素。因为每个“桶”都是一个链表,所以链式哈希表并不限制包含元素的个数。然而,如果表变得太大,它的性能将会降低。 ...

yzpbright的博客 2225

ConcurrentHashMap 底层原理

这篇文章,我打算从以下几个方面来讲。 1)多线程下的 HashMap 有什么问题? 2)怎样保证线程安全,为什么选用 ConcurrentHashMap? 3)ConcurrentHashMap 1.7 源码解析 底层存储结构 常用变量 构造函数 put() 方法 ensureSegment() 方法 scanAndLockForPut() 方法 rehash() 扩容机制 get() 获取元素方法 remove()

阳阳的博客 2801

【Java 基础】ConcurrentHashMap 底层原理

ConcurrentHashMap 是线程安全的哈希表,它是 Java 并发包中提供的一种高效的并发 Map 实现。ConcurrentHashMap 底层采用了分段锁的机制,不同的段(Segment)可以被不同的线程同时访问,从而提高了并发性能。

激流丶的博客 5639
上一篇: HashMap原理
下一篇: 1、Mysql架构与历史
geek-wmy
博客等级 码龄9年 322粉丝 126原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值