杰瑞科技汇

Java多线程中static变量线程安全吗?

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

Java多线程中static变量线程安全吗?-图1
(图片来源网络,侵删)

static 变量 (静态变量)

static 变量也叫类变量,它的核心特征是:被类的所有实例共享

关键特性

  • 内存位置static 变量存储在方法区(在 JDK 7 及之前是永久代,JDK 8 及之后是元空间)的运行时常量池中,而不是在堆中的对象实例里。
  • 生命周期:它随着类的加载而创建,随着类的卸载而销毁,它的生命周期比任何实例对象都长。
  • 访问方式:可以通过 类名.变量名实例名.变量名 的方式访问,但推荐使用前者,因为它更清晰地表明这是一个静态成员。

多线程影响:共享与并发问题

由于 static 变量是所有线程共享的,当多个线程同时读写同一个 static 变量时,就会引发并发问题,最典型的就是竞态条件

示例:一个不安全的计数器

假设我们有一个全局的计数器,多个线程同时对其进行递增操作。

Java多线程中static变量线程安全吗?-图2
(图片来源网络,侵删)
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 中,它通常被分解为三个独立的步骤:

  1. 读取:从主内存中读取 count 的值到线程的工作内存。
  2. 修改:将工作内存中的值加 1。
  3. 写入:将修改后的值写回到主内存。

在多线程环境下,这个顺序可能会被打乱,导致数据不一致。

  • 场景
    • 线程 A 读取 count (值为 0)。
    • 线程 B 也读取 count (值也为 0)。
    • 线程 A 将值加 1,写回主内存,count 变为 1。
    • 线程 B 也将其值加 1(基于它读取的 0),写回主内存,count 也变为 1。
  • 结果:两次 操作,但 count 只增加了 1,这就是典型的丢失更新问题。

解决方案:保证线程安全

为了解决 static 变量的并发问题,我们需要确保对它的操作是原子性的,有几种常见的方法:

Java多线程中static变量线程安全吗?-图3
(图片来源网络,侵删)

使用 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;
    }
}

为什么它是线程安全的?

  1. 懒加载Singleton 类加载时,SingletonHolder 不会被加载,只有当第一次调用 getInstance() 方法时,才会加载 SingletonHolder 类,并初始化 INSTANCE,这实现了懒加载。
  2. 线程安全INSTANCE 的初始化过程是由 JVM 的类加载机制保证的,JVM 规范中明确要求,类加载过程必须是线程安全的。INSTANCE 的创建过程是原子的,不会出现多线程竞争问题。

总结与最佳实践

static 成员 多线程核心要点 最佳实践
static 变量 共享资源,是并发问题的重灾区。 最小化原则:尽量减少 static 变量的使用。
2. 线程安全:如果必须共享,使用 synchronizedjava.util.concurrent.atomic 包下的原子类。
static 方法 只能访问 static 成员,与 synchronized 结合时,锁的是 Class 对象。 工具类方法static 方法非常适合用于工具类(如 Collections),它们是无状态的(不依赖实例变量)。
2. 同步static 方法操作共享数据,请使用 synchronized
static 代码块 在类加载时执行一次,JVM 保证其线程安全。 适合执行只需要一次的初始化操作,例如加载驱动、读取配置文件等。
static 内部类 不持有外部类引用,是实现单例模式等延迟初始化场景的绝佳选择,利用了 JVM 类加载的线程安全性。 推荐用于实现线程安全的单例模式(Holder 模式),也常用于组织逻辑上相关的工具类,避免命名空间污染。

核心思想

在多线程编程中,要时刻警惕共享状态static 变量因为其全局共享的特性,天然就是共享状态,当你使用 static 时,就要立刻思考:“这个变量会被多个线程同时访问和修改吗?” 如果答案是肯定的,就必须采取同步措施来保证其正确性和一致性。

分享:
扫描分享到社交APP
上一篇
下一篇