【Java多线程】线程安全的集合

线程安全的集合

Vector

Vector集合是对ArrayList集合线程安全的实现,它们两者在方法的实现上没有什么太大的区别,最大的区别就是,Vector在方法前面加上了synchronized关键字,用于保证线程安全。

Vector存在的问题:

  • 1、它的add()和get()方法都能够获取当前Vector对象的对象锁,但是有可能会发生读读互斥。
  • 2、当threadA在1下标处添加一个元素,threadB在2下标处修改一个元素时,同样有可能会发生互斥现象。
Vector v = new Vector(); 
thread1: v.add(100, 1); 
thread2: v.set(50, 2);

因此,我们可以看出Vector所存在的锁的粒度是非常大的,这也就会导致在多线程情况下,程序执行的效率有可能会十分低下。

HashTable

HashTable集合是对HashMap集合线程安全的实现,它们两者在方法的实现上没有什么太大的区别,最大的区别就是,HashTable在方法前面加上了synchronized关键字,用于保证线程安全。

HashTable存在的问题:

  • 由于HashTable和Vector在本质上都是在方法前面加上synchronized关键字,因此,它们两个存在的问题也是同样相同的,均有可能发生互斥现象。
  • 由此可知,HashTable所存在的锁的粒度也是非常大的,也同样会导致在多线程情况下,程序执行的效率有可能会十分低下。

为了解决Vector集合和HashTable集合效率低下的问题,我们在选取线程安全的集合时一般会选择CopyOnWriteArrayList集合和ConcurrentHashMap集合,它的锁的粒度相较于Vector和HashTable更小,因此能够高效率的解决Vector和HashTable所存在的问题。

ConcurrentHashMap

ConcurrentHashMap是Java中的一个线程安全且高效的HashMap实现。平时涉及高并发如果要用map结构,那第一时间想到的就是它。

我们从以下几个方面来了解一下ConcurrentHashMap:

  • 1、ConcurrentHashMap在JDK8里的结构。
  • 2、ConcurrentHashMap的put方法、szie方法等。
  • 3、ConcurrentHashMap的扩容。
  • 4、HashMap、Hashtable、ConccurentHashMap三者的区别。
  • 5、ConcurrentHashMap在JDK7和JDK8的区别。

相关链接:

ConcurrentHashMap在JDK8里结构

在这里插入图片描述

CurrentHashMap与HashMap的底层结构一致,都是基于数组+链表+红黑树进行实现。

那么它是如何保证线程安全的呢?

  • 答案:其中抛弃了原有JDK1.7的Segment分段锁,而采用了 CAS + synchronized 来保证并发安全性。

现在我们来解决另一个问题,为什么HashMap不是线程安全的。

  • 因为HashMap在多线程环境中很有可能产生死循环(注意不是死锁)。

在连续的put时,会发生扩容,当扩容时就会产生读取节点和移动节点,再次期间,并没有对访问进行一个控制,所以每一次在扩容时遍历的节点,可能完全不相同,那么这样很有可能产生一个 A -> B -> A的情况,这样就导致了死循环。

ConcurrentHashMap的重要变量以及方法

1、table
/**
 * The array of bins. Lazily initialized upon first insertion.
 * Size is always a power of two. Accessed directly by iterators.
 */
transient volatile Node<K,V>[] table;

装载 Node 的数组,作为 ConcurrentHashMap的数据容器,采用懒加载的方式,直到第一次插入数据的时候才会进行初始化操作,数组的大小总是为 2 的幂次方。

2、nextTable
/**
 * The next table to use; non-null only while resizing.
 */
private transient volatile Node<K,V>[] nextTable;

扩容时新生成的数组,大小为原数组的2倍。平时为null,只有在扩容的时候才为非null。

3、sizeCtl
/**
 * Table initialization and resizing control.  When negative, the
 * table is being initialized or resized: -1 for initialization,
 * else -(1 + the number of active resizing threads).  Otherwise,
 * when table is null, holds the initial table size to use upon
 * creation, or 0 for default. After initialization, holds the
 * next element count value upon which to resize the table.
 */
private transient volatile int sizeCtl;

该属性用来控制 table 数组的大小,根据是否初始化和是否正在扩容有几种情况:

  • 当值为负数时:如果为 -1 表示正在初始化 ,如果为 -N 则表示当前正有 N-1 个线程进行扩容操作。
  • 当值为正数时:如果当前数组为 null 的话表示 table 在初始化过程中,sizeCtl 表示为需要新建数组的长度。
  • 若已经初始化了,表示当前数据容器(table 数组)可用容量也可以理解成临界值(插入节点数超过了该临界值就需要扩容),具体指为数组的长度n 乘以 加载因子loadFactor
  • 默认值为0,当table被初始化后,sizeCtl的值为下一次要扩容时元素个数。
sun.misc.Unsafe U

在 ConcurrentHashMapde 的实现中可以看到大量的U.compareAndSwapXXXX 的方法去修改 ConcurrentHashMap 的一些属性。这些方法实际上是利用了 CAS 算法保证了线程安全性,这是一种乐观策略,假设每一次操作都不会产生冲突,当且仅当冲突发生的时候再去尝试。而 CAS 操作依赖于现代处理器指令集,通过底层CMPXCHG指令实现。

CAS(V,O,N)核心思想为:若当前变量实际值 V 与期望的旧值 O 相同,则表明该变量没被其他线程进行修改,因此可以安全的将新值 N 赋值给变量;若当前变量实际值 V 与期望的旧值 O 不相同,则表明该变量已经被其他线程做了处理,此时将新值 N 赋给变量操作就是不安全的,在进行重试。而在大量的同步组件和并发容器的实现中使用 CAS 是通过sun.misc.Unsafe类实现的,该类提供了一些可以直接操控内存和线程的底层操作,可以理解为 java 中的“指针”。该成员变量的获取是在静态代码块中:

static {
    try {
        U = sun.misc.Unsafe.getUnsafe();
        .......
    } catch (Exception e) {
        throw new Error(e);
    }
}
  • 也就是说通过Unsafe获取的值,都是从主内存去获取的。
4、Node

Node 类实现了 Map.Entry 接口,主要存放 key-value 对,并且具有 next 域。

/**
 * Key-value entry.  This class is never exported out as a
 * user-mutable Map.Entry (i.e., one supporting setValue; see
 * MapEntry below), but can be used for read-only traversals used
 * in bulk tasks.  Subclasses of Node with a negative hash field
 * are special, and contain null keys and values (but are never
 * exported).  Otherwise, keys and vals are never null.
 */
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;
    volatile Node<K,V> next;

    ......
}
5、TreeNode

树节点,用于存储红黑树节点,继承于承载数据的 Node 类。而红黑树的操作是针对 TreeBin 类的,从该类的注释也可以看出,也就是TreeBin 会将 TreeNode 进行再一次封装。

/**
 * Nodes for use in TreeBins
 */
static final class TreeNode<K,V> extends Node<K,V> {
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;

    ......
}
6、TreeBin

这个类并不负责包装用户的 key、value 信息,而是包装的很多 TreeNode 节点。实际的 ConcurrentHashMap“数组”中,存放的是 TreeBin 对象,而不是 TreeNode 对象。

/**
 * TreeNodes used at the heads of bins. TreeBins do not hold user
 * keys or values, but instead point to list of TreeNodes and
 * their root. They also maintain a parasitic read-write lock
 * forcing writers (who hold bin lock) to wait for readers (who do
 * not) to complete before tree restructuring operations.
 */
static final class TreeBin<K,V> extends Node<K,V> {
    TreeNode<K,V> root;
    volatile TreeNode<K,V> first;
    volatile Thread waiter;
    volatile int lockState;
    // values for lockState
    static final int WRITER = 1; // set while holding write lock
    static final int WAITER = 2; // set when waiting for write lock
    static final int READER = 4; // increment value for setting read lock

    ......
}
7、ForwardingNode

在扩容时才会出现的特殊节点,其 key,value,hash 全部为 null。并拥有 nextTable 指针引用新的 table 数组。 可以把此节点看成标记节点,如果,table的某一个节点被标记为ForwardingNode,那么此节点正在被一个线程执行扩容操作。

CAS操作:

//该方法用来获取 table 数组中索引为 i 的 Node 元素。
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 操作设置 table 数组中索引为 i 的元素
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);
}

//该方法用来设置 table 数组中索引为 i 的元素,非CAS操作
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
    U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}

ConcurrentHashMap的常用方法剖析

添加方法 put()

首先我们先看一张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) {
    if (key == null || value == null) throw new NullPointerException();
    //1. 计算key的hash值
    //spread(就是扰动函数),让hashcode右移32位进行异或操作,来减少hash冲突
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        //2. 如果当前table还没有初始化先调用initTable方法将tab进行初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        //3. tab中索引为i的位置的元素为null,则直接使用CAS将值插入即可
        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
        }
        //4. 当前正在扩容
        else if ((fh = f.hash) == MOVED)
        //当前线程去辅助扩容。
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    //5. 当前为链表,在链表中插入新的键值对
                    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;
                            }
                        }
                    }
                    // 6.当前为红黑树,将新的键值对插入到红黑树中
                    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;
                        }
                    }
                }
            }
            // 7.插入完键值对后再根据实际大小看是否需要转换成红黑树
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    //8.对当前容量大小进行检查,如果超过了临界值(实际大小*加载因子)就需要扩容 
    addCount(1L, binCount);
    return null;
}
初始化方法 initTable()
private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        if ((sc = sizeCtl) < 0)
            // 1.sizeCtl < 0表示其他线程也正在初始化,
            //保证只有一个线程正在进行初始化操作,所以让出时间片
            Thread.yield(); // lost initialization race; just spin
            //没有其他线程进行操作,那么就直接将sizeCtl置为-1。
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                if ((tab = table) == null || tab.length == 0) {
                    // 2. 得出数组的大小
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    // 3. 这里才真正的初始化数组
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    // 4. 计算数组中可用的大小:实际大小n*0.75(加载因子)
                    sc = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

ConcurrentHashMap的扩容

通过判断该节点的hash值是不是等于-1(MOVED),代码为(fh = f.hash) == MOVED,说明 Map 正在扩容。那么就帮助 Map 进行扩容。以加快速度。

helpTransfer(Node<K,V>[] tab, Node<K,V> f)就是协助扩容的方法。这里我们就能看出ConcurrentHashMap设计的精妙之处了,线程不仅可以进行增删改查,甚至可以去协助扩容,来减少扩容时移动数据的大量操作对阻塞时间的影响。让多个线程一起完成扩容,使得扩容速度非常的快,不仅仅减少了扩容需要的时间,还合理的利用了线程资源。这种想法属实太强了。

首先我们来看一下作为扩容的入口点,也就是什么时候扩容呢?

  • 就是当节点的个数等于 SizeCtl 的时候扩容,扩容依旧是2倍扩容。那么统计节点个数的方法就是扩容方法的入口点。也就是addCount()。
addCount()方法
private final void addCount(long x, int check) {
    CounterCell[] as; long b, s;
    //通过CAS更新baseCount,table的数量,counterCells表示元素个数的变化
    if ((as = counterCells) != null ||
        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        CounterCell a; long v; int m;
        boolean uncontended = true;
        //如果多个线程都在执行,则CAS失败,执行fullAddCount,全部加入count
        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;
        s = sumCount();
    }
     //check>=0表示需要进行扩容操作
    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);
            }
            //当前线程发起操作,nextTable=null
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null);
            s = sumCount();
        }
    }
}

实际上addCount的原理,很简单,统计并更新所有节点个数,更新时使用的是CAS操作。然后进行检查,查看当前是否需要扩容,如果需要扩容,进入transfer()方法中。

transfer()方法
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
 //1. 新建Node数组,容量为之前的两倍
    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;
 //2. 新建forwardingNode引用,在之后会用到
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    boolean advance = true;
    boolean finishing = false; // to ensure sweep before committing nextTab
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        // 3. 确定遍历中的索引i
  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;
            }
        }
  //4.将原数组中的元素复制到新数组中去
  //4.5 for循环退出,扩容结束修改sizeCtl属性
        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
            }
        }
  //4.1 当前数组中第i个元素为null,用CAS设置成特殊节点forwardingNode(可以理解成占位符)
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
  //4.2 如果遍历到ForwardingNode节点  说明这个点已经被处理过了 直接跳过  这里是控制并发扩容的核心
        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) {
      //4.3 处理当前节点为链表的头结点的情况,构造两个链表,一个是原链表  另一个是原链表的反序排列
                        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);
                        }
                       //在nextTable的i位置上插入一个链表
                       setTabAt(nextTab, i, ln);
                       //在nextTable的i+n的位置上插入另一个链表
                       setTabAt(nextTab, i + n, hn);
                       //在table的i位置上插入forwardNode节点  表示已经处理过该节点
                       setTabAt(tab, i, fwd);
                       //设置advance为true 返回到上面的while循环中 就可以执行i--操作
                       advance = true;
                    }
     //4.4 处理当前节点是TreeBin时的情况,操作和上面的类似
                    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;
                    }
                }
            }
        }
    }
}

代码逻辑请看注释,整个扩容操作分为两个部分:

  • 第一部分:构建一个 nextTable,它的容量是原来的两倍,这个操作是单线程完成的。新建 table 数组的代码为:Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1],在原容量大小的基础上右移一位。
  • 第二个部分:就是将原来 table 中的元素复制到 nextTable 中,主要是遍历复制的过程。

根据运算得到当前遍历的数组的位置 i,然后利用 tabAt 方法获得 i 位置的元素再进行判断:

  • 1、如果这个位置为空,就在原 table 中的 i 位置放入 forwardNode 节点,这个也是触发并发扩容的关键点。
  • 2、如果这个位置是 Node 节点(fh>=0),如果它是一个链表的头节点,就构造一个反序链表,把他们分别放在 nextTable 的 i 和 i+n 的位置上。
  • 3、如果这个位置是 TreeBin 节点(fh<0),也做一个反序处理,并且判断是否需要 untreefi,把处理的结果分别放在 nextTable 的 i 和 i+n 的位置上。
  • 4、遍历过所有的节点以后就完成了复制工作,这时让 nextTable 作为新的 table,并且更新 sizeCtl 为新容量的 0.75 倍 ,完成扩容。

设置为新容量的 0.75 倍代码为 sizeCtl = (n << 1) - (n >>> 1),仔细体会下是不是很巧妙,n<<1 相当于 n 左移一位表示 n 的两倍即 2n,n>>>1,n 右移相当于 n 除以 2 即 0.5n,然后两者相减为 2n-0.5n=1.5n,是不是刚好等于新容量的 0.75 倍即 2n*0.75=1.5n。

HashMap、Hashtable、ConccurentHashMap三者的区别

  • HashMap:非线程安全,允许NULL值与NULL键。默认大小为16,扩容为2倍扩容。
  • HashTable:线程安全,不允许NULL值与NULL键,默认大小为11,扩容为2倍+1扩容。HashTable的线程安全实现依靠Synchronized。
  • ConcurrentHashMap:线程安全,不允许NULL值与NULL键,默认大小为16,扩容为2倍扩容。ConcurrentHashMap的线程安全实现依靠于Synchronized + CAS 。

HashMap不应用于并发场景,会产生死循环,HashTable于ConcurrentHashMap运用于并发场景,但是两者有性能差距。当数据量足够大时,我们会发现ConcurrentHashMap的效率实际上比HashTable要低下一些,但是关于读操作,ConcurrentHashMap比HashTable快不止一个量级。

所以适用于什么场景关键还是需要进行压力测试,才可以断言需要用什么样的容器。

  • (Collections.synchronizedMap(new HashMap()); 可以将普通的hashMap变为线程安全的HashMap。)

ConcurrentHashMap在JDK7和JDK8的区别

ConcurrentHashMap在JDK7版本中实现的是Segment分段锁,一个Segment锁上一个或者几个table节点。当要对指定的节点上的数据进行操作时,先获取对应的Segement的锁才可以,而这种锁的粒度相对较大,并且采用 ReetrantLock 的方式去获取与释放锁。

JDK8版本中实现抛弃了原来的Segment分端锁,转而用锁table的节点,也就是锁链表头或者 树的根节点。这种转变直接将锁的粒度变小,使得线程的冲突变少,并且支持多线程协助扩容,使用3个CAS操作来确保 node 的一些操作的原子性,这种方式代替了锁。

  • 9
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值