参考
Unsafe - 线程类基石
在AQS中我们大量接触到了CAS操作,比如:
/**
* CAS head field. Used only by enq.
*/
private final boolean compareAndSetHead(Node update) {
return unsafe.compareAndSwapObject(this, headOffset, null, update);
}
这里我们注意到:这里的CAS,是使用Unsafe来实现的。
这里简单复习一下CAS:
CAS是compare and swap 的缩写
简单点来说:
- 因为JVM的设计初衷是尽可能地减少编程过程中内存的申请释放问题,因此java中并没有直接提供对应的功能,而是在JVM中做了这一工作;
- 但在一些性能要求高,或者需要调用对应内存地址的情况下,这一设计就很碍手碍脚。
因此,Java中提供了一个Unsafe类,来做这些操作,可以这么说:Unsafe给java程序员提供了一个类似C、Cpp语言中的部分能力。
然而,在JDK之外,Unsafe并不能通过Unsafe.getUnsafe()来获取,这部分在源码里可以看到:
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
也就是说,
这个方法只有在引导类加载器加载Unsafe类是调用才合法,否则会抛出一个SecurityException异常
可以通过反射的方式,来获取Unsafe:
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
} catch (Exception e) {
e.printStackTrace();
}
作者:赌一包辣条
链接:https://juejin.cn/post/6977993272538955806
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Unsafe还提供了线程相关的其他一些底层支持,比如:
-
unsafe提供了volatile关键字中所隐式引入的内存屏障的概念的使用:
//内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前 public native void loadFence(); //内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前 public native void storeFence(); //内存屏障,禁止load、store操作重排序 public native void fullFence();有这个工具,就可以使用Java代码而不是通过JVM底层的实现来进行屏障操作,这个的典型示例就是JDK1.8引入的StampedLock。
-
unsafe提供了线程挂起的功能使用:
public native void unpark(Object var1); public native void park(boolean var1, long var2);还记得AQS里,获取不到锁就挂起直到持有锁线程退出时会唤醒后续线程吗?就是下面这俩:
private final boolean parkAndCheckInterrupt() { //这里就挂起了 LockSupport.park(this); return Thread.interrupted(); } public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) //这里就会唤醒了 unparkSuccessor(h); return true; } return false; }而这里的park和unpark,最后落地到线程上就是通过这个park和unpark了。
Atomic - CAS封装
上面也提到了:
- Unsafe是不支持直接获取的
而很多情况下其实我们并不需要Unsafe这么重的东西,只是要使用CAS的功能,因此我们希望,能够提供一个封装好的CAS给我们用。
jdk里的Atomic就是基于这个前提下封装出来的。
我们就拿AtomicInteger为案例,来看看其中的具体结构:
public static void main(String[] args) {
AtomicInteger ai = new AtomicInteger(1);
ai.getAndIncrement();
ai.compareAndSet(ai.get(),100);
}
对于CAS最经典的方法就是compareAndSet了,也有地方是CompareAndSwap的。
这里的compareAndSet是会返回false的,也就是说:要保证更新成功,我们必须这么做:
while(!ai.compareAndSet(ai.get(),10))
毫无疑问在竞争强的case里这里会导致CPU的大量自旋操作浪费CPU资源。
一般来说AtomicInteger使用最多的是getAndIncrement。
在Atomic类里,会封装一个起始地址:
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
这里的valueOffset,指的并不是实际的地址,而是这个valueOffset相对于类起始位置的偏移地址。
在CAS操作中,我们知道是要有目标内存地址的,我们CAS操作比较以及写入的是跟这个地址相关的。
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
通过这个CAS的使用,能够反推Unsafe中,CAS的入参含义:
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
- Object:我们期望发生CAS操作的对象(假设地址为A)
- var2:我们期望发生CAS操作的字段和对象起始位置的偏移地址(假设地址为B)
- var4:期望原有值
- var5:期望写入值
映射到操作上,那就是:
- 我们期望将var5写入地址为A+B位置的变量;写入前比较A+B上的值,如果不为var4,则返回false,否则则写入。
\