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

后端 未结 5 778
陌清茗
陌清茗 2021-01-04 02:52

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 Sync         


        
相关标签:
5条回答
  • 2021-01-04 03:20

    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?

    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.

    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.

    0 讨论(0)
  • 2021-01-04 03:30

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

    So regarding your example, you could sometimes get a NullPointerException when a thread accesses the mySharedObject variable. But this might only happen on multi-core machines with multiple caches.

    Java Memory Model

    The main point here is the Java Memory Model. 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. 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.

    0 讨论(0)
  • 2021-01-04 03:32

    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:

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

    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.

    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.

    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. Reading that reference is performed outside the synchronized block.

    When in doubt, use final or volatile. Whichever is appropriate.

    0 讨论(0)
  • 2021-01-04 03:35

    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 SyncClass mySharedObject = new SyncClass();
    

    you cannot do the initialization of mySharedObject again in some other part of code like below

       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.

    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.

    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.

    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() i.e using Synchronization at any given point of time, only ONE thread will update the state of the object. 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.

    Volatile

    volatile on the other hand ensures read/write visibility to all the threads. Any read and write to volatile variable are always flushed to main memory.

    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.

    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.

    0 讨论(0)
  • 2021-01-04 03:42

    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
    0 讨论(0)
提交回复
热议问题