今天回味 volatile 時看到了別人的一個 Demo:
class VolatileDemo() {
var flag: Boolean = false
fun read() {
while (!flag) {
Log.d("AA", "讀取任務(wù)ing...")
// Thread.sleep(100)
}
Log.d("AA", "讀取任務(wù)結(jié)速")
}
fun write() {
flag = true
Log.d("AA", "寫入任務(wù)完成")
}
}
var volatileDemo = VolatileDemo()
var thread1 = Thread(Runnable { volatileDemo.write() })
var thread2 = Thread(Runnable { volatileDemo.read() })
//我們讓線程2的讀操作先執(zhí)行
thread2.start()
//睡30毫秒,為了保證線程2比線程1先執(zhí)行
Thread.sleep(30)
//再讓線程1的寫操作執(zhí)行
thread1.start()
并發(fā)沒有 volatile 的表現(xiàn)
讀取和寫入操作中的 Flag 沒用 volatile 標(biāo)記,這時大家猜猜線程會怎么運(yùn)行,這個例子當(dāng)初有人 用來解釋 volatile 的內(nèi)存可見性,說 thread2 棧幀中的內(nèi)存副本不會同步更新,即便 thread1 修改了 flag 的值,thread2 也會一直卡在這個循環(huán)里出不來。但是...重點(diǎn)是但是,這是不對的,thread2 還是能結(jié)束的,只是每次 thread2 每次表現(xiàn)都不一樣,誰也不知道 thread2 在刷新 flag 數(shù)據(jù)之前會運(yùn)行多少次
我們多運(yùn)行幾次,看看打印情況
結(jié)果完全超出我們認(rèn)知啊,這運(yùn)行起來完全沒有規(guī)律可言,明明我們沒用 volatile 標(biāo)記 flag ,但是為什么圖1、圖3 這么像 volatile 啊,但是圖2缺不是,這怎么理解,這就要盤盤 JVM 工作內(nèi)存和主內(nèi)存了
JVM 工作內(nèi)存和主內(nèi)存
JVM 把內(nèi)存分割為:主內(nèi)存 | 工作內(nèi)存 2個部分:
- 主內(nèi)存 - 堆內(nèi)存和本地方法區(qū)
- 工作內(nèi)存 - 每個線程都有一個工作內(nèi)存,工作內(nèi)存中主要包括兩個部分,一個是屬于該線程私有的棧和對主存部分變量拷貝的寄存器(包括程序計數(shù)器PC和cup工作的高速緩存區(qū))
每條線程都有自己的工作內(nèi)存,工作內(nèi)存中保存的是主存中某些變量的拷貝,線程對變量的所有操作都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫主內(nèi)存中的變量,線程之間無法直接訪問對方的工作內(nèi)存中的變量,線程間變量的傳遞均需要通過主內(nèi)存來完成
JVM規(guī)范定義了線程對內(nèi)存間交互操作:
- Lock(鎖定) - 作用于主內(nèi)存中的變量,把一個變量標(biāo)識為一條線程獨(dú)占的狀態(tài)。
- Read(讀取) - 作用于主內(nèi)存中的變量,把一個變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中。
- Load(加載) - 作用于工作內(nèi)存中的變量,把read操作從主內(nèi)存中得到的變量的值放入工作內(nèi)存的變量副本中
- Use(使用) - 作用于工作內(nèi)存中的變量,把工作內(nèi)存中一個變量的值傳遞給執(zhí)行引擎。
- Assign(賦值) - 作用于工作內(nèi)存中的變量,把一個從執(zhí)行引擎接收到的值賦值給工作內(nèi)存中的變量。
- Store(存儲) - 作用于工作內(nèi)存中的變量,把工作內(nèi)存中的一個變量的值傳送到主內(nèi)存中。
- Write(寫入) - 作用于主內(nèi)存中的變量,把store操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中。
- Unlock(解鎖) - 作用于主內(nèi)存中的變量,把一個處于鎖定狀態(tài)的變量釋放出來,之后可被其它線程鎖定。
是不是有點(diǎn)看的眼花繚亂啦,仔細(xì)看這些其實(shí)都是順序執(zhí)行的操作,很好理解,知道就可,同樣這些操作有自己的特性:
- read - load,store - write 都是成對進(jìn)行的,不允許單一出現(xiàn)使用
- 不允許線程丟棄它最近的一個 assign 操作,即變量在工作內(nèi)存被更改后必須同步改更改回主內(nèi)存
- 工作內(nèi)存中的變量在沒有執(zhí)行過 assign 操作時,不允許無意義的同步回主內(nèi)存
多線程并發(fā)的核心其實(shí)就是對于資源的可見性和有序性的處理
- 可見性 - 對于可見性來說,什么時候把線程工作內(nèi)存中的變量副本同步到主內(nèi)存中完全是 JVM 自己實(shí)現(xiàn)系統(tǒng)決定的,我找了好多資料也沒有明確說明的,更具上面例子的測試,我發(fā)現(xiàn)有時候?qū)?shù)據(jù)的修改會馬上同步到主內(nèi)存,有時候要等到線程上下文切換時才會更新數(shù)據(jù)。另外再說一點(diǎn),使用 volatile 同樣也會由工作內(nèi)存的問題,區(qū)別是工作內(nèi)存中的修改會馬上立即同步到主內(nèi)存
- 有序性 - 有序性這個大家應(yīng)該都門清,就是嚴(yán)格保證代碼按照我們的邏輯執(zhí)行,上面的例子就是個反面典型,執(zhí)行成啥樣我們完全控制不了
通過上面這個例子,就明明白白帶出了多線程我們關(guān)心什么,多個線程同時對相同資源的使用,只要我們的代碼中類似上面要處理相同的資源,那么我們必須要采用合適的多線程測量,否則執(zhí)行成啥樣誰知道
并發(fā)添加 volatile 的表現(xiàn)
還是上面的代碼,我們給 flag 加上 volatile
@Volatile
var flag: Boolean = false
然后我們看看運(yùn)行情況:
不管點(diǎn)幾次都是讀取先完事,然后再試寫入完事,這樣的確是保證了內(nèi)存可見了,我們在任何地方修改一個 Volatile 的變量,所有改變量的副本都會立馬相應(yīng),可以看到影響的速度是很快的,快的寫入都來不急執(zhí)行下面的任務(wù),讀取那邊就完成同步了
但是從結(jié)果上看光是有 Volatile 還是不行的,邏輯上讀取操作結(jié)束應(yīng)該在寫入完成之后執(zhí)行的,這樣看來 Volatile 并不能解決根本問題,還是得 Synchronized
很多人都說用 Volatile 做多線程同步必須小心再小心,通過這一個小小的例子就很明顯了,Volatile 的缺陷太大,無法保證連續(xù)性和邏輯性,Volatile 最適合的場景就是賦值操作了,典型的就是單例了對吧,這個大家都知道
并發(fā)添加 Synchronized 的表現(xiàn)
那么寫到這就完事了嗎,還沒有,最常用的多線程同步手段 Synchronized 我們還沒用呢,既然上面 volatile 保證不了連續(xù)性邏輯性,那么我們來看看 Synchronized ,我們給寫入和讀取方法都改成 Synchronized 的
class VolatileDemo(var index: Int) {
var flag: Boolean = false
@Synchronized
fun read() {
while (!flag) {
Log.d("AA", "讀取任務(wù)ing... - 第:$index 點(diǎn)擊")
// Thread.sleep(100)
}
Log.d("AA", "讀取任務(wù)結(jié)速 - 第:$index 點(diǎn)擊")
}
@Synchronized
fun write() {
flag = true
Log.d("AA", "寫入任務(wù)完成 - 第:$index 點(diǎn)擊")
}
}
但是結(jié)果呢,thread2 真的卡在這里了,thread2 拿到鎖一直運(yùn)行不釋放鎖,thread1 怎么由機(jī)會執(zhí)行呢,就會想下面 log 輸出一樣,一直跑停不下來
并發(fā) volatile + Synchronized 的表現(xiàn)
我們繼續(xù)修改代碼
class VolatileDemo(var index: Int) {
@Volatile
var flag: Boolean = false
@Synchronized
fun read() {
while (!flag) {
Log.d("AA", "讀取任務(wù)ing... - 第:$index 點(diǎn)擊")
// Thread.sleep(100)
}
Log.d("AA", "讀取任務(wù)結(jié)速 - 第:$index 點(diǎn)擊")
}
@Synchronized
fun write() {
flag = true
Log.d("AA", "寫入任務(wù)完成 - 第:$index 點(diǎn)擊")
}
}
是不是有人對此很期待啊,肯定有人聽說過多線程使用 volatile + Synchronized 來做,但是結(jié)果吧和上面單獨(dú)使用 Synchronized 一樣,thread2 一直運(yùn)行,thread1 沒有執(zhí)行的機(jī)會,可見多線程設(shè)計的復(fù)雜性,你這邊的邏輯說不準(zhǔn)就會這樣。不要迷信網(wǎng)上有人說的 volatile + Synchronized 萬能論調(diào),存扯淡
那我們應(yīng)該怎么辦,顯然這種單單依靠 flag 在多線程中異常危險
- 常規(guī)方式 - 我們可以放棄這個 flag 標(biāo)記,完全使用 Synchronized 來實(shí)現(xiàn)同步,但是 Synchronized 由局限性,Synchronized 修飾的是整個方法,只能同步整個方法的執(zhí)行,而不能在方法執(zhí)行的過程中進(jìn)行操作
- 自由加鎖 - 若是我們需要在方法中根據(jù)情況不筒進(jìn)行不同的同步操作,那么就剩下自己加鎖這種選擇了,這樣可以實(shí)現(xiàn)更精細(xì)的操作
并發(fā) ReentrantLock+ Condition的表現(xiàn)
沒啥說的直接看代碼
class VolatileDemo {
@Volatile
var flag: Boolean = false
var reentrantLock = ReentrantLock()
var condition = reentrantLock.newCondition()
fun read() {
try {
reentrantLock.lock()
Log.d("AA", "開始讀取任務(wù)")
if (!flag) {
Log.d("AA", "沒有數(shù)據(jù),進(jìn)入待機(jī)狀態(tài),釋放鎖")
condition.await()
Log.d("AA", "沒有數(shù)據(jù),被喚醒再進(jìn)入")
}
Log.d("AA", "讀取任務(wù)結(jié)速")
condition.signalAll()
} finally {
reentrantLock.unlock()
}
}
fun write() {
try {
reentrantLock.lock()
flag = true
Log.d("AA", "寫入任務(wù)完成")
condition.signalAll()
} finally {
reentrantLock.unlock()
}
}
}
var volatileDemo = VolatileDemo()
var thread1 = Thread(Runnable { volatileDemo.write() })
var thread2 = Thread(Runnable { volatileDemo.read() })
//我們讓線程2的讀操作先執(zhí)行
thread2.start()
//睡1毫秒,為了保證線程2比線程1先執(zhí)行
Thread.sleep(30)
//再讓線程1的寫操作執(zhí)行
thread1.start()
這里我們還是基于 flag 標(biāo)記進(jìn)行邏輯操作,所以 flag 還是要設(shè)計成 Volatile 的,然后我們自己加鎖,自己阻塞,自己喚醒,阻塞的代碼在被喚醒的地方繼續(xù)執(zhí)行,這樣整個邏輯我們恩那個完全按照自己的思路去做
感想
volatile 好久之前就看過了,這次精研多線程時又看了看當(dāng)初的文章,于是又看到了這個小例子,看過之后馬上反應(yīng)過來由問題,左想不對,右也不對,誰說線程有自己的工作內(nèi)存,核心標(biāo)記也不是 volatile 可見的,但是 Thread2 是循環(huán)不挺的執(zhí)行,不可能內(nèi)存一直不刷新的,只是執(zhí)行時間長短的問題,索性我把這個例子好號走走得了
然后連帶著想了很多問題,比如線程工作內(nèi)存何時同步到主內(nèi)存,多線程的幾種手段都是為了達(dá)到什么目的,意義,優(yōu)勢,缺陷?我是挨個試了個遍,還真是實(shí)踐見真章,自己掠過這么一遍之后感覺多線程的手段在腦海里徹底清晰起來,寫文章的意義也是在這里