不可变性是否保证线程安全?

3

好的,考虑下面给出的不可变类Immutable

public final class Immutable
{
    final int x;
    final int y;
    public Immutable(int x,int y)
    {
        this.x = x;
        this.y = y;
    }
    //Setters
    public int getX()
    {
        return this.x;
    }
    public int getY()
    {
        return this.y;
    }
}

现在我正在一个类Sharable中创建一个Immutable对象,这个对象将被多个线程共享:

public class Sharable
{
    private static Immutable obj;
    public static Immutable getImmutableObject()
    {
        if (obj == null) --->(1)
        {
            synchronized(this)
            {
                if(obj == null)
                {
                    obj = new Immutable(20,30); ---> (2)
                }
            }
        }
        return obj; ---> (3)
    }
}
Thread A看到objnull,进入同步块并创建对象。由于Java内存模型(JMM)允许多个线程在初始化开始之后但尚未结束之前观察对象。因此,Thread B可以看到对obj的写操作发生在对Immutable字段的写操作之前。因此,Thread B可能会看到一个部分构建的Immutable,它可能处于无效状态,并且其状态可能会意外更改。

这难道不是使Immutable不安全吗?


编辑
好的,在Stack Overflow上进行了很多查找并阅读了一些评论后,我知道了在对象构造完成后,您可以安全地在线程之间共享对不可变对象的引用。 此外,正如@Makoto所提到的,通常需要将包含其引用的字段声明为易失性,以确保可见性。 此外,正如@PeterLawrey所述,将对不可变对象的引用声明为final会使该字段成为线程安全


4
您可以直接编写 public class Sharable { public static final Immutable obj = new Immutable(); } ,这样更简单且线程安全。 - Peter Lawrey
2个回答

8
因此,线程B可以看到对objas的写操作发生在Immutable字段的写操作之前。因此,线程B可能会看到部分构建的Immutable,它可能处于无效状态,并且其状态可能在以后意外更改。
在Java 1.4中,这是真实的。在Java 5.0及以上版本中,final字段在构建后是线程安全的。

1
@mac 这是一种过于简单化的说法。将一个字段设置为 final 并不意味着所引用的对象是不可变的,而拥有一个不可变的对象也不能保证线程安全。然而,大多数开发者使用线程安全的方式在线程之间传递数据,例如使用 BlockingQueue 这样的线程安全集合,这样使用无论是否使用 final 都能确保对象的线程安全视图。 - Peter Lawrey
1
@Mac 如果你所有的东西都是不可变的,那么根据定义,你将拥有一个线程安全的程序(显然),但也是一个非常无聊的程序。问题出现在我们混合使用不可变和可变数据时,这在某个层面上是必需的,才能做出任何有趣的事情。即使涉及不可变数据,我们仍然可能存在线程安全问题。这并不意味着不可变性是无用的——它允许我们限制我们在特定时间点需要担心这些问题的范围。 - Voo
完美的措辞@PeterLawrey ;).. 谢谢.. 接受您的答案。我在stackoverflow.com/a/14653945/2536255中对您的答案发表了评论。 您能否在那里澄清我的疑惑? - Mac
2
@Mac:你似乎在询问这个规范 - Holger
1
关于java.util.concurrent提供的扩展线程安全保证,@PeterLawrey上面提到的参考资料可以在此处的“内存一致性属性”部分找到:链接 - Asa
显示剩余8条评论

5
你所描述的是两件不同的事情。首先,如果对Immutable实例进行操作,那么它就是线程安全的。
线程安全的一部分在于确保内存不会被其他线程意外覆盖。使用Immutable时,您永远无法覆盖其中包含的任何数据,因此在并发环境中,可以确信一个Immutable对象在构造时和线程操作时是相同的。
你现在的实现是双重检查锁定的错误实现。
你说得没错,线程A和线程B可能会在设置之前破坏实例,从而使Immutable对象的整个不可变性完全无效。
我认为修复这个问题的方法是使用volatile关键字来修饰obj字段,这样Java(>1.5)将尊重单例的预期用途,并禁止线程覆盖obj的内容。
现在,仔细阅读后,似乎有点奇怪,你需要两个静态数据才能创建一个不可变的单例。这更适合使用工厂模式。
public class Sharable {
    private Sharable() {
    }

    public static Immutable getImmutableInstance(int a, int b) {
        return new Immutable(a, b);
    }
}

每次获取Immutable实例,它都是真正的不可变的 - 创建一个新的Immutable不会影响其他对象,使用Immutable实例也不会对其他任何对象产生影响。

2
在这里,你所做的是单例的双重检查锁定。如果使用Immutable的类没有以线程安全的方式使用它,则围绕是否Immutable真正不可变的辩论将被抛弃。如果将不可变对象分配给可变引用,则该引用仍然不是线程安全的。不要混淆不可变性和线程安全性。 - Makoto
我一遍又一遍地阅读了您的答案和评论,但仍然无法理解为什么如果不恰当使用代码,不可变对象的线程安全性会被破坏,为什么它们被称为固有的线程安全。在什么情况下讨论不可变对象的线程安全性? - Mac
@Mac 这里有一个微妙的区别。在上面发布的代码中,Immutable 实例将被安全地发布,但是 Sharable 的引用,即称为 obj 的引用,不会被安全地发布。实例与对实例的引用是不同的。在这里,Immutable 的实例将始终在线程之间以一致的状态显示(两个字段都将完全初始化),但是引用 obj 可能会以不一致的状态(null 或有效引用)显示。这样是否更有意义? - Grundlefleck
@Grundlefleck 感谢您对此事的阐述。如果我对发布的理解有误,请纠正我,它只是发布的对象,引用变量只是发布该对象的一种方式... 关于 Immutable 实例始终以一致状态出现... 我对此表示怀疑... 因为正如我在帖子中所说... 由于 Java 允许在初始化开始之后但尚未结束之前可见对象... 所以线程 B 可能会看到不一致状态的 Immutable 对象。 - Mac
1
@Mac "由于Java允许对象在初始化开始之后但尚未结束时可见"这就是不可变对象的最终字段所提供的。引用JCIP(第3.5.2节)的话:“不可变对象...即使在未使用同步将对象引用发布时也可以安全地访问。”如果从Sharable的字段中删除final修饰符,则可以看到字段本身处于不一致状态,例如可能会观察到x首先为0然后为分配给它的任何int。 - Grundlefleck
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接