核心结论先行
static 本身不保证线程安全。

一个 static 变量或方法是否线程安全,完全取决于它的实现细节,而不是 static 这个修饰符。static 只是意味着这个成员(变量或方法)属于类,而不是类的某个实例。
static 变量的线程安全问题
static 变量(也称为类变量)在内存中只有一份拷贝,被所有该类的实例共享,正是这种“共享”的特性,使得它在多线程环境下成为潜在的线程安全风险。
1 线程不安全的 static 变量
当多个线程同时读写同一个可变的 static 变量时,如果不做任何同步处理,就会发生线程安全问题。
示例:非线程安全的计数器

public class UnsafeCounter {
// static 变量,所有实例共享
private static int count = 0;
public void increment() {
count++; // 这不是原子操作
}
public static int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
UnsafeCounter counter = new UnsafeCounter();
// 创建1000个线程,每个线程都增加100次
Runnable task = () -> {
for (int i = 0; i < 100; i++) {
counter.increment();
}
};
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
threads.add(new Thread(task));
}
for (Thread t : threads) {
t.start();
}
// 等待所有线程执行完毕
for (Thread t : threads) {
t.join();
}
// 预期结果是 1000 * 100 = 100000,但实际结果通常小于这个值
System.out.println("Final count: " + UnsafeCounter.getCount());
}
}
问题分析:
count++ 这个操作看起来是一行代码,但在 JVM 中,它至少包含三个步骤:
- 读取:从主内存读取
count的值到线程的工作内存。 - 修改:在工作内存中将
count的值加 1。 - 写入:将工作内存中修改后的值写回到主内存。
在多线程环境下,可能会发生这样的情况:
- 线程 A 读取
count值为 100。 - 线程 B 也读取
count值为 100。 - 线程 A 将值加 1 变为 101,并写回主内存。
- 线程 B 也将值加 1 变为 101,并写回主内存。
结果,count 从 100 增加到了 101,而不是预期的 102,这就是典型的竞态条件。

2 线程安全的 static 变量
要使 static 变量变得线程安全,我们需要采用同步机制来保证其操作的原子性。
解决方案 1:使用 synchronized 关键字
public class SafeCounterSync {
private static int count = 0;
// 使用 synchronized 修饰方法,保证同一时间只有一个线程能执行此方法
public synchronized void increment() {
count++;
}
// 同理,get 方法也应该同步,或者使用 volatile(见下文)
public synchronized static int getCount() {
return count;
}
}
synchronized 可以确保 increment() 方法内的代码是原子执行的,从而避免了竞态条件,但它的缺点是性能开销较大,且可能造成锁竞争。
解决方案 2:使用 java.util.concurrent.atomic 包
对于简单的计数器等场景,使用 AtomicInteger 等原子类是更高效、更推荐的方式。
import java.util.concurrent.atomic.AtomicInteger;
public class SafeCounterAtomic {
// 使用 AtomicInteger 替代 int
private static AtomicInteger count = new AtomicInteger(0);
public void increment() {
// AtomicInteger 的 incrementAndGet() 是原子操作
count.incrementAndGet();
}
public static int getCount() {
return count.get();
}
}
AtomicInteger 底层使用 CAS(Compare-And-Swap)操作,它是一种无锁算法,在性能上通常优于 synchronized,特别是在高并发场景下。
解决方案 3:使用 volatile 关键字
volatile 关键字可以确保变量的可见性和禁止指令重排序,但不能保证复合操作的原子性。
- 可见性:当一个线程修改了一个
volatile变量,新值会立刻同步到主内存,并且其他线程读取时会从主内存读取,保证了线程间的可见性。 - 不保证原子性:
volatile无法解决count++这样的复合操作问题。
volatile 适用于一个线程写,多个线程读的场景,对于上面的计数器例子,volatile 是不够的。
// 错误用法:volatile 不能保证 count++ 的原子性
private static volatile int count = 0;
public void increment() {
count++; // 仍然不是线程安全的!
}
static 方法的线程安全问题
static 方法的线程安全性与实例方法类似,关键在于方法内部访问和修改了哪些共享资源。
1 线程不安全的 static 方法
如果一个 static 方法内部修改了可变的 static 变量,那么它就是线程不安全的。
示例:
public class UnsafeStaticMethod {
private static List<String> names = new ArrayList<>();
// static 方法,但修改了 static 变量
public static void addName(String name) {
names.add(name);
}
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
addName("Thread-" + Thread.currentThread().getId());
}
};
// ... 创建并启动多个线程 ...
// names 列表的大小可能会小于预期,因为 ArrayList 的 add 操作不是线程安全的
}
}
2 线程安全的 static 方法
一个 static 方法是线程安全的,如果它满足以下任一条件:
-
不依赖任何共享的可变状态:方法内部只使用局部变量或参数,局部变量存储在各自线程的栈帧中,不在线程间共享,所以天然是线程安全的。
public class MathUtils { // 这个方法是线程安全的,因为它只依赖于参数,没有修改任何共享状态 public static int add(int a, int b) { return a + b; } } -
所有对共享可变状态的访问都经过了同步:比如使用
synchronized块或synchronized方法来保护共享资源。public class SafeStaticMethod { private static List<String> names = new ArrayList<>(); // 使用 synchronized 修饰 static 方法,锁的对象是 SafeStaticMethod.class public static synchronized void addName(String name) { names.add(name); } }
特殊情况:static final 常量
static final 修饰的变量是常量,如果这个常量的值是一个不可变对象,那么它就是线程安全的。
- 基本类型常量:如
public static final int MAX_AGE = 100;,这个值在编译时就确定了,并且不可更改,所以绝对线程安全。 - 引用类型常量:如
public static final String APP_NAME = "MyApp";。String是不可变类,所以这个引用和它指向的对象都是安全的。
如果 static final 引用的是一个可变对象,那么它本身引用是安全的,但对象的内容是不安全的!
public class UnsafeConstant {
// final 引用指向一个可变对象
public static final List<String> NAMES = new ArrayList<>();
public static void main(String[] args) {
// 不能重新赋值 NAMES = new ArrayList<>(); // 编译错误
// 但是可以修改 NAMES 指向的对象内容
NAMES.add("Alice"); // 线程不安全!
NAMES.remove("Alice"); // 线程不安全!
}
}
为了解决这个问题,通常的做法是让 static final 引用的对象本身也是不可变的,或者使用线程安全的集合。
// 安全的做法:使用不可变集合
public static final List<String> NAMES = Collections.unmodifiableList(new ArrayList<>());
// NAMES.add("Alice"); // 现在会抛出 UnsupportedOperationException
// 或者使用线程安全的集合
public static final List<String> NAMES = new CopyOnWriteArrayList<>();
总结与最佳实践
| 场景 | 线程安全? | 原因 | 解决方案 |
|---|---|---|---|
static 基本类型变量 |
否 | 多线程读写导致竞态条件 | synchronized、AtomicInteger |
static 引用类型变量 |
否 | 多线程读写或修改对象内容导致竞态条件 | synchronized、ConcurrentHashMap、CopyOnWriteArrayList 等 |
static final 基本类型常量 |
是 | 值在编译时确定,且不可变 | 无需特殊处理 |
static final 引用类型常量 |
否 | 引用不可变,但指向的对象内容可能可变且被并发修改 | 使用不可变对象(String, Collections.unmodifiableXXX)或线程安全对象 |
无状态的 static 方法 |
是 | 不依赖或修改任何共享可变状态 | 无需特殊处理,这是最佳实践 |
有状态的 static 方法 |
否 | 依赖或修改了共享可变状态 | 对共享状态进行同步(synchronized)或使用并发工具类 |
核心思想:
static是一把双刃剑:它方便了共享数据和全局访问,但也引入了线程安全问题。- 不变性是王道:尽量将
static变量设计为final且不可变,这是解决线程安全问题最简单、最有效的方法。 - 最小化共享状态:如果必须共享,尽量让共享的范围尽可能小。
- 同步是最后的手段:当必须共享可变状态时,优先使用
java.util.concurrent包中的并发工具(如AtomicInteger,ConcurrentHashMap),它们比synchronized更高效、更易用,只在必要时才使用synchronized。
