併發程式設計之原子操作Atomic&Unsafe
原子操作:不能被分割(中斷)的一個或一系列操作叫原子操作。
原子操作Atomic主要有12個類,4種類型的原子更新方式,原子更新基本型別,原子更新陣列,原子更新欄位,原子更新引用。Atomic包中的類基本都是使用Unsafe實現的包裝類。
基本型別:AtomicInteger,AtomicLong,AtomicBoolean;
引用型別:AtomicReference、AtomicReference的ABA例項、AtomicStampedRerence、AtomicMarkableReference;
陣列型別:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray;
屬性原子修改器(Updater):AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater;
1、原子更新基本型別類
用於通過原子的方式更新基本型別,Atomic包提供了以下三個類: AtomicBoolean:原子更新布林型別。 AtomicInteger:原子更新整型。 AtomicLong:原子更新長整型。 AtomicInteger的常用方法如下: int addAndGet(int delta) :以原子方式將輸入的數值與例項中的值 (AtomicInteger裡的value)相加,並返回結果 boolean compareAndSet(int expect, int update) :如果輸入的數值等於值,則以原子方式將該值設定為輸入的值。 int getAndIncrement():以原子方式將當前值加1,注意:這裡返回的是自前的值。void lazySet(int newValue):最終會設定成newValue,使用lazySet設定後,可能導致其他執行緒在之後的一小段時間內還是可以讀到舊的值。 int getAndSet(int newValue):以原子方式設定為newValue的值,並返回值。 Atomic包提供了三種基本型別的原子更新,但是Java的基本型別裡還有char,fldouble等。那麼問題來了,如何原子的更新其他的基本型別呢?Atomic包裡的類基本使用Unsafe實現的,Unsafe只提供了三種CAS方法,compareAndSwapObject, compareAndSwapInt和compareAndSwapLong,再看AtomicBoolean原始碼,發現先把Boolean轉換成整型,再使用compareAndSwapInt進行CAS,所以原子更新dou也可以用類似的思路來實現。下面我們來看一下每種型別的一個例項:
/** * <p>Title: AtomicIntegerTest.java</p > * <p>Description: </p > * <p>Copyright: NTT DATA Synergy All Rights Reserved.</p > * <p>Company: www.synesoft.com.cn</p > * <p>@datetime 2019年8月9日 上午8:01:30</p > * <p>$Revision$</p > * <p>$Date$</p > * <p>$Id$</p > */ package com.test; import java.util.concurrent.atomic.AtomicInteger; /** * @author hong_liping * */ public class AtomicIntegerTest { static AtomicInteger ai=new AtomicInteger(); public static void main(String[] args) { for(int i=0;i<10;i++){ new Thread(new Runnable() { @Override public void run() { ai.incrementAndGet(); } }).start(); } // try { // Thread.sleep(100); // } catch (InterruptedException e) { // e.printStackTrace(); // } System.out.println("迴圈後的結果如下:"+ai.get()); } }
//測試結果
迴圈後的結果如下:9
迴圈後的結果如下:10
根據上面的程式碼,我們多執行幾次,會發現,程式碼的測試結果一會兒是9一會兒是10,不是10,為什麼呢,因為執行緒還沒有跑完,我下面的就已經打出來了,讓執行緒睡眠一下就可以解決這個問題了。
下面我們來看一下atomic的ABA問題,這個問題在面試的時候經常問到。
/** * <p>Title: AtomicTest.java</p > * <p>Description: </p > * <p>@datetime 2019年8月8日 下午3:40:37</p > * <p>$Revision$</p > * <p>$Date$</p > * <p>$Id$</p > */ package com.test; import java.util.concurrent.atomic.AtomicInteger; /** * @author hong_liping * */ public class AtomicAbaTest { private static AtomicInteger ato=new AtomicInteger(1); public static void main(String[] args) { Thread mainT=new Thread(new Runnable() { @Override public void run() { int a=ato.get(); System.out.println(Thread.currentThread().getName()+"原子操作修改前資料"+a); try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } boolean successFlag=ato.compareAndSet(a, 2); if(successFlag){ System.out.println(Thread.currentThread().getName()+"原子操作修改後資料"+ato.get()); } } },"mainT"); Thread otherT=new Thread(new Runnable() { @Override public void run() { int b=ato.incrementAndGet();//1+1 System.out.println(Thread.currentThread().getName()+"原子操作自增後資料"+b); b=ato.decrementAndGet();//2-1 System.out.println(Thread.currentThread().getName()+"原子操作自減後資料"+b); } },"OtherT"); mainT.start(); otherT.start(); } }
測試結果:
OtherT原子操作自增後資料2
mainT原子操作修改前資料1
OtherT原子操作自減後資料1
mainT原子操作修改後資料2
根據上面的操作,我們可以看到的是AtomicInteger的操作自增,自減,值的替換等。但是此處應當注意的是原子操作存在一個ABA問題,ABA問題的現象就是:mainT執行完成後的值2(替換的2),otherT在執行2-1的時候的2是自增(1+1)的結果。在這兩個執行緒中用到的2不是同一個2,就相當於是一個漏洞,相當於說你從王健林賬號中偷走了10個億去投資,等你投資好了回本了,你再把這10個億打回了王健林賬號,這整個過程王建林沒有發現,你的整個操作過程也沒有記錄,所以對於王健林來說他的錢沒有丟失過,還是放在那裡的。很明顯要解決這個ABA問題最好的辦法就是每一步操作都打個標記,相當於一個銀行的流水,這樣你偷錢,還錢的整個過程就有一個出,一個入,王健林看的時候就會發現我的總金沒有變,但是操作記錄顯示我的錢曾經被人盜瞭然後又被人還回來了。這就需要用到AtomicStampeReference.
2、原子更新引用型別
原子更新基本型別的AtomicInteger,只能更新一個變數,如果要原子的更新多個變 量,就需要使用這個原子更新引用型別提供的類。Atomic包提供了以下三個類: AtomicReference:原子更新引用型別。 AtomicReferenceFieldUpdater:原子更新引用型別裡的欄位。 AtomicMarkableReference:原子更新帶有標記位的引用型別。可以原子的更 新一個布林型別的標記位和引用型別。構造方法是AtomicMarkableReference(V initialRef, boolean initialMark)接下來我們來看一下AtomicStampedReference的測試類:
/** * <p>Title: AtomicStampedReference.java</p > * <p>Description: </p > * <p>@datetime 2019年8月9日 上午8:35:56</p > * <p>$Revision$</p > * <p>$Date$</p > * <p>$Id$</p > */ package com.test; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicStampedReference; /** * @author hong_liping * */ public class AtomicStampedReferenceTest { private static AtomicStampedReference<Integer> asf=new AtomicStampedReference<Integer>(1, 0); public static void main(String[] args) { Thread mainT=new Thread(new Runnable() { @Override public void run() { int stamp= asf.getStamp(); System.out.println(Thread.currentThread().getName()+"原子操作修改前資料"+asf.getReference()+ "_"+stamp); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //此時expectedReference未發生改變,但是stamp已經被修改了,所以CAS失敗 boolean successFlag=asf.compareAndSet(1, 2, stamp, stamp+1); if(successFlag){ System.out.println(Thread.currentThread().getName()+"原子操作修改後資料"+asf.getReference()+ "_"+stamp); }else{ System.out.println(Thread.currentThread().getName()+"cas操作失敗"); } } },"mainT"); Thread otherT=new Thread(new Runnable() { @Override public void run() { int stamp=asf.getStamp(); asf.compareAndSet(1, 2, stamp, stamp+1); System.out.println(Thread.currentThread().getName()+"原子操作自增後資料"+asf.getReference()+ "_"+asf.getReference()); asf.compareAndSet(2, 1, stamp, stamp+1); System.out.println(Thread.currentThread().getName()+"原子操作自減後資料"+asf.getReference()+ "_"+stamp);; } },"OtherT"); mainT.start(); otherT.start(); } } //測試結果: mainT原子操作修改前資料2_0 OtherT原子操作自增後資料2_2 OtherT原子操作自減後資料2_0 mainTcas操作失敗3、原子更新陣列類 通過原子的方式更新數組裡的某個元素,Atomic包提供了以下三個類AtomicIntegerArray:原子更新整型數組裡的元素。AtomicLongArray:原子更新長整型數組裡的元素。 AtomicReferenceArray:原子更新引用型別數組裡的元素。 omicIntegerArray類主要是提供原子的方式更新數組裡的整型,其常用方法int addAndGet(int i, int delta):以原子方式將輸入值與陣列中索加。boolean compareAndSet(int i, int expect, int update):如果值,則以原子方式將陣列位置i的元素設定成update值。
接下來我們來看一下AtomicIntegerArray的一個案例
/** * <p>Title: AtomicArrayTest.java</p > * <p>Description: </p > * <p>@datetime 2019年8月10日 上午9:45:49</p > * <p>$Revision$</p > * <p>$Date$</p > * <p>$Id$</p > */ package com.test; import java.util.concurrent.atomic.AtomicIntegerArray; import com.sun.org.apache.bcel.internal.generic.NEWARRAY; /** * @author hong_liping * */ public class AtomicArrayTest { static int[] array=new int[]{1,2,3}; static AtomicIntegerArray aia=new AtomicIntegerArray(array); public static void main(String[] args) { aia.getAndSet(1, 5); System.out.println(aia.get(1)); System.out.println(array[1]); if(aia.get(1)==array[1]){ System.out.println("陣列中的值與原子陣列中的相等"); }else{ System.out.println("陣列中的值與原子陣列中的不相等"); } } }
結果:
5
2
陣列中的值與原子陣列中的不相等
由以上的程式碼可以看出原子陣列與我本身定義的資料同一個下標下的值是不一樣的,為什麼呢,我們看一下原始碼就會發現原子資料操作的並不是我定義的變數本身,而是先拷貝一份,然後操作的是拷貝的版本。
public AtomicIntegerArray(int[] array) { // Visibility guaranteed by final field guarantees this.array = array.clone();//初始化陣列的時候拷貝 }
public final int getAndSet(int i, int newValue) { return unsafe.getAndSetInt(array, checkedByteOffset(i), newValue); }
在進行資料原子操作的時候使用的是魔術類Unsafe.
4、原子更新欄位類
如果我們只需要某個類裡的某個欄位,那麼就需要使用原子更新欄位類,Atomic包提 供了以下三個類: AtomicIntegerFieldUpdater:原子更新整型的欄位的更新器。 AtomicLongFieldUpdater:原子更新長整型欄位的更新器。 AtomicStampedReference:原子更新帶有版本號的引用型別。該類將整數值 與引用關聯起來,可用於原子的更資料和資料的版本號,可以解決使用CAS進行原子 更新時,可能出現的ABA問題。原子更新欄位類都是抽象類,每次使用都時候必須使用靜態方法newUpdater建立一個 更新器。原子更新類的欄位的必須使用public volatile修飾符。接下來我們再來看看AtomicIngerFieldUpdater
/** * <p>Title: AtomicIntegerFieldUpdateTest.java</p > * <p>Description: </p > * <p>@datetime 2019年8月10日 上午10:02:22</p > * <p>$Revision$</p > * <p>$Date$</p > * <p>$Id$</p > */ package com.test; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; /** * @author hong_liping * */ public class AtomicIntegerFieldUpdateTest { static AtomicIntegerFieldUpdater aifu=AtomicIntegerFieldUpdater.newUpdater(Person.class, "age"); static class Person{ private String name; public volatile int age; public Person(String name,int age){ this.name=name; this.age=age; } public int getAge(){ return age; } } public static void main(String[] args) { Person person=new Person("張三", 18); System.out.println(aifu.getAndIncrement(person)); System.out.println(aifu.get(person)); } }
測試結果:
18
19
在age屬性上加volatile是為了保證在多執行緒併發的情況下保證可見性。
Unsafe
Unsafe是位於sun.misc包下的一個類,主要提供一些用於執行低級別、不安全操作的方法,如直接訪問系統記憶體資源、自主管理記憶體資源等,這些方法在提升Java執行效率、增強Java語言底層資源操作能力方面起到了很大的作用。 Unsafe類為一單例實現,提供靜態方法getUnsafe獲取Unsafe例項,當且僅當呼叫getUnsafe方法的類為引導類載入器所載入時才合法,否則丟擲SecurityException異常。
@CallerSensitive /* */ public static Unsafe getUnsafe() /* */ { /* 88 */ Class localClass = Reflection.getCallerClass(); /* 89 */ if (!VM.isSystemDomainLoader(localClass.getClassLoader()))// 僅在引導類載入器`BootstrapClassLoader載入時才合法 /* 90 */ throw new SecurityException("Unsafe"); /* 91 */ return theUnsafe; /* */ } /* */
Unsafe經常用到的就是CAS,記憶體屏障(禁止load,store重新排序),執行緒排程(執行緒掛起,恢復還有獲取,釋放鎖)。
如何獲取Unsafe,1、把呼叫Unsafe相關方法的類Demo所在jar包路徑追加到預設的bootstrap路徑中,使得A被引導類載入器載入 java -Xbootclasspath/Demo:${path} // 其中path為呼叫Unsafe相關方法的類所在jar包路徑
2、通過反射獲取單例物件theUnsafe
我們可以看一下下面的一個程式碼:
public class UnsafeInstance { public static Unsafe reflectGetUnsafe(){ Field field; try { field = Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); return (Unsafe) field.get(null); } catch (Exception e) { e.printStackTrace(); } return null; } }
接下來再來看一個利用Unsafe的程式碼:
/** * <p>Title: AtomicUnsafeUpdaterTest.java</p > * <p>Description: </p > * <p>@datetime 2019年8月10日 上午10:57:23</p > * <p>$Revision$</p > * <p>$Date$</p > * <p>$Id$</p > */ package com.test; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import sun.misc.Unsafe; /** * @author hong_liping * */ public class AtomicUnsafeUpdaterTest { private String name; private volatile int age; private static final Unsafe unsafe=UnsafeInstance.reflectGetUnsafe(); private static final long valueOffset; static{ try { valueOffset=unsafe.objectFieldOffset(AtomicUnsafeUpdaterTest.class.getDeclaredField("age"));//偏移量 System.out.println("initial valueOffset is "+valueOffset); } catch (Exception e) { throw new Error(e); } } public void compareAndSwapAge(int old,int target){ unsafe.compareAndSwapInt(this, valueOffset, old, target); } public AtomicUnsafeUpdaterTest(String name,int age){ this.name=name; this.age=age; } public int getAge(){ return this.age; } public static void main(String[] args) { AtomicUnsafeUpdaterTest test=new AtomicUnsafeUpdaterTest("美女",30); test.compareAndSwapAge(30, 25); System.out.println("年齡變換後的值為"+test.getAge()); } }
1、CAS(unsafe的用法)的幾個重要方法以及引數:
/** * CAS * @param o 包含要修改field的物件 * @param offset 物件中某field的偏移量 * @param expected 期望值 * @param update 更新值 * @return true | false */ public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5); public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
上述中的偏移量是什麼呢,我們來看一下:AtomicUnsafeUpdaterTest的實現中,靜態欄位valueOffset即為欄位value的記憶體偏移地址,valueOffset的值在AtomicInteger初始化時,在靜態程式碼塊中通過Unsafe的objectFieldOffset方法獲取。在AtomicInteger中提供的執行緒安全方法中,通過欄位valueOffset的值可以定位到AtomicUnsafeUpdaterTest物件中value的記憶體地址,從而可以根據CAS實現對value欄位的原子操作。
下圖為某個AtomicInteger物件自增操作前後的記憶體示意圖,物件的基地址baseAddress=“0x110000”,通過baseAddress+valueOffset得到value的記憶體地valueAddress=“0x11000c”;然後通過CAS進行原子性的更新操作,成功則返回,否則繼續重試,直到更新成功為止。
2、unsafe執行緒排程
包括執行緒掛起、恢復、鎖機制等方法。
//取消阻塞執行緒 public native void unpark(Object thread); //阻塞執行緒 public native void park(boolean isAbsolute, long time); //獲得物件鎖(可重入鎖) @Deprecated public native void monitorEnter(Object o); //釋放物件鎖 @Deprecated public native void monitorExit(Object o); //嘗試獲取物件鎖 @Deprecated public native boolean tryMonitorEnter(Object o); 方法park、unpark即可實現執行緒的掛起與恢復,將一個執行緒進行掛起是通過park方法實現的,呼叫park方法後,執行緒將一直阻塞直到超時或者中斷等條件出現;unpark可以終止一個掛起的執行緒,使其恢復正常。在使用park和unpark的時候是可以顛倒的,先使用unpark,相當於取得一張票,在使用park的時候相當於使用這張票。 典型應用 Java鎖和同步器框架的核心類AbstractQueuedSynchronizer,就是通過呼叫LockSupport.park()和LockSupport.unpark()實現執行緒的阻塞和喚醒的,而LockSupport的park、unpark方法實際是呼叫Unsafe的park、unpark方式來實現。public class ThreadParkerTest { public static void main(String[] args) { /*Thread t = new Thread(new Runnable() { @Override public void run() { System.out.println("thread - is running----"); LockSupport.park();//阻塞當前執行緒 System.out.println("thread is over-----"); } }); t.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } LockSupport.unpark(t);//喚醒指定的執行緒*/ //拿出票據使用 LockSupport.park(); System.out.println("main thread is over"); //相當於先往池子裡放了一張票據 LockSupport.unpark(Thread.currentThread());//Pthread_mutex System.out.println("im running step 1"); } }
public class ObjectMonitorTest { static Object object = new Object(); /* public void method1(){ unsafe.monitorEnter(object); } public void method2(){ unsafe.monitorExit(object); }*/ public static void main(String[] args) { /*synchronized (object){ }*/ Unsafe unsafe = UnsafeInstance.reflectGetUnsafe(); unsafe.monitorEnter(object);//獲取鎖 //業務邏輯寫在此處之間 unsafe.monitorExit(object);//鎖釋放 }
3、記憶體屏障
在Java 8中引入,用於定義記憶體屏障(也稱記憶體柵欄,記憶體柵障,屏障指令等,是一類同步屏障指令,是CPU或編譯器在對記憶體隨機訪問的操作中的一個同步點,使得此點之前的 所有讀寫操作都執行後才可以開始執行此點之後的操作),避免程式碼重排序。 //記憶體屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障後,屏障後的load操作不能被重排序到屏障前 public native void loadFence(); //記憶體屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障後,屏障後的store操作不能被重排序到屏障前 public native void storeFence(); //記憶體屏障,禁止load、store操作重排序 public native void fullFence(); 典型應用 在Java 8中引入了一種鎖的新機制——StampedLock,它可以看成是讀寫鎖的一個改進版本。StampedLock提供了一種樂觀讀鎖的實現,這種樂觀讀鎖類似於無鎖的操作,完 全不會阻塞寫執行緒獲取寫鎖,從而緩解讀多寫少時寫執行緒“飢餓”現象。由於StampedLock提供的樂觀讀鎖不阻塞寫執行緒獲取讀鎖,當執行緒共享變數從主記憶體load到執行緒工作記憶體時,會存在資料不一致問題,所以當使用StampedLock的樂觀讀鎖時,需要遵從如下圖用例中使用的模式來確保資料的一致性。
public class FenceTest { public static void main(String[] args) { UnsafeInstance.reflectGetUnsafe().loadFence();//讀屏障 UnsafeInstance.reflectGetUnsafe().storeFence();//寫屏障 UnsafeInstance.reflectGetUnsafe().fullFence();//讀寫屏障 } }
以上就是關於原子操作和Unsafe的解讀,歡迎留言評論,謝