博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
非阻塞同步之 CAS
阅读量:6931 次
发布时间:2019-06-27

本文共 2559 字,大约阅读时间需要 8 分钟。

hot3.png

为解决线程安全问题,互斥同步相当于以时间换空间。多线程情况下,只有一个线程可以访问同步代码。这种同步也叫阻塞同步(Blocking Synchronization).

这种同步属于一种悲观并发策略。认为只要不同步,共享数据就会被并发访问。随着硬件指令集的发展,我们可以采用基于冲突检测的乐观并发策略。

先进行操作,如果没有其他线程操作共享数据,就操作成功;否则采取补偿措施,去重试直到成功。这种策略不需要把线程挂起,因此称为非阻塞同步。(Non-Blocking Synchrinization)

要保证两个操作的原子性,需要借助处理器指令来完成。这类指令有:

  1. 测试并设置(test-and-set)

  2. 获取并增加(fetch-and-increment)

  3. 交换(swap)

  4. 比较并交换(compare-and-swap,简称 CAS)

  5. 加载链接、条件存储(load-linked/store-conditional)

CAS 指令需要有 3 个操作数,分别为内存位置(V,变量的内存地址),旧的预期值(A),和新值(B)。CAS 指令执行时,当且仅当 V 的值复合旧的预期值时,处理器使用 B 更新 V 值。否则不更新,上述操作是一个原子操作。

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”这其实和乐观锁的冲突检查+数据更新的原理是一样的。

Java 中的 CAS 操作:

public class CasTest {    //private Integer count = 0;    private final AtomicInteger count = new AtomicInteger(0);    private ThreadPoolExecutor executor = new ThreadPoolExecutor(4, 20,            10, TimeUnit.MINUTES, new ArrayBlockingQueue
(100)); private void increase() { // count++; count.incrementAndGet(); } @Test public void testMutiThreadAdd() { for (int i = 0; i < 5; i++) { executor.execute(() -> { for (int j = 0; j < 1000; j++) { increase(); } }); } try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(count); }}

使用 atomicInteger 后,每次都能输出一致的结果。increamentAndGet( ) 通过 CAS 保证了 自增操作的原子性;

CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作

1.  ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。

Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

2. 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

3. 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作.

参考: 深入理解Java 虚拟机

转载于:https://my.oschina.net/freedemon/blog/1834417

你可能感兴趣的文章
MySQL生产库主从重新同步操作注意事项
查看>>
Dive Into Docker
查看>>
全代码编写的iPhone界面源码1 UIView UILabel UIButton
查看>>
OpenSSL
查看>>
产品经理应该扮演的几种角色
查看>>
HTML5游戏开发进阶指南(亚马逊5星畅销书,教你用HTML5和JavaScript构建游戏!)...
查看>>
全程软件测试之测试需求分析与计划(2)
查看>>
“用 CompletableFuture 处理异步超时” 阅读总结
查看>>
plsql的程序包package
查看>>
我的友情链接
查看>>
检测某个输入检测输入的值是否是汉字(jQuery插件版本)
查看>>
window运行命令大全
查看>>
谷歌推Android版Chrome浏览器 无缝同步PC版
查看>>
U-Mail邮件网关内控外防令垃圾病毒无缝可钻
查看>>
Web开发者需养成的好习惯有哪些
查看>>
学会说服自己
查看>>
linux下安装oracle11g
查看>>
MySQL的btree索引和hash索引的区别
查看>>
图论(四)图的拓扑排序
查看>>
jQuery几种隐藏span的方法
查看>>