static 主要可以用来修饰变量、方法、代码块和内部类,在多线程场景下,我们最关心的是 static 变量和 static 方法。

static 变量 (静态变量)
static 变量也叫类变量,它的核心特征是:被类的所有实例共享。
关键特性
- 内存位置:
static变量存储在方法区(在 JDK 7 及之前是永久代,JDK 8 及之后是元空间)的运行时常量池中,而不是在堆中的对象实例里。 - 生命周期:它随着类的加载而创建,随着类的卸载而销毁,它的生命周期比任何实例对象都长。
- 访问方式:可以通过
类名.变量名或实例名.变量名的方式访问,但推荐使用前者,因为它更清晰地表明这是一个静态成员。
多线程影响:共享与并发问题
由于 static 变量是所有线程共享的,当多个线程同时读写同一个 static 变量时,就会引发并发问题,最典型的就是竞态条件。
示例:一个不安全的计数器
假设我们有一个全局的计数器,多个线程同时对其进行递增操作。

public class StaticCounterUnsafe {
// static 变量,所有线程共享
private static int count = 0;
public static void increment() {
count++; // 这不是一个原子操作
}
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
increment();
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
// 预期结果是 2000,但实际结果几乎总是小于 2000
System.out.println("Final count: " + count);
}
}
为什么 count++ 不安全?
count++ 看起来是一个操作,但在 JVM 中,它通常被分解为三个独立的步骤:
- 读取:从主内存中读取
count的值到线程的工作内存。 - 修改:将工作内存中的值加 1。
- 写入:将修改后的值写回到主内存。
在多线程环境下,这个顺序可能会被打乱,导致数据不一致。
- 场景:
- 线程 A 读取
count(值为 0)。 - 线程 B 也读取
count(值也为 0)。 - 线程 A 将值加 1,写回主内存,
count变为 1。 - 线程 B 也将其值加 1(基于它读取的 0),写回主内存,
count也变为 1。
- 线程 A 读取
- 结果:两次 操作,但
count只增加了 1,这就是典型的丢失更新问题。
解决方案:保证线程安全
为了解决 static 变量的并发问题,我们需要确保对它的操作是原子性的,有几种常见的方法:

使用 synchronized 关键字
可以同步整个方法或代码块。
// 同步方法
public static synchronized void incrementSyncMethod() {
count++;
}
// 同步代码块(更灵活,可以指定锁对象)
// 通常使用类对象作为锁,因为静态变量属于类
public static void incrementSyncBlock() {
synchronized (StaticCounterUnsafe.class) {
count++;
}
}
使用 java.util.concurrent.atomic 包
这是更现代、性能通常更好的方式。AtomicInteger 等类使用了底层的 CAS(Compare-And-Swap)指令来保证原子性,避免了 synchronized 的阻塞开销。
import java.util.concurrent.atomic.AtomicInteger;
public class StaticCounterSafe {
// 使用 AtomicInteger 替代 int
private static final AtomicInteger count = new AtomicInteger(0);
public static void increment() {
// AtomicInteger 的 incrementAndGet() 是原子操作
count.incrementAndGet();
}
// ... main 方法同上 ...
}
static 方法 (静态方法)
static 方法也叫类方法,它可以直接通过 类名.方法名() 调用,无需创建类的实例。
关键特性
- 访问限制:
static方法只能直接访问其他的static成员(变量或方法),它不能访问非静态(实例)成员,因为实例成员需要依赖于具体的对象实例,而static方法在调用时可能还没有任何对象实例存在。 - 没有
this引用:在static方法内部,不能使用this关键字,因为this代表当前对象实例,而static方法不与任何特定实例关联。
多线程影响:与锁的关系
static 方法与 synchronized 结合使用时,锁的机制有所不同。
- 同步实例方法:
synchronized public void instanceMethod() {}的锁是当前对象实例(即this)。 - 同步静态方法:
synchronized public static void staticMethod() {}的锁是当前类的Class对象(MyClass.class)。
示例:静态方法的同步
public class StaticMethodSync {
public static synchronized void staticMethod() {
System.out.println(Thread.currentThread().getName() + " is in staticMethod.");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " is leaving staticMethod.");
}
public static void main(String[] args) {
new Thread(() -> StaticMethodSync.staticMethod(), "Thread-A").start();
new Thread(() -> StaticMethodSync.staticMethod(), "Thread-B").start();
}
}
输出结果分析:
Thread-A is in staticMethod.
// (等待 2 秒)
Thread-A is leaving staticMethod.
Thread-B is in staticMethod.
// (再等待 2 秒)
Thread-B is leaving staticMethod.
为什么是顺序执行?
因为两个线程调用的都是 StaticMethodSync 这个类的静态同步方法,它们竞争的是同一个锁——StaticMethodSync.class 对象,一个线程获取到锁后,另一个线程必须等待。
static 代码块
static 代码块在类加载时执行,且只执行一次,在多线程环境下,类的加载过程(由类加载器 ClassLoader 负责)本身是线程安全的,JVM 内部机制确保了同一个类只会被加载一次,static 代码块的执行也具有天然的线程安全性。
示例:
public class StaticBlock {
static {
System.out.println("Static block is executed. Thread: " + Thread.currentThread().getName());
}
public static void main(String[] args) {
new Thread(() -> System.out.println("New thread running"), "T1").start();
System.out.println("Main thread running.");
}
}
输出可能为:
Static block is executed. Thread: main
Main thread running.
New thread running.
(线程启动的顺序可能略有不同,但 static 代码块只会在 main 方法开始前,由主线程触发执行一次。)
static 内部类
static 内部类(也称为嵌套类)与非静态内部类(成员内部类)的一个重要区别是:
- 非静态内部类:持有外部类的隐式引用(即
this),因此它可以访问外部类的所有成员(包括私有成员),创建非静态内部类实例时,必须先有一个外部类实例。 - static 内部类:不持有外部类的引用,它更像一个普通的顶层类,只是被嵌套在外部类内部,它可以访问外部类的
static成员,但不能访问非静态成员。
多线程应用:单例模式
static 内部类是实现单例模式的一种非常优雅和线程安全的方式,即 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() {
return SingletonHolder.INSTANCE;
}
}
为什么它是线程安全的?
- 懒加载:
Singleton类加载时,SingletonHolder不会被加载,只有当第一次调用getInstance()方法时,才会加载SingletonHolder类,并初始化INSTANCE,这实现了懒加载。 - 线程安全:
INSTANCE的初始化过程是由 JVM 的类加载机制保证的,JVM 规范中明确要求,类加载过程必须是线程安全的。INSTANCE的创建过程是原子的,不会出现多线程竞争问题。
总结与最佳实践
static 成员 |
多线程核心要点 | 最佳实践 |
|---|---|---|
static 变量 |
共享资源,是并发问题的重灾区。 | 最小化原则:尽量减少 static 变量的使用。 2. 线程安全:如果必须共享,使用 synchronized 或 java.util.concurrent.atomic 包下的原子类。 |
static 方法 |
只能访问 static 成员,与 synchronized 结合时,锁的是 Class 对象。 |
工具类方法:static 方法非常适合用于工具类(如 Collections),它们是无状态的(不依赖实例变量)。 2. 同步: static 方法操作共享数据,请使用 synchronized。 |
static 代码块 |
在类加载时执行一次,JVM 保证其线程安全。 | 适合执行只需要一次的初始化操作,例如加载驱动、读取配置文件等。 |
static 内部类 |
不持有外部类引用,是实现单例模式等延迟初始化场景的绝佳选择,利用了 JVM 类加载的线程安全性。 | 推荐用于实现线程安全的单例模式(Holder 模式),也常用于组织逻辑上相关的工具类,避免命名空间污染。 |
核心思想:
在多线程编程中,要时刻警惕共享状态。static 变量因为其全局共享的特性,天然就是共享状态,当你使用 static 时,就要立刻思考:“这个变量会被多个线程同时访问和修改吗?” 如果答案是肯定的,就必须采取同步措施来保证其正确性和一致性。
