Java并發(fā)編程學(xué)習(xí)之Unsafe類與LockSupport類源碼詳析
一.Unsafe類的源碼分析
JDK的rt.jar包中的Unsafe類提供了硬件級別的原子操作,Unsafe里面的方法都是native方法,通過使用JNI的方式來訪問本地C++實現(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 表示阻塞后到指定的時間點后會被喚醒,這里 time 是個絕對的時間,是某一個時間點換算為 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原子操作進行設(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的實現(xiàn),只是這里使用CAS的時候使用了原始值+傳遞的增量參數(shù)addValue的值。
那么如何使用Unsafe類呢?
看到 Unsafe 這個類如此牛叉,是不是很想進行練習(xí),好了,首先看如下代碼所示:
package com.hjc;
import sun.misc.Unsafe;
/**
* Created by cong on 2018/6/6.
*/
public class TestUnSafe {
//獲取Unsafe的實例(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è)置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的一個實例,代碼(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實例的compareAndSwapInt方法,設(shè)置test對象的state變量的值,具體意思是如果test對象內(nèi)存偏移量為stateOffset的state的變量為0,則更新改值為1
上面代碼我們希望輸入true,然而執(zhí)行后會輸出如下結(jié)果:

為什么會這樣呢?必然需要進入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虛擬機的類加載機制的人,很明顯看出是由于TestUnSafe.class 是使用 AppClassLoader 加載的,所以這里直接拋出了異常。
那么問題來了,為什么需要有這個判斷呢?
我們知道Unsafe類是在rt.jar里面提供的,而rt.jar里面的類是使用Bootstrap類加載器加載的,而我們啟動main函數(shù)所在的類是使用AppClassLoader加載的,所以在main函數(shù)里面加載Unsafe類時候鑒于雙親委派機制會委托給Bootstrap去加載Unsafe類。
如果沒有代碼(2.2.8)這個鑒權(quán),那么我們應(yīng)用程序就可以隨意使用Unsafe做事情了,而Unsafe類可以直接操作內(nèi)存,是很不安全的,所以JDK開發(fā)組特意做了這個限制,不讓開發(fā)人員在正規(guī)渠道下使用Unsafe類,而是在rt.jar里面的核心類里面使用Unsafe功能。
問題來了,如果我們真的想要實例化Unsafe類,使用Unsafe的功能,那該怎么辦呢?
我們不要忘記了反射這個黑科技,使用萬能的反射來獲取Unsafe的實例方法,代碼如下:
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.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的實例,運行結(jié)果如下:
二.LockSupport類源碼探究
JDK中的rt.jar里面的LockSupport是一個工具類,主要作用是掛起和喚醒線程,它是創(chuàng)建鎖和其他同步類的基礎(chǔ)。
LockSupport類與每個使用他的線程都會關(guān)聯(lián)一個許可證,默認調(diào)用LockSupport 類的方法的線程是不持有許可證的,LockSupport內(nèi)部使用Unsafe類實現(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)前線程會被掛起,這是因為默認下調(diào)用線程是不持有許可證的。運行結(jié)果如下:
在看到其他線程調(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!" );
}
}
運行結(jié)果如下:
接下來我們在看一個例子來加深對 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);
}
}
運行結(jié)果如下:
上面的代碼首先創(chuàng)建了一個子線程thread,啟動后子線程調(diào)用park方法,由于默認子線程沒有持有許可證,會把自己掛起
主線程休眠1s 目的是主線程在調(diào)用unpark方法讓子線程輸出 子線程park start! 并阻塞。
主線程然后執(zhí)行unpark方法,參數(shù)為子線程,目的是讓子線程持有許可證,然后子線程調(diào)用的park方法就返回了。
park方法返回時候不會告訴你是因為何種原因返回,所以調(diào)用者需要根據(jù)之前是處于什么目前調(diào)用的park方法,再次檢查條件是否滿足,如果不滿足的話,還需要再次調(diào)用park方法。
例如,線程在返回時的中斷狀態(tài),根據(jù)調(diào)用前后中斷狀態(tài)對比就可以判斷是不是因為被中斷才返回的。
為了說明調(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();
}
}
運行結(jié)果如下:

正如上面代碼,也就是只有當(dāng)子線程被中斷后子線程才會運行結(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)線程因為沒有持有許可證的情況下調(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();
}
}
運行結(jié)果如下:

可以看到運行在阻塞,那么我們要使用JDK/bin目錄下的工具看一下了,如果不知道的讀者,建議去先看一下JVM的監(jiān)控工具。
運行后使用jstack pid 查看線程堆棧的時候,可以看到的結(jié)果如下:


然后我們進行上面的代碼(1)進行修改如下:
LockSupport.park(this);//(1)
再次運行,再用jstack pid 查看的結(jié)果如下:

可以知道,帶blocker的park方法后,線程堆??梢蕴峁└嘤嘘P(guā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 變量,因為一般都是線程阻塞時候才分析原因
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)在某一個時間點換算為毫秒后的值,這個和parkNanos(Object blocker,long nanos)區(qū)別是后者是從當(dāng)前算等待nanos時間的,而前者是指定一個時間點,
比如我們需要等待到2018.06.06 日 20:34,則把這個時間點轉(zhuǎn)換為從1970年到這個時間點的總毫秒數(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);
// 只有隊首的線程可以獲取鎖(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());
}
}
可以看到這是一個先進先出的鎖,也就是只有隊列首元素可以獲取所,代碼(1)如果當(dāng)前線程不是隊首或者當(dāng)前鎖已經(jīng)被其他線程獲取,則調(diào)用park方法掛起自己。
接著代碼(2)做判斷,如果park方法是因為被中斷而返回,則忽略中斷,并且重置中斷標(biāo)志,只做個標(biāo)記,然后再次判斷當(dāng)前線程是不是隊首元素或者當(dāng)先鎖是否已經(jīng)被其他線程獲取,如果是則繼續(xù)調(diào)用park方法掛起自己。
然后代碼(3)中如果標(biāo)記為true 則中斷該線程,這個怎么理解呢?其實就是其他線程中斷了該線程,雖然我對中斷信號不感興趣,忽略它,但是不代表其他線程對該標(biāo)志不感興趣,所以要恢復(fù)下。
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
相關(guān)文章
IDEA2023隱藏.idea和.iml文件的實現(xiàn)步驟
IDEA新建項目會自動生成一個.idea文件夾和.iml文件,本文主要介紹了IDEA2023隱藏.idea和.iml文件的實現(xiàn)步驟,具有一定的參考價值,感興趣的可以了解一下2023-09-09
Mybatis-Plus 條件構(gòu)造器 QueryWrapper 的基本用法
這篇文章主要介紹了Mybatis-Plus - 條件構(gòu)造器 QueryWrapper 的使用,通過實例代碼給大家介紹了查詢示例代碼及實現(xiàn)需求,代碼簡單易懂,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-09-09

