一、無鎖
對(duì)于并發(fā)控制而言,鎖是一種悲觀的策略,總是假設(shè)每一次進(jìn)入臨界區(qū)操作都會(huì)產(chǎn)生沖突,如果多線程訪問臨界區(qū)資源,就寧可犧牲性能讓線程等待,所以鎖會(huì)阻塞線程執(zhí)行。而無鎖是一種樂觀策略,它會(huì)假設(shè)對(duì)臨界區(qū)資源的訪問是沒有沖突的,所有線程都會(huì)不停頓的執(zhí)行,但是如果遇到?jīng)_突那該怎么辦?無鎖策略使用一種叫做比較交換的技術(shù)(CAS Compare And Swap)來鑒定線程沖突,一旦檢測(cè)到?jīng)_突,就進(jìn)行重試直至沒有沖突為止。
1.1 比較交換(CAS)
使用無鎖的方式?jīng)]有鎖競(jìng)爭(zhēng)帶來的系統(tǒng)開銷,也沒有線程間上下文頻繁調(diào)度帶來的開銷,相對(duì)有鎖而言具有優(yōu)越的性能。
CAS算法的過程是這樣的:它包含三個(gè)參數(shù)CAS(V,E,N)。V表示要更新的變量,E表示預(yù)期值,N表示新值。僅當(dāng)V值等于E時(shí),才會(huì)將V的值設(shè)為N,如果V值和E值不同,則說明已經(jīng)有其他線程做了更新,則當(dāng)前線程什么都不做。最后,CAS返回當(dāng)前V的真實(shí)值。CAS操作是抱著樂觀的態(tài)度進(jìn)行的,它總認(rèn)為自己可以完成操作。當(dāng)多個(gè)線程同時(shí)使用CAS操作一個(gè)變量時(shí),只有一個(gè)會(huì)勝出,并成功更新,其余均會(huì)失敗。失敗的線程不會(huì)被掛起,僅是被告知失敗,并且允許再次嘗試,當(dāng)然也允許失敗的線程放棄操作。基于這樣的原理,CAS操作即使沒有鎖,也可以發(fā)現(xiàn)其他線程對(duì)當(dāng)前線程的干擾,并進(jìn)行恰當(dāng)?shù)奶幚怼?/p>
1.2 無鎖的線程安全整數(shù)
JDK并發(fā)包中有一個(gè)atomic包,里面實(shí)現(xiàn)了一些直接使用CAS操作的線程安全的類型。其中,最常用的一個(gè)類,應(yīng)該就是AtomicInteger。可以把它看做是一個(gè)整數(shù)。但是與Integer不同,它是可變的,并且是線程安全的。對(duì)其進(jìn)行任何修改等操作,都是用CAS指令進(jìn)行的。AtomicInteger的一些主要方法:
//獲取當(dāng)前的值
public final int get()
//設(shè)置當(dāng)前值
public final void set(int newValue)
//取當(dāng)前的值,并設(shè)置新的值
public final int getAndSet(int newValue)
//如果當(dāng)前值為expect,則設(shè)置為update
public final boolean compareAndSet(int expect,int update)
//獲取當(dāng)前的值,并自增
public final int getAndIncrement()
//獲取當(dāng)前的值,并自減
public final int getAndDecrement()
//,返回當(dāng)前值,并增加delta
public final int addAndGet(int delta)
//當(dāng)前值+1,返回新值
public final int incrementAndGet()
//當(dāng)前值-1,返回新值
public final int decrementAndGet()
AtomicInteger有兩個(gè)核心字段:
//當(dāng)前實(shí)際取值
private volatile int value;
//value字段在AtomicInteger對(duì)象中的偏移量
private static final long valueOffset;
AtomicInteger使用的具體示例如下:
public class AtomicIntegerDemo {
static AtomicInteger i = new AtomicInteger();
public static class AddThred implements Runnable {
@Override
public void run() {
for(int k=0; k<10000; k++) {
i.incrementAndGet();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] ts = new Thread[10];
for (int k = 0; k < 10; k++) {
ts[k] = new Thread(new AddThred());
}
for(int k=0; k<10; k++) {ts[k].start();}
for(int k=0; k<10; k++) {ts[k].join();}
System.out.println(i);
}
}
如果執(zhí)行這段代碼,程序輸出100000。說明程序正常執(zhí)行,沒有錯(cuò)誤。如果不是線程安全的,i的值應(yīng)該會(huì)小于100000。
incrementAndGet()的內(nèi)部實(shí)現(xiàn)為:
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
第2行的死循環(huán)是因?yàn)镃AS操作未必是成功的,因此對(duì)于不成功的情況,我們就需要進(jìn)行不斷的嘗試。第3行的get()取得當(dāng)前的值,接著加1后得到新值next。這樣,得到了CAS必需的兩個(gè)參數(shù):期望值以及新值。使用compareAndSet()方法將新值next寫入,成功的條件是在寫入的時(shí)刻,當(dāng)前的值應(yīng)該要等于剛剛?cè)〉玫腸urrent。如果不是這樣,說明AtomicInteger的值在第3行到第5行代碼之間,又被其他線程修改了。當(dāng)前線程看到的狀態(tài)就是一個(gè)過期狀態(tài)。因此,compareAndSet返回失敗,需要下一次重試,直到成功。
和AtomicInteger類似的類還有AtomicLong用來代表long型,AtomicBoolean表示boolean型,AtomicReference表示對(duì)象引用。
1.3 無鎖的對(duì)象引用:AtomicReference
AtomicReference是對(duì)普通的對(duì)象引用,也就是它可以保證你在修改對(duì)象引用時(shí)的線程安全性。線程判斷被修改對(duì)象是否可以正確寫入的條件是對(duì)象的當(dāng)前值和期望值是否一致。但有可能出現(xiàn)例外,當(dāng)獲得對(duì)象當(dāng)前數(shù)據(jù)后,對(duì)象的值又恢復(fù)為舊值。這樣,當(dāng)前線程就無法正確判斷這個(gè)對(duì)象究竟是否被修改過。有這樣一個(gè)案例:打一個(gè)比方,如果有一家蛋糕店,為了挽留客戶,決定為貴賓卡里余額小于20元的客戶一次性贈(zèng)送20元。但條件是,每一位客戶只能被贈(zèng)送一次。
定義用戶賬戶余額:
static AtomicReference<Integer> money = new AtomicReference<Integer>();
money.set(19);
接著,需要若干個(gè)后臺(tái)線程,它們不斷掃描數(shù)據(jù),并為滿足條件的客戶充值:
for(int i=0; i<3; i++) {
new Thread() {
public void run() {
while(true) {
while(true) {
Integer m = money.get();
if(m<20) {
if(money.compareAndSet(m, m+20)) {
System.out.println("余額小于20元,充值成功,余額:"+ money.get() + "元");
break;
} else {
break;
}
}
}
}
};
}.start();
此時(shí),如果很不幸,就在贈(zèng)予金額到賬的同時(shí),用戶進(jìn)行了一次消費(fèi),正好消費(fèi)20元,這時(shí)的金額又小于20元。這時(shí),后臺(tái)的贈(zèng)予就會(huì)誤以為這個(gè)賬戶還沒有贈(zèng)予。所以,存在多次被贈(zèng)予的可能。下面模擬了這個(gè)消費(fèi)線程:
new Thread() {
public void run() {
for (int j = 0; j < 100; j++) {
while(true) {
Integer m = money.get();
if(m>10) {
System.out.println("大于10元");
if(money.compareAndSet(m, m-10)) {
System.out.println("成功消費(fèi)10元,余額:" + money.get());
break;
}
} else {
System.out.println("沒有足夠金額");
break;
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
};
}.start();
上述代碼,消費(fèi)者只要貴賓卡里的錢大于10元,就會(huì)立即進(jìn)行一次10元的消費(fèi)。執(zhí)行上述代碼,得到的輸出如下:
余額小于20元,充值成功,余額:39元
大于10元
成功消費(fèi)10元,余額:29
大于10元
成功消費(fèi)10元,余額:19
余額小于20元,充值成功,余額:39元
大于10元
成功消費(fèi)10元,余額:29
大于10元
成功消費(fèi)10元,余額:39
余額小于20元,充值成功,余額:39元
可以看到,這個(gè)賬戶被先后反復(fù)多次充值。其原因正是因?yàn)橘~戶余額被反復(fù)修改,修改后的值等于原有的值,使得CAS操作無法正確判斷當(dāng)前數(shù)據(jù)狀態(tài)。
1.4 帶有時(shí)間戳的隊(duì)形引用:AtomicStampedReference
之所以出現(xiàn)上面的這種情況,是因?yàn)锳tomicReference在對(duì)象修改過程中,丟失了狀態(tài)信息。AtomicStampedReference則解決了上述問題,其內(nèi)部不僅維護(hù)了對(duì)象值,還維護(hù)了一個(gè)時(shí)間戳。當(dāng)AtomicStampedReference對(duì)應(yīng)的數(shù)值被修改時(shí),除了更新數(shù)據(jù)本身外,還必須要更新時(shí)間戳。當(dāng)AtomicStampedReference設(shè)置對(duì)象值時(shí),對(duì)象值以及時(shí)間戳都必須滿足期望值,寫入才會(huì)成功。因此,即使對(duì)象值被反復(fù)讀寫,寫回原值,只要時(shí)間戳發(fā)生變化,就能防止不恰當(dāng)?shù)膶懭搿?br> AtomicStampedReference的幾個(gè)API在AtomicReference的基礎(chǔ)上新增了有關(guān)時(shí)間戳的信息:
//比較設(shè)置參數(shù)依次為:期望值 寫入新值 期望時(shí)間戳 新時(shí)間戳
public boolean compareAndSet(V expectedReference, V newReference,int expectedStamp,int newStamp)
//獲得當(dāng)前對(duì)象引用
public V getReference()
//獲得當(dāng)前時(shí)間戳
public int getStamp()
//設(shè)置當(dāng)前對(duì)象引用和時(shí)間戳
public void set(V newReference, int newStamp)
我們使用AtomicStampedReference解決上面反復(fù)充值的問題:
public class AtomicStampedReferenceDemo {
static AtomicStampedReference<Integer> money = new AtomicStampedReference<Integer>(19,0);
public static void main(String[] args) {
for(int i=0; i<3; i++) {
final int timestamp = money.getStamp();
new Thread() {
public void run() {
while(true) {
while(true) {
Integer m = money.getReference();
if(m<20) {
if(money.compareAndSet(m, m+20,timestamp,timestamp+1)) {
System.out.println("余額小于20元,充值成功,余額:"+ money.getReference() + "元");
break;
} else {
break;
}
}
}
}
};
}.start();
}
new Thread() {
public void run() {
for (int j = 0; j < 100; j++) {
while(true) {
int timestamp = money.getStamp();
Integer m = money.getReference();
if(m>10) {
System.out.println("大于10元");
if(money.compareAndSet(m, m-10,timestamp,timestamp+1)) {
System.out.println("成功消費(fèi)10元,余額:" + money.getReference());
break;
}
} else {
System.out.println("沒有足夠金額");
break;
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO: handle exception
}
}
};
}.start();
}
}
結(jié)果輸出如下:
余額小于20元,充值成功,余額:39元
大于10元
成功消費(fèi)10元,余額:29
大于10元
成功消費(fèi)10元,余額:19
大于10元
成功消費(fèi)10元,余額:9
沒有足夠金額
1.5 數(shù)組也能無鎖:AtomicIntegerArray
當(dāng)前可用的原子數(shù)組有:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray,分別表示整數(shù)數(shù)組、long型數(shù)組和普通對(duì)象數(shù)組。以AtomicIntegerArray為例,展示原子數(shù)組的使用方式。AtomicIntegerArray本質(zhì)上是對(duì)int[]類型的封裝,使用Unsafe類通過CAS的方式控制int[]在多線程的安全性。它提供以下幾個(gè)核心API:
//獲得數(shù)組第i個(gè)下標(biāo)的元素
public final int get(int i)
//獲得數(shù)組的長(zhǎng)度
public final int length()
//將數(shù)組第i個(gè)下標(biāo)設(shè)置為newValue,并返回舊的值
public final int getAndSet(int i, int newValue)
//進(jìn)行CAS操作,如果第i個(gè)下標(biāo)的元素等于expect,則設(shè)置為update,設(shè)置成功返回true
public final boolean compareAndSet(int i, int expect, int update)
//將第i個(gè)下標(biāo)元素加 1
public final int getAndIncrement(int i)
//將第i個(gè)下標(biāo)的元素減1
public final int getAndDecrement(int i)
//將第i個(gè)下標(biāo)的元素增加delta(delta可以是負(fù)數(shù))
public final int getAndAdd(int i, int delta)
下面展示AtomicIntegerArray的使用:
public class AtomicIntegerArrayDemo {
static AtomicIntegerArray arr = new AtomicIntegerArray(10);
public static class AddThread implements Runnable {
@Override
public void run() {
for(int k=0; k<10000; k++) {
arr.getAndIncrement(k%arr.length());
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] ts = new Thread[10];
for(int k=0; k<10; k++) {
ts[k] = new Thread(new AddThread());
}
for(int k=0; k<10; k++) {
ts[k].start();
}
for(int k=0; k<10; k++) {
ts[k].join();
}
System.out.println(arr);
}
}
結(jié)果為:
[10000,10000,10000,10000,10000,10000,10000,10000,10000,10000]
如果線程安全,數(shù)組內(nèi)10個(gè)元素的值必然都是10000。反之,如果線程不安全,則部分或者全部數(shù)值會(huì)小于10000。
1.6 讓普通變量也享受原子操作:AtomicIntegerFieldUpdater
可能由于初期考慮不周,一些普通變量會(huì)有線程安全的需求,原子包中的一個(gè)實(shí)用工具類AtomicIntegerFieldUpdater
可以讓普通變量享受CAS原子操作帶來的線程安全性。根據(jù)類型的不同,這個(gè)Updater
有三種:AtomicIntegerFieldUpdater
,AtomicLongFieldUpdater
和AtomicReferenceFieldUpdater
。分別可以對(duì)int
,long
和普通對(duì)象進(jìn)行CAS修改。
現(xiàn)在思考一個(gè)場(chǎng)景。假設(shè)某地要進(jìn)行一次選舉。現(xiàn)在模擬這個(gè)投票場(chǎng)景,如果選民投了候選人一票,就記為1,否則記為0。最終的選票顯然就是所有數(shù)據(jù)的簡(jiǎn)單求和。
public class AtomicIntegerFieldUpdaterDemo {
public static class Candidate {
int id;
volatile int score;
}
public final static AtomicIntegerFieldUpdater<Candidate> scoreUpdater
= AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score");
public static AtomicInteger allScore = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
final Candidate stu = new Candidate();
Thread[] t = new Thread[10000];
for(int i=0; i<10000; i++) {
t[i] = new Thread() {
@Override
public void run() {
if(Math.random() > 0.4) {
scoreUpdater.incrementAndGet(stu);
allScore.incrementAndGet();
}
}
};
t[i].start();
}
for(int i=0; i<10000; i++) {t[i].join();}
System.out.println("score=" + stu.score);
System.out.println("allScore=" + allScore);
}
}
運(yùn)行這段程序,最終的Candidate.score
總是和allScore
絕對(duì)相等。這說明AtomicIntegerFieldUpdater
很好地保證了Candidate.score
的線程安全。
注意:
- Updater只能修改它可見范圍的變量。因?yàn)閁pdater是通過反射得到的這個(gè)變量。如果變量不可見,會(huì)出錯(cuò)。比如score申明為private,就不行;
- 為了確保變量被正確的讀取,必須是volatile修飾;
- 由于CAS操作會(huì)通過對(duì)象實(shí)例中的偏移量直接進(jìn)行賦值,因此,它不支持static字段(public native long objectFieldOffset()不支持靜態(tài)變量) 。
二、有關(guān)死鎖的問題
死鎖:兩個(gè)或者多個(gè)線程,相互占用對(duì)方需要的資源,而都不進(jìn)行釋放,導(dǎo)致彼此之間都相互等待對(duì)方釋放資源,產(chǎn)生了無限制等待的現(xiàn)象。死鎖一旦發(fā)生,如果沒有外力接入,這種等待將永遠(yuǎn)存在,從而對(duì)線程產(chǎn)生嚴(yán)重的影響。
下面用一個(gè)簡(jiǎn)單的例子模擬死鎖問題:
public class DeadLock extends Thread {
protected Object tool;
static Object fork1 = new Object();
static Object fork2 = new Object();
public DeadLock(Object obj) {
this.tool = obj;
if(tool == fork1) {
this.setName("A");
}
if(tool == fork2) {
this.setName("B");
}
}
@Override
public void run() {
if(tool == fork1) {
synchronized (fork1) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (fork2) {
System.out.println("哲學(xué)家A開始吃飯了");
}
}
}
if(tool == fork2) {
synchronized (fork2) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (fork1) {
System.out.println("哲學(xué)家B開始吃飯了");
}
}
}
}
public static void main(String[] args) throws InterruptedException {
DeadLock A = new DeadLock(fork1);
DeadLock B = new DeadLock(fork2);
A.start();
B.start();
Thread.sleep(1000);
}
}
上述代碼模擬了兩個(gè)哲學(xué)家互相等待對(duì)方的叉子。如果吃飯必須用兩個(gè)叉子,哲學(xué)家A先占用叉子1,哲學(xué)家B占用叉子2,接著他們就互相等待,都沒有辦法獲得兩個(gè)叉子用餐,這就是死鎖。
如果在實(shí)際環(huán)境中,遇到了這種情況,通常的表現(xiàn)就是相關(guān)的進(jìn)程不再工作,并且CPU占用率為0(因?yàn)樗梨i的線程不占用CPU)。想確認(rèn)問題,需要使用JDK提供的工具。首先,可以使用jps命令得到j(luò)ava進(jìn)程的進(jìn)程ID,接著使用jstack命令得到線程的線程堆棧:
jps
獲取進(jìn)程。
jstack [id]
查看指定進(jìn)程的堆棧信息。