上文中提及在java中可以使用synchronized
關鍵字來解決竟態條件。主要通過synchronized
關鍵字來標註程式碼塊,告訴jvm該程式碼塊為臨界區程式碼,以保證每次只會有一個執行緒能訪問到該程式碼塊裡的程式碼,直到一個執行緒執行完畢後,另一個執行緒才能執行。
synchronized
使用物件作為同步鎖。若多個同步程式碼塊使用的是同一個物件鎖,那麼一次只能有一個執行緒訪問多個同步程式碼塊中的一個。若多個同步程式碼塊使用的不是同一個物件鎖,那麼多個執行緒能夠同時訪問多個同步程式碼塊。
synchronized
一共有四種用法,如下所示:
- 標註例項方法
- 標註靜態方法
- 標註例項方法裡的程式碼塊
- 標註靜態方法裡的程式碼塊
標註方法
在方法簽名中宣告synchronized
,能夠讓整個方法標註為程式碼塊。
標註例項方法
public synchronized void method0() {
// do something
}
複製程式碼
示例程式碼中使用的是該方法所屬的例項物件作為物件鎖。
標註靜態方法
public synchronized static void method0() {
// do something
}
複製程式碼
示例程式碼中使用的是該方法所屬類宣告指向的靜態例項物件作為物件鎖。
標註例項方法裡的程式碼塊
若不希望將整個方法標註為程式碼塊,可以在方法中標註部分程式碼塊作為同步程式碼塊。
標註例項方法裡的程式碼塊
public void method1() {
synchronized (this) {
// do something
}
}
複製程式碼
在例項方法中,通過synchronized
構造方法的方式來標註程式碼塊,括號中傳遞的是該同步程式碼塊使用的物件鎖,需要實現同步的臨界區程式碼寫在{}
中。程式碼中的this
指該程式碼塊所屬方法所屬的物件例項作為物件鎖。該方式與在例項方法簽名中宣告synchronized
效果相當。
標註靜態方法裡的方法塊
public synchronized static void method1() {
synchronized (MyClass.class) {
// do something
}
}
複製程式碼
在靜態方法中,通過synchronized
構造方法的方式來標註程式碼塊,括號中傳遞的是該同步程式碼塊使用的物件鎖,需要實現同步的臨界區程式碼寫在{}
中。程式碼中的MyClass.class
指該程式碼塊所屬方法所屬類的靜態物件例項作為物件鎖。該方式與在靜態方法簽名中宣告synchronized
效果相當。
同步例項
synchronized
編碼例項,我們在SynchronizedExample中編寫四個方法,分別反映上文提及的四種情況。每個方法中都線上程進入後暫停3s,隨後執行緒退出程式碼塊。
public class SynchronizedExample {
private static void method(String name) {
final Thread thread = Thread.currentThread();
LocalDateTime now = LocalDateTime.now();
System.out.println(thread.getName() + ": [" + now + "] in " + name);
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void method0() {
method("instance-method0");
}
public void method1() {
synchronized (this) {
method("instance-method1");
}
}
public synchronized static void method2() {
method("static-method2");
}
public synchronized static void method3() {
synchronized (SynchronizedExample.class) {
method("static-method3");
}
}
public static void main(String[] args) {
SynchronizedExample example = new SynchronizedExample();
Runnable myRunnable0 = () -> {
example.method0();
example.method1();
};
Runnable myRunnable1 = () -> {
SynchronizedExample.method2();
SynchronizedExample.method3();
};
IntStream.range(1, 3)
.forEach(i -> new Thread(myRunnable0, "Thread-" + i).start());
// 例項同步方法需要12s才能執行完,主執行緒等待13s後再執行靜態同步方法
try {
Thread.sleep(13000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
IntStream.range(1, 3)
.forEach(i -> new Thread(myRunnable1, "Thread-" + i).start());
}
}
複製程式碼
執行結果:
Thread-1: [2019-03-15T14:48:50.251] in instance-method0
Thread-2: [2019-03-15T14:48:53.255] in instance-method0
Thread-2: [2019-03-15T14:48:56.258] in instance-method1
Thread-1: [2019-03-15T14:48:59.262] in instance-method1
Thread-1: [2019-03-15T14:49:03.234] in static-method2
Thread-2: [2019-03-15T14:49:06.238] in static-method2
Thread-2: [2019-03-15T14:49:09.243] in static-method3
Thread-1: [2019-03-15T14:49:12.247] in static-method3
從結果可以看出,使用同個物件例項作為物件鎖和使用同個靜態物件作為物件鎖的方法分別被執行緒1和執行緒2訪問。 從上文列印的時間可以看出每個執行緒每次僅能訪問使用同個物件鎖的多個同步程式碼塊中的一個。每3s執行完一個同步程式碼塊。
Java Concurrency 工具包
實際上synchronized
是java中第一次針對競態條件釋出的同步措施,但在實際開發中並不是那麼好用,因此在jdk1.5後,釋出了整個併發工具包
,提供了各式各樣的多執行緒安全控制元件,用於協助開發者編寫執行緒安全的應用程式。