【问题标题】:Why does this critical section not work in Java?为什么这个关键部分在 Java 中不起作用?
【发布时间】:2020-11-14 15:51:13
【问题描述】:

我开始在 uni 学习多线程,虽然我开始大致了解这个概念,但我也在玩一些例子,以了解它在实践中是如何工作的。我对以下课程的问题是:

它按预期工作,如果我将 getter/setter 与我的对象锁同步,那么主线程将 boolean done 设置为 true,run() 中的 while 循环中断,main 只是等待终止通过 join() 说明的工人。最后打印出来了,我很高兴。在这里也许您可以帮助我理解,为什么同步 getter/setter for done 很重要,但 getter for count 是否同步似乎并不重要。

但我真正的问题是,我不明白为什么工作线程永远不会终止,如果我在主方法内的同步块/关键部分中使用 setter 设置布尔值。然后它似乎永远运行并且 main 等待它,因为 join()。另一方面,当我让它通过 IntelliJ 调试器运行时,它似乎最终会终止,但在正常运行的情况下,它只会永远运行。为什么呢?据我在关键部分的理解,当布尔值设置为 true 时,run() 中正在运行的 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());

    }

}

【问题讨论】:

  • 添加你的类的使用示例。
  • 您是否在问:“如果 getDone 和 setDone 方法在锁定时同步,为什么这不起作用”或问您:“如果 getDone 和 setDone 方法未同步,为什么这不起作用锁定”或者您是在问:我是否锁定并不重要,无论哪种方式,yield() 调用都不会返回?
  • @Pavlus 对不起,我不确定,如果我理解的话,我的 main 方法中使用了 VisibilitySynchronized 类?不知道我还能举出什么样的例子?
  • 好吧,那么我正确地解释了你的问题 - 请参阅我的广泛回答,它准确解释了为什么没有 -synchronized- 它无法工作。
  • setDone() 在同步中被调用,但 getDone() 不是。

标签: java multithreading concurrency parallel-processing synchronized


【解决方案1】:

我自己运行了这段代码,当你设置了同步块时,它可以工作(yield() 调用很快返回),但是当你删除它们时,它不会(应用程序挂起,yield() 调用似乎永远不会回来)。

这或多或少是预期的行为。如果这段代码确实从那个 yield() 调用中迅速返回,那 ALSO 也是预期的行为。

欢迎使用 Java 内存模型。

每个线程都会得到一个不公平的硬币。这是不公平的,因为它可以随意捣乱你,并且每次都以相同的方式翻转,让它看起来可靠地工作,当你将演示提供给那个重要的客户时它会失败。基本规则很简单。如果一个线程曾经掷硬币,并且您的代码运行方式根据它的着陆方式而有所不同,你就输了:你有一个错误,编写一个捕捉它的测试几乎是不可能的。因此,编写这样的线程代码是一种需要极度自律的行为。

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

在这种情况下,您正在观察(几乎)无穷无尽的反面抛硬币。在现实生活中,你不会连续得到几百万条尾巴,但我有没有提到硬币不公平?这里很“正常”。但关键点是:VM 根本不保证抛硬币 - 每次都翻转正面(因此 yield() 快速返回)的 VM 一样好,将通过 TCK ,等等。

因此:如果抛硬币对你的代码运行方式很重要,那你就搞砸了。

你在这里做了:工作线程(线程,而不是对象)读取它的副本(它是假的并且仍然是假的),并且主线程设置它的工作线程的副本(对象,而不是线程) 的完成标志为真。 1 个字段,但 2 个缓存副本,现在我们正在等待一个硬币翻转,然后该工作线程设法查看主线程做了什么。

幸运的是,我们可以强制虚拟机不要抛硬币,这样做的途径是建立先入为主的关系

Java 内存模型已经写下了一系列规则,这些规则确定 VM 将随后确认“事件 A 发生在事件 B 之前”,并将 保证 A 所做的任何事情 对 B 可见。没有硬币翻转 - B 的副本必然反映 A 所做的事情。

清单很长,但重要的是:

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

  • 同步:每当线程退出 synchronize-on-object-X 块时,如果线程进入 synchronize-on-object-X 块,则“发生在”任何线程进入 synchronize-on-object-X 块之前所以后来。

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

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

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

因此,将同步块扔回,从而在写入完成标志的主线程和读取它的工作线程之间建立 CB/CA,并且代码可以正常工作(实际上将在每个 JVM 上工作,无论操作系统如何, CPU、月相或 VM 供应商)。没有它,这段代码可以为所欲为。退出,或不退出,或4小时后退出,或仅当您的winamp切换歌曲时退出。那么一切都是公平的,因为您编写的代码取决于掷硬币的结果,所以它在您身上。或者,将字段标记为“volatile”,也可以这样做。


那么,如果这是一个雷区,你如何编写多核 Java 代码??

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

  • isolate:如果根本没有线程与任何其他线程交互(除了该线程运行的完全本地的东西之外,没有任何东西读/写字段),那么世界上所有的硬币翻转都不会改变任何事情,所以这样做。在启动之前正确设置线程,让线程运行到结束,并让它通过安全通道传达它产生的东西(如果你需要返回值),不用担心。
  • streamline comms:不是通过字段进行通信(这里您的线程“通信”;main 以该布尔值的形式与 worker 通信),而是通过为并发控制而设计的通道进行通信:使用 DB (线程 A 写入 DB,线程 B 也这样做,并且与 A 具有相同的表和行;DB 具有管理这一点的设施)或消息队列。这可以是一个成熟的库,比如 rabbitMQ,或者是来自java.util.concurrent 的集合类型,比如BlockingQueue。这些都为您解决了CB/CA问题。
  • 依赖框架:网络框架有很多线程,并且会适当地调用你的“网络处理程序”代码。处理程序通过 DB 聊天,Web 框架负责所有线程:您所有的 CPU 内核都在缩放,您无需担心任何硬币翻转。您还可以在另一端使用框架并使用例如fork/join 或 stream->map->filter->collect 的概念(又名 MapReduce)。

*) 不是真的,我给你一个思维模型,告诉你如何思考它,这样你就不会写这个错误。关键是,它可以进行克隆。

【讨论】:

  • 首先非常感谢这个非常详细的答案,非常感谢你在那里所做的!好吧,我明白了你的硬币翻转比喻,我知道所有线程都有自己的堆栈,我猜堆空间也是分开的,并且主线程的内存与其他堆栈分开。因此,在所有“记忆”之间存在记忆障碍,我猜想使用 coinflip 您主要指的是写入字段/变量是否可以通过其他线程!问题是我只是不明白,为什么我这样做,我仍然依赖于“抛硬币”。
  • >线程将在任何时候读取或写入字段时翻转硬币。每个线程都有每个对象的每个字段的>自定义本地克隆副本*,它将从中读取和写入>。但每次这样做,它都会翻转那枚邪恶的硬币。如果硬币正面朝上,它将 > 任意将其值复制到某些或所有其他线程的副本,或者将值从 > 某个其他线程的本地副本复制到自身。在尾巴上,它不这样做,只是使用它的>本地副本。
  • 是的,我明白了,这一定是因为内存障碍。但是我不是通过调用方法来更改同步块中的布尔字段,如下所示: >synchronized (lock) { > worker.setDone(true); > } 从理论上讲,更改应该传递到工作线程,还是我完全理解错误?并且这些线程 run() 函数中的 while 循环不断检查布尔值,为什么它不会暂时中断?!我什至通过 main 方法中的 print 获取信息,即 worker.done=true...
  • 对于格式也很抱歉,sf 声明引用是通过使用“>”完成的,似乎对我不起作用。引用“你在这里这样做了:工作线程(线程,而不是对象)”这真的让我感到困惑,线程与对象有何不同?如果对象布尔值设置为真,为什么不是线程?如果我同步方法或在同步块中调用方法,真正的区别在哪里!?!
  • 关于规则,是的,我们已经通过了它们,称它们为“发生在规则之前”,我也按照您的建议使用 volatile 完成了相同的课程,效果很好。但问题是,我想了解为什么我的关键部分不起作用,因为正如我所说的那样,它对我没有任何意义。当然,您建议只在方法的同步块中折腾回来是可行的,但我看不出在同步中调用 main 中的方法有什么不同。阻止!
猜你喜欢
  • 2017-06-07
  • 2014-10-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-07-14
  • 1970-01-01
  • 2017-04-28
  • 1970-01-01
相关资源
最近更新 更多