线程安全与不可变性

更新于 2025-12-28

Jakob Jenkov 2014-07-18

竞态条件(Race conditions) 只有在多个线程访问同一资源,并且其中至少有一个线程对该资源进行写操作时才会发生。如果多个线程只是读取同一个资源,则不会出现竞态条件。

我们可以通过使线程间共享的对象不可变(immutable),从而确保这些对象永远不会被任何线程修改,进而实现线程安全。下面是一个示例:

public class ImmutableValue {
    private int value = 0;

    public ImmutableValue(int value) {
        this.value = value;
    }

    public int getValue() {
        return this.value;
    }
}

注意,ImmutableValue 实例的值是在构造函数中传入的,并且没有提供 setter 方法。一旦创建了 ImmutableValue 的实例,其值就无法再被更改——它是不可变的。不过,你可以通过 getValue() 方法来读取它的值。

如果你需要对 ImmutableValue 实例执行某些操作,可以通过返回一个包含操作结果的新实例来实现。以下是一个加法操作的示例:

public class ImmutableValue {
    private int value = 0;

    public ImmutableValue(int value) {
        this.value = value;
    }

    public int getValue() {
        return this.value;
    }

    public ImmutableValue add(int valueToAdd) {
        return new ImmutableValue(this.value + valueToAdd);
    }
}

注意,add() 方法返回的是一个新的 ImmutableValue 实例,而不是直接修改当前对象的值。


引用本身不是线程安全的!

需要特别注意的是:即使一个对象是不可变的(因此是线程安全的),指向该对象的引用本身却未必是线程安全的。请看下面这个例子:

public class Calculator {
    private ImmutableValue currentValue = null;

    public ImmutableValue getValue() {
        return currentValue;
    }

    public void setValue(ImmutableValue newValue) {
        this.currentValue = newValue;
    }

    public void add(int newValue) {
        this.currentValue = this.currentValue.add(newValue);
    }
}

Calculator 类持有一个 ImmutableValue 实例的引用。注意,通过 setValue()add() 方法都可以改变这个引用所指向的对象。因此,尽管 Calculator 内部使用的是不可变对象,但 Calculator 本身并不是不可变的,也就不是线程安全的

换句话说:ImmutableValue 类是线程安全的,但它的使用方式可能不是线程安全的。这一点在试图通过不可变性来实现线程安全时必须牢记。

要让 Calculator 类变成线程安全的,可以将 getValue()setValue()add() 方法声明为 synchronized。这样就能解决问题了。