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

Why threads do not cache object locally?

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

您看到 "correct" 执行的原因有多种。

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

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

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

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

当您谈论诸如线程缓存之类的概念时,您是在谈论可以实现 Java 的假想机器的属性。逻辑类似于 "Java permits an implementation to cache things, so it requires you to tell it when such things would break your program"。这并不意味着任何实际的机器都可以做任何类似的事情。实际上,您可能使用的大多数机器都有完全不同的优化类型,不涉及您正在考虑的那种缓存。

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

您的代码不太可能出现并发错误,因为它以非常低的并发执行。您有 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";
        }
    });

的确,这个 returns 10000,正如预期的那样。

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

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