并發(fā)編程是Java編程的核心領域,而Java并發(fā)包則凝聚了并發(fā)編程的精華,掌握并發(fā)編程基礎,熟練應用,理解思想則顯得尤為重要。
Java并發(fā)編程基礎之并發(fā)包源碼剖析書籍目錄暫定如下,熱烈歡迎大家補充吐槽。
本書不同于其他類似并發(fā)書籍晦澀難懂,本書特色之一是通俗易懂,對Java有一定基礎的開發(fā)人員都可以看懂,本文專門第二章來講解并發(fā)編程基礎,總結了并發(fā)編程中常用基礎知識以及常用概念,并通過圖形結合降低理解的難度,通過圖形結合和少量的代碼讓讀者輕松的掌握了并發(fā)編程的基礎知識,讓讀者逐步建立起自信心,然后在給讀者介紹JUC里面最簡單的原子類,讓讀者能夠使用起來基礎篇里面介紹的最簡單的CAS操作。編寫本書一開始打算把并發(fā)list放到鎖后面,因為并發(fā)list里面使用了鎖,但是鎖這章理解難度比List大太多,還是堅持讓讀者從易入難的主題,為了不打消讀者積極性,先講解list章節(jié),把鎖章節(jié)放到了后面。
另外并發(fā)包采用目前最近新JDK8源碼進行講解,其中不乏有JDK8新增的并發(fā)類,所以不乏前沿性。
JDK8新增并發(fā)類簡單介紹:
4.8 JDK8新增的StampedLock鎖探究
StampedLock是并發(fā)包里面jdk8版本新增的一個鎖,該鎖提供了三種模式的讀寫控制,三種模式分別如下:
- 寫鎖writeLock,是個排它鎖或者叫獨占鎖,同時只有一個線程可以獲取該鎖,當一個線程獲取該鎖后,其它請求的線程必須等待,當目前沒有線程持有讀鎖或者寫鎖的時候才可以獲取到該鎖,請求該鎖成功后會返回一個stamp票據變量用來表示該鎖的版本,當釋放該鎖時候需要unlockWrite并傳遞參數stamp。
- 悲觀讀鎖readLock,是個共享鎖,在沒有線程獲取獨占寫鎖的情況下,同時多個線程可以獲取該鎖,如果已經有線程持有寫鎖,其他線程請求獲取該讀鎖會被阻塞。這里講的悲觀其實是參考數據庫中的樂觀悲觀鎖的,這里說的悲觀是說在具體操作數據前悲觀的認為其他線程可能要對自己操作的數據進行修改,所以需要先對數據加鎖,這是在讀少寫多的情況下的一種考慮,請求該鎖成功后會返回一個stamp票據變量用來表示該鎖的版本,當釋放該鎖時候需要unlockRead并傳遞參數stamp。
- 樂觀讀鎖tryOptimisticRead,是相對于悲觀鎖來說的,在操作數據前并沒有通過CAS設置鎖的狀態(tài),如果當前沒有線程持有寫鎖,則簡單的返回一個非0的stamp版本信息,獲取該stamp后在具體操作數據前還需要調用validate驗證下該stamp是否已經不可用,也就是看當調用tryOptimisticRead返回stamp后到到當前時間間是否有其他線程持有了寫鎖,如果是那么validate會返回0,否者就可以使用該stamp版本的鎖對數據進行操作。由于tryOptimisticRead并沒有使用CAS設置鎖狀態(tài)所以不需要顯示的釋放該鎖。該鎖的一個特點是適用于讀多寫少的場景,因為獲取讀鎖只是使用與或操作進行檢驗,不涉及CAS操作,所以效率會高很多,但是同時由于沒有使用真正的鎖,在保證數據一致性上需要拷貝一份要操作的變量到方法棧,并且在操作數據時候可能其他寫線程已經修改了數據,而我們操作的是方法棧里面的數據,也就是一個快照,所以最多返回的不是最新的數據,但是一致性還是得到保障的。
下面通過JDK8注釋里面的一個例子講解來加深對上面講解的理解。
class Point {
// 成員變量
private double x, y;
// 鎖實例
private final StampedLock sl = new StampedLock();
// 排它鎖-寫鎖(writeLock)
void move(double deltaX, double deltaY) {
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
// 樂觀讀鎖(tryOptimisticRead)
double distanceFromOrigin() {
// 嘗試獲取樂觀讀鎖(1)
long stamp = sl.tryOptimisticRead();
// 將全部變量拷貝到方法體棧內(2)
double currentX = x, currentY = y;
// 檢查在(1)獲取到讀鎖票據后,鎖有沒被其他寫線程排它性搶占(3)
if (!sl.validate(stamp)) {
// 如果被搶占則獲取一個共享讀鎖(悲觀獲取)(4)
stamp = sl.readLock();
try {
// 將全部變量拷貝到方法體棧內(5)
currentX = x;
currentY = y;
} finally {
// 釋放共享讀鎖(6)
sl.unlockRead(stamp);
}
}
// 返回計算結果(7)
return Math.sqrt(currentX * currentX + currentY * currentY);
}
// 使用悲觀鎖獲取讀鎖,并嘗試轉換為寫鎖
void moveIfAtOrigin(double newX, double newY) {
// 這里可以使用樂觀讀鎖替換(1)
long stamp = sl.readLock();
try {
// 如果當前點在原點則移動(2)
while (x == 0.0 && y == 0.0) {
// 嘗試將獲取的讀鎖升級為寫鎖(3)
long ws = sl.tryConvertToWriteLock(stamp);
// 升級成功,則更新票據,并設置坐標值,然后退出循環(huán)(4)
if (ws != 0L) {
stamp = ws;
x = newX;
y = newY;
break;
} else {
// 讀鎖升級寫鎖失敗則釋放讀鎖,顯示獲取獨占寫鎖,然后循環(huán)重試(5)
sl.unlockRead(stamp);
stamp = sl.writeLock();
}
}
} finally {
// 釋放鎖(6)
sl.unlock(stamp);
}
}
}
如上代碼Point類里面有兩個成員變量,和三個操作成員變量的方法,另外實例化了一個StampedLock對象用來保證操作的原子性。
首先分析下move方法,該函數作用是在添加增量,改變當前point坐標的位置,代碼先獲取到了寫鎖,然后對point坐標進行修改,然后釋放鎖。該鎖是排它鎖,這保證了其他線程調用move函數時候會被阻塞,直到當前線程顯示釋放了該鎖,也就是保證了對變量x,y操作的原子性。
然后看下distanceFromOrigin方法,該方法作用是計算當前位置到原點的距離,代碼(1)首先嘗試獲取樂觀讀鎖,如果當前沒有其它線程獲取到了寫鎖,那么(1)會返回一個非0的stamp用來表示版本信息,代碼(2)拷貝變量到本地方法棧里面,代碼(3)檢查在(1)獲取到的票據是否還有效,之所以還要在此校驗是因為代碼(1)獲取讀鎖時候并沒有通過CAS操作修改鎖的狀態(tài)而是簡單的通過與或操作返回了一個版本信息,這里校驗是看在在獲取版本信息到現在的時間段里面是否有其他線程持有了寫鎖,如果有則之前獲取的版本信息就無效了。這里如果校驗成功則執(zhí)行(7)使用本地方法棧里面的值進行計算然后返回。需要注意的是在代碼(3)校驗成功后,代碼(7)計算中其他線程可能獲取到了寫鎖并且修改了x,y的值,而當前線程執(zhí)行代碼(7)進行計算時候采用的才是對修改前值的拷貝,也就是操作的值是對之前值的一個拷貝,并不是新的值。另外還有個問題,代碼(2)和(3)能否互換,答案是不能,假設位置換了,那么首先執(zhí)行validate,假如驗證通過了,要拷貝x,y值到本地方法棧,而在拷貝的過程中很有可能其他線程已經修改了x,y中的一個,這就造成了數據的不一致性了。那么你可能會問,那不交換(2)和(3)時候在拷貝x,y值到本地方法棧里面時候也會存在其他線程修改了x,y中的一個值那,這個確實會存在,但是,別忘了拷貝后還有一道validate,如果這時候有線程修改了x,y中的值,那么肯定是有線程在調用validate前sl.tryOptimisticRead后獲取了寫鎖,那么validate時候就會失敗。現在應該明白了吧,這也是樂觀讀設計的精妙之處也是使用時候容易出問題的地方。下面繼續(xù)分析validate失敗后會執(zhí)行代碼(4)獲取悲觀讀鎖,如果這時候騎行線程持有寫鎖則代碼(4)會導致的當前線程阻塞直到其它線程釋放了寫鎖。獲取到讀鎖后,代碼(5)拷貝變量到本地方法棧,然后就是代碼(6)釋放了鎖,拷貝的時候由于加了讀鎖在拷貝期間其它線程獲取寫鎖時候會被阻塞,這保證了數據的一致性。最后代碼(7)使用方法棧里面數據計算返回,同理這里在計算時候使用的數據也可能不是最新的,其它寫線程可能已經修改過原來的x,y值了。
最后一個方法moveIfAtOrigin方法作用是如果當前坐標為原點則移動到指定的位置。代碼(1)獲取悲觀讀鎖,保證其它線程不能獲取寫鎖修改x,y值,然后代碼(2)判斷如果當前點在原點則更新坐標,代碼(3)嘗試升級讀鎖為寫鎖,這里升級不一定成功,因為多個線程都可以同時獲取悲觀讀鎖,當多個線程都執(zhí)行到(3)時候只有一個可以升級成功,升級成功則返回非0的stamp,否非返回0,這里假設當前線程升級成功,然后執(zhí)行步驟(4)更新stamp值和坐標值然后退出循環(huán),如果升級失敗則執(zhí)行步驟(5)首先釋放讀鎖然后申請寫鎖,獲取到寫鎖后在循環(huán)重新設置坐標值。最后步驟(6)釋放鎖。
使用樂觀讀鎖還是很容易犯錯誤的,必須要小心,必須要保證如下的使用順序:
long stamp = lock.tryOptimisticRead(); //非阻塞獲取版本信息
copyVaraibale2ThreadMemory();//拷貝變量到線程本地堆棧
if(!lock.validate(stamp)){ // 校驗
long stamp = lock.readLock();//獲取讀鎖
try {
copyVaraibale2ThreadMemory();//拷貝變量到線程本地堆棧
} finally {
lock.unlock(stamp);//釋放悲觀鎖
}
}
useThreadMemoryVarables();//使用線程本地堆棧里面的數據進行操作
總結: 相比ReentrantLock讀寫鎖,StampedLock通過提供樂觀讀鎖在多線程多寫線程少的情況下提供更好的性能,因為樂觀讀鎖不需要進行CAS設置鎖的狀態(tài)而只是簡單的測試狀態(tài)。更具體測試數據期待Java并發(fā)編程基礎之并發(fā)包源碼剖析一書的出版。