在 Java 多線程程式設計中,synchronized 關鍵字是實現線程安全的基礎工具。然而,許多開發者忽略了一個關鍵細節:同步鎖定的是對象實例,而非變量引用本身。當你在同步塊內部重新分配被鎖定對象的引用時,會導致嚴重的線程安全問題。
考慮以下使用數組實現的棧結構,當容量不足時會重新分配更大的數組:
class Stack {
private int stackSize = 10;
private int[] intArr = new int[stackSize];
private int index = 0;
public void push(int val) {
synchronized(intArr) {
if (index == intArr.length) {
stackSize *= 2;
int[] newIntArr = new int[stackSize];
System.arraycopy(intArr, 0, newIntArr, 0, intArr.length);
intArr = newIntArr; // 危險:重新分配引用
}
intArr[index] = val;
index++;
}
}
public int pop() {
synchronized(intArr) {
if (index > 0) {
return intArr[--index];
}
}
throw new EmptyStackException();
}
}
push(),獲得 intArr 當前引用的數組對象鎖intArr 指向新數組pop(),獲得舊數組的鎖(因為它在擴容前就已等待)push(),獲得新數組的鎖將同步粒度提升到方法層級,鎖定 Stack 實例本身(this),而非內部數組引用:
class Stack {
private int stackSize = 10;
private int[] intArr = new int[stackSize];
private int index = 0;
public synchronized void push(int val) {
if (index == intArr.length) {
stackSize *= 2;
int[] newIntArr = new int[stackSize];
System.arraycopy(intArr, 0, newIntArr, 0, intArr.length);
intArr = newIntArr;
}
intArr[index++] = val;
}
public synchronized int pop() {
if (index > 0) {
return intArr[--index];
}
throw new EmptyStackException();
}
}
this、私有 final 對象或專用鎖對象synchronized 方法更安全且可讀性更高這個陷阱提醒我們:同步保護的是對象身份,而非變量名稱。任何可能導致鎖對象身份改變的操作,都會破壞同步語義。