原創(chuàng)文章&經(jīng)驗(yàn)總結(jié)&從校招到A廠一路陽光一路滄桑
詳情請戳www.codercc.com
1. 原子操作類介紹
在并發(fā)編程中很容易出現(xiàn)并發(fā)安全的問題亡笑,有一個很簡單的例子就是多線程更新變量i=1,比如多個線程執(zhí)行i++操作,就有可能獲取不到正確的值,而這個問題靶瘸,最常用的方法是通過Synchronized進(jìn)行控制來達(dá)到線程安全的目的(關(guān)于synchronized可以看這篇文章)咬腋。但是由于synchronized是采用的是悲觀鎖策略吸申,并不是特別高效的一種解決方案两嘴。實(shí)際上,在J.U.C下的atomic包提供了一系列的操作簡單届慈,性能高效徒溪,并能保證線程安全的類去更新基本類型變量忿偷,數(shù)組元素,引用類型以及更新對象中的字段類型臊泌。atomic包下的這些類都是采用的是樂觀鎖策略去原子更新數(shù)據(jù)鲤桥,在java中則是使用CAS操作具體實(shí)現(xiàn)。
2. 預(yù)備知識--CAS操作
能夠弄懂a(chǎn)tomic包下這些原子操作類的實(shí)現(xiàn)原理缺虐,就要先明白什么是CAS操作。
什么是CAS?
使用鎖時礁凡,線程獲取鎖是一種悲觀鎖策略高氮,即假設(shè)每一次執(zhí)行臨界區(qū)代碼都會產(chǎn)生沖突,所以當(dāng)前線程獲取到鎖的時候同時也會阻塞其他線程獲取該鎖顷牌。而CAS操作(又稱為無鎖操作)是一種樂觀鎖策略剪芍,它假設(shè)所有線程訪問共享資源的時候不會出現(xiàn)沖突,既然不會出現(xiàn)沖突自然而然就不會阻塞其他線程的操作窟蓝。因此罪裹,線程就不會出現(xiàn)阻塞停頓的狀態(tài)。那么运挫,如果出現(xiàn)沖突了怎么辦状共?無鎖操作是使用CAS(compare and swap)又叫做比較交換來鑒別線程是否出現(xiàn)沖突,出現(xiàn)沖突就重試當(dāng)前操作直到?jīng)]有沖突為止谁帕。
CAS的操作過程
CAS比較交換的過程可以通俗的理解為CAS(V,O,N)峡继,包含三個值分別為:V 內(nèi)存地址存放的實(shí)際值;O 預(yù)期的值(舊值)匈挖;N 更新的新值碾牌。當(dāng)V和O相同時,也就是說舊值和內(nèi)存中實(shí)際的值相同表明該值沒有被其他線程更改過儡循,即該舊值O就是目前來說最新的值了舶吗,自然而然可以將新值N賦值給V。反之择膝,V和O不相同誓琼,表明該值已經(jīng)被其他線程改過了則該舊值O不是最新版本的值了,所以不能將新值N賦給V肴捉,返回V即可踊赠。當(dāng)多個線程使用CAS操作一個變量是,只有一個線程會成功每庆,并成功更新筐带,其余會失敗。失敗的線程會重新嘗試缤灵,當(dāng)然也可以選擇掛起線程
CAS的實(shí)現(xiàn)需要硬件指令集的支撐伦籍,在JDK1.5后虛擬機(jī)才可以使用處理器提供的CMPXCHG指令實(shí)現(xiàn)蓝晒。
Synchronized VS CAS
元老級的Synchronized(未優(yōu)化前)最主要的問題是:在存在線程競爭的情況下會出現(xiàn)線程阻塞和喚醒鎖帶來的性能問題,因?yàn)檫@是一種互斥同步(阻塞同步)帖鸦。而CAS并不是武斷的間線程掛起斥滤,當(dāng)CAS操作失敗后會進(jìn)行一定的嘗試,而非進(jìn)行耗時的掛起喚醒的操作杆故,因此也叫做非阻塞同步舱沧。這是兩者主要的區(qū)別。
CAS的問題
ABA問題
因?yàn)镃AS會檢查舊值有沒有變化攻锰,這里存在這樣一個有意思的問題晾嘶。比如一個舊值A(chǔ)變?yōu)榱顺葿,然后再變成A娶吞,剛好在做CAS時檢查發(fā)現(xiàn)舊值并沒有變化依然為A垒迂,但是實(shí)際上的確發(fā)生了變化。解決方案可以沿襲數(shù)據(jù)庫中常用的樂觀鎖方式妒蛇,添加一個版本號可以解決机断。原來的變化路徑A->B->A就變成了1A->2B->3C。自旋時間過長
使用CAS時非阻塞同步绣夺,也就是說不會將線程掛起吏奸,會自旋(無非就是一個死循環(huán))進(jìn)行下一次嘗試,如果這里自旋時間過長對性能是很大的消耗陶耍。如果JVM能支持處理器提供的pause指令苦丁,那么在效率上會有一定的提升。
3. 原子更新基本類型
atomic包提高原子更新基本類型的工具類物臂,主要有這些:
- AtomicBoolean:以原子更新的方式更新boolean旺拉;
- AtomicInteger:以原子更新的方式更新Integer;
- AtomicLong:以原子更新的方式更新Long;
這幾個類的用法基本一致棵磷,這里以AtomicInteger為例總結(jié)常用的方法
- addAndGet(int delta) :以原子方式將輸入的數(shù)值與實(shí)例中原本的值相加蛾狗,并返回最后的結(jié)果;
- incrementAndGet() :以原子的方式將實(shí)例中的原值進(jìn)行加1操作仪媒,并返回最終相加后的結(jié)果沉桌;
- getAndSet(int newValue):將實(shí)例中的值更新為新值,并返回舊值算吩;
- getAndIncrement():以原子的方式將實(shí)例中的原值加1留凭,返回的是自增前的舊值;
還有一些方法偎巢,可以查看API蔼夜,不再贅述。為了能夠弄懂AtomicInteger的實(shí)現(xiàn)原理压昼,以getAndIncrement方法為例求冷,來看下源碼:
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
可以看出瘤运,該方法實(shí)際上是調(diào)用了unsafe實(shí)例的getAndAddInt方法,unsafe實(shí)例的獲取時通過UnSafe類的靜態(tài)方法getUnsafe獲冉程狻:
private static final Unsafe unsafe = Unsafe.getUnsafe();
Unsafe類在sun.misc包下拯坟,Unsafer類提供了一些底層操作,atomic包下的原子操作類的也主要是通過Unsafe類提供的compareAndSwapInt韭山,compareAndSwapLong等一系列提供CAS操作的方法來進(jìn)行實(shí)現(xiàn)郁季。下面用一個簡單的例子來說明AtomicInteger的用法:
public class AtomicDemo {
private static AtomicInteger atomicInteger = new AtomicInteger(1);
public static void main(String[] args) {
System.out.println(atomicInteger.getAndIncrement());
System.out.println(atomicInteger.get());
}
}
輸出結(jié)果:
1
2
例子很簡單,就是新建了一個atomicInteger對象钱磅,而atomicInteger的構(gòu)造方法也就是傳入一個基本類型數(shù)據(jù)即可梦裂,對其進(jìn)行了封裝。對基本變量的操作比如自增续搀,自減塞琼,相加菠净,更新等操作禁舷,atomicInteger也提供了相應(yīng)的方法進(jìn)行這些操作。但是毅往,因?yàn)閍tomicInteger借助了UnSafe提供的CAS操作能夠保證數(shù)據(jù)更新的時候是線程安全的牵咙,并且由于CAS是采用樂觀鎖策略,因此攀唯,這種數(shù)據(jù)更新的方法也具有高效性洁桌。
AtomicLong的實(shí)現(xiàn)原理和AtomicInteger一致,只不過一個針對的是long變量侯嘀,一個針對的是int變量另凌。而boolean變量的更新類AtomicBoolean類是怎樣實(shí)現(xiàn)更新的呢?核心方法是compareAndSet
t方法,其源碼如下:
public final boolean compareAndSet(boolean expect, boolean update) {
int e = expect ? 1 : 0;
int u = update ? 1 : 0;
return unsafe.compareAndSwapInt(this, valueOffset, e, u);
}
可以看出戒幔,compareAndSet方法的實(shí)際上也是先轉(zhuǎn)換成0,1的整型變量吠谢,然后是通過針對int型變量的原子更新方法compareAndSwapInt來實(shí)現(xiàn)的∈ィ可以看出atomic包中只提供了對boolean,int ,long這三種基本類型的原子更新的方法工坊,參考對boolean更新的方式,原子更新char,doule,float也可以采用類似的思路進(jìn)行實(shí)現(xiàn)敢订。
4. 原子更新數(shù)組類型
atomic包下提供能原子更新數(shù)組中元素的類有:
- AtomicIntegerArray:原子更新整型數(shù)組中的元素王污;
- AtomicLongArray:原子更新長整型數(shù)組中的元素;
- AtomicReferenceArray:原子更新引用類型數(shù)組中的元素
這幾個類的用法一致楚午,就以AtomicIntegerArray來總結(jié)下常用的方法:
- addAndGet(int i, int delta):以原子更新的方式將數(shù)組中索引為i的元素與輸入值相加昭齐;
- getAndIncrement(int i):以原子更新的方式將數(shù)組中索引為i的元素自增加1;
- compareAndSet(int i, int expect, int update):將數(shù)組中索引為i的位置的元素進(jìn)行更新
可以看出矾柜,AtomicIntegerArray與AtomicInteger的方法基本一致司浪,只不過在AtomicIntegerArray的方法中會多一個指定數(shù)組索引位i泊业。下面舉一個簡單的例子:
public class AtomicDemo {
// private static AtomicInteger atomicInteger = new AtomicInteger(1);
private static int[] value = new int[]{1, 2, 3};
private static AtomicIntegerArray integerArray = new AtomicIntegerArray(value);
public static void main(String[] args) {
//對數(shù)組中索引為1的位置的元素加5
int result = integerArray.getAndAdd(1, 5);
System.out.println(integerArray.get(1));
System.out.println(result);
}
}
輸出結(jié)果:
7
2
通過getAndAdd方法將位置為1的元素加5,從結(jié)果可以看出索引為1的元素變成了7啊易,該方法返回的也是相加之前的數(shù)為2吁伺。
5. 原子更新引用類型
如果需要原子更新引用類型變量的話,為了保證線程安全租谈,atomic也提供了相關(guān)的類:
- AtomicReference:原子更新引用類型篮奄;
- AtomicReferenceFieldUpdater:原子更新引用類型里的字段;
- AtomicMarkableReference:原子更新帶有標(biāo)記位的引用類型割去;
這幾個類的使用方法也是基本一樣的窟却,以AtomicReference為例,來說明這些類的基本用法呻逆。下面是一個demo
public class AtomicDemo {
private static AtomicReference<User> reference = new AtomicReference<>();
public static void main(String[] args) {
User user1 = new User("a", 1);
reference.set(user1);
User user2 = new User("b",2);
User user = reference.getAndSet(user2);
System.out.println(user);
System.out.println(reference.get());
}
static class User {
private String userName;
private int age;
public User(String userName, int age) {
this.userName = userName;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"userName='" + userName + '\'' +
", age=" + age +
'}';
}
}
}
輸出結(jié)果:
User{userName='a', age=1}
User{userName='b', age=2}
首先將對象User1用AtomicReference進(jìn)行封裝夸赫,然后調(diào)用getAndSet方法,從結(jié)果可以看出咖城,該方法會原子更新引用的user對象茬腿,變?yōu)?code>User{userName='b', age=2},返回的是原來的user對象User{userName='a', age=1}
宜雀。
6. 原子更新字段類型
如果需要更新對象的某個字段切平,并在多線程的情況下,能夠保證線程安全辐董,atomic同樣也提供了相應(yīng)的原子操作類:
- AtomicIntegeFieldUpdater:原子更新整型字段類悴品;
- AtomicLongFieldUpdater:原子更新長整型字段類;
- AtomicStampedReference:原子更新引用類型简烘,這種更新方式會帶有版本號苔严。而為什么在更新的時候會帶有版本號,是為了解決CAS的ABA問題孤澎;
要想使用原子更新字段需要兩步操作:
- 原子更新字段類都是抽象類届氢,只能通過靜態(tài)方法
newUpdater
來創(chuàng)建一個更新器,并且需要設(shè)置想要更新的類和屬性亥至; - 更新類的屬性必須使用
public volatile
進(jìn)行修飾悼沈;
這幾個類提供的方法基本一致,以AtomicIntegerFieldUpdater為例來看看具體的使用:
public class AtomicDemo {
private static AtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(User.class,"age");
public static void main(String[] args) {
User user = new User("a", 1);
int oldValue = updater.getAndAdd(user, 5);
System.out.println(oldValue);
System.out.println(updater.get(user));
}
static class User {
private String userName;
public volatile int age;
public User(String userName, int age) {
this.userName = userName;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"userName='" + userName + '\'' +
", age=" + age +
'}';
}
}
}
輸出結(jié)果:
1
6
從示例中可以看出姐扮,創(chuàng)建AtomicIntegerFieldUpdater
是通過它提供的靜態(tài)方法進(jìn)行創(chuàng)建絮供,getAndAdd
方法會將指定的字段加上輸入的值,并且返回相加之前的值茶敏。user對象中age字段原值為1壤靶,加5之后,可以看出user對象中的age字段的值已經(jīng)變成了6惊搏。