有如下需求,保证 account.withdraw 取款方法的线程安全。
先定义一个接口如下:
interface Account {
/**
* 获取余额
* @return
*/
Integer getBalance();
/**
* 取款
* @param amount
*/
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");
}
}
正常的实现类如下:
class AccountUnsafe implements Account {
private Integer balance;
public AccountUnsafe(Integer balance) {
this.balance = balance;
}
@Override
public Integer getBalance() {
return balance;
}
@Override
public void withdraw(Integer amount) {
balance -= amount;
}
}
根据前面的知识我们知道,这样在多线程下肯定结果是不对的。需要对共享变量balance加锁,代码如下:
class Accountsafe implements Account {
private Integer balance;
public Accountsafe(Integer balance) {
this.balance = balance;
}
@Override
public synchronized Integer getBalance() {
return balance;
}
@Override
public synchronized void withdraw(Integer amount) {
balance -= amount;
}
}
除了上面加锁的方法,还有一种无锁的方法,如下:
class AccountSafe implements Account {
private AtomicInteger balance;
public AccountSafe(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;
if (balance.compareAndSet(prev, next)) {
break;
}
}
// 可以简化为下面的方法
// balance.addAndGet(-1 * amount);
}
}
下面具体看下其原理。
前面看到的 AtomicInteger
的解决方法,内部并没有用锁来保护共享变量的线程安全。那么它是如何实现的呢?
其中的关键是 compareAndSet
,它的简称就是 CAS (也有 Compare And Swap 的说法),它必须是原子操作。
compareAndSet 做检查,在 set 前,先比较 prev 与当前值。如果不一致了,next 作废,返回 false 表示失败,比如,别的线程已经做了减法,当前值已经被减成了 990,那么本线程的这次 990 就作废了,进入 while 下次循环重试。如果一致,以 next 设置为新值,返回 true 表示成功。
注意:
其实 CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的原子性。
在多核状态下,某个核执行到带 lock 的指令时,CPU 会让总线锁住,当这个核把此指令执行完毕,再开启总线。这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的。
获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。
前面我们了解到,volatile 可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主中获取它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。
volatile 仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原子性)。CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果。
无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。打个比喻线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速… 恢复到高速运行,代价比较大。
但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。
结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
与synchronized锁比较:
CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
而synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会
CAS 体现的是无锁并发、无阻塞并发。因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一;但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响。
J.U.C 并发包提供了:
以 AtomicInteger 为例:
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));
// 获取并计算(i = 0, p 为 i 的当前值, x 为参数1, 结果 i = 10, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
// getAndUpdate 如果在 lambda 中引用了外部的局部变量,要保证该局部变量是 final 的
// getAndAccumulate 可以通过 参数1 来引用外部的局部变量,但因为其不在 lambda 中因此不必是 final
System.out.println(i.getAndAccumulate(10, (p, x) -> p + x));
// 计算并获取(i = 10, p 为 i 的当前值, x 为参数1, 结果 i = 0, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.accumulateAndGet(-10, (p, x) -> p + x));
有时候我们的变量并不是整数或其他基本类型,那么这时候就需要引用类型了。
常见的引用类型如下:
示例如下。
有如下接口,用来抽象出取钱的逻辑:
interface DecimalAccount {
/**
* 获取余额
*/
BigDecimal getBalance();
/**
* 取款
*/
void withdraw(BigDecimal amount);
/**
* 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
* 如果初始余额为 10000 那么正确的结果应当是 0
*/
static void demo(DecimalAccount account) {
List<Thread> ts = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
ts.add(new Thread(() -> {
account.withdraw(BigDecimal.TEN);
}));
}
ts.forEach(Thread::start);
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(account.getBalance());
}
}
下面试着提供不同的 DecimalAccount 实现,实现安全的取款操作。
首先是不安全的操作:
class DecimalAccountUnsafe implements DecimalAccount {
BigDecimal balance;
public DecimalAccountUnsafe(BigDecimal balance) {
this.balance = balance;
}
@Override
public BigDecimal getBalance() {
return balance;
}
@Override
public void withdraw(BigDecimal amount) {
BigDecimal balance = getBalance();
this.balance = balance.subtract(amount);
}
}
多线程下共享变量balance在多线程下是不安全的。
下面使用锁来实现线程安全:
class DecimalAccountSafeLock implements DecimalAccount {
private final Object lock = new Object();
BigDecimal balance;
public DecimalAccountSafeLock(BigDecimal balance) {
this.balance = balance;
}
@Override
public BigDecimal getBalance() {
return balance;
}
@Override
public void withdraw(BigDecimal amount) {
synchronized (lock) {
BigDecimal balance = getBalance();
this.balance = balance.subtract(amount);
}
}
}
下面看看如何使用 CAS实现线程安全:
class DecimalAccountSafeCas implements DecimalAccount {
AtomicReference<BigDecimal> balance;
public DecimalAccountSafeCas(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 res = prev.subtract(amount);
if (balance.compareAndSet(prev, res)) {
break;
}
}
}
}
测试结果如下:
DecimalAccount.demo(new DecimalAccountUnsafe(new BigDecimal("10000"))); // 不是0
DecimalAccount.demo(new DecimalAccountSafeLock(new BigDecimal("10000"))); // 0
DecimalAccount.demo(new DecimalAccountSafeCas(new BigDecimal("10000"))); // 0
CAS看似很好用,却存在一个问题,就是ABA问题。即compareAndSet
方法仅能判断出预期结果与实际结果是否相同,而不能判断出中间是否被改过。
看下面一个示例:
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicReference;
@Slf4j
public class ABATest {
static AtomicReference<String> ref = new AtomicReference<>("A");
public static void main(String[] args) throws InterruptedException {
log.info("main start...");
// 获取值 A
String prev = ref.get();
// other方法来修改变量
other();
Thread.sleep(1000);
// 尝试改为 C
log.info("change A->C {}", ref.compareAndSet(prev, "C"));
}
/**
* 此方法开了两个线程,先把ref改为了B,再改回A
* @throws InterruptedException
*/
private static void other() throws InterruptedException {
new Thread(() -> {
log.info("change A->B {}", ref.compareAndSet(ref.get(), "B"));
}, "t1").start();
Thread.sleep(500);
new Thread(() -> {
log.info("change B->A {}", ref.compareAndSet(ref.get(), "A"));
}, "t2").start();
}
}
运行结果如下:
2023-06-22 15:27:14,816 - 0 INFO [main] up.cys.chapter05.ABATest:17 - main start...
2023-06-22 15:27:14,831 - 15 INFO [t1] up.cys.chapter05.ABATest:34 - change A->B true
2023-06-22 15:27:15,330 - 514 INFO [t2] up.cys.chapter05.ABATest:40 - change B->A true
2023-06-22 15:27:16,335 - 1519 INFO [main] up.cys.chapter05.ABATest:25 - change A->C true
主线程仅能判断出共享变量的值与最初值 A 是否相同,不能感知到这种从 A 改为 B 又 改回 A 的情况。
可以使用AtomicStampedReference
来解决ABA问题。AtomicStampedReference有个版本号的概念,通用比较版本即可知道是否被更改过。
代码如下:
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
@Slf4j
public class AtomicStampedReferenceTest {
// 第二个参数0代表版本
static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
public static void main(String[] args) throws InterruptedException {
log.info("main start...");
// 获取值 A
String prev = ref.getReference();
// 获取版本号
int stamp = ref.getStamp();
// other方法来修改变量
other();
Thread.sleep(1000);
// compareAndSet多了两个参数,分别是当前版本和修改后的版本
log.info("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));
}
/**
* 此方法开了两个线程,先把ref改为了B,再改回A
* @throws InterruptedException
*/
private static void other() throws InterruptedException {
new Thread(() -> {
log.info("change A->B {}", ref.compareAndSet(ref.getReference(), "B", ref.getStamp(), ref.getStamp() + 1));
log.info("更新版本为 {}", ref.getStamp());
}, "t1").start();
Thread.sleep(500);
new Thread(() -> {
log.info("change B->A {}", ref.compareAndSet(ref.getReference(), "A", ref.getStamp(), ref.getStamp() + 1));
log.info("更新版本为 {}", ref.getStamp());
}, "t2").start();
}
}
运行结果如下:
2023-06-22 15:52:18,077 - 0 INFO [main] up.cys.chapter05.AtomicStampedReferenceTest:19 - main start...
2023-06-22 15:52:18,091 - 14 INFO [t1] up.cys.chapter05.AtomicStampedReferenceTest:38 - change A->B true
2023-06-22 15:52:18,093 - 16 INFO [t1] up.cys.chapter05.AtomicStampedReferenceTest:39 - 更新版本为 1
2023-06-22 15:52:18,590 - 513 INFO [t2] up.cys.chapter05.AtomicStampedReferenceTest:45 - change B->A true
2023-06-22 15:52:18,591 - 514 INFO [t2] up.cys.chapter05.AtomicStampedReferenceTest:46 - 更新版本为 2
2023-06-22 15:52:19,595 - 1518 INFO [main] up.cys.chapter05.AtomicStampedReferenceTest:29 - change A->C false
AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个的变化过程,如: A -> B -> A -> C ,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了几次。
有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了AtomicMarkableReference
。
下面我们引用一个自定义对象来说明,定义一个垃圾袋对象,代码如下:
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;
}
}
然后写一个程序,判断垃圾袋是否是满的,满的需要更换,不满不需要更换。先装满垃圾袋,然倒空,再判断是否需要更换。
@Slf4j
public class AtomicMarkableReferenceTest {
public static void main(String[] args) throws InterruptedException {
GarbageBag bag = new GarbageBag("装满了垃圾");
// 参数2 mark 可以看作一个标记,表示垃圾袋满了
AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<>(bag, true);
log.info("主线程 start...");
GarbageBag prev = ref.getReference();
log.info(prev.toString());
// 开个线程把垃圾倒了
new Thread(() -> {
log.info("倒垃圾线程 start...");
bag.setDesc("空垃圾袋");
while (!ref.compareAndSet(bag, bag, true, false)) {}
log.info(bag.toString());
}).start();
Thread.sleep(1000);
log.info("主线程想换一只新垃圾袋?");
boolean success = ref.compareAndSet(prev, new GarbageBag("空垃圾袋"), true, false);
log.info("换了么?" + success);
log.info(ref.getReference().toString());
}
}
运行结果如下:
2023-06-22 22:39:29,195 - 0 INFO [main] up.cys.chapter05.AtomicMarkableReferenceTest:20 - 主线程 start...
2023-06-22 22:39:29,220 - 25 INFO [main] up.cys.chapter05.AtomicMarkableReferenceTest:22 - up.cys.chapter05.GarbageBag@1a3869f4 装满了垃圾
2023-06-22 22:39:29,222 - 27 INFO [Thread-0] up.cys.chapter05.AtomicMarkableReferenceTest:26 - 倒垃圾线程 start...
2023-06-22 22:39:29,225 - 30 INFO [Thread-0] up.cys.chapter05.AtomicMarkableReferenceTest:29 - up.cys.chapter05.GarbageBag@1a3869f4 空垃圾袋
2023-06-22 22:39:30,227 - 1032 INFO [main] up.cys.chapter05.AtomicMarkableReferenceTest:34 - 主线程想换一只新垃圾袋?
2023-06-22 22:39:30,237 - 1042 INFO [main] up.cys.chapter05.AtomicMarkableReferenceTest:36 - 换了么?false
2023-06-22 22:39:30,238 - 1043 INFO [main] up.cys.chapter05.AtomicMarkableReferenceTest:37 - up.cys.chapter05.GarbageBag@1a3869f4 空垃圾袋
原子数组有如下3个:
其用法与前面讲过的原子整数和原子引用类似,原子数组可以保护数组里的元素。
看下面一个例子,是一个不安全的数组:
package up.cys.chapter05;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
public class AtomicArrayTest01 {
public static void main(String[] args) {
demo(
()->new int[10],
(array)->array.length,
(array, index) -> array[index]++,
array-> System.out.println(Arrays.toString(array))
);
}
/**
参数1,提供数组、可以是线程不安全数组或线程安全数组
参数2,获取数组长度的方法
参数3,自增方法,回传 array, index
参数4,打印数组的方法
*/
// supplier 提供者 无中生有 ()->结果
// function 函数 一个参数一个结果 (参数)->结果 , BiFunction (参数1,参数2)->结果
// consumer 消费者 一个参数没结果 (参数)->void, BiConsumer (参数1,参数2)->
private static <T> void demo(
Supplier<T> arraySupplier,
Function<T, Integer> lengthFun,
BiConsumer<T, Integer> putConsumer,
Consumer<T> printConsumer ) {
List<Thread> ts = new ArrayList<>();
T array = arraySupplier.get();
int length = lengthFun.apply(array);
// 数组的每个元素使用多线程加10000
for (int i = 0; i < length; i++) {
// 每个线程对数组作 10000 次操作
ts.add(new Thread(() -> {
for (int j = 0; j < 10000; j++) {
putConsumer.accept(array, j%length);
}
}));
}
ts.forEach(t -> t.start()); // 启动所有线程
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}); // 等所有线程结束
printConsumer.accept(array);
}
}
运行结果如下:
[9128, 9063, 9157, 9117, 9141, 9155, 9143, 9112, 9179, 9153]
下面使用安全的数组:
demo(
()-> new AtomicIntegerArray(10),
(array) -> array.length(),
(array, index) -> array.getAndIncrement(index),
array -> System.out.println(array)
);
getAndIncrement方法是用来获取元素并加1。
运行结果如下:
[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]
字段更新器是用来保护对象的成员变量,可以保证多个线程方程访问统一对象的统一变量时的线程安全性。
主要有如下3种:
利用字段更新器,可以针对对象的某个域(Field)进行原子操作。
注意只能配合 volatile 修饰的字段使用,否则会出现异常:
Exception in thread "main" java.lang.IllegalArgumentException: Must be volatile type
示例如下:
package up.cys.chapter05;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
public class AtomicFieldUpdaterTest {
private volatile int field;
public static void main(String[] args) {
// 初始化一个更新器,参数是要保护的类和属性
AtomicIntegerFieldUpdater fieldUpdater = AtomicIntegerFieldUpdater.newUpdater(AtomicFieldUpdaterTest.class, "field");
AtomicFieldUpdaterTest obj = new AtomicFieldUpdaterTest();
// 要保护的对象和要修改的属性值
fieldUpdater.compareAndSet(obj, 0, 10);
// 修改成功 field = 10
System.out.println(obj.field);
// 修改成功 field = 20
fieldUpdater.compareAndSet(obj, 10, 20);
System.out.println(obj.field);
// 修改失败 field = 20
fieldUpdater.compareAndSet(obj, 10, 30);
System.out.println(obj.field);
}
}
java里也提供了专门来做累加的线程安全类,如果有线程安全的累加需求,使用原子累加器性能更高。
下面写个程序,看下使用原子累加器和不使用累加器的效率对比,因为虚拟机内部在程序执行多次才能做出优化,所以下面都执行5次看下后面效果:
package up.cys.chapter05;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.Consumer;
import java.util.function.Supplier;
public class LongAdderTest {
public static void main(String[] args) {
// 第一个使用原子累加器LongAdder
for (int i = 0; i < 5; i++) {
demo(() -> new LongAdder(), adder -> adder.increment());
}
// 第二个不实用累加器,使用正常的原子整数做累加
for (int i = 0; i < 5; i++) {
demo(() -> new AtomicLong(), adder -> adder.getAndIncrement());
}
}
private static <T> void demo(Supplier<T> adderSupplier, Consumer<T> action) {
T adder = adderSupplier.get();
long start = System.nanoTime();
List<Thread> ts = new ArrayList<>();
// 4 个线程,每人累加 50 万
for (int i = 0; i < 40; i++) {
ts.add(new Thread(() -> {
for (int j = 0; j < 500000; j++) {
action.accept(adder);
}
}));
}
ts.forEach(t -> t.start());
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();}
});
long end = System.nanoTime();
System.out.println(adder + " cost:" + (end - start)/1000_000);
}
}
运行结果如下:
20000000 cost:566
20000000 cost:89
20000000 cost:26
20000000 cost:29
20000000 cost:30
20000000 cost:1022
20000000 cost:837
20000000 cost:1061
20000000 cost:843
20000000 cost:838
可以看到,运行速度上,使用累加器的速度确实要比不使用快不少。
性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加Cell[1]… 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能。
其实我们前面学到的AtomicInteger、park/unpark等,底层都是使用的Unsafe的方法。
AtomicInteger的部分源码如下:
private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");
private volatile int value;
public final boolean compareAndSet(int expectedValue, int newValue) {
return U.compareAndSetInt(this, VALUE, expectedValue, newValue);
}
Unsafe 对象提供了非常底层的,操作内存、线程的方法,Unsafe类是final修饰的, 对象不能直接调用产生,只能通过反射获得。
示例:
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class UnsafeAccessor {
static Unsafe unsafe;
static {
try {
// 反射获取Unsafe对象theUnsafe
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;
}
}
下面看一下如何使用底层Unsafe实现前面的CAS操作。
Unsafe底层主要有下面3个方法:
其中第一个参数是对象,第二个参数是偏移量,第三个参数是当前值,第4个参数是要修改的新值。
示例:
import lombok.Data;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class UnsafeCasTest {
public static void main(String[] args) throws NoSuchFieldException {
Unsafe unsafe = UnsafeAccessor.getUnsafe();
Field id = Student.class.getDeclaredField("id");
Field name = Student.class.getDeclaredField("name");
// 获得成员变量的偏移量
long idOffset = UnsafeAccessor.unsafe.objectFieldOffset(id);
long nameOffset = UnsafeAccessor.unsafe.objectFieldOffset(name);
Student student = new Student();
// 使用 cas 方法替换成员变量的值
UnsafeAccessor.unsafe.compareAndSwapInt(student, idOffset, 0, 20); // 返回 true
UnsafeAccessor.unsafe.compareAndSwapObject(student, nameOffset, null, "张三"); // 返回 true
System.out.println(student); // Student(id=20, name=张三)
}
}
@Data
class Student {
volatile int id;
volatile String name;
}