JUC是java.util.concurrent包的简称,JUC有2大核心,CAS和AQS,CAS是java.util.concurrent.atomic包的基础,即AtomicInteger和AtomicLong等是用CAS实现的。
一. CAS原理
现在有一个AtomicInteger类型的变量,初始值为0。有两个线程需要同时对它进行一次自增操作,期待的结果是2。按照时间顺序分析一下两个线程具体的执行逻辑。
- t1时刻:线程1读取到当前的值是0;
- t2时刻:线程2也读取到当前的值是0;
- t3时刻:线程1先拿到CPU执行权,尝试将值设置为1,此时发现当前的值与t1时刻读取的值相等(0==0),说明没有其它线程进行改动,则将值成功设置为1;
- t4时刻:线程2拿到CPU执行权,尝试将值设置为1,此时发现当前的值已经变成了1(线程1所改),与t2时刻读取的值0不相等(0!=1),那么线程2此次尝试设置值失败;
- t5时刻:线程2重新读取当前值为1;
- t6时刻:线程2自增,尝试将值设置为2,此时发现当前的值还是1,与t5时刻读取的值相等(1==1),则可以成功设置值为2。
以上的3、4、6步骤都是CAS(Compare And Swap)操作,CAS操作在底层的硬件级别保证一定是原子的,同一时间只有一个线程可以执行CAS,先比较再设置。
二.CAS 源码
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// 第一个参数为当前这个对象
// 第二个参数为AtomicInteger对象value成员变量在内存中的偏移量
// 第三个参数为要增加的值
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
// 调用底层方法得到value值
var5 = this.getIntVolatile(var1, var2);
//通过var1和var2得到底层值,var5为当前值,如果底层值=当前值,则将值设为var5+var4,并返回true,否则返回false
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
三. 利用CAS构造一个TryLock(立即失败)自定义的显示锁
背景:使用synchronized或者基于wait()的自定义的显示锁,没有抢到锁的线程将被阻塞,现在需要一种功能尝试加锁的功能,如果线程没有争抢到锁,则抛出异常,线程直接释放掉。可以用AtomicInteger来实现。
public class CompareAndSwapLock {
/**
* 锁标志
* 0:锁空闲
* 2:锁占用
*/
private static final AtomicInteger lock = new AtomicInteger(0);
/**
* 记录当前占有锁的线程
*/
private Thread lockedThread;
/**
* 尝试加锁
*/
public void tryLock() throws GetLockException {
// CAS操作,原子性的,多线程安全
boolean success = lock.compareAndSet(0, 1);
if (!success) {
throw new GetLockException(Thread.currentThread().getName() + " try lock failed");
} else {
lockedThread = Thread.currentThread();
}
}
public void unLock() {
if (0 == lock.get()) {
return;
}
// 如果是当前线程占有锁,释放
if (lockedThread == Thread.currentThread()) {
lock.compareAndSet(1, 0);
}
}
}
public class GetLockException extends Exception {
public GetLockException() {
super();
}
public GetLockException(String message) {
super(message);
}
}
测试:
public class TryLockTest {
public static void main(String[] args) {
CompareAndSwapLock lock = new CompareAndSwapLock();
IntStream.range(0, 3).forEach(i -> new Thread(() -> {
try {
// 尝试加锁
lock.tryLock();
try {
Thread.sleep(1_000);
System.out.println(Thread.currentThread().getName() + " do something");
} catch (InterruptedException e) {
e.printStackTrace();
}
} catch (GetLockException e) {
// 尝试加锁异常,进行一些操作
System.out.println(Thread.currentThread().getName() + " " + e.getMessage());
} finally {
// 释放锁
lock.unLock();
}
}).start());
}
}
测试结果:
可以看出,线程0抢到了锁,正常执行,线程1和线程2没有抢到锁,抛出了异常。
四. ABA问题(带有版本号的更新)
CAS机制虽然保证了原子性,但是会引发ABA问题。何为ABA问题?假设初始值为A,线程1需要将A更新为B,但是线程2在线程1更新之前进行了两步操作,先将A更新B,再将B更新回为A,此时根据CAS原理,只要预期值与当前值相等(A=A),线程1就能成功更新为B,但是实际上其它线程已经对数据进行了两次操作,只不过经过两次操作之后数据还跟原来一样。
这种场景对某些特殊的数据结构会存在隐藏的问题,比如说栈:
[https://www.cnblogs.com/549294286/p/3766717.html]
利用AtomicStampedReference解决ABA问题:
public class ABATest {
private static AtomicStampedReference stampedReference
= new AtomicStampedReference<>(100, 0);
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
boolean success = stampedReference.compareAndSet(100, 101, stampedReference.getStamp(), stampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "尝试将100改为101,修改前版本号是:" + (stampedReference.getStamp() - 1) + ", 修改结果:" + ":" + success);
success = stampedReference.compareAndSet(101, 100, stampedReference.getStamp(), stampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "尝试将101改为100,修改前版本号是:" + (stampedReference.getStamp() - 1) + ", 修改结果:" + ":" + success);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread t2 = new Thread(() -> {
try {
int stamp = stampedReference.getStamp();
System.out.println("Before sleep:stamp = " + stamp);
TimeUnit.SECONDS.sleep(2);
boolean success = stampedReference.compareAndSet(100, 101, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName() + "尝试将100改为101,修改前版本号是:" + stamp + ", 修改结果:" + success);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
t2.start();
}
}
测试结果:
Thread-1期待值和当前值都为100,但是修改失败,因为stamp的期待值和当前值不相等。
AtomicStampedReference的源码
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair current = pair;
return
//期望对象的引用和版本号和目标对象的引用和版本好都一样时,才会新建一个Pair对象,然后用新建的Pair对象和原理的Pair对象做CAS操作
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
其实很多地方(ES、ZK)利用带有版本号(version)进行更新的操作就是基于该原理。
AtomicReference简介
AtomicReference类提供了一个可以原子读写的对象引用变量。 原子意味着尝试更改相同AtomicReference的多个线程(例如,使用比较和交换操作)不会使AtomicReference最终达到不一致的状态。 AtomicReference甚至有一个先进的compareAndSet()方法,它可以将引用与预期值(引用)进行比较,如果它们相等,则在AtomicReference对象内设置一个新的引用。
上面的例子如果使用AtomicReference,Thread-1能成功将100更新为101,因为它更新的时候没有stamp概念。
public class AtomicReferenceTest {
public static void main(String[] args) {
SimpleObject simpleObject = new SimpleObject("tom", 88);
AtomicReference atomicReference = new AtomicReference<>(simpleObject);
// 更新结果成功,因为期待的对象引用与当前的对象引用是同一个
boolean success = atomicReference.compareAndSet(simpleObject, new SimpleObject("tom", 100));
System.out.println(success);
}
static class SimpleObject {
String name;
int id;
public SimpleObject(String name, int id) {
this.name = name;
this.id = id;
}
}
}
public class AtomicReferenceTest {
public static void main(String[] args) {
SimpleObject simpleObject = new SimpleObject("tom", 88);
AtomicReference atomicReference = new AtomicReference<>(simpleObject);
// 第一个参数传入一个新的对象,name、id与初始化的对象一样,但是更新结果任然失败,因为期待的对象引用与当前的对象引用不是同一个
boolean success = atomicReference.compareAndSet(new SimpleObject("tom", 88), new SimpleObject("tom", 100));
System.out.println(success);
}
static class SimpleObject {
String name;
int id;
public SimpleObject(String name, int id) {
this.name = name;
this.id = id;
}
}
}