Java併發編程實戰(chapter_2)(對象發佈、不變性、設計線程安全類)

来源:https://www.cnblogs.com/1024Community/archive/2018/04/01/8685396.html
-Advertisement-
Play Games

又是三星期的生活。感覺自從有了這個分享之後,會無形多了一份動力,逼著自己不能落後,必須要去不停的學習,這其實是我想要的,各位少年團中的成員也都是有共鳴的,在此很感動,省去一萬字。。。。。這一次會總結對象的安全發佈、不變性,這幾點,在我們工程實踐中,同樣也是非常具有參考與思考價值的基礎知識點。看書枯燥 ...


又是三星期的生活。感覺自從有了這個分享之後,會無形多了一份動力,逼著自己不能落後,必須要去不停的學習,這其實是我想要的,各位少年團中的成員也都是有共鳴的,在此很感動,省去一萬字。。。。。這一次會總結對象的安全發佈、不變性,這幾點,在我們工程實踐中,同樣也是非常具有參考與思考價值的基礎知識點。看書枯燥,理解生澀,可是當你看過,理解一點,再平時業務代碼中就會比別人多思考一分,就會比別人在更“惡劣”的網路環境中,更穩定一分。這幾天想起《三傻》中,那句很經典的話:追求卓越,成功將會悄悄的靠近你。

一、發佈與溢出

“發佈(Publish)”一個對象的意思是指,使對象能夠在當前作用於之外的代碼中使用。這個“之外”,尤為關鍵,各種出問題的地方,都是因為這個“之外”所引起的。例如,如果在對象構造完成之前就發佈該對象,就會破壞線程安全性。當某個不應該發佈的對象被髮布時,這種情況就被稱為“溢出”。下麵使用簡單的例子進行說明:

1. 日常非常不註意的行為

class Status {
    private String[] states = new String[]{"AA","BB","CC"};

    public String[] getStates(){
        return states;
    }
}

思考:很多人會不服的來爭吵:這特麽哪裡有問題,跑了這麼久的線上了,一直沒出問題啊!好,那麼問題了來:是不是線上一直沒問題的代碼,就是好代碼?就是正確的代碼?

類似的代碼還有:

class Cache {
    private static HashMap<String,Object> cache = new HashMap<>();

    public static Object getCacheValue(String key){
        return return cache.get(key);
    }

    public static HashMap<String,Object> getCache(String key){
        return cache;
    }

    public static void addCache(String key, Object object){
        cache.push(key, object);
    }
}

P.S.:以上代碼是我去年年底,再項目工程中看到的代碼,而且線上上運行著,千真萬確!

2. 分析問題所在

你問我:這錯在哪?如果我要回答,我會說:沒錯,你都沒錯。個人原因,我不喜歡程式員當面懟,因為我知道,大家都不容易,並且還知道:真的有問題那天,你知道痛了,你會主動改的,根本不用我說啥。當然,更嚴重的是,代碼中(恩,線上代碼),有人將states命名成了s,cache命名成了c,這我也說不了啥,什麼叫做“追求卓越”,可能每人心中都會有自己的詮釋吧。如果是下麵代碼出現在一個神不知鬼不覺的地方,請看:

class Controller {
    public void cache(){//1
        Status status = new Status();
        String[] allStatus = status.getStates();
        Cache.addCache("ALL_STATUS",allStatus);
    }

    public void modify(){//2
        String[] allStatus = (String[])Cache.getCacheValue("ALL_STATUS");
        allStatus[0] = null;// 也許變成了其他值,null是一種比較極端的情況
    }


    public String getFirstUpcaseStatus(){//3
        String[] allStatus = (String[])Cache.getCacheValue("ALL_STATUS");
        return allStatus[0].toUpperCase(); // oh no! NPE!
    }

    public void remove(){//4
        Cache.getCache().remove("ALL_STATUS");
    }
}
  • 1、2、3、4四個方法我們並不知道是什麼時候觸發的
  • 就是說時間順序上,有可能是4號方法首先被觸發,那1、2、3都將有問題
  • 即使4不被觸發,先1、2,後3,也是出問題的
  • 也許我們代碼寫的很複雜,例如在2號程式中調用了非常多的service,用了非常多的設計模式,最終我們將修改數組中的值
  • 也許我們知道問題所在不去修改數組中的狀態值,可是你能保證你能維護這個代碼一輩子嗎?
  • 以後交給兩個人維護,兩個人由於沒啥子追求,別人代碼不看,一個人在一邊修改了數組,而另一個人在另一邊使用了數組中的狀態值,後果不堪設想

3. 更加隱蔽式的危險發佈

下麵這種,新學到的一種危險性行為發佈:

public class ThisEscape{
    public ThisEscape(EventSource source){
        source.registerListener(e->doSomething(e));
    }
}

心得:請大家儘量使用Java8語法,整潔、大方、可擼(這是什麼鬼!)
思考:註意doSomething方法,會有什麼問題呢?

4. 構造器與構造者

  • 作為構造者不要在構造器裡面添加過多的邏輯,出錯之後,這個鍋你背不起!
  • 即使在一個構造器的最後一行,這個對象也是沒有沒初始化完成的!
  • this指針被髮布出去,後果不堪設想,對象沒初始化完成,而使用this指針。
  • 上面代碼,可以在doSomething方法內部使用ThisEscape.this來訪問父類
  • 如果父類沒有初始化完,而訪問父類,那將報錯,這就是問題所在

5.針對這種隱蔽式的情況,我們怎麼做

public class SafeListener {
    private final EventListener listener;

    private SafeListener() {
        listener = e->doSomething(e);
    }

    public static SafeListener newInstance(EventSource source){
        SafeListener safe = new SafeListener();
        source.registerListener(safe.listener);
        return source;
    }
}

6. 再舉例一些不安全發佈的例子


class Holder{
    // 曝露類屬性,大忌~
    public Holder holder;

    public void initialize(){
        holder = new Holder();
    }
}
//由於未被正確發佈,因此這個類可能出現故障
public class Holder{
    private int n;

    public Holder(int n){
        this.n = n;
    }

    public void assertSanity(){
        if(n != n){
            throw new Exception("initial erroe");
        }
    }
}

說明:拋出異常這個類是很玄乎的,因為線程可見性的原因,線程初次讀取n的時候是老的值,可是這之後n值被其他線程更新,這個線程再次讀取的時候,讀取到一個失效的值,這就是拋出異常的原因。可以見得普普通通的自身與自身的比較,在多線程的環境下,都是很有問題的!!

6-線程封閉

  • 常見的封閉模式:棧封閉。就是在局部方法中使用一個變數,而不把他暴露出去。另外我自己的理解,每次方法返回一個新對象,也是一種使用方式。
public int loadThe Ark(Collection<Animal> candidates){
    // 將animals封閉在方法內部
    SortedSet<Animal> animals;
    int numberParies = 0;
    Animal candidate = null;

    //針對animals容器進行各種統計

    return numberParise;

}
class Status {
    public String[] getStates(){
        //每次都返回新的對象數組
        return new String[]{"AA","BB","CC"};
    }
}
  • 另一種封閉模式:ThreadLocal模式。這種模式也比較常用,每次在web項目中保存session的時候,常常使用這種模式,來標記當前訪問線程的登陸情況。不過這個要註意的是,再web中使用TreadLocal容易導致溢出,具體的分析,請期待到springMVC系列。
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>(){
    public Connection initialValue(){
        return DriverManager.getConnection(DB_URL);
    }
};
public static Connection getConnection(){
    return connectionHolder.get();
}

7-給出寫安全發佈的模式

  • 在靜態初始化函數中初始化一個對象引用
  • 將對象的引用保存到volatile類型的域或者AtomicReferance對象中
  • 將對象的引用保存到某個正確構造對象的final類型域中
  • 將對象的引用保存到一個由鎖保護的域中

二、不變性

滿足線程安全的另外一種方式,就是使用不可變對象。如果想要創建不可變對象的話,要滿足以下條件:

  • 對象創建以後其狀態就不能修改
  • 對象的所有域都是final類型
  • 對象是正確創建的(在對象的創建期間,this引用沒有溢出)

1. 基礎的不可能變模型

這種方式,有點像我在《CC》觀後感那篇文章中,講到的一個觀點:儘量對原始工具包中的類進行封裝,有節制的使用其中的功能。下麵代碼就展示了,再可變對象的基礎上構建不可變類


public final class ThreeStooges{
    //註意,這個stooges變數是可變的!
    private final Set<String> stooges = new HashSet<>();

    public ThreeStooges(){
        stooges.add("1");
        stooges.add("2");
        stooges.add("3");
    }

    public boolean isStooges(String name){
        return stooges.contains(name);
    }
}

2. 有點高端的貨:使用不可變對象與volatile保證線程同步

這裡使用了三個內在的基本功點:對象不可變、對象讀寫分離、對象可見性。上代碼:

class Value{
    private final BigInteger lastNumber;
    private final BigInteger[] lastFactors;

    public Value(BigInteger lastNumber, BigInteger[] factors){
        this.lastNumber = lastNumber;
        this.lastFactors = Arrays.copyOf(factors,factors.length);//這裡進行寫複製
    }
    public BigInteger[] getFactors(BigInteger i){
        if(lastNumber = null || !lastNumber.equals(i))
            return null;
        else
            return Arrays.copyOf(lastFactors,lastFactors.length);//這裡進行讀複製
    }
}

說明:由於每次初始化時候都進行類屬性的初始化,並與外界分離,因為factors數組每次都是複製一個副本進行初始化的!並且每次讀的時候,也是講數組對象進行複製分離。這樣,只要一初始化對象之後,實際上,類對象裡面的兩個類屬性都是不可變的了,因為全部與外界隔離了

下麵我們看看怎麼使用:

public class VolatileCacheFactorizer implements Servlet{
    private volatile Value cache = new Value(null,new BigInteger[0]);

    public void service(ServletRequest req, ServletResponse resp){
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = cache.getFactors(i);
        if(factors == null){
            factors = factor(i);
            cache = new Value(i,factors);
        }
        encodeIntoResponse(resp,factors);
    }
}

說明:這裡cache類屬性使用volatile,保證多線程寫入的時候,都能夠同步到主記憶體中,在這種情況下,多線程即是可見的,而通過Value對象的不變性又保證了對cache對象訪問的安全性,那這樣,整個service就是線程安全的了!

三、設計線程安全的類

這一部分,我看書中涉及到很多名詞,需要上網搜搜資料看看解釋,否則讀這一部分會很懵逼。我下麵從一些名詞解釋入手來說說這一章。

1. 什麼叫做監視器模式

乍看之下還以為這是一種設計模式,的確是一種設計模式!不過還想不起來是什麼樣子的。我一google才發現是非常簡單的,其實就是一段互斥訪問的代碼段(管程):

class SynClass{
    private long value = 0;

    public synchronized long getValue(){
        return value;
    }

    public synchronized long increment(){
        if(value == Long.MAX_VALUE)
            throw new IllegalStateException("counter overflow");
        return ++value;
    }
}

說明:加了synchronized關鍵字的代碼段,就相當一個屋子,每次只允許一個線程訪問,如果訪問有需求了,還可能進行掛起工作,那監視器是誰能?監視器就是對象本身,synchronized是加鎖操作,這個鎖也是這個對象持有的一個內部鎖,如果要掛起代碼,可是使用對象本身就天然繼承自Object的wait方法,這就是監視器的作用。我看網上解釋說:監視器(其實就是每個對象自己,因為每個對象都繼承了Object)就像一個屋子的管理者,然後把對象這個“屋子”分成了三個地方:互斥訪問區域、準備訪問的區域和等待區域。

2. 什麼叫做先驗條件和後驗條件

  • 先驗條件(precondition):針對方法(method),它規定了在調用該方法之前必須為真的條件。
  • 後驗條件(postcondition):也是針對方法,它規定了方法順利執行完畢之後必須為真的條件。

3. 設計線程安全的類的三要素

  • 找出構成對象狀態的所有變數
  • 找出約束狀態變數的不變性條件
  • 建立對象狀態的併發訪問管理策略

4. 什麼叫做不變性條件

這個也是要做一定解釋:程式在一系列的操作之後,還能夠滿足自己的先驗條件和後驗條件的,就叫做不變性條件(這個理解有點困難,大致我自己的想法是這樣)

5. 收集同步的需求

class SafeClass{
    private long value = 0;

    public synchronized long increment(){
        if(value == Long.MAX_VALUE)
            throw new IllegalStateException("counter overflow");
        return ++value;
    }
}
  • 我們要做的,確定本類中的那些狀態,會再多線程的操作下影響對象的不變性
  • 如果一個狀態轉變是依賴於前一個狀態的話,那就會複合操作,需要同步機制
  • 當然,有些狀態轉變不依賴之前,例如溫度
  • 上例中increment加上了synchronized就是一種保護程式不變性與後驗條件的機制

6. 註意狀態的所有權

舉個簡單的例子

class Owner{
    private SunChild sub;
}

其實這個sub對象就是Owner所擁有的一個子對象,所有權歸Owner。但是如果加上如下代碼

class Owner{
    private SunChild sub;

    SubChild getSub(){
        return sub;
    }

}

這種情況下,所有權就被髮布了出去,這樣的情況就要考慮同步機制進行保護。

註意:交出所有權的時候一定要多加思考程式的運行情景,以防不備!

7. 實例封閉

如果某個對象不是線程安全的,我們可以將其進行封裝,或者通過單一鎖進行保護。下麵是使用實例封閉模式進行的一種樣例:

public class PersonSet{
    private final Set<Person> mySet = new HashSet<>();//mySet本身並非線程安全

    public synchronized void addPerson(Person p){
        mySet.add(p)
    }
    public synchronized boolean contains(Person p){
        return mySet.contain(p);
    }
}

說明:將數據封裝在對象內部,可以將數據的訪問限制在對象的方法上,從而更容易確保線程再訪問數據時總能持有正確的鎖

8. 線程安全的又一方式:委托

委托,其實就是將對象的涉及到的影響可變性條件的狀態,放到JDK提供的一些線程安全的容器中去,進行統一管理。同樣也是一個簡單的例子:


public class Tracher{
    private final ConcurrentMap<String,Object> localMap;

    public Tracher(){
        localMap = new ConcurrentHashMap<>();
    }

    public Map<String,Object> getLocations(){
        return localMap;
    }
    public Object getLocation(String key){
        return lcoalMap.get(key);
    }
}

上面講統一使用ConcurrentMap進行管理。如果想要獲取一個不變的狀態的話,可以進行讀複製:

public Map<String,Object> getLocations(){
    return new ConcurrentHashMap<>(localMap);
}

9. 委托不是萬能的

過分依賴原子類所造成的“殘局”:

public class NumberRange {
    private final AtomicInteger lower = new AtomicInteger(0);
    private final AtomicInteger upper = new AtomicInteger(0);

    public void setLower(int i){
        if(i>upper.get()){//註意這裡
            throw new Exception("error");
        }
        lower.set(i)
     }

     public AtomicInteger getUpper(){
        return upper;
     }
}

說明:由於upper被暴露了出去,可是setLower方法內部進行了“先檢查後執行”的步驟,依賴於upper值,這樣,lower屬性的值就出現了不可預估性,原子操作沒達成,原子類失效了。可以使用加鎖來修改上述代碼

10. 特別需要註意的由委托引起的非線程安全

這種模式屬於一種叫做“客戶端加鎖”,其實就是寫程式中很不註意的,將內置鎖和屬性對象的鎖混淆所致,下麵是問題代碼:

public class ListHelper<E>{
    public List<E> list = Collections.synchronizedList(new ArrayList<E>());

    ...

    public synchronized boolean putIfAbsent(E e){
    //註意這裡synchronized使用的是內置鎖
        boolean absent = !list.contains(e);
        if(absent){
            //這裡add使用的是list對象裡面的同步鎖
            list.add(e);
        }
        return absent;
    }
}

兩種鎖並不一樣,導致並沒有對“先判斷再執行”進行同步操作,還是會存在不安全性問題。下麵是解決的方式:

public class ListHelper<E>{
    public List<E> list = Collections.synchronizedList(new ArrayList<E>());

    ...

    public boolean putIfAbsent(E e){
        synchronized (list){//統一使用屬性對象的鎖
            boolean absent = !list.contains(e);
            if(absent){
                list.add(e);
            }
            return absent;
        }
    }
}

四、總結

本次主要講了三個方面:

  • 對象的發佈
  • 不變性
  • 設計線程安全的類

相對來說比較枯燥,儘量都是用簡潔明瞭的例子來混合講解了,望給位看官多多包涵~哈哈哈。接下來要分享的東西,就會實用很多,涉及到JDK線程工具的良好實用(如閉鎖、FutureTask等),並且我在接下來的線程分享文章中,會每次安排一個大章節,逐步進行生活必備品之一的java.util.concurrent.ThreadPoolExecutor源碼分析,敬請期待!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 最近公司做的業務都是使用Vue、Element寫的,涉及到的相應的基礎業務像輪播、預載入、懶載入,都是使用 NPM上的工具來實現,原理和基礎還是要有的,就來實現幾個項目中常用到的業務。 經常見到這樣的效果,導航在頁面中間,當界面滾動到導航的時候,導航就變成了 佈局。為了看效果,我加了邊框。 剛開始, ...
  •   這是第一次寫博客玩,也是第一次使用markdown寫東西(一邊看著語法一邊寫的來著),不過進步還是有滴~   申請博客以後,趕緊去翻了一下wjj童鞋的博客,發現他的封面是這樣的:   嗯……不得不說,這個網站的確是有一丟丟醜。不過怎麼能 ...
  • Ajax 即“Asynchronous Javascript And XML”(非同步 JavaScript 和 XML),是指一種創建交互網頁應用的網頁開發技術。 一. Ajax ajax技術的目的是讓javascript發送http請求,與後臺通信,獲取數據和信息。ajax技術的原理是實例化xml ...
  • 常用Grid佈局屬性介紹 下麵從一個簡單Grid佈局例子說起。 CSS Grid 佈局由兩個核心組成部分是 wrapper(父元素)和 items(子元素)。 wrapper 是實際的 grid(網格),items 是 grid(網格) 內的內容。 下麵是一個 wrapper 元素,內部包含6個 i ...
  • 我們大家一聽到設計模式就感覺設計模式是一個高端的東西,到底什麼是設計模式呢?其實設計模式也就是我們的前輩在寫代碼的時候遇到的問題,提出的解決方案,為了方便人與人之間的交流,取了個名字,叫做設計模式。 創建型設計模式 本文今天主要寫一部分創建型設計模式,創建型設計模式呢就是我門創建對象的時候的一種模式 ...
  • 本文篇幅較長,希望能堅持看完,轉載請註明出處,如果覺得好文請給個贊吧 CSS實現梯形 CSS實現三角形和梯形主要是依靠border是梯形的特性來做的,有點像相框的那種感覺。 首先我們先給一個正方形設置比較寬的邊框。如下。 <div id="test1"></div> <style> #test1{ ...
  • 最近有些忙,雙休都在加班,心情比較煩躁,寫篇博客靜靜心。 今天扯結構型設計模式。重要的話多說幾遍,程式員最重要的是編程思想,圈起來,要考試。哈哈。 說說面向對象的三大特性之一繼承。繼承,白話意思是最起碼是那個東西。例如A是基類,B繼承A,意思就是B最起碼是個A.所以當A有顯式構造函數時,子類必須也要 ...
  • |版權聲明:本文為博主原創文章,未經博主允許不得轉載。 最近嘗試在STM32F4下用MBEDTLS實現了公鑰導入(我使用的是ECC加密),整個過程使用起來比較簡單。 首先,STM32F4系列CUBE里已經集成了MBEDTLS, MBEDTLS是ARM公司的開源加密庫,遵守APACHE協議,大家可以隨 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...