CAS 是什么
CAS 是 Compare And Swap 的缩写,即比较并交换,其底层使用的是 Unsafe 的 Native 方法,该方法是使用 C 语言中的方法调用 CPU 的读取,比较 并 交换 这三个系统指令,组成的一个原子操作,当读取到的值跟预期的值一致时就交换,不一致就不交换。
CAS 的不足
- 只能保证一个变量操作的原子性
- CAS 可能会循环时间过长, CPU 开销比较大
- 使用 CAS 过程中可能会发生 ABA 的问题
CAS 中的 ABA 问题
ABA 问题是指,如果一个值原来是 A,变成了 B,又变成了 A,那么使用 CAS 进行检查时会发现它的值没有发生变化,但是实际上却变化了。
对于 A-B-A 问题,通常的处理措施是对每一次 CAS 操作设置版本号。在变量前面追加版本号,每次变量更新就把版本号加 1,则 A-B-A 就变成 1A-2B-3A。
在 Java 的 java.util.concurrent.atomic 包下,提供了 AtomicMarkableReference 和 AtomicStampedReference 类来解决 A-B-A 问题。其 compareAndSet()方法首先检查当前引用是否等于预期引用,并且还会检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用的该标志的值设置为给定的更新值。
悲观锁与乐观锁
- 悲观锁:悲观锁是非常悲观,每次修改数据的时候都认为别人会同时修改数据,会发生冲突。因此操作数据时会直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。
- 乐观锁:乐观锁是非常乐观,每次修改数据的时候都认为别人不会同时修改数据。因此乐观锁不会锁住数据,只是在执行更新的时候判断一下在此期间别人是否修改了数据;如果别人修改了数据则放弃此次更新操作,否则执行更新操作。
synchronized是悲观锁,这种线程一旦得到锁,其他需要锁的线程就需要挂起。
CAS操作就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止,CAS是无阻塞模型。
CAS 的简单实例
不加锁,线程不安全实例
package com.aha.train.test.lock.cas;
/**
* 测试线程安全的问题
*
* @author WT
* @date 2021/10/12
*/
public class Test01 {
private static int count = 0;
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
//每个线程让count自增100次
for (int j = 0; j < 100; j++) {
count++;
}
}).start();
}
try{
// 3s 之后查看 count 的值 让多线程给跑完
Thread.sleep(3000);
}catch (Exception e){
e.printStackTrace();
}
System.out.println(count);
}
}
请问 count 的输出值是否为 1000 ?
答案是否定的,因为这个程序是线程不安全的,所以造成的结果 count 值可能小于 1000;
那么如何改造成线程安全的呢,其实我们可以使用上 Synchronized 同步锁,我们只需要在 count++ 的位置添加同步锁,代码如下:
package com.aha.train.test.lock.cas;
/**
* 测试线程安全的问题
*
* @author WT
* @date 2021/10/12
*/
public class Test01 {
private static int count = 0;
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
//每个线程让count自增100次
for (int j = 0; j < 100; j++) {
synchronized (Test01.class) {
count++;
}
}
}).start();
}
try{
Thread.sleep(2100);
}catch (Exception e){
e.printStackTrace();
}
System.out.println(count);
}
}
加了同步锁之后,count 自增的操作变成了原子性操作,所以最终的输出一定是 count=100 ,代码实现了线程安全。
但是 Synchronized 虽然确保了线程的安全,但是在性能上却不是最优的,Synchronized 关键字会让没有得到锁资源的线程进入 BLOCKED 状态,而后在争夺到锁资源后恢复为 RUNNABLE 状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。
尽管 Java1.6 为 Synchronized 做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然较低。
这边更优的一个操作就是就是使用原子类操作,所谓原子类指的是 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 < 10; i++) {
new Thread(() -> {
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
// 每个线程让count自增100次
for (int j = 0; j < 100; j++) {
// 返回的是新值 还有一个 getAndIncrement 是返回旧值然后加一
count.incrementAndGet();
}
}).start();
}
try{
Thread.sleep(2100);
}catch (Exception e){
e.printStackTrace();
}
System.out.println(count);
}
原子引用
对于基础类型的包装类,JUC 包下提供了 AtomicInteger 这种原子类,如果是自定义的类如何能实现原子操作呢?这边 JUC 提供了 AtomicReference<V> , 具体操作可以参考下面的代码:
@AllArgsConstructor
class User {
int age;
String name;
public static void main(String[] args){
User user = new User(20,"张三");
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(user);
}
}
原子引用设置版本号解决 ABA 的问题
package com.aha.train.test.lock.cas;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* 测试 CAS 的 ABA 问题
* @author WT
* @date 2021/10/21
*/
@Slf4j
public class ABADemo {
static AtomicStampedReference<String> atomicReference = new AtomicStampedReference<>("A", 1);
public static void main(String[] args) {
new Thread(() -> {
try {
// 睡一秒,让 t1 线程拿到最初的版本号
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicReference.compareAndSet("A", "B", atomicReference.getStamp(), atomicReference.getStamp() + 1);
atomicReference.compareAndSet("B", "A", atomicReference.getStamp(), atomicReference.getStamp() + 1);
}, "t2").start();
new Thread(() -> {
//拿到最开始的版本号
int stamp = atomicReference.getStamp();
try {
// 睡3秒,让t2线程的ABA操作执行完
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("是否将值设置成功:{}",atomicReference.compareAndSet("A", "C", stamp, stamp + 1));
}, "t1").start();
}
}