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