CAS操作
CAS的定义
CAS的全称为“Compare-And-Swap”,直译就是对比并交换。是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值。其实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM只是封装了汇编调用,如AtomicInteger类便是使用了这些封装后的接口。CAS的简单解释:CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下在旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。CAS操作是原子性的,所以多线程并发使用CAS更新数据时,可以不使用锁。JDK中大量使用了CAS来更新数据而防止加锁(synchronized 重量级锁)来保持原子更新。
public interface Account {
// 获取余额
Integer getBalance();
// 取款
void withdraw(Integer amount);
/**
* 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
* 如果初始余额为 10000 那么正确的结果应当是 0
*/
static void demo(Account account) {
List<Thread> ts = new ArrayList<>();
long start = System.nanoTime();
for (int i = 0; i < 1000; i++) {
ts.add(new Thread(() -> {
account.withdraw(10);
}));
}
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");
}
public static void main(String[] args) {
Account.demo(new AccountCas(10000));
}
}
前面的学习中,可以使用加锁的方式来保证线程安全,如:
public class AccountLock implements Account{
private Integer balance;
public AccountLock(Integer balance) {
this.balance = balance;
}
@Override
public Integer getBalance() {
return balance;
}
@Override
public void withdraw(Integer amount) {
balance -= amount;
}
}
但是,如果不加锁,怎么确保线程安全?其实,线程安全问题就是出在共享变量“Integer balance”身上,Java中其实提供了原子操作的Interger类:AtomicInteger,它可以确保线程安全,如:
public class AccountCas implements Account {
private final AtomicInteger balance;
public AccountCas(Integer 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;
}
}
// 可以简化为下面的方法
// balance.addAndGet(-1 * amount);
}
}
可以看到,我们没有加锁,依然保证了线程安全。其原因就是在于“compareAndSet”这个方法,其实现就是采用的CAS操作实现的。CAS其实就是“compareAndSet”的缩写,当然,也有另一种说法是“CompareAndSwap”的缩写,不过表达的意思都是一样:在修改共享变量前,会将工作内存的变量和主存的变量进行比较(compare),如果一样就进行修改(Set或Swap),如果不一样则放弃修改并进入下一轮循环(如上面的例子),直到一样为止,示意图如下所示:
这样,就可以保证变量balance的线程安全。
CAS与锁的比较
性能的比较
CAS和加锁可以达到一样的效果,而且通过上面的例子比较,CAS的性能甚至会高于加锁,原因解释如下面的比喻:
无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速… 恢复到高速运行,代价比较大。但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。
当前,性能优于加锁是有前提条件的:【适用于线程数少、多核 CPU 的场景下】。如果只有单核CPU,没有多余的CPU核心去维持在高速运转的CAS线程;如果运行的线程很多,可以想到重试必然频繁发生,反而效率会受影响。
实现思想的比较
CAS与volatile
CAS操作的实现其实是需要依赖volatile这个关键字的,因为需要和主存的变量进行比较,这就需要volatile修饰哪个变量了。volatile可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。CAS 必须借助 volatile 才能读取到共享变量的最新值来实现比较并交换的效果。
CAS的一些问题
ABA问题
AtomicStampedReference的使用示例如下:
@Slf4j(topic = "c.main")
public class Main {
private static AtomicStampedReference<Integer> atomicStampedRef =
new AtomicStampedReference<>(1, 0);
public static void main(String[] args) {
Thread main = new Thread(() -> {
System.out.println("操作线程" + Thread.currentThread() + ",初始值 a = " + atomicStampedRef.getReference());
int stamp = atomicStampedRef.getStamp(); //获取当前标识别
try {
Thread.sleep(1000); //等待1秒 ,以便让干扰线程执行
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean isCASSuccess = atomicStampedRef.compareAndSet(1, 2, stamp, stamp + 1); //此时expectedReference未发生改变,但是stamp已经被修改了,所以CAS失败
System.out.println("操作线程" + Thread.currentThread() + ",CAS操作结果: " + isCASSuccess);
}, "主操作线程");
Thread other = new Thread(() -> {
Thread.yield(); // 确保thread-main 优先执行
atomicStampedRef.compareAndSet(1, 2, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
System.out.println("操作线程" + Thread.currentThread() + ",【increment】 ,值 = " + atomicStampedRef.getReference());
atomicStampedRef.compareAndSet(2, 1, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
System.out.println("操作线程" + Thread.currentThread() + ",【decrement】 ,值 = " + atomicStampedRef.getReference());
}, "干扰线程");
main.start();
other.start();
}
}
循环时间长开销大
只能保证一个共享变量的原子操作
13个原子操作类
Atomic包下的原子操作类
Java 从 JDK 1.5 开始提供了 java.util.concurrent.atomic 包(简称Atomic包),这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。Atoic包下提供了12个类,他们分别可以实现四种原子操作:原子更新基本类型、原子更新数组、原子更新引用、原子更新属性(字段)。
原子更新基本类型
实例代码:
public static void main(String[] args) {
AtomicInteger ai = new AtomicInteger(1);
System.out.println("ai.get() = " + ai.get());
System.out.println("ai.addAndGet(5) = " + ai.addAndGet(5));
System.out.println("ai.get() = " + ai.get());
System.out.println("ai.compareAndSet(ai.get(), 10) = " + ai.compareAndSet(ai.get(), 10));
System.out.println("ai.get() = " + ai.get());
System.out.println("ai.getAndIncrement() = " + ai.getAndIncrement());
System.out.println("ai.get() = " + ai.get());
ai.lazySet(8);
System.out.println("ai.lazySet(8)");
System.out.println("ai.get() = " + ai.get());
System.out.println("ai.getAndSet(5) = " + ai.getAndSet(5));
System.out.println("ai.get() = " + ai.get());
}
原子更新引用类型
以AtomicReference为例说明:
public class Main {
public static AtomicReference<User> atomicUserRef = new AtomicReference<>();
public static void main(String[] args) {
User user = new User("conan", 15);
atomicUserRef.set(user);
User updateUser = new User("Shinichi", 17);
atomicUserRef.compareAndSet(user, updateUser);
System.out.println(atomicUserRef.get().getName());
System.out.println(atomicUserRef.get().getOld());
}
@Data
@AllArgsConstructor
@NoArgsConstructor
static class User {
private String name;
private int old;
}
}
原子更新字段类
如:
public class Main {
//创建一个原子更新器
private static final AtomicIntegerFieldUpdater<User> ATOMIC_INTEGER_FIELD_UPDATER =
AtomicIntegerFieldUpdater.newUpdater(User.class, "old");
public static void main(String[] args) {
User user = new User("Tom", 15);
//原来的年龄
System.out.println(ATOMIC_INTEGER_FIELD_UPDATER.getAndIncrement(user));
//现在的年龄
System.out.println(ATOMIC_INTEGER_FIELD_UPDATER.get(user));
}
@Data
@AllArgsConstructor
@NoArgsConstructor
static class User {
private String name;
public volatile int old;
}
}
原子更新数组
示例代码:
public class Main {
static int[] value = new int[]{1, 2};
static AtomicIntegerArray ai = new AtomicIntegerArray(value);
public static void main(String[] args) {
ai.getAndSet(1, 300);
System.out.println(ai.get(1));
System.out.println(value[1]);
}
}
Unsafe类
Unsafe类简介
Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。但由于Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语言变得不再“安全”,因此对Unsafe的使用一定要慎重。这个类尽管里面的方法都是 public 的,但是并没有办法使用它们,JDK API 文档也没有提供任何关于这个类的方法的解释。总而言之,对于 Unsafe 类的使用都是受限制的,只有授信的代码才能获得该类的实例,当然 JDK 库里面的类是可以随意使用的。
如上图所示,Unsafe提供的API大致可分为内存操作、CAS、Class相关、对象操作、线程调度、系统信息获取、内存屏障、数组操作等几类。CAS的API的详细用法参考文章:
https://blog.csdn.net/muyimo/article/details/121379139
获取Unsafe类
Unsafe 对象提供了非常底层的,操作内存、线程的方法,Unsafe 对象不能直接调用,只能通过反射获得。
public class Main {
static Unsafe unsafe;
static {
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe = (Unsafe) theUnsafe.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new Error(e);
}
}
static Unsafe getUnsafe() {
return unsafe;
}
public static void main(String[] args) {
System.out.println(getUnsafe());
}
}
CAS操作
public class Main {
static Unsafe unsafe;
static {
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe = (Unsafe) theUnsafe.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new Error(e);
}
}
static Unsafe getUnsafe() {
return unsafe;
}
public static void main(String[] args) throws NoSuchFieldException {
Unsafe unsafe = getUnsafe();
Field id = Student.class.getDeclaredField("id");
Field name = Student.class.getDeclaredField("name");
// 获得成员变量的偏移量
long idOffset = unsafe.objectFieldOffset(id);
long nameOffset = unsafe.objectFieldOffset(name);
Student student = new Student();
// 使用 cas 方法替换成员变量的值
unsafe.compareAndSwapInt(student, idOffset, 0, 20); // 返回 true
unsafe.compareAndSwapObject(student, nameOffset, null, "张三"); // 返回 true
System.out.println(student);
}
}
@Data
class Student {
volatile int id;
volatile String name;
}
因此,利用Unsafe类,可以自己实现一个简单的AtomicInteger类,如:
public class AtomicData {
private volatile int data;
static final Unsafe unsafe;
static final long DATA_OFFSET;
static {
unsafe = Main.getUnsafe();
try {
// data 属性在 DataContainer 对象中的偏移量,用于 Unsafe 直接访问该属性
DATA_OFFSET = unsafe.objectFieldOffset(AtomicData.class.getDeclaredField("data"));
} catch (NoSuchFieldException e) {
throw new Error(e);
}
}
public AtomicData(int data) {
this.data = data;
}
public void decrease(int amount) {
int oldValue;
while (true) {
if (unsafe.compareAndSwapInt(this, DATA_OFFSET, oldValue, oldValue - amount)) {
return;
}
}
}
public int getData() {
return data;
}
}