《Java中的Happens-Before》或《如何编写线程安全的应用程序》

Happens-Before解决了主要多线程问题

在我们开始学习“发生”这个概念之前,我们必须了解创造它的原因。一旦使用了多线程,您的代码可能会变得不一致,因为线程之间的共享对象可能具有不同且不可预知的值。让我们回顾一个简单的例子。在这个例子中,我们将在一个线程中更新值,并在另一个线程中读取和打印它们。

预期行为(错误预期)

现在,让我们考虑一下在打印时,在第二个线程中可能会看到哪些值。根据第一个线程的进程,我们可能会看到3种情况:

  1. XX and 和yy 都没有初始化,所以呢0 and 和0 将打印。
  2. XX 设置和yy 还没有确定;然后,1 and 和0 将打印。
  3. XX 和?设置;然后,1 and 和1将打印。

现在让我们运行程序一百万次,看看输出了哪些值。

在一百万份打印文件中,只有三种不同的选项被打印出来(我的硬件、Windows操作系统和我的JDK)。由于某种原因,B没有出现因为它不像0,1对那样令人困惑,这看起来是不可能的。我们唯一要知道的是结果是不可预测的。

为什么共享内存可能不一致

重新排序或JVM优化

JVM可以改变指令的顺序,这不会对程序造成任何重大的改变。例如,它可能会改变变量初始化的顺序。

CPU Memory Cache

每个线程可以在单独的CPU中执行。CPU有自己的缓存,复制了不同的变量。当一个线程在CPU中更新这个值时,另一个线程仍然会有一个过时的值。我们可以这样来说明:

两个线程在重新同步处理共享RAM内存中的数据后从共享RAM内存中读取数据。默认情况下,没有强制CPU更新共享RAM内存值。

什么是真正重要的:线程安全的代码

前面解释的一切都只对通过面试有价值;但在实践中,所有开发人员需要的是一种在多线程环境中可预测和一致地编写代码的方法,也称为线程安全代码。幸运的是,Java提供了编写此类代码的方法。

保证了相同字段在不同线程中的可见性

happens-before关系在Oracle文档17.4.5章中有描述。如果在两个线程之间安装happens-before关系,那么第二个线程(结束线程)将看到第一个线程中发生的所有更改。因此,第二个线程的执行将类似于单线程应用程序。

安装Happens-Before关系:使字段可见(使用Volatile关键字)

安装happens-before关系的第一种方法是使用volatile关键字标记共享变量。如果变量是volatile,那么在每次写操作和后续读操作之间都会安装happens-before关系。

对volatile字段(§8.3.1.4)的写操作发生在每次后续读取该字段之前。

这意味着:

  • Once 在第一个线程中更新,它将在第二个线程中可见。
  • When 在第二个线程中可见,所有字段都在之前设置YY.
  • 考虑到这一点,为了保持x和y的一致性,我们必须做出以下更改:

  • 使YY 不稳定。
  • 使XX 阅读后YY 在第二个线程中。
  • 检查YY 被设置为1.
  • 考虑到所有的变化,我们以下面的方式重写代码:

    这可能有点复杂。您必须注意的一件事是,蓝色门户意味着安装关系之前发生(起点),而黄色门户意味着完成关系之前发生(并且“传递”的所有数据都以最新的状态接收)。

    安装Happens-Before关系:有序访问字段(使用同步监视器)

    Java提供了一种方法来组织对指令的有序访问,称为Java监视器。我在三篇文章中描述了它是什么以及它是如何工作的:

  • 多线程Java和面试第1部分:介绍
  • 多线程Java和访谈第2部分:互斥锁,Java监控模型
  • 多线程Java和面试第3部分:全部等待和通知
  • 这个问题相当复杂。我也推荐阅读以下文章:

  • What is a Monitor in Computer Science?
  • Guide to the Synchronized Keyword in Java
  • 在任何锁获取和锁释放之间安装happens-before关系。因此,如果我们使用锁重写代码,我们也有happens-before保证:

    正如您所看到的,这两个字段都不是volatile;但是,当第一个线程更新该值并释放锁时,然后在第二个线程获得锁时,由于happens -before关系,该值也会得到更新。此规则适用于Java .util.concurrent中在底层使用Java监视器的所有锁。

    安装Happens-Before关系:线程开始和连接

    与前两种情况相比,这可能是最简单的情况。Java规范说:

    -对线程的start()调用发生在启动线程的任何操作之前。

    -线程中的所有操作都发生在其他线程成功从该线程的join()返回之前。

    当线程启动时,它会看到父线程共享变量的正确状态。当父线程在join()之后被释放时,这是向后的:它接收子线程的最新状态。

    让我们重写我们的例子,并说明当我们调用start()方法时,bean数据是如何传递给子线程的:

    这个例子解释说,由于安装了happens-before关系,以及start()方法,主线程中设置的值被成功地传递给了子线程。

    现在让我们来演示相同的逻辑,但当数据通过join()方法从子节点传递给父节点时:

    在本例中,我们将数据从子线程传递给等待的主线程。一旦子线程结束运行并释放连接,由于happens-before关系的存在,值将被正确传递。

    示例是否可用于生产并推荐使用?

    不,不,不!我提供的例子只有一个目的:解释发生了什么——以前是什么,以及它是如何工作的。在实践中,在多线程环境中再小心也不为过。为了更好地避免以下内容:

  • 在第一个可见性(volatile关键字)的例子中,我只将一个字段标记为volatile。代码将按预期工作;但在实践中,你可以改变阅读的顺序,“破碎”就发生了——在恋爱之前。
  • 对于同步监视器,我没有将字段标记为volatile,因为这不是必需的。不要这样做。保持字段为volatile,即使它们在锁内被更改。
  • 欢迎建设性讨论

    happens-before主题是最复杂的主题之一,通常是高级Java访谈的一部分。如果你不同意或者你想补充一些相关的东西,我很高兴在评论中有一个建设性的讨论。感谢你的阅读。

    声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

    上一篇 2022年6月27日
    下一篇 2022年6月27日

    相关推荐