並發編程之原子操作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的解讀,歡迎留言評論,謝謝。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM