java并发编程实践_Java并发编程实践如何正确使用Unsafe
一、前言
Java 并發(fā)編程實(shí)踐中的話:
編寫(xiě)正確的程序并不容易,而編寫(xiě)正常的并發(fā)程序就更難了。相比于順序執(zhí)行的情況,多線程的線程安全問(wèn)題是微妙而且出乎意料的,因?yàn)樵跊](méi)有進(jìn)行適當(dāng)同步的情況下多線程中各個(gè)操作的順序是不可預(yù)期的。
并發(fā)編程相比 Java 中其他知識(shí)點(diǎn)學(xué)習(xí)起來(lái)門(mén)檻相對(duì)較高,學(xué)習(xí)起來(lái)比較費(fèi)勁,從而導(dǎo)致很多人望而卻步;
而無(wú)論是職場(chǎng)面試和高并發(fā)高流量的系統(tǒng)的實(shí)現(xiàn)卻還都離不開(kāi)并發(fā)編程,從而導(dǎo)致能夠真正掌握并發(fā)編程的人才成為市場(chǎng)比較迫切需求的。
本場(chǎng) Chat 作為 Java 并發(fā)編程之美系列的高級(jí)篇之二,主要講解內(nèi)容如下:(建議先閱讀:Java 編程之美:并發(fā)編程高級(jí)篇之一 )
- rt.jar 中 Unsafe 類主要函數(shù)講解, Unsafe 類提供了硬件級(jí)別的原子操作,可以安全的直接操作內(nèi)存變量,其在 JUC 源碼中被廣泛的使用,了解其原理為研究 JUC 源碼奠定了基礎(chǔ)。
- rt.jar 中 LockSupport 類主要函數(shù)講解,LockSupport 是個(gè)工具類,主要作用是掛起和喚醒線程,是創(chuàng)建鎖和其它同步類的基礎(chǔ),了解其原理為研究 JUC 中鎖的實(shí)現(xiàn)奠定基礎(chǔ)。
- 講解 JDK8 新增原子操作類 LongAdder 實(shí)現(xiàn)原理,并講解 AtomicLong 的缺點(diǎn)是什么,LongAdder 是如何解決 AtomicLong 的缺點(diǎn)的,LongAdder 和 LongAccumulator 是什么關(guān)系?
- JUC 并發(fā)包中并發(fā)組件 CopyOnWriteArrayList 的實(shí)現(xiàn)原理,CopyOnWriteArrayList 是如何通過(guò)寫(xiě)時(shí)拷貝實(shí)現(xiàn)并發(fā)安全的 List?
二、 Unsafe 類探究
JDK 的 rt.jar 包中的 Unsafe 類提供了硬件級(jí)別的原子操作,Unsafe 里面的方法都是 native 方法,通過(guò)使用 JNI 的方式來(lái)訪問(wèn)本地 C++ 實(shí)現(xiàn)庫(kù)。下面我們看下 Unsafe 提供的幾個(gè)主要方法以及編程時(shí)候如何使用 Unsafe 類做一些事情。
2.1 主要方法介紹
- long objectFieldOffset(Field field) 方法
作用:返回指定的變量在所屬類的內(nèi)存偏移地址,偏移地址僅僅在該 Unsafe 函數(shù)中訪問(wèn)指定字段時(shí)候使用。如下代碼使用 unsafe 獲取AtomicLong 中變量 value 在 AtomicLong 對(duì)象中的內(nèi)存偏移。
valueOffset = unsafe.objectFieldOffset
(AtomicLong.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
- int arrayBaseOffset(Class arrayClass) 方法
獲取數(shù)組中第一個(gè)元素的地址 - int arrayIndexScale(Class arrayClass) 方法
獲取數(shù)組中單個(gè)元素占用的字節(jié)數(shù) - boolean compareAndSwapLong(Object obj, long offset, long expect, long update) 方法
比較對(duì)象 obj 中偏移量為 offset 的變量的值是不是和 expect 相等,相等則使用 update 值更新,然后返回 true,否者返回 false - public native long getLongVolatile(Object obj, long offset) 方法
獲取對(duì)象 obj 中偏移量為 offset 的變量對(duì)應(yīng)的 volatile 內(nèi)存語(yǔ)義的值。 - void putLongVolatile(Object obj, long offset, long value) 方法
設(shè)置 obj 對(duì)象中內(nèi)存偏移為 offset 的 long 型變量的值為 value,支持 volatile 內(nèi)存語(yǔ)義。 - void putOrderedLong(Object obj, long offset, long value) 方法
設(shè)置 obj 對(duì)象中 offset 偏移地址對(duì)應(yīng)的 long 型 field 的值為 value。這是有延遲的 putLongVolatile 方法,并不保證值修改對(duì)其它線程立刻可見(jiàn)。變量只有使用 volatile 修飾并且期望被意外修改的時(shí)候使用才有用。 - void park(boolean isAbsolute, long time)
阻塞當(dāng)前線程,其中參數(shù) isAbsolute 等于 false 時(shí)候,time 等于 0 表示一直阻塞,time 大于 0 表示等待指定的 time 后阻塞線程會(huì)被喚醒,這個(gè) time 是個(gè)相對(duì)值,是個(gè)增量值,也就是相對(duì)當(dāng)前時(shí)間累加 time 后當(dāng)前線程就會(huì)被喚醒。
如果 isAbsolute 等于 true,并且 time 大于 0 表示阻塞后到指定的時(shí)間點(diǎn)后會(huì)被喚醒,這里 time 是個(gè)絕對(duì)的時(shí)間,是某一個(gè)時(shí)間點(diǎn)換算為 ms 后的值。
另外當(dāng)其它線程調(diào)用了當(dāng)前阻塞線程的 interrupt 方法中斷了當(dāng)前線程時(shí)候,當(dāng)前線程也會(huì)返回,當(dāng)其它線程調(diào)用了 unpark 方法并且把當(dāng)前線程作為參數(shù)時(shí)候當(dāng)前線程也會(huì)返回。 - void unpark(Object thread)
喚醒調(diào)用 park 后阻塞的線程,參數(shù)為需要喚醒的線程。
下面是 Jdk8 新增的方法,這里簡(jiǎn)單的列出 Long 類型操作的方法
- long getAndSetLong(Object obj, long offset, long update) 方法
獲取對(duì)象 obj 中偏移量為 offset 的變量 volatile 語(yǔ)義的值,并設(shè)置變量 volatile 語(yǔ)義的值為 update。
{ long l;
do
{
l = getLongVolatile(obj, offset);//(1)
} while (!compareAndSwapLong(obj, offset, l, update)); return l;
}
從代碼可知內(nèi)部代碼 (1) 處使用 getLongVolatile 獲取當(dāng)前變量的值,然后使用 CAS 原子操作進(jìn)行設(shè)置新值,這里使用 while 循環(huán)是考慮到多個(gè)線程同時(shí)調(diào)用的情況 CAS 失敗后需要自旋重試。
- long getAndAddLong(Object obj, long offset, long addValue) 方法
獲取對(duì)象 obj 中偏移量為 offset 的變量 volatile 語(yǔ)義的值,并設(shè)置變量值為原始值 +addValue。
{ long l;
do
{
l = getLongVolatile(obj, offset);
} while (!compareAndSwapLong(obj, offset, l, l + addValue)); return l;
}
類似 getAndSetLong 的實(shí)現(xiàn),只是這里使用CAS的時(shí)候使用了原始值+傳遞的增量參數(shù) addValue 的值。
2.2 如何使用 Unsafe 類
看到 Unsafe 這個(gè)類如此牛叉,你肯定會(huì)忍不住擼下下面代碼,期望能夠使用 Unsafe 做點(diǎn)事情。
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 的一個(gè)實(shí)例,代碼(2.2.3)創(chuàng)建了一個(gè)變量 state 初始化為 0。
代碼(2.2.4)使用 unsafe.objectFieldOffset 獲取 TestUnSafe 類里面的 state 變量在 TestUnSafe 對(duì)象里面的內(nèi)存偏移量地址并保存到 stateOffset 變量。
代碼(2.2.6)調(diào)用創(chuàng)建的 unsafe 實(shí)例的 compareAndSwapInt 方法,設(shè)置 test 對(duì)象的 state 變量的值,具體意思是如果 test 對(duì)象內(nèi)存偏移量為 stateOffset 的 state 的變量為 0,則更新該值為 1。
運(yùn)行上面代碼我們期望會(huì)輸出 true,然而執(zhí)行后會(huì)輸出如下結(jié)果:
為研究其原因,必然要翻看 getUnsafe 代碼,看看里面做了啥:
Class localClass = Reflection.getCallerClass(); //(2.2.8)
} return theUnsafe;
} //判斷paramClassLoader是不是BootStrap類加載器(2.2.9)
{ return paramClassLoader == null;
}
代碼(2.2.7)獲取調(diào)用 getUnsafe 這個(gè)方法的對(duì)象的 Class 對(duì)象,這里是 TestUnSafe.class。
代碼(2.2.8)判斷是不是 Bootstrap 類加載器加載的 localClass,這里是看是不是 Bootstrap 加載器加載了 TestUnSafe.class。很明顯由于 TestUnSafe.class 是使用 AppClassLoader 加載的,所以這里直接拋出了異常。
思考下,這里為何要有這個(gè)判斷那?
我們知道 Unsafe 類是在 rt.jar 里面提供的,而 rt.jar 里面的類是使用 Bootstrap 類加載器加載的,而我們啟動(dòng) main 函數(shù)所在的類是使用 AppClassLoader 加載的。
所以在 main 函數(shù)里面加載 Unsafe 類時(shí)候鑒于委托機(jī)制會(huì)委托給 Bootstrap 去加載 Unsafe 類。
如果沒(méi)有代碼(2.2.8)這鑒權(quán),那么我們應(yīng)用程序就可以隨意使用 Unsafe 做事情了,而 Unsafe 類可以直接操作內(nèi)存,是不安全的。
所以 JDK 開(kāi)發(fā)組特意做了這個(gè)限制,不讓開(kāi)發(fā)人員在正規(guī)渠道下使用 Unsafe 類,而是在 rt.jar 里面的核心類里面使用 Unsafe 功能。
那么如果開(kāi)發(fā)人員真的想要實(shí)例化 Unsafe 類,使用 Unsafe 的功能該如何做那?
方法有很多種,既然正規(guī)渠道訪問(wèn)不了,那么就玩點(diǎn)黑科技,使用萬(wàn)能的反射來(lái)獲取 Unsafe 實(shí)例方法:
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);
}
}
如上代碼通過(guò)代碼(2.2.10),(2.2.11),(2.2.12)反射獲取 unsafe 的實(shí)例,然后運(yùn)行結(jié)果輸出:
三、LockSupport類探究
JDK 中的 rt.jar 里面的 LockSupport 是個(gè)工具類,主要作用是掛起和喚醒線程,它是創(chuàng)建鎖和其它同步類的基礎(chǔ)。
LockSupport 類與每個(gè)使用它的線程都會(huì)關(guān)聯(lián)一個(gè)許可證,默認(rèn)調(diào)用 LockSupport 類的方法的線程是不持有許可證的,LockSupport 內(nèi)部使用 Unsafe 類實(shí)現(xiàn),下面介紹下 LockSupport 內(nèi)的幾個(gè)主要函數(shù):
- void park() 方法
如果調(diào)用 park() 的線程已經(jīng)拿到了與 LockSupport 關(guān)聯(lián)的許可證,則調(diào)用 LockSupport.park() 會(huì)馬上返回,否者調(diào)用線程會(huì)被禁止參與線程的調(diào)度,也就是會(huì)被阻塞掛起。
如下代碼,直接在 main 函數(shù)里面調(diào)用 park 方法,最終結(jié)果只會(huì)輸出begin park!,然后當(dāng)前線程會(huì)被掛起,這是因?yàn)槟J(rèn)下調(diào)用線程是不持有許可證的。
{
System.out.println( "begin park!" );
LockSupport.park();
System.out.println( "end park!" );
}
在其它線程調(diào)用 unpark(Thread thread) 方法并且當(dāng)前線程作為參數(shù)時(shí)候,調(diào)用park方法被阻塞的線程會(huì)返回。
另外其它線程調(diào)用了阻塞線程的 interrupt() 方法,設(shè)置了中斷標(biāo)志時(shí)候或者由于線程的虛假喚醒原因后阻塞線程也會(huì)返回,所以調(diào)用 park() 最好也是用循環(huán)條件判斷方式。
需要注意的是調(diào)用 park() 方法被阻塞的線程被其他線程中斷后阻塞線程返回時(shí)候并不會(huì)拋出 InterruptedException 異常。
- void unpark(Thread thread) 方法
當(dāng)一個(gè)線程調(diào)用了 unpark 時(shí)候,如果參數(shù) thread 線程沒(méi)有持有 thread 與 LockSupport 類關(guān)聯(lián)的許可證,則讓 thread 線程持有。
如果 thread 之前調(diào)用了 park() 被掛起,則調(diào)用 unpark 后,該線程會(huì)被喚醒。
如果 thread 之前沒(méi)有調(diào)用 park,則調(diào)用 unPark 方法后,在調(diào)用 park() 方法,會(huì)立刻返回,上面代碼修改如下:
{
System.out.println( "begin park!" ); //使當(dāng)前線程獲取到許可證
LockSupport.unpark(Thread.currentThread()); //再次調(diào)用park
LockSupport.park();
System.out.println( "end park!" );
}
begin park!
end park!
下面再來(lái)看一個(gè)例子來(lái)加深對(duì) park,unpark 的理解
Thread thread = new Thread(new Runnable() { @Override
System.out.println("child thread begin park!"); // 調(diào)用park方法,掛起自己
LockSupport.park();
System.out.println("child thread unpark!");
}
}); //啟動(dòng)子線程
thread.start(); //主線程休眠1S
Thread.sleep(1000);
System.out.println("main thread begin unpark!"); //調(diào)用unpark讓thread線程持有許可證,然后park方法會(huì)返回
LockSupport.unpark(thread);
}
輸出為:
child thread begin park!
main thread begin unpark!
child thread unpark!
上面代碼首先創(chuàng)建了一個(gè)子線程 thread,啟動(dòng)后子線程調(diào)用 park 方法,由于默認(rèn)子線程沒(méi)有持有許可證,會(huì)把自己掛起。
主線程休眠 1s 為的是主線程在調(diào)用 unpark 方法前讓子線程輸出 child thread begin park! 并阻塞。
主線程然后執(zhí)行 unpark 方法,參數(shù)為子線程,目的是讓子線程持有許可證,然后子線程調(diào)用的 park 方法就返回了。
park 方法返回時(shí)候不會(huì)告訴你是因?yàn)楹畏N原因返回,所以調(diào)用者需要根據(jù)之前是處于什么目前調(diào)用的 park 方法,再次檢查條件是否滿足,如果不滿足的話還需要再次調(diào)用 park 方法。
例如,線程在返回時(shí)的中斷狀態(tài),根據(jù)調(diào)用前后中斷狀態(tài)對(duì)比就可以判斷是不是因?yàn)楸恢袛嗖欧祷氐摹?/p>
為了說(shuō)明調(diào)用 park 方法后的線程被中斷后會(huì)返回,修改上面例子代碼,刪除 LockSupport.unpark(thread); 然后添加 thread.interrupt(); 代碼如下:
Thread thread = new Thread(new Runnable() { @Override
System.out.println("child thread begin park!"); // 調(diào)用park方法,掛起自己,只有被中斷才會(huì)退出循環(huán)
LockSupport.park();
}
System.out.println("child thread unpark!");
}
}); // 啟動(dòng)子線程
thread.start(); // 主線程休眠1S
Thread.sleep(1000);
System.out.println("main thread begin unpark!"); // 中斷子線程線程
thread.interrupt();
}
輸出為:
child thread begin park!
main thread begin unpark!
child thread unpark!
如上代碼也就是只有當(dāng)子線程被中斷后子線程才會(huì)運(yùn)行結(jié)束,如果子線程不被中斷,即使你調(diào)用 unPark(thread) 子線程也不會(huì)結(jié)束。
- void parkNanos(long nanos)函數(shù)
和 park 類似,如果調(diào)用 park 的線程已經(jīng)拿到了與 LockSupport 關(guān)聯(lián)的許可證,則調(diào)用 LockSupport.park() 會(huì)馬上返回,不同在于如果沒(méi)有拿到許可調(diào)用線程會(huì)被掛起 nanos 時(shí)間后在返回。
park 還支持三個(gè)帶有 blocker 參數(shù)的方法,當(dāng)線程因?yàn)闆](méi)有持有許可的情況下調(diào)用 park 被阻塞掛起時(shí)候,這個(gè) blocker 對(duì)象會(huì)被記錄到該線程內(nèi)部。
使用診斷工具可以觀察線程被阻塞的原因,診斷工具是通過(guò)調(diào) getBlocker(Thread) 方法來(lái)獲取該 blocker 對(duì)象的,所以 JDK 推薦我們使用帶有 blocker 參數(shù)的 park 方法,并且 blocker 設(shè)置為 this,這樣當(dāng)內(nèi)存 dump 排查問(wèn)題時(shí)候就能知道是那個(gè)類被阻塞了。
例如下面代碼:
LockSupport.park();//(1)
} public static void main(String[] args) {
TestPark testPark = new TestPark();
testPark.testPark();
}
}
運(yùn)行后使用 jstack pid 查看線程堆棧時(shí)候可以看到如下:
修改 代碼(1)為 LockSupport.park(this) 后運(yùn)行在 jstack pid 結(jié)果為:
可知使用帶 blocker 的 park 方法后,線程堆棧可以提供更多有關(guān)阻塞對(duì)象的信息。
- park(Object blocker) 函數(shù)
Thread t = Thread.currentThread(); //設(shè)置該線程的 blocker 變量
setBlocker(t, blocker); //掛起線程
UNSAFE.park(false, 0L); //線程被激活后清除 blocker 變量,因?yàn)橐话愣际蔷€程阻塞時(shí)候才分析原因
setBlocker(t, null);
}
Thread 類里面有個(gè)變量 volatile Object parkBlocker 用來(lái)存放 park 傳遞的 blocker 對(duì)象,也就是把 blocker 變量存放到了調(diào)用 park 方法的線程的成員變量里面。
- void parkNanos(Object blocker, long nanos) 函數(shù)
相比 park(Object blocker) 多了個(gè)超時(shí)時(shí)間。 - void parkUntil(Object blocker, long deadline)
parkUntil 的代碼如下:
Thread t = Thread.currentThread();
setBlocker(t, blocker); //isAbsolute=true,time=deadline;表示到 deadline 時(shí)間時(shí)候后返回
UNSAFE.park(true, deadline);
setBlocker(t, null);
}
可知是設(shè)置一個(gè) deadline,時(shí)間單位為 milliseconds,是從 1970 到現(xiàn)在某一個(gè)時(shí)間點(diǎn)換算為毫秒后的值,這個(gè)和 parkNanos(Object blocker, long nanos) 區(qū)別是后者是從當(dāng)前算等待 nanos 時(shí)間,而前者是指定一個(gè)時(shí)間點(diǎn)。
比如我需要等待到 2017.12.11 日 12:00:00,則吧這個(gè)時(shí)間點(diǎn)轉(zhuǎn)換為從 1970 年到這個(gè)時(shí)間點(diǎn)的總毫秒數(shù)。
最后在看一個(gè)例子
Thread current = Thread.currentThread();
waiters.add(current); // 只有隊(duì)首的線程可以獲取鎖(1)
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());
}
}
這是一個(gè)先進(jìn)先出的鎖,也就是只有隊(duì)列首元素可以獲取鎖,代碼(1)處如果當(dāng)前線程不是隊(duì)首或者當(dāng)前鎖已經(jīng)被其它線程獲取,則調(diào)用park方法掛起自己。
然后代碼(2)處判斷,如果 park 方法是因?yàn)楸恢袛喽祷?#xff0c;則忽略中斷,并且重置中斷標(biāo)志,只做個(gè)標(biāo)記,然后再次判斷當(dāng)前線程是不是隊(duì)首元素或者當(dāng)前鎖是否已經(jīng)被其它線程獲取,如果是則繼續(xù)調(diào)用 park 方法掛起自己。
然后代碼(3)中如果標(biāo)記為 true 則中斷該線程,這個(gè)怎么理解那?其實(shí)意思是其它線程中斷了該線程,雖然我對(duì)中斷信號(hào)不感興趣,忽略它,但是不代表其它線程對(duì)該標(biāo)志不感興趣,所以要恢復(fù)下。
四、 LongAdder 和 LongAccumulator 原理探究
文章到此就結(jié)束了 喜歡小編的文章可以點(diǎn)贊支持哦!
......
總結(jié)
以上是生活随笔為你收集整理的java并发编程实践_Java并发编程实践如何正确使用Unsafe的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: java native方法_并发系列-n
- 下一篇: java中class_Java中Clas