并发学习第六篇——Unsafe类和CAS
之前看JUC的源码,一直有发现一个怪异的类:Unsafe,怪,是因为名字有点怪......它在sun.misc包下,
不属于Java标准,但是很多java的高性能的类都基于Unsafe,而且它竟然可以直接操作内存,让java直
接操作内存是很危险的,这个类就明着告诉我们,不安全,不要直接用我
先看一个最常见到的方法:getUnsafe()
private Unsafe() {} private static final Unsafe theUnsafe = new Unsafe(); @CallerSensitive ------这个注解查了下是用来控制权限的,跟踪到最初的调用者,望文生义:调用者敏感的 public static Unsafe getUnsafe() { Class<?> caller = Reflection.getCallerClass();
//仅在BootstrapClassLoader加载时是合法的 if (!VM.isSystemDomainLoader(caller.getClassLoader())) throw new SecurityException("Unsafe"); return theUnsafe; }
很容易看出来是个单列模式,上面的注解,用来控制权限,可以跟踪到最初的调用者,可以望文生义的理解:
调用者敏感;普通调用者调用直接抛异常,必须是systemDomainLoader这样的classLoader(BootstrapClassLoader)
Unsafe类提供的API的脑图:
CAS操作
/**
* @param o 包含要修改field的对象
* @param offset 对象中某field的偏移量
* @param expected 期望值(预期的原值)
* @param update 更新值
* @return true | false
*/
public native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);
public native boolean compareAndSwapLong(Object obj, long offset, long expect, long update);
public native boolean compareAndSwapObject(Object obj, long offset, Object expect, Object update);
Unsafe提供的CAS操作有这三个,都是compareAndSwapXXX的形式,执行的是一条CPU的原子指令(cmpxchg)
CAS操作有3个操作数,内存值M,预期值E,新值U,如果M==E,则将内存值修改为U,否则啥都不做
这个对比下mysql中的MVCC机制很好理解,MVCC存的是个隐藏的版本号值,做记录变更的操作时会拿
先前已经获取到的版本号跟当前记录最新的版本号做比较(就像比较M和E),然后决定操作是成功还是失败
典型应用:java.util.concurrent.atomic相关类、Java AQS、CurrentHashMap
这里讲CAS,需要顺带提一下坊间关于CAS设计存在的三个问题:
1、ABA问题
很好理解,既然比较的是值,那最新的值A可能是变成了B后又变回了A,如果要求A不变指的是完全没变,而不是结果没变
那CAS无法判断,这个时候可以加版本号解决,如上面说的mysql的mvcc的设计,java里有专门的针对此问题的设计类:
AtomicStampedReference
2、自旋消耗
经常见到的是 while(!compareAndSwap(a,b,c)){},然后就一直自旋转,如果自旋一直不成功,会一直让CPU花费开销
聪明的设计者自然有应对之策--------> 自适应自旋锁:
即自旋的次数不再固定,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定
补充一下:LongAdder这个类,可以将单一变量的CAS操作分散为对数组Cell中多个cell的CAS操作,最后再求和,也是
解决的一个思路,java8已提供
3、只能支持单个共享变量的原子操作
这个问题思路也好解决,多个共享变量组合成一个对象,只要保证对象可原子操作即可------->JUC中的AtomicReference
内存操作
包含堆外内存的分配、拷贝、释放、给定地址值操作等方法
在Java中创建的对象都处于堆内内存(heap)中,堆内内存是由JVM所管控的Java进程内存,遵循JVM的内存
管理机制,JVM会采用垃圾回收机制统一管理堆内存。
堆外内存存在于JVM管控之外的内存区域,Java中对堆外内存的操作,依赖于Unsafe提供的和memory相关的
native方法,如分配内存allocateMemory,扩充内存reallocateMemory,释放内存freeMemory
线程调度
包括线程的挂起,恢复,锁机制方法
//取消阻塞线程 public native void unpark(Object thread); //阻塞线程 public native void park(boolean isAbsolute, long time); //获得对象锁(可重入锁) @Deprecated
public native void monitorEnter(Object o); //释放对象锁 @Deprecated
public native void monitorExit(Object o); //尝试获取对象锁 @Deprecated
public native boolean tryMonitorEnter(Object o);
锁机制相关的native方法已过时,不关注
park:将一个线程挂起,调用park方法后,线程将一直阻塞直到超时或者中断等条件出现;
unpark:终止一个挂起的线程,使其恢复正常
典型应用:
Java锁和同步器框架的核心类AbstractQueuedSynchronizer,通过调用LockSupport.park()
和LockSupport.unpark()
实现线程的阻塞和唤醒,LockSupport的park、unpark方法实际是调用Unsafe的park、unpark方式来实现
LockSupport先点名,下一篇上
内存屏障
有点熟悉,volatile也用到了内存屏障,用来禁止代码重排序,Unsafe的这几个方法也是为了禁止重排,理解上是一样的
//内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前 public native void loadFence(); //内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前 public native void storeFence(); //内存屏障,禁止load、store操作重排序 public native void fullFence();
典型应用
java8中读写锁的改进版本:StampedLock
StampedLock.validate方法源码:
StampedLock的典型用法是
1、获取乐观读锁
2、copy变量到工作内存
3、锁状态检测
其中第2和3步要保证不会发生重排序,因为按照执行逻辑,2必须在3之前发生,3处使用loadFence()加上内存屏障,保证顺序
StampedLock类,后面介绍
其他功能,碰到的位置比较少,不做介绍
总结Unsafe类
单例模式的体现;
提供了很多native的不安全操作的方法;
提供了三个CAS原子操作的API方法;
提供了JUC下实现"锁"的基本类(Atomic,AQS,LockSupport)所需的线程调度和CAS方法