Java學習95:同步方法

我們知道Java程序依靠synchronized對線程進行同步,使用synchronized的時候,鎖住的是哪個對象非常重要。

讓線程自己選擇鎖對象往往會使得代碼邏輯混亂,也不利於封裝。更好的方法是把synchronized邏輯封裝起來。例如,我們編寫一個計數器如下:

public class Counter {
    private int count = 0;

    public void add(int n) {
        synchronized(this) {
            count += n;
        }
    }

    public void dec(int n) {
        synchronized(this) {
            count += n;
        }
    }

    public int get() {
        return count;
    }
} 

這樣一來,線程調用add()、dec()方法時,它不必關心同步邏輯,因爲synchronized代碼塊在add()、dec()方法內部。並且,我們注意到,synchronized鎖住的對象是this,即當前實例,這又使得創建多個Counter實例的時候,它們之間互不影響,可以併發執行:

var c1 = Counter();
var c2 = Counter();

// 對c1進行操作的線程:
new Thread(() -> {
    c1.add();
}).start();
new Thread(() -> {
    c1.dec();
}).start();

// 對c2進行操作的線程:
new Thread(() -> {
    c2.add();
}).start();
new Thread(() -> {
    c2.dec();
}).start();

現在,對於Counter類,多線程可以正確調用。

如果一個類被設計爲允許多線程正確訪問,我們就說這個類就是“線程安全”的(thread-safe),上面的Counter類就是線程安全的。Java標準庫的java.lang.StringBuffer也是線程安全的。

還有一些不變類,例如String,Integer,LocalDate,它們的所有成員變量都是final,多線程同時訪問時只能讀不能寫,這些不變類也是線程安全的。

最後,類似Math這些只提供靜態方法,沒有成員變量的類,也是線程安全的。

除了上述幾種少數情況,大部分類,例如ArrayList,都是非線程安全的類,我們不能在多線程中修改它們。但是,如果所有線程都只讀取,不寫入,那麼ArrayList是可以安全地在線程間共享的。

沒有特殊說明時,一個類默認是非線程安全的。
我們再觀察Counter的代碼:

public class Counter {
    public void add(int n) {
        synchronized(this) {
            count += n;
        }
    }
    ...
}

當我們鎖住的是this實例時,實際上可以用synchronized修飾這個方法。下面兩種寫法是等價的:

public void add(int n) {
    synchronized(this) { // 鎖住this
        count += n;
    } // 解鎖
}
public synchronized void add(int n) { // 鎖住this
    count += n;
} // 解鎖

因此,用synchronized修飾的方法就是同步方法,它表示整個方法都必須用this實例加鎖。

我們再思考一下,如果對一個靜態方法添加synchronized修飾符,它鎖住的是哪個對象?

public synchronized static void test(int n) {
    ...
}

對於static方法,是沒有this實例的,因爲static方法是針對類而不是實例。但是我們注意到任何一個類都有一個由JVM自動創建的Class實例,因此,對static方法添加synchronized,鎖住的是該類的class實例。上述synchronized static方法實際上相當於:

public class Counter {
    public static void test(int n) {
        synchronized(Counter.class) {
            ...
        }
    }
}

我們再考察Counter的get()方法:

public class Counter {
    private int count;

    public int get() {
        return count;
    }
    ...
}

它沒有同步,因爲讀一個int變量不需要同步。

然而,如果我們把代碼稍微改一下,返回一個包含兩個int的對象:

public class Counter {
    private int first;
    private int last;

    public Pair get() {
        Pair p = new Pair();
        p.first = first;
        p.last = last;
        return p;
    }
    ...
}

就必須要同步了。

小結
用synchronized修飾方法可以把整個方法變爲同步代碼塊,synchronized方法加鎖對象是this;

通過合理的設計和數據封裝可以讓一個類變爲“線程安全”;

一個類沒有特殊說明,默認不是thread-safe;

多線程能否安全訪問某個非線程安全的實例,需要具體問題具體分析。

謝謝觀看

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