问题
在JDK 5之前Java语言是靠synchronized关键字保证同步
的,这会导致有锁
锁机制存在以下问题:
(1)在多线程竞争下,加锁、释放锁 会导致比较多的 上下文切换 和 调度延时,引起性能问题。
(2)一个线程持有锁会导致 其它所有需要此锁 的 线程挂起。
(3)如果 一个优先级高的线程 等待一个 优先级低的线程 释放锁会导致优先级倒置,引起性能风险。
volatile是不错的机制,但是volatile不能保证原子性。因此对于同步最终还是要回到锁机制上来。
独占锁是一种悲观锁,synchronized就是一种独占锁
,会导致其它所有需要锁的线程挂起
,等待持有锁的线程释放锁
。
而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止
。乐观锁用到的机制就是CAS,Compare and Swap。
什么是CAS
CAS,compare and swap的缩写,中文翻译成比较并交换。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)
。 如果内存位置的值
与预期原值
相匹配,那么处理器会自动将该位置值更新为新值
。否则,处理器不做任何操作。
无论哪种情况,它都会在 CAS 指令之前返回该位置的值
。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前 值。)CAS 有效地说明了“我认为位置 V 的值以前是 A;如果是该A,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。
”
通常将 CAS 用于同步的方式
是从地址 V 读取值 A,执行多步计算来获得新 值 B
,然后使用 CAS 将 V 的值从 A 改为 B
。如果 V 处的值尚未同时更改,则 CAS 操作成功。
类似于 CAS 的指令允许算法执行读-修改-写
操作,而无需害怕其他线程同时 修改变量,因为如果其他线程修改变量,那么 CAS 会检测它
(并失败),算法 可以对该操作重新计算
。
CAS的目的
利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法
。其它原子操作都是利用类似的特性完成的
。而整个J.U.C都是建立在CAS之上
的,因此对于synchronized阻塞算法,J.U.C在性能上有了很大的提升。
CAS存在的问题
CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。
ABA问题
因为CAS需要在操作值的时候检查下值有没有发生变化
,如果没有发生变化则更新
但是如果一个值原来是A,变成了B,又变成了A
,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了
。ABA问题的解决思路就是使用版本号
。在变量前面追加上版本号
,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A
。
循环时间长开销大。
自旋CAS如果长时间不成功
,会给CPU带来非常大的执行开销
。
如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
只能保证一个共享变量的原子操作。
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作
但是对多个共享变量操作时
,循环CAS就无法保证操作的原子性,这个时候就可以用锁
,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij
。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。