声明:本文为作者原创,如若转发,请指明转发地址
1、CAS是什么?
interface Account {
// 获取余额
Integer getBalance();
// 取款
void withdraw(Integer amount);
/**
* 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
* 如果初始余额为 10000 那么正确的结果应当是 0
*/
static void demo(Account account) {
List<Thread> ts = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
ts.add(new Thread(() -> {
account.withdraw(10);
}));
}
long start = System.nanoTime();
ts.forEach(Thread::start);
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.nanoTime();
System.out.println(account.getBalance()
+ " cost: " + (end - start) / 1000_000 + " ms");
}
}
//下面的方法时线程不安全的
class AccountUnsafe implements Account {
private Integer balance;
public AccountUnsafe(Integer balance) {
this.balance = balance;
}
@Override
public Integer getBalance() {
return this.balance;
}
@Override
public void withdraw(Integer amount) {
this.balance -= amount;
}
}
//测试类
public class TestAccount {
public static void main(String[] args) {
Account account = new AccountUnsafe(10000);
Account.demo(account);
}
}
1、解决方法:使用synchronized解决线程安全问题
//对成员变量使用同步保证线程安全
class AccountUnsafe implements Account {
private Integer balance;
public AccountUnsafe(Integer balance) {
this.balance = balance;
}
@Override
public Integer getBalance() {
synchronized (this) {
return this.balance;
}
}
@Override
public void withdraw(Integer amount) {
synchronized (this) {
this.balance -= amount;
}
}
}
2、解决方法:使用无锁CAS解决线程安全问题
//使用无锁的方式也能保证线程安全
class AccountCas implements Account {
private AtomicInteger balance;
public AccountCas(int balance) {
this.balance = new AtomicInteger(balance);
}
@Override
public Integer getBalance() {
return balance.get();
}
@Override
public void withdraw(Integer amount) {
while (true) {
// 获取余额的最新值
int prev = balance.get();
// 修改余额
int next = prev - amount;
// 真正修改
/*
compareAndSet 正是做这个检查,在 set 前,先比较 prev 与当前值
- 不一致了,next 作废,返回 false 表示失败
比如,别的线程已经做了减法,当前值已经被减成了 990
那么本线程的这次 990 就作废了,进入 while 下次循环重试
- 一致,以 next 设置为新值,返回 true 表示成功
*/
if (balance.compareAndSet(prev, next)) {
break;
}
}
}
}
其中的关键是 compareAndSet,它的简称就是 CAS (也有 Compare And Swap 的说法),它必须是原子操作。
CAS有三个操作数,旧值prev,主存中的新值,要更改成的新值next。当且仅当旧值prev和主存中的新值一致时,才会将主存中的值更改为新值next,否则什么也不做。
上面的流程图:线程1和线程2同时更新同一变量Account对象的值
(1) 线程1和线程2从主存中读取Account=100到自己的工作内存中
(2) 线程1将Account=100修改为90,并将结果同步到主存中(写屏障的原因),因此此时主存中Account=90
(3) 线程2向要通过CAS操作将Account变量的值修改为90,于是在set之前就会重新读取主存的值(读屏障的原因)并与工作内存中的值进行比较,如果相同就说明没有其他线程更改共享数据,成功的将主存中的值修改为90,但是如果不一致就说明CAS失败,此时就会进行下一次CAS操作(CAS自旋,前提在while循环内)
具体执行流程图:
2、CAS需要volatile的支持
获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。
注意:
volatile 仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原
子性)
CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果
public class AtomicInteger extends Number implements java.io.Serializable {
private volatile int value;
public AtomicInteger(int initialValue) {
value = initialValue;
}
}
3、为什么CAS+volatile效率更高?
无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时
候,发生上下文切换,进入阻塞。
打个比喻:
线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,
等被唤醒又得重新打火、启动、加速… 恢复到高速运行,代价比较大
但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑
道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还
是会导致上下文切换。
结合 CAS + volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再
重试呗。
synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想
改,我改完了解开锁,你们才有机会。
CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思。因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一,但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响。
CAS缺点:
- CPU开销较大,多线程反复尝试更新某一个变量的时候容易出现;
- 不能保证代码块的原子性,只能保证变量的原子性操作;
- ABA问题
4、AtomicInteger?
注意:如果是这个问题,就是在问你CAS,你只需要将CAS讲一遍,然后再加上本类的特点即可。
class AccountCas implements Account {
private AtomicInteger balance;
public AccountCas(int balance) { this.balance = new AtomicInteger(balance);}
@Override
public Integer getBalance() {return balance.get();}
@Override
public void withdraw(Integer amount) {
while (true) {
int prev = balance.get();
int next = prev - amount;
// 真正修改(因为该变量balance是AtomicInteger类型的,因此可以调用该方法)
if (balance.compareAndSet(prev, next)) {
break;
}
}
}
}
除了AtomicInteger类中的CAS方法外,还有其他封住的一些比较方便的方法(CAS需要放在while循环中):
AtomicInteger i = new AtomicInteger(0);
// 获取并自增(i = 0, 结果 i = 1, 返回 0),类似于 i++
System.out.println(i.getAndIncrement());
// 自增并获取(i = 1, 结果 i = 2, 返回 2),类似于 ++i
System.out.println(i.incrementAndGet());
// 自减并获取(i = 2, 结果 i = 1, 返回 1),类似于 --i
System.out.println(i.decrementAndGet());
// 获取并自减(i = 1, 结果 i = 0, 返回 1),类似于 i--
System.out.println(i.getAndDecrement());
// 获取并加值(i = 0, 结果 i = 5, 返回 0)
System.out.println(i.getAndAdd(5));
// 加值并获取(i = 5, 结果 i = 0, 返回 0)
System.out.println(i.addAndGet(-5));
// 获取并更新(i = 0, p 为 i 的当前值, 结果 i = -2, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.getAndUpdate(p -> p - 2));
// 更新并获取(i = -2, p 为 i 的当前值, 结果 i = 0, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.updateAndGet(p -> p + 2));
将上面的CAS操作改成下面的方法会简便很多:不需要使用while循环,并且简化了操作步骤
class AccountCas implements Account {
private AtomicInteger balance;
public AccountCas(int balance) { this.balance = new AtomicInteger(balance);}
@Override
public Integer getBalance() {return balance.get();}
@Override
public void withdraw(Integer amount) {
balance.getAndAdd(-1*amount)
}
}
5、CAS和ABA的问题如何解决?
AtomicReference(原子引用)
AtomicMarkableReference
AtomicStampedReference
class DecimalAccountCas implements DecimalAccount {
private AtomicReference<BigDecimal> balance;
public DecimalAccountCas(BigDecimal balance) {
this.balance = new AtomicReference<>(balance);
}
//获取余额
@Override
public BigDecimal getBalance() {
return balance.get();
}
@Override
public void withdraw(BigDecimal amount) {
while(true) {
BigDecimal prev = balance.get();
BigDecimal next = prev.subtract(amount);
if (balance.compareAndSet(prev, next)) {
break;
}
}
}
}
1、什么是ABA问题?
因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了
@Slf4j(topic = "c.TestCAS")
public class TestCAS {
static AtomicReference<String> ref = new AtomicReference<>("A");
public static void main(String[] args) throws InterruptedException {
log.debug("main start...");
//获取共享变量的旧值A
String prev = ref.get();
//调用other()方法
other();
Thread.sleep(1000);
// CAS操作将A改为C
log.debug("change A->C {}", ref.compareAndSet(prev, "C"));
}
//其他线程将共享变量从A改成B,又从B改成A,但主线程感知不到,主线程只会判断最新获取到的值A与prev是否相同
private static void other() throws InterruptedException {
new Thread(() -> {
//线程t1将A改成B
log.debug("change A->B {}", ref.compareAndSet(ref.get(), "B"));
}, "t1").start();
Thread.sleep(500);
new Thread(() -> {
//线程t2将B改成A
log.debug("change B->A {}", ref.compareAndSet(ref.get(), "A"));
}, "t2").start();
}
}
执行结果:
14:05:10.388 c.TestCAS [main] - main start...
14:05:10.544 c.TestCAS [t1] - change A->B true
14:05:11.049 c.TestCAS [t2] - change B->A true
14:05:12.047 c.TestCAS [main] - change A->C true
如图所示:
(1) t1线程读取主存中的Ref引用变量的值A到自己的工作内存中,将其从A改为了B,并同步到了主存
(2) t2线程读取主存中的Ref引用变量的值B到自己的工作内存中,将其从B改成了A,并同步到了主存
(3) 此时主存中的值经历了A----》B---》A
的过程,但是主线程感知不到
(4) 主线程进行CAS操作将Ref变量的值更改为了C(将工作线程中的值和主存中的值进行比较,发现一致,就认为主存中的共享变量的值没有更改过)
主线程仅能判断出共享变量的值与最初值 A 是否相同,不能感知到这种从 A 改为 B 又 改回 A 的情况,如果主线程
希望:只要有其它线程【动过了】共享变量,那么自己的 cas 就算失败,这时,仅比较值是不够的,需要再加一个版本号 。
2、AtomicStampedReference
在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。 从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
@Slf4j(topic = "c.TestCAS1")
public class TestCAS1 {
//原子引用变量的初始值为A
static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
public static void main(String[] args) throws InterruptedException {
log.debug("main start...");
//获取旧值A
String prev = ref.getReference();
// 获取版本号
int stamp = ref.getStamp();
log.debug("版本 {}", stamp);
// 如果中间有其它线程干扰,发生了 ABA 现象
other();
Thread.sleep(1000);
// 主线程尝试改为 C
log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));
}
private static void other() throws InterruptedException {
//每次更新成功后,将版本号加1
new Thread(() -> {
log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B", ref.getStamp(), ref.getStamp() + 1));
log.debug("更新版本为 {}", ref.getStamp());
}, "t1").start();
Thread.sleep(500);
//每次更新成功后,将版本号加1
new Thread(() -> {
log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A", ref.getStamp(), ref.getStamp() + 1));
log.debug("更新版本为 {}", ref.getStamp());
}, "t2").start();
}
}
执行结果:
14:04:28.851 c.TestCAS1 [main] - main start...
14:04:28.856 c.TestCAS1 [main] - 版本 0
14:04:28.977 c.TestCAS1 [t1] - change A->B true
14:04:28.977 c.TestCAS1 [t1] - 更新版本为 1
14:04:29.482 c.TestCAS1 [t2] - change B->A true
14:04:29.483 c.TestCAS1 [t2] - 更新版本为 2
14:04:30.477 c.TestCAS1 [main] - change A->C false
(1) 线程1从主存中将Ref
引用变量的值和Stamp
版本号的值读入到工作内存中,并将Ref从A改成了B,Stamp从0改成了1
(2) 线程1从主存中将Ref
引用变量的值和Stamp
版本号的值读入到工作内存中,并将Ref从B改成了A,Stamp从1改成了2
(3) 此时主存中的Ref引用变量的值为A,Stamp版本号的值为2
(4) 主线程在进行CAS操作时,会将主存中的Ref变量的值A(新值)和工作内存中Ref变量的值A(旧值)进行比较
,判断是否相同(相同),同时还会将主存中Stamp版本号的值2(新值)和工作内存中Stamp版本号的值0(旧值)进行比较
,判断是否相同(不同),只有两者都相同,CAS才会成功。
AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个的变化过程,如: A -> B -> A ->
C ,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了几次。
但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过。
3、AtomicMarkableReference
AtomicMarkableReference可以理解为上面AtomicStampedReference的简化版,就是不关心修改过几次,仅仅关心是否修改过。因此变量mark是boolean类型,仅记录值是否有过修改。
@Slf4j(topic = "c.Test38")
public class Test38 {
public static void main(String[] args) throws InterruptedException {
GarbageBag bag = new GarbageBag("装满了垃圾");
// 参数2 mark 可以看作一个标记,表示垃圾袋满了
AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<>(bag, true);
log.debug("start...");
//获取垃圾袋
GarbageBag prev = ref.getReference();
log.debug(prev.toString());
new Thread(() -> {
log.debug("start...");
bag.setDesc("空垃圾袋");
//保洁阿姨将垃圾桶的垃圾倒空,并将垃圾状态从true(满)改为false(空)
ref.compareAndSet(bag, bag, true, false);
log.debug(bag.toString());
},"保洁阿姨").start();
sleep(1);
log.debug("想换一只新垃圾袋?");
//房东想要更换垃圾袋,发现垃等换失败,因为垃圾袋状态为false(空),但是期待的为true(满)
boolean success = ref.compareAndSet(prev, new GarbageBag("空垃圾袋"), true, false);
log.debug("换了么?" + success);
log.debug(ref.getReference().toString());
}
}
class GarbageBag {
String desc;
public GarbageBag(String desc) {
this.desc = desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
@Override
public String toString() {
return super.toString() + " " + desc;
}
}
执行结果:
14:43:55.104 c.Test38 [main] - start...
14:43:55.113 c.Test38 [main] - cn.itcast.test.GarbageBag@769c9116 装满了垃圾
14:43:55.254 c.Test38 [保洁阿姨] - start...
14:43:55.254 c.Test38 [保洁阿姨] - cn.itcast.test.GarbageBag@769c9116 空垃圾袋
14:43:56.277 c.Test38 [main] - 想换一只新垃圾袋?
14:43:56.277 c.Test38 [main] - 换了么?false
14:43:56.277 c.Test38 [main] - cn.itcast.test.GarbageBag@769c9116 空垃圾袋
(1) 保洁阿姨线程从主存中读取bag变量到工作内存中,并将当前垃圾袋倒空,但是并没有更换垃圾袋同时将flag的状态从true更改为false
然后同步到主存中。
(2) 此时主存中的bag还是原来的bag(还是原来的垃圾袋),标记变量boolean flag = false
(3) 此时房东线程想要通过CAS更换垃圾袋,首先将工作内存中的垃圾袋bag和主存中的垃圾袋bag进行比较
,判断是否相同(相同),然后将工作内存中的flag=true与主存中的flag=false
进行比较,判断是否相同(不同),自由两个都相同,CAS才能成功。