你好!我们继续研究多线程。今天我们将了解
当我们
volatile
关键字和yield()
方法。让我们潜入:)
volatile 关键字
创建多线程应用程序时,我们会遇到两个严重的问题。 首先,当多线程应用程序运行时,不同的线程可以缓存变量的值(我们已经在标题为“使用 volatile”的课程中讨论过这个)。您可能会遇到这样的情况,其中一个线程更改了变量的值,但第二个线程看不到更改,因为它正在使用变量的缓存副本。 自然,后果可能很严重。假设它不仅仅是任何旧变量,而是您的银行账户余额,它突然开始随机上下跳跃:) 这听起来并不有趣,对吧? 其次,在 Java 中,读写所有原始类型的操作,long
double
, 是原子的。 好吧,例如,如果您int
在一个线程上更改变量的值,而在另一个线程上读取该变量的值,您将获得它的旧值或新值,即更改产生的值在线程 1 中。没有“中间值”。但是,这不适用于long
s 和double
s。为什么? 因为跨平台支持。 还记得我们在开始阶段说过 Java 的指导原则是“一次编写,随处运行”吗?这意味着跨平台支持。换句话说,Java 应用程序可以运行在各种不同的平台上。例如,在 Windows 操作系统、不同版本的 Linux 或 MacOS 上。它将在所有这些上顺利运行。称重 64 位,long
double
是 Java 中“最重”的原语。并且某些 32 位平台根本不实现 64 位变量的原子读写。此类变量在两个操作中读取和写入。首先,将前 32 位写入变量,然后再写入另外 32 位。结果,可能出现问题。一个线程将一些 64 位值写入一个X
变量,并在两个操作中完成。同时,第二个线程尝试读取变量的值,并在这两个操作之间执行 - 当前 32 位已写入但后 32 位尚未写入时。结果,它读取了一个中间的、不正确的值,我们遇到了一个错误。例如,如果在这样的平台上我们尝试将数字写入 9223372036854775809 对于一个变量,它将占用 64 位。在二进制形式中,它看起来像这样: 10000000000000000000000000000000000000000000000000000000000000001 第一个线程开始将数字写入变量。首先,它写入前 32 位 (1000000000000000000000000000000) ,然后写入第二个 32 位 (00000000000000000000000000000001) 第二个线程可以插入这些操作之间,读取变量的中间值 (1000000000000000000000000000000),这是已经写入的前 32 位。在十进制中,这个数字是 2,147,483,648。换句话说,我们只是想将数字 9223372036854775809 写入一个变量,但由于这个操作在某些平台上不是原子的,我们有一个邪恶的数字 2,147,483,648,它不知从哪里冒出来,会对程序。第二个线程在完成写入之前简单地读取变量的值,即线程看到前 32 位,但看不到后 32 位。当然,这些问题不是昨天出现的。Java 用一个关键字解决了它们:volatile
. 如果我们使用volatile
在我们的程序中声明一些变量时的关键字......
public class Main {
public volatile long x = 2222222222222222222L;
public static void main(String[] args) {
}
}
…代表着:
- 它将始终以原子方式读取和写入。即使它是 64 位
double
或long
. - Java 机器不会缓存它。因此,您不会遇到 10 个线程正在使用它们自己的本地副本的情况。
yield() 方法
我们已经复习了该Thread
课程的许多方法,但有一个重要的方法对您来说是全新的。就是yield()
方法。它的功能正如它的名字所暗示的那样! 
yield
在一个线程上调用该方法时,它实际上会与其他线程对话:'嘿,伙计们。我并不特别着急去任何地方,所以如果对你们中的任何人来说获得处理器时间很重要,那就拿去吧——我可以等。这是一个简单的例子,说明它是如何工作的:
public class ThreadExample extends Thread {
public ThreadExample() {
this.start();
}
public void run() {
System.out.println(Thread.currentThread().getName() + " yields its place to others");
Thread.yield();
System.out.println(Thread.currentThread().getName() + " has finished executing.");
}
public static void main(String[] args) {
new ThreadExample();
new ThreadExample();
new ThreadExample();
}
}
我们依次创建并启动三个线程:Thread-0
、Thread-1
和Thread-2
。 Thread-0
首先开始并立即屈服于其他人。然后Thread-1
开始,也产量。然后Thread-2
开始,这也产生了。我们没有更多的线程,在Thread-2
让出它最后的位置后,线程调度程序说,‘嗯,没有更多的新线程了。队列中有谁?谁先让位了Thread-2
?好像是Thread-1
。好的,这意味着我们让它运行'。 Thread-1
完成它的工作,然后线程调度程序继续它的协调:'好的,Thread-1
完成了。我们还有其他人在排队吗?Thread-0 在队列中:它刚刚让出它的位置Thread-1
. 现在轮到它并运行完成。然后调度程序完成协调线程:'好吧,Thread-2
你屈服于其他线程,它们现在都完成了。你是最后一个屈服的,所以现在轮到你了。然后Thread-2
运行完成。 控制台输出将如下所示: Thread-0 将其位置让给其他 Thread-1 将其位置让给其他 Thread-2 将其位置让给其他 Thread-1 已完成执行。Thread-0 已完成执行。Thread-2 已完成执行。 当然,线程调度程序可能会以不同的顺序启动线程(例如,2-1-0 而不是 0-1-2),但原理是一样的。
happens-before 规则
我们今天要谈的最后一件事是“发生在之前”的概念。如您所知,在 Java 中,线程调度程序执行涉及为线程分配时间和资源以执行其任务的大部分工作。您还反复看到线程如何以通常无法预测的随机顺序执行。通常,在我们之前进行的“顺序”编程之后,多线程编程看起来像是随机的。您已经开始相信可以使用许多方法来控制多线程程序的流程。但是 Java 中的多线程还有一个支柱 — 4 个“ happens-before ”规则。理解这些规则非常简单。想象一下,我们有两个线程——A
并且B
. 这些线程中的每一个都可以执行操作1
和2
。在每个规则中,当我们说“ A happens-before BA
”时,我们的意思是线程在操作之前所做的所有更改以及此操作导致的更改在执行操作时和之后1
对线程可见。每条规则保证当你编写一个多线程程序时,某些事件将在 100% 的时间内发生在其他事件之前,并且在运行时线程将始终知道该线程在运行期间所做的更改。让我们回顾一下。 B
2
2
B
A
1
规则1。
释放互斥量发生在同一监视器被另一个线程获取之前。我想你明白这里的一切。如果一个对象或类的互斥锁被一个线程获取,例如被线程获取A
,则另一个线程 (thread B
) 无法同时获取它。它必须等到互斥量被释放。
规则 2。
该Thread.start()
方法发生在 Thread.run()
. 同样,这里没有什么困难。您已经知道要开始运行方法内的代码,您必须在线程上run()
调用该方法。start()
具体来说,是启动方法,而不是run()
方法本身!此规则确保在调用之前设置的所有变量的值在方法开始后将在方法 Thread.start()
内可见。run()
规则 3。
run()
方法结束发生在方法返回之前join()
。让我们回到我们的两个线程:A
和B
。我们调用该join()
方法以确保线程在执行其工作之前B
等待线程完成。A
这意味着 A 对象的run()
方法保证运行到最后。run()
并且线程方法中发生的所有数据更改A
都将 100% 保证在线程B
完成后在线程中可见,等待线程A
完成其工作以便它可以开始自己的工作。
规则 4。
写入volatile
变量发生在从同一变量读取之前。当我们使用volatile
关键字时,我们实际上总是得到当前值。即使是long
or double
(我们之前谈到过这里可能发生的问题)。 正如您已经了解的那样,对某些线程所做的更改并不总是对其他线程可见。但是,当然,这种行为不适合我们的情况非常常见。假设我们给线程上的一个变量赋值A
:
int z;
….
z = 555;
如果我们的B
线程应该在控制台上显示变量的值z
,它很容易显示 0,因为它不知道分配的值。但是规则 4 保证,如果我们将z
变量声明为volatile
,那么在一个线程上对其值的更改将始终在另一个线程上可见。如果我们将单词添加volatile
到之前的代码中......
volatile int z;
….
z = 555;
...然后我们防止线程可能显示 0 的情况。B
写入volatile
变量发生在读取变量之前。
GO TO FULL VERSION