Java多執行緒-無鎖
1 無鎖類的原理詳解
1.1 CAS
CAS演算法的過程是這樣:它包含3個引數CAS(V,E,N)。V表示要更新的變數,E表示預期值,N表示新值。僅當V
值等於E值時,才會將V的值設為N,如果V值和E值不同,則說明已經有其他執行緒做了更新,則當前執行緒什麼
都不做。最後,CAS返回當前V的真實值。CAS操作是抱著樂觀的態度進行的,它總是認為自己可以成功完成
操作。當多個執行緒同時使用CAS操作一個變數時,只有一個會勝出,併成功更新,其餘均會失敗。失敗的執行緒
不會被掛起,僅是被告知失敗,並且允許再次嘗試,當然也允許失敗的執行緒放棄操作。基於這樣的原理,CAS
操作即時沒有鎖,也可以發現其他執行緒對當前執行緒的干擾,並進行恰當的處理。
我們會發現,CAS的步驟太多,有沒有可能在判斷V和E相同後,正要賦值時,切換了執行緒,更改了值。造成了資料不一致呢?
事實上,這個擔心是多餘的。CAS整一個操作過程是一個原子操作,它是由一條CPU指令完成的。
1.2 CPU指令
CAS的CPU指令是cmpxchg
指令程式碼如下:
/*
accumulator = AL, AX, or EAX,depending on whether
a byte, word, or doublewordcomparison is beingperformed
*/
if(accumulator == Destination){
ZF =1;
Destination =Source;
}
else{
ZF =0;
accumulator =Destination;
}
目標值和暫存器裡的值相等的話,就設定一個跳轉標誌,並且把原始資料設到目標裡面去。如果不等的話,就不設定跳轉標誌了。
Java當中提供了很多無鎖類,下面來介紹下無鎖類。
2 無所類的使用
我們已經知道,無鎖比阻塞效率要高得多。我們來看看Java是如何實現這些無鎖類的。
2.1. AtomicInteger
AtomicInteger和Integer一樣,都繼承與Number類
public classAtomicInteger extends Number implements java.io.Serializable
AtomicInteger裡面有很多CAS操作,典型的有:
public final booleancompareAndSet(int expect, int update) {
returnunsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
這裡來解釋一下unsafe.compareAndSwapInt方法,他的意思是,對於this這個類上的偏移量為valueOffset的變數值如果與期望值expect相同,那麼把這個變數的值設為update。
其實偏移量為valueOffset的變數就是value
static {
try {
valueOffset =unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw newError(ex); }
}
我們此前說過,CAS是有可能會失敗的,但是失敗的代價是很小的,所以一般的實現都是在一個無限迴圈體內,直到成功為止。
public final intgetAndIncrement() {
for (;;) {
int current =get();
int next = current + 1;
if (compareAndSet(current,next))
returncurrent;
}
}
2.2 Unsafe
從類名就可知,Unsafe操作是非安全的操作,比如:
- 根據偏移量設定值(在剛剛介紹的AtomicInteger中已經看到了這個功能)
- park()(把這個執行緒停下來,在以後的Blog中會提到)
- 底層的CAS操作
非公開API,在不同版本的JDK中,可能有較大差異
2.3. AtomicReference
前面已經提到了AtomicInteger,當然還有AtomicBoolean,AtomicLong等等,都大同小異。
這裡要介紹的是AtomicReference。
AtomicReference是一種模板類
public classAtomicReference<V> implementsjava.io.Serializable
它可以用來封裝任意型別的資料。
比如String
package test;
importjava.util.concurrent.atomic.AtomicReference;
public classTest
{
public final staticAtomicReference<String> atomicString = newAtomicReference<String>("hosee");
public static voidmain(String[]args)
{
for(int i = 0; i < 10; i++)
{
finalint num = i;
newThread(){
publicvoid run(){
try
{
Thread.sleep(Math.abs((int)Math.random()*100));
}
catch(Exception e)
{
e.printStackTrace();
}
if(atomicString.compareAndSet("hosee","ztk"))
{
System.out.println(Thread.currentThread().getId()+ "Changevalue");
}else{
System.out.println(Thread.currentThread().getId()+"Failed");
}
};
}.start();
}
}
}
結果:
10Failed
13Failed
9Changevalue
11Failed
12Failed
15Failed
17Failed
14Failed
16Failed
18Failed
可以看到只有一個執行緒能夠修改值,並且後面的執行緒都不能再修改。
2.4.AtomicStampedReference
我們會發現CAS操作還是有一個問題的
比如之前的AtomicInteger的incrementAndGet方法
public final intincrementAndGet() {
for (;;) {
int current =get();
int next = current + 1;
if (compareAndSet(current,next))
return next;
}
}
假設當前value=1當某執行緒intcurrent =get()執行後,切換到另一個執行緒,這個執行緒將1變成了2,然後又一個執行緒將2又變成了1。此時再切換到最開始的那個執行緒,由於value仍等於1,所以還是能執行CAS操作,當然加法是沒有問題的,如果有些情況,對資料的狀態敏感時,這樣的過程就不被允許了。
此時就需要AtomicStampedReference類。
其內部實現一個Pair類來封裝值和時間戳。
private static classPair<T> {
final T reference;
final int stamp;
private Pair(T reference, intstamp) {
this.reference =reference;
this.stamp =stamp;
}
static <T> Pair<T>of(T reference, int stamp) {
return newPair<T>(reference, stamp);
}
}
這個類的主要思想是加入時間戳來標識每一次改變。
//比較設定 引數依次為:期望值 寫入新值期望時間戳 新時間戳
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference ==current.reference &&
expectedStamp == current.stamp&&
((newReference == current.reference&&
newStamp == current.stamp)||
casPair(current,Pair.of(newReference, newStamp)));
}
當期望值等於當前值,並且期望時間戳等於現在的時間戳時,才寫入新值,並且更新新的時間戳。
這裡舉個用AtomicStampedReference的場景,可能不太適合,但是想不到好的場景了。
場景背景是,某公司給餘額少的使用者免費充值,但是每個使用者只能充值一次。
package test;
importjava.util.concurrent.atomic.AtomicStampedReference;
public classTest
{
staticAtomicStampedReference<Integer> money = newAtomicStampedReference<Integer>(
19,0);
public static voidmain(String[]args)
{
for(int i = 0; i < 3; i++)
{
finalint timestamp = money.getStamp();
newThread()
{
publicvoidrun()
{
while(true)
{
while(true)
{
Integerm = money.getReference();
if(m <20)
{
if(money.compareAndSet(m, m + 20,timestamp,
timestamp+1))
{
System.out.println("充值成功,餘額:"
+money.getReference());
break;
}
}
else
{
break;
}
}
}
};
}.start();
}
newThread()
{
publicvoidrun()
{
for(int i = 0; i < 100;i++)
{
while(true)
{
inttimestamp =money.getStamp();
Integerm = money.getReference();
if(m >10)
{
if(money.compareAndSet(m, m - 10, timestamp,
timestamp+ 1))
{
System.out.println("消費10元,餘額:"
+money.getReference());
break;
}
}else{
break;
}
}
try
{
Thread.sleep(100);
}
catch(Exception e)
{
//TODO: handleexception
}
}
};
}.start();
}
}
解釋下程式碼,有3個執行緒在給使用者充值,當使用者餘額少於20時,就給使用者充值20元。有100個執行緒在消費,每次消費10元。使用者初始有9元,當使用AtomicStampedReference來實現時,只會給使用者充值一次,因為每次操作使得時間戳+1。執行結果:
充值成功,餘額:39
消費10元,餘額:29
消費10元,餘額:19
消費10元,餘額:9
如果使用AtomicReference<Integer>或者Atomic Integer來實現就會造成多次充值。
充值成功,餘額:39
消費10元,餘額:29
消費10元,餘額:19
充值成功,餘額:39
消費10元,餘額:29
消費10元,餘額:19
充值成功,餘額:39
消費10元,餘額:29
2.5. AtomicIntegerArray
與AtomicInteger相比,陣列的實現不過是多了一個下標。
public final booleancompareAndSet(int i, int expect, int update) {
returncompareAndSetRaw(checkedByteOffset(i), expect, update);
}
它的內部只是封裝了一個普通的array
private final int[]array;
裡面有意思的是運用了二進位制數的前導零來算陣列中的偏移量。
shift = 31 -Integer.numberOfLeadingZeros(scale);
前導零的意思就是比如8位表示12,00001100,那麼前導零就是1前面的0的個數,就是4。
具體偏移量如何計算,這裡就不再做介紹了。
2.6. AtomicIntegerFieldUpdater
AtomicIntegerFieldUpdater類的主要作用是讓普通變數也享受原子操作。
就比如原本有一個變數是int型,並且很多地方都應用了這個變數,但是在某個場景下,想讓int型變成AtomicInteger,但是如果直接改型別,就要改其他地方的應用。AtomicIntegerFieldUpdater就是為了解決這樣的問題產生的。
package test;
importjava.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
public classTest
{
public static classV{
intid;
volatileintscore;
publicint getScore()
{
returnscore;
}
publicvoid setScore(int score)
{
this.score= score;
}
}
public final staticAtomicIntegerFieldUpdater<V> vv =AtomicIntegerFieldUpdater.newUpdater(V.class,"score");
public static AtomicIntegerallscore = newAtomicInteger(0);
public static voidmain(String[] args) throws InterruptedException
{
finalV stu = newV();
Thread[]t = newThread[10000];
for(int i = 0; i < 10000;i++)
{
t[i]= new Thread() {
@Override
publicvoidrun()
{
if(Math.random()>0.4)
{
vv.incrementAndGet(stu);
allscore.incrementAndGet();
}
}
};
t[i].start();
}
for(int i = 0; i < 10000; i++)
{
t[i].join();
}
System.out.println("score="+stu.getScore());
System.out.println("allscore="+allscore);
}
}
上述程式碼將score使用AtomicIntegerFieldUpdater變成AtomicInteger。保證了執行緒安全。
這裡使用allscore來驗證,如果score和allscore數值相同,則說明是執行緒安全的。
最後
1、Updater只能修改它可見範圍內的變數。因為Updater使用反射得到這個變數。如果變數不可見,就會出錯。比如如果某變數申明為private,就是不可行的。
2、為了確保變數被正確的讀取,它必須是volatile型別的。如果我們原有程式碼中未申明這個型別,那麼簡單得申明一下就行,這不會引起什麼問題。
3、由於CAS操作會通過物件例項中的偏移量直接進行賦值,因此,它不支援static欄位(Unsafe.objectFieldOffset()不支援靜態變數)。
相關文章
- Java多執行緒(2)執行緒鎖Java執行緒
- java多執行緒–同步鎖Java執行緒
- Java多執行緒(五):死鎖Java執行緒
- java多執行緒(5)死鎖Java執行緒
- Java多執行緒7:死鎖Java執行緒
- Java多執行緒中執行緒安全與鎖問題Java執行緒
- JAVA多執行緒詳解(3)執行緒同步和鎖Java執行緒
- 多執行緒_鎖執行緒
- Java多執行緒 -- 公平鎖和非公平鎖Java執行緒
- JAVA多執行緒與鎖機制Java執行緒
- java多執行緒中的死鎖、活鎖、飢餓、無鎖都是什麼鬼?Java執行緒
- Java多執行緒5:synchronized鎖方法塊Java執行緒synchronized
- Java多執行緒4:synchronized鎖機制Java執行緒synchronized
- Java多執行緒——執行緒Java執行緒
- Java多執行緒/併發06、執行緒鎖Lock與ReadWriteLockJava執行緒
- Java 多執行緒之內建鎖與顯示鎖Java執行緒
- Java多執行緒-執行緒中止Java執行緒
- Java多執行緒——執行緒池Java執行緒
- iOS多執行緒安全-13種執行緒鎖?iOS執行緒
- 多執行緒(2)-執行緒同步互斥鎖Mutex執行緒Mutex
- Java多執行緒程式設計—鎖優化Java執行緒程式設計優化
- Java多執行緒-鎖的區別與使用Java執行緒
- Java執行緒:執行緒的同步與鎖Java執行緒
- 【Java多執行緒】輕鬆搞定Java多執行緒(二)Java執行緒
- java——多執行緒Java執行緒
- java 多執行緒Java執行緒
- 【Java】多執行緒Java執行緒
- JAVA 多執行緒 ??Java執行緒
- java多執行緒Java執行緒
- Java - 多執行緒Java執行緒
- java 多執行緒守護執行緒Java執行緒
- Java多執行緒-執行緒通訊Java執行緒
- Java多執行緒-執行緒狀態Java執行緒
- java多執行緒9:執行緒池Java執行緒
- Java多執行緒之執行緒中止Java執行緒
- 【java多執行緒】(二)執行緒停止Java執行緒
- Java多執行緒——守護執行緒Java執行緒
- Java多執行緒16:執行緒組Java執行緒