目录
一、什么是CAS
二、乐观锁与悲观锁
1.乐观锁出现原因
2.乐观锁
3.乐观锁的实现机制---CAS
三、JAVA对CAS的支持
首先演示实际的操作
上述过程的内部原理(java层面)
四、CAS缺陷
1.ABA问题
解决ABA问题
2.循环时间长开销大
3.只能保证一个变量的原子操作
4.解决方式
总结
CAS的全称为Compare-And-Swap ,它是一条CPU同步原语,是一种硬件对并发的支持。它的功能是判断内存某个位置的值是否为预期值,如果是则更新为新的值,这个过程是原子的。
CAS并发原语体现在Java语言中就是sun.misc.UnSafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现CAS汇编指令。这是一种完全依赖于硬件功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用于范畴,是由若干条指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许中断,也就是说CAS是一条原子指令,不会造成所谓的数据不一致的问题。
CAS加volatile关键字是实现并发包的基石。没有CAS就不会有并发包,synchronized是一种独占锁、悲观锁,java.util.concurrent中借助了CAS指令实现了一种区别于synchronized的一种乐观锁。
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样当第二个线程想拿这个数据的时候,第二个线程会一直堵塞,直到第一个释放锁,他拿到锁后才可以访问。传统的数据库里面就用到了这种锁机制,例如:行锁,表锁,读锁,写锁,都是在操作前先上锁。java中的synchronized的实现也是一种悲观锁。
乐观锁:乐观锁概念为,每次拿数据的时候都认为别的线程不会修改这个数据,所以不会上锁,但是在更新的时候会判断一下在此期间别的线程有没有修改过数据,乐观锁适用于读操作多的场景,这样可以提高程序的吞吐量。在Java中java.util.concurrent.atomic包下面的原子变量就是使用了乐观锁的一种实现方式CAS实现。
java在1.5之前都是靠synchronized关键字保证同步,synchronized保证了无论哪个线程持有共享变量的锁,都会采用独占的方式来访问这些变量。这种情况下:
1.在多线程竞争下,加锁、释放锁会导致较多的上下文切换和调度延时,引起性能问题
2.如果一个线程持有锁,其他的线程就都会挂起,等待持有锁的线程释放锁。
3.如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能风险。
对比于悲观锁的这些问题,另一个更加有效的锁就是乐观锁。 乐观锁就是:每次不加锁而是假设没有并发冲突去操作同一变量,如果有并发冲突导致失败,则重试直至成功。
乐观锁( Optimistic Locking )在上文已经说过了,其实就是一种思想。相对悲观锁而言,乐观锁假设认为数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。
乐观锁是一种思想,CAS只是这种思想的一种实现方式。
乐观锁主要就是两个步骤:冲突检测和数据更新。当多个线程尝试使用CAS同时更新同一个变量时,只有一个线程可以更新变量的值,其他的线程都会失败,失败的线程并不会挂起,而是告知这次竞争中失败了,并可以再次尝试。
CAS操作包括三个操作数:需要读写的内存位置(V)、预期原值(A)、新值(B)。如果内存位置与预期原值的A相匹配,那么将内存位置的值更新为新值B。如果内存位置与预期原值的值不匹配,那么处理器不会做任何操作。
下面通过看下并发包中的原子操作类AtomicInteger来看下,如何在不使用锁的情况下保证线程安全,主要看下getAndIncrement方法,相当于i++的操作:
public class Test2 {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(45);
//返回的是原来的值
int andIncrement = atomicInteger.getAndIncrement();
System.out.println(andIncrement); //输出45
System.out.println(atomicInteger); //输出计算后的值46
}
}
运行结果:
public class AtomicInteger extends Number implements java.io.Serializable {
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
// 返回指定的变量value在所属类中的内存偏移地址
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//保证了内存可见性
private volatile int value;
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
}
下面就是Unsafe类中的方法
// var1表示这个unsafe对象的地址
// var2表示返回指定的变量在所属类中的内存偏移地址
//var4表示要加的数,所以在这里为1
//var5表示读到对应地址的值
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
//自旋
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public native int getIntVolatile(Object var1, long var2);
首先value使用了volatile修饰,这也就保证了他的可见性与有序性。
假设内存中有一个值为A的变量,存储在地址V中
此时有三个线程想使用CAS的方式更新这个变量的值,每个线程的执行时间有略微偏差。线程1和线程2已经获取当前值,线程3还未获取当前值。
接下来,线程1先一步执行成功,把当前值成功从A更新为B;同时线程2因为某种原因被阻塞住,没有做更新操作;线程3在线程1更新之后,获取了当前值B。
在之后,线程2仍然处于阻塞状态,线程3继续执行,成功把当前值从B更新成了A。
最后,线程2终于恢复了运行状态,由于阻塞之前已经获得了“当前值A”,并且经过compare检测,内存地址V中的实际值也是A,所以成功把变量值A更新成了B。
上述就是ABA问题,他的问题在于此时获取的A值已经不是原来的A值了,会对结果有很大影响。
真正要做到严谨的CAS机制,我们在compare阶段不仅要比较期望值A和地址V中的实际值,还要比较变量的版本号是否一致。
我们仍然以刚才的例子来说明,假设地址V中存储着变量值A,当前版本号是01。线程1获取了当前值A和版本号01,想要更新为B,但是被阻塞了。
这时候,内存地址V中变量发生了多次改变,版本号提升为03,但是变量值仍然是A。
随后线程1恢复运行,进行compare操作。经过比较,线程1所获得的值和地址的实际值都是A,但是版本号不相等,所以这一次更新失败。
在Java中,AtomicStampedReference类就实现了用版本号作比较额CAS机制。
自旋CAS如果一直循环执行,一直不成功,会给CPU带来非常大的执行开销。很多时候,CAS思想体现,是有个自旋次数的,就是为了避开这个耗时问题~
这里注意一下子,如果只是单纯的使用compareAndSet(expect,update),内部只是调用了一次unsafe.compareAndSwapInt()方法,这是没有自旋的,返回的结果要么是true,要么是false。而对于上述讲的操作类AtomicInteger,它相当于是实现了自旋锁,如果没有就会一直循环 unsafe.compareAndSwapInt()方法。
CAS 保证的是对一个变量执行操作的原子性,如果对多个变量操作时,CAS 目前无法直接保证操作的原子性的。
1.使用互斥锁来保证原子性;
2.将多个变量封装成对象,通过AtomicReference来保证原子性。
内部的源码:
private volatile V value;
就是使用泛型来接收多个变量的对象。
加油哦~~推荐小伙伴去看一看源码会更好的了解,源码也不是太难~~~