問題
在Java多線程編程中,你需要保護某些數據,防止多個線程同時訪問導致數據不一致或程序錯誤。
解決方案
在需要保護的方法或代碼段上使用synchronized
關鍵字。
討論
synchronized
關鍵字是Java提供的同步機制,用于確保在同一時刻只有一個線程能夠執行指定的方法或代碼塊。這種機制特別適用于保護共享資源,防止多線程并發訪問引發的問題。以下是synchronized
的主要功能:
- 對于實例方法,
synchronized
限制同一對象實例中只有一個線程可以執行該方法或其他同步方法。 - 對于靜態方法,
synchronized
限制同一類中只有一個線程可以執行該方法。 - 對于代碼塊,可以通過
synchronized(object)
指定鎖定某個對象,只保護特定的代碼段。
同步整個方法實現起來更簡單且更安全,但可能會因阻塞其他線程而影響性能。如果只需要保護部分代碼,可以使用同步代碼塊以提高效率。
示例:同步方法
以下是一個簡單的線程安全列表添加操作示例:
public class SafeList {private Object[] data;private int max = 0;public SafeList(int size) {data = new Object[size];}public synchronized void add(Object obj) {data[max] = obj;max = max + 1;}
}
在這個例子中,add()
方法被synchronized
修飾,確保同一時刻只有一個線程可以修改data
數組,避免數據覆蓋或丟失。
未同步的風險
假設我們去掉synchronized
,如下:
public void add(Object obj) {data[max] = obj; // 第一步:存儲對象max = max + 1; // 第二步:遞增索引
}
如果線程A在執行第一步后被中斷,線程B緊接著運行并執行兩步,會覆蓋線程A存儲的對象。線程A恢復后繼續執行第二步,導致max
指向一個未初始化的位置。這種情況可能導致數據丟失和數組狀態不一致,如下圖所示:
正常情況:
data[max] = obj; max = 1;失敗情況:
線程A: data[0] = obj1;
線程B: data[0] = obj2; max = 1;
線程A: max = 2; // obj1丟失,data[1]未初始化
即使將兩行合并為data[max++] = obj;
,問題依然存在,因為線程可能在JVM指令之間被中斷。只有使用synchronized
才能徹底解決問題。
示例:同步代碼塊
如果只想同步部分代碼,可以使用synchronized
代碼塊。例如:
public class SafeList {private Object[] data;private int max = 0;public SafeList(int size) {data = new Object[size];}public void add(Object obj) {synchronized (data) {data[max] = obj;max = max + 1;}}
}
這里,synchronized (data)
確保對data
數組的訪問是線程安全的,同時未同步的代碼(如構造函數)不會阻塞其他線程。
選擇同步對象
同步代碼塊需要指定一個對象作為鎖。通常選擇與共享資源相關的對象,例如:
synchronized(this)
:鎖定當前對象實例。synchronized(data)
:鎖定共享數組。- 自定義鎖對象:如
private final Object lock = new Object();
。
例如,同步對ArrayList
的訪問:
public class ListManager {private ArrayList<String> myList = new ArrayList<>();public void process(String item) {synchronized (myList) {if (myList.indexOf(item) != -1) {System.out.println("Item found!");} else {myList.add(item);}}}
}
示例:多線程數組操作
以下代碼展示了同步與非同步操作的對比:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class ArrayAdding {private static final int HOWMANY = 1000;private static int[] array;private static ExecutorService pool = Executors.newFixedThreadPool(2);static Runnable runBad = () -> {for (int i = 0; i < array.length; i++) {array[i] = array[i] + i;}};static Runnable runGood = () -> {synchronized (array) {for (int i = 0; i < array.length; i++) {array[i] = array[i] + i;}}};public static void main(String[] args) throws Exception {process("runGood", runGood);process("runBad", runBad);}static void process(String name, Runnable run) throws Exception {System.out.println("Starting: " + name);array = new int[HOWMANY];var t1 = pool.submit(run);var t2 = pool.submit(run);t1.get();t2.get();for (int i = 0; i < array.length; i++) {if (array[i] != 2 * i) {System.out.printf("%d found at offset %d\n", array[i], i);return;}}System.out.println(name + " completed successfully");}
}
運行結果可能如下:
Starting: runGood
runGood completed successfully
Starting: runBad
468 found at offset 468
runGood
使用同步,始終正確;runBad
未同步,可能因競態條件失敗。這種失敗在現實中可能導致嚴重后果,如Therac-25事件中的輻射治療事故。
結論
synchronized
關鍵字是Java中保護數據免受多線程并發訪問的有效工具。通過同步方法或代碼塊,可以防止數據不一致和競態條件。選擇同步整個方法還是代碼塊取決于性能和安全性的權衡。合理的同步設計能顯著提升程序的可靠性。