併發編程之原子操做Atomic&Unsafe

  原子操做:不能被分割(中斷)的一個或一系列操做叫原子操做。java

原子操做Atomic主要有12個類,4種類型的原子更新方式,原子更新基本類型,原子更新數組,原子更新字段,原子更新引用。Atomic包中的類基本都是使用Unsafe實現的包裝類。面試

基本類型:AtomicInteger,AtomicLong,AtomicBoolean;apache

引用類型:AtomicReference、AtomicReference的ABA實例、AtomicStampedRerence、AtomicMarkableReference;bootstrap

數組類型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray;數組

屬性原子修改器(Updater):AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater;安全

一、原子更新基本類型類

  用於經過原子的方式更新基本類型,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原子操做修改後數據2ide

 

根據上面的操做,咱們能夠看到的是AtomicInteger的操做自增,自減,值的替換等。可是此處應當注意的是原子操做存在一個ABA問題,ABA問題的現象就是:mainT執行完成後的值2(替換的2),otherT在執行2-1的時候的2是自增(1+1)的結果。在這兩個線程中用到的2不是同一個2,就至關因而一個漏洞,至關於說你從王健林帳號中偷走了10個億去投資,等你投資好了回本了,你再把這10個億打回了王健林帳號,這整個過程王建林沒有發現,你的整個操做過程也沒有記錄,因此對於王健林來講他的錢沒有丟失過,仍是放在那裏的。很明顯要解決這個ABA問題最好的辦法就是每一步操做都打個標記,至關於一個銀行的流水,這樣你偷錢,還錢的整個過程就有一個出,一個入,王健林看的時候就會發現個人總金沒有變,可是操做記錄顯示個人錢曾經被人盜了而後又被人還回來了。這就須要用到AtomicStampeReference.

二、原子更新引用類型

原子更新基本類型的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操做失敗
三、原子更新數組類
  經過原子的方式更新數組裏的某個元素,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.

四、原子更新字段類

若是咱們只須要某個類裏的某個字段,那麼就須要使用原子更新字段類,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,一、把調用Unsafe相關方法的類Demo所在jar包路徑追加到默認的bootstrap路徑中,使得A被引導類加載器加載 java -Xbootclasspath/Demo:${path} // 其中path爲調用Unsafe相關方法的類所在jar包路徑

二、經過反射獲取單例對象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()); } }

一、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進行原子性的更新操做,成功則返回,不然繼續重試,直到更新成功爲止。

 

 

二、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);//鎖釋放 }
 
 
 

 

三、內存屏障

在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的解讀,歡迎留言評論,謝謝。

相關文章
相關標籤/搜索