登录
注册
开源
企业版
高校版
搜索
帮助中心
使用条款
关于我们
开源
企业版
高校版
私有云
模力方舟
AI 队友
登录
注册
3月21日 深圳|OpenClaw 线下实战沙龙:招聘、资讯、项目协同三大场景实操,VS ZeroClaw 横向对比评测,别再只会装,来现场跑通真实业务!
代码拉取完成,页面将自动刷新
捐赠
捐赠前请先登录
取消
前往登录
扫描微信二维码支付
取消
支付完成
支付提示
将跳转至支付宝完成支付
确定
取消
Watch
不关注
关注所有动态
仅关注版本发行动态
关注但不提醒动态
24
Star
55
Fork
2
Java技术交流
/
Java技术提升库
代码
Issues
56
Pull Requests
0
Wiki
统计
流水线
服务
JavaDoc
PHPDoc
质量分析
Jenkins for Gitee
腾讯云托管
腾讯云 Serverless
悬镜安全
阿里云 SAE
Codeblitz
SBOM
我知道了,不再自动展开
更新失败,请稍后重试!
移除标识
内容风险标识
本任务被
标识为内容中包含有代码安全 Bug 、隐私泄露等敏感信息,仓库外成员不可访问
009、如何保证集合是线程安全的?ConcurrentHashMap如何实现高效地线程安全!
待办的
#I1VX78
高强
创建于
2020-09-19 21:27
### 考点分析! 谈到线程安全和并发,可以说是 Java面试中必考的考点。 而且ConcurrentHashMap等并发容器实现也在不断演进,不能一概而论。如果要深入思考并回答这个问题及其扩展方面,至少需要∶· 理解基本的线程安全工具。 - 理解传统集合框架并发编程中Map存在的问题,清楚简单同步方式的不足。 - 梳理并发包内,尤其是ConcurrentHashMap采取了哪些方法来提高并发表现。 - 最好能够掌握ConcurrentHashMap自身的演进,目前的很多分析资料还是基于其早期版本。 ### 典型回答! Java提供了不同层面的线程安全支持。在传统集合框架内部,除了Hashtable等同步容器,还提供了所谓的同步包装器(Synchronized Wrapper),我们可以调用 Collctions 工具类提供的包装方法,来获取一个同步的包装容器(如CollectionssynchronizedMap),但是它们都是利用非常粗粒度的同步方式,在高并发情况下,性能比较低下。 另外,更加普遍的选择是利用并发包提供的线程安全容器类,它提供了∶ - 各种并发容器,比如 ConcurrentHashMap、CopyOnWriteArrayList。 - 各种线程安全队列(Queue/Deque),如 ArrayBlockingQueue、SynchronousQueue。 - 各种有序容器的线程安全版本等。 具体保证线程安全的方式,包括有从简单的synchronize 方式,到基于更加精细化的,比如基于分离锁实现的ConcurrentHashMap等并发实现等。具体选择要看开发的场景需求,总体来说,并发包内提供的容器通用场景,远优于早期的简单同步实现。 ### 知识扩展! **1.为什么需要ConcurrentHashMap?** Hashtable本身比较低效,因为它的实现基本就是将put、get、size等各种方法加上"synchronized"。简单来说,这就导致了所有并发操作都要竞争同一把锁,一个线程在进行同步操作时,其他线程只能等待,大大降低了并发操作的效率。 前面已经提过 HashMap不是线程安全的,并发情况会导致类似CPU占用100%等一些问题。 **2.ConcurrentHashMap分析** 我们再来看看ConcurrentHashMap 是如何设计实现的,为什么它能大大提高并发效率。首先,我这里强调,ConcurrentHashMap的设计实现其实一直在演化,比如在Java 8中就发生了非常大的变化(Java7其实也有不少更新),所以,我这里将比较分析结构、实现机制等方面,对比不同版本的主要区别。 早期 ConcurrentHashMap,其实现是基于∶ - 分离锁,也就是将内部进行分段(Segment),里面则是HashEntry的数组,和 HashMap 类似,哈希相同的条目也是以链表形式存放。 - HashEntry内部使用 volatile的value字段来保证可见性,也利用了不可变对象的机制以改进利用 Unsafe提供的底层能力,比如 volatile access,去直接完成部分操作,以最优化性能,毕竟 Unsafe 中的很多操作都是VM itrinsic 优化过的。 你可以参考下面这个早期ConcurrentHashMap 内部结构的示意图,其核心是利用分段设计,在进行并发操作的时候,只需要锁定相应段,这样就有效避免了类似 Hashtable整体同步的问题,大大提高了性能。  在构造的时候,Segment 的数量由所谓的concurrentcyLevel 决定,默认是16,也可以在相应构造函数直接指定。 注意,Java需要它是2的幂数值,如果输入是类似15这种非幂值,会被自动调整到16 之类2的幂数值。 具体情况,我们一起看看一些Map基本操作的源码,这是 JDK7比较新的get代码。针对具体的优化部分,为方便理解,我直接注释在代码段里,get操作需要保证的是可见性,所以并没有什么同步逻辑。 ``` public V get(Object key){ Segment<K,V> s;//manually integrate access methods to reduce overhead HashEntryk,V>[]tab; int h= hash(key.hashcode()); //利用位操作替换普通数学运算 long u=((h>>segmentShift)& segmentMask)<< SSHIFT)+SBASE; // 以 Segment为单位,进行定位 //利用 Unsafe 直接进行 volatile access if((s=(Segment<k,V>)UNSAFE.getobjectVolatile(segments,u))!= null&& (tab= s.table)!= nul1){//省略 } return null; } ``` 而对于 put 操作,首先是通过二次哈希避免哈希冲突,然后以Unsafe调用方式,直接获取相应的Segment,然后进行线程安全的 put 操作∶ ``` public V put(K key, V value){ Segment<K,V>s; if(value == null) throw new NullPointerException();// 二次哈希,以保证数据的分散性,避免哈希冲突 int hash= hash(key.hashCode()); int j=(hash >>>segmentShift)&segmentMask; if((s=(Segment<K,V>)UNSAFE.getoObject // nonvolatile; recheck (segments,(j<< SSHIFT)+SBASE))== null)// in ensureSegment s= ensureSegment(j); return s.put(key, hash, value, false); ``` 其核心逻辑实现在下面的内部方法中∶ ``` final V put(K key, int hash, V value, boolean onlyIfAbsent){ // scanAndLockForPut 会去查找是否有 key 相同Node // 无论如何,确保获取锁 HashEntryK,V> node= tryLock()?null: scanAndLockForPut(key, hash, value); v oldvValue; try{ HashEntry<K,V>[]tab = table; int index=(tab.length-1)& hash; HashEntry<K,V>first =entryAt(tab, index); for(HashEntry<K,V> e= first;;){ if(e!=null){ K k; //更新已有 value.. } else{ //放置 HashEntry 到特定位置,如果超过阈值,进行 rehash //..· } } }finally{ unlock(); } return oldValue; ``` 所以,从上面的源码清晰的看出,在进行并发写操作时∶ - ConcurrentHashMap 会获取再入锁,以保证数据一致性,Segment本身就是基于ReentrantLock的扩展实现,所以,在并发修改期间,相应Segment是被锁定的。 - 在最初阶段,进行重复性的扫描,以确定相应 key值是否已经在数组里面,进而决定是更新 还是放置操作,你可以在代码里看到相应的注释。重复扫描、检测冲突是ConcurrentHashMap的常见技巧。 - 在之前的面试题 HashMap时,提到了可能发生的扩容问题,在ConcurrentHashMap 中同样存在。不过有一个明显区别,就是它进行的不是整体的扩容,而是单独对Segment 进行扩容,细节就不介绍了。 另外一个 Map的 size 方法同样需要关注,它的实现涉及分离锁的一个副作用。 试想,如果不进行同步,简单的计算所有 Segment 的总值,可能会因为并发 put,导致结果不准确,但是直接锁定所有Segment进行计算,就会变得非常昂贵。其实,分离锁也限制了Map 的初始化等操作。 所以,ConcrrentHashMap的实现是通过重试机制(RETRIES_BEFORE_LOCK,指定重试次数2),来试图获得可靠值。如果没有监控到发生变化(通过对比 Segment.modCount),就直接返回,否则获取锁进行操作。 下面我来对比一下,在 Java8和之后的版本中,ConcurrentHashMap发生了哪些变化呢? - 总体结构上,它的内部存储变得和 HashMap结构非常相似,同样是大的桶(bucket)数组,然后内部也是一个个所谓的链表结构(bin),同步的粒度要更细致 一些。 - 其内部仍然有 Segment 定义,但仅仅是为了保证序列化时的兼容性而已,不再有任何结构 上的用处。 - 因为不再使用Segment,初始化操作大大简化,修改为 lazy-load形式,这样可以有效避免 初始开销,解决了老版本很多人抱怨的这一点。 - 数据存储利用 volatile来保证可见性。 - 使用CAS 等操作,在特定场景进行无锁并发操作。 - 使用 Unsafe、LongAdder 之类底层手段,进行极端情况的优化。 先看看现在的数据存储内部实现,我们可以发现 Key是 final的,因为在生命周期中,一个条目的Key发生变化是不可能的;与此同时 val,则声明为 volatile,以保证可见性。 ``` static class Node<K, V> implements Map.Entry<K,V>{ final int hash; final K key; volatile V val; volatile Node<K, V> next; //.. ``` 我这里就不再介绍 get 方法和构造函数了,相对比较简单,直接看并发的put 是如何实现的。 ``` final V putVal(K key, V value, boolean onlyIfAbsent){if (key == null ||value == null1)thro int hash= spread(key.hashCode(); int binCount =o; for (Node<k,V>[] tab= table;;){ Nodek,V> f; int n,i,fh; K fk; V fv; if(tab== null (n= tab.1ength)==0) tab= initTable(); else if((f= tabAt(tab,i=(n-1)& hash)== null){ //利用CAS 去进行无锁线程安全操作,如果 bin 是空的 if(casTabAt(tab, i, null, new Node<K,V>(hash,key,value))) break; )else if((fh=f.hash)==MOVED) tab= helpTransfer(tab, f); else if(onlyIfAbsent//不加锁,进行检查 && fh == hash &&((fk=f.key)== key || (fk !=null && key.equals(fk))) &&(fv =f.val)!=null) return fv; else{ V oldval=null; synchronized(f){ //细粒度的同步修改操作.. } } //Bin 超过阈值,进行树化 if(bincount !=e){ if(binCount >= TREEIFY_THRESHOLD) treeifyBin(tab,i); if(oldval !=null) return oldVal; break; } } } addCount(1L, binCount); return null; ``` 初始化操作实现在 initTable里面,这是一个典型的CAS使用场景,利用volatile的 sizeCt作为互斥手段∶如果发现竞争性的初始化,就 spin 在那里,等待条件恢复;否则利用CAS设置排他标志。如果成功则进行初始化; 否则重试。请参考下面代码∶ ``` private final Node<K,V>[] initTable(){ NodeK,V>[] tab; int sc; while(tab= table)= null l tab.length== o)( //如果发现冲突,进行 spin 等待 if(sc= sizecCtl)< 0) Thread.yield(); //CAS 成功返回 true,则进入真正的初始化逻辑 else if(U.compareAndSetInt(this,SIZECTL, sc,-1)){ if((tab=table)== nulltab.length == e){ int n=(sc>e)?sc:DEFAULT_CAPACITY; @SuppressMarnings("unchecked") Node<K,V>[]nt =(Node<K,V>[])new Node<?,?>[n]; table= tab= nt; sc=n -(n>>>2); } }finally{ sizeCtl = sc; } break; } } return tab; } ``` 当 bin 为空时,同样是没有必要锁定,也是以CAS操作去放置。 你有没有注意到,在同步逻辑上,它使用的是 synchronized,而不是通常建议的 ReentrantLock 之类,这是为什么呢?现代 JDK中,synchronized 已经被不断优化,可以不再过分担心性能差异,另外,相比于ReentrantLlock,它可以减少内存消耗,这是个非常大的优势。 与此同时,更多细节实现通过使用 Unsafe进行了优化,例如 tabAt 就是直接利用getObjectAcquire,避免间接调用的开销。 ``` static final<K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i){ return(Node<K,V>)U.getObjectAcquire(tab,((long)i << ASHIFT)+ABASE); } ``` 再看看,现在是如何实现 size操作的。阅读代码你会发现,真正的逻辑是在 sumCount 方法中,那么 sumCount做了什么呢? ``` final long sumcount(){ CounterCell[]as = counterCells; CounterCell a;long sum= baseCount; if(as !=null){ for(int i=6;i< as.length;++i){ if((a= as[1])!=null) sum += a.value; return sum; } } } ``` 我们发现,虽然思路仍然和以前类似,都是分而治之的进行计数,然后求和处理,但实现却基于一个奇怪的CounterCell。难道它的数值,就更加准确吗?数据一致性是怎么保证的? ``` static final class CounterCell{ volatile long value; CounterCell(long x){ value=x; } ``` 其实,对于CounterCell的操作,是基于java.util.concurrent.atomic.LongAdder进行的,是一种JVM利用空间换取更高效率的方法,利用了Striped64内部的复杂逻辑。这个东西非常小众,大多数情况下,建议还是使用 AtomicLong,足以满足绝大部分应用的性能需求。 从线程安全问题开始,概念性的总结了基本容器工具,分析了早期同步容器的问题,进而分析了Java7和 Java8中ConcurrentHashMap是如何设计实现的,希望ConcurrentHashMap 的并发技巧对你在日常开发可以有所帮助。
### 考点分析! 谈到线程安全和并发,可以说是 Java面试中必考的考点。 而且ConcurrentHashMap等并发容器实现也在不断演进,不能一概而论。如果要深入思考并回答这个问题及其扩展方面,至少需要∶· 理解基本的线程安全工具。 - 理解传统集合框架并发编程中Map存在的问题,清楚简单同步方式的不足。 - 梳理并发包内,尤其是ConcurrentHashMap采取了哪些方法来提高并发表现。 - 最好能够掌握ConcurrentHashMap自身的演进,目前的很多分析资料还是基于其早期版本。 ### 典型回答! Java提供了不同层面的线程安全支持。在传统集合框架内部,除了Hashtable等同步容器,还提供了所谓的同步包装器(Synchronized Wrapper),我们可以调用 Collctions 工具类提供的包装方法,来获取一个同步的包装容器(如CollectionssynchronizedMap),但是它们都是利用非常粗粒度的同步方式,在高并发情况下,性能比较低下。 另外,更加普遍的选择是利用并发包提供的线程安全容器类,它提供了∶ - 各种并发容器,比如 ConcurrentHashMap、CopyOnWriteArrayList。 - 各种线程安全队列(Queue/Deque),如 ArrayBlockingQueue、SynchronousQueue。 - 各种有序容器的线程安全版本等。 具体保证线程安全的方式,包括有从简单的synchronize 方式,到基于更加精细化的,比如基于分离锁实现的ConcurrentHashMap等并发实现等。具体选择要看开发的场景需求,总体来说,并发包内提供的容器通用场景,远优于早期的简单同步实现。 ### 知识扩展! **1.为什么需要ConcurrentHashMap?** Hashtable本身比较低效,因为它的实现基本就是将put、get、size等各种方法加上"synchronized"。简单来说,这就导致了所有并发操作都要竞争同一把锁,一个线程在进行同步操作时,其他线程只能等待,大大降低了并发操作的效率。 前面已经提过 HashMap不是线程安全的,并发情况会导致类似CPU占用100%等一些问题。 **2.ConcurrentHashMap分析** 我们再来看看ConcurrentHashMap 是如何设计实现的,为什么它能大大提高并发效率。首先,我这里强调,ConcurrentHashMap的设计实现其实一直在演化,比如在Java 8中就发生了非常大的变化(Java7其实也有不少更新),所以,我这里将比较分析结构、实现机制等方面,对比不同版本的主要区别。 早期 ConcurrentHashMap,其实现是基于∶ - 分离锁,也就是将内部进行分段(Segment),里面则是HashEntry的数组,和 HashMap 类似,哈希相同的条目也是以链表形式存放。 - HashEntry内部使用 volatile的value字段来保证可见性,也利用了不可变对象的机制以改进利用 Unsafe提供的底层能力,比如 volatile access,去直接完成部分操作,以最优化性能,毕竟 Unsafe 中的很多操作都是VM itrinsic 优化过的。 你可以参考下面这个早期ConcurrentHashMap 内部结构的示意图,其核心是利用分段设计,在进行并发操作的时候,只需要锁定相应段,这样就有效避免了类似 Hashtable整体同步的问题,大大提高了性能。  在构造的时候,Segment 的数量由所谓的concurrentcyLevel 决定,默认是16,也可以在相应构造函数直接指定。 注意,Java需要它是2的幂数值,如果输入是类似15这种非幂值,会被自动调整到16 之类2的幂数值。 具体情况,我们一起看看一些Map基本操作的源码,这是 JDK7比较新的get代码。针对具体的优化部分,为方便理解,我直接注释在代码段里,get操作需要保证的是可见性,所以并没有什么同步逻辑。 ``` public V get(Object key){ Segment<K,V> s;//manually integrate access methods to reduce overhead HashEntryk,V>[]tab; int h= hash(key.hashcode()); //利用位操作替换普通数学运算 long u=((h>>segmentShift)& segmentMask)<< SSHIFT)+SBASE; // 以 Segment为单位,进行定位 //利用 Unsafe 直接进行 volatile access if((s=(Segment<k,V>)UNSAFE.getobjectVolatile(segments,u))!= null&& (tab= s.table)!= nul1){//省略 } return null; } ``` 而对于 put 操作,首先是通过二次哈希避免哈希冲突,然后以Unsafe调用方式,直接获取相应的Segment,然后进行线程安全的 put 操作∶ ``` public V put(K key, V value){ Segment<K,V>s; if(value == null) throw new NullPointerException();// 二次哈希,以保证数据的分散性,避免哈希冲突 int hash= hash(key.hashCode()); int j=(hash >>>segmentShift)&segmentMask; if((s=(Segment<K,V>)UNSAFE.getoObject // nonvolatile; recheck (segments,(j<< SSHIFT)+SBASE))== null)// in ensureSegment s= ensureSegment(j); return s.put(key, hash, value, false); ``` 其核心逻辑实现在下面的内部方法中∶ ``` final V put(K key, int hash, V value, boolean onlyIfAbsent){ // scanAndLockForPut 会去查找是否有 key 相同Node // 无论如何,确保获取锁 HashEntryK,V> node= tryLock()?null: scanAndLockForPut(key, hash, value); v oldvValue; try{ HashEntry<K,V>[]tab = table; int index=(tab.length-1)& hash; HashEntry<K,V>first =entryAt(tab, index); for(HashEntry<K,V> e= first;;){ if(e!=null){ K k; //更新已有 value.. } else{ //放置 HashEntry 到特定位置,如果超过阈值,进行 rehash //..· } } }finally{ unlock(); } return oldValue; ``` 所以,从上面的源码清晰的看出,在进行并发写操作时∶ - ConcurrentHashMap 会获取再入锁,以保证数据一致性,Segment本身就是基于ReentrantLock的扩展实现,所以,在并发修改期间,相应Segment是被锁定的。 - 在最初阶段,进行重复性的扫描,以确定相应 key值是否已经在数组里面,进而决定是更新 还是放置操作,你可以在代码里看到相应的注释。重复扫描、检测冲突是ConcurrentHashMap的常见技巧。 - 在之前的面试题 HashMap时,提到了可能发生的扩容问题,在ConcurrentHashMap 中同样存在。不过有一个明显区别,就是它进行的不是整体的扩容,而是单独对Segment 进行扩容,细节就不介绍了。 另外一个 Map的 size 方法同样需要关注,它的实现涉及分离锁的一个副作用。 试想,如果不进行同步,简单的计算所有 Segment 的总值,可能会因为并发 put,导致结果不准确,但是直接锁定所有Segment进行计算,就会变得非常昂贵。其实,分离锁也限制了Map 的初始化等操作。 所以,ConcrrentHashMap的实现是通过重试机制(RETRIES_BEFORE_LOCK,指定重试次数2),来试图获得可靠值。如果没有监控到发生变化(通过对比 Segment.modCount),就直接返回,否则获取锁进行操作。 下面我来对比一下,在 Java8和之后的版本中,ConcurrentHashMap发生了哪些变化呢? - 总体结构上,它的内部存储变得和 HashMap结构非常相似,同样是大的桶(bucket)数组,然后内部也是一个个所谓的链表结构(bin),同步的粒度要更细致 一些。 - 其内部仍然有 Segment 定义,但仅仅是为了保证序列化时的兼容性而已,不再有任何结构 上的用处。 - 因为不再使用Segment,初始化操作大大简化,修改为 lazy-load形式,这样可以有效避免 初始开销,解决了老版本很多人抱怨的这一点。 - 数据存储利用 volatile来保证可见性。 - 使用CAS 等操作,在特定场景进行无锁并发操作。 - 使用 Unsafe、LongAdder 之类底层手段,进行极端情况的优化。 先看看现在的数据存储内部实现,我们可以发现 Key是 final的,因为在生命周期中,一个条目的Key发生变化是不可能的;与此同时 val,则声明为 volatile,以保证可见性。 ``` static class Node<K, V> implements Map.Entry<K,V>{ final int hash; final K key; volatile V val; volatile Node<K, V> next; //.. ``` 我这里就不再介绍 get 方法和构造函数了,相对比较简单,直接看并发的put 是如何实现的。 ``` final V putVal(K key, V value, boolean onlyIfAbsent){if (key == null ||value == null1)thro int hash= spread(key.hashCode(); int binCount =o; for (Node<k,V>[] tab= table;;){ Nodek,V> f; int n,i,fh; K fk; V fv; if(tab== null (n= tab.1ength)==0) tab= initTable(); else if((f= tabAt(tab,i=(n-1)& hash)== null){ //利用CAS 去进行无锁线程安全操作,如果 bin 是空的 if(casTabAt(tab, i, null, new Node<K,V>(hash,key,value))) break; )else if((fh=f.hash)==MOVED) tab= helpTransfer(tab, f); else if(onlyIfAbsent//不加锁,进行检查 && fh == hash &&((fk=f.key)== key || (fk !=null && key.equals(fk))) &&(fv =f.val)!=null) return fv; else{ V oldval=null; synchronized(f){ //细粒度的同步修改操作.. } } //Bin 超过阈值,进行树化 if(bincount !=e){ if(binCount >= TREEIFY_THRESHOLD) treeifyBin(tab,i); if(oldval !=null) return oldVal; break; } } } addCount(1L, binCount); return null; ``` 初始化操作实现在 initTable里面,这是一个典型的CAS使用场景,利用volatile的 sizeCt作为互斥手段∶如果发现竞争性的初始化,就 spin 在那里,等待条件恢复;否则利用CAS设置排他标志。如果成功则进行初始化; 否则重试。请参考下面代码∶ ``` private final Node<K,V>[] initTable(){ NodeK,V>[] tab; int sc; while(tab= table)= null l tab.length== o)( //如果发现冲突,进行 spin 等待 if(sc= sizecCtl)< 0) Thread.yield(); //CAS 成功返回 true,则进入真正的初始化逻辑 else if(U.compareAndSetInt(this,SIZECTL, sc,-1)){ if((tab=table)== nulltab.length == e){ int n=(sc>e)?sc:DEFAULT_CAPACITY; @SuppressMarnings("unchecked") Node<K,V>[]nt =(Node<K,V>[])new Node<?,?>[n]; table= tab= nt; sc=n -(n>>>2); } }finally{ sizeCtl = sc; } break; } } return tab; } ``` 当 bin 为空时,同样是没有必要锁定,也是以CAS操作去放置。 你有没有注意到,在同步逻辑上,它使用的是 synchronized,而不是通常建议的 ReentrantLock 之类,这是为什么呢?现代 JDK中,synchronized 已经被不断优化,可以不再过分担心性能差异,另外,相比于ReentrantLlock,它可以减少内存消耗,这是个非常大的优势。 与此同时,更多细节实现通过使用 Unsafe进行了优化,例如 tabAt 就是直接利用getObjectAcquire,避免间接调用的开销。 ``` static final<K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i){ return(Node<K,V>)U.getObjectAcquire(tab,((long)i << ASHIFT)+ABASE); } ``` 再看看,现在是如何实现 size操作的。阅读代码你会发现,真正的逻辑是在 sumCount 方法中,那么 sumCount做了什么呢? ``` final long sumcount(){ CounterCell[]as = counterCells; CounterCell a;long sum= baseCount; if(as !=null){ for(int i=6;i< as.length;++i){ if((a= as[1])!=null) sum += a.value; return sum; } } } ``` 我们发现,虽然思路仍然和以前类似,都是分而治之的进行计数,然后求和处理,但实现却基于一个奇怪的CounterCell。难道它的数值,就更加准确吗?数据一致性是怎么保证的? ``` static final class CounterCell{ volatile long value; CounterCell(long x){ value=x; } ``` 其实,对于CounterCell的操作,是基于java.util.concurrent.atomic.LongAdder进行的,是一种JVM利用空间换取更高效率的方法,利用了Striped64内部的复杂逻辑。这个东西非常小众,大多数情况下,建议还是使用 AtomicLong,足以满足绝大部分应用的性能需求。 从线程安全问题开始,概念性的总结了基本容器工具,分析了早期同步容器的问题,进而分析了Java7和 Java8中ConcurrentHashMap是如何设计实现的,希望ConcurrentHashMap 的并发技巧对你在日常开发可以有所帮助。
评论 (
0
)
登录
后才可以发表评论
状态
待办的
待办的
进行中
已完成
已关闭
负责人
未设置
赵伟风
zhao-weifeng
负责人
协作者
+负责人
+协作者
标签
未设置
标签管理
里程碑
01.JavaSE阶段面试题
未关联里程碑
Pull Requests
未关联
未关联
关联的 Pull Requests 被合并后可能会关闭此 issue
分支
未关联
未关联
master
开始日期   -   截止日期
-
置顶选项
不置顶
置顶等级:高
置顶等级:中
置顶等级:低
优先级
不指定
严重
主要
次要
不重要
参与者(1)
1
https://gitee.com/beike-java-interview-alliance/java-interview.git
git@gitee.com:beike-java-interview-alliance/java-interview.git
beike-java-interview-alliance
java-interview
Java技术提升库
点此查找更多帮助
搜索帮助
Git 命令在线学习
如何在 Gitee 导入 GitHub 仓库
Git 仓库基础操作
企业版和社区版功能对比
SSH 公钥设置
如何处理代码冲突
仓库体积过大,如何减小?
如何找回被删除的仓库数据
Gitee 产品配额说明
GitHub仓库快速导入Gitee及同步更新
什么是 Release(发行版)
将 PHP 项目自动发布到 packagist.org
评论
仓库举报
回到顶部
登录提示
该操作需登录 Gitee 帐号,请先登录后再操作。
立即登录
没有帐号,去注册