ConcurrentHashMap-添加元素

复习HashMap

在jdk1.8的ConcurrentHashMap也是变成跟HashMap一样的数据结构,所以开始之前先复习一下jdk1.8的HashMap。

HashMap没有任何锁机制,所以线程不安全
HashMap底层维护了Node数组+Node链表+红黑树。
HashMap初始化和扩容只能是2的乘方
HashMap负载因子阈值是数组的0.75
HashMap链表尾插法
HashMap是懒加载机制
HashMap单链表大于8,数组长度大于64变成红黑树提高链表的查找速度。
HashMap无序(根据hash值确定数组位置)不重复(重复就是替换)。
HashMap的key和value允许为null

ConcurrentHashMap和HashMap和Hashtable三者的区别

HashMap:线程不安全

Hashtable:线程安全但是效率低

ConcurrentHashMap:线程安全,相比Hashtable效率高

源码

1、插入

一些参数
static final int MOVED     = -1; // hash for forwarding nodes 转发节点的哈希
static final int TREEBIN   = -2; // hash for roots of trees 红黑树root的哈希
static final int RESERVED  = -3; // hash for transient reservations 临时保留的哈希
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash 正常节点散列的可用位,这里在算hash值时用到
sizeCtl
  • -1:正在扩容
  • 0:当前还未初始化
  • 大于0
    • 数组建议大小
    • 阈值

大于0:

/*
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.

机翻:table初始化和调整大小控制。如果为负,则table正在初始化或调整大小:-1 用于初始化,否则 -(1 + 活动调整大小线程的数量)。否则,当table为空时,保留创建时使用的初始表大小,或默认为 0。初始化后,保存下一个元素计数值,根据该值调整table的大小。
*/
private transient volatile int sizeCtl;
有参构造
public ConcurrentHashMap(int initialCapacity) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException();
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
               MAXIMUM_CAPACITY :
               // 有参构造给值的时候,sizeCtl会计算出一个值,2次幂
               tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
    this.sizeCtl = cap;
}
Unsaft

unsaft:他是操作内存,或者是一些java操作不到的操作,通过JNI(Java Native Interface:java本地接口)的方法

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

static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                    Node<K,V> c, Node<K,V> v) {
    // 竞争成功返回true并修改,竞争失败返回false不修改
    return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
putVal方法
final V putVal(K key, V value, boolean onlyIfAbsent) {
    // 与hashmap为null给0不同,这里不能为null
    if (key == null || value == null) throw new NullPointerException();
    // 得到hash值:(h ^ (h >>> 16)) & HASH_BITS;与hahsmap比多了一个& HASH_BITS操作
    // 扰动函数,尽量为了随机化
    int hash = spread(key.hashCode());
    // 一个计数器,用来计数是否有冲突,且冲突的次数是多少,如果太多了,需要从链表变为红黑树
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        // Node<K,V> f 头节点
        // int n 数组长度
        // int i 具体的数组索引下标
        // int fh hash值
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            // 初始化table,懒加载
            tab = initTable();
        // tabAt:返回tab下标的node;
        // i = (n - 1) & hash:算出下标
        // 为什么拿这个slot中的node也要保证原子性???:拿的过程中,可能其他线程插入了一个,node不为null,但是内存不可见,条件还是满足,进入了下面代码:但是其他添加元素的时候还是拿的null做cas,就算出现不可见性,其实问题也不大
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 空node直接用cas添加元素,不用加锁
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        // 当前在扩容迁移,并且迁移到当前我要插入的这个slot了
        else if ((fh = f.hash) == MOVED)
            // 当前已经在扩容和迁移了,我要尝试加入进去
            // 并发迁移,多线程一起完成迁移工作
            tab = helpTransfer(tab, f);
        else {
            // 代表当前slot已经有节点了,发生了冲突
            // 并且没有扩容
            V oldVal = null;
            // 锁的是头节点f
            synchronized (f) {
                // 首先判断,当前这个f与最新的节点是否相同。如果不相同直接退出,然后继续for自旋,再去插入
                // 这里可能已经迁移一次了,当前f与最新的slot不相同,所以需要重新自旋,再找位置插入
                if (tabAt(tab, i) == f) {
                    // static final int MOVED     = -1; // hash for forwarding nodes 转发节点的哈希
                    // static final int TREEBIN   = -2; // hash for roots of trees 红黑树root的哈希 
                    // 节点hash值大于等于0说明是链表 -2是红黑树
                    if (fh >= 0) {
                        // bincount计数,判断是否要转红黑树
                        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;
                            }
                        }
                    }
                    // 如果fh小于0就走这边。这边是树的操作
                    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;
                        }
                    }
                }
            }
            // bincount==0:说明数组已经变了,根本没插入
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    // 尝试转为红黑树
                    treeifyBin(tab, i);
                // 如果oldval存在,不为空的情况下,就是发生了覆盖问题,需要将之前的值返回给调用put方法
                if (oldVal != null)
                    // 覆盖不会执行addCount
                    return oldVal;
                break;
            }
        }
    }
    // 元素计数:走到这里代表已经插入一个节点了,总长度需要++一次
    // binCount,能知道当前是否冲突了,并且冲突次数是多少
    // 多线程下++是不安全的:++不是原子性,局部变量表保存变量,操作数栈操作数据后再返回给局部变量表
    addCount(1L, binCount);
    return null;
}
initTable方法
private final Node<K,V>[] initTable() {
    // Node<K,V>[] tab
    // int sc 
    Node<K,V>[] tab; int sc;
    // 双重检测锁:double check lock
    while ((tab = table) == null || tab.length == 0) {
        // sizeCtl全局变量:他有多层含义,不同时期,有不同的表示
        // 扩容
        if ((sc = sizeCtl) < 0)
            // while循环:竞争失败的会再次进入,
            // 这里使用yield让出CPU的资源,不消耗cpu资源,因为有线程在扩容了,只需要等待即可
            Thread.yield(); // lost initialization race; just spin
        // 线程争抢扩容的cas操作
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                // 如果有建议大小就选择建议大小
                // 如果没有设置建议大小,就走默认16的大小
                /* 这里用的compareAndSwapInt,失败的直接往后走再次进入到while中,然后yield了,
               	   不存在说有线程在等待,锁释放后进入到这里再多初始化一个数组,
               	   已经是一个原子性操作了,那么为什么还要加一个判断????
                   */
                if ((tab = table) == null || tab.length == 0) { 
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    // 把数组赋值给全局变量
                    table = tab = nt;
                    // 这边是通过负载因子计算阈值
                    sc = n - (n >>> 2);
                }
            } finally {
                // 这个地方解释了double check lock为什么要加第二个不为null的判断
                // 初始化table成功后,sizeCtl做了改变,可能有线程会再次进入初始化代码
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

2、节点计数

用的longadder的代码,唯一区别是concurrenthashmap没用到钩子方法

3、扩容机制

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值