并发学习第六篇——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方法

 

posted @ 2021-03-06 21:43  鼠标的博客  阅读(146)  评论(0编辑  收藏  举报