【问题标题】:Immutable objects are thread safe, but why?不可变对象是线程安全的,但为什么呢?
【发布时间】:2012-02-15 23:57:14
【问题描述】:

例如,一个线程通过创建它的对象来创建和填充不可变类的引用变量,另一个线程在第一个线程完成之前启动并创建不可变类的另一个对象,不可变类不会使用是线程不安全的吗?

创建不可变对象也意味着所有字段都必须标记为 final。

如果引用 新创建的实例从一个线程传递到另一个线程 同步

他们是否想说其他线程可能会将引用变量重新指向不可变类的其他对象,这样线程将指向不同的对象而导致状态不一致?

【问题讨论】:

  • 没有。每个线程都在创建一个不可变对象的不同实例。我根本看不到线程风险。
  • +不,不可变模型可以包含一个非线程安全的方法(做一个期望运行同步的逻辑+这个方法不会改变不可变的内部状态型号)

标签: java


【解决方案1】:

实际上不可变对象总是线程安全的,但它的引用可能不是。

困惑??你不应该:-

回归基本: 线程安全仅仅意味着两个或多个线程必须在共享资源或对象上协同工作。它们不应覆盖任何其他线程所做的更改。

现在 String 是一个不可变的类,每当一个线程试图改变它时,它最终只会创建一个新对象。因此,即使是同一个线程也不能对原始对象进行任何更改,谈论另一个线程就像去 Sun 一样,但这里的问题是,通常我们使用相同的旧引用来指向新创建的对象。

当我们编写代码时,我们只使用引用来评估对象的任何变化。

声明 1: 字符串 str = "123"; // 最初字符串共享给两个线程

声明 2: str = str+“第一线程”; // 由线程一执行

声明 3: str=str+"第二线程"; // 由线程二执行

现在由于没有 synchronize、volatile 或 final 关键字告诉编译器跳过使用其智能进行优化(任何重新排序或缓存的东西),这段代码可以按以下方式运行。

  1. 加载 Statement2,所以 str = "123"+"FirstThread"
  2. 加载 Statement3,所以 str = "123"+"SecondThread"
  3. Store Statement3,所以 str = "123SecondThread"
  4. 存储 Statement2,所以 str = "123FirstThread"

最后是引用 str="123FirstThread" 中的值,如果我们假设幸运的是我们的 GC 线程正在休眠,那么我们的不可变对象仍然存在于我们的字符串池中,并且未被触及。

因此,不可变对象始终是线程安全的,但它们的引用可能不是。为了使它们的引用线程安全,我们可能需要从同步的块/方法中访问它们。

【讨论】:

  • 考虑一个集合,ArrayList of string,ArrayList l = list1; l =列表2;其中list1和list2之前已经创建,所以第一个语句l指向list1,第二个它指向list2,所以我猜ArrayList也是线程安全的,但它的引用不是线程安全的?......让我知道我在这里错过了什么
  • @Akki 不,ArrayList 不能被认为是线程安全的,除非我们使用同步之类的东西手动处理并发。您在这里考虑了两个引用,这就是为什么它看起来是线程安全的,这对于任何其他类都是如此。我们在这里讨论的是线程安全的不可变性,而 ArrayList 不是不可变的(除非您使用过 Collections.unmodifiableList)。
【解决方案2】:

除了已经发布的其他答案之外,不可变对象一旦创建,就无法进一步修改。因此它们本质上是只读的。

众所周知,只读的东西总是线程安全的。即使在数据库中,多个查询也可以同时读取相同的行,但是如果要修改某些内容,则需要排他锁。

【讨论】:

    【解决方案3】:

    不可变对象是线程安全的,但为什么呢?

    不可变对象是一旦构造就不再修改的对象。此外,如果不可变对象仅在构造完成后才可供其他线程访问,并且这是通过适当的同步完成的,则所有线程将看到该对象的相同有效状态。

    如果一个线程正在通过创建其对象来填充不可变类的引用变量,而第二次另一个线程在第一个线程完成并创建不可变类的另一个对象之前启动并创建不可变类的另一个对象,那么不可变类不会使用是线程不安全的吗?

    没有。是什么让你这么想的?对象的线程安全完全不受您对同一类的其他对象所做的影响。

    他们是否想说其他线程可能会将引用变量重新指向不可变类的其他对象,这样线程将指向不同的对象而导致状态不一致?

    他们试图说,每当您将某些内容从一个线程传递到另一个线程时,即使它只是对不可变对象的引用,您也需要同步线程。 (例如,如果您通过将引用从一个线程传递到另一个线程,将其存储在一个对象或静态字段中,则该对象或字段由多个线程访问,并且必须是线程安全的)

    【讨论】:

    • 请注意,关于“仅在构造后可访问”的部分可以通过仅使用 final 字段变得微不足道(并且由于对象是不可变的,所以没有理由不这样做)。如果我们避免一些明显的错误,java 内存模型会提供我们需要的所有guarantees
    • @Voo:我不认为它是万无一失的。构造函数完成时需要分配最终字段,但构造函数很有可能在分配所有最终字段之前将this存储到另一个线程可访问的对象中。
    • 是的,像someCollection.add(this) 和 co 之类的操作正是我们必须避免的明显错误(但我认为众所周知,从构造函数中泄漏它几乎总是由于其他原因而被破坏) .即使泄漏指令是构造函数中的 last 指令,保证 not 也适用(参见第 3 段)。
    • 有意思,感谢指出构造函数补全的特殊意义。
    【解决方案4】:

    线程安全是数据共享的安全,因为在您的代码中您根据对象所持有的数据做出决策,所以它的完整性和确定性行为至关重要。即

    假设我们有一个跨两个线程的共享布尔实例变量,它们将要执行具有以下逻辑的方法

    • 如果 flag 为 false,则打印“false”,然后将 flag 设置回 true。
    • 如果 flag 为 true,则打印“true”,然后将 flag 设置回 false。

    如果您在单个线程循环中连续运行,您将获得如下所示的确定性输出:

    假-真-假-真-假-真-假...

    但是,如果你用两个线程运行相同的代码,那么你的输出就不再是确定性的了,原因是线程 A 可以唤醒,读取标志,看到它是假的,但在它之前可以做任何事情,线程B唤醒并读取标志,这也是错误的!所以两者都会打印错误...这只是我能想到的一个有问题的场景...如您所见,这很糟糕。

    如果您取出方程式的更新,问题就消失了,因为您消除了与数据同步相关的所有风险。这就是为什么我们说不可变对象是线程安全的。

    值得注意的是,不可变对象并不总是解决方案,您可能需要在不同线程之间共享数据,在这种情况下,有许多技术超越了普通同步,并且可以对应用程序的性能产生很大影响,但这是一个完全不同的主题。

    不可变对象对于保证我们确定不需要更新的应用程序区域不更新非常重要,因此我们确定不会出现多线程问题

    您可能有兴趣看几本书:

    这是最受欢迎的:http://www.amazon.co.uk/Java-Concurrency-Practice-Brian-Goetz/dp/0321349601/ref=sr_1_1?ie=UTF8&qid=1329352696&sr=8-1

    但我个人更喜欢这个:http://www.amazon.co.uk/Concurrency-State-Models-Java-Programs/dp/0470093552/ref=sr_1_3?ie=UTF8&qid=1329352696&sr=8-3

    请注意,多线程可能是任何应用程序中最棘手的方面!

    【讨论】:

      【解决方案5】:

      不变性并不意味着线程安全。从某种意义上说,对不可变对象的引用可以更改,即使在创建之后也是如此。

      //No setters provided
      class ImmutableValue
      {
      
           private final int value = 0;
      
           public ImmutableValue(int value)
           {
                this.value = value;
           }
      
           public int getValue()
           {
                return value;
           }
      }
      
      public class ImmutableValueUser{
        private ImmutableValue currentValue = null;//currentValue reference can be changed even after the referred underlying ImmutableValue object has been constructed.
      
        public ImmutableValue getValue(){
          return currentValue;
        }
      
        public void setValue(ImmutableValue newValue){
          this.currentValue = newValue;
        }
      
      }
      

      【讨论】:

        【解决方案6】:

        两个线程不会创建同一个对象,所以没有问题。

        关于“可能有必要确保...”,他们的意思是,如果您不将所有字段都设为最终字段,则您必须自己确保正确的行为。

        【讨论】:

          猜你喜欢
          • 2013-12-31
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-11-20
          • 2013-10-11
          相关资源
          最近更新 更多