CAS
CAS是配合volatile
使用的重要技术,全称是Compare and Swap
,他体现的是一种乐观锁
的思想,他也被称之为一种叫无锁并发
。
之前提到要保护一些共享变量的安全性要使用syncronized
加锁,而CAS是根本不加锁,比如用CAS想对多个线程要对一个共享的整型变量执行+1
操作:
// 需要不断尝试
while(true) {
int 旧值 = 共享变量; // 比如拿到了当前值0
int 结果 = 旧值 + 1; // 在旧值0的基础上增加1,正确结果是1
/*
这时候如果别的线程把共享变量改成了5,本县城的正确结果1就作废了,这时候compareAndSwap返回false,重新尝试,直到:
compareAndSwap返回true,表示我本线程做修改的同时,别的线程没有干扰
*/
if (compareAndSwap(旧值,结果)) {
// 成功,退出循环
}
}
他首先用的是while
死循环,他会首先去把共享变量
的值读进来,从主存
读到工作内存
,赋值给‘旧值
’,接下来我想在这个线程内对他进行+1
操作,那我就在他的旧值
基础上进行+1
,赋值给‘结果
’,接下来是关键,他会调用compareAndSwap
函数,这个方法他就是会尝试把‘结果
’赋值给共享变量
,但是赋值的同时,他会把这个‘旧值
’跟共享变量
当前的值做比较,因为他怕我在写入‘结果
’的时候,有其他线程已经把共享变量
值改了,所以他得拿原先上一次他读到的值(‘旧值
’)跟这个共享变量
当前的值做比较,如果这两个是一致的,那我这个结果可以成功的写入到这个变量里(网友1:共享变量还是不是旧值,如果是,就赋新值。),但如果共享变量
当前的值,与我上一次的‘旧值
’不一样了,那说明其他线程把这个共享变量
改了(网友2:匹配不上就重试。),那改了的话我这一次操作或这一次尝试就失败了,失败了以后compareAndSwap
就会返回false
,返回false
的话就会重新进行一次while
循环,重新一次while
循环的话,他就会重新读取共享变量
的最新值,来重复刚才的过程。
可以想象一下,比如‘旧值
’假设是0
,他在‘旧值
’基础上+1
,得到了最新的‘结果
’为1
,他想把这个1
写回给共享变量,但写的时候‘旧值
’是0
,但比如共享变量
已经被别人给修改成5
了,那0
跟5
相比肯定不一致了,说明其他线程干扰了,那么这一次的修改就作废,重新循环一次,循环时重新获取最新的共享变量
的值5
,然后在5
的基础上再去+1
得到了6
,得到6
以后我再把‘旧值 5
’再跟共享变量
的当前值做比较,假设没有其他线程干扰,结果这回发现共享变量
当前值和‘旧值
’都是5
,那我就可以安全的把6
写入共享变量
。这就是CAS的机制,他采用的是一种不断尝试
的机制,直到成功的那一天
。
这里为了读到共享变量
的最新结果,比如有可能存在可见性
的问题(比如拿到的共享变量不是最新的
),所以需要使用volatile
修饰共享变量
,所以CAS一定要跟volatile
结合才能有效,否则你拿到的共享变量
值不一定是最新的,那CAS的比较机制就无从谈起了。这种方式可以实现无锁并发
,适用于竞争不激烈、多核CPU的场景下。
首先竞争不激烈
这是一个前提,如果竞争激烈
,可以想象比如说我想去用CAS修改,我刚计算好了‘结果
’就想把它set给共享变量
,结果其他线程就把他改了,那我还得重试,那我又除了一次新值,又想去改,结果别的线程又把他给变动了,那我又得循环一次…所以,若竞争激烈,那重试就会频繁发生,反而会让效率受到一定的影响。
第二点是他应该工作在多核的CPU的场景
下,因为重试操作他是要使用CPU时间的,他不像原来的synchronized
,比如synchronized
就是说你在等待的过程中他线程就阻塞
了,换句话说他就在休眠
了,那就必须等到其他线程把这个锁释放开我这个线程才能恢复运行,而我们的CAS他是要不断重试不断重试的,所以他需要利用CPU时间,如果你CPU就一个,那这个CAS就无从谈起了,因为其他的线程在修改时你想重试也没有CPU的时间可用,所以第二个条件是他必须工作在多核
的场景下,所以他是不会陷入阻塞
,这也是他效率提升的因素之一,一旦线程阻塞
,涉及到线程的上下文切换
,所谓的上下文切换
就是它得把这个线程当前的运行状态给保存下来,然后让他去一遍阻塞休眠
去,所以等到他再次唤醒,他又得把上次的状态又进行恢复,这个是很耗时的,而我们的CAS是由于线程一直在跑,他是不会陷入阻塞了,所以就没有线程的上下文切换,所以他的效率在低竞争的时候其实要比synchronized高很多。
CAS底层(实现)依赖于一个Unsafe类
来直接调用操作系统底层的CAS指令
,下面是直接使用Unsafe对象
进行线程安全保护的一个例子,Unsafe
我们并不能直接去用,而是要通过反射
的方式去拿到这个类,才能调用CAS的相关方法。当然,这个例子并不用去掌握,因为JDK
内部他封装了Unsafe
这个类的一些方法,底层会用Unsafe
去调用我们的CAS指令
。他的本意并不是让程序员自己去使用Unsafe
:(例子代码省略,因为自己没必要去实现,JDK为我们设计好了,就是原子类)
Java里的乐观锁和悲观锁
Java里的悲观锁
实际上就是指syncronized
,而Java里的乐观锁
就是指CAS
。
- CAS是基于
乐观锁
的思想:基于最乐观的估计,不怕别的线程来修改共享变量,计算改了也没关系,我吃亏点再重试呗。 - synchronized是基于
悲观锁
的思想:基于最悲观的估计,得防着其他线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
原子操作类
从JDK5
开始,新增了juc(java.util.concurrent Java并发工具包)
线程工具类,其中提供了一系列的原子操作类,可以提供线程安全的操作,例如:AtomicInteger
、AtomicBoolean
等,其中AtomicInteger
是原子整数类,他能保证整数操作的一些自增自减的线程安全,他们的底层就是采用CAS技术+volatile
来实现的。
// 创建原子整数对象,初始值为0
private static AtomicInteger i = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
// 主方法里开启了两个线程
// 线程t1循环5000次,调用原子整数对象的自增方法
Thread t1 = new Thread(()->{
for(int j = 0; j < 5000; j ++) {
i.getAndIncrement();// 获取并且自增,类似于i++
// i.incrementAndGet(); // 自增并且获取,类似于++i
}
});
// 线程t2循环5000次,自减
Thread t2 = new Thread(()->{
for(int j = 0; j < 5000; j ++) {
i.getAndDecrement();// 获取并且自减i--
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);// 0
}
他是利用了无锁并发
的方式,来保证我的原子整数类中整数信息的线程安全。