本文內容
- Synchronized 關鍵字
- 示例
- Synchronized 方法
- 內部鎖(Intrinsic Locks)和 Synchronization
- 參考資料
下載 Demo
Synchronized 關鍵字
Java 語言提供兩個基本的同步機制:synchronized 方法(synchronized methods )和 synchronized 語句(synchronized statements)。
示例
先大概說一下 Java Synchronized 關鍵字,當它用來修飾一個方法或者一個代碼塊的時候,能夠保證在同一時刻只有一個線程執行該段代碼。
- 當兩個線程訪問同一個對象 synchronized(this) 代碼塊時,只能有一個線程執行,另一個線程必須等待這個線程執行完后才能執行;
- 但是,當一個線程訪問一個對象 synchronized(this) 同步代碼塊時,另一個線程仍能訪問該對象的非 synchronized(this) 代碼塊;
- 尤其是,當一個線程訪問一個對象的一個 synchronized(this) 代碼塊時,其他線程對該對象中其他所有 synchronized(this) 代碼塊的訪問也將被阻塞;
- 以上規則對其它對象鎖同樣適用。
如下代碼所示:
package cn.db.syncdemo;
public class NewClass {
/**
* 同步方法
*/
public void synchronizedMethod() {
synchronized (this) {
int i = 5;
while (i-- > 0) {
System.out.println(Thread.currentThread().getName() + " : " + i
+ " synchronized method");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println(e.toString());
}
}
}
}
/**
* 同步方法 2
*/
public void synchronizedMethod2() {
synchronized (this) {
int i = 5;
while (i-- > 0) {
System.out.println(Thread.currentThread().getName() + " : " + i
+ " synchronized method 2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println(e.toString());
}
}
}
}
/**
* 非同步方法
*/
public void nonSynchronizedMethod() {
int i = 5;
while (i-- > 0) {
System.out.println(Thread.currentThread().getName() + " : " + i
+ " nonSynchronized method");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println(e.toString());
}
}
}
public static void main(String[] args) {
final NewClass mClass = new NewClass();
// t1 和 t2 都要訪問同一個同步方法 synchronizedMethod
Thread t1 = new Thread(new Runnable() {
public void run() {
mClass.synchronizedMethod();
}
}, "Thread 1");
Thread t2 = new Thread(new Runnable() {
public void run() {
mClass.synchronizedMethod();
}
}, "Thread 2");
// t3 要訪問另一個同步方法 synchronizedMethod2
Thread t3 = new Thread(new Runnable() {
public void run() {
mClass.synchronizedMethod2();
}
}, "Thread 3");
// t4 要訪問非同步方法 nonSynchronizedMethod
Thread t4 = new Thread(new Runnable() {
public void run() {
mClass.nonSynchronizedMethod();
}
}, "Thread 4");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
運行結果:
Thread 4 : 4 nonSynchronized method
Thread 1 : 4 synchronized method
Thread 4 : 3 nonSynchronized method
Thread 4 : 2 nonSynchronized method
Thread 1 : 3 synchronized method
Thread 4 : 1 nonSynchronized method
Thread 4 : 0 nonSynchronized method
Thread 1 : 2 synchronized method
Thread 1 : 1 synchronized method
Thread 1 : 0 synchronized method
Thread 3 : 4 synchronized method 2
Thread 3 : 3 synchronized method 2
Thread 3 : 2 synchronized method 2
Thread 3 : 1 synchronized method 2
Thread 3 : 0 synchronized method 2
Thread 2 : 4 synchronized method
Thread 2 : 3 synchronized method
Thread 2 : 2 synchronized method
Thread 2 : 1 synchronized method
Thread 2 : 0 synchronized method
說明:
- synchronizedMethod 同步方法,每隔 2 秒輸出一條信息;
- synchronizedMethod2 同步方法,每隔 1 秒輸出一條信息;
- nonSynchronizedMethod 非同步方法,每隔 1 秒輸出一條信息;
另外,
- 線程 t1 和線程 t2 都訪問 synchronizedMethod 方法;
- 只有線程 t3 訪問 synchronizedMethod2 方法;
- 只有線程 t4 訪問 nonSynchronizedMethod 方法。
注意:
- t4 訪問非同步方法 nonSynchronizedMethod2,無論何時都可以訪問,所以“thread 4:……”出現的是不規律的,交錯的;
- t1 和 t2 都要訪問同一個同步方法 synchronizedMethod,所以“thread 1:……”和“thread 2:……”絕對不會交錯顯示,一個線程訪問完后,另一個線程才能訪問;
- 對於 t3,t3 自己訪問另一個同步方法 synchronizedMethod2,並且沒有其他線程再訪問,而且當 t3 訪問該方法時,其他同步方法也被鎖了,所以,“thread 3:……”是連續出現的;
- 如果不看 t4,也就是沒有該線程,那么結果會是:
Thread 1 : 4 synchronized methodThread 1 : 3 synchronized methodThread 1 : 2 synchronized methodThread 1 : 1 synchronized methodThread 1 : 0 synchronized methodThread 3 : 4 synchronized method 2Thread 3 : 3 synchronized method 2Thread 3 : 2 synchronized method 2Thread 3 : 1 synchronized method 2Thread 3 : 0 synchronized method 2Thread 2 : 4 synchronized methodThread 2 : 3 synchronized methodThread 2 : 2 synchronized methodThread 2 : 1 synchronized methodThread 2 : 0 synchronized method說明:
- “thread 1……”和”thread 2……” 絕對不會交錯出現,只有當其中一個訪問完,另一個才能訪問;
- 調試時,“thread 1……”和”thread 2……” 誰先出現,貌似是不確定的,但如果“thread 1……”先執行,那“thread 1……”先出現的幾率肯定會大很多。這得看場景,畢竟是並發,誰先出現,誰后出現,並不重要的;
- ”thread 3……“訪問時,“thread 1……”和”thread 2……” 也不能訪問。雖然沒有線程跟”thread 3……“搶,但這三個線程訪問的都是兩個同步方法。
Synchronized 方法
要想使一個方法為同步方法,只需簡單給它的聲明加上 synchronized
關鍵字就行:
package cn.db.syncdemo;
public class SynchronizedCounter {
private int c = 0;
public synchronized void increment() {
c++;
}
public synchronized void decrement() {
c--;
}
public synchronized int value() {
return c;
}
}
如果 count
是 SynchronizedCounter 的一個實例,那么把這些方法變成
synchronized 方法會有兩個效果:
- 首先,在同一個對象上,交錯調用 synchronized 方法是不可能的。當一個線程正在執行一個對象的 synchronized 方法時,所有調用該 synchronized 方法的其他線程會被掛起,直到第一個線程完成;
- 其次,當一個 synchronized 方法退出時,它自動地建立一個對同一個對象的這個同步方法的任何后續調用的發生前關系(happens-before relationship)。這保證了對象狀態的改變對所有線程都是可見的。
注意:構造函數不能是 synchronized,也就是說,
synchronized
關鍵字修飾構造函數是一個語法錯誤。synchronized 構造函數沒有意義,因為當對象創建時,只有創建對象的那個線程才能訪問它。
警告:當構造一個對象在線程之間共享時,必須非常小心,一個對象的引用不能過早地“泄漏”。例如,假設你想維護一個名為
instances
的 List,它包含類的每個實例。你可能會用下面代碼在你的構造函數里:instances.add(this);
但是,當對象構造完成后,其他線程使用
instances
訪問對象。
synchronized 方法用一個簡單策略來阻止線程干擾和內存一致性錯誤:如果一個對象對多個線程可見,所有讀或寫該對象的變量都通過 synchronized
方法完成。 (一個重要的例外:final 字段,對象被構造后不能被修改,可以通過非 synchronized 方法來安全地讀取,一旦對象被構造)。
內部鎖(Intrinsic Locks)和 Synchronization
同步是建立在被稱為內部鎖(Intrinsic Locks)或監視器鎖定的一個內部實體。(API 規范往往指的是作為一個“顯示器”的實體。)內在鎖扮演兩方面同步的角色:執行獨占訪問對象的狀態,建立發生前關系(happens-before relationships )必要的可見性。
每個對象都有一個與之關聯的內部鎖。通常,一個需要獨占和一致的訪問對象字段的線程必須在訪問之前要求對象的內部鎖,然后當完成后釋放內部鎖。線程在要求和釋放鎖之間的時間擁有內部鎖。只要一個線程擁有一個內部鎖,其他線程就不能再要求這個鎖。當其他線程試圖要求鎖時就會被阻塞。
當一個線程釋放內部鎖時,一個發生前關系(happens-before relationship)在該動作和后續相同鎖的動作之間被建立。
Synchronized Methods 方法
當一個線程調用一個 synchronized 方法,它會自動獲取該方法對象的內部鎖,並且,當方法返回時,釋放鎖。即使返回由未捕獲的異常發生時造成的鎖釋放。
你可能想知道,一個靜態的 synchronized 方法被調用時發生了什么,因為一個靜態方法與一個類關聯,而不是一個對象。在這種情況下,線程獲得與類有關的 Class 對象的內部鎖。因此,訪問類的靜態字段是由鎖控制的,而這個鎖與任何類的實例的鎖不同。
Synchronized 語句
創建 synchronized 代碼的另一個方式是 synchronized 語句。與 synchronized 方法不同,synchronized 語句必須指定提供內部鎖的對象,如下代碼所示中的“this”:
public void addName(String name) {
synchronized(this) {
lastName = name;
nameCount++;
}
nameList.add(name);
}
本例中,addName 方法需要同步改變 lastName 和 nameCount,還需要避免其它對象的方法同步調用。要是沒有 synchronized 語句,就必須有一個單獨的、非 synchronized 方法用於調用 nameList.add 的目的。
Synchronized 語句對用細粒度同步來改進並發也很有用。假設,例如,類 MsLunch
有兩個實例字段,c1
和 c2
,它們絕不會一起使用。所有這些字段的更新都必須被同步,但沒有理由阻止 c1 和 c2 交叉更新(通過創建一個沒有必要的代碼塊,來減少並發)。因此,不是使用 synchronized(this),而是為兩個對象提供單獨的鎖。如下所示:
public class MsLunch {
private long c1 = 0;
private long c2 = 0;
private Object lock1 = new Object();
private Object lock2 = new Object();
public void inc1() {
synchronized(lock1) {
c1++;
}
}
public void inc2() {
synchronized(lock2) {
c2++;
}
}
}
這么用要格外小心。你必須絕對確保它對交叉訪問受影響字段是真的安全。
Reentrant Synchronization
回想一下,一個線程不能獲取由另一個線程擁有的鎖。但一個線程可以獲取,它已經擁有的鎖。讓一個線程不只一次獲取相同的鎖會造成折返同步(reentrant synchronization)。這描述了一種情況,同步代碼碼直接或間接地調用一個方法,該方法也包含的同步代碼,並且這兩組代碼都使用相同的鎖。如果沒有折返同步(reentrant synchronization),同步代碼必須采取很多額外的預防措施,來避免線程阻塞自己。
參考資料
- https://docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html
- https://docs.oracle.com/javase/tutorial/essential/concurrency/syncrgb.html
- java 並發演示動畫