3

我有一个 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

4 回答 4

4

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

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

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

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

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

于 2017-06-21T22:17:47.507 回答
1

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

Java 要求您volatile精确地使用,这样您就不必担心您正在使用的实际机器可能有或可能没有什么样的荒谬复杂的优化。这是一件非常好的事情。

于 2017-06-21T22:42:19.403 回答
1

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

于 2017-06-21T23:26:34.860 回答
0

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

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

于 2017-06-22T06:09:14.093 回答