为什么线程不在本地缓存对象?

时间:2017-06-21 21:54:09

标签: java multithreading threadpool volatile non-volatile

我有一个String和ThreadPoolExecutor来改变这个String的值。看看我的样本:

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();
                }

            }
        });
    }

所以执行此操作后,我得到类似的东西:

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

所以问题是:如果我的String对象具有volatile修饰符,我只希望得到这样的结果。但是我对这个修饰符有相同的结果而没有。

4 个答案:

答案 0 :(得分:4)

有几个原因可以看出&#34;正确&#34;执行。

首先,CPU设计人员尽其所能,以便我们的程序即使在数据竞争的情况下也能正常运行。 Cache coherence处理缓存行并尝试最小化可能的冲突。例如,在某个时间点只有一个CPU可以写入缓存行。写入完成后,其他CPU应该请求缓存行能够写入。更不用说x86架构(你使用的最可能的架构)与其他架构相比非常严格。

其次,你的程序很慢,并且线程会在一段随机的时间内休眠。因此,他们几乎在不同的时间点完成所有工作。

如何实现不一致的行为?尝试使用for循环,没有任何睡眠。在这种情况下,字段值很可能会缓存在CPU寄存器中,并且某些更新将不可见。

P.S。字段str_example的更新不是原子的,因此即使在volatile关键字的存在,您的程序也可能生成相同的字符串值。

答案 1 :(得分:1)

当你谈到线程缓存这样的概念时,你会谈论可能在Java上实现Java的假想机器的属性。逻辑类似于&#34; Java 允许实现缓存内容,因此需要告诉它何时会破坏您的程序&#34;。这并不意味着任何实际的机器都可以做任何类似的事情。实际上,您可能使用的大多数计算机都有完全不同的优化方式,而这些优化并不涉及您正在考虑的那种缓存。

Java要求您精确地使用volatile,这样您就不必担心您正在使用的实际机器可能会或可能不具备哪种荒谬复杂的优化。这真的很棒。

答案 2 :(得分:1)

您的代码不太可能出现并发错误,因为它以非常低的并发性执行。你有10个线程,每个线程在进行字符串连接之前平均睡眠500毫秒。粗略猜测,字符串连接每个字符大约需要1ns,并且因为你的字符串只有80个字符长,这意味着每个线程花费大约80个执行5000000个ns。因此,两个或多个线程同时运行的可能性非常小。

如果我们更改您的程序以便多个线程同时运行,我们会看到完全不同的结果:

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());
}

在没有数据竞赛的情况下,这应该打印10000.在我的计算机上,这打印大约4200,这意味着超过一半的更新在数据竞赛中丢失。

如果我们声明s volatile怎么办?有趣的是,我们仍然得到大约4200,因此数据竞赛没有被阻止。这是有道理的,因为volatile确保写入对其他线程可见,但不会阻止中间更新,即发生的事情如下:

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

为了防止这种情况,您可以使用普通的旧同步块:

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

事实上,正如预期的那样,它会返回10000.

答案 3 :(得分:0)

它正在工作,因为你正在使用let controller = ViewController() 所以每个线程都有不同的休眠时间,执行可能是一个接一个,因为所有其他线程处于睡眠模式或完成执行。但是你的代码工作不是线程安全的。即使你使用Volatile也不会使你的代码线程安全.Volatile只确保可见性,即当一个线程做出一些更改,其他线程能够看到它。

在你的情况下,你的操作是多步骤过程读取变量,更新然后写入内存。所以你需要锁定机制,使其线程安全。