在JAVA中,多線程并發訪問共享資源是一個經常遇到的問題。為了保證數據的正確性和一致性,在多線程編程中需要使用同步機制來實現對臨界資源的互斥訪問。Java中的synchronized關鍵字提供了一種簡單而有效的同步機制,可以用于保護臨界區。
臨界區的概念
在多線程的程序中,臨界區指的是一段需要互斥訪問的代碼塊,即同一時間只能由一個線程執行的代碼。在這段代碼執行期間,如果其他線程試圖訪問該代碼塊,那么它們會被阻塞,直到當前線程釋放了鎖。
相對應地,非臨界區指的是所有不需要互斥訪問的代碼,也就是說,多個線程可以同時執行該代碼,而不會有數據競爭或并發問題。
在Java中,synchronized關鍵字用于保證對臨界區代碼的互斥訪問,使得在任何時刻只有一個線程可以執行該代碼。但是,synchronized不能保證對非臨界區代碼的可見性和有序性,因此,在修改共享變量的值、檢查共享變量的狀態等操作時,需要注意使用volatile關鍵字或其他同步機制來確保線程之間的正確協作。
1. 監視器鎖和Java對象
在Java中,每個對象都有一個關聯的監視器鎖,也稱為內部鎖或隱式鎖。每個鎖都有一個相關的等待集(Wait Set)和喚醒集(Notification Set)。當一個線程試圖獲取該對象的鎖時,如果該鎖已經被其他線程持有,則該線程將被阻塞,直到該鎖被釋放。當一個線程持有該鎖時,其他線程試圖獲取該鎖時會進入鎖的等待集,并在該鎖被釋放并通知時嘗試重新獲取鎖。
2. synchronized關鍵字的使用
synchronized關鍵字可以用于控制對臨界資源的訪問。它可以用于方法上或代碼塊上,并將其作為一個鎖來進行同步。當一個線程執行synchronized方法或代碼塊時,它將獲取與該方法或代碼塊關聯的對象的鎖。如果其他線程試圖獲取相同對象上的鎖,則它們將被阻塞,直到該鎖被釋放。
下面是一個簡單的示例,展示了如何使用synchronized來實現對臨界資源的同步訪問:
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized void decrement() {
count--;
}
public synchronized int getCount() {
return count;
}
}
在這個例子中,increment、decrement和getCount方法都被聲明為synchronized,并且它們都操作Counter對象的共享狀態。當一個線程調用其中一個方法時,它將獲取Counter對象的鎖,并允許該線程訪問其中的代碼。其他線程將被阻塞,直到鎖被釋放。
需要注意的是,在Java中,每個對象只有一個鎖。因此,如果一個類中有多個同步方法或代碼塊,它們都將競爭同一個鎖。這意味著,如果多個線程同時調用這些方法或代碼塊,它們只能以串行方式執行,從而降低了并發效率。因此,我們應該盡可能地減少同步代碼塊的大小和范圍,以便使其他線程能夠更快地訪問臨界資源。
3. 監視器鎖原理
在JVM層面,每個對象都有一個關聯的監視器鎖。當一個線程試圖獲取該對象的鎖時,如果該鎖已經被其他線程持有,則該線程將進入阻塞狀態。
在字節碼層面,synchronized關鍵字通過monitorenter和monitorexit指令來實現對臨界資源的同步訪問。這兩個指令可以分別看作是獲取鎖和釋放鎖的操作。當一個線程執行到monitorenter指令時,它將嘗試獲取對象的鎖。如果該鎖已經被其他線程持有,則該線程將進入阻塞狀態。當線程執行到monitorexit指令時,它將釋放對象的鎖,并喚醒等待集中的線程來競爭鎖。
在上面的代碼示例中,increment、decrement和getCount方法的實現都包含一個同步塊,其中使用了synchronized(this)來獲取Counter對象的鎖。當線程執行到這個同步塊時,它會執行monitorenter指令來獲取鎖,并在退出同步塊時執行monitorexit指令來釋放鎖。需要注意的是,如果多個線程同時調用這些方法,并且它們持有不同的Counter對象,那么它們將競爭不同的鎖,并且能夠并發地執行。因此,在編寫多線程程序時,應該盡量使用局部變量來限制鎖的范圍,以避免多個線程競爭同一個鎖而導致性能下降。
4. synchronized關鍵字的進階使用技巧
除了基本的同步機制外,synchronized關鍵字還提供了一些進階的使用技巧,可以更加靈活地控制對臨界資源的訪問。下面介紹幾種常見的技巧:
4.1 synchronized塊
除了將synchronized關鍵字用于方法聲明外,它還可以用于代碼塊。在這種情況下,同步塊的范圍可以更加靈活,從而使得多個線程可以并發地執行非臨界代碼。下面是一個示例代碼:
public class Counter {
private int count = 0;
private Object lock = new Object();
public void increment() {
synchronized (lock) {
count++;
}
}
public void decrement() {
synchronized (lock) {
count--;
}
}
public int getCount() {
synchronized (lock) {
return count;
}
}
}
在這個例子中,我們使用了一個Object對象作為鎖,并在每個方法內部使用synchronized(lock)來實現同步訪問。由于鎖的范圍被限制在同步塊內部,因此多個線程可以并發地執行非臨界代碼。
需要注意的是,在使用synchronized塊時,我們應該盡量使用局部變量或私有實例變量作為鎖對象,以避免和其他類共享鎖對象而導致死鎖等問題。
4.2 synchronized靜態方法
除了將synchronized關鍵字用于實例方法外,它還可以用于靜態方法。在這種情況下,鎖對象是該類的Class對象。下面是一個示例代碼:
public class Counter {
private static int count = 0;
public static synchronized void increment() {
count++;
}
public static synchronized void decrement() {
count--;
}
public static synchronized int getCount() {
return count;
}
}
在這個例子中,我們將synchronized關鍵字用于靜態方法,并使用類的Class對象作為鎖。由于鎖對象是唯一的,因此多個線程將串行地執行方法。
需要注意的是,在使用synchronized靜態方法時,我們應該盡可能地避免阻塞和競爭,以提高程序的并發性能。
4.3 可重入性
Java的synchronized關鍵字實現了可重入性,也就是說,如果一個線程已經持有了某個對象的鎖,那么它可以再次獲取該對象的鎖,從而繼續訪問臨界資源。這種機制可以有效地避免死鎖和餓死等問題。
在上面代碼示例中,increment方法調用了decrement方法,并且兩個方法都使用了synchronized關鍵字來同步訪問。由于這兩個方法都被聲明為synchronized,因此它們都需要獲取Counter對象的鎖來執行。如果一個線程已經持有了該鎖,那么它可以繼續獲取該鎖而不會被阻塞,從而避免了死鎖問題。
需要注意的是,在使用可重入性時,我們應該盡可能地避免嵌套鎖和遞歸鎖,以避免死鎖和性能問題。
4.4 volatile關鍵字
雖然synchronized關鍵字可以保證對臨界資源的同步訪問,但它并不能保證對非臨界資源的可見性。換句話說,如果一個線程修改了一個變量的值,但該變量沒有被聲明為volatile或使用同步機制進行同步訪問,那么其他線程可能無法看到該變量的新值。
下面是一個示例代碼:
public class Counter {
private int count = 0;
private boolean flag = false;
public void increment() {
count++;
flag = true;
}
public int getCount() {
if (flag) {
return count;
} else {
return -1;
}
}
}
在這個例子中,increment方法將count變量的值增加1,并設置flag變量的值為true。getCount方法檢查flag變量的值,如果為true,則返回count變量的值;否則返回-1。由于flag變量沒有被聲明為volatile或使用同步機制進行同步訪問,因此它的值可能無法被其他線程看到,從而導致getCount方法返回錯誤的結果。
為了解決這個問題,我們可以使用volatile關鍵字來保證對變量的可見性和有序性。下面是一個修改后的示例代碼:
public class Counter {
private volatile int count = 0;
private volatile boolean flag = false;
public void increment() {
count++;
flag = true;
}
public int getCount() {
if (flag) {
return count;
} else {
return -1;
}
}
}
在這個例子中,我們將count和flag變量都聲明為volatile,以保證它們的可見性和有序性。這樣,當一個線程修改了count或flag變量的值時,其他線程可以立即看到該變化。
需要注意的是,在使用volatile關鍵字時,我們應該避免使用復合操作以及依賴于先前狀態的操作,以確保對變量的操作的原子性和一致性。
5. 一些常見問題
在使用synchronized關鍵字時,我們應該避免以下常見的錯誤和陷阱:
5.1 不要過度同步
在多線程編程中,過度同步可能會導致死鎖、饑餓等問題,并且降低程序的并發性能。因此,我們應該盡可能地減少同步代碼塊的大小和范圍,以便使其他線程能夠更快地訪問非臨界資源。
5.2 避免死鎖
死鎖是多線程編程中常見的問題之一,它會導致多個線程相互等待彼此釋放鎖而無法繼續執行。避免死鎖的關鍵是正確地掌握鎖的粒度和順序,并盡可能地減少鎖的嵌套層數。
5.3 善用Wait和Notify
Java提供了Wait和Notify機制來協調多個線程對共享資源的訪問。在使用這些機制時,我們應該注意正確地使用鎖對象,并避免出現死鎖、饑餓等問題。
5.4 不要依賴于原子性
雖然synchronized關鍵字可以保證對臨界資源的同步訪問,但它并不能保證對非臨界資源的原子性操作。因此,在進行復合操作或依賴于先前狀態的操作時,我們應該使用原子類或其他同步工具來確保操作的原子性和一致性。
6. 總結
本文介紹了Java中的synchronized關鍵字及其基本用法、實現原理和進階使用技巧。需要注意的是,在編寫多線程程序時,我們應該注意避免常見的錯誤和陷阱,以確保程序的正確性和效率。