static 变量和方法不属于任何实例,而是属于类本身,这意味着它们在 JVM 的内存中只有一份拷贝,并且所有线程共享这份拷贝。
下面我们从几个方面来深入理解。
static 变量(静态变量)的线程安全问题
static 变量也称为类变量,由于它在所有实例之间共享,所以任何线程对它的修改都会影响到其他所有线程,这天然就带来了线程安全问题。
为什么是线程不安全的?
想象一个场景:多个线程同时读写一个共享的 static 变量。
class SharedCounter {
// static 变量,所有线程共享
private static int count = 0;
public static void increment() {
count++; // 这一步不是原子的
}
public static int getCount() {
return count;
}
}
count++ 这个操作,在底层其实包含三个步骤:
- 读取:从主内存中读取
count的值到线程的工作内存。 - 修改:在工作内存中将
count的值加 1。 - 写入:将工作内存中修改后的值写回到主内存。
如果多个线程同时执行这个操作,就可能发生 竞态条件。
竞态条件示例:
假设 count 的初始值为 0,两个线程 A 和 B 同时调用 increment()。
- 线程 A 读取
count的值(0)。 - 线程 B 也读取
count的值(0)。 - 线程 A 将值加 1,得到 1,并写回主内存。
count现在是 1。 - 线程 B 也将其读取的值(0)加 1,得到 1,并写回主内存。
count现在还是 1。
预期结果:count 应该是 2。
实际结果:count 是 1。
如果多个线程要并发地修改同一个 static 变量,并且修改操作不是原子的,那么就必须进行同步控制,否则结果是不可预测的。
如何解决 static 变量的线程安全问题?
有几种常见的解决方案:
使用 synchronized 关键字
可以同步整个方法,或者同步代码块,同步代码块是更优的选择,因为它可以精确锁定资源,减少锁的粒度,提高并发性能。
class SharedCounter {
private static int count = 0;
// 同步整个方法,锁是 SharedCounter.class 对象
public static synchronized void increment() {
count++;
}
// 或者使用同步代码块,更灵活
public static void incrementWithBlock() {
synchronized (SharedCounter.class) { // 锁定类的 Class 对象
count++;
}
}
public static int getCount() {
return count;
}
}
注意:对于静态方法,锁的对象是 类名.class(SharedCounter.class),对于实例方法,锁的对象是 this。
使用 Atomic 类(推荐)
Java 并发包提供了 java.util.concurrent.atomic 下的原子类,如 AtomicInteger, AtomicLong 等,它们使用 CAS(Compare-And-Swap)机制,在硬件层面保证了原子性,性能通常比 synchronized 更好。
import java.util.concurrent.atomic.AtomicInteger;
class SharedCounter {
// 使用 AtomicInteger 替代 int
private static AtomicInteger count = new AtomicInteger(0);
public static void increment() {
// 原子操作,无需手动加锁
count.incrementAndGet();
}
public static int getCount() {
return count.get();
}
}
使用不可变对象
static 变量是一个对象,并且该对象的状态是不可变的(String, Integer 包装类等),那么它本身就是线程安全的,因为任何修改操作(如 连接字符串)都会返回一个新对象,而不是修改原对象。
public class ImmutableConfig {
// String 是不可变的,所以是线程安全的
private static final String APP_NAME = "MyApp";
// final 保证了引用的不可变性,Config 类内部状态也是 final 的,那么它也是线程安全的
private static final Config config = new Config("1.0", "admin");
}
static 方法的线程安全问题
static 方法的线程安全性取决于其内部实现。
无状态方法(线程安全)
如果一个 static 方法只依赖于其内部的局部变量,并且不修改任何共享资源(static 变量或实例变量),那么它就是线程安全的。
public class MathUtils {
// 这个方法是线程安全的,因为它不依赖任何共享状态
public static int add(int a, int b) {
return a + b;
}
}
有状态方法(线程不安全)
如果一个 static 方法读取或修改了 static 变量,那么它就变成了有状态的,会面临和 static 变量一样的线程安全问题,这种情况的解决方案与上面提到的 static 变量解决方案完全一致(使用 synchronized 或 Atomic 类)。
public class SharedCounter {
private static int count = 0;
// 这个方法不是线程安全的
public static void unsafeIncrement() {
count++;
}
}
特殊情况:synchronized 静态方法
一个 synchronized 静态方法,其锁是 类对象,这意味着同一时间,只有一个线程可以执行这个类的任何一个 synchronized 静态方法。
public class ClassLevelLock {
public static synchronized void methodA() {
System.out.println("Thread " + Thread.currentThread().getName() + " is in methodA.");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static synchronized void methodB() {
System.out.println("Thread " + Thread.currentThread().getName() + " is in methodB.");
}
}
如果线程1调用了 ClassLevelLock.methodA(),线程2在 methodA() 执行完毕前,无法调用 ClassLevelLock.methodB(),因为它们都锁的是 ClassLevelLock.class。
static 代码块的线程安全问题
static 代码块在类被加载到 JVM 时由类加载器执行。类加载过程本身是线程安全的,JVM 内部通过加锁确保一个类只被加载一次,static 代码块的执行是原子的,不会有多个线程同时进入 static 代码块的情况。
static 代码块是线程安全的。
public class MyClass {
static {
System.out.println("Static block is being executed by thread: " + Thread.currentThread().getName());
// 初始化一些静态资源
// ...
}
}
static 内部类(静态内部类)的线程安全
静态内部类与非静态内部类最大的区别在于:
- 静态内部类:不持有外部类的隐式引用(即
Outer.this),它像一个独立的类。 - 非静态内部类:持有外部类的隐式引用。
线程安全性分析: 静态内部类本身的线程安全性,和普通类一样,取决于它的成员变量和方法是否被多个线程共享和修改。
一个重要的应用:单例模式
静态内部类是实现单例模式的推荐方式之一(Initialization-on-demand holder idiom),因为它天然是线程安全的,并且实现了懒加载。
public class Singleton {
// 私有构造函数,防止外部实例化
private Singleton() {}
// 静态内部类
private static class SingletonHolder {
// 静态实例,由 JVM 类加载机制保证其唯一性和线程安全
private static final Singleton INSTANCE = new Singleton();
}
// 公共获取实例的方法
public static Singleton getInstance() {
// 当调用此方法时,才会加载 SingletonHolder 类,从而创建 INSTANCE
return SingletonHolder.INSTANCE;
}
}
为什么它是线程安全的?
- 懒加载:
Singleton类被加载时,SingletonHolder类不会被加载,只有当getInstance()方法第一次被调用时,JVM才会加载SingletonHolder并初始化INSTANCE。 - 类加载的原子性:类的加载和初始化过程在 JVM 中是严格同步的,由
Class对象作为锁。INSTANCE的初始化过程是原子的,只有一个线程能执行它。 - 内存可见性:JVM 保证在类初始化完成前,其他线程是无法使用它的。
static 成员类型 |
线程安全性分析 | 解决方案 |
|---|---|---|
static 变量 |
不安全,所有线程共享一份拷贝,并发修改会导致竞态条件。 | synchronized 关键字(同步方法或代码块)java.util.concurrent.atomic 原子类(推荐)使用不可变对象 |
static 方法 |
取决于实现。 - 如果是无状态方法(只操作局部变量),则安全。 - 如果有状态(读写 static 变量),则不安全。 |
同上(针对有状态方法)。 |
static 代码块 |
安全,类加载过程由 JVM 保证线程安全,只会执行一次。 | 无需额外处理。 |
static 内部类 |
类本身的安全性同普通类,但其一个重要应用——静态内部单例模式,是线程安全的,因为它利用了类加载的原子性。 | 无需额外处理(用于单例模式时)。 |
核心思想:static 成员的共享性是其线程风险的根源,判断其是否线程安全,关键在于分析它是否被多个线程并发地、非原子地修改,如果存在这种风险,就必须使用同步机制(如 synchronized)或并发工具(如 Atomic 类)来保证其正确性。
