【问题标题】:Does immutability guarantee thread safety?不变性是否保证线程安全?
【发布时间】:2014-08-09 23:19:21
【问题描述】:

好吧,考虑下面给出的不可变类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 Aobj 视为null 并移动到同步块并创建对象。现在,由于 Java 内存模型 (JMM) 允许多个线程在对象初始化开始之后但结束之前观察对象。 因此,Thread B 可以看到对obj 的写入正在发生在写入Immutable 的字段之前。因此,Thread B 可以看到部分构造的 Immutable,它很可能处于无效状态,并且其状态可能会在以后意外更改。

这不是让Immutable 成为非线程安全的吗?


编辑
好的,在对 SO 进行了大量查找并彻底了解了一些 cmets 之后,我知道 您可以在线程之间安全地共享对不可变对象的引用 在构造对象后。此外,正如@Makoto 所提到的,通常需要将包含其引用的字段声明为 volatile 以确保可见性。此外,正如@PeterLawrey 所述,将对不可变对象的引用声明为final 使该字段为thread-safe

【问题讨论】:

  • 你可以写public class Sharable { public static final Immutable obj = new Immutable(); } 这样会更简单,线程安全。

标签: java multithreading thread-safety immutability


【解决方案1】:

因此,线程 B 可以看到对 objas 的写入发生在对不可变字段的写入之前。因此,线程 B 可以看到一个部分构造的 Immutable,它很可能处于无效状态,并且其状态可能会在以后意外更改。

在 Java 1.4 中,这是真的。在 Java 5.0 及更高版本中,final 字段在构造后是线程安全的。

【讨论】:

  • 谢谢彼得。那么,这意味着不可变对象在任何地方都不是天生的线程安全的吗?在这种情况下,它们确实需要声明为final 以使其成为线程安全的吗?
  • 如果您运行的是 Java 1.4,则可能会出现问题。注意:也不保证会有问题。但是你不应该运行这么旧的版本。使用当前版本,您的 Immutable 将是不可变且线程安全的。字段必须是final 才能获得此保证。
  • @mac 过于简单化了。使字段 final 不会使引用的对象不可变,也不会使不可变对象保证线程安全。但是,大多数开发人员使用线程安全的方式在线程之间传递数据,例如一个线程安全的集合,例如 BlockingQueue,使用它可以确保对象的线程安全视图,无论是否使用final
  • @Mac 如果你所拥有的一切都是不可变的——根据定义——你将拥有一个线程安全的程序(显然)——这也是一个非常无聊的程序。当我们混合不可变和可变数据时,问题就来了,在某种程度上,你需要做任何有趣的事情。即使涉及不可变数据,我们仍然会遇到线程安全问题。这并不意味着不变性是无用的——它允许我们限制在特定时间点我们必须担心这些问题的范围。
  • @Mac:你好像要this specification
【解决方案2】:

你在这里描述的是两个不同的东西。首先,Immutable 线程安全的,如果操作是针对 it 的实例进行的。

线程安全部分是确保内存不会被另一个线程意外覆盖。就使用Immutable 而言,您永远无法覆盖它的实例中包含的任何数据,因此您可以确信,在并发环境中,当您将Immutable 对象构造到线程运行时,它将是相同的。操纵它。

你所知道的是 double-checked locking 的一个损坏的实现。

你说得对,线程 A 和线程 B 可能会在实例设置之前践踏它,从而使对象 Immutable 的整个不变性完全没有意义。

我相信解决此问题的方法是在 obj 字段中使用 volatile 关键字,以便 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 的一个实例对其他人没有任何影响也是。

【讨论】:

  • 谢谢@Makoto。该错字已更正:)。我重复我在@Peter Lawrey 回答中提到的相同评论。那么,这意味着不可变对象在任何地方都不是天生的线程安全的吗?它们确实需要声明为finalvolatie 以使它们在这种情况下是线程安全的?
  • 再一次,您在这里所做的是对单例进行双重检查锁定。如果使用它的类没有以线程安全的方式使用它,那么关于 Immutable 是否真正不可变的争论就会被抛到脑后。如果将不可变对象分配给可变引用,则该引用仍然不是线程安全的。不要将不变性与线程安全混为一谈。
  • 我正在阅读您的回答并一次又一次地发表评论,但我仍然无法弄清楚为什么如果代码中的线程安全性被模糊地破坏了,为什么不可变对象被称为本质上是线程安全的。在什么上下文这个线程安全是为不可变对象讨论的..
  • 在我发布的代码中,Immutable 的对象尽管本质上是不可变的,但并未安全发布。它需要显式声明为 volatile。即使是可变对象也声明 volatile 会导致对象的安全发布。那么究竟是什么原因以及不可变对象被称为线程安全的呢?
  • @Mac “因为 java 允许对象在初始化开始之后但在结束之前可见”这就是不可变对象的最终字段给你的。引用 JCIP(第 3.5.2 节):“不可变对象……即使不使用同步来发布对象引用,也可以安全地访问。”如果您将final 修饰符从Sharable 的字段中移开,则可以看到字段本身处于不一致的状态,例如x 可以被观察为 0 然后稍后作为分配给它的任何 int。
猜你喜欢
  • 2013-03-01
  • 1970-01-01
  • 1970-01-01
  • 2018-12-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多