這次不講原理了,主要是一些應用方面的知識,和上幾次的JUC並發編程的知識點更容易理解.
上次主要說了Semaphore信號量的使用,就是一個票據的使用,我們舉例了看3D電影拿3D眼鏡的例子,還說了內部的搶3D眼鏡,和后續排隊的源碼解析,還有CountDownLatch的使用,我們是用王者農葯來舉例的,CyclicBarrier柵欄的使用和CountDownLatch幾乎是一致的,Executors用的很少我只是簡單的寫了一個小示例。上次遺漏了一個CountDownLatch和CyclicBarrier的區別。
CountDownLatch和CyclicBarrier的區別:
區別的根本在於有無主線程參與,這樣就很容易區別了,CountDownLatch有主線程,CyclicBarrier沒有主線程,我們來舉兩個例子,CountDownLatch主線程是游戲程序,而我們開啟的10個線程是玩家加載程序,我們的游戲主程序會等待10個玩家加載完成,線程可能結束,然后主程序游戲程序繼續運行。CyclicBarrier沒有主線程,但是具有重復性,再舉一個例子,年會了,公司團建活動,三人跨柵欄,要求是必須三人全部跨過柵欄以后才可以繼續跨下一個柵欄。
CountDownLatch和CyclicBarrier都有讓多個線程等待同步然后再開始下一步動作的意思,但是CountDownLatch的下一步的動作實施者是主線程,具有不可重復性;而CyclicBarrier的下一步動作實施者還是“其他線程”本身,具有往復多次實施動作的特點。
本次新知識
什么是原子操作?
原子(atom)本意是“不能被進一步分割的小粒子”,而原子操作(atomic operation)意為”不可被中斷的一個或一系列操作” 。就像是我們的mysql里面的提到的ACID,原子性,也是不可分割的操作,最小的單位。
我們以前說的MESI,說到了緩存行,也是上鎖的最小單位,原子變更就不做過多解釋了,就是把一個變量的值改為另外一個值。比較與交換我們在Semaphore源碼里也接觸過了,也就是CAS操作需要輸入兩個數值,一個舊值,一個新值,在將要變更為新值之前,會比較舊值是否已經改變,如果改變了修改失敗,如果沒有改變,修改成功。
Atomic的使用
在Atomic包內一共有12個類,四種原子更新方式,原子更新基本類型,原子更新數組,原子更新字段,Atomic包里的類基本都是基於Unsafe實現的包裝類。
基本類型:AtomicInteger,AtomicBoolean,AtomicLong。
引用類型:AtomicReference、AtomicReference的ABA實例、AtomicStampedReference、AtomicMarkableReference。
數組類型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray。
屬性原子修改器:AtomicLongFieldUpdater、AtomicReferenceFieldUpdater、AtomicIntegerFieldUpdater。
來一個簡單的實例,就是開啟10個線程然后做一個自加的操作,還是很好理解的。
public class AtomicIntegerTest { static AtomicInteger atomicInteger = new AtomicInteger(); public static void main(String[] args) { for (int i = 0; i<10; i++){ new Thread(new Runnable() { @Override public void run() { atomicInteger.incrementAndGet(); } }).start(); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("自加10次數值:--->"+atomicInteger.get()); } }
ABA問題,ABA這樣更能好理解一些,一眼就可以看出來A已經不是原來的A了,雖然值一樣,但是里面的屬性變成了紅色的,先來看一段代碼。
package com.xiaocai.main; import java.util.concurrent.atomic.AtomicInteger; public class AtomicIntegerTest { static AtomicInteger atomicInteger = new AtomicInteger(1); public static void main(String[] args) { Thread main = new Thread(new Runnable() { @Override public void run() { int a = atomicInteger.get(); System.out.println("操作線程"+Thread.currentThread().getName()+",修改前操作數值:"+a); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } boolean isCasSuccess = atomicInteger.compareAndSet(a,2); if(isCasSuccess){ System.out.println("操作線程"+Thread.currentThread().getName()+",Cas修改后操作數值:"+atomicInteger.get()); }else{ System.out.println("CAS修改失敗"); } } },"主線程"); Thread other = new Thread(new Runnable() { @Override public void run() { atomicInteger.incrementAndGet();// 1+1 = 2; System.out.println("操作線程"+Thread.currentThread().getName()+",自加后值:"+atomicInteger.get()); atomicInteger.decrementAndGet();// atomic-1 = 2-1; System.out.println("操作線程"+Thread.currentThread().getName()+",自減后值:"+atomicInteger.get()); } },"干擾線程"); main.start(); other.start(); } }
我們可以看到主線程設置一個初始值為1,然后進行等待,干擾線程將1修改為2,又將2修改回1,然后主線程繼續操作1修改為2,這一系列的動作,主線程並沒有感知到1已經不是原來的1了。
這樣的操作其實是很危險的,我們假象,小王是銀行的職員,他可以操作每個賬戶的金額(假設啊,具體能不能我也不知道),他將撕蔥的賬戶轉走了1000萬用於炒股,股市大漲,小王賺了2000萬,還了1千萬,自己還剩下2千萬,過幾天撕蔥來查看自己賬戶錢並沒有少,但是錢已經不是那個錢了,有人動過的。所以ABA問題我們還是要想辦法來處理的。我們每次轉賬匯款的操作都是有一個流水號(回執單)的,也就是每次我們加一個版本號碼就可以了,我們來改一下代碼。
public class AtomicIntegerTest { static AtomicStampedReference atomicInteger = new AtomicStampedReference<>(1,0); public static void main(String[] args) { Thread main = new Thread(new Runnable() { @Override public void run() { int stamp = atomicInteger.getStamp(); //獲取當前標識別 System.out.println("操作線程"+Thread.currentThread().getName()+"修改前的版本號為:"+stamp+",修改前操作數值:"+atomicInteger.getReference()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } boolean isCasSuccess = atomicInteger.compareAndSet(1,2,stamp,stamp +1); //此時expectedReference未發生改變,但是stamp已經被修改了,所以CAS失敗 if(isCasSuccess){ System.out.println("操作線程"+Thread.currentThread().getName()+",Cas修改后操作數值:"+atomicInteger.getReference()); }else{ System.out.println("CAS修改失敗,當前版本為:"+atomicInteger.getStamp()); } } },"主線程"); Thread other = new Thread(new Runnable() { @Override public void run() { int stamp = atomicInteger.getStamp(); atomicInteger.compareAndSet(1,2,atomicInteger.getStamp(),atomicInteger.getStamp()+1); System.out.println("操作線程"+Thread.currentThread().getName()+",版本號為:"+stamp+",修改后的版本號為:"+atomicInteger.getStamp()+",自加后值:"+atomicInteger.getReference()); int newStamp = atomicInteger.getStamp(); atomicInteger.compareAndSet(2,1,atomicInteger.getStamp(),atomicInteger.getStamp()+1); System.out.println("操作線程"+Thread.currentThread().getName()+",版本號為:"+newStamp+",修改后的版本號為:"+atomicInteger.getStamp()+",自減后值:"+atomicInteger.getReference()); } },"干擾線程"); main.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } other.start(); } }
我們先初始一個主線程,並且設置版本號為0。然后干擾線程進行修改,每次修改時版本號加一,干擾線程結束,而主線程想繼續修改時,發現版本不匹配,修改失敗。
其余Atomic的類使用都是大同小異的,可以自行嘗試一遍。
Unsafe魔術類的使用
Unsafe是位於sun.misc包下的一個類,主要提供一些用於執行低級別、不安全操作的 方法,如直接訪問系統內存資源、自主管理內存資源等,這些方法在提升Java運行效率、增 強Java語言底層資源操作能力方面起到了很大的作用。但由於Unsafe類使Java語言擁有了 類似C語言指針一樣操作內存空間的能力,這無疑也增加了程序發生相關指針問題的風險。 在程序中過度、不正確使用Unsafe類會使得程序出錯的概率變大,使得Java這種安全的語 言變得不再“安全”,因此對Unsafe的使用一定要慎重。
在過去的幾篇博客里也說到了Unsafe這個類,我們需要通過反射來使用它,比如讀寫屏障、加鎖解鎖,線程的掛起操作等等。
如何獲取Unsafe實例?
1、從getUnsafe方法的使用限制條件出發,通過Java命令行命令-Xbootclasspath/a把 調用Unsafe相關方法的類A所在jar包路徑追加到默認的bootstrap路徑中,使得A被 引導類加載器加載,從而通過Unsafe.getUnsafe方法安全的獲取Unsafe實例。 java Xbootclasspath/a:${path} // 其中path為調用Unsafe相關方法的類所在jar包路徑。
2、通過反射獲取單例對象theUnsafe。
public static Unsafe reflectGetUnsafe() { try { Field field = Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); return (Unsafe) field.get(null); } catch (Exception e) { e.printStackTrace(); } return null; }
總結:
這次博客完全沒有代碼的解析閱讀,都是一些簡單的使用,我們開始時候說到了什么是原子操作,接下來我們說了Atomic類的基本使用,再就是什么是ABA問題,如何用Atomic來解決ABA問題,再就是我們的魔術類Unsafe類,越過虛擬機直接來操作我們的系統的一些操作(不是超級熟練別玩這個,玩壞了不好修復)。希望對大家在工作面試中能有一些幫助。
最進弄了一個公眾號,小菜技術,歡迎大家的加入