知名企业网站搭建新感觉全网价值营销服务商,成都专业网站建设厂,北京企业免费建站,怎么建设信息网站目录 原理#xff1a;
初始化数据结构时的线程安全 put 操作时的线程安全 原理#xff1a; 多段锁cassynchronize
初始化数据结构时的线程安全
在 JDK 1.8 中#xff0c;初始化 ConcurrentHashMap 的时候这个 Node[] 数组是还未初始化的#xff0c;会等到第一次 put() 方…目录 原理
初始化数据结构时的线程安全 put 操作时的线程安全 原理 多段锁cassynchronize
初始化数据结构时的线程安全
在 JDK 1.8 中初始化 ConcurrentHashMap 的时候这个 Node[] 数组是还未初始化的会等到第一次 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 (NodeK,V[] tab table;;) {NodeK,V f; int n, i, fh;// 判断Node数组为空if (tab null || (n tab.length) 0)// 初始化Node数组tab initTable();......
}此时会有并发问题的如果多个线程同时调用 initTable() 初始化 Node[] 数组怎么办看看 Doug Lea 大师是如何处理的
private final NodeK,V[] initTable() {NodeK,V[] tab; int sc;// 每次循环都获取最新的Node[]数组引用while ((tab table) null || tab.length 0) {// sizeCtl是一个标记位若为-1代表有线程在进行初始化工作了if ((sc sizeCtl) 0)// 让出CPU时间片Thread.yield(); // 此时代表没有线程在进行初始化工作CAS操作将本实例的sizeCtl变量设置为-1 else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {// 如果CAS操作成功了代表本线程将负责初始化工作try {// 再检查一遍数组是否为空if ((tab table) null || tab.length 0) {// 在初始化ConcurrentHashMap时sizeCtl代表数组大小默认16// 所以此时n默认为16int n (sc 0) ? sc : DEFAULT_CAPACITY;SuppressWarnings(unchecked)NodeK,V[] nt (NodeK,V[])new Node?,?[n];// 将其赋值给table变量table tab nt;// 通过位运算n减去n二进制右移2位相当于乘以0.75// 例如16经过运算为12与乘0.75一样只不过位运算更快sc n - (n 2);}} finally {// 将计算后的sc12直接赋值给sizeCtl表示达到12长度就扩容// 由于这里只会有一个线程在执行直接赋值即可没有线程安全问题只需要保证可见性sizeCtl sc;}break;}}return tab;
}总结就算有多个线程同时进行 put 操作在初始化 Node[] 数组时使用了 CAS 操作来决定到底是哪个线程有资格进行初始化其他线程只能等待。用到的并发技巧如下
volatile 修饰 sizeCtl 变量它是一个标记位用来告诉其他线程这个坑位有没有线程在进行初始化工作其线程间的可见性由 volatile 保证CAS 操作CAS 操作保证了设置 sizeCtl 标记位的原子性保证了在多线程同时进行初始化 Node[] 数组时只有一个线程能成功 put 操作时的线程安全
public V put(K key, V value) {return putVal(key, value, false);
}final V putVal(K key, V value, boolean onlyIfAbsent) {// K,V 都不能为空if (key null || value null) throw new NullPointerException();// 取得 key 的 hash 值int hash spread(key.hashCode());// 用来计算在这个节点总共有多少个元素用来控制扩容或者转换为树int binCount 0;// 数组的遍历自旋插入结点直到成功for (NodeK,V[] tab table;;) { NodeK,V f; int n, i, fh;// 当Node[]数组为空时进行初始化if (tab null || (n tab.length) 0) tab initTable();// Unsafe类volatile的方式取出hashCode散列后通过与运算得出的Node[]数组下标值对应的Node对象// 此时 Node 位置若为 null则表示还没有线程在此 Node 位置进行插入操作说明本次操作是第一次else if ((f tabAt(tab, i (n - 1) hash)) null) {// 如果这个位置没有元素的话则通过 CAS 的方式插入数据if (casTabAt(tab, i, null, // 创建一个 Node 添加到数组中null 表示的是下一个节点为空new NodeK,V(hash, key, value, null)))// 插入成功退出循环 break; }// 如果检测到某个节点的 hash 值是 MOVED则表示正在进行数组扩容 else if ((fh f.hash) MOVED) // 帮助扩容tab helpTransfer(tab, f);// 此时说明已经有线程对Node[]进行了插入操作后面的插入很有可能会发生Hash冲突else {V oldVal null;// ----------------synchronized----------------synchronized (f) {// 二次确认此Node对象还是原来的那一个if (tabAt(tab, i) f) {// ----------------table[i]是链表结点----------------if (fh 0) {// 记录结点数超过阈值后需要转为红黑树提高查找效率binCount 1; // 遍历这个链表for (NodeK,V e f;; binCount) {K ek;// 要存的元素的 hash 值和 key 跟要存储的位置的节点的相同的时候替换掉该节点的 value 即可if (e.hash hash ((ek e.key) key ||(ek ! null key.equals(ek)))) {oldVal e.val;if (!onlyIfAbsent)e.val value;break;}// 到了链表的最末端将新值放到链表的最末端NodeK,V pred e;// 如果不是同样的 hash同样的 key 的时候则判断该节点的下一个节点是否为空if ((e e.next) null) { // ----------------“尾插法”插入新结点----------------pred.next new NodeK,V(hash, key,value, null);break;}}}// ----------------table[i]是红黑树结点----------------else if (f instanceof TreeBin) { NodeK,V p;binCount 2;// 调用putTreeVal方法将该元素添加到树中去if ((p ((TreeBinK,V)f).putTreeVal(hash, key,value)) ! null) {oldVal p.val;if (!onlyIfAbsent)p.val value;}}}}if (binCount ! 0) {// 当在同一个节点的数目达到8个的时候则扩张数组或将给节点的数据转为treeif (binCount TREEIFY_THRESHOLD)// 链表 - 红黑树 转换treeifyBin(tab, i); // 表明本次put操作只是替换了旧值不用更改计数值 if (oldVal ! null)return oldVal;break;}}}addCount(1L, binCount);// 计数值加1return null;
}总结 put() 方法的核心思想由于其减小了锁的粒度若 Hash 完美不冲突的情况下可同时支持 n 个线程同时 put 操作n 为 Node 数组大小在默认大小 16 下可以支持最大同时 16 个线程无竞争同时操作且线程安全
当 Hash 冲突严重时Node 链表越来越长将导致严重的锁竞争此时会进行扩容将 Node 进行再散列下面会介绍扩容的线程安全性。总结一下用到的并发技巧
减小锁粒度将 Node 链表的头节点作为锁若在默认大小 16 情况下将有 16 把锁大大减小了锁竞争上下文切换就像开头所说将串行的部分最大化缩小在理想情况下线程的 put 操作都为并行操作。同时直接锁住头节点保证了线程安全使用了 volatile 修饰 table 变量并使用 Unsafe 的 getObjectVolatile() 方法拿到最新的 NodeCAS 操作如果上述拿到的最新的 Node 为 null则说明还没有任何线程在此 Node 位置进行插入操作说明本次操作是第一次synchronized 同步锁如果此时拿到的最新的 Node 不为 null则说明已经有线程在此 Node 位置进行了插入操作此时就产生了 hash 冲突此时的 synchronized 同步锁就起到了关键作用防止在多线程的情况下发生数据覆盖线程不安全接着在 synchronized 同步锁的管理下按照相应的规则执行操作 参考
【精选】ConcurrentHashMap是如何实现线程安全的_concurrenthashmap如何保证线程安全-CSDN博客