本文轉(zhuǎn)自網(wǎng)絡(luò),侵刪
本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內(nèi)容請到我的倉庫里查看
喜歡的話麻煩點(diǎn)下Star哈
文章同步發(fā)于我的個人博客:
本文是微信公眾號【Java技術(shù)江湖】的《Java并發(fā)指南》其中一篇,本文大部分內(nèi)容來源于網(wǎng)絡(luò),為了把本文主題講得清晰透徹,也整合了很多我認(rèn)為不錯的技術(shù)博客內(nèi)容,引用其中了一些比較好的博客文章,如有侵權(quán),請聯(lián)系作者。
該系列博文會告訴你如何全面深入地學(xué)習(xí)Java并發(fā)技術(shù),從Java多線程基礎(chǔ),再到并發(fā)編程的基礎(chǔ)知識,從Java并發(fā)包的入門和實(shí)戰(zhàn),再到JUC的源碼剖析,一步步地學(xué)習(xí)Java并發(fā)編程,并上手進(jìn)行實(shí)戰(zhàn),以便讓你更完整地了解整個Java并發(fā)編程知識體系,形成自己的知識框架。
為了更好地總結(jié)和檢驗(yàn)?zāi)愕膶W(xué)習(xí)成果,本系列文章也會提供一些對應(yīng)的面試題以及參考答案。
如果對本系列文章有什么建議,或者是有什么疑問的話,也可以關(guān)注公眾號【Java技術(shù)江湖】聯(lián)系作者,歡迎你參與本系列博文的創(chuàng)作和修訂。
前言
最近在看Java并發(fā)包的源碼,發(fā)現(xiàn)了神奇的Unsafe類,仔細(xì)研究了一下,在這里跟大家分享一下。

Unsafe類是在sun.misc包下,不屬于Java標(biāo)準(zhǔn)。但是很多Java的基礎(chǔ)類庫,包括一些被廣泛使用的高性能開發(fā)庫都是基于Unsafe類開發(fā)的,比如Netty、Cassandra、Hadoop、Kafka等。Unsafe類在提升Java運(yùn)行效率,增強(qiáng)Java語言底層操作能力方面起了很大的作用。


Unsafe類使Java擁有了像C語言的指針一樣操作內(nèi)存空間的能力,同時也帶來了指針的問題。過度的使用Unsafe類會使得出錯的幾率變大,因此Java官方并不建議使用的,官方文檔也幾乎沒有。Oracle正在計劃從Java 9中去掉Unsafe類,如果真是如此影響就太大了。

通常我們最好也不要使用Unsafe類,除非有明確的目的,并且也要對它有深入的了解才行。要想使用Unsafe類需要用一些比較tricky的辦法。Unsafe類使用了單例模式,需要通過一個靜態(tài)方法getUnsafe()來獲取。但Unsafe類做了限制,如果是普通的調(diào)用的話,它會拋出一個SecurityException異常;只有由主類加載器加載的類才能調(diào)用這個方法。其源碼如下:
1 public static Unsafe getUnsafe() {
2 Class var0 = Reflection.getCallerClass();
3 if(!VM.isSystemDomainLoader(var0.getClassLoader())) {
4 throw new SecurityException("Unsafe");
5 } else {
6 return theUnsafe;
7 }
8 }
網(wǎng)上也有一些辦法來用主類加載器加載用戶代碼,比如設(shè)置bootclasspath參數(shù)。但更簡單方法是利用Java反射,方法如下:
1 Field f = Unsafe.class.getDeclaredField("theUnsafe");
2 f.setAccessible(true);
3 Unsafe unsafe = (Unsafe) f.get(null);
獲取到Unsafe實(shí)例之后,我們就可以為所欲為了。Unsafe類提供了以下這些功能:
一、內(nèi)存管理。包括分配內(nèi)存、釋放內(nèi)存等。
該部分包括了allocateMemory(分配內(nèi)存)、reallocateMemory(重新分配內(nèi)存)、copyMemory(拷貝內(nèi)存)、freeMemory(釋放內(nèi)存 )、getAddress(獲取內(nèi)存地址)、addressSize、pageSize、getInt(獲取內(nèi)存地址指向的整數(shù))、getIntVolatile(獲取內(nèi)存地址指向的整數(shù),并支持volatile語義)、putInt(將整數(shù)寫入指定內(nèi)存地址)、putIntVolatile(將整數(shù)寫入指定內(nèi)存地址,并支持volatile語義)、putOrderedInt(將整數(shù)寫入指定內(nèi)存地址、有序或者有延遲的方法)等方法。getXXX和putXXX包含了各種基本類型的操作。
利用copyMemory方法,我們可以實(shí)現(xiàn)一個通用的對象拷貝方法,無需再對每一個對象都實(shí)現(xiàn)clone方法,當(dāng)然這通用的方法只能做到對象淺拷貝。
二、非常規(guī)的對象實(shí)例化。
allocateInstance()方法提供了另一種創(chuàng)建實(shí)例的途徑。通常我們可以用new或者反射來實(shí)例化對象,使用allocateInstance()方法可以直接生成對象實(shí)例,且無需調(diào)用構(gòu)造方法和其它初始化方法。
這在對象反序列化的時候會很有用,能夠重建和設(shè)置final字段,而不需要調(diào)用構(gòu)造方法。
三、操作類、對象、變量。
這部分包括了staticFieldOffset(靜態(tài)域偏移)、defineClass(定義類)、defineAnonymousClass(定義匿名類)、ensureClassInitialized(確保類初始化)、objectFieldOffset(對象域偏移)等方法。
通過這些方法我們可以獲取對象的指針,通過對指針進(jìn)行偏移,我們不僅可以直接修改指針指向的數(shù)據(jù)(即使它們是私有的),甚至可以找到JVM已經(jīng)認(rèn)定為垃圾、可以進(jìn)行回收的對象。
四、數(shù)組操作。
這部分包括了arrayBaseOffset(獲取數(shù)組第一個元素的偏移地址)、arrayIndexScale(獲取數(shù)組中元素的增量地址)等方法。arrayBaseOffset與arrayIndexScale配合起來使用,就可以定位數(shù)組中每個元素在內(nèi)存中的位置。
由于Java的數(shù)組最大值為Integer.MAX_VALUE,使用Unsafe類的內(nèi)存分配方法可以實(shí)現(xiàn)超大數(shù)組。實(shí)際上這樣的數(shù)據(jù)就可以認(rèn)為是C數(shù)組,因此需要注意在合適的時間釋放內(nèi)存。
五、多線程同步。包括鎖機(jī)制、CAS操作等。
這部分包括了monitorEnter、tryMonitorEnter、monitorExit、compareAndSwapInt、compareAndSwap等方法。
其中monitorEnter、tryMonitorEnter、monitorExit已經(jīng)被標(biāo)記為deprecated,不建議使用。
Unsafe類的CAS操作可能是用的最多的,它為Java的鎖機(jī)制提供了一種新的解決辦法,比如AtomicInteger等類都是通過該方法來實(shí)現(xiàn)的。compareAndSwap方法是原子的,可以避免繁重的鎖機(jī)制,提高代碼效率。這是一種樂觀鎖,通常認(rèn)為在大部分情況下不出現(xiàn)競態(tài)條件,如果操作失敗,會不斷重試直到成功。
六、掛起與恢復(fù)。
這部分包括了park、unpark等方法。
將一個線程進(jìn)行掛起是通過park方法實(shí)現(xiàn)的,調(diào)用 park后,線程將一直阻塞直到超時或者中斷等條件出現(xiàn)。unpark可以終止一個掛起的線程,使其恢復(fù)正常。整個并發(fā)框架中對線程的掛起操作被封裝在 LockSupport類中,LockSupport類中有各種版本pack方法,但最終都調(diào)用了Unsafe.park()方法。
七、內(nèi)存屏障。
這部分包括了loadFence、storeFence、fullFence等方法。這是在Java 8新引入的,用于定義內(nèi)存屏障,避免代碼重排序。
loadFence() 表示該方法之前的所有l(wèi)oad操作在內(nèi)存屏障之前完成。同理storeFence()表示該方法之前的所有store操作在內(nèi)存屏障之前完成。fullFence()表示該方法之前的所有l(wèi)oad、store操作在內(nèi)存屏障之前完成。
Unsafe類是啥?
Java最初被設(shè)計為一種安全的受控環(huán)境。盡管如此,Java HotSpot還是包含了一個“后門”,提供了一些可以直接操控內(nèi)存和線程的低層次操作。這個后門類——sun.misc.Unsafe——被JDK廣泛用于自己的包中,如java.nio和java.util.concurrent。但是絲毫不建議在生產(chǎn)環(huán)境中使用這個后門。因?yàn)檫@個API十分不安全、不輕便、而且不穩(wěn)定。這個不安全的類提供了一個觀察HotSpot JVM內(nèi)部結(jié)構(gòu)并且可以對其進(jìn)行修改。有時它可以被用來在不適用C++調(diào)試的情況下學(xué)習(xí)虛擬機(jī)內(nèi)部結(jié)構(gòu),有時也可以被拿來做性能監(jiān)控和開發(fā)工具。
為什么叫Unsafe?
Java官方不推薦使用Unsafe類,因?yàn)楣俜秸J(rèn)為,這個類別人很難正確使用,非正確使用會給JVM帶來致命錯誤。而且未來Java可能封閉丟棄這個類。
1、簡單介紹
?? LockSupport是JDK中比較底層的類,用來創(chuàng)建鎖和其他同步工具類的基本線程阻塞原語??梢宰龅脚cjoin() 、wait()/notifyAll() 功能一樣,使線程自由的阻塞、釋放。
?? Java鎖和同步器框架的核心AQS(AbstractQueuedSynchronizer 抽象隊(duì)列同步器),就是通過調(diào)用LockSupport.park()和LockSupport.unpark()實(shí)現(xiàn)線程的阻塞和喚醒的。
補(bǔ)充:AQS定義了一套多線程訪問共享資源的同步器框架,許多同步類實(shí)現(xiàn)都依賴于它,
如常用的ReentrantLock/Semaphore/CountDownLatch...。
2、簡單原理
??LockSupport方法底層都是調(diào)用Unsafe的方法實(shí)現(xiàn)。全名sun.misc.Unsafe,該類可以直接操控內(nèi)存,被JDK廣泛用于自己的包中,如java.nio和java.util.concurrent。但是不建議在生產(chǎn)環(huán)境中使用這個類。因?yàn)檫@個API十分不安全、不輕便、而且不穩(wěn)定。
LockSupport提供park()和unpark()方法實(shí)現(xiàn)阻塞線程和解除線程阻塞,LockSupport和每個使用它的線程都與一個許可(permit)關(guān)聯(lián)。permit是相當(dāng)于1,0的開關(guān),默認(rèn)是0,調(diào)用一次unpark就加1變成1,調(diào)用一次park會消費(fèi)permit, 也就會將1變成0,同時park立即返回。
再次調(diào)用park會變成block(因?yàn)閜ermit為0了,會阻塞在這里,直到permit變?yōu)?), 這時調(diào)用unpark會把permit置為1。每個線程都有一個相關(guān)的permit, permit最多只有一個,重復(fù)調(diào)用unpark也不會積累。意思就是說unpark 之后,如果permit 已經(jīng)變?yōu)?,之后,再執(zhí)行unpark ,permit 依舊是1。下邊有例子會說到。
3、簡單例子
??以下邊的做飯例子,正常來說,做飯 之前,要有鍋、有菜才能開始做飯 。具體如下:
(1)先假設(shè)已經(jīng)有了鍋 ,那只需要買菜就可以做飯。如下,即注釋掉了買鍋的步驟:
public class LockSupportTest {
public static void main(String[] args) throws InterruptedException {
//買鍋
// Thread t1 = new Thread(new BuyGuo(Thread.currentThread()));
// t1.start();
//買菜
Thread t2 = new Thread(new BuyCai(Thread.currentThread()));
t2.start();
// LockSupport.park();
// System.out.println("鍋買回來了...");
LockSupport.park();
System.out.println("菜買回來 了...");
System.out.println("開始做飯");
}
}
class BuyGuo implements Runnable{
private Object threadObj;
public BuyGuo(Object threadObj) {
this.threadObj = threadObj;
}
@Override
public void run() {
System.out.println("去買鍋...");
LockSupport.unpark((Thread)threadObj);
}
}
class BuyCai implements Runnable{
private Object threadObj;
public BuyCai(Object threadObj) {
this.threadObj = threadObj;
}
@Override
public void run() {
System.out.println("買菜去...");
LockSupport.unpark((Thread)threadObj);
}
}
執(zhí)行后,可出現(xiàn)下面的結(jié)果:
買菜去...
菜買回來了...
開始做飯
如上所述,可以達(dá)到阻塞主線程等到買完菜之后才開始做飯。這即是park()、unpark() 的用法。簡單解釋一下上述的步驟:
main 方法啟動后,主線程 和 買菜線程 同時開始執(zhí)行。
因?yàn)閮烧咄瑫r進(jìn)行,當(dāng)主線程 走到park() 時,發(fā)現(xiàn)permit 還為0 ,即會等待在這里。
當(dāng)買菜線程執(zhí)行進(jìn)去后,走到unpark() 會將permit 變?yōu)? 。
主線程 park() 處發(fā)現(xiàn)permit 已經(jīng)變成1 ,就可以繼續(xù)往下執(zhí)行了,同時消費(fèi)掉permit ,重新變成0 。
?? 以上permit 只是park/unpark 執(zhí)行的一種邏輯開關(guān),執(zhí)行的步驟大致如此。
4、注意點(diǎn)及思考
(1)必須將park()與uppark() 配對使用才更高效。
??如果上邊也把買鍋的線程放開,main 方法改為如下:
//買鍋
Thread t1 = new Thread(new BuyGuo(Thread.currentThread()));
t1.start();
//買菜
Thread t2 = new Thread(new BuyCai(Thread.currentThread()));
t2.start();
LockSupport.park();
System.out.println("鍋買回來了...");
LockSupport.park();
System.out.println("菜買回來了...");
System.out.println("開始做飯");
即調(diào)用了兩次park() 和unpark() ,發(fā)現(xiàn)有時候可以,有時候會使線程卡在那里,然后我又換了下順序,如下:
//買鍋
Thread t1 = new Thread(new BuyGuo(Thread.currentThread()));
t1.start();
LockSupport.park();
System.out.println("鍋買回來了...");
//買菜
Thread t2 = new Thread(new BuyCai(Thread.currentThread()));
t2.start();
LockSupport.park();
System.out.println("菜買回來了...");
System.out.println("開始做飯");
原理沒有詳細(xì)去研究,不過想了想,上邊兩種其實(shí)并無區(qū)別,只是執(zhí)行順序有了影響,park() 和unpark() 既然是成對配合使用,通過標(biāo)識permit 來控制,如果像前邊那個例子那樣,出現(xiàn)阻塞的情況原因,我分析可能是這么個原因:
當(dāng)買鍋的時候,通過unpark()將permit 置為1,但是還沒等到外邊的main方法執(zhí)行第一個park() ,買菜的線程又調(diào)了一次unpark(),但是這時候permit 還是從1變成了1,等回到主線程調(diào)用park()的時候,因?yàn)檫€有兩個park()需要執(zhí)行,也就是需要兩個消費(fèi)permit ,因?yàn)閜ermit 只有1個,所以,可能會剩下一個park()卡在那里了。
(2)使用park(Object blocker) 方法更能明確問題
??其實(shí)park() 有個重載方法park(Object blocker) ,這倆方法效果差不多,但是有blocker的可以傳遞給開發(fā)人員更多的現(xiàn)場信息,可以查看到當(dāng)前線程的阻塞對象,方便定位問題。所以java6新增加帶blocker入?yún)⒌南盗衟ark方法,替代原有的park方法。
5、與wait()/notifyAll() 的比較
LockSupport 的 park/unpark 方法,雖然與平時Object 中wait/notify 同樣達(dá)到阻塞線程的效果。但是它們之間還是有區(qū)別的。
面向的對象主體不同。LockSupport() 操作的是線程對象,直接傳入的就是Thread ,而wait() 屬于具體對象,notifyAll() 也是針對所有線程進(jìn)行喚醒。
wait/notify 需要獲取對象的監(jiān)視器,即synchronized修飾,而park/unpark 不需要獲取對象的監(jiān)視器。
實(shí)現(xiàn)的機(jī)制不同,因此兩者沒有交集。也就是說 LockSupport 阻塞的線程,notify/notifyAll 沒法喚醒。但是 park 之后,同樣可以被中斷(interrupt()) !
JAVA高并發(fā)—LockSupport的學(xué)習(xí)及簡單使用
1、簡單介紹
?? LockSupport是JDK中比較底層的類,用來創(chuàng)建鎖和其他同步工具類的基本線程阻塞原語??梢宰龅脚cjoin() 、wait()/notifyAll() 功能一樣,使線程自由的阻塞、釋放。
Java鎖和同步器框架的核心AQS(AbstractQueuedSynchronizer 抽象隊(duì)列同步器),就是通過調(diào)用LockSupport.park()和LockSupport.unpark()實(shí)現(xiàn)線程的阻塞和喚醒的。
補(bǔ)充:AQS定義了一套多線程訪問共享資源的同步器框架,許多同步類實(shí)現(xiàn)都依賴于它,
如常用的ReentrantLock/Semaphore/CountDownLatch...。
2、簡單原理
??LockSupport方法底層都是調(diào)用Unsafe的方法實(shí)現(xiàn)。全名sun.misc.Unsafe,該類可以直接操控內(nèi)存,被JDK廣泛用于自己的包中,如java.nio和java.util.concurrent。但是不建議在生產(chǎn)環(huán)境中使用這個類。因?yàn)檫@個API十分不安全、不輕便、而且不穩(wěn)定。
LockSupport提供park()和unpark()方法實(shí)現(xiàn)阻塞線程和解除線程阻塞,LockSupport和每個使用它的線程都與一個許可(permit)關(guān)聯(lián)。
permit是相當(dāng)于1,0的開關(guān),默認(rèn)是0,調(diào)用一次unpark就加1變成1,調(diào)用一次park會消費(fèi)permit, 也就會將1變成0,同時park立即返回。再次調(diào)用park會變成block(因?yàn)閜ermit為0了,會阻塞在這里,直到permit變?yōu)?), 這時調(diào)用unpark會把permit置為1。
每個線程都有一個相關(guān)的permit, permit最多只有一個,重復(fù)調(diào)用unpark也不會積累。意思就是說unpark 之后,如果permit 已經(jīng)變?yōu)?,之后,再執(zhí)行unpark ,permit 依舊是1。下邊有例子會說到。
3、簡單例子
??以下邊的做飯例子,正常來說,做飯 之前,要有鍋、有菜才能開始做飯 。具體如下:
(1)先假設(shè)已經(jīng)有了鍋 ,那只需要買菜就可以做飯。如下,即注釋掉了買鍋的步驟:
public class LockSupportTest {
public static void main(String[] args) throws InterruptedException {
//買鍋
// Thread t1 = new Thread(new BuyGuo(Thread.currentThread()));
// t1.start();
//買菜
Thread t2 = new Thread(new BuyCai(Thread.currentThread()));
t2.start();
// LockSupport.park();
// System.out.println("鍋買回來了...");
LockSupport.park();
System.out.println("菜買回來 了...");
System.out.println("開始做飯");
}
}
class BuyGuo implements Runnable{
private Object threadObj;
public BuyGuo(Object threadObj) {
this.threadObj = threadObj;
}
@Override
public void run() {
System.out.println("去買鍋...");
LockSupport.unpark((Thread)threadObj);
}
}
class BuyCai implements Runnable{
private Object threadObj;
public BuyCai(Object threadObj) {
this.threadObj = threadObj;
}
@Override
public void run() {
System.out.println("買菜去...");
LockSupport.unpark((Thread)threadObj);
}
}
執(zhí)行后,可出現(xiàn)下面的結(jié)果:
買菜去...
菜買回來了...
開始做飯
如上所述,可以達(dá)到阻塞主線程等到買完菜之后才開始做飯。這即是park()、unpark() 的用法。簡單解釋一下上述的步驟:
main 方法啟動后,主線程 和 買菜線程 同時開始執(zhí)行。
因?yàn)閮烧咄瑫r進(jìn)行,當(dāng)主線程 走到park() 時,發(fā)現(xiàn)permit 還為0 ,即會等待在這里。
當(dāng)買菜線程執(zhí)行進(jìn)去后,走到unpark() 會將permit 變?yōu)? 。
主線程 park() 處發(fā)現(xiàn)permit 已經(jīng)變成1 ,就可以繼續(xù)往下執(zhí)行了,同時消費(fèi)掉permit ,重新變成0 。
?? 以上permit 只是park/unpark 執(zhí)行的一種邏輯開關(guān),執(zhí)行的步驟大致如此。
4、注意點(diǎn)及思考
(1)必須將park()與uppark() 配對使用才更高效。
??如果上邊也把買鍋的線程放開,main 方法改為如下:
//買鍋
Thread t1 = new Thread(new BuyGuo(Thread.currentThread()));
t1.start();
//買菜
Thread t2 = new Thread(new BuyCai(Thread.currentThread()));
t2.start();
LockSupport.park();
System.out.println("鍋買回來了...");
LockSupport.park();
System.out.println("菜買回來了...");
System.out.println("開始做飯");
即調(diào)用了兩次park() 和unpark() ,發(fā)現(xiàn)有時候可以,有時候會使線程卡在那里,然后我又換了下順序,如下:
//買鍋
Thread t1 = new Thread(new BuyGuo(Thread.currentThread()));
t1.start();
LockSupport.park();
System.out.println("鍋買回來了...");
//買菜
Thread t2 = new Thread(new BuyCai(Thread.currentThread()));
t2.start();
LockSupport.park();
System.out.println("菜買回來了...");
System.out.println("開始做飯");
原理沒有詳細(xì)去研究,不過想了想,上邊兩種其實(shí)并無區(qū)別,只是執(zhí)行順序有了影響,park() 和unpark() 既然是成對配合使用,通過標(biāo)識permit 來控制,如果像前邊那個例子那樣,出現(xiàn)阻塞的情況原因,我分析可能是這么個原因:
當(dāng)買鍋的時候,通過unpark()將permit 置為1,但是還沒等到外邊的main方法執(zhí)行第一個park() ,買菜的線程又調(diào)了一次unpark(),但是這時候permit 還是從1變成了1,等回到主線程調(diào)用park()的時候,因?yàn)檫€有兩個park()需要執(zhí)行,也就是需要兩個消費(fèi)permit ,因?yàn)閜ermit 只有1個,所以,可能會剩下一個park()卡在那里了。
(2)使用park(Object blocker) 方法更能明確問題
??其實(shí)park() 有個重載方法park(Object blocker) ,這倆方法效果差不多,但是有blocker的可以傳遞給開發(fā)人員更多的現(xiàn)場信息,可以查看到當(dāng)前線程的阻塞對象,方便定位問題。所以java6新增加帶blocker入?yún)⒌南盗衟ark方法,替代原有的park方法。
5、與wait()/notifyAll() 的比較
LockSupport 的 park/unpark 方法,雖然與平時Object 中wait/notify 同樣達(dá)到阻塞線程的效果。但是它們之間還是有區(qū)別的。
面向的對象主體不同。LockSupport() 操作的是線程對象,直接傳入的就是Thread ,而wait() 屬于具體對象,notifyAll() 也是針對所有線程進(jìn)行喚醒。
wait/notify 需要獲取對象的監(jiān)視器,即synchronized修飾,而park/unpark 不需要獲取對象的監(jiān)視器。
實(shí)現(xiàn)的機(jī)制不同,因此兩者沒有交集。也就是說 LockSupport 阻塞的線程,notify/notifyAll 沒法喚醒。但是 park 之后,同樣可以被中斷(interrupt()) !