溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Java并發(fā)編程Unsafe類的源碼分析以及Unsafe類的使用方法

發(fā)布時間:2021-09-06 16:30:13 來源:億速云 閱讀:125 作者:chen 欄目:編程語言

本篇內(nèi)容介紹了“Java并發(fā)編程Unsafe類的源碼分析以及Unsafe類的使用方法”的有關(guān)知識,在實(shí)際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!

一.Unsafe類的源碼分析

JDK的rt.jar包中的Unsafe類提供了硬件級別的原子操作,Unsafe里面的方法都是native方法,通過使用JNI的方式來訪問本地C++實(shí)現(xiàn)庫。

rt.jar 中 Unsafe 類主要函數(shù)講解, Unsafe 類提供了硬件級別的原子操作,可以安全的直接操作內(nèi)存變量,其在 JUC 源碼中被廣泛的使用,了解其原理為研究 JUC 源碼奠定了基礎(chǔ)。

首先我們先了解Unsafe類中主要方法的使用,如下:

  1.long objectFieldOffset(Field field)  方法:返回指定的變量在所屬類的內(nèi)存偏移地址,偏移地址僅僅在該Unsafe函數(shù)中訪問指定字段時使用。如下代碼使用unsafe獲取AtomicLong中變量value在AtomicLong對象中的內(nèi)存偏移,代碼如下:

static {
 try {
 valueOffset = unsafe.objectFieldOffset(AtomicLong.class.getDeclaredField("value"));
 } catch (Exception ex) {
 throw new Error(ex);
 }

 }

  2.int arrayBaseOffset(Class arrayClass)方法:獲取數(shù)組中第一個元素的地址 

  3.int arrayIndexScale(Class arrayClass)方法:獲取數(shù)組中單個元素占用的字節(jié)數(shù) 

  3.boolean compareAndSwapLong(Object obj,long offset,long expect,long update)方法:比較對象obj中偏移量offset的變量的值是不是和expect相等,相等則使用update值更新,然后返回true,否則返回false。 

  4.public native long getLongVolative(Object obj,long offset)方法:獲取對象obj中偏移量offset的變量對應(yīng)的volative內(nèi)存語義的值。 

  5.void putOrderedLong(Object obj, long offset, long value) 方法:設(shè)置 obj 對象中 offset 偏移地址對應(yīng)的 long 型 field 的值為 value。這是有延遲的 putLongVolatile 方法,并不保證值修改對其它線程立刻可見。變量只有使用 volatile 修飾并且期望被意外修改的時候使用才有用。 

  6.void park(boolean isAbsolute, long time) 方法:阻塞當(dāng)前線程,其中參數(shù) isAbsolute 等于 false 時候,time 等于 0 表示一直阻塞,time 大于 0 表示等待指定的 time 后阻塞線程會被喚醒,這個 time 是個相對值,是個增量值,也就是相對當(dāng)前時間累加 time 后當(dāng)前線程就會被喚醒。 如果 isAbsolute 等于 true,并且 time 大于 0 表示阻塞后到指定的時間點(diǎn)后會被喚醒,這里 time 是個絕對的時間,是某一個時間點(diǎn)換算為 ms 后的值。另外當(dāng)其它線程調(diào)用了當(dāng)前阻塞線程的 interrupt 方法中斷了當(dāng)前線程時候,當(dāng)前線程也會返回,當(dāng)其它線程調(diào)用了 unpark 方法并且把當(dāng)前線程作為參數(shù)時候當(dāng)前線程也會返回。 

  7.void unpark(Object thread)方法: 喚醒調(diào)用 park 后阻塞的線程,參數(shù)為需要喚醒的線程。 

在JDK1.8中新增加了幾個方法,這里簡單的列出Long類型操作的方法如下:

  8.long getAndSetLong(Object obj, long offset, long update) 方法: 獲取對象 obj 中偏移量為 offset 的變量 volatile 語義的值,并設(shè)置變量 volatile 語義的值為 update。使用方法如下代碼:

public final long getAndSetLong(Object obj, long offset, long update)
 {
 long l;
 do
 {
 l = getLongVolatile(obj, offset);//(1)
 } while (!compareAndSwapLong(obj, offset, l, update));
 return l;
 }

從代碼中可以內(nèi)部代碼(1)處使用了getLongVolative獲取當(dāng)前變量的值,然后使用CAS原子操作進(jìn)行設(shè)置新值,這里使用while循環(huán)是考慮到多個線程同時調(diào)用的情況CAS失敗后需要自旋重試。 

  9.long getAndAddLong(Object obj, long offset, long addValue) 方法 :獲取對象 obj 中偏移量為 offset 的變量 volatile 語義的值,并設(shè)置變量值為原始值 +addValue。使用方法如下代碼:

public final long getAndAddLong(Object obj, long offset, long addValue)
 {
 long l;
 do
 {
 l = getLongVolatile(obj, offset);
 } while (!compareAndSwapLong(obj, offset, l, l + addValue));
 return l;
 }

類似于getAndSetLong的實(shí)現(xiàn),只是這里使用CAS的時候使用了原始值+傳遞的增量參數(shù)addValue的值。 

那么如何使用Unsafe類呢?

  看到 Unsafe 這個類如此牛叉,是不是很想進(jìn)行練習(xí),好了,首先看如下代碼所示:

package com.hjc;
import sun.misc.Unsafe;
/**
 * Created by cong on 2018/6/6.
 */
public class TestUnSafe {
 //獲取Unsafe的實(shí)例(2.2.1)
 static final Unsafe unsafe = Unsafe.getUnsafe();
 //記錄變量state在類TestUnSafe中的偏移值(2.2.2)
 static final long stateOffset;
 //變量(2.2.3)
 private volatile long state = 0;
 static {
 try {
  //獲取state變量在類TestUnSafe中的偏移值(2.2.4)
  stateOffset = unsafe.objectFieldOffset(TestUnSafe.class.getDeclaredField("state"));

 } catch (Exception ex) {
  System.out.println(ex.getLocalizedMessage());
  throw new Error(ex);
 }
 }

 public static void main(String[] args) {
 //創(chuàng)建實(shí)例,并且設(shè)置state值為1(2.2.5)
 TestUnSafe test = new TestUnSafe();
 //(2.2.6)
 Boolean sucess = unsafe.compareAndSwapInt(test, stateOffset, 0, 1);
 System.out.println(sucess);
 }
}

代碼(2.2.1)獲取了Unsafe的一個實(shí)例,代碼(2.2.3)創(chuàng)建了一個變量state初始化為0.

代碼(2.2.4)使用unsafe.objectFieldOffset 獲取 TestUnSafe類里面的state變量 在 TestUnSafe對象里面的內(nèi)存偏移量地址并保存到stateOffset變量。

代碼(2.2.6)調(diào)用創(chuàng)建的unsafe實(shí)例的compareAndSwapInt方法,設(shè)置test對象的state變量的值,具體意思是如果test對象內(nèi)存偏移量為stateOffset的state的變量為0,則更新改值為1 

上面代碼我們希望輸入true,然而執(zhí)行后會輸出如下結(jié)果:

Java并發(fā)編程Unsafe類的源碼分析以及Unsafe類的使用方法

為什么會這樣呢?必然需要進(jìn)入getUnsafe代碼中如看看里面做了啥:

private static final Unsafe theUnsafe = new Unsafe();
 public static Unsafe getUnsafe(){
 //(2.2.7)
 Class localClass = Reflection.getCallerClass();
 //(2.2.8)
 if (!VM.isSystemDomainLoader(localClass.getClassLoader())) {
 throw new SecurityException("Unsafe");
 }
 return theUnsafe;
}

 //判斷paramClassLoader是不是BootStrap類加載器(2.2.9)
 public static boolean isSystemDomainLoader(ClassLoader paramClassLoader){
 return paramClassLoader == null;
 }

代碼(2.2.7)獲取調(diào)用getUnsafe這個方法的對象的Class對象,這里是TestUnSafe.calss。

代碼(2.2.8)判斷是不是Bootstrap類加載器加載的localClass,這里關(guān)鍵要看是不是Bootstrap加載器加載了TestUnSafe.class??催^Java虛擬機(jī)的類加載機(jī)制的人,很明顯看出是由于TestUnSafe.class 是使用 AppClassLoader 加載的,所以這里直接拋出了異常。

那么問題來了,為什么需要有這個判斷呢?

我們知道Unsafe類是在rt.jar里面提供的,而rt.jar里面的類是使用Bootstrap類加載器加載的,而我們啟動main函數(shù)所在的類是使用AppClassLoader加載的,所以在main函數(shù)里面加載Unsafe類時候鑒于雙親委派機(jī)制會委托給Bootstrap去加載Unsafe類。

如果沒有代碼(2.2.8)這個鑒權(quán),那么我們應(yīng)用程序就可以隨意使用Unsafe做事情了,而Unsafe類可以直接操作內(nèi)存,是很不安全的,所以JDK開發(fā)組特意做了這個限制,不讓開發(fā)人員在正規(guī)渠道下使用Unsafe類,而是在rt.jar里面的核心類里面使用Unsafe功能。 

問題來了,如果我們真的想要實(shí)例化Unsafe類,使用Unsafe的功能,那該怎么辦呢?

我們不要忘記了反射這個黑科技,使用萬能的反射來獲取Unsafe的實(shí)例方法,代碼如下:

package com.hjc;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
/**
 * Created by cong on 2018/6/6.
 */
public class TestUnSafe {
 static final Unsafe unsafe;
 static final long stateOffset;
 private volatile long state = 0;
 static {
 try {
  // 反射獲取 Unsafe 的成員變量 theUnsafe(2.2.10)
  Field field = Unsafe.class.getDeclaredField("theUnsafe");
  // 設(shè)置為可存?。?.2.11)
  field.setAccessible(true);

  // 獲取該變量的值(2.2.12)
  unsafe = (Unsafe) field.get(null);
  //獲取 state 在 TestUnSafe 中的偏移量 (2.2.13)
  stateOffset = unsafe.objectFieldOffset(TestUnSafe.class.getDeclaredField("state"));
 } catch (Exception ex) {
  System.out.println(ex.getLocalizedMessage());
  throw new Error(ex);
 }
 }

 public static void main(String[] args) {
 TestUnSafe test = new TestUnSafe();
 Boolean sucess = unsafe.compareAndSwapInt(test, stateOffset, 0, 1);
 System.out.println(sucess);
 }
}

如果上面的代碼(2.2.10    2.2.11   2.2.12)反射獲取unsafe的實(shí)例,運(yùn)行結(jié)果如下:

Java并發(fā)編程Unsafe類的源碼分析以及Unsafe類的使用方法 

二.LockSupport類源碼探究

JDK中的rt.jar里面的LockSupport是一個工具類,主要作用是掛起和喚醒線程,它是創(chuàng)建鎖和其他同步類的基礎(chǔ)。

LockSupport類與每個使用他的線程都會關(guān)聯(lián)一個許可證,默認(rèn)調(diào)用LockSupport 類的方法的線程是不持有許可證的,LockSupport內(nèi)部使用Unsafe類實(shí)現(xiàn)。

這里要注意LockSupport的幾個重要的函數(shù),如下:

  1.void park() 方法: 如果調(diào)用 park() 的線程已經(jīng)拿到了與 LockSupport 關(guān)聯(lián)的許可證,則調(diào)用 LockSupport.park() 會馬上返回,否者調(diào)用線程會被禁止參與線程的調(diào)度,也就是會被阻塞掛起。例子如下代碼:

package com.hjc;
import java.util.concurrent.locks.LockSupport;
/**
 * Created by cong on 2018/6/6.
 */
public class LockSupportTest {
 public static void main( String[] args ) {
 System.out.println( "park start!" );
 LockSupport.park();
 System.out.println( "park stop!" );
 }
}

如上面代碼所示,直接在main函數(shù)里面調(diào)用park方法,最終結(jié)果只會輸出park start!  然后當(dāng)前線程會被掛起,這是因?yàn)槟J(rèn)下調(diào)用線程是不持有許可證的。運(yùn)行結(jié)果如下:

Java并發(fā)編程Unsafe類的源碼分析以及Unsafe類的使用方法 

在看到其他線程調(diào)用 unpark(Thread thread) 方法并且當(dāng)前線程作為參數(shù)時候,調(diào)用park方法被阻塞的線程會返回,另外其他線程調(diào)用了阻塞線程的interrupt()方法,設(shè)置了中斷標(biāo)志時候或者由于線程的虛假喚醒原因后阻塞線程也會返回,所以調(diào)用 park() 最好也是用循環(huán)條件判斷方式。

需要注意的是調(diào)用park()方法被阻塞的線程被其他線程中斷后阻塞線程返回時候并不會拋出InterruptedException 異常。

  2.void unpark(Thread thread) 方法 當(dāng)一個線程調(diào)用了 unpark 時候,如果參數(shù) thread 線程沒有持有 thread 與 LockSupport 類關(guān)聯(lián)的許可證,則讓 thread 線程持有。如果 thread 之前調(diào)用了 park() 被掛起,則調(diào)用 unpark 后,該線程會被喚醒。

如果 thread 之前沒有調(diào)用 park,則調(diào)用 unPark 方法后,在調(diào)用 park() 方法,會立刻返回,上面代碼修改如下:

package com.hjc;
import java.util.concurrent.locks.LockSupport;
/**
 * Created by cong on 2018/6/6.
 */
public class LockSupportTest {
 public static void main( String[] args ) {
 System.out.println( "park start!" );
 //使當(dāng)前線程獲取到許可證
 LockSupport.unpark(Thread.currentThread());
 //再次調(diào)用park
 LockSupport.park();
 System.out.println( "park stop!" );
 }
}

運(yùn)行結(jié)果如下:

Java并發(fā)編程Unsafe類的源碼分析以及Unsafe類的使用方法 

 接下來我們在看一個例子來加深對 park,unpark 的理解,代碼如下:

import java.util.concurrent.locks.LockSupport;
/**
 * Created by cong on 2018/6/6.
 */
public class LockSupportTest {
 public static void main(String[] args) throws InterruptedException {
 Thread thread = new Thread(new Runnable() {
  @Override
  public void run() {
  System.out.println("子線程 park start!");
  // 調(diào)用park方法,掛起自己
  LockSupport.park();
  System.out.println("子線程 unpark!");
  }
 });

 //啟動子線程
 thread.start();
 //主線程休眠1S
 Thread.sleep(1000);
 System.out.println("主線程 unpark start!");
 //調(diào)用unpark讓thread線程持有許可證,然后park方法會返回
 LockSupport.unpark(thread);
 }
}

運(yùn)行結(jié)果如下:

Java并發(fā)編程Unsafe類的源碼分析以及Unsafe類的使用方法 

上面的代碼首先創(chuàng)建了一個子線程thread,啟動后子線程調(diào)用park方法,由于默認(rèn)子線程沒有持有許可證,會把自己掛起

主線程休眠1s 目的是主線程在調(diào)用unpark方法讓子線程輸出 子線程park start! 并阻塞。

主線程然后執(zhí)行unpark方法,參數(shù)為子線程,目的是讓子線程持有許可證,然后子線程調(diào)用的park方法就返回了。

park方法返回時候不會告訴你是因?yàn)楹畏N原因返回,所以調(diào)用者需要根據(jù)之前是處于什么目前調(diào)用的park方法,再次檢查條件是否滿足,如果不滿足的話,還需要再次調(diào)用park方法。

例如,線程在返回時的中斷狀態(tài),根據(jù)調(diào)用前后中斷狀態(tài)對比就可以判斷是不是因?yàn)楸恢袛嗖欧祷氐摹?/p>

為了說明調(diào)用 park 方法后的線程被中斷后會返回,修改上面例子代碼,刪除 LockSupport.unpark(thread); 然后添加 thread.interrupt(); 代碼如下:

import java.util.concurrent.locks.LockSupport;
/**
 * Created by cong on 2018/6/6.
 */
public class LockSupportTest {
 public static void main(String[] args) throws InterruptedException {
 Thread thread = new Thread(new Runnable() {
  @Override
  public void run() {
  System.out.println("子線程 park start!");
  // 調(diào)用park方法,掛起自己,只有中斷才會退出循環(huán)
  while (!Thread.currentThread().isInterrupted()) {
   LockSupport.park();

  }
  System.out.println("子線程 unpark!");
  }
 });

 //啟動子線程
 thread.start();
 //主線程休眠1S
 Thread.sleep(1000);
 System.out.println("主線程 unpark start!");
 //中斷子線程
 thread.interrupt();
 }
}

運(yùn)行結(jié)果如下:

Java并發(fā)編程Unsafe類的源碼分析以及Unsafe類的使用方法

正如上面代碼,也就是只有當(dāng)子線程被中斷后子線程才會運(yùn)行結(jié)束,如果子線程不被中斷,即使你調(diào)用unPark(Thread) 子線程也不會結(jié)束。

  3.void parkNanos(long nanos)方法:和 park 類似,如果調(diào)用 park 的線程已經(jīng)拿到了與 LockSupport 關(guān)聯(lián)的許可證,則調(diào)用 LockSupport.park() 會馬上返回,不同在于如果沒有拿到許可調(diào)用線程會被掛起 nanos 時間后在返回。

park 還支持三個帶有blocker參數(shù)的方法,當(dāng)線程因?yàn)闆]有持有許可證的情況下調(diào)用park  被阻塞掛起的時候,這個blocker對象會被記錄到該線程內(nèi)部。

使用診斷工具可以觀察線程被阻塞的原因,診斷工具是通過調(diào)用getBlocker(Thread)方法來獲取該blocker對象的,所以JDK推薦我們使用帶有blocker參數(shù)的park方法,并且blocker設(shè)置為this,這樣當(dāng)內(nèi)存dump排查問題時候就能知道是哪個類被阻塞了。

例子如下:

import java.util.concurrent.locks.LockSupport;
/**
 * Created by cong on 2018/6/6.
 */
public class TestPark {
 public void testPark(){
  LockSupport.park();//(1)
 }
 public static void main(String[] args) {
  TestPark testPark = new TestPark();
  testPark.testPark();
 }
}

運(yùn)行結(jié)果如下:

Java并發(fā)編程Unsafe類的源碼分析以及Unsafe類的使用方法

可以看到運(yùn)行在阻塞,那么我們要使用JDK/bin目錄下的工具看一下了,如果不知道的讀者,建議去先看一下JVM的監(jiān)控工具。

運(yùn)行后使用jstack pid 查看線程堆棧的時候,可以看到的結(jié)果如下:

Java并發(fā)編程Unsafe類的源碼分析以及Unsafe類的使用方法

Java并發(fā)編程Unsafe類的源碼分析以及Unsafe類的使用方法

然后我們進(jìn)行上面的代碼(1)進(jìn)行修改如下:

 LockSupport.park(this);//(1)

再次運(yùn)行,再用jstack pid 查看的結(jié)果如下:

Java并發(fā)編程Unsafe類的源碼分析以及Unsafe類的使用方法

可以知道,帶blocker的park方法后,線程堆??梢蕴峁└嘤嘘P(guān)阻塞對象的信息。

那么我們接下來進(jìn)行park(Object blocker) 函數(shù)的源代碼查看,源碼如下:

public static void park(Object blocker) {
 //獲取調(diào)用線程
 Thread t = Thread.currentThread();
 //設(shè)置該線程的 blocker 變量
 setBlocker(t, blocker);
 //掛起線程
 UNSAFE.park(false, 0L);
 //線程被激活后清除 blocker 變量,因?yàn)橐话愣际蔷€程阻塞時候才分析原因
 setBlocker(t, null);
}

Thread類里面有個變量volatile Object parkBlocker 用來存放park傳遞的blocker對象,也就是把blocker變量存放到了調(diào)用park方法的線程的成員變量里面

  4.void parkNanos(Object blocker, long nanos) 函數(shù) 相比 park(Object blocker) 多了個超時時間。

  5.void parkUntil(Object blocker, long deadline)  parkUntil源代碼如下:

public static void parkUntil(Object blocker, long deadline) {
  Thread t = Thread.currentThread();
  setBlocker(t, blocker);
  //isAbsolute=true,time=deadline;表示到 deadline 時間時候后返回
  UNSAFE.park(true, deadline);
  setBlocker(t, null);
 }

可以看到是一個設(shè)置deadline,時間單位為milliseconds,是從1970到現(xiàn)在某一個時間點(diǎn)換算為毫秒后的值,這個和parkNanos(Object blocker,long nanos)區(qū)別是后者是從當(dāng)前算等待nanos時間的,而前者是指定一個時間點(diǎn),

比如我們需要等待到2018.06.06 日 20:34,則把這個時間點(diǎn)轉(zhuǎn)換為從1970年到這個時間點(diǎn)的總毫秒數(shù)。

我們再來看一個例子,代碼如下:

import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.LockSupport;
/**
 * Created by cong on 2018/6/6.
 */
public class FIFOMutex {
 private final AtomicBoolean locked = new AtomicBoolean(false);
 private final Queue<Thread> waiters = new ConcurrentLinkedQueue<Thread>();
 public void lock() {
  boolean wasInterrupted = false;
  Thread current = Thread.currentThread();
  waiters.add(current);
  // 只有隊(duì)首的線程可以獲取鎖(1)
  while (waiters.peek() != current || !locked.compareAndSet(false, true)) {
   LockSupport.park(this);
   if (Thread.interrupted()) // (2)
    wasInterrupted = true;
  }

  waiters.remove();
  if (wasInterrupted) // (3)
   current.interrupt();
 }

 public void unlock() {
  locked.set(false);
  LockSupport.unpark(waiters.peek());
 } 
}

可以看到這是一個先進(jìn)先出的鎖,也就是只有隊(duì)列首元素可以獲取所,代碼(1)如果當(dāng)前線程不是隊(duì)首或者當(dāng)前鎖已經(jīng)被其他線程獲取,則調(diào)用park方法掛起自己。

接著代碼(2)做判斷,如果park方法是因?yàn)楸恢袛喽祷?,則忽略中斷,并且重置中斷標(biāo)志,只做個標(biāo)記,然后再次判斷當(dāng)前線程是不是隊(duì)首元素或者當(dāng)先鎖是否已經(jīng)被其他線程獲取,如果是則繼續(xù)調(diào)用park方法掛起自己。

然后代碼(3)中如果標(biāo)記為true 則中斷該線程,這個怎么理解呢?其實(shí)就是其他線程中斷了該線程,雖然我對中斷信號不感興趣,忽略它,但是不代表其他線程對該標(biāo)志不感興趣,所以要恢復(fù)下。

“Java并發(fā)編程Unsafe類的源碼分析以及Unsafe類的使用方法”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI