我們知道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;
多線程能否安全訪問某個非線程安全的實例,需要具體問題具體分析。
謝謝觀看