除了Synchronized關鍵字還有什么可以保證線程安全?


除了Synchronized關鍵字還有什么可以保證線程安全?

    日常使用Java開發時,多線程開發,一般就用Synchronized保證線程安全,防止並發出現的錯誤和異常,那么
除了Synchronized關鍵字還有什么可以保證線程安全嗎?

什么是線程安全?

    在了解什么方法可以保證線程安全之前,我們先定義什么是線程安全。Wikipedia是如此定義的:

線程安全是程式設計中的術語,指某個函數、函數庫在多線程環境中被調用時,能夠正確地處理多個線程之間的
共享變量,使程序功能正確完成。

按我的理解,在多線程環境下程序功能正確完成,說白了就是無論單線程或者多線程環境下,這個程序運行要完全
實現了程序設計時功能的需求,沒有BUG。所以為了沒有BUG,我們必須保證單或者多線程環境下代碼運行的線程
安全。

不考慮線程安全問題行不行?

    有人會說,不要多線程行不行,我只寫單線程代碼不就可以了,單線程總不會有線程安全問題了吧。假如只有單
線程,那么訪問一個網頁就會出現類似排隊買票的場景,一個請求返回了才能繼續處理下一個請求,效率非常之低。
利用多線程,可以充分利用多核CPU資源,提高性能,特別是存在計算密集型、IO密集型任務。
    那么開多進程(process),一個進程對應一個線程行不行?這種情況下,基於Java語言的程序運行,每次訪問一
個請求就需要開啟一個新的JVM,再來一次class加載等等,包括JVM的優化、緩存、預熱之類的優化優勢全部沒了。
    那么只開一個線程,一個線程對應多個協程(coroutine)或者微線程(fiber)行不行? 首先Java語言本身沒有提供原
生的協程實現,其次即便實現了了,只用單線程還是無法充分利用多核CPU資源,性能也會達到瓶頸。
    說到底為了性能,使用多線程有益,代價就是對於一個共享並且狀態可變的資源而言,只要存在競爭條件(race
condition),線程安全問題就必須納入開發代碼時的考慮范圍。

什么情況下才不線程安全?

    為了避免線程不安全,我們必須先了解什么情況下才會出現線程不安全的問題。一個變量只在線程內使用,這種
情況下需要考慮線程安全問題嗎?不需要,只有對需要在多個線程之間共享的變量的訪問操作,我們才需要考慮線程
安全問題。所有線程共享的變量都需要考慮嗎?不是,如果一個變量初始化后就不再變化,即變量為不可變狀態,我
們也無需考慮。所以,只有線程之間存在對共享可變的對象訪問操作時,才會出現線程安全問題。

如何保證線程安全?

不共享

    正如前文所說,只有需要操作共享的可變對象時會出現線程安全問題,那么只要不共享對象不就就可以保證線程
安全了嗎。方法在於要確保對象本身或者其引用不會被共享出去,例如局部變量肯定是不會暴露出去,也可以利用
ThreadLocal保存並傳遞對象。

不可變(immutable)

    如果必須共享,那么對象為不可變狀態,也能保證線程安全。什么情況下對象才是不可變的呢?對象構造完后所
有屬性都不會再發生改變就是不可變。以A類為例,

public class A {
    private int fieldA;
    private int[] fieldB;
    
    public A(C c) {
        c.setD(new D());
        // 初始化
    }

    public void setFieldA(int a) {
        fieldA = a;
    }

    public int[] getFieldB() {
        return fieldB;
    }

    private void updateA() {
        //修改內部狀態
    }

    class D {
        public void updateD() {
            updateA();
        }
    }
}

首先需要將屬性變為final,保證構造函數初始化后其他屬性不會發生變動,防止屬性狀態會被更改。

public class A {
    private final int fieldA;
    private final int[] fieldB;
    
    public A(C c) {
        c.setD(new D());
        // 初始化
    }

    public int[] getFieldB() {
        return fieldB;
    }

    private void updateA() {
        //修改內部狀態
    }

    class D {
        public void updateD() {
            updateA();
        }
    }
}

其次防止內部的可變對象引用暴露出去。

public class A {
    // 置為final狀態
    private final int fieldA;
    private final int[] fieldB;
    
    public A(C c) {
        c.setD(new D());
        // 初始化
    }

    public int[] getFieldB() {
        if (fieldB == null) {
            return null;
        }
        // 防止引用暴露,外部可以修改fieldB狀態
        return Arrays.copyOf(fieldB, fieldB.length);
    }

    private void updateA() {
        //修改內部狀態
    }

    class D {
        public void updateD() {
            updateA();
        }
    }
}

最后構造函數時要防止A類的this逃逸(this escape),防止構造函數還沒有完成初始化時,其他線程可以利用逃逸的
this調用內部方法。

public class A {
    // 置為final狀態
    private final int fieldA;
    private final int[] fieldB;
    
    public A(C c) {
        // updateD方法可能在c被其他線程調用,間接調用updateA方法,修改了Class A實例的狀態
        c.setD(new D());
        // 初始化
    }

    public int[] getFieldB() {
        if (fieldB == null) {
            return null;
        }
        // 防止引用暴露,外部可以修改fieldB狀態
        return Arrays.copyOf(fieldB, fieldB.length);
    }

    private void updateA() {
        //修改內部狀態
    }

    class D {
        // 去除updateD方法
    }
}

單線程修改

    對象必須共享且是可變的,如果只會被其中一個線程修改,其他線程只是讀取,那么只需要對該對象其屬性狀態添加volatile
關鍵字修飾,保證內存可見性,其他線程能讀取到對象的最新狀態,這樣也可以保證線程安全。

將對象屬性修改為線程安全類型

public class A {
    private Map<Integer, B> fieldB;

    public A() {
        fieldB = new HashMap<>();
    }
    
    public B getB(Integer key) {
        return fieldB.get(key);
    }

    public B addB(Integer key, B value) {
        B b = fieldB.get(key);
        if (b == null) {
            fieldB.put(key, value);
        }
        return b;
    }
}

將線程安全委托給屬性對象,由屬性對象保證線程安全

public class A {
    private final Map<Integer, B> fieldB;

    public A() {
        fieldB = new ConcurrentHashMap<>();
    }
    
    public B getB(Integer key) {
        return fieldB.get(key);
    }

    public B addB(Integer key, B value) {
        return fieldB.putIfAbsent(key, value);
    }
}

使用synchronized關鍵字

    對於共享的可變對象,要求所有屬性狀態修改讀取都用synchronized修飾

public class A {
    private B fieldB;
    
    public synchronized B getB() {
        return fieldB;
    }

    public synchronized void updateB() {
        // 其他操作
        fieldB.update();
        // 其他操作
    }
}

可以優化使用內部的私有對象作為鎖對象,避免鎖住整個方法

public class A {
    private B fieldB;
    private final Object lock = new Object();
    
    public synchronized B getB() {
        return fieldB;
    }

    public void updateB() {
        // 其他操作
        synchronized (lock) {
            fieldB.update();
        }
        // 其他操作
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM