同步以确保另一个线程可以看到对不可变对象的引用

时间:2011-07-06 15:33:33

标签: java multithreading concurrency immutability final

我正在研究this以了解新JMM(5以后)中最终字段的行为。这个概念很清楚:在正确构造对象之后,保证初始化的最终字段对所有线程的可见性。

但是在本节的最后,我读到了这个,这让我感到困惑:

  

现在,说完所有这些,如果在一个线程构造一个不可变对象(即一个只包含最终字段的对象)之后,你想要确保所有其他线程都正确地看到它,你仍然通常需要使用同步。例如,没有其他方法可以确保第二个线程可以看到对不可变对象的引用。

这是否意味着虽然单个最终字段(组成不可变对象)没有同步(例如,此处可见性)问题。但是,首次在线程中创建的不可变对象本身可能在其他线程中不可见(正确创建)?

如果是这样,虽然我们可以跨线程共享初始化的不可变对象而没有任何线程不安全的担忧,但是在创建时,他们需要“特别关注”线程安全,就像其他可变项一样?

4 个答案:

答案 0 :(得分:5)

section 17.5 of the JLS中定义的最终字段的语义保证:

  

在该对象完全初始化之后只能看到对象引用的线程可以保证看到该对象的最终字段的正确初始化值。

换句话说,它表示如果线程看到一个完全初始化的对象,那么就可以保证正确看到它的最终字段初始化。

但是,无法保证对象对给定线程可见。这是一个不同的问题。

如果您不使用某种同步来发布对象的引用,那么另一个线程可能永远无法看到对它的引用。

请考虑以下代码:

final class A {
  private final int x;
  A(int x) { this.x = x; }
  public getX() { return x; }
}

class Main {
  static volatile A a1 = null;
  static A a2 = null;
  public static void main(String[] args) {
    new Thread(new Runnable() { void run() { try {
      while (a1 == null) Thread.sleep(50);
      System.out.println(a1.getX()); } catch (Throwable t) {}
    }}).start()
    new Thread(new Runnable() { void run() { try {
      while (a2 == null) Thread.sleep(50);
      System.out.println(a2.getX()); } catch (Throwable t) {}
    }}).start()
    a1 = new A(1); a2 = new A(1);
  }
}

请注意,a1字段是易变的。这样可以确保最终对该字段的写入对于稍后读取它的所有线程都是可见的。字段a2不是volatile(因此,一个线程对此字段的写入可能永远不会被其他线程注意到。)

在这段代码中,我们可以确定线程1将完成执行(也就是说,它会看到a1 != null。但是,线程2可能会停止,因为它永远不会看到写入字段a2,因为它不易变。

答案 1 :(得分:3)

  

您希望确保所有其他线程都能正确看到它,您通常仍需要使用同步。例如,没有其他方法可以确保第二个线程可以看到对不可变对象的引用。

对于在句子空间中将通常转换为而不是其他方式的文本,我会有点怀疑。事实上,这是真的取决于“使用同步”究竟是什么意思。

Java语言规范的相关部分是:

  

发生在之前的关系可以订购两个动作。如果一个动作发生在另一个动作之前,那么第一个动作在第二个动作之前是可见的并且在第二个动作之前被命令。

  

更具体地说,如果两个动作共享一个发生在之前的关系,那么它们不一定必须按照那个顺序发生在它们不与之共享的任何代码中。例如,在另一个线程中读取的数据争用中的一个线程中的写入可能看起来与这些读取无关。

以前发生的事情可以通过多种方式确定:

  

如果我们有两个动作x和y,我们写hb(x,y)来表示x发生在y之前。

     
      
  • 如果x和y是同一个线程的动作,并且x在程序顺序中位于y之前,那么hb(x,y)。
  •   
  • 从对象的构造函数末尾到该对象的终结符(第12.6节)的开头有一个发生前的边缘。
  •   
  • 如果动作x与后续动作y同步,那么我们也有hb(x,y)。
  •   
  • 如果是hb(x,y)和hb(y,z),那么hb(x,z)。
  •   

,其中

  

同步动作引发与动作的同步关系,定义如下:

     
      
  • 监视器m上的解锁操作与m上的所有后续锁定操作同步(后续操作根据同步顺序定义)。
  •   
  • 对volatile变量(第8.3.1.4节)的写入v与任何线程的v的所有后续读取同步(其中后续根据同步顺序定义)。
  •   
  • 启动线程的操作与其启动的线程中的第一个操作同步。
  •   
  • 向每个变量写入默认值(零,false或null)与每个线程中的第一个操作同步。虽然在分配包含变量的对象之前将默认值写入变量似乎有点奇怪,但从概念上讲,每个对象都是在程序开始时使用其默认初始化值创建的。
  •   
  • 线程T1中的最终操作与另一个检测到T1已终止的线程T2中的任何操作同步。 T2可以通过调用T1.isAlive()或T1.join()来完成此任务。
  •   
  • 如果线程T1中断线程T2,则T1的中断与任何其他线程(包括T2)确定T2已被中断的任何点同步(通过抛出InterruptedException或通过调用Thread.interrupted或Thread.isInterrupted)
  •   

通过使字段成为最终字段,确保在构造函数完成之前发生它们的赋值。您仍需要确保的是构造函数的完成 - 在访问对象之前发生。如果该访问发生在另一个线程中,则需要使用上面显示的6种方法中的任何一种建立 synchronize-with 。通常使用的是:

  1. 初始化完成后启动读取线程。实际上,在启动其他线程之前初始化主线程中的对象可以很好地实现这一点。
  2. 声明其他线程用来访问volatile对象的字段。例如:

    class CacheHolder {
        private static volatile Cache cache;
    
        public static Cache instance() {
            if (cache == null) {
                // note that several threads may get here at the same time,
                // in which case several caches will be constructed.
                cache = new Cache();
            }
            return cache;
        }
    }
    
  3. 同步块中的初始分配和字段读取。

    class CacheHolder {
        private static Cache cache;
    
        public synchronized static Cache instance() {
            if (cache == null) {
                cache = new Cache();
            }
            return cache;
        }
    }
    

答案 2 :(得分:1)

创建所有字段final将确保它们正确地发布到其他线程。该评论可能涉及以下情况:

private myField;

public void createSomething()
{
    myField = new MyImmutableClass();
}

在这种情况下,您仍然需要围绕对myField的任何访问进行适当的同步,否则其他线程可能永远不会看到新创建的对象。

答案 3 :(得分:0)

我相信作者提到了非final字段引用不可变对象时的情况。如果引用本身是final,则不需要额外的同步 另外考虑的是,上面仅适用于在对象的构造函数内初始化的对象字段。