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