您好,登錄后才能下訂單哦!
本篇內容介紹了“Java并發編程Unsafe類的源碼分析以及Unsafe類的使用方法”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
一.Unsafe類的源碼分析
JDK的rt.jar包中的Unsafe類提供了硬件級別的原子操作,Unsafe里面的方法都是native方法,通過使用JNI的方式來訪問本地C++實現庫。
rt.jar 中 Unsafe 類主要函數講解, Unsafe 類提供了硬件級別的原子操作,可以安全的直接操作內存變量,其在 JUC 源碼中被廣泛的使用,了解其原理為研究 JUC 源碼奠定了基礎。
首先我們先了解Unsafe類中主要方法的使用,如下:
1.long objectFieldOffset(Field field) 方法:返回指定的變量在所屬類的內存偏移地址,偏移地址僅僅在該Unsafe函數中訪問指定字段時使用。如下代碼使用unsafe獲取AtomicLong中變量value在AtomicLong對象中的內存偏移,代碼如下:
static { try { valueOffset = unsafe.objectFieldOffset(AtomicLong.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } }
2.int arrayBaseOffset(Class arrayClass)方法:獲取數組中第一個元素的地址
3.int arrayIndexScale(Class arrayClass)方法:獲取數組中單個元素占用的字節數
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的變量對應的volative內存語義的值。
5.void putOrderedLong(Object obj, long offset, long value) 方法:設置 obj 對象中 offset 偏移地址對應的 long 型 field 的值為 value。這是有延遲的 putLongVolatile 方法,并不保證值修改對其它線程立刻可見。變量只有使用 volatile 修飾并且期望被意外修改的時候使用才有用。
6.void park(boolean isAbsolute, long time) 方法:阻塞當前線程,其中參數 isAbsolute 等于 false 時候,time 等于 0 表示一直阻塞,time 大于 0 表示等待指定的 time 后阻塞線程會被喚醒,這個 time 是個相對值,是個增量值,也就是相對當前時間累加 time 后當前線程就會被喚醒。 如果 isAbsolute 等于 true,并且 time 大于 0 表示阻塞后到指定的時間點后會被喚醒,這里 time 是個絕對的時間,是某一個時間點換算為 ms 后的值。另外當其它線程調用了當前阻塞線程的 interrupt 方法中斷了當前線程時候,當前線程也會返回,當其它線程調用了 unpark 方法并且把當前線程作為參數時候當前線程也會返回。
7.void unpark(Object thread)方法: 喚醒調用 park 后阻塞的線程,參數為需要喚醒的線程。
在JDK1.8中新增加了幾個方法,這里簡單的列出Long類型操作的方法如下:
8.long getAndSetLong(Object obj, long offset, long update) 方法: 獲取對象 obj 中偏移量為 offset 的變量 volatile 語義的值,并設置變量 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; }
從代碼中可以內部代碼(1)處使用了getLongVolative獲取當前變量的值,然后使用CAS原子操作進行設置新值,這里使用while循環是考慮到多個線程同時調用的情況CAS失敗后需要自旋重試。
9.long getAndAddLong(Object obj, long offset, long addValue) 方法 :獲取對象 obj 中偏移量為 offset 的變量 volatile 語義的值,并設置變量值為原始值 +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的實現,只是這里使用CAS的時候使用了原始值+傳遞的增量參數addValue的值。
那么如何使用Unsafe類呢?
看到 Unsafe 這個類如此牛叉,是不是很想進行練習,好了,首先看如下代碼所示:
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) { //創建實例,并且設置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)創建了一個變量state初始化為0.
代碼(2.2.4)使用unsafe.objectFieldOffset 獲取 TestUnSafe類里面的state變量 在 TestUnSafe對象里面的內存偏移量地址并保存到stateOffset變量。
代碼(2.2.6)調用創建的unsafe實例的compareAndSwapInt方法,設置test對象的state變量的值,具體意思是如果test對象內存偏移量為stateOffset的state的變量為0,則更新改值為1
上面代碼我們希望輸入true,然而執行后會輸出如下結果:
為什么會這樣呢?必然需要進入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)獲取調用getUnsafe這個方法的對象的Class對象,這里是TestUnSafe.calss。
代碼(2.2.8)判斷是不是Bootstrap類加載器加載的localClass,這里關鍵要看是不是Bootstrap加載器加載了TestUnSafe.class。看過Java虛擬機的類加載機制的人,很明顯看出是由于TestUnSafe.class 是使用 AppClassLoader 加載的,所以這里直接拋出了異常。
那么問題來了,為什么需要有這個判斷呢?
我們知道Unsafe類是在rt.jar里面提供的,而rt.jar里面的類是使用Bootstrap類加載器加載的,而我們啟動main函數所在的類是使用AppClassLoader加載的,所以在main函數里面加載Unsafe類時候鑒于雙親委派機制會委托給Bootstrap去加載Unsafe類。
如果沒有代碼(2.2.8)這個鑒權,那么我們應用程序就可以隨意使用Unsafe做事情了,而Unsafe類可以直接操作內存,是很不安全的,所以JDK開發組特意做了這個限制,不讓開發人員在正規渠道下使用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"); // 設置為可存取(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的實例,運行結果如下:
二.LockSupport類源碼探究
JDK中的rt.jar里面的LockSupport是一個工具類,主要作用是掛起和喚醒線程,它是創建鎖和其他同步類的基礎。
LockSupport類與每個使用他的線程都會關聯一個許可證,默認調用LockSupport 類的方法的線程是不持有許可證的,LockSupport內部使用Unsafe類實現。
這里要注意LockSupport的幾個重要的函數,如下:
1.void park() 方法: 如果調用 park() 的線程已經拿到了與 LockSupport 關聯的許可證,則調用 LockSupport.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!" ); LockSupport.park(); System.out.println( "park stop!" ); } }
如上面代碼所示,直接在main函數里面調用park方法,最終結果只會輸出park start! 然后當前線程會被掛起,這是因為默認下調用線程是不持有許可證的。運行結果如下:
在看到其他線程調用 unpark(Thread thread) 方法并且當前線程作為參數時候,調用park方法被阻塞的線程會返回,另外其他線程調用了阻塞線程的interrupt()方法,設置了中斷標志時候或者由于線程的虛假喚醒原因后阻塞線程也會返回,所以調用 park() 最好也是用循環條件判斷方式。
需要注意的是調用park()方法被阻塞的線程被其他線程中斷后阻塞線程返回時候并不會拋出InterruptedException 異常。
2.void unpark(Thread thread) 方法 當一個線程調用了 unpark 時候,如果參數 thread 線程沒有持有 thread 與 LockSupport 類關聯的許可證,則讓 thread 線程持有。如果 thread 之前調用了 park() 被掛起,則調用 unpark 后,該線程會被喚醒。
如果 thread 之前沒有調用 park,則調用 unPark 方法后,在調用 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!" ); //使當前線程獲取到許可證 LockSupport.unpark(Thread.currentThread()); //再次調用park LockSupport.park(); System.out.println( "park stop!" ); } }
運行結果如下:
接下來我們在看一個例子來加深對 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!"); // 調用park方法,掛起自己 LockSupport.park(); System.out.println("子線程 unpark!"); } }); //啟動子線程 thread.start(); //主線程休眠1S Thread.sleep(1000); System.out.println("主線程 unpark start!"); //調用unpark讓thread線程持有許可證,然后park方法會返回 LockSupport.unpark(thread); } }
運行結果如下:
上面的代碼首先創建了一個子線程thread,啟動后子線程調用park方法,由于默認子線程沒有持有許可證,會把自己掛起
主線程休眠1s 目的是主線程在調用unpark方法讓子線程輸出 子線程park start! 并阻塞。
主線程然后執行unpark方法,參數為子線程,目的是讓子線程持有許可證,然后子線程調用的park方法就返回了。
park方法返回時候不會告訴你是因為何種原因返回,所以調用者需要根據之前是處于什么目前調用的park方法,再次檢查條件是否滿足,如果不滿足的話,還需要再次調用park方法。
例如,線程在返回時的中斷狀態,根據調用前后中斷狀態對比就可以判斷是不是因為被中斷才返回的。
為了說明調用 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!"); // 調用park方法,掛起自己,只有中斷才會退出循環 while (!Thread.currentThread().isInterrupted()) { LockSupport.park(); } System.out.println("子線程 unpark!"); } }); //啟動子線程 thread.start(); //主線程休眠1S Thread.sleep(1000); System.out.println("主線程 unpark start!"); //中斷子線程 thread.interrupt(); } }
運行結果如下:
正如上面代碼,也就是只有當子線程被中斷后子線程才會運行結束,如果子線程不被中斷,即使你調用unPark(Thread) 子線程也不會結束。
3.void parkNanos(long nanos)方法:和 park 類似,如果調用 park 的線程已經拿到了與 LockSupport 關聯的許可證,則調用 LockSupport.park() 會馬上返回,不同在于如果沒有拿到許可調用線程會被掛起 nanos 時間后在返回。
park 還支持三個帶有blocker參數的方法,當線程因為沒有持有許可證的情況下調用park 被阻塞掛起的時候,這個blocker對象會被記錄到該線程內部。
使用診斷工具可以觀察線程被阻塞的原因,診斷工具是通過調用getBlocker(Thread)方法來獲取該blocker對象的,所以JDK推薦我們使用帶有blocker參數的park方法,并且blocker設置為this,這樣當內存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(); } }
運行結果如下:
可以看到運行在阻塞,那么我們要使用JDK/bin目錄下的工具看一下了,如果不知道的讀者,建議去先看一下JVM的監控工具。
運行后使用jstack pid 查看線程堆棧的時候,可以看到的結果如下:
然后我們進行上面的代碼(1)進行修改如下:
LockSupport.park(this);//(1)
再次運行,再用jstack pid 查看的結果如下:
可以知道,帶blocker的park方法后,線程堆棧可以提供更多有關阻塞對象的信息。
那么我們接下來進行park(Object blocker) 函數的源代碼查看,源碼如下:
public static void park(Object blocker) { //獲取調用線程 Thread t = Thread.currentThread(); //設置該線程的 blocker 變量 setBlocker(t, blocker); //掛起線程 UNSAFE.park(false, 0L); //線程被激活后清除 blocker 變量,因為一般都是線程阻塞時候才分析原因 setBlocker(t, null); }
Thread類里面有個變量volatile Object parkBlocker 用來存放park傳遞的blocker對象,也就是把blocker變量存放到了調用park方法的線程的成員變量里面
4.void parkNanos(Object blocker, long nanos) 函數 相比 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); }
可以看到是一個設置deadline,時間單位為milliseconds,是從1970到現在某一個時間點換算為毫秒后的值,這個和parkNanos(Object blocker,long nanos)區別是后者是從當前算等待nanos時間的,而前者是指定一個時間點,
比如我們需要等待到2018.06.06 日 20:34,則把這個時間點轉換為從1970年到這個時間點的總毫秒數。
我們再來看一個例子,代碼如下:
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)如果當前線程不是隊首或者當前鎖已經被其他線程獲取,則調用park方法掛起自己。
接著代碼(2)做判斷,如果park方法是因為被中斷而返回,則忽略中斷,并且重置中斷標志,只做個標記,然后再次判斷當前線程是不是隊首元素或者當先鎖是否已經被其他線程獲取,如果是則繼續調用park方法掛起自己。
然后代碼(3)中如果標記為true 則中斷該線程,這個怎么理解呢?其實就是其他線程中斷了該線程,雖然我對中斷信號不感興趣,忽略它,但是不代表其他線程對該標志不感興趣,所以要恢復下。
“Java并發編程Unsafe類的源碼分析以及Unsafe類的使用方法”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。