Why don't threads cache the object locally?

I have a String and a ThreadPoolExecutor that is changing the value of that string. Just check my example:

String str_example = "";     
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(10, 30, (long)10, TimeUnit.SECONDS, runnables);
    for (int i = 0; i < 80; i++){
        poolExecutor.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep((long) (Math.random() * 1000));
                    String temp = str_example + "1";
                    str_example = temp;
                    System.out.println(str_example);

                } catch (Exception e) {
                    e.printStackTrace();
                }

            }
        });
    }

      

so after doing this I get something like this:

1
11
111
1111
11111
.......

      

So the question is: I just expect this kind of result if my String object has a mutable modifier. But I have the same result with and without this modifier.

+3


source to share


4 answers


There are several reasons why you see "correct" execution.

First, processor developers do their best to make our programs work correctly even in the presence of data calculations. Cache coherency deals with cache lines and tries to minimize possible conflicts. For example, only one processor can write to a cache line at a time. After a write has been made, other processors must request that the cache line can write to it. Not to say that x86 architecture (most likely what you are using) is very strict compared to others.

Second, your program is slow and the threads sleep for some random amount of time. Therefore, they do almost all the work at different points in time.



How to achieve inconsistent behavior? Try something with a sleep cycle. In this case, the field value will most likely be cached in the CPU registers, and some updates will not be visible.

PS Updates to a field str_example

are not atomic, so you can create the same string values ​​even if the keyword is present volatile

.

+4


source


When you talk about concepts like thread caching, you are talking about properties of a hypothetical machine on which Java might be implemented. Logic is something like "Java allows implementations to cache things, so you need to tell when things like that break your program." This does not mean that any real machine does something like that. In fact, most of the machines you are likely to use have completely different kinds of optimizations that are not related to the type of caches you are thinking of.



Java requires you to use volatile

it in such a way that you don't have to worry about what kinds of absurdly complex optimizations on the very computer you are working on may or may not be. And that's a really good thing.

+1


source


Your code is unlikely to show concurrency errors as it is executed with very low concurrency value. You have 10 threads, each of which sleeps for an average of 500ms before doing string concatenation. As a rough guess, String concatenation takes about 1ns per character, and since your string is only 80 characters long, that would mean that each thread wastes around 80 out of 500,000,000 ns. The likelihood of two or more threads running concurrently is therefore vanishingly small.

If we change your program so that multiple threads run concurrently all the time, we see completely different results:

static String s = "";

public static void main(String[] args) throws Exception {
    ExecutorService executor = Executors.newFixedThreadPool(5);

    for (int i = 0; i < 10_000; i ++) {
        executor.submit(() -> {
            s += "1";
        });
    }
    executor.shutdown();
    executor.awaitTermination(1, TimeUnit.MINUTES);
    System.out.println(s.length());
}

      

In the absence of data calculations, this should print 10000. On my computer, this prints about 4200, which means that more than half of the updates are lost in the data race.

What if we announce s

volatile

? Interestingly, we still get about 4200 as a result, so data races were not prevented. This makes sense because volatile guarantees that records are visible to other threads, but does not interfere with intermediate updates, that is, something like:

Thread 1 reads s and starts making a new String
Thread 2 reads s and starts making a new String
Thread 1 stores its result in s
Thread 2 stores its result in s, overwriting the previous result

      

To prevent this, you can use a simple old synchronized block:

    executor.submit(() -> {
        synchronized (Test.class) {
            s += "1";
        }
    });

      

And indeed, this returns 10,000, as expected.

+1


source


It works because you are using Thread.sleep((long) (Math.random() * 100));

. In this way, each thread has a different sleep time, and execution can be one after the other, like all other threads in sleep mode or completed execution. But while your code works, it is not thread safe. if you are using Volatile will not make your code flow safe either. Steady only make sure the visibility, i.e. when one thread makes some changes, other threads can see it.

In your case, your operation is a multi-step process, reading a variable, updating, and then writing to memory. Therefore, you need a locking mechanism to make it thread safe.

0


source







All Articles