volatile是不错的机制,但是volatile不能保证原子性。因此对于同步最终还是要回到锁机制上来。为了保证原子性操作就引出了CAS
独占锁是一种悲观锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁用到的机制就是CAS,Compare and Swap
一、什么是CAS
CAS,compare and swap的缩写,翻译成比较并交换(底层的思想用到的乐观锁,也称自旋锁)
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”
通常将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来获得新 值 B,然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。
//比较并交换 compareAndSet
public class Cas {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(1);
//期望更新
//public final boolean compareAndSet(int expect, int update)
//如果我们的期望值达到了那我们就更新否则就不更新 CAS是CPU的并发原理
System.out.println(atomicInteger.compareAndSet(1, 2));
System.out.println(atomicInteger.get());
atomicInteger.getAndIncrement();//i++
System.out.println(atomicInteger.compareAndSet(1, 1));
System.out.println(atomicInteger.get());
}
}
输出:
true
2
false
3
Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
注意:Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务
二、CAS存在的问题
CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作
//比较并交换 compareAndSet
public class Cas {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(1);
//ABA问题 线程1
System.out.println(atomicInteger.compareAndSet(1, 2));
System.out.println(atomicInteger.get());
//捣乱的线程 线程2
System.out.println(atomicInteger.compareAndSet(2, 1));
System.out.println(atomicInteger.get());
//线程3
System.out.println(atomicInteger.compareAndSet(1, 66));
System.out.println(atomicInteger.get());
//线程1把1修改为2后线程2又把2修改为1,但是线程3完全不知道1被动过
}
}
输出:
true
2
true
1
true
66
举个例子:
怎么解决ABA问题——原子引用
public class CasReference {
//AtomicReference 注意,如果泛型是一个包装类,注意对象的引用问题
//参数1 值 参数2 版本号
static AtomicStampedReference<Integer> atReference = new AtomicStampedReference<>(1,1);
public static void main(String[] args) {
new Thread(()->{
int stamp = atReference.getStamp();//获取版本号
System.out.println("a1=>"+stamp);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atReference.compareAndSet(1, 2, atReference.getStamp(), atReference.getStamp() + 1));
System.out.println("a2=>"+atReference.getStamp());
System.out.println(atReference.compareAndSet(2,3,atReference.getStamp(),atReference.getStamp()+1));
System.out.println( "a3=>"+atReference.getStamp());
},"a").start();
new Thread(()->{
int stamp = atReference.getStamp();//获取版本号
System.out.println("b1=>"+stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atReference.compareAndSet(1, 2, stamp, stamp + 1));
System.out.println("b2=>"+atReference.getStamp());
},"b").start();
}
}
线程b就会修改失败,因为版本号已经发生改变
循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
CAS 与 Synchronized 的对比:
synchronized 是悲观的,它假设更新都是可能冲突的,所以要先获取锁,得到锁才更新,它是阻塞式算法,得不到锁就进入锁池等待。
CAS 是乐观的,它假设冲突比较少,但使用CAS 更新,进行冲突检测,如果确实冲突就继续尝试直到成功,它是非阻塞式算法,有更新冲突就重试