在深刻理解乐观锁与悲观锁一文中咱们介绍过锁。本文在这篇文章的基础上,深刻分析一下乐观锁的实现机制,介绍什么是CAS、CAS的应用以及CAS存在的问题等。html
众所周知,Java是多线程的。可是,Java对多线程的支持实际上是一把双刃剑。一旦涉及到多个线程操做共享资源的状况时,处理很差就可能产生线程安全问题。线程安全性多是很是复杂的,在没有充足的同步的状况下,多个线程中的操做执行顺序是不可预测的。java
Java里面进行多线程通讯的主要方式就是共享内存的方式,共享内存主要的关注点有两个:可见性和有序性。加上复合操做的原子性,咱们能够认为Java的线程安全性问题主要关注点有3个:可见性、有序性和原子性。算法
Java内存模型(JMM)解决了可见性和有序性的问题,而锁解决了原子性的问题。这里再也不详细介绍JMM及锁的其余相关知识。可是咱们要讨论一个问题,那就是锁究竟是不是有利无弊的?安全
Java在JDK1.5以前都是靠synchronized
关键字保证同步的,这种经过使用一致的锁定协议来协调对共享状态的访问,能够确保不管哪一个线程持有共享变量的锁,都采用独占的方式来访问这些变量。独占锁其实就是一种悲观锁,因此能够说synchronized
是悲观锁。多线程
悲观锁机制存在如下问题:并发
在多线程竞争下,加锁、释放锁会致使比较多的上下文切换和调度延时,引发性能问题。性能
一个线程持有锁会致使其它全部须要此锁的线程挂起。this
若是一个优先级高的线程等待一个优先级低的线程释放锁会致使优先级倒置,引发性能风险。spa
而另外一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操做,若是由于冲突失败就重试,直到成功为止。.net
与锁相比,volatile
变量是一个更轻量级的同步机制,由于在使用这些变量时不会发生上下文切换和线程调度等操做,可是volatile
不能解决原子性问题,所以当一个变量依赖旧值时就不能使用volatile
变量。所以对于同步最终仍是要回到锁机制上来。
乐观锁( Optimistic Locking
)实际上是一种思想。相对悲观锁而言,乐观锁假设认为数据通常状况下不会形成冲突,因此在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,若是发现冲突了,则让返回用户错误的信息,让用户决定如何去作。
上面提到的乐观锁的概念中其实已经阐述了他的具体实现细节:主要就是两个步骤:冲突检测和数据更新。其实现方式有一种比较典型的就是Compare and Swap(CAS
)。
CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知此次竞争中失败,并能够再次尝试。
CAS 操做包含三个操做数 —— 内存位置(V)、预期原值(A)和新值(B)。若是内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。不然,处理器不作任何操做。不管哪一种状况,它都会在 CAS 指令以前返回该位置的值。(在 CAS 的一些特殊状况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;若是包含该值,则将 B 放到这个位置;不然,不要更改该位置,只告诉我这个位置如今的值便可。”这其实和乐观锁的冲突检查+数据更新的原理是同样的。
这里再强调一下,乐观锁是一种思想。CAS是这种思想的一种实现方式。
在JDK1.5 中新增java.util.concurrent
(J.U.C)就是创建在CAS之上的。相对于对于synchronized
这种阻塞算法,CAS是非阻塞算法的一种常见实现。因此J.U.C在性能上有了很大的提高。
咱们以java.util.concurrent
中的AtomicInteger
为例,看一下在不使用锁的状况下是如何保证线程安全的。主要理解getAndIncrement
方法,该方法的做用至关于 ++i
操做。
public class AtomicInteger extends Number implements java.io.Serializable { private volatile int value; public final int get() { return value; } public final int getAndIncrement() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return current; } } public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); } }
getAndIncrement
采用了CAS操做,每次从内存中读取数据而后将此数据和+1
后的结果进行CAS操做,若是成功就返回结果,不然重试直到成功为止。而compareAndSet
利用JNI来完成CPU指令的操做。在没有锁的机制下须要字段value要借助volatile原语,保证线程间的数据是可见的。这样在获取变量的值的时候才能直接读取。而后来看看++i
是怎么作到的。
CAS会致使“ABA问题”。
CAS算法实现一个重要前提须要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差类会致使数据的变化。
好比说一个线程one从内存位置V中取出A,这时候另外一个线程two也从内存中取出A,而且two进行了一些操做变成了B,而后two又将V位置的数据变成A,这时候线程one进行CAS操做发现内存中仍然是A,而后one操做成功。尽管线程one的CAS操做成功,可是不表明这个过程就是没有问题的。
部分乐观锁的实现是经过版本号(version
)的方式来解决ABA问题,乐观锁每次在执行数据的修改操做时,都会带上一个版本号,一旦版本号和数据的版本号一致就能够执行修改操做并对版本号执行+1
操做,不然就执行失败。由于每次操做的版本号都会随之增长,因此不会出现ABA问题,由于版本号只会增长不会减小。
Java中的线程安全问题相当重要,要想保证线程安全,就须要锁机制。锁机制包含两种:乐观锁与悲观锁。悲观锁是独占锁,阻塞锁。乐观锁是非独占锁,非阻塞锁。有一种乐观锁的实现方式就是CAS ,这种算法在JDK 1.5中引入的java.util.concurrent
中有普遍应用。可是值得注意的是这种算法会存在ABA问题。
另外,CAS还有一个应用,那就是在JVM建立对象的过程当中。对象建立在虚拟机中是很是频繁的。即便是仅仅修改一个指针所指向的位置,在并发状况下也不是线程安全的,可能正在给对象A分配内存空间,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的状况。解决这个问题的方案有两种,其中一种就是采用CAS配上失败重试的方式保证更新操做的原子性。