Java 靜態變量(static)是初學者面試高頻考點、項目中常見 Bug 源頭。本文以「s1+s2 值為何是 15」為切入點,從字節碼層面、內存模型、線程安全三個維度徹底講透 static 變量共享機制與累加陷阱。
public class StaticTest {
static int num = 0; // 靜態變量,屬於類,不屬於實例
public StaticTest() {
num += 5; // 每次 new 實例,num +5
}
public static void main(String[] args) {
StaticTest s1 = new StaticTest(); // num = 5
StaticTest s2 = new StaticTest(); // num = 10
System.out.println("s1.num = " + s1.num); // 10
System.out.println("s2.num = " + s2.num); // 10
System.out.println("s1 + s2 = " + (s1.num + s2.num)); // 20
}
}
若輸出 s1+s2 = 15,說明構造函數執行次數或累加邏輯與預期不符,需檢查實例創建次數。
方法區 / Metaspace (JDK 8+) 堆
┌─────────────────────────────┐ ┌──────────────────┐
│ StaticTest.class │ │ s1 實例 │
│ ├─ 方法表 │ │ ├─ 非靜態字段... │
│ ├─ 靜態字段: num = 10 ◀────┼──────┤ (無 num 副本) │
│ └─ 常量池 │ └──────────────────┘
└─────────────────────────────┘ ┌──────────────────┐
│ s2 實例 │
│ ├─ 非靜態字段... │
│ (無 num 副本) │
└──────────────────┘
static int num 存儲在方法區(JDK 8 之前 PermGen,JDK 8+ Metaspace),僅有一份。s1、s2、s3... 訪問 num 時,**指向同一個內存地址**。s1.num 和 s2.num 在編譯期會被編譯器優化為 StaticTest.num(字節碼 getstatic 指令)。// javap -c StaticTest.class 關鍵片段
// 構造函數
0: aload_0
1: invokespecial #1 // Object.
4: getstatic #2 // Field num:I ← 讀取類靜態變量
7: iconst_5
8: iadd
9: putstatic #2 // Field num:I ← 寫回類靜態變量
12: return
// main 中 s1.num + s2.num
10: getstatic #2 // Field num:I ← 讀取 num (10)
13: getstatic #2 // Field num:I ← 再次讀取 num (10)
16: iadd
17: invokevirtual #3 // PrintStream.println
兩次 getstatic 讀取的是**同一個靜態槽位**,值均為 10,相加得 20。
| 代碼變體 | 執行流程 | 結果 | |
|---|---|---|---|
構造函數 num += 5,創建 3 個實例 | 5 → 10 → 15 | s1.num=15, s2.num=15, s3.num=15, 和=30 | |
構造函數 num++,創建 3 次 + 額外手動 num+=10 | 1→2→3→13 | 視具體調用點而定 | |
num 非 static,但錯誤用 s1.num + s2.num | 各實例獨立累加 | s1=5, s2=5, 和=10 | |
| 混合 static/非 static 同名字段(字段遮蔽) | 極易混淆 | 不可預測,嚴禁同名 |
| 陷阱 | 錯誤代碼 | 後果 | 修正 |
|---|---|---|---|
| 多線程非原子累加 | static int count; count++; | 數據丟失,結果小於預期 | AtomicInteger / synchronized / LongAdder |
| 靜態集合共享導致污染 | static List list = new ArrayList(); | 不同業務流程互相影響 | 改為局部變量、ThreadLocal、或明確清理 |
| 單例模式雙重檢查缺少 volatile | static Singleton instance; | 指令重排導致部分初始化對象泄露 | static volatile Singleton instance; |
| 類加載器隔離導致多份副本 | Tomcat/WebLogic 多應用共用 JAR | 每個 Webapp 類加載器各有一份 static | 放入共享類加載器路徑、或用 JNDI/Redis 共享 |
| 序列化不序列化 static | static transient int x; | 反序列化後 static 值為類當前值,非序列化時值 | 理解 static 不參與序列化,反序列化後不恢復 |
// 1. 低併發、簡單計數
static AtomicInteger count = new AtomicInteger();
count.incrementAndGet();
// 2. 高併發、統計計數 (JDK 8+)
static LongAdder adder = new LongAdder();
adder.increment(); // 無鎖分段累加,極高吞吐
long value = adder.sum();
// 3. 需要原子複合操作 (CAS 循環)
static AtomicLong seq = new AtomicLong();
seq.updateAndGet(x -> x + 1);
// 4. 只是單線程或已有鎖保護
static long counter = 0L; // 非 static 更好
static 變量屬於類、存儲在方法區、全進程(同類加載器)共享一份。構造函數中 num += 5 每次實例化都修改同一個 num,最終所有實例讀取到的值相同。s1.num + s2.num 實際是 StaticTest.num + StaticTest.num,結果為 2 * 最终num值。理解「靜態歸類、實例歸對象」,靜態累加陷阱自然迎刃而解。