杰瑞科技汇

Java static变量在多线程下会存在线程安全问题吗?

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++ 这个操作,在底层其实包含三个步骤:

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

如果多个线程同时执行这个操作,就可能发生 竞态条件

竞态条件示例:

假设 count 的初始值为 0,两个线程 A 和 B 同时调用 increment()

  1. 线程 A 读取 count 的值(0)。
  2. 线程 B 也读取 count 的值(0)。
  3. 线程 A 将值加 1,得到 1,并写回主内存。count 现在是 1。
  4. 线程 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;
    }
}

注意:对于静态方法,锁的对象是 类名.classSharedCounter.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 变量解决方案完全一致(使用 synchronizedAtomic 类)。

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

为什么它是线程安全的?

  1. 懒加载Singleton 类被加载时,SingletonHolder 类不会被加载,只有当 getInstance() 方法第一次被调用时,JVM 才会加载 SingletonHolder 并初始化 INSTANCE
  2. 类加载的原子性:类的加载和初始化过程在 JVM 中是严格同步的,由 Class 对象作为锁。INSTANCE 的初始化过程是原子的,只有一个线程能执行它。
  3. 内存可见性:JVM 保证在类初始化完成前,其他线程是无法使用它的。

static 成员类型 线程安全性分析 解决方案
static 变量 不安全,所有线程共享一份拷贝,并发修改会导致竞态条件。 synchronized 关键字(同步方法或代码块)
java.util.concurrent.atomic 原子类(推荐)
使用不可变对象
static 方法 取决于实现
- 如果是无状态方法(只操作局部变量),则安全
- 如果有状态(读写 static 变量),则不安全
同上(针对有状态方法)。
static 代码块 安全,类加载过程由 JVM 保证线程安全,只会执行一次。 无需额外处理。
static 内部类 类本身的安全性同普通类,但其一个重要应用——静态内部单例模式,是线程安全的,因为它利用了类加载的原子性。 无需额外处理(用于单例模式时)。

核心思想static 成员的共享性是其线程风险的根源,判断其是否线程安全,关键在于分析它是否被多个线程并发地、非原子地修改,如果存在这种风险,就必须使用同步机制(如 synchronized)或并发工具(如 Atomic 类)来保证其正确性。

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