为什么这个关键部分在 Java 中不起作用?

Why does this critical section not work in Java?

我开始在 uni 学习多线程,虽然我开始总体上掌握这个概念,但我也在玩一些例子,以了解它在实践中的工作原理。我对以下 class 的问题是:

它按预期工作,如果我将 getters/setters 与我的对象锁同步,则主线程将布尔值 done 设置为 true,运行() 中的 while 循环中断main 只是等待 worker 的终止,如 join() 所述。最后打印出来了,我很高兴。在这里也许你可以帮助我理解,为什么同步 done 的 getter/setter 很重要,但是同步 count 的 getter 似乎无关紧要。

但我真正的问题是,我不明白为什么工作线程永远不会终止,如果我在 main 方法内的同步块/关键部分中使用 setter 设置布尔值完成。然后它似乎永远 运行 并且 main 等待它,因为 join()。另一方面,当我让这个 运行 通过 IntelliJ 调试器时,它似乎最终会终止,但是对于正常的 运行ning,它永远只是 运行s。为什么?据我在关键部分的理解,当布尔值设置为真时,运行() 中的 运行ning while 循环应该在短时间内注意到这一点并终止 worker?

public class VisibilitySynchronized extends Thread {
    private static Object lock = new Object();
    private boolean done = false;
    public long counter = 1;

    public  boolean getDone() {
    //synchronized (lock) {
            return this.done;
    //}
    }

    public long getCounter() {
    //synchronized(lock) {
            return this.counter;
        //}
    }

    public void setDone(boolean changeDone){
        //synchronized(lock) {
            this.done = changeDone;
        //}
    }

    @Override
    public void run() {

        while (!getDone()) {
            counter++;
        }
    }



    
    public static void main(String[] args) throws InterruptedException {

        Thread main = Thread.currentThread();

        main.setName("I am the main thread executing the main method right now and am ybout to start 
        a worker of the class VisibilitySynchronized");

        System.out.println("Main Thread is: " + main.getName());


        VisibilitySynchronized worker = new VisibilitySynchronized();
        worker.start();

        Thread.sleep(1000);

        synchronized (lock) {
            worker.setDone(true);
        }
            //worker.setDone(true); this would be working if method was synchronized
            System.out.println("Waiting for other Thread to terminate...");

        System.out.println("Done in worker is " + worker.getDone());

        worker.join();


        System.out.printf("Done! Counted until %d.", worker.getCounter());

    }

}

我自己 运行 这段代码,当你设置了同步块时,它可以工作(yield() 快速调用 returns),但是当你删除它们时,它没有(应用程序挂起,yield() 调用似乎从未 returning)。

这或多或少是预期的行为。如果此代码 did return 迅速从 yield() 调用,那将 ALSO 是预期的行为。

欢迎使用 Java 内存模型。

每个线程都会得到一个不公平的硬币。这是不公平的,因为它可以随心所欲地搞砸你,每次都以同样的方式翻转,让它看起来像是可靠地工作,当你把那个演示给那个重要的客户时,它就会失败。基本规则很简单。如果一个线程曾经抛硬币并且你的代码运行方式根据它的着陆方式而不同,你输了游戏:你有一个错误,并且编写一个捕获它的测试几乎是不可能的.因此,像这样编写线程代码是一种需要严格遵守纪律的行为。

线程在读取或写入字段时会抛硬币。每个线程都有每个 object* 的每个字段的自定义本地克隆副本,它将读取和写入。但每次这样做,它都会抛出那枚邪恶的硬币。如果硬币正面朝上,它会任意复制它的值到一些或所有其他线程的副本,或者将值从其他线程的本地副本复制到它自己。在 tails 上,它不会那样做,只是使用它的本地副本。

在这种情况下,您正在观察(几乎)无穷无尽的尾巴。在现实生活中,你不会连续得到几百万个尾巴,但我有没有提到这个硬币是不公平的?这里是'normal'。但关键点是:VM 完全不对抛硬币做出任何判断运行 - VM 每次都会抛正面(因此 yield()return 快)一样好,会通过 TCK 等等。

因此:如果抛硬币对代码的运行有影响,那你就搞砸了。

你在这里这样做了:worker-thread(线程,而不是 object)读取它的副本(它是错误的并且仍然是错误的),并且 main-thread 设置它的工作线程(object,不是线程)的完成标志的副本为真。 1 个字段,但是 2 个缓存副本,现在我们正在等待 worker-thread 设法看到 main-thread 做了什么。

幸运的是,我们可以强制 VM 不掷硬币,这样做的途径是 建立 comes-before 关系

java 内存模型已经写下了一系列规则,这些规则确定 VM 然后将确认 'event A happened before event B',并将 gua运行tee A 所做的任何事情 都将 对 B 可见。没有掷硬币 - B 的副本必然会反映 A 所做的事情。

名单很长,但重要的是:

  • 命令式[​​=85=]:在单个线程中,任何在另一个 came-before 之前运行的语句。这是 'duh, obviously' 一个:在 { foo(); bar(); } 中,foo 所做的任何事情对 bar 都是可见的,因为同一个线程。

  • synchronized:每当线程退出 synchronize-on-object-X 块时,'happens-before' 任何线程进入 synchronize-on-object-X 块,如果线程进入之后这样做。

  • volatilevolatile 是可以放在字段上的关键字。这有点棘手; volatile 也是根据 CB/CA 定义的,但考虑到对 volatile 变量的任何写入都会迫使硬币正面朝上,将更新写出到每个线程的副本,这样更容易理解。这让 volatile 看起来很神奇,但它付出了相当大的代价:volatile 相当慢,而且并不像您最初想象的那么有用,因为您不能使用它来设置原子操作。

  • 线程启动:无论何时启动一个线程,启动新线程的线程在调用 .start() 之前所做的任何事情对新线程都是可见的立即。

当然,如果某些方法通过结合同步规则和命令规则在内部同步事物,那么该方法也有效地充当 comes-before 建立因素。你应该使用这个 - 定义了一堆 JDK 方法来做到这一点!

因此,将同步块扔回去,从而在 main-thread 写入完成标志和 worker-thread 读取完成标志之间建立 CB/CA,并且代码有效(并且实际上会有效在每个 JVM 上,无论 OS、CPU、月相或 VM 供应商)。没有它,这段代码可以为所欲为。退出,或不退出,或4小时后退出,或仅在您的winamp切换歌曲时退出。那么一切都是公平的,因为您编写的代码取决于抛硬币的结果,所以这取决于您。或者,标记字段 'volatile',这样也可以。


那么,如果雷区这么大,你如何编写多核 java 代码??

真正的答案是:你大多不会。它太复杂了。相反,你这样做:

  • isolate:如果根本没有线程与任何其他线程交互(没有 reads/writes 字段,除了完全属于该线程运行的本地内容),那么所有的硬币翻转都是微不足道的在世界上不会改变任何事情,所以这样做。在启动之前正确设置线程,让线程 运行 结束,并让它通过安全通道传达它产生的东西(如果你根本需要 return 值),然后不要不用担心。
  • streamline comms:不是通过字段进行通信(这里是您的线程 'communicate';main 以该布尔值的形式与 worker 通信),而是通过一个通道进行通信专为并发控制而设计:使用 DB(线程 A 写入 DB,线程 B 也这样做,并且与 A 具有相同的表和行;DB 具有管理此功能的工具),或消息 queue。这可以是像 rabbitMQ 这样的完整库,也可以是来自 java.util.concurrent 的 collection 类型,例如 BlockingQueue。这些都为你解决了CB/CA的问题
  • 依赖一个框架: 一个网络框架有很多线程,会在一个适当的线程上调用你的'web handler'代码。处理程序通过 DB 聊天,Web 框架负责所有线程:您的所有 CPU 核心都在快速增长,您永远不需要担心任何抛硬币。您还可以在另一端使用框架并使用例如fork/join还是stream->map->filter->collect的概念(a.k.a.MapReduce).

*) 不是真的,我正在给你一个如何思考它的心智模型,这样你就不会写出这个错误。重点是,它 可以 进行克隆。