JDK源码阅读(十一) : 并发容器——ConcurrentHashMap

1. Why ConcurrentHashMap?

在java1.7版本,数组中发生冲突的节点以链表形式相连,在进行HashMap在执行put方法时,通过头插法插入到单链表中。然而put过程中可能会触发扩容操作,此时会将原数组的内容重新散列(rehash)到新的数组中。在多线程环境下,如果多个线程同时执行put方法,可能会使数组某一位置上的链表形成闭环,继而出现死循环,cpu飙升到100%。所以java1.7中的hashmap不是线程安全的。

而同步容器HashTable虽然是线程安全的,但是锁的粒度太大,仅仅在与写操作相关的方法上加了synchronized关键字,那么所有涉及到写操作的线程将竞争同一把锁,串行度太高,效率很低。

很明显,HashTable的优化方案应该在于降低锁的粒度。ConcurrentHashMap在java1.7和java1.8对此作出了改进。

接下来,较为粗糙地阅读了ConcurrentHashMap的源码实现。

2. Java1.7版本的ConcurrentHashMap

在java1.7中,ConcurrentHashMap采用分段锁技术,将数据分成一段一段地存储,给每一段配上一把锁。当一个线程访问某段数据时,仅占用了该段的锁,不影响其他线程访问其他段中的数据。

ConcurrentHashMap是由一个Segment数组和多个HashEntry数组组成。每一个Segment元素都指向一个HashEntry数组。当对某个Segment下属的HashEntry数组中的元素执行写操作时,首先需要获取对应的Segment锁。可以说,一个Segment元素守护着一个HashEntry数组

此外,ConcurrentHashMap和HashTable一样,key或value不可以存储null,而HashMap是可以存储null。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EHVIxgIU-1609146049951)(/Users/phil/Documents/blog/ConcurrentHashMap.png)]

(1) put方法

put方法首先通过一次hash定位到Segment的位置,然后通过二次hash,在该Segment处找到相应的HashEntry的位置。由于Segment类继承了ReentrantLock,所以带有了锁功能。在插入到链表中的时候,需要首先尝试获取锁。

(2) get方法

get方法首先通过一次hash定位到Segment的位置,然后通过二次hash,找到HashEntry的位置。然后遍历该位置上的链表。

(3)size方法

由于多线程环境下,不能够直接将每个Segment中的元素数count相加而得到总的元素数,因为count在不停地发生变化。如果在统计size时,将所有的写操作都锁住,效率又很低下。

这里采用的方案是,首先尝试多次不加锁地统计每个Segment的元素数,然后比较前后2次统计过程中容器是否发生了变化(通过modCount比较),如果没有变化,就认为期间没有元素加入,直接返回结果;如果发生了变化,就采用加锁的方式统计所有Segment的元素数。

3. Java1.8版本的ConcurrentHashMap

java1.8对于ConcurrentHashMap的设计作出了较大改变,不再使用Segment数据结构,而是使用了与java1.8的HashMap相同的数据结构,Node数组+链表(或红黑树),使用Synchronized和CAS进行并发控制。

3.1 常量

最大容量为2的30次幂

private static final int MAXIMUM_CAPACITY = 1 << 30;

默认容量为16,必须是2的整数次幂。

private static final int DEFAULT_CAPACITY = 16;

最大数组长度

static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

默认并发级别,只是兼容以前版本,java1.8不使用

private static final int DEFAULT_CONCURRENCY_LEVEL = 16;

默认负载因子

private static final float LOAD_FACTOR = 0.75f;

树化的阈值,即每个槽上的节点数大于8时,链表转换为红黑树

static final int TREEIFY_THRESHOLD = 8;

去树化的阈值,小于等于6时,红黑树退化为链表

static final int UNTREEIFY_THRESHOLD = 6;

树化的最小容量,即当数组长度大于等于64时,链表转换为红黑树

static final int MIN_TREEIFY_CAPACITY = 64;

用于扩容时的标记位数

private static int RESIZE_STAMP_BITS = 16;

MAX_RESIZERS辅助扩容的最大线程数

private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;

sizeCtl中记录size大小的偏移量32-16=16

private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;

forwardingNodes的hash值,为负数

static final int MOVED     = -1; // hash for forwarding nodes

树根节点的hash值,为负数

static final int TREEBIN   = -2; // hash for roots of trees

ReservationNode的hash值

static final int RESERVED  = -3; // hash for transient reservations

可用的处理器数量

static final int NCPU = Runtime.getRuntime().availableProcessors();

3.2 属性

链表节点结构,只能遍历,不能修改节点的value。

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;
    }

    public final K getKey()       { return key; }
    public final V getValue()     { return val; }
    public final int hashCode()   { return key.hashCode() ^ val.hashCode(); }
    public final String toString(){ return key + "=" + val; }
  	//不能更新value
    public final V setValue(V value) {
        throw new UnsupportedOperationException();
    }

    public final boolean equals(Object o) {
        Object k, v, u; Map.Entry<?,?> e;
        return ((o instanceof Map.Entry) &&
                (k = (e = (Map.Entry<?,?>)o).getKey()) != null &&
                (v = e.getValue()) != null &&
                (k == key || k.equals(key)) &&
                (v == (u = val) || v.equals(u)));
    }

    /**
     * Virtualized support for map.get(); overridden in subclasses.
     */
    Node<K,V> find(int h, Object k) {
        Node<K,V> e = this;
        if (k != null) {
            do {
                K ek;
                if (e.hash == h &&
                    ((ek = e.key) == k || (ek != null && k.equals(ek))))
                    return e;
            } while ((e = e.next) != null);
        }
        return null;
    }
}

Node数组

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

在扩容时,要使用的新的Node数组

private transient volatile Node<K,V>[] nextTable;

容器内元素数量的基础值,通过CAS进行更新。

private transient volatile long baseCount;

数组初始化和扩容的控制符。当它是负数时,表示数组正在初始化或者扩容:-1表示初始化,-N表示有N-1个线程正在一起扩容。当它是0时,表示还没有初始化。当它是正数时,表示下一次初始化或者扩容的数组大小。

private transient volatile int sizeCtl;

3.3 构造器

ConcurrentHashMap同样采取了延迟初始化策略,即在构造实例时并不分配空间,在第一次添加元素时才分配。

(1)空参构造器

public ConcurrentHashMap() {
}

(2)设置起始容量的构造器

public ConcurrentHashMap(int initialCapacity) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException();
  	//如果要设置的容量大于等于最大容量的一半,则直接设置容量为最大容量;
  	//否则,将initialCapacity*1.5+1的结果,取大于等于该结果的最小的2的整数次幂
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
               MAXIMUM_CAPACITY :
               tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
  	//下次初始化时,将以cap作为数组的长度
    this.sizeCtl = cap;
}

3.4 添加节点

(1)put(K key, V value)方法不允许key为null,也不允许value为null。

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

putVal方法,主要做了下面几件事:

  • 如果数组没有创建,则创建数组;
  • 如果已经创建过数组了,就通过两次hash定位到某一个位置;
  • 如果该位置上还没节点,则通过CAS技术,将新节点插入;
  • 如果已经有节点了,但是该节点是ForwardingNode,说明现在正在扩容,则当前线程开始辅助扩容,并返回新的数组,下一轮操作在新数组上;
  • 如果该节点只是普通节点,那么插入时要加锁,每个位置上的所有节点共用一把锁,即该位置上的第一个节点;
  • 如果该位置以单链表形式存储节点,则以尾插法插入;如果以红黑树存储,则以红黑树的旋转插入。
  • 添加完成后,判断是否需要转换成红黑树;
  • 调用addCount方法统计容器的size,并检查是否需要扩容。
final V putVal(K key, V value, boolean onlyIfAbsent) {
  	//key和value都不能为null
    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;
      	//如果还没有初始化,则进行初始化,即initTable方法,初始化完成后,继续下一轮循环
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
      	//否则,通过(n-1)&hash方法定位到要存放的数组位置i
      	//该位置的第一个元素赋值给f,如果是空的,则直接通过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;                   
        }
      	//如果第一个节点的hash值时MOVED,则说明这是forwardingNode,表示现在正在扩容
        else if ((fh = f.hash) == MOVED)
          	//当前线程辅助扩容,返回新表,下一轮循环将在新表上操作
            tab = helpTransfer(tab, f);
      	//如果以上都不是,就通过加锁的方式,插入到链表或者红黑树中
        else {
            V oldVal = null;
          	//每个槽上发生冲突的节点,使用一把锁,这把锁是槽上的第一个节点
            synchronized (f) {
              	//获得锁后,还需要再次验证这个锁是否依然是槽上第一个节点
              	//如果该锁因为删除等操作,已经不是第一个节点了,就不能锁住这个槽上所有的节点
                if (tabAt(tab, i) == f) {
                  	//如果第一个节点hash大于等于0,则表示是链表节点
                    if (fh >= 0) {
                      	//当前槽上的节点数置为1
                        binCount = 1;
                      	//指针e在链表上进行遍历,遍历过程中bincount每次加1
                        for (Node<K,V> e = f;; ++binCount) {
                          	//ek表示链表上每个节点的key
                            K ek;
                          	//比较每个节点的可用与当前要插入的key是否相同
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                              	//如果找到了相同的key,则获取key对应的旧值
                                oldVal = e.val;
                              	//赋上新的值
                                if (!onlyIfAbsent)
                                    e.val = value;
                              	//退出循环
                                break;
                            }
                          	//否则,pred指针指向上一个节点
                            Node<K,V> pred = e;
                          	//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;
                        }
                    }
                }
            }
          	//添加完成后,判断是否要转化为红黑树
            if (binCount != 0) {
              	//如果当前槽上的节点数大于等于8,则尝试转化为红黑树
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
              	//如果是修改操作,则返回旧值
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
  	//统计size,同时检查是否需要扩容
    addCount(1L, binCount);
    return null;
}

initTable方法作数组初始化。

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
  	//while循环判断数组是否建好
    while ((tab = table) == null || tab.length == 0) {
      	//获取用于初始化和扩容的控制符sizeCtl,如果该值小于0,则说明已经有其他线程在做初始化了
        if ((sc = sizeCtl) < 0)//当前线程让出cpu的使用
            Thread.yield(); 
      	//否则,通过CAS方式,将sizeCtl赋值为-1,表示当前线程正在初始化数组
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
              	//再次判断数组是否已经建立
                if ((tab = table) == null || tab.length == 0) {
                  	//如果sc如果大于0,则使用sc作为容量,否则使用默认容量
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                  	//将n乘以0.75赋值给sc
                    sc = n - (n >>> 2);
                }
            } finally {
              	//当数组容量到达sizeCtl时,要进行扩容
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

CAS(Compare And Swap)即比较交换技术,是一种乐观的无锁方案。该指令包含了3个参数:共享变量的地址V、该地址处的期望值E、共享遍历的新值N。只有当V==E时,才会将V更新为新值N。

CAS是由硬件支持的CPU指令,具有原子性

compareAndSwapInt(this, SIZECTL, sc, -1)方法传入了4个参数:this表示当前对象,SIZECTL表示sizeCtl变量距离当前对象起始地址的偏移量,那么this+SIZECTL就是我们上面所说的共享变量sizeCtl的内存地址,sc是期望值,-1是新值。也就是说,只有当sizeCtl等于期待值sc,才会将sizeCtl赋值为-1。

回到开始处,我们将sizeCtl赋值给sc后,进入cas方法前,如果这期间sizeCtl没有发生变化,cas就可以成功将sizeCtl更新为-1,返回true。如果期间sizeCtl被改变,就会与我们的期望值不同,那么就不能更新成功,返回false。


接着,如果当前位置上没有元素,就使用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);
}

helpTransfer方法。如果正在扩容,多个线程一起帮忙扩容,这样效率更高。

final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
    Node<K,V>[] nextTab; int sc;
  	//ForwardingNode的一个域是nextTable,指向下一个新的数组
    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;
          	//调用cas,将sizeCtl加1
            if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
              	//辅助传输数据
                transfer(tab, nextTab);
                break;
            }
        }
        return nextTab;
    }
    return table;
}

ForwardingNode是一个特殊的节点。在数据传输过程中,ForwardingNode被插入到bins的头部,表示该位置上的节点已经处理。

nextTable域指向新数组,hash值为MOVED(负数),key和value都为null。

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;
    }
  	...
}

transfer方法将数据从旧数组传输到新数组。

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
  	//每核搬运的量至少为MIN_TRANSFER_STRIDE(16)
    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];//创建一个新数组,容量为原来的2倍
            nextTab = nt;
        } catch (Throwable ex) {      // try to cope with OOME
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        nextTable = nextTab;
        transferIndex = n;
    }
    int nextn = nextTab.length;
  	//创建一个ForwardingNode,并给nextTable域赋值为新数组
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
  	//advance为true表示当前节点已经处理
    boolean advance = true;
    boolean finishing = false; // to ensure sweep before committing nextTab
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
      	//通过索引i遍历原数组
        while (advance) {
            int nextIndex, nextBound;
            if (--i >= bound || finishing)
                advance = false;
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
          	//计算nextBound为nextIndex-stride,通过cas赋值给t'r
            else if (U.compareAndSwapInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {
              	bound = nextBound;
              	//i更新为nextIndex-1
                i = nextIndex - 1;
                advance = false;
            }
        }
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
          	//已经完成了传输
            if (finishing) {
                nextTable = null;
                table = nextTab;
              	//sizeCtl阈值设置为1.5*n,即0.75*n*2
                sizeCtl = (n << 1) - (n >>> 1);
              	//方法返回
                return;
            }
          	//sizeCtl减1,表示新加入一个线程辅助扩容
            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
            }
        }
      	//如果该位置上没有要传输的节点,则将ForwardingNode插入这个位置
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
      	//如果某个位置上的首节点的hash为MOVED,则表示这个位置已经处理过了
        else if ((fh = f.hash) == MOVED)
            advance = true; // already processed
        else {//否则加锁处理,锁为该位置上的首节点
            synchronized (f) {
              	//再次判断锁是否依然是首节点
                if (tabAt(tab, i) == f) {
                  	//将该位置上的节点分成两部分,ln和hn分别为两条链表的首节点
                    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);
                        }
                      	//将ln链表插入新数组的i位置,hn链表插入i+n位置
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                      	//将ForwardingNode插入到原数组的位置上,表示该位置已经处理
                        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;
                            }
                        }
                      	//如果扩容后,新数组位置上的节点小于等于6,则要去树变链表
                        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;
                    }
                }
            }
        }
    }
}

整个扩容过程是多线程并发扩容的,用ForwardingNode表示数组的某个位置为空不用处理或者已经被处理。辅助扩容过程中,当线程遇到ForwardingNode,就继续往后遍历。

扩容完成后,接下来要判断是否要转化成红黑树,如果数组长度小于64,就继续扩容1倍,不用转换。

private final void treeifyBin(Node<K,V>[] tab, int index) {
    Node<K,V> b; int n, sc;
    if (tab != null) {
        if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
            tryPresize(n << 1);
        else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
            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));
                }
            }
        }
    }
}

3.5 查询节点

查询方法主要做了下面几件事:

  • 首先经过两次hash,定位要找的节点在数组中的某个索引位置;
  • 如果该位置上的首节点就是要找的节点,直接返回;
  • 如果首节点是ForwardingNode,说明正在扩容,并且这个位置上的节点已经全部搬运,那就去新的数组中查找;
  • 否则,就进行常规地向下遍历查找,找不到返回null。
public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
  	//两次hash
    int h = spread(key.hashCode());
  	//定位到该key所在的数组索引位置,e指向该位置上的第一个节点
    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)))
                return e.val;
        }
      	//如果第一个节点的hash值小于0,表示ForwardingNode节点,说明现在正在扩容,
        else if (eh < 0)//并且这个位置上的节点已经全部搬运到新数组中的两个位置上了
          	//调用ForwardingNode节点的find方法定位到新数组,在两个位置上进行遍历查找
            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;
        }
    }
  	//找不到返回null
    return null;
}

4. 小结

比较java1.7和java1.8中,ConcurrentHashMap不同的实现方式:

  • java1.7中,ConcurrentHashMap由Segment数组+HashEntry数组实现,采用分段锁技术实现并发,以Segment为ReentrantLock。

  • java1.8中,ConcurrentHashMap由Node数组+链表+红黑树实现,采用CAS技术和Synchronized实现并发。以每个位置上的首节点作为锁,降低了锁的粒度,提升了并发度。

    java1.8中,使用链表和红黑树来解决冲突,当某个位置上冲突节点过多,如果在单链表上查询,效率很低;而使用红黑树,大大提升了查询效率,从O(n)到O(logn)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值