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

Java:引用同步对象是否需要volatile/final?,java,multithreading,synchronized,volatile,Java,Multithreading,Synchronized,Volatile,这似乎是一个相当基本的问题,但我找不到一个明确的确认 假设我有一个类本身正确同步: public class SyncClass { private int field; public synchronized void doSomething() { field = field * 2; } public synchronized void doSomethingElse() { field = field * 3; } } 如

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

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

public class SyncClass {

   private int field;

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

   public synchronized void doSomethingElse() {
       field = field * 3;
   }
}
如果我需要对该类的一个实例进行引用,在线程之间共享,我仍然需要声明该实例为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不能是final,因为它的实例化取决于一些事先未知的其他条件(与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
同步对其自身成员的访问,这一事实并不免除确保引用在线程之间共享的注意。是这样吗

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

2-换句话说,所提到的问题/答案是关于锁定/释放同一对象的,我的问题是:如何确保不同线程实际看到同一对象?在锁定/访问它之前

当提到的问题的第一个答案明确提到一个易失性引用时,它是关于一个没有同步的不可变对象的。第二个答案仅限于基本类型。 我确实发现它们很有用(见下文),但还不够完整,无法对我在这里给出的案例释疑

3-参考答案是对一个非常开放的问题的非常抽象和学术性的解释,完全没有代码;正如我在导言中所说的,我需要对涉及一个特定的、虽然很常见的问题的实际代码进行明确的确认。当然,它们是相关的,但就像教科书与特定问题相关一样。(实际上,我在开始这个问题之前读过它,发现它很有用,但我仍然需要讨论一个具体的应用。)如果教科书解决了人们在应用它们时可能遇到的所有问题/疑问,我们可能根本不需要stackoverflow


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

是的,您是对的。必须使对变量的访问也是线程安全的。您可以通过使其成为
final
volatile
,或者确保所有线程在同步块内再次访问该变量。例如,如果您不这样做,可能是一个线程已经“看到”了变量的新值,但另一个线程可能仍然“看到”
null

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

Java内存模型

这里的要点是Java内存模型。它声明,如果一个线程的内存更新发生在所谓的“发生在之前”关系中读取该状态之前,则只能保证该线程看到另一个线程的内存更新。可以使用
final
volatile
synchronized
来强制执行“发生在”关系。如果您不使用这些构造中的任何一个,那么一个线程的变量赋值永远不能保证被任何其他线程看到

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


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

不强制使用它们中的任何一个,但是如果您想编写正确的多线程代码,您应该了解它们

决赛

final
表示不能再次重新初始化该变量,因此

final SyncClass mySharedObject = new SyncClass();
您不能在下面这样的代码的其他部分中再次初始化
mySharedObject

   mySharedObject = new SyncClass(); // throws compiler error
即使不能将
mySharedObject
引用重新分配给其他对象,也可以通过调用其上的方法来更新其状态(字段计数器变量),因为
field
不是final

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

同步

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

因此,在您的情况下,如果线程1尝试执行
mySharedObject.doSomething()
,它将获得
mySharedObject
上的锁,线程2必须等待线程1释放同一对象上的锁才能执行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(); } }
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.
}