Java: 引用同步对象是否需要volatile / final?

Java: Is volatile / final required for reference to synchronized object?

这似乎是一个非常基本的问题,但我找不到明确的确认。

假设我有一个 class 本身正确同步:

public class SyncClass {

   private int field;

   public synchronized void doSomething() {
       field = field * 2;
   }

   public synchronized void doSomethingElse() {
       field = field * 3;
   }
}

如果我需要 reference 到 class 的实例,在线程之间共享,我仍然需要声明该实例volatile 或 final,我说的对吗?如:

public class MainClass { // previously OuterClass

    public static void main(String [ ] args) {

        final SyncClass mySharedObject = new SyncClass();

        new Thread(new Runnable() {
            public void run() {
                mySharedObject.doSomething();
            }
       }).start();

       new Thread(new Runnable() {
            public void run() {
                mySharedObject.doSomethingElse();
            }
       }).start();
    }
}        

或者,如果 mySharedObject 不能是最终的,因为它的实例化取决于一些其他条件(与 GUI 的交互、来自套接字的信息等),事先不知道:

public class MainClass { // previously OuterClass

    public static void main(String [ ] args) {

        volatile SyncClass mySharedObject;

        Thread initThread = new Thread(new Runnable() {
            public void run() {

            // just to represent that there are cases in which
            //   mySharedObject cannot be final
            // [...]
            // interaction with GUI, info from socket, etc.
            // on which instantation of mySharedObject depends

            if(whateverInfo)
                mySharedObject = new SyncClass();
            else
               mySharedObject = new SyncClass() {
                   public void someOtherThing() {
                     // ...
                   }
               }
            }
       });

       initThread.start();

       // This guarantees mySharedObject has been instantied in the
       //  past, but that still happened in ANOTHER thread
       initThread.join();

       new Thread(new Runnable() {
            public void run() {
                mySharedObject.doSomething();
            }
       }).start();

       new Thread(new Runnable() {
            public void run() {
                mySharedObject.doSomethingElse();
            }
       }).start();
    }
}        

Final 或 volatile 是强制性的,事实上 MyClass 同步对其成员的访问,不免除确保引用共享的注意线程之间。是吗?

的区别Difference between volatile and synchronized in Java

1- 提到的问题是关于 synchronized 和 volatile 作为替代方案,对于相同的 field/variable,我的问题是关于如何正确使用已经正确同步的 class(即已选择 synchronized ),考虑到调用者需要考虑的影响,可能在已经同步的 class.

的引用上使用 volatile/final

2- 换句话说,引用的 question/answers 是关于 locking/volatile 相同的对象,我的问题是:我如何确定不同的线程实际上看到相同的对象?在 locking/accessing 它之前。

当引用问题的第一个答案明确引用易失性引用时,它是关于一个不可变对象没有同步。第二个答案仅限于原始类型。 我确实发现它们很有用(见下文),但还不够完整,不足以消除对我在这里给出的案例的任何疑虑。

3- 所提到的答案是对一个非常开放的问题的非常抽象和学术的解释,根本没有代码;正如我在介绍中所说,我需要明确确认引用特定但很常见的问题的实际代码。当然,它们是相关的,但就像教科书与特定问题相关一样。 (我实际上在打开这个问题之前阅读了它,发现它很有用,但我仍然需要讨论一个特定的应用程序。)如果教科书解决了所有 problems/doubts 人可能已经应用它们的问题,我们可能不需要 Whosebug全部.

考虑到,在多线程中,您不能 "just try it out",您需要正确理解并确定细节,因为竞争条件可以正确一千次,然后千次+ 1 次出现可怕的错误。

是的,你是对的。有必要使对变量的访问也是线程安全的。您可以通过使其成为 finalvolatile 来实现,或者确保所有线程在同步块内再次访问该变量。如果您不这样做,可能是一个线程 'sees' 已经是变量的新值,但另一个线程可能仍然是 'see' null,例如 [=19] =]

因此,对于您的示例,当线程访问 mySharedObject 变量时,您有时会得到 NullPointerException。但这可能只发生在具有多个缓存的多核机器上。

Java内存模型

这里的要点是 Java 内存模型。它指出,如果更新发生在所谓的 happens-before 关系 之前读取该状态之前,则线程只能保证看到另一个线程的内存更新。可以通过使用 finalvolatilesynchronized 来强制执行先发生关系。如果您不使用这些构造中的任何一个,则一个线程的变量赋值永远不会保证对任何其他线程可见。

你可以认为线程在概念上有本地缓存​​,只要你不强制同步多个线程的缓存,一个线程就只是读取和写入它的本地缓存。这可能会导致两个线程在读取同一字段时看到完全不同的值。

请注意,还有一些其他方法可以强制实现内存更改的可见性,例如,使用静态初始化程序。此外,新创建的线程总是看到其父线程的当前内存,而无需进一步同步。所以你的例子甚至可以在没有任何同步的情况下工作,因为你的线程的创建以某种方式强制发生在字段初始化之后。 但是 依赖这样一个微妙的事实是非常危险的,如果您稍后在没有考虑这些细节的情况下重构您的代码,很容易崩溃。 Java Language Specification 中描述了(但难以理解)关于 happens-before 关系的更多细节。

使用它们并不是强制性的,但如果您想编写适当的多线程代码,您应该了解它们。

决赛

final 意味着你不能再次重新初始化那个变量,所以当你说

final SyncClass mySharedObject = new SyncClass();

您不能在代码的其他部分再次对 mySharedObject 进行初始化,如下所示

   mySharedObject = new SyncClass(); // throws compiler error

即使您不能将 mySharedObject 引用重新分配给其他对象,您仍然可以通过调用其上的方法来更新它的状态(字段计数器变量),因为 field 不是最终的。

同步和 volatile 只是构造,以确保一个线程对共享可变对象的任何更改(在本例中更新 field 计数器)对所有其他线程都是可见的。

同步

synchronized 方法意味着任何试图调用该方法的线程都应该获取定义该方法的对象的锁。

所以在你的情况下,如果线程 1 正在尝试执行 mySharedObject.doSomething(),它将获取 mySharedObject 上的锁,而线程 2 必须等到线程 1 释放对 mySharedObject 的锁同一对象能够执行 mySharedObject.doSomethingElse() 即在任何给定时间点使用同步,只有一个线程会更新对象的状态。在该方法的最后,就在释放锁之前,thread-1 所做的所有更改都被刷新到主内存,以便 thread-2 可以在最近的状态下工作。

易挥发

另一方面,

volatile 可确保 read/write 对所有线程的可见性。对 volatile 变量的任何读写总是刷新到主内存。

如果 SyncClass 中的 field 变量是易变的,则线程 1 对 field++ 的任何更新对线程 2 都是可见的,但我不确定它如何应用于对象引用。

由于volatile只保证可见性,不保证原子性,有可能thread-1和thread-2都尝试同时更新field计数器,最终更新的值可能不正确。

If I need to have a refence to an instance of that class, shared between threads, I do still need to declare that instance volatile or final, am I right?

是的,你是对的。在这种情况下,您有两个共享变量:

private int field

private SyncClass mySharedObject

由于您定义 SyncClass 的方式,任何对 SyncClass 的引用都会为您提供该 SyncClass 的最新值。

如果您没有正确同步对 mySharedObject 的访问(非最终、非易失性)字段并且您更改了 mySharedObject 的值,您可能会得到 mySharedObject 已经过时了。

这完全取决于如何共享此变量的上下文。

这是一个简单的例子,它很好:

class SimpleExample {
    private String myData;

    public void doSomething() {
        myData = "7";

        new Thread(() -> {
            // REQUIRED to print "7"
            // because Thread#start
            // mandates happens-before ordering.
            System.out.println(myData);
        }).start();
    }
}

您给出的示例可能属于这种情况。 17.4.5:

  • If x and y are actions of the same thread and x comes before y in program order, then hb(x, y).

  • A call to start() on a thread happens-before any actions in the started thread.

换句话说,如果对 mySharedObject 的赋值发生在启动新线程的同一个线程上,则无论同步如何,新线程都必须查看该赋值。

但是,如果您期望 init 可以在与调用 doSomething 的线程不同的线程上调用,那么您可能会遇到竞争条件。

public static void main(String[] args) {
    final OuterClass myOuter = new OuterClass();

    Thread t1 = new Thread( () -> myOuter.init(true)    );
    Thread t2 = new Thread( () -> myOuter.doSomething() );

    t1.start(); // Does t1#run happen before t2#run? No guarantee.
    t2.start(); // t2#run could throw NullPointerException.
}

SyncClass 具有同步方法这一事实与 mySharedObject 引用的保证状态完全无关。读取该引用是在同步块之外执行的。

如有疑问,请使用 finalvolatile。哪个合适。

这里要记住两点以便理解:

  1. 为您的参考变量竞速与成员字段的竞速在概念上没有区别。
  2. 共享引用变量需要谨慎处理safe publication