悲观锁
假设是每次获取数据都认为会被其他线程修改,每次来操作数据(可读、可写)的时候都会加锁;悲观锁的实现是Synchronized。
悲观锁存在问题:
1、多线程竞争,引起性能问题 (锁的释放和获取需要上下文切换和调度等)
2、一个线程获取锁会导致其他线程被挂起(阻塞)
乐观锁
假设对数据的操作一般都不会发生冲突,读取操作时,不会加锁,在对数据进行变更操作是,才来检测当前的数据是否发送冲突,发生冲突返回错误信息,让用户类决定如何做乐观锁阐述的思路,主要有两个步骤操作:冲突检测和数据更新,该方式的实现就是CAS是乐观锁。
CAS
CAS(Compare And Set),多个线程通过CAS尝试修改同一个变量,只有一个线程在同一时刻进行修改,而其他的操作失败,失败的线程不会挂起,告诉失败的线程可以再次尝试。
CAS操作涉及到三个操作数:需要读写的内存位置(V)、进行比较的预期的原值(A)、待写入的新值(B)。
第一步:获取位置V的值A
第二步:将A和B同时进行处理,将A和位置V存储值进行比较,如果相等,则将位置V的值由A变更为B,则操作成功;不相等,则不变更为B,继续循环进入第一步。
如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B.否则处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。(在 CAS的一些特殊情况下将仅返回CAS是否成功,而不提取当前值。) CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。
CAS引发的ABA问题
假设存在以下线程操作序列:
1、线程1从内存位置V中取得A
2、线程2从内存位置V获取A
3、线程2进行一些操作,将A修改为其他的结果,将B修改为A
4、线程2将A再次写入到位置V
5、线程1进行CAS操作,发现位置V中任然是A,直接修改为B,操作成功
6、尽管线程1操作成功,但并不该表该过程没有问题,对于线程1而言,线程2 的修改以导致数据丢失
举例说明ABA问题:
1、现有一个单向链表实现的堆栈,栈顶为A,线程1获取到A.next为B,线程1希望通过CAS操作将栈顶替换为B
2、在线程1执行CAS操作之前,线程2来执行,将A、B出站,在依次入栈D、C、A,而对象B处于游离状态
3、此时线程1执行CAS操作,检测栈顶为A,CAS成功执行,栈顶为B,实际是B.next = null,此时堆栈只有一个B,C和D组成的链表不在堆栈中,C\D 被丢弃了
ABA问题的解决方案
ABA问题的解决需要使用版本号,在变量前加上版本号,每次变量的变更操作版本号+1,那么A-B-A就变成1A-2B-3A。
使用CAS会引发的问题
CAS虽然比Java中提供的锁的开销小,但是存在问题
- ABA问题 ABA问题通过版本号解决
- 循环时间长开销大 自旋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操作