Java并发编程实战~Balking模式
上一篇文章中,我們提到可以用“多線程版本的 if”來(lái)理解 Guarded Suspension 模式,不同于單線程中的 if,這個(gè)“多線程版本的 if”是需要等待的,而且還很執(zhí)著,必須要等到條件為真。但很顯然這個(gè)世界,不是所有場(chǎng)景都需要這么執(zhí)著,有時(shí)候我們還需要快速放棄。
需要快速放棄的一個(gè)最常見的例子是各種編輯器提供的自動(dòng)保存功能。自動(dòng)保存功能的實(shí)現(xiàn)邏輯一般都是隔一定時(shí)間自動(dòng)執(zhí)行存盤操作,存盤操作的前提是文件做過(guò)修改,如果文件沒(méi)有執(zhí)行過(guò)修改操作,就需要快速放棄存盤操作。下面的示例代碼將自動(dòng)保存功能代碼化了,很顯然 AutoSaveEditor 這個(gè)類不是線程安全的,因?yàn)閷?duì)共享變量 changed 的讀寫沒(méi)有使用同步,那如何保證 AutoSaveEditor 的線程安全性呢?
class AutoSaveEditor{//文件是否被修改過(guò)boolean changed=false;//定時(shí)任務(wù)線程池ScheduledExecutorService ses = Executors.newSingleThreadScheduledExecutor();//定時(shí)執(zhí)行自動(dòng)保存void startAutoSave(){ses.scheduleWithFixedDelay(()->{autoSave();}, 5, 5, TimeUnit.SECONDS); }//自動(dòng)存盤操作void autoSave(){if (!changed) {return;}changed = false;//執(zhí)行存盤操作//省略且實(shí)現(xiàn)this.execSave();}//編輯操作void edit(){//省略編輯邏輯......changed = true;} }解決這個(gè)問(wèn)題相信你一定手到擒來(lái)了:讀寫共享變量 changed 的方法 autoSave() 和 edit() 都加互斥鎖就可以了。這樣做雖然簡(jiǎn)單,但是性能很差,原因是鎖的范圍太大了。那我們可以將鎖的范圍縮小,只在讀寫共享變量 changed 的地方加鎖,實(shí)現(xiàn)代碼如下所示。
//自動(dòng)存盤操作 void autoSave(){synchronized(this){if (!changed) {return;}changed = false;}//執(zhí)行存盤操作//省略且實(shí)現(xiàn)this.execSave(); } //編輯操作 void edit(){//省略編輯邏輯......synchronized(this){changed = true;} }如果你深入地分析一下這個(gè)示例程序,你會(huì)發(fā)現(xiàn),示例中的共享變量是一個(gè)狀態(tài)變量,業(yè)務(wù)邏輯依賴于這個(gè)狀態(tài)變量的狀態(tài):當(dāng)狀態(tài)滿足某個(gè)條件時(shí),執(zhí)行某個(gè)業(yè)務(wù)邏輯,其本質(zhì)其實(shí)不過(guò)就是一個(gè) if 而已,放到多線程場(chǎng)景里,就是一種“多線程版本的 if”。這種“多線程版本的 if”的應(yīng)用場(chǎng)景還是很多的,所以也有人把它總結(jié)成了一種設(shè)計(jì)模式,叫做 Balking 模式。
Balking 模式的經(jīng)典實(shí)現(xiàn)
Balking 模式本質(zhì)上是一種規(guī)范化地解決“多線程版本的 if”的方案,對(duì)于上面自動(dòng)保存的例子,使用 Balking 模式規(guī)范化之后的寫法如下所示,你會(huì)發(fā)現(xiàn)僅僅是將 edit() 方法中對(duì)共享變量 changed 的賦值操作抽取到了 change() 中,這樣的好處是將并發(fā)處理邏輯和業(yè)務(wù)邏輯分開。
boolean changed=false; //自動(dòng)存盤操作 void autoSave(){synchronized(this){if (!changed) {return;}changed = false;}//執(zhí)行存盤操作//省略且實(shí)現(xiàn)this.execSave(); } //編輯操作 void edit(){//省略編輯邏輯......change(); } //改變狀態(tài) void change(){synchronized(this){changed = true;} }用 volatile 實(shí)現(xiàn) Balking 模式
前面我們用 synchronized 實(shí)現(xiàn)了 Balking 模式,這種實(shí)現(xiàn)方式最為穩(wěn)妥,建議你實(shí)際工作中也使用這個(gè)方案。不過(guò)在某些特定場(chǎng)景下,也可以使用 volatile 來(lái)實(shí)現(xiàn),但使用 volatile 的前提是對(duì)原子性沒(méi)有要求。
在《Copy-on-Write 模式》中,有一個(gè) RPC 框架路由表的案例,在 RPC 框架中,本地路由表是要和注冊(cè)中心進(jìn)行信息同步的,應(yīng)用啟動(dòng)的時(shí)候,會(huì)將應(yīng)用依賴服務(wù)的路由表從注冊(cè)中心同步到本地路由表中,如果應(yīng)用重啟的時(shí)候注冊(cè)中心宕機(jī),那么會(huì)導(dǎo)致該應(yīng)用依賴的服務(wù)均不可用,因?yàn)檎也坏揭蕾嚪?wù)的路由表。為了防止這種極端情況出現(xiàn),RPC 框架可以將本地路由表自動(dòng)保存到本地文件中,如果重啟的時(shí)候注冊(cè)中心宕機(jī),那么就從本地文件中恢復(fù)重啟前的路由表。這其實(shí)也是一種降級(jí)的方案。
自動(dòng)保存路由表和前面介紹的編輯器自動(dòng)保存原理是一樣的,也可以用 Balking 模式實(shí)現(xiàn),不過(guò)我們這里采用 volatile 來(lái)實(shí)現(xiàn),實(shí)現(xiàn)的代碼如下所示。之所以可以采用 volatile 來(lái)實(shí)現(xiàn),是因?yàn)閷?duì)共享變量 changed 和 rt 的寫操作不存在原子性的要求,而且采用 scheduleWithFixedDelay() 這種調(diào)度方式能保證同一時(shí)刻只有一個(gè)線程執(zhí)行 autoSave() 方法。
Balking 模式有一個(gè)非常典型的應(yīng)用場(chǎng)景就是單次初始化,下面的示例代碼是它的實(shí)現(xiàn)。這個(gè)實(shí)現(xiàn)方案中,我們將 init() 聲明為一個(gè)同步方法,這樣同一個(gè)時(shí)刻就只有一個(gè)線程能夠執(zhí)行 init() 方法;init() 方法在第一次執(zhí)行完時(shí)會(huì)將 inited 設(shè)置為 true,這樣后續(xù)執(zhí)行 init() 方法的線程就不會(huì)再執(zhí)行 doInit() 了。
class InitTest{boolean inited = false;synchronized void init(){if(inited){return;}//省略doInit的實(shí)現(xiàn)doInit();inited=true;} }線程安全的單例模式本質(zhì)上其實(shí)也是單次初始化,所以可以用 Balking 模式來(lái)實(shí)現(xiàn)線程安全的單例模式,下面的示例代碼是其實(shí)現(xiàn)。這個(gè)實(shí)現(xiàn)雖然功能上沒(méi)有問(wèn)題,但是性能卻很差,因?yàn)榛コ怄i synchronized 將 getInstance() 方法串行化了,那有沒(méi)有辦法可以優(yōu)化一下它的性能呢?
class Singleton{private static Singleton singleton;//構(gòu)造方法私有化 private Singleton() {}//獲取實(shí)例(單例)public synchronized static Singleton getInstance(){if(singleton == null){singleton=new Singleton();}return singleton;} }辦法當(dāng)然是有的,那就是經(jīng)典的雙重檢查(Double Check)方案,下面的示例代碼是其詳細(xì)實(shí)現(xiàn)。在雙重檢查方案中,一旦 Singleton 對(duì)象被成功創(chuàng)建之后,就不會(huì)執(zhí)行 synchronized(Singleton.class){}相關(guān)的代碼,也就是說(shuō),此時(shí) getInstance() 方法的執(zhí)行路徑是無(wú)鎖的,從而解決了性能問(wèn)題。不過(guò)需要你注意的是,這個(gè)方案中使用了 volatile 來(lái)禁止編譯優(yōu)化,其原因你可以參考《01 | 可見性、原子性和有序性問(wèn)題:并發(fā)編程 Bug 的源頭》中相關(guān)的內(nèi)容。至于獲取鎖后的二次檢查,則是出于對(duì)安全性負(fù)責(zé)。
class Singleton{private static volatile Singleton singleton;//構(gòu)造方法私有化 private Singleton() {}//獲取實(shí)例(單例)public static Singleton getInstance() {//第一次檢查if(singleton==null){synchronize{Singleton.class){//獲取鎖后二次檢查if(singleton==null){singleton=new Singleton();}}}return singleton;} }總結(jié)
Balking 模式和 Guarded Suspension 模式從實(shí)現(xiàn)上看似乎沒(méi)有多大的關(guān)系,Balking 模式只需要用互斥鎖就能解決,而 Guarded Suspension 模式則要用到管程這種高級(jí)的并發(fā)原語(yǔ);但是從應(yīng)用的角度來(lái)看,它們解決的都是“線程安全的 if”語(yǔ)義,不同之處在于,Guarded Suspension 模式會(huì)等待 if 條件為真,而 Balking 模式不會(huì)等待。
Balking 模式的經(jīng)典實(shí)現(xiàn)是使用互斥鎖,你可以使用 Java 語(yǔ)言內(nèi)置 synchronized,也可以使用 SDK 提供 Lock;如果你對(duì)互斥鎖的性能不滿意,可以嘗試采用 volatile 方案,不過(guò)使用 volatile 方案需要你更加謹(jǐn)慎。
當(dāng)然你也可以嘗試使用雙重檢查方案來(lái)優(yōu)化性能,雙重檢查中的第一次檢查,完全是出于對(duì)性能的考量:避免執(zhí)行加鎖操作,因?yàn)榧渔i操作很耗時(shí)。而加鎖之后的二次檢查,則是出于對(duì)安全性負(fù)責(zé)。雙重檢查方案在優(yōu)化加鎖性能方面經(jīng)常用到,例如《17 | ReadWriteLock:如何快速實(shí)現(xiàn)一個(gè)完備的緩存?》中實(shí)現(xiàn)緩存按需加載功能時(shí),也用到了雙重檢查方案。
總結(jié)
以上是生活随笔為你收集整理的Java并发编程实战~Balking模式的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 性能测量工具类——TimeMeasure
- 下一篇: Java消息服务~JMSReplyTo示