JavaRush /Java 博客 /Random-ZH /流量管理。易失性关键字和yield()方法

流量管理。易失性关键字和yield()方法

已在 Random-ZH 群组中发布
你好!我们继续研究多线程,今天我们将熟悉一个新的关键字—— volatile 和yield() 方法。让我们弄清楚它是什么:)

关键字易失性

创建多线程应用程序时,我们可能面临两个严重的问题。 首先,在多线程应用程序运行过程中,不同的线程可以缓存变量的值(我们将在“使用 volatile”讲座中详细讨论这一点)。有可能一个线程更改了变量的值,但第二个线程没有看到此更改,因为它正在使用自己的变量缓存副本。当然,后果可能很严重。想象一下,这不仅仅是某种“变量”,而是,例如,你的银行卡余额,突然开始随机地来回跳跃:)不太令人愉快,对吧? 其次,在Java中,除了和之外的所有类型的字段的读写操作都是原子的。longdouble 什么是原子性?好吧,例如,如果您在一个线程中更改变量的值int,并且在另一个线程中读取该变量的值,您将获得其旧值或新值 - 更改后的值线程 1. 那里可能不会出现“中间选项”。但是,这不适用于long和。double为什么?因为它是跨平台的。您还记得我们在第一级时说过的 Java 原则是“一次编写,随处可用”吗?这是跨平台的。也就是说,Java 应用程序运行在完全不同的平台上。例如,在Windows操作系统、不同版本的Linux或MacOS以及任何地方,该应用程序都可以稳定运行。 long- doubleJava 中最“重”的原语:它们有 64 位。而有些32位平台根本就没有实现读写64位变量的原子性。此类变量的读取和写入分两次操作。首先,前 32 位被写入变量,然后是另外 32 位。因此,在这些情况下可能会出现问题。一个线程将一些 64 位值写入变量Х,他“分两步”完成。同时,第二个线程尝试读取该变量的值,并在中间执行此操作,此时前 32 位已被写入,但第二个尚未写入。结果,它读取了一个不正确的中间值,并发生错误。例如,如果在这样的平台上我们尝试将数字写入变量 - 9223372036854775809 - 它将占用 64 位。以二进制形式,它看起来像这样: 100000000000000000000000000000000000000000000000000000000000000001 第一个线程将开始将此数字写入变量,并首先写入前 32 位: 100000000000000000000000 0000 00000 然后第二个 32: 00000000000000000000000000000001 第二个线程可以楔入这个间隙并读取变量的中间值 - 100000000000000000000000000000000,即已写入的前 32 位。在十进制中,这个数字等于2147483648。也就是说,我们只是想将数字9223372036854775809写入变量,但由于这个操作在某些平台上不是原子的,所以我们得到了“左边”的数字2147483648 ,我们不需要它,但它会如何影响程序的运行尚不清楚。第二个线程只是在最终写入变量之前读取该变量的值,也就是说,它看到了前 32 位,但看不到第二个 32 位。当然,这些问题并不是昨天才出现的,在 Java 中,这些问题只需使用一个关键字volatile就可以解决。如果我们在程序中声明某个变量,并使用单词“易失性”...
public class Main {

   public volatile long x = 2222222222222222222L;

   public static void main(String[] args) {

   }
}
…代表着:
  1. 它将始终以原子方式读取和写入。即使它是 64 位doublelong.
  2. Java机器不会缓存它。因此排除了 10 个线程与其本地副本一起工作的情况。
这就是如何一言解决两个非常严重的问题:)

yield() 方法

我们已经了解了该类的许多方法Thread,但有一个重要的方法对您来说是新的。这就是yield()方法。从英语翻译过来就是“屈服”。这正是该方法的作用! 流量管理。 volatile 关键字和yield() 方法 - 2当我们在一个线程上调用yield方法时,它实际上是在对其他线程说:“好吧,伙计们,我并不着急,所以如果对你们中的任何一个人来说获得CPU时间很重要,那就接受吧,我很高兴。”不紧急。” 这是一个简单的例子来说明它是如何工作的:
public class ThreadExample extends Thread {

   public ThreadExample() {
       this.start();
   }

   public void run() {

       System.out.println(Thread.currentThread().getName() + "give way 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-0Thread-1Thread-2Thread-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),但原理是相同的。

发生在规则之前

今天我们要讨论的最后一件事是“发生在之前”原则。如您所知,在 Java 中,为线程分配时间和资源以完成其任务的大部分工作都是由线程调度程序完成的。此外,您不止一次看到线程如何以任意顺序执行,并且大多数情况下无法预测它。一般来说,在我们之前进行的“顺序”编程之后,多线程看起来像是一个随机的事情。正如您已经看到的,可以使用一整套方法来控制多线程程序的进度。但除此之外,Java 多线程中还有另一个“稳定岛”——称为“ happens-before ”的 4 条规则。从英语字面意思来看,这被翻译为“happens before”或“happens before”。这些规则的含义很容易理解。想象一下我们有两个线程 -AB。这些线程中的每一个都可以执行操作12。当在每个规则中我们说“ A发生在B之前”时,这意味着线程A在操作之前所做的所有更改以及该操作所带来的更改在执行操作时1对线程可见,并且执行操作后。这些规则中的每一个都确保在编写多线程程序时,某些事件在 100% 的时间里会先于其他事件发生,并且操作时的线程将始终知道该线程在操作期间所做的更改。让我们看看它们。 B2B2А1

规则1。

释放互斥体发生在另一个线程获取同一监视器之前。嗯,这里一切似乎都清楚了。如果某个对象或类的互斥量被一个线程(例如 一个 thread )获取,则А另一个线程( thread B)不能同时获取它。您需要等待直到互斥锁被释放。

规则 2.

发生在方法Thread.start() 之前 Thread.run()。也没什么复杂的。您已经知道:为了让方法内的代码开始执行run(),您需要在线程上调用该方法start()。这是他的,而不是方法本身run()!该规则确保Thread.start()执行前设置的所有变量的值在开始执行的方法内部可见run()

规则 3.

方法完成run() 发生在方法退出之前join()。让我们回到我们的两个流 -АB。我们以这样的方式调用该方法join(),即线程B必须等到完成A才能执行其工作。这意味着run()对象A的方法肯定会运行到最后。当线程等待完成并开始工作 时,run()线程方法中发生的所有数据更改都A将在线程中完全可见。BA

规则 4.

写入易失性变量发生在读取同一变量之前。事实上,通过使用 volatile 关键字,我们将始终获得当前值。long即使在和的情况下double,其问题已在前面讨论过。正如您已经了解的那样,某些线程中所做的更改并不总是对其他线程可见。但是,当然,在很多情况下,这种程序行为并不适合我们。假设我们为线程中的变量赋值A
int z;.

z= 555;
如果我们的线程B要将变量的值打印z到控制台,它很容易打印 0,因为它不知道分配给它的值。所以,规则 4 向我们保证:如果你将一个变量声明z为 volatile,那么在一个线程中对其值的更改将始终在另一个线程中可见。如果我们在前面的代码中添加“易失性”一词......
volatile int z;.

z= 555;
B...排除流向控制台输出 0 的情况。写入易失性变量发生在读取之前。
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION