享學課堂特邀做者:瀟湘夜雨java
轉載請聲明出處!數組
原子的意思是說「不能被進一步分割的粒子」,而原子操做是說「不可被終端的一個或多個系列的操做」。假定有兩個操做A和B,若是從執行A的線程來看,當另外一個線程執行B時,要麼將B所有執行完,要麼徹底不執行B,那麼A和B對彼此來講是原子的。安全
java中能夠經過鎖,鎖機制的方式來實現原子操做,可是有時候須要更有效靈活的機制,synchronized關鍵字是基於阻塞的鎖機制,也就是說當一個線程擁有鎖的時候,訪問同一資源的其它線程須要等待,直到該線程釋放鎖,由於synchronized關鍵字具備排他性,若是有大量的線程來競爭資源,那CPU將會花費大量的時間和資源來處理這些競爭,同時也會形成死鎖的狀況。並且鎖的機制至關於其餘輕量級的需求有點過於笨重,例如計數器,這個後邊我會介紹二者之間的性能的比較。bash
實現原子操做還可使用CAS實現原子操做,利用了處理器提供的CMPXCHG指令來實現的,每個CAS操做過程都包含三個運算符:一個內存地址V,一個指望的值A和一個新值B,操做的時候若是這個地址上存放的值等於這個指望的值A,則將地址上的值賦爲新值B,不然不作任何操做。
CAS的基本思路就是,若是這個地址上的值和指望的值相等,則給其賦予新值,不然不作任何事,可是要返回原值是多少。循環CAS就是在一個循環裏不斷的作cas操做,直到成功爲止。下面的代碼實現了一個CAS線程安全的計數器safeCount。併發
public class Counter {
private AtomicInteger atomicCount = new AtomicInteger(0);
private int i = 0;
/** cas cafecount **/
private void safeCount() {
for (; ; ) {
int i = atomicCount.get();
boolean suc = atomicCount.compareAndSet(i, ++i);
if (suc) {
break;
}
}
}
public static void main(String[] args) {
Counter cas = new Counter();
List<Thread> ts = new ArrayList<>(500);
long start = System.currentTimeMillis();
for (int j = 0; j < 100; j++) {
Thread t = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
cas.safeCount();
}
});
ts.add(t);
}
for (Thread t : ts) {
t.start();
}
for (Thread t : ts) {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(cas.i);
System.out.println(cas.atomicCount.get());
System.out.println(System.currentTimeMillis() - start);
}
}
複製代碼
在Java併發包中有一些併發框架也使用了自旋CAS的方式來實現原子操做,好比LinkedTransferQueue類的xfer方法。CAS雖然很高的解決了原子操做,可是CAS仍然存在三大問題。ABA問題、循環時間長開銷大、以及只能保證一個共享變量的原子操做。框架
由於CAS須要在操做值的時候,檢查值有沒有發生變化,若是發生變化則更新,可是若是一個值爲A,變成了B,又變成了A,那麼使用CAS進行檢查時就會發現它的值沒有發生變化,但實際上發生變化了。ABA問題的解決思路就是使用版本號,在變量前邊追加版本號,每次變量更新的時候把版本號加1,那麼A→B→A就會變成1A→2B→3A。從java1.5開始,JDK提供了AtomicStampedReference、AtomicMarkableReference來解決ABA的問題,經過compareAndSet方法檢查值是否發生變化之外檢查版本號知否發生變化。(AtomicStampedReference可以獲得變化的次數這裏下邊會介紹到)工具
自旋CAS若是長時間不成功,會給CPU帶來很是大的執行開銷。性能
當對一個共享變量執行操做時,咱們可使用循環CAS的方式來保證原子操做,可是對多個共享變量操做時,循環CAS就沒法保證操做的原子性,這個時候就能夠用鎖。還有一個取巧的辦法,就是把多個共享變量合併成一個共享變量來操做。好比,有兩個共享變量i=2,j=a,合併一下ij=2a,而後用CAS來操做ij。從Java 1.5開始,JDK提供了AtomicReference類來保證引用對象之間的原子性,就能夠把多個變量放在一個對象裏來進行CAS操做。ui
從1.5開始,JDK的併發包裏提供了一些類來支持原子操做,如AtomicBoolean、AtomicInter。這些原子包裝類還提供了簡單、性能高效、線程安全有用的工具方法,而且 在併發代碼來講是很是關鍵的。原子變量將發生在單個的變量上,粒度最細的狀況。原子變量類有不少種,因此Atomic包裏一共提供了13個類,屬於4種類型的原子更新方式,分別是原子更新基本類型、原子更新數組、原子更新引用和原子更新屬性。Atomic包裏的類基本都是經過Unsafe實現包裝類。this
使用原子方式更新基本類型,Atomic包提供瞭如下3個類。
AtomicBoolean:原子更新布爾值類型
AtomicInteger:原子更新整型
AtomicLong:原子更新長整形
以AtomicInteger爲例:
public final int addAndGet(int delta)
複製代碼
以原子方式將輸入的數值與實例的值相加,並返回結果。
public final boolean compareAndSet(int expect, int update)
複製代碼
若是輸入的數值等於預期值,則以原子的方式設置輸入的值。
public final int getAndIncrement()
複製代碼
以原子方式將當前值加1,注意,這裏返回的是自增前的值。
public final int getAndSet(int newValue)
複製代碼
以原子方式設置爲newValue的值,並返回舊值。
接下來咱們看一下getAndIncrement源碼
public final int getAndIncrement()
複製代碼
以原子方式將當前值設置爲1,這裏返回的是自增的值。 在JDK1.8中getAndIncrement是如何實現原子操做的呢?咱們分析一下源碼,
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
複製代碼
getAndIncrement方法調用了首先Unsafe的getAndAddInt方法,獲取當前數值,而後循環compareAndSwapObject驗證v和delta是否相等若是不相等返回v,這裏意味着A tmoicInteger值是否修改過。
接下來咱們看Unsafe類
//更新變量值爲x,若是當前值爲expected
//o:對象 offset:偏移量 expected:指望值 x:新值
public final native boolean compareAndSwapObject(Object o,
long offset,
Object expected,
Object x);
public final native boolean compareAndSwapInt(Object o, long offset,
int expected,
int x);
public final native boolean compareAndSwapLong(Object o, long offset,
long expected,
long x);
複製代碼
經過代碼,咱們發現Unsafe只提供了3種CAS方法:compareAndSwapObject、compareAndSwapInt和compareAndSwapLong,再看AtomicBoolean源碼,發現它是先把Boolean轉換成整 型,再使用compareAndSwapInt進行CAS,因此原子更新char、float和double變量也能夠用相似 的思路來實現。
經過原子的方式更新數組裏的某個元素,Atomic包提供瞭如下四個類。
AtomicIntegerArray:原子更新整型數組裏的元素。
AtomicLongArray:原子更新長征信數組裏的元素。
AtomicReferenceArray:原子更新引用類型數組裏的元素。
以AtomicIntegerArray爲例,AtomicIntegerArray的使用實例代碼以下
public class AtomicIntegerArrayTest {
static int[] value = new int[] { 1, 2 };
static AtomicIntegerArray ai = new AtomicIntegerArray(value);
public static void main(String[] args) {
ai.getAndSet(0, 3);
System.out.println(ai.get(0));
System.out.println(value[0]);
}
}
複製代碼
輸出的結果是3和1,這裏要注意的是數組value經過構造方法傳遞進去,而後AtomicIntegerArray會將當前數組 複製一份,因此當AtomicIntegerArray對內部的數組元素進行修改時,不會影響傳入的數組。
原子更新基本類型的AtomicInteger,只能更新一個變量,則須要使用這個原子引用類型提供的類。
AtomicReference:原子更新引用類型。
AtomicReferenceFieldUpdater:原子更新引用類型裏的字段。
AtomicMarkableReference:原子更新帶有標記的引用類型,能夠以原子更新一個布爾類型的標誌位和引用類型。 以AtomicReference爲例,AtomicReference的使用示例代碼以下
public class AtomicReferenceTest {
public static AtomicReference<user> atomicUserRef = new
AtomicReference<user>();
public static void main(String[] args) {
User user = new User("tim", 15);
atomicUserRef.set(user);
User updateUser = new User("jack", 17);
atomicUserRef.compareAndSet(user, updateUser);
System.out.println(atomicUserRef.get().getName());
System.out.println(atomicUserRef.get().getOld());
}
}
複製代碼
代碼中首先構建一個user對象,而後把user對象設置進AtomicReferenc中,最後調用 compareAndSet方法進行原子更新操做,實現原理同AtomicInteger裏的compareAndSet方法。
若是效原子更新某個類的字段須要使用更新字段類
AtomicIntegerFieldUpdater:原子更新整數型字段更新器
AtomicLongFieldUpdater:原子更新長整數字段更新器 AtomicStampedReference:原子更新帶有版本號的引用類型。
要想原子地更新字段類須要兩步。第一步,由於原子更新字段類都是抽象類,每次使用的 時候必須使用靜態方法newUpdater()建立一個更新器,而且須要設置想要更新的類和屬性。第 二步,更新類的字段(屬性)必須使用public volatile修飾符。
public class AtomicIntegerFieldUpdaterTest {
// 建立原子更新器,並設置須要更新的對象類和對象的屬性
private static AtomicIntegerFieldUpdater<User> a = AtomicIntegerFieldUpdater.newUpdater(User.class, "old");
public static void main(String[] args) {
// 設置柯南的年齡是10歲
User conan = new User("tim", 10);
// 柯南長了一歲,可是仍然會輸出舊的年齡
System.out.println(a.getAndIncrement(conan));
// 輸出柯南如今的年齡
System.out.println(a.get(conan));
}
public static class User {
private String name;
public volatile int old;
public User(String name, int old) {
this.name = name;
this.old = old;
}
public String getName() {
return name;
}
public int getOld() {
return old;
}
}
}
複製代碼
你的贊和關注是我繼續創做的動力~