3

This topic is a refinement of : [STACK-REF] In Java, is it required to synchronize write access to an array if each thread writes to a separate cell space?

Context : We need to huge arrays of row type, to make statistical computation. [STACK-REF] let us discover that array content can be processed simultaneously by N (worker) threads.

Problem : A main thread T0 launches N workers. When all workers have finished, what T0 'sees' in array ? Is its sontent synchronized with main memory ?

(simplistic) Test case :

  • Thread T0 creates a small array of bytes : 8 items.
  • It starts a new new thread T1 which writes 8 other distinct values.
  • T0 wait a long time, so that T1 ends before wakeup.
  • Array content is displayed (by T0)

Result : T0 views it as T1 built it. Is it quite normal or accidental ?

public class TestCase extends Thread { 

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

    //Current thread (main) stands for 'T0' in topic description.

    // byte : to get a very small array.
    byte[] counts    =  { (byte) 128, (byte) 129, (byte) 130, 
                          (byte) 131, (byte) 132, (byte) 133,
                          (byte) 134, (byte) 135 };

    StringBuilder sb = new StringBuilder();
    for ( int i = 0; i < counts.length; i++)
        sb.append(counts[i]+" ");

    // all values are displays as negative numbers
    System.out.println("'counts' initial content = " + sb);

    Thread T1 = new TestCase(counts); 
    T1.start();


    // 'join' can't be suspected to do refresh 'counts' array.
    // T1.join();
    Thread.sleep(1000);

    //Avoid to display each item, to avoid any unexpected
    // sync, as explained in :
    // see : https://stackoverflow.com/questions/21583448/what-can-force-a-non-volatile-variable-to-be-refreshed
    //        for ( int i = 0; i < counts.length; i++)
    //          System.out.println(counts[i]+" ");


    sb = new StringBuilder();
    for ( int i = 0; i < counts.length; i++)
        sb.append(counts[i]+" ");

    // all values are displayed as written by thread T1
    //How 'counts' copy in L1 cache has been updated from main memory ?
    System.out.println("'counts' post content    = " + sb);

}

Array size being small, we imagine that each thread owns a full copy in its L1/2/... cache ( >=32Ko). When T1 ends, array is fully updated in main memory.

Question : When T0 wakeup, how it gets an 'updated' view of array, without any special instruction ?

We built more complicated tests, involving a huge array of int, and many threads under conditions as stated by [STACK-REF]. Every time, thread T0 (i. e. 'main' one), got an fully consistent view of array.

Yes I know that

Thread Caching and Java Memory model

claims : "Each Thread does not have a local copy of memory..." but answers doesn't explain clearly our problem.

@Basilevs

I expect Thread.join() to perform synchronization. Why do you reject it?

When writing initial version of test-case, I used Thread.join() statement. I got same result.

Like you, I imagined that join() request for synch. Si to test this hypothesis, I replaced join() by sleep().

   ...I'd recommend not to sleep while waiting ..., thread rescheduling is likely to synchronize thread's context of wake-up.

I agree fully with you. Some time after my question beeing posted, I did new test in accordance with your proposal : sleep() statement was replaced by a loop :

while (System.currentTimeMillis() - t0 < 2000) ;

Once more, I didn't see any inconsistency.

@Andrey Cheboksarov

( Oops ! I omitted to describe my PC : Win7 64bits, jdk8, proc Intel Xeon E5630)

... if you do some computation on this array and then stop in a background thread the main thread will almost always see updated values

I test it, keeping T1 in activity by a no-op loop. : You are right.

    ... This will print something like -11 100 100 100 ...

Right.

Q 1 : Is your answer valid so, for other processors : Power PC by example ?

Q 2 : So your answer confirm that result of my test-cas is quite Normal ?

Cœur
  • 37,241
  • 25
  • 195
  • 267
X. Quiniou
  • 31
  • 2
  • I expect `Thread.join()` to perform synchronization. Why do you reject it? – Basilevs Nov 26 '16 at 18:40
  • To observe inconsistencies I'd recommend not to sleep while waiting for results, thread rescheduling is likely to synchronize thread's context of wake-up. – Basilevs Nov 26 '16 at 18:44
  • No guarantee of visibility is not the same as guaranteed non-visibility. There can be all sorts of reasons why caches are invalidated and to second-guess them is a mostly pointless exercise. – biziclop Dec 04 '16 at 12:07
  • The Java memory consistency model is specified at https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html – ninjalj Dec 04 '16 at 12:33

1 Answers1

1

This is a good question. I start right with the answer you need. There is a thing called cache coherence and this is a main reason why you see values from T1. Basically, cache lines can be acquired by particular core for writing or reading and only one core can request cache line for writing. If another core wants to read that cache line then it requests all cores and memory controller for this cache line. Memory controller knows that this cache line was requested for writing so it stays silent and core that owns cache line for writing should send that cache line to requesting core.

Now you understand that caches are smart and they will do everything for you to not see data races even if there a lot of them in you code. Very good and short paper about that coherency and memory barriers. Regarding you example i can say that if you do some computation on this array and then stop in a background thread the main thread will almost always see updated values. The easiest way to observe an effect of data race is to write a variable in a loop. A good compiler will place a variable to a register and increment just this register. Consider simple example

    byte[] counts    =  { (byte) 100, (byte) 129, (byte) 130,
            (byte) 131, (byte) 132, (byte) 133,
            (byte) 134, (byte) 135 };

    ForkJoinPool.commonPool().submit(
            () -> {
                for(int i = 0; i < 100_000_000; i++) {
                    counts[0]++;
                }
            }
    );

    ForkJoinPool.commonPool().submit(
            () -> {
                for(int i = 0; i < 10_000_000; i++) {
                    System.out.println(counts[0]);
                    try {
                        Thread.sleep(100L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
    );

    Thread.sleep(50000L);

This will print something like -11 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100

Note: here compiler also must have done something. To see what code realy does look at assembly

Andrey Cheboksarov
  • 649
  • 1
  • 5
  • 9