在看源代碼時遇到多線程需要同步的時候,總是會看見幾種寫法,修飾方法、修飾靜態方法、
synchronized(Xxx.class)
、synchronized(this)
、synchronized(obj)
,之前一直沒深究幾種方式的區別,現在想來真是驚出一身冷汗,居然這個問題都沒有仔細想清楚。
synchronized的語義
每個對象都有一個監視器monitor,被synchronized修飾,語義就是獲取這個對象的monitor,反編譯后可以看到monitorenter和monitorexit。synchronized關鍵字有三種應用方式(其實按標題來講應該是5種,但是其中有兩種都是與另外兩種等價的):
- 修飾實例方法
- 修飾靜態方法
- 修飾代碼塊(指定對象)
實驗
先上代碼做實驗驗證一下
public class SyncThread {
private final Object lock = new Object();
public void foo() throws Exception {
synchronized (SyncThread.class) {
for (int i = 0; i < 5; i++) {
System.out.println(">>>foo: " + i);
Thread.sleep(1000);
}
}
}
public void bar() throws Exception {
synchronized (this) {
for (int i = 0; i < 5; i++) {
System.out.println("<<<bar: " + i);
Thread.sleep(1000);
}
}
}
public void cpp() throws Exception {
synchronized (lock) {
for (int i = 0; i < 5; i++) {
System.out.println("===cpp: " + i);
Thread.sleep(1000);
}
}
}
public void der() throws Exception {
for (int i = 0; i < 5; i++) {
System.out.println("!!!der: " + i);
Thread.sleep(1000);
}
}
}
可以看到有四種不同的synchronized修飾,以及一個沒有同步的方法,再運行一下看看結果
public class ThreadApp {
public static void main(String[] args) {
final SyncThread syncThread = new SyncThread();
new Thread(() -> {
try {
syncThread.foo();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
syncThread.bar();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
syncThread.cpp();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
syncThread.der();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
可以看到結果
>>>foo: 0
<<<bar: 0
===cpp: 0
!!!der: 0
>>>foo: 1
===cpp: 1
<<<bar: 1
!!!der: 1
>>>foo: 2
!!!der: 2
<<<bar: 2
===cpp: 2
>>>foo: 3
<<<bar: 3
===cpp: 3
!!!der: 3
>>>foo: 4
!!!der: 4
<<<bar: 4
===cpp: 4
分析
從以上結果來看各線程並沒有發生競爭,互不影響,其實明白了synchronized語義也很好理解以上結果,幾個synchronized獲取的monitor都不是一個,當然相互不影響。
但是值得注意的幾點:
synchronized(Xxx.class)
獲取的是類的monitor,所以與public synchronized static void some()
修飾靜態方法是等價的synchronized(this)
獲取的是當前實例的monitor,所以與public synchronized void some()
修飾實例方法是等價的
以上兩點可以通過修改上述代碼中方法可以很容易驗證,我們修改最后一個方法
public synchronized static void der() throws Exception {
for (int i = 0; i < 5; i++) {
System.out.println("!!!der: " + i);
Thread.sleep(1000);
}
}
運行得到結果
!!!der: 0
!!!der: 1
!!!der: 2
!!!der: 3
!!!der: 4
>>>foo: 0
>>>foo: 1
>>>foo: 2
>>>foo: 3
>>>foo: 4
可以看到,確實synchronized(Xxx.class)
與synchronized
修飾靜態方法是等價的。再修改為synchronized
修飾實例方法
public synchronized void der() throws Exception {
for (int i = 0; i < 5; i++) {
System.out.println("!!!der: " + i);
Thread.sleep(1000);
}
}
運行查看結果
<<<bar: 0
<<<bar: 1
<<<bar: 2
<<<bar: 3
<<<bar: 4
!!!der: 0
!!!der: 1
!!!der: 2
!!!der: 3
!!!der: 4
可以看到,確實synchronized(this)
與synchronized
修飾實例方法是等價的。
總之,個人認為要理解幾種不一樣的地方,關鍵是理解清楚是獲取的誰的monitor,只要是同一個monitor,當然就會發生同步!