一文带你搞懂Java中的CAS(乐观锁)

什么是悲观锁、乐观锁?在java语言里,总有一些名词看语义跟本不明白是啥玩意儿,也就总有部分面试官拿着这样的词来忽悠面试者,以此来找优越感,其实理解清楚了,这些词也就唬不住人了。java

  • synchronized是悲观锁,这种线程一旦获得锁,其余须要锁的线程就挂起的状况就是悲观锁。node

  • CAS操做的就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操做,若是由于冲突失败就重试,直到成功为止。web

在进入正题以前,咱们先理解下下面的代码:面试

private static int count = 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++;
}
}
}).start();
}

try{
Thread.sleep(2000);
}catch (Exception e){
e.printStackTrace();
}
System.out.println(count);
}

请问cout的输出值是否为200?答案是否认的,由于这个程序是线程不安全的,因此形成的结果count值可能小于200;算法

那么如何改形成线程安全的呢,其实咱们可使用上Synchronized同步锁,咱们只须要在count++的位置添加同步锁,代码以下:安全

private static int count = 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++) {
synchronized (ThreadCas.class){
count++;
}
}
}
}).start();
}

try{
Thread.sleep(2000);
}catch (Exception e){
e.printStackTrace();
}
System.out.println(count);
}

加了同步锁以后,count自增的操做变成了原子性操做,因此最终的输出必定是count=200,代码实现了线程安全。微信

可是Synchronized虽然确保了线程的安全,可是在性能上却不是最优的,Synchronized关键字会让没有获得锁资源的线程进入BLOCKED状态,然后在争夺到锁资源后恢复为RUNNABLE状态,这个过程当中涉及到操做系统用户模式和内核模式的转换,代价比较高。多线程

尽管Java1.6为Synchronized作了优化,增长了从偏向锁到轻量级锁再到重量级锁的过分,可是在最终转变为重量级锁以后,性能仍然较低。并发

所谓原子操做类,指的是
java.util.concurrent.atomic包下,一系列以Atomic开头的包装类。例如AtomicBoolean,AtomicInteger,AtomicLong。它们分别用于Boolean,Integer,Long类型的原子性操做。
app


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。而且在某些状况下,代码的性能会比Synchronized更好。

而Atomic操做的底层实现正是利用的CAS机制,好的,咱们切入到这个博客的正点。

什么是CAS机制

CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。

CAS机制当中使用了3个基本操做数:内存地址V,旧的预期值A,要修改的新值B。

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改成B。

CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。

CAS机制当中使用了3个基本操做数:内存地址V,旧的预期值A,要修改的新值B。

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改成B。

这样说或许有些抽象,咱们来看一个例子:

1.在内存地址V当中,存储着值为10的变量。


2.此时线程1想要把变量的值增长1。对线程1来讲,旧的预期值A=10,要修改的新值B=11。


3.在线程1要提交更新以前,另外一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。


4.线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败。


5.线程1从新获取内存地址V的当前值,并从新计算想要修改的新值。此时对线程1来讲,A=11,B=12。这个从新尝试的过程被称为自旋。


6.这一次比较幸运,没有其余线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的。


7.线程1进行SWAP,把地址V的值替换为B,也就是12。


从思想上来讲,Synchronized属于悲观锁,悲观地认为程序中的并发状况严重,因此严防死守。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();
}

}
}

输出的结果:

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-一、Thread-0都会执行if=true条件,并且还不会产生线程脏读脏写,这是如何作到的了,这就用到了咱们的compareAndSet(boolean expect,boolean update)方法 咱们看到当Thread-1在进行操做的时候,Thread一直在进行重试机制,程序原理图:

这个图中重最要的是compareAndSet(true,false)方法要拆开成compare(true)方法和Set(false)方法理解,是compare(true)是等于true后,就立刻设置共享内存为false,这个时候,其它线程不管怎么走都没法走到只有获得共享内存为true时的程序隔离方法区。

看到这里,这种CAS机制就是完美的吗?这个程序其实存在一个问题,不知道你们注意到没有?

可是这种得不到状态为true时使用递归算法是很耗cpu资源的,因此通常状况下,都会有线程sleep。

CAS的缺点:

1.CPU开销较大 在并发量比较高的状况下,若是许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。

2.不能保证代码块的原子性 CAS机制所保证的只是一个变量的原子性操做,而不能保证整个代码块的原子性。好比须要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。

   

本文分享自微信公众号 - Java学习提高(javaxuexitisheng)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索