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

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

閱讀本文約 “15分鐘”

適讀人羣:Java 中級

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

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;

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

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

    // 1、初始化實例
    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

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

  • 1、類本身有fianl:防止被子類修改定義的行爲
  • 2、所有字段用fianl修飾:可以在多線程下有JMM保證被修飾字段所引用對象的初始化安全
  • 3、對象創建時,this關鍵字沒有給到其他類
  • 4、若引用了其他狀態可變的對象(數組、集合),必須用private,不能對外暴露,需要返回字段,則進行防禦性複製(Defensive Copy)

Immutable Object 模式有兩個重要的東西,你們應該差不多知道的

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

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

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

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

典型交互場景

  • 1、獲取當前ImmutableObject的各個狀態值
  • 2、調用Manipulator的changeStateTo方法來更新應用狀態
  • 3、changeStateTo創建新的ImmutableObject實例以反映新的狀態,並返回
  • 4、獲取新的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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章