早点学会Unsafe和CAS早下班陪女朋友
一 Unsafe类常用API了解
今天的内容是Unsafe类,学习原子类的底层实现,并发编程中的基石之一,也是JDK源码中的重要成员。
Java无法直接访问底层操作系统,而是通过本地(native)方法来访问。不过尽管如此,JDK中有一个Unsafe类提供了硬件级别的原子操作,它们使用JIN的方式实现C++;由于是硬件级别的操作API,我们平时几乎无法遇见,因为它是提供给JDK内部使用,我们也使用不到,不过我们在看JDK源码的时候还是能经常见到它们的身影;
先了解一些unsafe一些常用的API
先看第一组获取偏移值
- 返回变量在类中的内存偏移值;
public native long objectFieldOffset(Field var1);
- 获取数组中第一个元素所在的偏移地址
public native int arrayBaseOffset(Class<?> var1);
- 获取数组中第一个元素所占用的字节
public native int arrayIndexScale(Class<?> var1);
其次看第二组内存分配
// 分配内存
public native long allocateMemory(long var1);
// 扩展内存
public native long reallocateMemory(long var1, long var3);
// 指定对象设置指定内存值
public native void setMemory(Object var1, long var2, long var4, byte var6);
// 释放内存
public native void freeMemory(long var1);
再看看第三组 CAS(compareAndSwap)
解释语义:当obj对象中的偏移为offse的变量值与期望值expect值相等时,就使用update更新obj;成功返回true,失败返回false;
// 对象CAS
public final native boolean compareAndSwapObject(Object obj, long offset, Object expect, Object update);
// int CAS
public final native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);
// long CAS
public final native boolean compareAndSwapLong(Object obj, long offset, long expect, long update);
看 一组 Volatile 语义;
这边只列出object对象使用方式,其实还有其它8大基本数据类型,使用方式一样;
// 获取 obj 对象 中偏移为 offset 的 Volatile语义值
public native Object getObjectVolatile(Object obj, long offset);
// 设置 obj 对象 中偏移为 offset 的 Volatile语义值
public native void putObjectVolatile(Object obj, long offset, Object value);
由CAS和 Volatile 语义衍生的一组
对象obj 的偏移值 为offset的Volatile 语义值 则用 update 更新 Volatile 语义值 var5;注意返回的是旧值var5;
public final Object getAndSetObject(Object obj, long offset, Object update) {
Object var5;
do {
var5 = this.getObjectVolatile(obj, offset);
} while(!this.compareAndSwapObject(obj, offset, var5, update));
return var5;
}
对象 obj的偏移 为 offset 的 变量Volatile 语义 为 var6, 则用 var6 + add 的值 更新 var6;
public final long getAndAddLong(Object obj, long offset, long add) {
long var6;
do {
var6 = this.getLongVolatile(obj, offset);
} while(!this.compareAndSwapLong(obj, offset, var6, var6 + add));
return var6;
}
Park/Unpark 组合主要是JVM用来切换线程;Park 为阻塞当前线程,Unpark 为唤醒线程;
最后看一组 putOrdered 操作;设置 对象obj 偏移为 offset 的变量值为 value, 支持 violate语义
public native void putOrderedObject(Object obj, long ofsset, Object value);
public native void putOrderedInt(Object obj, long ofsset, int value);
public native void putOrderedLong(Object obj, long ofsset, long value);
二 原子类使用分析
我们都知道原子类是线程安全的原子性操作;我们先来熟悉下如何操作原子类,验证是否真的是线程安全;
r如下代码中 使用 getAndIncrement 方法对 atomicInteger 变量进行自增;启动2 个线程后运行atomicInteger结果为正确值;
public class UnsafeTest4 {
private static AtomicInteger atomicInteger = new AtomicInteger();
private static volatile Integer count = 0;
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
for (int i = 0; i < 10000; i++) {
atomicInteger.getAndIncrement();
count++;
}
};
// 启动2 个线程
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
// 携程
thread1.join();
thread2.join();
// atomicInteger=20000
System.out.println("atomicInteger=" + atomicInteger);
// count=13401
System.out.println("count=" + count);
}
}
getAndIncrement 方法是如何做到 原子性操作呢?我们 试着从源码角度分析, 内部其实就是 使用 unsafe 类 getAndAddInt 方法, 与之前分析 getAndAddLong 的 效果功能差不多;
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
瞧一眼getAndAddInt即可, obj 的偏移为offset 的变量 Volatile语义值为 var5 , 使用var5 + addValue 更新 var5;
public final int getAndAddInt(Object obj, long offset, int addValue) {
int var5;
do {
var5 = this.getIntVolatile(obj, offset);
} while(!this.compareAndSwapInt(obj, offset, var5, var5 + addValue));
return var5;
}
原子类的线程安全操作其实底层就是使用CAS操作;
三 CAS使用与验证
我们无法直接使用 Unsafe 类,如果按照jdk源码中给出的示例调用我们会撞的头破血流
public static void main(String[] args) {
Unsafe unsafe = Unsafe.getUnsafe();
}
错误信息如下
Exception in thread "main" java.lang.SecurityException: Unsafe
at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
at com.youku1327.base.cas.UnsafeTest.main(UnsafeTest.java:15)
Process finished with exit code 1
源码判定只要调用者的类加载器不是系统域的直接报错,所以我们根本不能使用静态方式调用;
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
java提供了强大的反射机制能够让我们调用Unsafe类;
private static Unsafe getUnsafe(){
// 通过反射获取 unsafe
Unsafe unsafe = null;
try {
Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
Field theUnsafe = unsafeClass.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe = (Unsafe)theUnsafe.get(null);
} catch (Exception e) {
e.printStackTrace();
}
return unsafe;
}
知识追寻者使用 unsafe 的 objectFieldOffset 先计算出 变量的地址偏移,然后通过 CAS 验证该对象的偏移 是否 与计算的偏移相同;结果明显相等,掌握这一步,我们就知道如何使用CAS;
public static void main(String[] args) {
try {
UnsafeTest unsafeTest = new UnsafeTest();
Unsafe unsafe = UnsafeTest.getUnsafe();
long value = unsafe.objectFieldOffset(unsafeTest.getClass().getDeclaredField("name"));
// 偏移值为12
System.out.println(value);
// CAS 操作 判定 unsafeTest 对象的偏移值 为 12 值是否为 kxg ; 如果是 就用 zszxz 代替
boolean compareAndSwapObject = unsafe.compareAndSwapObject(unsafeTest ,12, "kxg", "zszxz");
// true
System.out.println(compareAndSwapObject);
// zszxz
System.out.println(unsafeTest.name);
} catch (Exception e) {
e.printStackTrace();
}
}
早期 的Unsafe 类 还能使用 monitorEnter 和 monitorExit 模拟 synchronized 锁,多线程的安全性;由于 这两个API 在JDK1.8已经过时,不做过多讲解;
四 CAS 存在 问题
4.1 CAS 问题
CAS 和 锁都能解决 多线程情况下 的原子性问题;与锁相比,它没有 锁的竞争 的 额外开销,但缺点也很明显,要不断的自旋,循环时间非常长;只能保证一个变量的原子性操作;存在ABA问题;
关注公众号:知识追寻者领取面试题集
4.2 ABA 问题
其它都好理解,着重说下什么是ABA问题;
如果一个值原来是A,变成了B,然后又变成了A,那么在CAS检查的时候会发现没有改变,但是实质上它已经发生了改变,这就是所谓的ABA问题;
变量A 变为B,B再变为 A的过程中;线程 N 拿到的A在CAS之前是 初始变量A吗? 显然不一定 ,线程M 将 A 经过CAS 变为B,线程 M再 将变量 B再经过 CAS 变为 A; 线程N获取后面的A 与前面的A 就不是同一个变量;
ABA问题的解决
CAS解决ABA 问题的关键就是 使用版本号; A1---> B2 ---> A3 , 就明显区分了不同的变量;
原子类之AtomicStampedReference可以解决ABA问题,它内部不仅维护了对象值,还维护了一个Stamp(可以理解为版本号) ,使用 compareAndSet 方法就可以实现无锁自旋;
我们可以看下 AtomicStampedReference 类源码;
// 参数为:期望值 新值 期望版本号 新版本号
public boolean compareAndSet(V expectedReference, V
newReference, int expectedStamp, int newStamp);
//获得当前对象引用
public V getReference();
//获得当前版本号
public int getStamp();
//设置当前对象引用和版本号
public void set(V newReference, int newStamp);
//如果当前引用等于预期引用, 将更新新的版本号到内存
public boolean attemptStamp(V expectedReference, int newStamp)
//构造方法, 传入引用和版本号
public AtomicStampedReference(V initialRef, int initialStamp)
4.3 验证 AtomicStampedReference 解决 ABA 问题
使用 AtomicStampedReference 来模拟 CAS 的 ABA 问题,我们对其加版本号后,CAS后的结果肯定为失败;
public class UnsafeTest3 {
// 初始值10,版本号0
private static AtomicStampedReference<Integer> count = new AtomicStampedReference<>(10, 0);
private static final Logger logger = LoggerFactory.getLogger(UnsafeTest3.class);
public static void main(String[] args) {
new Thread(() -> {
//获取当前版本
int stamp = count.getStamp();
logger.info("线程{} 当前版本{}",Thread.currentThread(),stamp);
try {
//等待1秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//此时expectedReference未发生改变,但是stamp已经被修改了,所以CAS失败
boolean isCASSuccess = count.compareAndSet(10, 12, stamp, stamp + 1);
logger.info("CAS是否成功? {}",isCASSuccess);
}, "主操作线程").start();
new Thread(() -> {
//获取当前版本
int stamp = count.getStamp();
logger.info("线程{} 当前版本{}",Thread.currentThread(),stamp);
count.compareAndSet(10, 12, stamp, stamp + 1);
logger.info("线程{} 增加后版本{}",Thread.currentThread(),count.getStamp());
// 模拟ABA问题 先更新成12 又更新回10
//获取当前版本
int newStamp = count.getStamp();
count.compareAndSet(12, 10, newStamp, newStamp + 1);
logger.info("线程{} 减少后版本{}",Thread.currentThread(),count.getStamp());
}, "干扰线程").start();
}
}
输出结果:
线程Thread[主操作线程,5,main] 当前版本0
线程Thread[干扰线程,5,main] 当前版本0
线程Thread[干扰线程,5,main] 增加后版本1
线程Thread[干扰线程,5,main] 减少后版本2
CAS是否成功? false
Unsafe类功能这么强大,为什么JDK不开给我用,而是限制JDK内部使用呢?个人觉得就是因为Unsafe操作的是底层硬件资源,如果分配内存出现问题,就很容易造成系统奔溃;越强大的工具危险性越高;