CAS乐观锁原理
1. 乐观锁和悲观锁
悲观锁
总是假设最坏的情况,为防止每次去拿数据别人修改,每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现。
Synchronized
虽然确保了线程的安全,但是在性能上却不是最优的,Synchronized
关键字会让没有得到锁资源的线程进入BLOCKED
状态,而后在争夺到锁资源后恢复为RUNNABLE
状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。
乐观锁
总是假设最好的情况,每次拿数据的时候认为别人不会更改数据,所以不对数据加锁,但是在更新数据的时候都要先判定一下这个数据在计算期间有没有被别人修改(例如:判断值是否与之前的一致),如果被修改则再次重试计算。
CAS(Compare and Swap,即比较再交换)是乐观锁的典型实现,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
2. 什么是CAS?
CAS机制
CAS机制当中使用了3个基本操作数:内存地址V,旧的值A,要修改的新值B。
CAS原理更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改。
借用别人的一段代码,感觉很形象,可以用来模拟CAS的过程:
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @author hrabbit
* 2018/07/16.
*/
public class AtomicBooleanTest implements Runnable {
private static AtomicBoolean flag = new AtomicBoolean(true);
public static void main(String[] args) {
AtomicBooleanTest ast = new AtomicBooleanTest();
Thread thread1 = new Thread(ast);
Thread thread = new Thread(ast);
thread1.start();
thread.start();
}
@Override
public void run() {
System.out.println("thread:"+Thread.currentThread().getName()+";flag:"+flag.get());
if (flag.compareAndSet(true,false)){
System.out.println(Thread.currentThread().getName()+""+flag.get());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag.set(true);
}else{
System.out.println("重试机制thread:"+Thread.currentThread().getName()+";flag:"+flag.get());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
run();
}
}
}
我们来看上面这段代码,同时启动两个线程,同时去更改AtomicBoolean
类型的属性flag
的值(通过flag.compareAndSet(true,false)
将true值更新成false)。compareAndSet的官方解释如下:
也就是说当flag的值为true的时候,会触发更新操作将flag的值更新成false。一旦有一个线程执行了这个操作,其他线程将会等待if条件成立才会去执行。
最终的输出:
thread:Thread-1;flag:true
thread:Thread-0;flag:true
Thread-1false
重试机制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重试机制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重试机制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重试机制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重试机制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重试机制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重试机制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重试机制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重试机制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重试机制thread:Thread-0;flag:false
thread:Thread-0;flag:true
Thread-0false
通过以上的输出可以看到,两个线程Thread-0和Thread-1同时执行run函数,但是Thread1先执行了flag.compareAndSet(true,false)
,更新了flag的值,这时候Thread1进入sleep,Thread0一直没有进入if的条件,当Thread1 sleep完之后,重置flag的值到true,这时候flag.compareAndSet(true,false)
的条件满足,Thread0才可以进入if执行。我们看到flag.compareAndSet(true,false)
跟flag.set(true)
相当于一把锁,锁住了中间的代码。
CAS的缺点
1.CPU开销较大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。
2.不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。
CAS对比Synchronized:
-
对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
-
对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。
补充: synchronized在jdk1.6之后,已经改进优化。synchronized的底层实现主要依靠Lock-Free的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。
Atomic类
在java.util.concurrent.atomic包下,一系列以Atomic开头的包装类都是能够保证原子性操作的。例如AtomicBoolean
,AtomicInteger
,AtomicLong
。它们分别用于Boolean
,Integer
,Long
类型的原子性操作。
先看个例子:
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
//每个线程让count自增100次
for (int i = 0; i < 100; i++) {
count.incrementAndGet();
}
}
}).start();
}
try{
Thread.sleep(2000);
}catch (Exception e){
e.printStackTrace();
}
System.out.println(count);
}
使用AtomicInteger之后,最终的输出结果可以保证是200,效果跟sychronized是一样的,但是在某些情况下,代码的性能会比Synchronized更好。
atomic操作的底层实现正是利用的CAS机制。
AtomicReference
AtomicReference类提供了一种读和写都是原子性的对象引用变量。原子意味着多个线程试图改变同一个AtomicReference(例如比较和交换操作)将不会使得AtomicReference处于不一致的状态。
AtomicReference有一个非常有用的方法是compareAndSet()。compareAndSet()方法可以将存储在AtomicReference中的引用同你的预期值做一个比较,如果他们是相同的(not equal as in equals() but same as in ==),那么在AtomicReference实例上会设置一个新的引用。
atomic的源码
我们可以看到java.util.concurrent.atomic
包下面各个类的源码都比较简单,主要利用了sun.misc.Unsafe
实现的CAS原理,其中value用volatile
声明,使得value对各个线程可见。valueOffset
为内存地址的偏移量。sun.misc.Unsafe里关于对象字段访问的方法把对象布局抽象出来,它提供了objectFieldOffset()方法用于获取某个字段相对Java对象的“起始地址”的偏移量。
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
//...省略若干行
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
sun.misc.Unsafe是JDK内部用的工具类。它通过暴露一些Java意义上说“不安全”的功能给Java层代码,来让JDK能够更多的使用Java代码来实现一些原本是平台相关的、需要使用native语言(例如C或C++)才可以实现的功能。该类不应该在JDK核心类库之外使用。
我们再来看下get和set方法:
/**
* Gets the current value.
*
* @return the current value
*/
public final int get() {
return value;
}
/**
* Sets to the given value.
*
* @param newValue the new value
*/
public final void set(int newValue) {
value = newValue;
}
我们看到get和set方法没有调用unsafe的方法。
参考文献:
https://www.imooc.com/article/details/id/44217
https://www.cnblogs.com/snow-man/p/9970523.html