Java:引用同步对象是否需要易失性/最终性?

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

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

public class SyncClass {

   private int field;

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

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

如果我需要引用该类的实例,在线程之间共享,我仍然需要声明该实例易失性或最终性,我是对的吗?如:

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();
    }
}        

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

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();
    }
}        

最终或易失性是强制性的,将访问同步到其自己的成员的事实并不免除确保在线程之间共享引用时要小心。是吗?MyClass

Java中易失性和同步之间的差异

1-引用的问题是关于同步和易失性作为替代方案,对于相同的字段/变量,我的问题是关于如何正确使用已经正确同步的类(即已选择同步),考虑调用方需要考虑的含义,可能在已经同步的类的引用上使用 volatile/final。

2-换句话说,引用的问题/答案是关于锁定/易失相同的对象,我的问题是:我如何确定不同的线程实际上看到相同的对象?在锁定/访问它之前。

当引用问题的第一个答案显式引用易失性引用时,它是关于一个没有同步的不可变对象。第二个答案将自己限制为基元类型。我确实发现它们很有用(见下文),但还不够完整,无法对我在这里给出的案例产生任何疑问。

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

考虑到,在多线程中,你不能“只是尝试一下”,你需要一个正确的理解并确保细节,因为竞争条件可以正确一千次,然后错得可怕一千次+1次。


答案 1

是的,你是对的。对变量的访问也必须是线程安全的。您可以通过创建它或 来执行此操作,也可以确保所有线程在同步块内再次访问该变量。如果您不这样做,则可能是一个线程已经“看到”了变量的新值,但另一个线程可能仍然“看到”,例如。finalvolatilenull

因此,关于您的示例,您有时可能会在线程访问变量时获得一个。但这可能只发生在具有多个缓存的多核计算机上。NullPointerExceptionmySharedObject

Java 内存模型

这里的重点是Java内存模型。它声明,只有当另一个线程的更新发生在所谓的“发生之前”关系中该状态的读取之前,该线程才能保证看到该线程的内存更新。可以使用 、 或 强制实施之前发生关系。如果不使用这些构造中的任何一个,则一个线程的变量赋值永远不会保证任何其他线程都可见。finalvolatilesynchronized

您可以将线程视为在概念上具有本地缓存,并且只要您不强制多个线程的缓存同步,线程就会读取并写入其本地缓存。这可能会导致两个线程在从同一字段读取时看到完全不同的值的情况。

请注意,还有一些其他方法可以强制显示内存更改,例如,使用静态初始值设定项。此外,新创建的线程始终可以看到其父线程的当前内存,而无需进一步同步。因此,您的示例甚至可能在没有任何同步的情况下工作,因为线程的创建以某种方式强制在字段初始化后发生。然而,依赖这样一个微妙的事实是非常有风险的,如果你以后重构你的代码而没有考虑到这些细节,很容易被破坏。有关发生之前关系的更多详细信息在 Java 语言规范中进行了描述(但难以理解)。


答案 2

如果我需要对该类的实例进行重新验证,并在线程之间共享,我仍然需要声明该实例易失性或最终状态,我是对的吗?

是的,你是对的。在本例中,您有两个共享变量:

private int field

private SyncClass mySharedObject

由于您定义了对 a 的任何引用的方式,将为您提供该的最新值。SyncClassSyncClassSyncClass

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