本文首要内容

  1. Unsafe根本介绍
  2. 获取Unsafe实例
  3. Unsafe中的CAS操作
  4. Unsafe中原子操作相关办法介绍
  5. Unsafe中线程调度相关办法介绍
  6. park和unpark示例
  7. Unsafe锁示例
  8. Unsafe中对volatile的支持

根本介绍

最近咱们一直在学习java高并发,java高并发中首要涉及到类位于java.util.concurrent包中,简称juc,juc中大部分类都是依赖于Unsafe来完结的,首要用到了Unsafe中的CAS、线程挂起、线程恢复等相关功用。所以假如计划深入了解JUC原理的,有必要先了解一下Unsafe类。

Unsafe是位于sun.misc包下的一个类,首要供给一些用于履行低等级、不安全操作的办法,如直接拜访体系内存资源、自主办理内存资源等,这些办法在提高Java运转功率、增强Java语言底层资源操作能力方面起到了很大的作用。但由于Unsafe类使Java语言具有了类似C语言指针相同操作内存空间的能力,这无疑也添加了程序发生相关指针问题的风险。在程序中过度、不正确运用Unsafe类会使得程序犯错的概率变大,使得Java这种安全的语言变得不再“安全”,因而对Unsafe的运用一定要稳重。

从Unsafe功用图上看出,Unsafe供给的API大致可分为内存操作CASClass相关目标操作线程调度体系信息获取内存屏障数组操作等几类,本文首要介绍3个常用的操作:CAS、线程调度、目标操作。

看一下UnSafe的原码部分:

publicfinalclassUnsafe{
//单例目标
privatestaticfinalUnsafetheUnsafe;
privateUnsafe(){
}
@CallerSensitive
publicstaticUnsafegetUnsafe(){
Classvar0=Reflection.getCallerClass();
//仅在引导类加载器`BootstrapClassLoader`加载时才合法
if(!VM.isSystemDomainLoader(var0.getClassLoader())){
thrownewSecurityException("Unsafe");
}else{
returntheUnsafe;
}
}
}

从代码中能够看出,Unsafe类为单例完结,供给静态办法getUnsafe获取Unsafe实例,内部会判断当时调用者是否是由体系类加载器加载的,假如不是体系类加载器加载的,会抛出SecurityException异常。

那咱们想运用这个类,如何获取呢?

能够把咱们的类放在jdk的lib目录下,那么启动的时分会自动加载,这种方式不是很好。

咱们学过反射,经过反射能够获取到Unsafe中的theUnsafe字段的值,这样能够获取到Unsafe目标的实例。

经过反射获取Unsafe实例

代码如下:

publicclassDemo1{
staticUnsafeunsafe;
static{
try{
Fieldfield=Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe=(Unsafe)field.get(null);
}catch(Exceptione){
e.printStackTrace();
}
}
publicstaticvoidmain(String[]args){
System.out.println(unsafe);
}
}

输出:

sun.misc.Unsafe@76ed5528

Unsafe中的CAS操作

看一下Unsafe中CAS相关办法界说:

/**
*CAS操作
*
*@paramo包括要修改field的目标
*@paramoffset目标中某field的偏移量
*@paramexpected期望值
*@paramupdate更新值
*@returntrue|false
*/
publicfinalnativebooleancompareAndSwapObject(Objecto,longoffset,Objectexpected,Objectupdate);
publicfinalnativebooleancompareAndSwapInt(Objecto,longoffset,intexpected,intupdate);
publicfinalnativebooleancompareAndSwapLong(Objecto,longoffset,longexpected,longupdate);

什么是CAS? 即比较并替换,完结并发算法经常用到的一种技能。CAS操作包括三个操作数——内存方位、预期原值及新值履行CAS操作的时分,将内存方位的值与预期原值比较,假如相匹配,那么处理器会自动将该方位值更新为新值,不然,处理器不做任何操作,多个线程一起履行cas操作,只有一个会成功。咱们都知道,CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe供给的CAS办法(如compareAndSwapXXX)底层完结即为CPU指令cmpxchg。履行cmpxchg指令的时分,会判断当时体系是否为多核体系,假如是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会履行cas操作,也就是说CAS的原子性实践上是CPU完结的, 其实在这一点上仍是有排他锁的,只是比重用synchronized, 这儿的排他时刻要短的多, 所以在多线程情况下性能会比较好。

说一下offset,offeset为字段的偏移量,每个目标有个地址,offset是字段相对于目标地址的偏移量,目标地址记为baseAddress,字段偏移量记为offeset,那么字段对应的实践地址就是baseAddress+offeset,所以cas经过目标、偏移量就能够去操作字段对应的值了。

CAS在java.util.concurrent.atomic相关类、Java AQS、JUC中并发调集等完结上有十分广泛的应用,咱们看一下java.util.concurrent.atomic.AtomicInteger类,这个类能够在多线程环境中对int类型的数据履行高效的原子修改操作,并确保数据的正确性,看一下此类顶用到Unsafe cas的当地:

java东西-高并发-JUC下东西类Unsafe解密

java东西-高并发-JUC下东西类Unsafe解密

JUC中其他当地运用到CAS的当地就不列举了,有爱好的能够去看一下源码。

Unsafe中原子操作相关办法介绍

5个办法,看一下完结:

/**
*int类型值原子操作,对var2地址对应的值做原子添加操作(添加var4)
*
*@paramvar1操作的目标
*@paramvar2var2字段内存地址偏移量
*@paramvar4需求加的值
*@return
*/
publicfinalintgetAndAddInt(Objectvar1,longvar2,intvar4){
intvar5;
do{
var5=this.getIntVolatile(var1,var2);
}while(!this.compareAndSwapInt(var1,var2,var5,var5+var4));
returnvar5;
}
/**
*long类型值原子操作,对var2地址对应的值做原子添加操作(添加var4)
*
*@paramvar1操作的目标
*@paramvar2var2字段内存地址偏移量
*@paramvar4需求加的值
*@return回来旧值
*/
publicfinallonggetAndAddLong(Objectvar1,longvar2,longvar4){
longvar6;
do{
var6=this.getLongVolatile(var1,var2);
}while(!this.compareAndSwapLong(var1,var2,var6,var6+var4));
returnvar6;
}
/**
*int类型值原子操作办法,将var2地址对应的值置为var4
*
*@paramvar1操作的目标
*@paramvar2var2字段内存地址偏移量
*@paramvar4新值
*@return回来旧值
*/
publicfinalintgetAndSetInt(Objectvar1,longvar2,intvar4){
intvar5;
do{
var5=this.getIntVolatile(var1,var2);
}while(!this.compareAndSwapInt(var1,var2,var5,var4));
returnvar5;
}
/**
*long类型值原子操作办法,将var2地址对应的值置为var4
*
*@paramvar1操作的目标
*@paramvar2var2字段内存地址偏移量
*@paramvar4新值
*@return回来旧值
*/
publicfinallonggetAndSetLong(Objectvar1,longvar2,longvar4){
longvar6;
do{
var6=this.getLongVolatile(var1,var2);
}while(!this.compareAndSwapLong(var1,var2,var6,var4));
returnvar6;
}
/**
*Object类型值原子操作办法,将var2地址对应的值置为var4
*
*@paramvar1操作的目标
*@paramvar2var2字段内存地址偏移量
*@paramvar4新值
*@return回来旧值
*/
publicfinalObjectgetAndSetObject(Objectvar1,longvar2,Objectvar4){
Objectvar5;
do{
var5=this.getObjectVolatile(var1,var2);
}while(!this.compareAndSwapObject(var1,var2,var5,var4));
returnvar5;
}

看一下上面的办法,内部经过自旋的CAS操作完结的,这些办法都能够确保操作的数据在多线程环境中的原子性,正确性。

来个示例,咱们仍是来完结一个网站计数功用,一起有100个人主张对网站的恳求,每个人主张10次恳求,每次恳求算一次,最终成果是1000次,代码如下:

publicclassDemo2{
staticUnsafeunsafe;
//用来记录网站拜访量,每次拜访+1
staticintcount;
//count在Demo.class目标中的地址偏移量
staticlongcountOffset;
static{
try{
//获取Unsafe目标
Fieldfield=Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe=(Unsafe)field.get(null);
FieldcountField=Demo2.class.getDeclaredField("count");
//获取count字段在Demo2中的内存地址的偏移量
countOffset=unsafe.staticFieldOffset(countField);
}catch(Exceptione){
e.printStackTrace();
}
}
//模仿拜访一次
publicstaticvoidrequest()throwsInterruptedException{
//模仿耗时5毫秒
TimeUnit.MILLISECONDS.sleep(5);
//对count原子加1
unsafe.getAndAddInt(Demo2.class,countOffset,1);
}
publicstaticvoidmain(String[]args)throwsInterruptedException{
longstarTime=System.currentTimeMillis();
intthreadSize=100;
CountDownLatchcountDownLatch=newCountDownLatch(threadSize);
for(inti=0;i<threadSize;i++){
Threadthread=newThread(()->{
try{
for(intj=0;j<10;j++){
request();
}
}catch(InterruptedExceptione){
e.printStackTrace();
}finally{
countDownLatch.countDown();
}
});
thread.start();
}
countDownLatch.await();
longendTime=System.currentTimeMillis();
System.out.println(Thread.currentThread().getName()+",耗时:"+(endTime-starTime)+",count="+count);
}
}

输出:

main,耗时:114,count=1000

代码中咱们在静态块中经过反射获取到了Unsafe类的实例,然后获取Demo2中count字段内存地址偏移量countOffset,main办法中模仿了100个人,每人主张10次恳求,等到所有恳求完毕之后,输出count的成果。

代码顶用到了CountDownLatch,经过countDownLatch.await()让主线程等候,等候100个子线程都履行完毕之后,主线程在进行运转。

Unsafe中线程调度相关办法

这部分,包括线程挂起、恢复、锁机制等办法。

//撤销堵塞线程
publicnativevoidunpark(Objectthread);
//堵塞线程,isAbsolute:是否是肯定时刻,假如为true,time是一个肯定时刻,假如为false,time是一个相对时刻,time表明纳秒
publicnativevoidpark(booleanisAbsolute,longtime);
//取得目标锁(可重入锁)
@Deprecated
publicnativevoidmonitorEnter(Objecto);
//开释目标锁
@Deprecated
publicnativevoidmonitorExit(Objecto);
//测验获取目标锁
@Deprecated
publicnativebooleantryMonitorEnter(Objecto);

调用park后,线程将被堵塞,直到unpark调用或许超时,假如之前调用过unpark,不会进行堵塞,即parkunpark不区分先后顺序。monitorEnter、monitorExit、tryMonitorEnter3个办法已过期,不主张运用了。

park和unpark示例

代码如下:

publicclassDemo3{
staticUnsafeunsafe;
static{
try{
Fieldfield=Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe=(Unsafe)field.get(null);
}catch(Exceptione){
e.printStackTrace();
}
}
/**
*调用park和unpark,模仿线程的挂起和唤醒
*
*@throwsInterruptedException
*/
publicstaticvoidm1()throwsInterruptedException{
Threadthread=newThread(()->{
System.out.println(System.currentTimeMillis()+","+Thread.currentThread().getName()+",start");
unsafe.park(false,0);
System.out.println(System.currentTimeMillis()+","+Thread.currentThread().getName()+",end");
});
thread.setName("thread1");
thread.start();
TimeUnit.SECONDS.sleep(5);
unsafe.unpark(thread);
}
/**
*堵塞指定的时刻
*/
publicstaticvoidm2(){
Threadthread=newThread(()->{
System.out.println(System.currentTimeMillis()+","+Thread.currentThread().getName()+",start");
//线程挂起3秒
unsafe.park(false,TimeUnit.SECONDS.toNanos(3));
System.out.println(System.currentTimeMillis()+","+Thread.currentThread().getName()+",end");
});
thread.setName("thread2");
thread.start();
}
publicstaticvoidmain(String[]args)throwsInterruptedException{
m1();
m2();
}
}

输出:

1565000238474,thread1,start
1565000243475,thread1,end
1565000243475,thread2,start
1565000246476,thread2,end

m1()中thread1调用park办法,park办法会将当时线程堵塞,被堵塞了5秒之后,被主线程调用unpark办法给唤醒了,unpark办法参数表明需求唤醒的线程。

线程中相当于有个答应,答应默许是0,调用park的时分,发现是0会堵塞当时线程,调用unpark之后,答应会被置为1,并会唤醒当时线程。假如在park之前先调用了unpark办法,履行park办法的时分,不会堵塞。park办法被唤醒之后,答应又会被置为0。多次调用unpark的效果是相同的,答应仍是1。

juc中的LockSupport类是经过unpark和park办法完结的。

Unsafe锁示例

代码如下:

publicclassDemo4{
staticUnsafeunsafe;
//用来记录网站拜访量,每次拜访+1
staticintcount;
static{
try{
Fieldfield=Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe=(Unsafe)field.get(null);
}catch(Exceptione){
e.printStackTrace();
}
}
//模仿拜访一次
publicstaticvoidrequest(){
unsafe.monitorEnter(Demo4.class);
try{
count++;
}finally{
unsafe.monitorExit(Demo4.class);
}
}
publicstaticvoidmain(String[]args)throwsInterruptedException{
longstarTime=System.currentTimeMillis();
intthreadSize=100;
CountDownLatchcountDownLatch=newCountDownLatch(threadSize);
for(inti=0;i<threadSize;i++){
Threadthread=newThread(()->{
try{
for(intj=0;j<10;j++){
request();
}
}finally{
countDownLatch.countDown();
}
});
thread.start();
}
countDownLatch.await();
longendTime=System.currentTimeMillis();
System.out.println(Thread.currentThread().getName()+",耗时:"+(endTime-starTime)+",count="+count);
}
}

输出:

main,耗时:64,count=1000

留意:

  1. monitorEnter、monitorExit、tryMonitorEnter 3个办法已过期,不主张运用了
  2. monitorEnter、monitorExit有必要成对呈现,呈现的次数有必要一致,也就是说锁了n次,也有必要开释n次,不然会造成死锁

Unsafe中确保变量的可见性

java中操作内存分为主内存和作业内存,共享数据在主内存中,线程假如需求操作主内存的数据,需求先将主内存的数据复制到线程独有的作业内存中,操作完结之后再将其刷新到主内存中。如线程A要想看到线程B修改后的数据,需求满意:线程B修改数据之后,需求将数据从自己的作业内存中刷新到主内存中,而且A需求去主内存中读取数据。

被关键字volatile润饰的数据,有2点语义:

  1. 假如一个变量被volatile润饰,读取这个变量时分,会强制从主内存中读取,然后将其复制到当时线程的作业内存中运用
  2. 给volatile润饰的变量赋值的时分,会强制将赋值的成果从作业内存刷新到主内存

上面2点语义确保了被volatile润饰的数据在多线程中的可见性。

Unsafe中供给了和volatile语义相同的功用的办法,如下:

//设置给定目标的int值,运用volatile语义,即设置后立马更新到内存对其他线程可见
publicnativevoidputIntVolatile(Objecto,longoffset,intx);
//取得给定目标的指定偏移量offset的int值,运用volatile语义,总能获取到最新的int值。
publicnativeintgetIntVolatile(Objecto,longoffset);

putIntVolatile办法,2个参数:

o:表明需求操作的目标

offset:表明操作目标中的某个字段地址偏移量

x:将offset对应的字段的值修改为x,而且当即刷新到主存中

调用这个办法,会强制将作业内存中修改的数据刷新到主内存中。

getIntVolatile办法,2个参数

o:表明需求操作的目标

offset:表明操作目标中的某个字段地址偏移量

每次调用这个办法都会强制从主内存读取值,将其复制到作业内存中运用。

其他的还有几个putXXXVolatile、getXXXVolatile办法和上面2个类似。