猿问

多线程中内存一致性错误的真实示例?

在java多线程的教程中,给出了一个Memory Consistency Errors的例子。但我无法重现它。还有其他方法可以模拟内存一致性错误吗?

教程中提供的示例:

假设定义并初始化了一个简单的 int 字段:

int counter = 0;

计数器字段在两个线程 A 和 B 之间共享。假设线程 A 递增计数器:

counter++;

然后,不久之后,线程 B 打印出计数器:

System.out.println(counter);

如果这两个语句在同一个线程中执行,则可以安全地假设打印出的值是“1”。但是,如果这两个语句在不同的线程中执行,则打印出的值很可能是“0”,因为不能保证线程 A 对计数器的更改对线程 B 可见——除非程序员在两者之间建立了 happens-before 关系这两个声明。


子衿沉夜
浏览 130回答 5
5回答

慕村225694

鉴于这段代码:public class Test {    volatile static private int a;    static private int b;    public static void main(String [] args) throws Exception {        for (int i = 0; i < 100; i++) {            new Thread() {                @Override                public void run() {                    int tt = b; // makes the jvm cache the value of b                    while (a==0) {                    }                    if (b == 0) {                        System.out.println("error");                    }                }            }.start();        }        b = 1;        a = 1;    }}的易变存储a发生在 的正常存储之后b。因此,当线程运行并看到时a != 0,由于 JMM 中定义的规则,我们必须看到b == 1。JRE 中的 bug 允许线程进入生产error线并随后得到解决。如果您没有a定义为,这肯定会失败volatile。

牧羊人nacy

这可能会重现问题,至少在我的电脑上,我可以在一些循环后重现它。假设你有一个Counter类:class&nbsp;Holder&nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;boolean&nbsp;flag&nbsp;=&nbsp;false; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;long&nbsp;modifyTime&nbsp;=&nbsp;Long.MAX_VALUE; }设为thread_A,flag将true时间存入&nbsp;modifyTime。让另一个线程,比方说thread_B,阅读Counter的flag。如果晚于thread_B仍然get even&nbsp;,那么我们可以说我们已经重现了问题。falsemodifyTime示例代码class Holder {&nbsp; &nbsp; boolean flag = false;&nbsp; &nbsp; long modifyTime = Long.MAX_VALUE;}public class App {&nbsp; &nbsp; public static void main(String[] args) {&nbsp; &nbsp; &nbsp; &nbsp; while (!test());&nbsp; &nbsp; }&nbsp; &nbsp; private static boolean test() {&nbsp; &nbsp; &nbsp; &nbsp; final Holder holder = new Holder();&nbsp; &nbsp; &nbsp; &nbsp; new Thread(new Runnable() {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; @Override&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; public void run() {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; try {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Thread.sleep(10);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; holder.flag = true;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; holder.modifyTime = System.currentTimeMillis();&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } catch (Exception e) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; e.printStackTrace();&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; }).start();&nbsp; &nbsp; &nbsp; &nbsp; long lastCheckStartTime = 0L;&nbsp; &nbsp; &nbsp; &nbsp; long lastCheckFailTime = 0L;&nbsp; &nbsp; &nbsp; &nbsp; while (true) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; lastCheckStartTime = System.currentTimeMillis();&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (holder.flag) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } else {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; lastCheckFailTime = System.currentTimeMillis();&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; System.out.println(lastCheckFailTime);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; if (lastCheckFailTime > holder.modifyTime&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; && lastCheckStartTime > holder.modifyTime) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; System.out.println("last check fail time " + lastCheckFailTime);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; System.out.println("modify time&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; " + holder.modifyTime);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return true;&nbsp; &nbsp; &nbsp; &nbsp; } else {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return false;&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; }}结果last check time 1565285999497modify time&nbsp; &nbsp; &nbsp;1565285999494这意味着从提交的时间thread_B获取,甚至将其设置为时间(早 3 毫秒)。falseCounterflag1565285999497thread_Atrue1565285999494

慕码人2483693

使用的示例太糟糕,无法证明内存一致性问题。让它工作将需要脆弱的推理和复杂的编码。然而,您可能无法看到结果。多线程问题是由于时机不巧而发生的。如果有人想增加观察问题的机会,我们需要增加不幸时机的机会。以下程序实现了它。public class ConsistencyIssue {&nbsp; &nbsp; static int counter = 0;&nbsp; &nbsp; public static void main(String[] args) throws InterruptedException {&nbsp; &nbsp; &nbsp; &nbsp; Thread thread1 = new Thread(new Increment(), "Thread-1");&nbsp; &nbsp; &nbsp; &nbsp; Thread thread2 = new Thread(new Increment(), "Thread-2");&nbsp; &nbsp; &nbsp; &nbsp; thread1.start();&nbsp; &nbsp; &nbsp; &nbsp; thread2.start();&nbsp; &nbsp; &nbsp; &nbsp; thread1.join();&nbsp; &nbsp; &nbsp; &nbsp; thread2.join();&nbsp; &nbsp; &nbsp; &nbsp; System.out.println(counter);&nbsp; &nbsp; }&nbsp; &nbsp; private static class Increment implements Runnable{&nbsp; &nbsp; &nbsp; &nbsp; @Override&nbsp; &nbsp; &nbsp; &nbsp; public void run() {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; for(int i = 1; i <= 10000; i++)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; counter++;&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; }}执行1输出:10963,执行2输出:14552最终计数应该是 20000,但它比那个少。原因是count++是多步操作,1.读count 2.increment count 3.store it两个线程可能会同时读取计数 1,将其递增到 2。然后写出 2。但如果它是串行执行,则应该是 1++ -> 2++ -> 3。我们需要一种方法使所有 3 个步骤成为原子。即一次只能由一个线程执行。解决方案 1:Synchronized 用 Synchronized 包围增量。由于计数器是静态变量,您需要使用类级同步@Override&nbsp; &nbsp; &nbsp; &nbsp; public void run() {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; for (int i = 1; i <= 10000; i++)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; synchronized (ConsistencyIssue.class) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; counter++;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; }现在输出:20000解决方案 2:AtomicIntegerpublic class ConsistencyIssue {&nbsp; &nbsp; static AtomicInteger counter = new AtomicInteger(0);&nbsp; &nbsp; public static void main(String[] args) throws InterruptedException {&nbsp; &nbsp; &nbsp; &nbsp; Thread thread1 = new Thread(new Increment(), "Thread-1");&nbsp; &nbsp; &nbsp; &nbsp; Thread thread2 = new Thread(new Increment(), "Thread-2");&nbsp; &nbsp; &nbsp; &nbsp; thread1.start();&nbsp; &nbsp; &nbsp; &nbsp; thread2.start();&nbsp; &nbsp; &nbsp; &nbsp; thread1.join();&nbsp; &nbsp; &nbsp; &nbsp; thread2.join();&nbsp; &nbsp; &nbsp; &nbsp; System.out.println(counter.get());&nbsp; &nbsp; }&nbsp; &nbsp; private static class Increment implements Runnable {&nbsp; &nbsp; &nbsp; &nbsp; @Override&nbsp; &nbsp; &nbsp; &nbsp; public void run() {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; for (int i = 1; i <= 10000; i++)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; counter.incrementAndGet();&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; }}我们可以使用信号量,也可以使用显式锁定。但是对于这个简单的代码,AtomicInteger 就足够了

跃然一笑

请再看看你的源代码中是如何介绍这个例子的。避免内存一致性错误的关键是理解 happens-before 关系。这种关系只是保证一个特定语句对内存的写入对另一个特定语句可见。要看到这一点,请考虑以下示例。这个例子说明了多线程不是确定性的,因为你不能保证不同线程操作的执行顺序,这可能会导致多次运行的不同观察结果。但是并不能说明内存一致性错误!要了解什么是内存一致性错误,您需要首先了解内存一致性。Lamport 在 1979 年引入了最简单的内存一致性模型。这是原始定义。任何执行的结果都是一样的,就好像所有进程的操作都按某种顺序执行,并且每个进程的操作都按照其程序指定的顺序出现在这个序列中现在,考虑这个示例多线程程序,请看一下最近一篇关于顺序一致性的研究论文中的这张图片。它说明了真正的内存一致性错误可能是什么样子。要最终回答您的问题,请注意以下几点:内存一致性错误始终取决于底层内存模型(特定的编程语言可能允许更多行为以进行优化)。什么是最好的内存模型仍然是一个悬而未决的研究问题。上面给出的例子给出了一个违反顺序一致性的例子,但是不能保证你可以用你喜欢的编程语言观察到它,原因有两个:它取决于编程语言精确的内存模型,并且由于不确定性,你没有强制执行特定错误执行的方法。

qq_笑_17

有时当我试图重现一些真正的并发问题时,我会使用调试器。在 print 上创建一个断点,在 increment 上创建一个断点并运行整个过程。释放不同顺序的断点会得到不同的结果。也许很简单,但它对我有用。
随时随地看视频慕课网APP

相关分类

Java
我要回答