-1

i use jdk1.8. i think that double check lock without volatile is right. I use countdownlatch test many times and the object is singleton. How to prove that it must need “volatile”?

update 1

Sorry, my code is not formatted, because I can’t receive some JavaScript public class DCLTest {

private static /*volatile*/ Singleton instance = null;

static class Singleton {

    public String name;

    public Singleton(String name) {
        try {
            //We can delete this sentence, just to simulate various situations
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.name = name;
    }
}

public static Singleton getInstance() {
    if (null == instance) {
        synchronized (Singleton.class) {
            if (null == instance) {
                instance = new Singleton(Thread.currentThread().getName());
            }
        }
    }
    return instance;
}

public static void test() throws InterruptedException {
    int count = 1;
    while (true){
        int size = 5000;
        final String[] strs = new String[size];
        final CountDownLatch countDownLatch = new CountDownLatch(1);
        for (int i = 0; i < size; i++) {
            final int index = i;
            new Thread(()->{
                try {
                    countDownLatch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Singleton instance = getInstance();
                strs[index] = instance.name;
            }).start();
        }
        Thread.sleep(100);
        countDownLatch.countDown();
        Thread.sleep(1000);
        for (int i = 0; i < size-1; i++) {
            if(!(strs[i].equals(strs[i+1]))){
                System.out.println("i = " + strs[i] + ",i+1 = "+strs[i+1]);
                System.out.println("need volatile");
                return;
            }
        }
        System.out.println(count++ + " times");
    }
}

public static void main(String[] args) throws InterruptedException {
    test();
}

}

javaars
  • 1
  • 1
  • 1
    post code, so we can help out better – JayC667 Jul 04 '21 at 15:00
  • 1
    Plenty of material on the 'net on the double-checked lock paradigm. Here is one example: https://www.cs.cornell.edu/courses/cs6120/2019fa/blog/double-checked-locking/ – iggy Jul 04 '21 at 15:10
  • 1
    OK. How to prove. Imagine, for example, that object variable assignment and new object initialization are separate actions (no atomicity). It may occasionally lead to inconsistency in multi-threaded env. Also, Java utilizes caching which needs to be disabled explicitly in case of double-check approach. The first check happens outside synchronization. – Alexander Alexandrov Jul 04 '21 at 15:51
  • @AlexanderAlexandrov what "caching" are you exactly talking about? it is [a lot more complicated then you over-simplify it](https://stackoverflow.com/questions/59208041/do-we-need-volatile-when-implementing-singleton-using-double-check-locking/61166685#61166685) – Eugene Jul 04 '21 at 18:12
  • Here is an explanation about caching in the context of volatile keyword https://stackabuse.com/concurrency-in-java-the-volatile-keyword – Alexander Alexandrov Jul 04 '21 at 18:52
  • @AlexanderAlexandrov the article you posted is rubbish. This is not how CPU's work. Caches are always coherent; so it can't happen that after a value is written to the cache, that another CPU can still see an older version. For a detailed explaination see https://www.morganclaypool.com/doi/abs/10.2200/S00962ED2V01Y201910CAC049; which you can download for free. – pveentjer Jul 06 '21 at 07:59

2 Answers2

2

The key problem that you are not seeing is that instructions can be reordered. So the order they are in the source code, isn't the same as they are applied on memory. CPU's and compilers are the cause or this reordering.

I'm not going through the whole example of example of double checked locking because many examples are available, but will provide you just enough information to do some more research.

if you would have the following code:

 if(singleton == null){
     synchronized{
         if(singleton == null){
            singleton = new Singleton("foobar")
         }
     }
 }

Then under the hood something like this will happen.

if(singleton == null){
     synchronized{
         if(singleton == null){
            tmp = alloc(Singleton.class)
            tmp.value = "foobar"
            singleton = tmp
         }
     }
 }

Till so far, all is good. But the following reordering is legal:

if(singleton == null){
     synchronized{
         if(singleton == null){
            tmp = alloc(Singleton.class)
            singleton = tmp
            tmp.value = "foobar"
         }
     }
 }

So this means that a singleton that hasn't been completely constructed (the value has not yet been set) has been written to the singleton global variable. If a different thread would read this variable, it could see a partially created object.

There are other potential problems like atomicity (e.g. if the value field would be a long, it could be fragmented e.g. torn read/write). And also visibility; e.g. the compiler could optimize the code so that the load/store from memory is optimized-out. Keep in mind that thinking in term of reading from memory instead of cache, is fundamentally flawed and the most frequently encountered misunderstandings I see on SO; even many seniors get this wrong. Atomicity, visibility and reordering are part of the Java memory model, and making the singleton' variable volatile, resolves all these problems. It removes the data race (you can look it up for more details).

If you want to be really hardcore, it would be sufficient to place a [storestore] barrier between the creation of an object and the assignment to the singleton and a [loadload] barrier on the reading side and make sure you use a VarHandle with opaque for the singleton.

But this goes well beyond what most engineers understand and it won't make much of a performance difference in most situations.

If you want to check if something can break, please check out JCStress:

https://github.com/openjdk/jcstress

It is a great tool and can help you help you to show that your code is broken.

pveentjer
  • 10,545
  • 3
  • 23
  • 40
  • In the original program and your first, non-reordered example (introducing `tmp`), `singleton` gets assigned a fully constructed object. In your reordered example, it does not. This breaks assignment semantics even from a single threaded perspective. That is something that the compiler, nor the CPU is ever allowed to do. – Emmef Sep 01 '21 at 11:42
  • From a single threaded perspective there is no difference since the thread didn't see it happened. – pveentjer Sep 01 '21 at 13:34
  • Can you explain what "the thread didn't see it happened" means? Because if your reordering example were allowed, assignment would be broken, even in the thread that the assignment was done. That would be bad. – Emmef Sep 01 '21 at 17:53
  • The new operator is guaranteed to return a completely constructed object. The original program assigns that constructed value to a variable. Before _using_ that variable, the variable must be in the state that you expect from the language semantics. The synchronized block commits the variable to main memory: language semantics requires it to be in a fully constructed state. If you would use the variable as a function result, the function is required to return a fully constructed variable. Your reordering breaks all that. Thus it cannot be allowed. – Emmef Sep 01 '21 at 18:05
  • The new operator doesn't exist on byte code level. There is an INVOKE_SPECIAL that will give you a chunk of memory for the specified object type. Then the constructor code needs to be called so that the fields are set correctly. My code is pseudo code for what happens at a lower level. And committing to main memory is a fallacy. – pveentjer Sep 02 '21 at 02:11
  • The problem with the DCL is that if you would allow for a thread to access the 'singleton' field without synchronization, it could see the object partially constructed and I explained above how this can happen. The cause of this is a data-race on the singleton field. If everyone would go through the synchronized block or by making singleton volatile, then there is no data-race and the could should work fine. – pveentjer Sep 02 '21 at 02:15
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/236674/discussion-between-emmef-and-pveentjer). – Emmef Sep 02 '21 at 08:33
  • @Emmef I did this exercise [once](https://stackoverflow.com/questions/59208041/do-we-need-volatile-when-implementing-singleton-using-double-check-locking/61166685#61166685) and it would require a fair amount of reading (may be even some editing), but might be a good start of why `volatile` is needed. – Eugene Sep 12 '21 at 03:29
  • that `tmp` actually happens in reality, afaik, via [this](https://en.wikipedia.org/wiki/Static_single_assignment_form). So you have a very valid point here. Not trying to bug you Peter, but need to read some good answers after a terrible week :) – Eugene Sep 12 '21 at 03:31
  • 1
    @Eugene I agree (I removed my earlier response) and it is never too late to learn or correct what you've learned before. Pitty that good answers have relatively low scores and the Wikipedia page is not great! – Emmef Sep 12 '21 at 10:34
  • @Emmef agreed. The miss infornation and dead wrong assumptions on the internet about this subject are a lot. One never atops learning, you have a very correct approach, imo. – Eugene Sep 12 '21 at 21:08
0

How to prove that it must need “volatile”?

As a general rule, you cannot prove correctness of a multi-threaded application by testing. You may be able to prove incorrectness, but even that is not guaranteed. As you are observing.

The fact that you haven't succeeded in making your application fail is not a proof that it is correct.

The way to prove correctness is to do a formal (i.e. mathematical) happens before analysis.

It is fairly straightforward to show that when the singleton is not volatile there are executions in which there is a missing happens before. This may lead to an incorrect outcome such as the initialization happening more than once. But it is not guaranteed that you will get an incorrect outcome.

The flip-side is that if a volatile is used, the happens before relationships combined with the logic of the code are sufficient to construct a formal (mathematical) proof that you will always get a correct outcome.


(I am not going to construct the proofs here. It is too much effort.)

Stephen C
  • 698,415
  • 94
  • 811
  • 1,216