java多线程系列3:悲观锁和乐观锁
1.悲观锁和乐观锁的基本概念
悲观锁:
- 总是认为当前想要获取的资源存在竞争(很悲观的想法),因此获取资源后会立刻加锁,于是其他线程想要获取该资源的时候就会一直阻塞直到能够获取到锁;
- 在传统的关系型数据库中,例如行锁、表锁、读锁、写锁等,都用到了悲观锁。还有java中的同步关键字Synchronized也是一种悲观锁;
乐观锁:
- 总是认为当前想要获取的资源不存在竞争(很乐观的想法),因此在获取资源后,并不会加锁;
- 但是在执行更新操作时,会判断在这期间是否有其他人更新过这个数据,可使用版本号等机制实现;
- 适用于多读的应用程序,可提高吞吐量;
- 像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
2.乐观锁的一种实现方式:CAS
因为乐观锁的思想是:在通常情况下都认为不会产生并发冲突,因此在对数据进行提交更新的时候,会对将要提交更新的数据进行并发冲突检测、如果冲突存在,则会返回错误信息给用户,让用户决定处理方式。
基于乐观锁的思想,我们可以知道乐观锁实现的步骤包含两个部分:冲突检测和数据更新,而CAS就是其中一个典型的实现方式.
CAS:Compare And Swap(比较并交换)
CAS是一种乐观锁技术。当多个线程使用CAS尝试更新同一个变量时,只有一个线程能够成功更新,其他线程都会失败,但是失败的线程并不会挂起,而是被告知在此次竞争中失败并可再次尝试。
CAS包含三个操作数:
在JDK1.5中新增的java.util.concurrent包中的内容就是建立早CAS基础之上的,相对于Synchronized的阻塞式算法,CAS其实是一种非阻塞算法的实现,因此java.util.concurrent包中组件的性能大大提升。
下面以java.util.concurrent中的AtomicInteger的getAndIncrement(该操作相当于变量自加) 为例,看一下在不加锁的情况下,如何保证线程安全:
public class AtomicInteger extends Number implements java.io.Serializable { private volatile int value; public final int get() { return value; } public final int getAndIncrement() { //自旋方式采用CAS来修改当前值,直到成功为止 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); } }
如果compareAndSet(current, next)方法成功执行,则直接返回;如果线程竞争激烈,导致compareAndSet(current, next)方法一直不能成功执行,则会一直循环等待。
3.CAS存在的问题
①. ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
②.循环时间开销大:因为CAS中存在自旋,当自旋长时间不成功时,会给CPU带来极大开销,如果CPU执行支持pause指令,效率能够得到提升。
pause指令作用1:延迟流水线执行指令,(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。
pause指令作用2:可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
③.只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
4.CAS与Synchronized的选择
1、线程冲突严重时,使用CAS等乐观锁,自旋几率较大,会因为自旋浪费更多的CPU资源;此时使用Synchronized等悲观锁性能较好。
2、线程冲突较轻时,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源,而自旋概率较小,使用CAS性能高于同步锁。