【问题标题】:Why threads do not cache object locally?为什么线程不在本地缓存对象?
【发布时间】:2017-06-21 21:54:08
【问题描述】:

我有一个 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 修饰符,我只希望得到这样的结果。但我有这个修饰符和没有相同的结果。

【问题讨论】:

  • 也许你走运了。
  • 我只是检查了很多......我试图理解一个线程缓存,但是那个例子对我来说就像一个魔法。
  • 它是魔法。这就是为什么你不能从这样的 sn-p 中得出结论。当您编写线程不安全代码时,添加或删除代码行可能会导致不同的行为。

标签: java multithreading threadpool volatile non-volatile


【解决方案1】:

您看到“正确”执行的原因有多种。

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

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

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

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

【讨论】:

【解决方案2】:

当您谈论诸如线程缓存之类的概念时,您谈论的是可能在其上实现 Java 的假设机器的属性。其逻辑类似于“Java 允许一种实现来缓存东西,因此它要求你告诉它什么时候这些东西会破坏你的程序”。这并不意味着任何实际的机器都可以做任何事情。实际上,您可能使用的大多数机器都具有完全不同类型的优化,不涉及您正在考虑的缓存类型。

Java 要求您准确地使用volatile,这样您就不必担心您正在使用的实际机器可能会或可能不会有哪些荒谬复杂的优化。这真是一件好事。

【讨论】:

    【解决方案3】:

    您的代码不太可能出现并发错误,因为它以非常低的并发执行。您有 10 个线程,每个线程在进行字符串连接之前平均休眠 500 毫秒。作为一个粗略的猜测,字符串连接每个字符大约需要 1ns,并且因为您的字符串只有 80 个字符长,这意味着每个线程在 500000000 ns 执行中花费大约 80 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,正如预期的那样。

    【讨论】:

      【解决方案4】:

      因为您使用的是Thread.sleep((long) (Math.random() * 100));所以每个线程都有不同的睡眠时间,并且执行可能是一个一个,因为所有其他线程都处于睡眠模式或已完成执行。但是虽然您的代码正在工作,但它不是线程安全的。即使如果您使用 Volatile 也不会使您的代码线程安全。Volatile 仅确保可见性,即当一个线程进行一些更改时,其他线程能够看到它。

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

      【讨论】:

        猜你喜欢
        • 2010-10-05
        • 2014-07-07
        • 2011-11-16
        • 1970-01-01
        • 1970-01-01
        • 2016-06-12
        • 1970-01-01
        相关资源
        最近更新 更多