在不同執行緒之間同步靜態變數
1. 概述
在 Java 中,需要同步存取靜態變數並不罕見。在這個簡短的教學中,我們將了解在不同執行緒之間同步存取靜態變數的幾種方法。
2.關於靜態變數
快速回顧一下, static
變數屬於類別而不是類別的實例。這意味著類別的所有實例都具有相同的變數狀態。
例如,讓我們考慮一個帶有static
變數的Employee
類別:
public class Employee {
static int count;
int id;
String name;
String title;
}
在本例中, count
變數是靜態的,表示曾經在公司工作過的員工總數。無論我們建立多少個Employee
實例,它們都將共用相同的count
值。
然後,我們可以向建構函數添加程式碼,以確保追蹤每個新員工的計數:
public Employee(int id, String name, String title) {
count = count + 1;
// ...
}
雖然這種方法很簡單,但當我們想要讀取count
變數時,它可能會出現問題。在具有Employee
類別的多個實例的多執行緒環境中尤其如此。
下面,我們將看到同步存取count
變數的不同方法。
3. 使用synchronized
關鍵字同步靜態變數
同步靜態變數的第一種方法是使用Java 的synchronized
關鍵字。我們可以透過多種方式利用此關鍵字來存取靜態變數。
首先,我們可以建立一個靜態方法,在其聲明中使用synchronized
關鍵字作為修飾符:
public Employee(int id, String name, String title) {
incrementCount();
// ...
}
private static synchronized void incrementCount() {
count = count + 1;
}
public static synchronized int getCount() {
return count;
}
在這種情況下, synchronized
關鍵字會鎖定類別對象,因為變數是 static 。這意味著無論我們創建多少個Employee
實例,只要使用這兩個靜態方法,一次只有一個實例可以存取該變數。
其次,我們可以使用同步區塊在類別物件上明確同步:
private static void incrementCount() {
synchronized(Employee.class) {
count = count + 1;
}
}
public static int getCount() {
synchronized(Employee.class) {
return count;
}
}
請注意,這在功能上與第一個範例等效,但程式碼更明確一些。
最後,我們也可以使用具有特定物件實例而不是類別的synchronized
區塊:
private static final Object lock = new Object();
public Employee(int id, String name, String title) {
incrementCount();
// ...
}
private static void incrementCount() {
synchronized(lock) {
count = count + 1;
}
}
public static int getCount() {
synchronized(lock) {
return count;
}
}
有時首選這種方法的原因是因為鎖是我們班級私有的。在第一個範例中,我們控制之外的其他程式碼也可能鎖定我們的類別。有了私人鎖,我們就可以完全控制它的使用方式。
Java的synchronized
關鍵字只是同步靜態變數存取的一種方法。下面,我們將了解一些也可以提供靜態變數同步的 Java API。
4. 同步靜態變數的 Java API
Java 程式語言提供了多個有助於同步的 API。讓我們來看看其中的兩個。
4.1.原子包裝器
[AtomicInteger](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/atomic/AtomicInteger.html)
類別在 Java 1.5 中引入,是同步靜態變數存取的另一種方法。此類別提供原子讀取和寫入操作,確保所有執行緒的基礎值的視圖一致。
例如,我們可以使用AtomicInteger
類型而不是int
來重寫Employee
類別:
public class Employee {
private final static AtomicInteger count = new AtomicInteger(0);
public Employee(int id, String name, String title) {
count.incrementAndGet();
}
public static int getCount() {
count.get();
}
}
除了AtomicInteger
之外, Java 還提供了long
和boolean
以及引用類型的原子包裝器。所有這些包裝類別都是同步靜態資料存取的好工具。
4.2.可重入鎖
ReentrantLock
類別也在 Java 1.5 中引入,是我們可以用來同步靜態資料存取的另一個機制。它提供與我們之前使用的synchronized
關鍵字相同的基本行為和語義,但具有附加功能。
讓我們來看一個範例,說明Employee
類別如何使用ReentrantLock
而不是int
:
public class Employee {
private static int count = 0;
private static final ReentrantLock lock = new ReentrantLock();
public Employee(int id, String name, String title) {
lock.lock();
try {
count = count + 1;
}
finally {
lock.unlock();
}
// set fields
}
public static int getCount() {
lock.lock();
try {
return count;
}
finally {
lock.unlock();
}
}
}
關於這種方法有幾點要注意。首先,它比其他的冗長得多。每次訪問共享變數時,我們都必須確保在訪問之前鎖定並在訪問之後解鎖。如果我們忘記在存取共享靜態變數的每個地方執行此序列,這可能會導致程式設計師錯誤。
此外,該類別的文件建議使用try
/ finally
區塊來正確鎖定和解鎖。這會增加額外的程式碼行和冗長,如果我們在所有情況下都忘記這樣做,則程式設計師更容易犯錯。
也就是說, ReentrantLock
類別提供了超出synchronized
關鍵字的附加行為。除此之外,它允許我們設定公平標誌並查詢鎖的狀態,以詳細了解有多少線程正在等待它。
5. 結論
在本文中,我們研究了在不同實例和執行緒之間同步存取靜態變數的幾種不同方法。我們首先查看了 Java synchronized
關鍵字,並查看如何將其用作方法修飾符和靜態程式碼區塊的範例。
然後我們研究了 Java 並發 API 的兩個特性: AtomicInteger
和ReeantrantLock
。這兩個 API 都提供了同步對共享資料的存取的方法,並且除了synchronized
關鍵字之外還有一些額外的好處。
上面的所有範例都可以在GitHub上找到。