4

In the tutorial of java multi-threading, it gives an exmaple of Memory Consistency Errors. But I can not reproduce it. Is there any other method to simulate Memory Consistency Errors?

The example provided in the tutorial:

Suppose a simple int field is defined and initialized:

int counter = 0;

The counter field is shared between two threads, A and B. Suppose thread A increments counter:

counter++;

Then, shortly afterwards, thread B prints out counter:

System.out.println(counter);

If the two statements had been executed in the same thread, it would be safe to assume that the value printed out would be "1". But if the two statements are executed in separate threads, the value printed out might well be "0", because there's no guarantee that thread A's change to counter will be visible to thread B — unless the programmer has established a happens-before relationship between these two statements.

Community
  • 1
  • 1
xingbin
  • 27,410
  • 9
  • 53
  • 103
  • 1
    *What* have you written to reproduce it? The mere `counter++;` isn't enough to see an inconsistency. – Andrew Tobilko Aug 08 '19 at 17:40
  • 1
    The "no guarantee" goes both ways. There is no guarantee that updates made by one thread will be seen by the other thread, but there is also no guarantee that it won't, so *Memory Consistency Errors* are very difficult to force, especially in a short thread like that. – Andreas Aug 08 '19 at 17:41
  • 1
    This is difficult to test. How do you know that the `counter++` even happened before the `println(counter)`? Almost everything you can do to be sure of this will force a "happens-before" relationship between the two events. – Matt Timmermans Aug 08 '19 at 18:21

5 Answers5

3

This might reproduce the problem, at least on my computer, I can reproduce it after some loops.

  1. Suppose you have a Counter class:

    class Holder {
        boolean flag = false;
        long modifyTime = Long.MAX_VALUE;
    }
    
  2. Let thread_A set flag as true, and save the time into modifyTime.
  3. Let another thread, let's say thread_B, read the Counter's flag. If thread_B still get false even when it is later than modifyTime, then we can say we have reproduced the problem.

Example code

class Holder {
    boolean flag = false;
    long modifyTime = Long.MAX_VALUE;
}

public class App {

    public static void main(String[] args) {
        while (!test());
    }

    private static boolean test() {

        final Holder holder = new Holder();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(10);
                    holder.flag = true;
                    holder.modifyTime = System.currentTimeMillis();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();

        long lastCheckStartTime = 0L;
        long lastCheckFailTime = 0L;
        while (true) {
            lastCheckStartTime = System.currentTimeMillis();
            if (holder.flag) {
                break;
            } else {
                lastCheckFailTime = System.currentTimeMillis();
                System.out.println(lastCheckFailTime);
            }
        }

        if (lastCheckFailTime > holder.modifyTime 
                && lastCheckStartTime > holder.modifyTime) {
            System.out.println("last check fail time " + lastCheckFailTime);
            System.out.println("modify time          " + holder.modifyTime);
            return true;
        } else {
            return false;
        }
    }
}

Result

last check time 1565285999497
modify time     1565285999494

This means thread_B get false from Counter's flag filed at time 1565285999497, even thread_A has set it as true at time 1565285999494(3 milli seconds ealier).

xingbin
  • 27,410
  • 9
  • 53
  • 103
  • Your test is quite nice, but it does not achieve what you claim: 'This means thread_B get false from Counter's flag filed at time 1565285999497'. The check of the flag might have happened earlier, e.g. 1565285999493, and the assignment to the `lastCheckTime` or rather the call to `System.currentTimeMillis()` might have happened three milliseconds later. You would need to put the assignment to `lastCheckTime` *before* the check of the flag, at the very beginning of the while loop. – ciamej Aug 08 '19 at 17:57
  • @ciamej Thanks, you're right. But if I put it *at the very beginning of the while loop*, then the *lastCheckTime* does not represent *thread_B still read false from the flag* any more. Do you have any idea to achive it both? – xingbin Aug 08 '19 at 18:04
  • You cannot have it both, because you cannot execute two operations atomically. However, if you record time at the beginning of the loop, and you do print that time through `System.out.println`, then it means that `holder.flag` had to be `false` even after taking the time. Of course, this all is relevant only if the compiler did not reorder the operations, but we take the sequential execution of operations as the assumption. – ciamej Aug 08 '19 at 18:10
  • @ciamej I impoted a new timestamp `lastCheckStartTime` and edited the answer. Now is it right? – xingbin Aug 08 '19 at 18:12
  • And have you run the code and found some inconsistencies? Because the timestamps seem the same... The code is definitely correct now. – ciamej Aug 08 '19 at 20:34
  • @ciamej Seems `lastCheckStartTime` is always greater than or equal to `lastCheckFailTime`, I think it still not right.. – xingbin Aug 09 '19 at 04:11
  • Right now after the last `lastCheckFailTime` assignment, the loop continues and executes `lastCheckStartTime` and breaks. That's why `lastCheckStartTime > lastCheckFailTime`. You can replace `lastCheckStartTime = System.currentTimeMillis();` with `tmp = System.currentTimeMillis();` and put in the else branch of the if `lastCheckStartTime = tmp;`. This will cause the lastCheckStartTime to be written down only if the check failed. – ciamej Aug 09 '19 at 09:29
3

The example used is too bad to demonstrate the memory consistency issue. Making it work will require brittle reasoning and complicated coding. Yet you may not be able to see the results. Multi-threading issues occur due to unlucky timing. If someone wants to increase the chances of observing issue, we need to increase chances of unlucky timing. Following program achieves it.

public class ConsistencyIssue {

    static int counter = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new Increment(), "Thread-1");
        Thread thread2 = new Thread(new Increment(), "Thread-2");
        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println(counter);
    }

    private static class Increment implements Runnable{

        @Override
        public void run() {
            for(int i = 1; i <= 10000; i++)
                counter++;
        }

    }
}

Execution 1 output: 10963, Execution 2 output: 14552

Final count should have been 20000, but it is less than that. Reason is count++ is multi step operation, 1. read count 2. increment count 3. store it

two threads may read say count 1 at once, increment it to 2. and write out 2. But if it was a serial execution it should have been 1++ -> 2++ -> 3.

We need a way to make all 3 steps atomic. i.e to be executed by only one thread at a time.

Solution 1: Synchronized Surround the increment with Synchronized. Since counter is static variable you need to use class level synchronization

@Override
        public void run() {
            for (int i = 1; i <= 10000; i++)
                synchronized (ConsistencyIssue.class) {
                    counter++;
                }
        }

Now it outputs: 20000

Solution 2: AtomicInteger

public class ConsistencyIssue {

    static AtomicInteger counter = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new Increment(), "Thread-1");
        Thread thread2 = new Thread(new Increment(), "Thread-2");
        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();
        System.out.println(counter.get());
    }

    private static class Increment implements Runnable {

        @Override
        public void run() {
            for (int i = 1; i <= 10000; i++)
                counter.incrementAndGet();
        }

    }
}

We can do with semaphores, explicit locking too. but for this simple code AtomicInteger is enough

  • Thanks. This might happens because Memory Consistency Errors. But it also could because Thread Interference.https://docs.oracle.com/javase/tutorial/essential/concurrency/interfere.html – xingbin Aug 17 '19 at 07:57
  • This is an example of having uncoordinated access to a single variable by two threads - a memory consistency issue is when the order of memory operations are visible in different, inconsistent, orders in different threads. – dan.m was user2321368 Aug 19 '19 at 13:25
3

I answered a question a while ago about a bug in Java 5. Why doesn't volatile in java 5+ ensure visibility from another thread?

Given this piece of code:

public class Test {
    volatile static private int a;
    static private int b;

    public static void main(String [] args) throws Exception {
        for (int i = 0; i < 100; i++) {
            new Thread() {

                @Override
                public void run() {
                    int tt = b; // makes the jvm cache the value of b

                    while (a==0) {

                    }

                    if (b == 0) {
                        System.out.println("error");
                    }
                }

            }.start();
        }

        b = 1;
        a = 1;
    }
}

The volatile store of a happens after the normal store of b. So when the thread runs and sees a != 0, because of the rules defined in the JMM, we must see b == 1.

The bug in the JRE allowed the thread to make it to the error line and was subsequently resolved. This definitely would fail if you don't have a defined as volatile.

John Vint
  • 39,695
  • 7
  • 78
  • 108
  • I just remove variable `b`, and the loop never stop though tne main thread assign `1` to `a`, I think it is enough to prove that there is a memory consistency error. – xingbin Aug 18 '19 at 15:28
2

Sometimes when I try to reproduce some real concurrency problems, I use the debugger. Make a breakpoint on the print and a breakpoint on the increment and run the whole thing. Releasing the breakpoints in different sequences gives different results.

Maybe to simple but it worked for me.

Turo
  • 4,724
  • 2
  • 14
  • 27
2

Please have another look at how the example is introduced in your source.

The key to avoiding memory consistency errors is understanding the happens-before relationship. This relationship is simply a guarantee that memory writes by one specific statement are visible to another specific statement. To see this, consider the following example.

This example illustrates the fact that multi-threading is not deterministic, in the sense that you get no guarantee about the order in which operations of different threads will be executed, which might result in different observations across several runs. But it does not illustrate a memory consistency error!

To understand what a memory consistency error is, you need to first get an insight about memory consistency. The simplest model of memory consistency has been introduced by Lamport in 1979. Here is the original definition.

The result of any execution is the same as if the operations of all the processes were executed in some sequential order and the operations of each individual process appear in this sequence in the order specified by its program

Now, consider this example multi-threaded program, please have a look at this image from a more recent research paper about sequential consistency. It illustrates what a real memory consistency error might look like.

Example error

To finally answer your question, please note the following points:

  1. A memory consistency error always depends on the underlying memory model (A particular programming languages may allow more behaviours for optimization purposes). What's the best memory model is still an open research question.
  2. The example given above gives an example of sequential consistency violation, but there is no guarantee that you can observe it with your favorite programming language, for two reasons: it depends on the programming language exact memory model, and due to undeterminism, you have no way to force a particular incorrect execution.

Memory models are a wide topic. To get more information, you can for example have a look at Torsten Hoefler and Markus Püschel course at ETH Zürich, from which I understood most of these concepts.

Sources

  1. Leslie Lamport. How to Make a Multiprocessor Computer That Correctly Executes Multiprocessor Programs, 1979
  2. Wei-Yu Chen, Arvind Krishnamurthy, Katherine Yelick, Polynomial-Time Algorithms for Enforcing Sequential Consistency in SPMD Programs with Arrays, 2003
  3. Design of Parallel and High-Performance Computing course, ETH Zürich
Raphael D.
  • 778
  • 2
  • 7
  • 18