我开始在uni学习多线程,当我开始理解这个概念的时候,我也在玩一些例子,以了解它在实践中是如何工作的。我对以下课程的问题是:
它的工作原理与预期的一样,如果我将getter/setter与我的对象锁同步,那么主线程将boolean done设置为true,run()中的while循环中断,main只需等待worker的终止,如join()所述。最后打印出来了,我很高兴。在这里,也许您可以帮助我理解,为什么同步getter/setter for done很重要,但是如果同步getter for count似乎无关紧要。
但我真正的问题是,我不明白,如果我在main方法的同步块/关键部分中设置setter的boolean,为什么工作线程永远不会终止。然后它似乎永远运行,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());
}
}
1条答案
按热度按时间ux6nzvsh1#
我自己运行了这段代码,当您设置了同步块时,它就可以工作了
yield()
调用很快就会返回),但是当你删除它们时,它就不会(应用程序挂起,而yield()
电话似乎从未回过)。这或多或少是人们期待的行为。如果此代码确实从yield()调用中快速返回,那么这也是预期的行为。
欢迎使用java内存模型。
每根线都会得到一枚不公平的硬币。这是不公平的,因为它可以随意和你捣乱,并且每次都以同样的方式翻转,使它看起来像可靠的工作一样,当你给那个重要的客户演示时,它一定会失败。基本规则很简单。如果一个线程抛出了硬币,并且代码的运行方式因其着陆方式的不同而不同,那么您就失去了游戏:您有一个bug,编写一个捕获它的测试几乎是不可能的。因此,像这样编写线程代码是一种需要严格遵守纪律的行为。
线程在读取或写入字段的任何时候都会翻转硬币。每个线程都有一个自定义的本地克隆副本,它将读取和写入每个对象*的每个字段。但每次它这么做的时候,它都会把那枚邪恶的硬币翻过来。如果硬币落在头部,它会任意地将其值复制到其他线程的某些或所有副本,或者将其他线程的本地副本中的值复制到自身。最后,它不这样做,只使用它的本地副本。
在这种情况下,你观察到了(几乎)无休止的尾巴翻转。在现实生活中,你不会连续得到几百万条尾巴,但我有没有提到硬币是不公平的?这很正常。但关键的一点是:vm根本不能保证投币的正确性——一个每次都会投币的vm(因此具有
yield()
快速返回)一样好,会通过tck等。因此:如果掷硬币对你的代码如何运行很重要,那你就把事情搞砸了。
在这里您已经这样做了:工作线程(线程,而不是对象)读取其副本(该副本为false,并且保持为false),主线程将其workerthread的副本(对象,而不是线程)的done标志设置为true。1个字段,但有2个缓存副本,现在我们正在等待一个新的头翻转,然后工作线程才能看到主线程做了什么。
幸运的是,我们可以强制vm不要抛硬币,这样做的途径是建立一种先到先的关系。
java内存模型已经写下了一系列规则,这些规则确定vm将确认“事件a发生在事件b之前”,并保证a所做的任何事情对b都是可见的。没有投币-b的拷贝必然会反映a所做的。
清单很长,但重要的是:
命令式:在一个线程中,任何在另一个线程之前运行的语句都在另一个线程之前。这是“嗯,很明显”的一个:在
{ foo(); bar(); }
,foo所做的任何事情对bar都是可见的,因为相同的线程。synchronized:每当线程退出synchronized-on-object-x块时,如果进入synchronized-on-object-x块的线程在进入synchronized-on-object-x块之前这样做,那么这种情况就会在任何线程进入synchronized-on-object-x块之前发生。
不稳定的:
volatile
是可以放在字段上的关键字。这有点棘手;volatile也是用cb/ca来定义的,但是更容易考虑的是,对volatile变量的任何写入都会迫使硬币翻转头部,将更新写入每个线程的副本。这使得volatile看起来很神奇,但它付出了相当大的代价:volatile相当慢,而且没有您最初想象的那么有用,因为您不能使用它来设置原子操作。线程启动:无论何时启动线程,启动新线程的线程在调用.start()之前所做的任何操作都会立即对新线程可见。
当然,如果某个方法通过将同步规则和命令式规则结合起来,在内部对事物进行同步,那么该方法也有效地充当了一个先到先建因素。你应该使用这个-一堆jdk方法就是这样定义的!
因此,将同步块放回原处,从而在写done标志的主线程和读它的工作线程之间建立cb/ca,代码就可以工作了(事实上,它可以在每个jvm上工作,而不管操作系统、cpu、月相或vm供应商如何)。没有它,这个代码可以做它想做的任何事情。退出或不退出,或4小时后退出,或仅在winamp切换歌曲时退出。那么一切都是公平的,因为你写的代码取决于投币的结果,所以这取决于你。或者,将字段标记为“volatile”,也可以这样做。
那么,如果这是一个雷区,你怎么写多核java代码呢??
真正的答案是:你基本上没有。太复杂了。相反,您可以这样做:
孤立:如果没有线程与任何其他线程进行交互(没有任何线程读取/写入字段,除了该线程运行时完全本地的内容),那么世界上所有的硬币都不会改变任何东西,所以就这样做。在启动线程之前正确设置它,让线程运行到它的末尾,让它通过一个安全的通道传递它产生的东西(如果你需要一个返回值的话),不用担心。
简化通信:而不是通过字段进行通信(这里你的线程“通信”;main以该布尔值的形式与worker通信),通过一个为并发控制而设计的通道进行通信:使用db(线程a写入db,线程b也这样做,并且表和行与a相同;dbs有设施来管理这一点),或一个消息队列。这可以是rabbitmq之类的完整库,也可以是
java.util.concurrent
,例如BlockingQueue
. 这些都为您解决了cb/ca问题。依赖于一个框架:一个web框架有许多线程,并将适当地调用一个线程上的“web处理程序”代码。处理程序通过db聊天,web框架负责所有线程:所有cpu内核都在快速运行,您不必担心任何错误。您也可以在另一端使用框架,并使用例如fork/join或stream->map->filter->collect(又称mapreduce)的概念。