简体   繁体   English

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

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

This seems a pretty basic issue, but I cannot find a clear confirmation. 这似乎是一个非常基本的问题,但我找不到明确的确认。

Let's say I have a class properly synchronized in itself: 假设我有一个类本身正确同步:

public class SyncClass {

   private int field;

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

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

If I need to have a reference to an instance of that class, shared between threads, I do still need to declare that instance volatile or final , am I right? 如果我需要引用该类的实例,在线程之间共享, 我仍然需要声明该实例是volatile还是final ,我是对的吗? As in: 如:

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

Or, if mySharedObject cannot be final, because its instantiation depends on some other conditions (interaction with GUI, info from socket, etc.), not known beforehand: 或者,如果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 or volatile are mandatory , the fact that MyClass synchronizes the access to its own members, does NOT exempt to take care in ensuring that the reference is shared among threads. 最终或volatile是强制性的, MyClass同步对其自己成员的访问这一事实并不豁免确保在线程之间共享引用。 Is that right? 是对的吗?

Differences with Difference between volatile and synchronized in Java Java中的volatile和synchronized之间的 差异的 差异

1- The referred question is about synchronized and volatile as alternatives, for the same field/variable, my question is about how to correctly use an already properly synchronized class (ie synchronized has been choosen), considering implications needed to be considered by the caller, possibly using volatile/final on a reference of an already synchronized class. 1-引用的问题是关于同步和易失性的替代方案,对于相同的字段/变量,我的问题是关于如何正确使用已经正确同步的类(即已选择同步),考虑到调用者需要考虑的含义,可能在已经同步的类的引用上使用volatile / final。

2- In other words, the referred question/answers are about locking/volatile THE SAME OBJECT, my question is: how can I be sure different threads actually SEE THE SAME OBJECT? 2-换句话说,所引用的问题/答案是关于锁定/易变的同一个对象,我的问题是:我怎样才能确定不同的线程实际上看到相同的对象? BEFORE locking/accessing it. 在锁定/访问它之前。

When the first answer of referred question refers explicitly to a volatile reference, it's about an immutable object without synchronization . 当引用问题的第一个答案明确指向易失性引用时,它是关于不同步不可变对象。 The second answer limits itself to primitive types. 第二个答案仅限于原始类型。 I DID find them useful (see below), but not complete enough to shed any doubts on the case I'm giving here. 我发现它们很有用(见下文),但还不够完整,不足以对我在这里给出的案件表示怀疑。

3- The referred answers are very abstract and scholarly explanations to a very open question, with quite no code at all; 3-所提到的答案对于一个非常开放的问题是非常抽象和学术性的解释,完全没有代码; as I stated in the introduction I need to a clear confirmation to actual code referring a specific, while quite common, issue. 正如我在介绍中所说,我需要明确确认实际代码,引用一个特定的,虽然很常见的问题。 They're related, sure, but just as a text book is related to a specific problem. 当然,它们是相关的,但正如教科书与特定问题有关。 (I actually read it before opening this question, and find it useful, yet I still need to discuss a specific application.) If text books resolved all problems/doubts people may have applying them, we probably wouldn't need stackoverflow at all. (我实际上是在打开这个问题之前阅读它,并发现它很有用,但我仍然需要讨论一个特定的应用程序。)如果教科书解决了人们可能已经应用它们的所有问题/疑问,我们可能根本不需要stackoverflow。

Consider that, in multithreading, you cannot "just try it out", you need a proper understanding and be sure of details, because race conditions can go right a thousand times and then go horribly wrong the thousand + 1 time. 考虑到,在多线程中,你不能“只是尝试一下”,你需要一个正确的理解并确保细节,因为竞争条件可能会发生一千次,然后在一千+一次时出现可怕的错误。

Yes you are right. 是的,你是对的。 It is necessary that you make access to the variable also thread-safe. 您还必须访问变量也是线程安全的。 You can do that either by making it final or volatile , or you ensure that all threads access that variable again inside a synchronous block. 您可以通过使其成为finalvolatile来执行此操作,或者确保所有线程在同步块内再次访问该变量。 If you wouldn't do that, it might be that one thread 'sees' already the new value of the variable, but the other thread might still 'see' null , for example. 如果你不这样做,它可能是一个线程“看到”已变量的新值,但其他线程可能还是“看” null ,例如。

So regarding your example, you could sometimes get a NullPointerException when a thread accesses the mySharedObject variable. 因此,关于您的示例,当线程访问mySharedObject变量时,有时可能会出现NullPointerException But this might only happen on multi-core machines with multiple caches. 但这可能只发生在具有多个缓存的多核机器上。

Java Memory Model Java内存模型

The main point here is the Java Memory Model. 这里的要点是Java内存模型。 It states a thread is only guaranteed to see a memory update of another thread if that update happens before the read of that state in the so-called happens-before relation . 它声明一个线程只能保证看到另一个线程的内存更新,如果该更新在所谓的发生在之前的关系中读取该状态之前发生 The happens-before relation can be enforced by using final , volatile , or synchronized . 可以通过使用finalvolatilesynchronized来强制执行之前发生的关系。 If you do not use any of these constructs a variable assignment by one thread is never guaranteed to be visible by any other thread. 如果不使用任何这些结构,则任何其他线程都不会保证一个线程的变量赋值可见。

You can think of threads to conceptually have local caches and as long as you do not enforce that caches of multiple threads are synchronized, a thread just reads and writes to its local cache. 您可以认为线程在概念上具有本地缓存​​,并且只要您不强制执行多线程的缓存同步,线程就会读取和写入其本地缓存。 This might lead to the situation where two threads see completely different values when reading from the same field. 这可能导致两个线程在从同一字段读取时看到完全不同的值的情况。

Note that there are some additional ways to enforce visibility of memory changes, for example, using static initializers. 请注意,还有一些其他方法可以强制实现内存更改的可见性,例如,使用静态初始化程序。 In addition, a newly created thread always sees the current memory of its parent thread, without further synchronization. 此外,新创建的线程始终可以看到其父线程的当前内存,而无需进一步同步。 So your example might even work without any synchronization, because the creation of your threads are somehow enforced to happen after the field has been initialized. 因此,您的示例甚至可以在没有任何同步的情况下工作,因为在初始化字段之后,以某种方式强制创建线程。 However relying on such a subtle fact is very risky and can easily break if you later refactor your code without having that detail in mind. 然而,依赖于这样一个微妙的事实是非常危险的,并且如果您稍后重构代码而没有记住这些细节,则很容易破坏。 Further details about the happens-before relation are described (but hard to understand) in the Java Language Specification . Java语言规范中描述了关于先发生关系的更多细节(但很难理解)。

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? 如果我需要对该类的实例进行refence,在线程之间共享,我仍然需要声明该实例是volatile还是final,我是对的吗?

Yes, you are right. 是的,你是对的。 In this case you have two shared variables: 在这种情况下,您有两个共享变量:

private int field

private SyncClass mySharedObject

Because of the way you have defined SyncClass any reference to a SyncClass will give you the most up to date value of that SyncClass . 由于您定义了SyncClass的方式,对SyncClass任何引用SyncClass将为您提供该SyncClass最新值。

If you don't synchronize the access to mySharedObject correctly (a non-final, non-volatile) field and you change the value of mySharedObject you may get a mySharedObject which is out of date. 如果您没有正确同步对mySharedObject的访问(非最终的,非易失性)字段并且您更改mySharedObject的值,则可能会得到一个过期的mySharedObject

This depends entirely on the context of how this variable is shared. 这完全取决于如何共享此变量的上下文。

Here is a simple example where it's fine: 这是一个简单的例子,它很好:

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

Your given examples may fall under this case. 您的例子可能属于这种情况。 17.4.5 : 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) . 如果xy是同一个线程的动作,并且x在程序顺序中出现在y之前,那么hb(x,y)

  • A call to start() on a thread happens-before any actions in the started thread. 在启动线程中的任何操作之前,对线程的start()调用发生

In other words if the assignment to mySharedObject takes place on the same thread that starts the new thread, the new thread is mandated to see the assignment regardless of synchronization. 换句话说,如果对mySharedObject的赋值发生在启动新线程的同一线程上,则新线程被强制要求查看赋值,而不管同步如何。

However, if you expect, for example, that init could be called on a thread that is different from the one that calls doSomething , then you may have a race condition. 但是,如果您希望,例如,可以在与调用doSomething的线程不同的线程上调用init ,那么您可能会遇到竞争条件。

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.
}

The fact that SyncClass has synchronized methods is completely irrelevant with respect to guaranteed state of the mySharedObject reference. SyncClass具有同步方法的事实与mySharedObject引用的保证状态完全无关。 Reading that reference is performed outside the synchronized block. 读取该引用是在同步块之外执行的。

When in doubt, use final or volatile . 如有疑问,请使用finalvolatile Whichever is appropriate. 哪个合适。

Two things to keep in mind here for understanding: 要理解以下两点需要注意的事项:

  1. Racing for your reference variable has no conceptual difference from that of a member field. 竞争您的参考变量与成员字段没有概念上的区别。
  2. Sharing a reference variable requires careful handling of safe publication 共享参考变量需要仔细处理安全发布

It's not mandatory to use any of them but you should know about them if you want to write proper multi-threaded code. 它不是必须使用它们中的任何一个,但如果你想编写适当的多线程代码,你应该知道它们。

Final 最后

final means you cannot re-initialize that variable again, so when you say final意味着你不能再次重新初始化那个变量,所以当你说

final SyncClass mySharedObject = new SyncClass();

you cannot do the initialization of mySharedObject again in some other part of code like below 你不能在下面的代码的其他部分再次初始化mySharedObject

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

Even though you cannot re-assign mySharedObject reference to some other object, you can still update it's state (field counter variable) by calling methods on it because field is not final. 即使您无法将mySharedObject引用重新分配给其他对象,您仍然可以通过调用其上的方法来更新它的状态(字段计数器变量),因为field不是最终的。

Synchronization and volatile are just constructs to ensure that any change to a shared mutable object (in this case updating a field counter) by one thread is visible to all other threads. 同步和volatile只是构造,以确保一个线程对共享可变对象(在这种情况下更新field计数器)的任何更改对所有其他线程可见。

Synchronization 同步

synchronized method means that any thread trying to invoke that method should acquire a lock on the object in which that method is defined. synchronized方法意味着任何尝试调用该方法的线程都应该获取对定义该方法的对象的锁定。

So in your case, If thread-1 is trying to do mySharedObject.doSomething() , it will acquire lock on mySharedObject and thread-2 has to wait until thread-1 releases that lock on the same object to be able to do mySharedObject.doSomethingElse() ie using Synchronization at any given point of time, only ONE thread will update the state of the object. 所以在你的情况下,如果thread-1试图执行mySharedObject.doSomething() ,它将获取对mySharedObject锁定,而thread-2必须等到thread-1释放锁定同一对象才能执行mySharedObject.doSomethingElse()即在任何给定的时间点使用同步,只有一个线程将更新对象的状态。 At the end of the method, just before releasing the lock all the changes done by thread-1 are flushed to main memory so that thread-2 can work on the recent state. 在方法结束时,就在释放锁之前,所有由thread-1完成的更改都会刷新到主内存,以便thread-2可以处理最近的状态。

Volatile 挥发物

volatile on the other hand ensures read/write visibility to all the threads. 另一方面, volatile确保所有线程的读/写可见性。 Any read and write to volatile variable are always flushed to main memory. 对volatile变量的任何读写操作总是刷新到主存储器。

If your field variable inside SyncClass is volatile, any update like field++ by thread-1 is visible to thread-2 but I'm not sure how it applies to object references. 如果SyncClassfield变量是易失性的,那么thread-1对field++的任何更新都是可见的,但我不确定它如何应用于对象引用。

As volatile only guarantees visibility but not atomicity, it's possible that both thread-1 and thread-2 try to update field counter at the same time and the final updated value may not be proper. 由于volatile仅保证可见性而非原子性,因此thread-1和thread-2可能同时尝试更新field计数器,并且最终更新的值可能不正确。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM