Java 多线程编程 - Unsafe 类、源码角度看 CAS、ABA 问题、ABA 问题处理策略
·
一、Unsafe 类
- 普通类不能直接调用 Unsafe 的 getUnsafe 方法获取 Unsafe 对象,会抛出异常
Unsafe unsafe = Unsafe.getUnsafe();
# 输出结果
Exception in thread "main" java.lang.SecurityException: Unsafe
- Unsafe 类的源码有类加载器检查
@CallerSensitive
public static Unsafe getUnsafe() {
Class<?> caller = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(caller.getClassLoader()))
throw new SecurityException("Unsafe");
return theUnsafe;
}
- 可以通过反射获取 Unsafe 对象
public static Unsafe getUnsafe() throws Exception {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
}
try {
Unsafe unsafe = getUnsafe();
System.out.println(unsafe);
} catch (Exception e) {
e.printStackTrace();
}
# 输出结果
sun.misc.Unsafe@682a0b20
- Unsafe 类允许开发者像 C 语言一样操作内存,但是极其危险,一旦写错,JVM 就会直接奔溃
public static Unsafe getUnsafe() throws Exception {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
}
try {
Unsafe unsafe = getUnsafe();
System.out.println(unsafe);
unsafe.putInt(0x12345678, 123);
} catch (Exception e) {
e.printStackTrace();
}
# 输出结果
sun.misc.Unsafe@682a0b20
#
# A fatal error has been detected by the Java Runtime Environment:
#
# EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x00007ffa6b2eab64, pid=13396, tid=10064
#
# JRE version: Java(TM) SE Runtime Environment (17.0.7+8) (build 17.0.7+8-LTS-224)
# Java VM: Java HotSpot(TM) 64-Bit Server VM (17.0.7+8-LTS-224, mixed mode, sharing, tiered, compressed oops, compressed class ptrs, g1 gc, windows-amd64)
# Problematic frame:
# V [jvm.dll+0x7aab64]
#
# No core dump will be written. Minidumps are not enabled by default on client versions of Windows
#
# An error report file with more information is saved as:
# D:\javaCode\MyTest\hs_err_pid13396.log
#
# If you would like to submit a bug report, please visit:
# https://bugreport.java.com/bugreport/crash.jsp
#
二、源码角度看 CAS
AtomicInteger atomicInteger = new AtomicInteger(10);
atomicInteger.incrementAndGet();
public final int incrementAndGet() {
return U.getAndAddInt(this, VALUE, 1) + 1;
}
-
this:当前 AtomicInteger 对象 -
VALUE:value 字段在对象内存中的地址偏移量(静态初始化时算好的) -
1:要增加的值 -
+ 1:因为 getAndAddInt 方法返回旧值,加1才得到新值
@IntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
@IntrinsicCandidate
public native int getIntVolatile(Object o, long offset);
@IntrinsicCandidate
public final boolean weakCompareAndSetInt(Object o, long offset,
int expected,
int x) {
return compareAndSetInt(o, offset, expected, x);
}
@IntrinsicCandidate
public final native boolean compareAndSetInt(Object o, long offset,
int expected,
int x);
-
@IntrinsicCandidate:这个注解就像给方法贴了个标签,告知 JVM,这方法性能要求高,如果能用更高效的方式实现(例如,CPU 指令),就别用 Java 代码了(建议性的,而非强制性的) -
v = getIntVolatile(o, offset);:获取当前值,带 volatile 语义,强制从主存读 -
weakCompareAndSetInt(o, offset, v, v + delta):尝试 CAS 更新,如果还是 v,说明没人改过,改成 v + delta,如果失败了就重试(自选)
三、ABA 问题
1、基本介绍
线程 1:读取到 v 为 100,准备做 CAS
线程 2:把 100 改成 101,做点操作,又改回 100
线程 1:做 CAS(100 -> 101)成功
以为没人动过,其实中间变了
2、ABA 问题影响场景
(1)无影响的场景
- 只关心最终数值,中间过程不影响业务,ABA 相当于白折腾,结果对就行
- 计数器
线程 1:想从 5 加到 6
线程 2:5 -> 6 -> 5(加 1 又减 1)
线程 1:CAS(5 -> 6)成功
最终结果 6,业务正确(只关心最终值)
- 库存扣减
要扣 1 件:10 -> 9
别人折腾:10 -> 9 -> 10(下单又退货)
你 CAS(10 -> 9)成功
库存 9,正确(过程不重要,只关心最终数量)
(2)有影响的场景
- 栈 / 链表结构
public class Stack {
private AtomicReference<Node> top = new AtomicReference<>();
public Node pop() {
Node oldTop = top.get();
Node newTop = oldTop.next;
if (top.compareAndSet(oldTop, newTop)) {
return oldTop;
}
return null;
}
}
初始:A -> B -> C(当前:top = A)
线程 1:想弹出 A(期望:top = A,新 top = B)
线程 2:弹出 A(当前:top = B)
线程 2:弹出 B(当前:top = C)
线程 2:推入 A(当前:top = A,A.next = C)
线程 1:CAS 发现 top 还是 A,成功
结果:弹出 A,但实际 top 变成 C,期望的 top 是 B,B 已经不在栈中了
- 账户扣款
初始 state:balance = 1000, locked = false
线程 1:开始扣款 500(当前 state:balance = 1000, locked = false)
线程 2:由于某些原因冻结账户(当前 state:balance = 1000, locked = true)
线程 2:发现误冻结,立即解冻(当前 state:balance = 1000, locked = false)
线程 1:恢复执行,发现 state 未改变,扣款成功,但是中间的状态标志没有记录
- 更简单的理解,借书流程
线程 1:准备借书(状态 = 可借)
线程 2:管理员把书拿去消毒(状态 = 消毒中)
线程 2:消毒完放回(状态 = 可借)
线程 1:直接借走
书被消毒过这个信息丢了,下个读者不知道这本书刚消过毒
四、ABA 问题处理策略
1、基本介绍
-
AtomicStampedReference:用 int 版本号,可多次变化(0 -> 1 -> 2…)
-
AtomicMarkableReference:用 boolean 标记,只能表示是否被修改过(false -> true -> false…)
2、演示
- 使用 AtomicStampedReference
// 初始化
AtomicStampedReference<String> atomicStampedReference = new AtomicStampedReference<>("test", 0);
// get 方法用于同时获取值和版本
int[] stampHolder = new int[1];
String value = atomicStampedReference.get(stampHolder);
System.out.println("值: " + value + ", 版本: " + stampHolder[0]);
// 尝试更新
boolean result = atomicStampedReference.compareAndSet("test", "content", 0, 1);
System.out.println("更新结果: " + result);
// getReference 方法用于获取值,getStamp 方法用于获取版本
System.out.println("新值: " + atomicStampedReference.getReference() + ", 新版本: " + atomicStampedReference.getStamp());
# 输出结果
值: test, 版本: 0
更新结果: true
新值: content, 新版本: 1
- 使用 AtomicMarkableReference
// 初始化
AtomicMarkableReference<String> atomicMarkableReference = new AtomicMarkableReference<>("test", false);
boolean[] markHolder = new boolean[1];
String value = atomicMarkableReference.get(markHolder);
System.out.println("值: " + value + ", 标记: " + markHolder[0]);
// 尝试更新
boolean result = atomicMarkableReference.compareAndSet("test", "content", false, true);
System.out.println("更新结果: " + result);
System.out.println("新值: " + atomicMarkableReference.getReference() + ", 新标记: " + atomicMarkableReference.isMarked());
# 输出结果
值: test, 标记: false
更新结果: true
新值: content, 新标记: true
更多推荐



所有评论(0)