ConcurrentHashMap源码解析

6 篇文章 0 订阅

ConcurrentHashMap

@author 无忧少年

@createTime 2021/03/07

1. ConcurrentHashMap和HashMap、HashTable之间的区别

  • HashMap不是并发安全的容器,在并发情况下使用put操作会引起死循环,原因为多线程会导致HashMap的Entry链表形成环形的数据结构,一旦形成环形数据结构,Entry的next节点就永不为空,就会产生死循环获取Entry,并发情况下put值得时候可能会互相覆盖,一个线程遍历,另一个线程修改,会触发fast-fail。
  • HashTable在并发情况下效率低下,HashTable是使用的全局锁,当一个线程访问HashTable的同步方法时,其他访问同步方法的线程均会阻塞,在线程竞争激烈的情况,效率非常低。
  • ConcurrentHashMap的锁分段技术可有效提升并发访问效率,HashTable效率低下的主要原因是因为所有访问HashTable的线程都是竞争同一把锁,如果说容器中有很多把锁,并把锁应用于一部分数据上,那么多线程访问容器里的不同数据段的数据时,各个线程之间就不会存在锁的竞争,从而提高并发访问效率,这就是ConcurrentHashMap的锁分段技术。首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个县城占用锁访问一段数据的时候,其他段的数据也能被其他县城访问。

2. ConcurrentHashMap的结构

  • ConcurrentHashMap在JDK1.8中和HashMap结构大同小异,下面介绍一下ConcurrentHashMap的基本属性

// node数组最大容量:2^30=1073741824
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认初始值,必须是2的幕数
private static final int DEFAULT_CAPACITY = 16
//数组可能最大值,需要与toArray()相关方法关联
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 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(tranfer时,lc、hc=0两个计数器分别++记录原bin、新binTreeNode数量,<=UNTREEIFY_THRESHOLD 则untreeify(lo))
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
private static final int MIN_TRANSFER_STRIDE = 16;
private static int RESIZE_STAMP_BITS = 16;
// 2^15-1,help resize的最大线程数
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
// 32-16=16,sizeCtl中记录size大小的偏移量
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
// forwarding nodes的hash值
static final int MOVED     = -1;
// 树根节点的hash值
static final int TREEBIN   = -2;
// ReservationNode的hash值
static final int RESERVED  = -3;
// 可用处理器数量
static final int NCPU = Runtime.getRuntime().availableProcessors();
//存放node的数组
transient volatile Node<K,V>[] table;
/*控制标识符,用来控制table的初始化和扩容的操作,不同的值有不同的含义
 *当为负数时:-1代表正在初始化,-N代表有N-1个线程正在 进行扩容
 *当为0时:代表当时的table还没有被初始化
 *当为正数时:表示初始化或者下一次进行扩容的大小
 *
private transient volatile int sizeCtl;

3. 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) {
 
        //1.校验参数是否合法
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
 
        //2.遍历Node
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            
            //2.1.Node为空,初始化Node
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
 
            //2.2.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
            }
 
            //2.3.如果Node的hash值等于-1,map进行扩容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
 
            //2.4.如果Node有值,锁定该Node。
            //如果key的hash值大于0,key的hash值和key值都相等,则替换,否则new一个新的后继Node节点存放数据。
            //如果key的hash小于0,则考虑节点是否为TreeBin实例,替换节点还是额外添加节点。
 
            else {
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        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;
                                }
                            }
                        }
                        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) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
 
        //3.计算map的size
        addCount(1L, binCount);
        return null;
    }
  • 根据 key 计算出 hashcode 。
  • 判断是否需要进行初始化。
  • f 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
  • 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
  • 如果都不满足,则利用 synchronized 锁写入数据。
  • 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。

4. 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) {
            // 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            // 如果是红黑树那就按照树的方式获取值。
            else if (eh < 0)
                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;
    }
  • 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
  • 如果是红黑树那就按照树的方式获取值。
  • 不满足那就按照链表的方式遍历获取值。

5.size方法

HashMap的size()是在put的时候对size进行++size的操作,每增加一个元素size大小自增1,调用size()的时候,直接赋值即可获得。

ConcurrentHashMap的计算map中映射的个数,可以由mappingCount完全替代,因为ConcurrentHashMap可能包含的映射数多于返回值为为int的映射数。该返回的值是估计值,实际数量可能会因为并发插入或删除而有所不同。

ConcurrentHashMap的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;
        long sum = baseCount;
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
  }

public long mappingCount() {
	long n = sumCount();
	return (n < 0L) ? 0L : n; // ignore transient negative values
}

————————————————

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值