21 | 原子类:无锁工具类的典范
王宝令
该思维导图由 AI 生成,仅供参考
前面我们多次提到一个累加器的例子,示例代码如下。在这个例子中,add10K() 这个方法不是线程安全的,问题就出在变量 count 的可见性和 count+=1 的原子性上。可见性问题可以用 volatile 来解决,而原子性问题我们前面一直都是采用的互斥锁方案。
其实对于简单的原子性问题,还有一种无锁方案。Java SDK 并发包将这种无锁方案封装提炼之后,实现了一系列的原子类。不过,在深入介绍原子类的实现之前,我们先看看如何利用原子类解决累加器问题,这样你会对原子类有个初步的认识。
在下面的代码中,我们将原来的 long 型变量 count 替换为了原子类 AtomicLong,原来的 count +=1 替换成了 count.getAndIncrement(),仅需要这两处简单的改动就能使 add10K() 方法变成线程安全的,原子类的使用还是挺简单的。
无锁方案相对互斥锁方案,最大的好处就是性能。互斥锁方案为了保证互斥性,需要执行加锁、解锁操作,而加锁、解锁操作本身就消耗性能;同时拿不到锁的线程还会进入阻塞状态,进而触发线程切换,线程切换对性能的消耗也很大。 相比之下,无锁方案则完全没有加锁、解锁的性能消耗,同时还能保证互斥性,既解决了问题,又没有带来新的问题,可谓绝佳方案。那它是如何做到的呢?
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
- 深入了解
- 翻译
- 解释
- 总结
原子类是并发编程中的重要工具,通过CAS指令实现原子性操作,提高了并发性能。相比互斥锁方案,无锁方案避免了加锁、解锁操作的性能消耗,同时保证了互斥性。Java SDK提供了丰富的原子类,包括原子化的基本数据类型、对象引用类型、数组、对象属性更新器和累加器。这些原子类提供了一系列方法,如原子化的加减操作、CAS操作等。无锁方案相对于互斥锁方案,优点非常多,但需要注意ABA问题。文章还介绍了原子化的对象引用类型、原子化数组、原子化对象属性更新器和原子化的累加器的相关实现。总体而言,原子类在并发编程中发挥着重要作用,但使用时需要慎之又慎。
仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Java 并发编程实战》,新⼈⾸单¥59
《Java 并发编程实战》,新⼈⾸单¥59
立即购买
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
登录 后留言
全部留言(75)
- 最新
- 精选
- 张天屹如果线程1 运行到WMRange or = rf.get();停止,切换到线程2 更新了值,切换回到线程1,进入循环将永远比较失败死循环,解决方案是将读取的那一句放入循环里,CAS每次自旋必须要重新检查新的值才有意义
作者回复: 👍
2019-04-162111 - Geek_ebda96老师你举的这个例子,自己实现CAS是不是有点不对 class SimulatedCAS{ volatile int count; // 实现 count+=1 addOne(){ do { newValue = count+1; //① }while(count != cas(count,newValue) //② } // 模拟实现 CAS,仅用来帮助理解 synchronized int cas( int expect, int newValue){ // 读目前 count 的值 int curValue = count; // 比较目前 count 值是否 == 期望值 if(curValue == expect){ // 如果是,则更新 count 的值 count= newValue; } // 返回写入前的值 return curValue; } } 2 这里是不是应该用oldValue来比较,在do里面的时候先把count的值用oldValue保存下来,传入的参数expected为oldValue,newValue为oldValue+1 do{ oldValue = count; newValue = oldValue + 1; }while(oldValue != cas(oldValue, newValue)) 望指正
作者回复: 你的这个写法是对的👍
2019-04-181342 - 长脖子树原子类在 java 层面虽然看起来是无锁的, 但是深入到操作系统和cpu层面仍然有锁 比如像在多处理器计算机里常常会有一种 TSL 指令, (测试并加锁 test and set lock), 它是一种硬件支持的互斥方案, 执行这个指令的cpu将锁住内存总线, 以禁止其他CPU 在本指令结束之前访问内存 这个指令类似于 intel 处理器上的 lock cmpxchg , 但在近几代的实现上, 对内存总线的锁定做了类似于分段锁的优化, 仅仅锁定部分的缓存行 参考: 1. 现代操作系统 2. https://en.wikipedia.org/wiki/Compare-and-swap 3. https://stackoverflow.com/questions/11065675/lock-prefix-of-intel-instruction-what-is-the-point 4. https://cloud.tencent.com/developer/article/1189884
作者回复: 👍
2020-04-25426 - 木卫六首先,or=rf.get()需要放到do{},每次需要重新获取,以防其他线程更新过导致死循环; 然后,nr是new的,我觉得应该不会发生ABA的问题(reference的compareAndSet比较的是内存地址)。另外ABA问题应该容易发生在值类型上吧,引用类型的应该几乎不会发生?对于引用类型,几乎不会发生经过至少两次new对象,最后对象放在了同一块or之前使用的内存区块上吧?
作者回复: 我也觉得没有ABA问题
2019-04-16212 - andypublic class SafeWM { class WMRange{ final int upper; final int lower; WMRange(int upper,int lower){ // 省略构造函数实现 } } final AtomicReference<WMRange> rf = new AtomicReference<>( new WMRange(0,0) ); // 设置库存上限 void setUpper(int v){ WMRange nr; WMRange or; do{ or = rf.get(); // 检查参数合法性 if(v < or.lower){ throw new IllegalArgumentException(); } nr = new WMRange(v, or.lower); }while(!rf.compareAndSet(or, nr)); } } 这样子对吗?
作者回复: 对👍
2019-04-16412 - 郑晨Ccor是原始的 nr是new出来的 指向不同的内存地址 compareandset的结果永远返回false 结果是死循环?是不是应该用atomicfieldreference?
作者回复: 👍,不过我觉得没必要用atomicfieldreference
2019-04-16311 - 随风而逝老师,这些原子操作类在分布式程序中还有效吗?
作者回复: 无效
2019-04-3037 - 刘志兵老师,compareAndSwapLong方法是一个native方法,比较共享变量和expect值是否相等,相等才设置新的值x, 不明白这里的对比是怎么保证原子性的,对比也是要再读一次共享变量,然后对比吧,如果先读出来之后对比的时候被其他线程修改了,那还是会有问题
作者回复: 最终依赖的是cpu提供的原子指令,不用我们操心。
2019-04-177 - 刘育飞不明白 synchronized int cas() 这不是已经用了 同步synchronized 关键字 吗怎么会 无锁 无堵塞呢
作者回复: cas大部分都是CPU提供的指令,这里只是模拟它的原理
2019-10-176 - 忍者无敌1995例子中的模拟CAS,cas函数是加了锁的,保证整个操作的原子性;我的理解是这个只是一个模拟,实际中肯定不会加上锁的
作者回复: 👍
2019-05-026
收起评论