程序員:併發下如何保證共享變量安全且不用鎖?!

本博客 貓叔的博客,轉載請申明出處java

閱讀本文約 「15分鐘」git

適讀人羣:Java 中級github

學習筆記,休息了兩天(其實期間在作一個模擬項目實戰),偶爾也想到本身究竟應該作些什麼,是真的對本身或社會有意義的呢?數據庫

image
Photo on Visual hunt

說出你的回答

emmm,答案不止一個,今天先介紹一個簡單易懂的設計模式

讀題:咱們應該如何保證共享變量訪問的線程安全,同時又避免引入鎖產生的開銷呢數組

在併發環境下,一個對象是很容易被多個線程共享的,那麼對於數據的一致性是有要求的安全

雖然可使用顯式鎖或者CAS操做,不過這也會帶來一些上下文切換等額外開銷多線程

先舉個例子說明下目前的問題吧架構

/** * @ClassName Cup * @Description 杯子 非線程安全 * @Author MySelf * @Date 2019/9/25 21:28 * @Version 1.0 **/
public class Cup {

    //直徑
    private double diameter;

    //高度
    private double height;

    public double getDiameter() {
        return diameter;
    }

    public double getHeight() {
        return height;
    }

    //非原子操做
    public void setCup(double diameter,double height){
        this.diameter = diameter;
        this.height = height;
    }
}
複製代碼

上面這段代碼,你們應該都能看出是非線程安全的對吧(若是你看不出來,翻上一篇文章複習下)併發

由於在咱們對setCup操做賦值其直徑的時候,可能另外一個線程已經開始讀取他的高度了,那麼這就會出現線程安全問題。

那麼在不使用鎖的狀況,能夠怎麼作呢?

好好往下看唄,和刷朋友圈的時間差很少,一會兒就懂了

不可變對象

是的,今天說的方式就是講Cup變成不可變對象!

不可變對象:對象一經建立,其對外可見的狀態就保持不變(相似String、Integer)

那麼上面的Cup須要怎麼修改呢?

/** * @ClassName Cup * @Description 不可變對象,線程安全 * @Author MySelf * @Date 2019/9/25 21:32 * @Version 1.0 **/
public final class Cup {

    private final double diameter;

    private final double height;

    public Cup(double diameter,double height){
        this.diameter = diameter;
        this.height = height;
    }

}
複製代碼

這下不就好啦,永遠不會改變了

等下,那我要怎麼修改Cup呀?就算是併發操做,個人業務也可能會須要修改這個Cup呀

讓咱們調整一下視野,修改Cup屬性 == 替換Cup實例

假設咱們是一家茶杯鑄模工廠,有5條流水線在生成最近的網紅茶杯,不過由於互聯網趨勢的印象,偶爾須要小改動我們的這個茶杯參數,停機生產會虧本的,因此在模具適配器的代碼上我們能夠在使用不可變對象的狀況下更換茶杯屬性

/** * @ClassName MoldAdapter * @Description 模具適配器 * @Author MySelf * @Date 2019/9/25 21:35 * @Version 1.0 **/
public class MoldAdapter {
    
    private Map<String,Cup> cupMap = new ConcurrentHashMap<String, Cup>();

    public void updateCup(String version,Cup newCup){
        cupMap.put(version, newCup);
    }

}
複製代碼

這裏ConcurrentHashMap內部涉及的鎖,和Demo中的茶杯新建、替換並沒有關係,其過程不涉及鎖

可能還有點模糊,說說娃娃機案例?

還記得當年毀我青春的娃娃機嗎?

記得好久之前還在泡老婆的時候,帶她去玩娃娃機,誇下海口說必定能抓到她要的那一隻,結果·····

image

它叫 50

如今輪到咱們翻身作主人了,哼

假設咱們是一個片區娃娃機的頭兒,每一個娃娃機都有他們對應的機器編號、支付二維碼url、機械手頻率(對,非職業機械工做者,這裏給的是假設,這個纔是賺錢的重點),假設咱們是一個嗜錢如命的短褲青年,每晚都清算了一次收益清單。

最近恰逢國慶期間,遊客人數即將上漲····

插入,特此【Java貓說】公衆號提早預祝祖國70週年繁榮昌盛、國泰民安!

想賺錢的想法,搜搜搜的一直往胸口跳

那麼好的手段就是娃娃機上的機械手頻率了

我須要針對性的去修改部分娃娃機的屬性,不過還好我一開始是有一張編碼與娃娃機的關係映射表的

我將不可變對象的想法引入到本身的賺錢生意中去

首先是娃娃機對象,先變爲不可變對象

/** * @ClassName DollMachineInfo * @Description 娃娃機不可變對象 * @Author MySelf * @Date 2019/9/25 21:51 * @Version 1.0 **/
public final class DollMachineInfo {

    //編號
    private final String number;

    //支付二維碼url
    private final String url;

    //機械手頻率
    private final int frequency;

    public DollMachineInfo(String number,String url,int frequency){
        this.number = number;
        this.url = url;
        this.frequency = frequency;
    }

    public DollMachineInfo(DollMachineInfo dollMachineInfoType){
        this.number = dollMachineInfoType.number;
        this.url = dollMachineInfoType.url;
        this.frequency = dollMachineInfoType.frequency;
    }

    public String getNumber() {
        return number;
    }

    public String getUrl() {
        return url;
    }

    public int getFrequency() {
        return frequency;
    }
}
複製代碼

此次我須要修改的是編碼與娃娃機的關係映射表,因此這個表也須要是不可變的,他須要支持我獲取關係映射表,並且須要替換最新的關係映射內容

/** * @ClassName MachineRouter * @Description 機器信息表 * @Author MySelf * @Date 2019/9/25 21:57 * @Version 1.0 **/
public final class MachineRouter {
    //保證其在併發環境的內存可見性
    private static volatile MachineRouter instance = new MachineRouter();
    //code與機器之間的映射關係
    private final Map<String,DollMachineInfo> routeMap;

    // 二、存儲不可變量routeMap
    public MachineRouter(){
        //將數據庫表中的數據加載到內存,存爲Map
        this.routeMap = MachineRouter.setRouteFromeDB();
    }

    // 三、從db將數據存入Map
    private static Map<String, DollMachineInfo> setRouteFromeDB(){
        Map<String, DollMachineInfo> map = new HashMap<String, DollMachineInfo>();
        //DB 代碼
        return map;
    }

    // 一、初始化實例
    public static MachineRouter getInstance(){
        return instance;
    }

    /** * 根據code獲取對應的機器信息 * @param code 對應編碼 * @return 機器信息 */
    public DollMachineInfo getMacheine(String code){
        return routeMap.get(code);
    }

    /** * 修改當前MachineRouter實例 * @param newInstance 新的實例 */
    public static void setInstance(MachineRouter newInstance){
        instance = newInstance;
    }


    private static Map<String, DollMachineInfo> deepCopy(Map<String,DollMachineInfo> d){
        Map<String, DollMachineInfo> result = new HashMap<String, DollMachineInfo>();
        for (String key : d.keySet()){
            result.put(key, new DollMachineInfo(d.get(key)));
        }
        return result;
    }


    public Map<String, DollMachineInfo> getRouteMap() {
        //防護性複製
        return Collections.unmodifiableMap(deepCopy(routeMap));
    }
}
複製代碼

接下來就是在併發業務中去添加更新代碼了

/** * @ClassName Worker * @Description 通信對接類 * @Author MySelf * @Date 2019/9/25 22:13 * @Version 1.0 **/
public class Worker extends Thread {

    @Override
    public void run(){
        boolean isRouterModification = false;
        String updateMachineInfo = null;
        while (true){
            //其他業務代碼
            /** * 在通信的Socket信息中解析,並更新數據表信息,再重置MachineRouter實例 */
            if (isRouterModification){
                if ("DollMachineInfo".equals(updateMachineInfo)){
                    MachineRouter.setInstance(new MachineRouter());
                }
            }
            //其他業務代碼
        }
    }

}
複製代碼

敲黑板,記筆記

案例說完,有個基本概念,那麼說點專業術語

這是不可變對象模式,Immutable Object

嚴格上說,不可變對象須要知足什麼條件:

  • 一、類自己有fianl:防止被子類修改定義的行爲
  • 二、全部字段用fianl修飾:能夠在多線程下有JMM保證被修飾字段所引用對象的初始化安全
  • 三、對象建立時,this關鍵字沒有給到其餘類
  • 四、若引用了其餘狀態可變的對象(數組、集合),必須用private,不能對外暴露,須要返回字段,則進行防護性複製(Defensive Copy)

Immutable Object 模式有兩個重要的東西,大家應該差很少知道的

ImmutableObject:負責存儲一組不可變狀態

  • getState* :返回ImmutableObject所維護的相關變量值,實例化時經過構造器的參數得到值
  • getStateSnapshot:返回ImmutableObject維護的一組狀態快照

Manipulator:維護ImmutableObject的變動,當須要變動時,則參與生成新的ImmutableObject實例

  • changeStateTo:根據新的狀態值生成新的ImmutableObject實例

典型交互場景

  • 一、獲取當前ImmutableObject的各個狀態值
  • 二、調用Manipulator的changeStateTo方法來更新應用狀態
  • 三、changeStateTo建立新的ImmutableObject實例以反映新的狀態,並返回
  • 四、獲取新的ImmutableObject的狀態快照

什麼場景適合使用

是的,他確實能夠知足咱們的題目要求,不過任何一種設計模式都有其適合的場景

通常比較適合:

  • 對象變化不頻繁(娃娃機案例)
  • 同時對數據組進行寫操做,保證原子性(茶杯案例)
  • 使用某個對象做爲HashMap的key(注意對象的HashCode)

注意的幾點:

  • 對象變動頻繁:會產生CPU消耗、GC也會有負擔
  • 防護性複製:避免外部代碼修改其內部狀態

專業案例

集合遍歷在多線程環境常常被引入鎖,以防止遍歷過程當中該集合內部結構被改變

java.util.concurrent.CopyOnWriteArrayList就使用了ImmutableObject模式

固然也是須要場景的,在遍歷比修改操做更加頻繁的場景

其內部維護一個array變量用於存儲集合,在你添加一個元素時,它會生成一個新的數組,將集合元素複製到新數組,並在最後一個元素設置爲添加的元素,且新數組複製給array, 即array引用的數組能夠等效一個ImmutableObject,注意是等效

因此,在遍歷CopyOnWriteArrayList時,直接根據array實例生成一個Iterator實例,無須加鎖

結語

由於最後的CopyOnWriteArrayList我沒有認真的看源碼,因此就不細緻展開講,主要是你們能夠理解不可變對象模式,最好能夠寫一個Demo出來,但願你們能夠在生產環境中使用到這一理念,文筆拙劣,見諒。

我是MySelf,還在堅持學習技術與產品經理相關的知識,但願本文能給你帶來新的知識點。

公衆號:Java貓說

學習交流羣:728698035

現架構設計(碼農)兼創業技術顧問,不羈平庸,熱愛開源,雜談程序人生與不按期乾貨。

Image Text
相關文章
相關標籤/搜索