简介
原先所学的加锁,例如synchronized,ReetrantLock这些加锁方式都是悲观锁,即当前线程对加锁的内容进行操作时,其它的线程只能等待该线程锁住的内容执行完成之后,才可以获得锁,最终执行自己的业务。
而今天所学的cas,是一种乐观锁的机制。cas,全拼是compare and set,即先比较后赋值(cas是个原子操作)。即当线程修改共享变量的时候,并不会像悲观锁一样,控制着不让其它线程修改当前变量,可以这么理解:
1.线程A先获取到了主存值1,拷贝至工作内存
2.线程B获取到主存值1,拷贝至工作内存
3.线程B对工作内存的值1进行修改为2,并在主存中进行赋值
4.线程A修改工作内存值为3,在往主存里试图写时,发现原先预期的值是1,现在变化成2了,不会,从主存中重新拉取值
5.从主存中拉取了2,然后再次修改工作内存 值为 3,在往主存里试图写时,发现预期的值是2,主存现在的值也是2,没有问题,对内容进行写入。
转账例子
就设置一个金额,有两个线程对其进行转账,一个是往出转,另一个往内部转。
加锁方面的代码的话,很简单,直接在转账金额方面的代码加锁就可以了。
package com.bo.threadstudy.six;
import com.bo.threadstudy.two.ReentrantLockTest;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
public class TransferTest01 {
public static void main(String[] args) throws InterruptedException {
Transfer01 transfer01 = new Transfer01(10000);
int loopNum = 1000;
//理解为主线程等待两个线程执行完成
CountDownLatch countDownLatch = new CountDownLatch(2);
new Thread(() -> {
for (int i = 0; i < loopNum; i++) {
transfer01.addPrice(200);
}
countDownLatch.countDown();
}).start();
new Thread(() -> {
for (int i = 0; i < loopNum; i++) {
transfer01.subjectPrice(200);
}
countDownLatch.countDown();
}).start();
countDownLatch.await();
//没有线程安全问题,应该是1万
log.debug("最终转账后的结果"+transfer01.getMoney());
}
}
@Slf4j
class Transfer01{
private int price;
private ReentrantLock lock = new ReentrantLock();
public Transfer01(int price) {
this.price = price;
}
public int getMoney(){
return price;
}
public void addPrice(int money){
lock.lock();
try{
price = price+money;
}finally {
lock.unlock();
}
}
public void subjectPrice(int money){
lock.lock();
try{
if(price < money){
log.debug("金额不足");
return ;
}
price = price - money;
}finally {
lock.unlock();
}
}
}
那么用CAS的方式来看看。
package com.bo.threadstudy.six;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
public class TransferTest02 {
public static void main(String[] args) throws InterruptedException {
Transfer02 transfer02 = new Transfer02(new AtomicInteger(1000));
int loopNum = 1000;
//理解为主线程等待两个线程执行完成
CountDownLatch countDownLatch = new CountDownLatch(2);
new Thread(() -> {
for (int i = 0; i < loopNum; i++) {
transfer02.addPrice(200);
}
countDownLatch.countDown();
}).start();
new Thread(() -> {
for (int i = 0; i < loopNum; i++) {
transfer02.subjectPrice(200);
}
countDownLatch.countDown();
}).start();
countDownLatch.await();
//没有线程安全问题,应该是1万
log.debug("最终转账后的结果"+transfer02.getMoney());
}
}
/**
* 这里采用CAS来做,锁的粒度较小,更为轻量,AtomicInteger
*/
@Slf4j
class Transfer02{
private AtomicInteger price;
public Transfer02(AtomicInteger price) {
this.price = price;
}
public int getMoney(){
return price.get();
}
public void addPrice(int money){
while(true){
int curPirce = price.get();
if(price.compareAndSet(curPirce,curPirce+money)){
//金额替换成功,退出循环
break;
}
}
}
public void subjectPrice(int money){
while(true){
int curPirce = price.get();
if(curPirce < money){
log.debug("金额不足");
break;
}
//在这一步如果发生了指令交错,那么会再走一次循环,不会出现问题
if(price.compareAndSet(curPirce,curPirce-money)){
break;
}
}
}
}
AtomicInteger,是一个原子整数类型。可以这么理解,他内部的一系列的方法,如果不在线程中使用组合这些方法的情况下,那么这些方法都是线程安全的。
看一下compareAndSet方法是怎么操作的。打个断点看一下,此时预期的值是200800。
采用idea的debug功能中的setValue对这个预期值进行一下改动。我现在将其改为了201000值。
此时现在的代码是这样的,price中主存的值改为了201000,但是当前线程中的预期值是200800,修改后的值为200600,不符合规则,也就不会break结束这个循环,而是继续调用循环,重新从主存中获取。
在源码中,采用了volatile关键字来保证了可以及时写入主存以及读取主存的内容。
获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。
它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。
CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果。
为什么CAS无锁效率高
无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。打个比喻
线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速... 恢复到高速运行,代价比较大
但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换
可以理解为,在加锁的情况下,线程之间会出现上下文切换,这个过程会极其的损耗性能。而如果使用CAS的话,所有的线程此时都在执行,没有线程阻塞的现象发生,性能也比较快。但是并不是说CAS没有缺点,举个例子,现在有许多个线程在CAS使用,就有一个可能,某个线程他经历了很多次循环,但是每次都是预期值与主存中的实际值不同,所以只能一直循环重复获取,无法结束,从而占用CPU,损耗性能。而其它的线程,如果线程数量过多,很可能出现线程饥饿的现象,最终性能上不去。
所以,CAS实际应用的场景应该是,线程数量较少,多核CPU的情况,这种情况下,使用CAS可以提升效率。
CAS特点
结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再
重试呗。
synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想
改,我改完了解开锁,你们才有机会。
CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思
因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响