当前位置: 首页 > 科技观察

我真的不想学习Happens-Before!

时间:2023-03-16 14:41:53 科技观察

我觉得这是大家学习Java并发编程时容易忽略的一点。为什么,因为太抽象了。刚开始学习的时候,遇到happens-before的时候,还没有意识到,“哪里来的这么破的东西?”!Happens-before不像任何Java并发工具类那样简单易懂,易于使用。happens-before侧重于理解。happens-before与JMM有关,JMM是Java的内存模型,所以我们需要先从JMM入手,才能更好的理解happens-before的原理。JMM的设计JMM是JVM的基础,因为JVM中的堆区、方法区、栈区都是基于JMM的,你可能还是不明白是怎么回事,没关系,我们来看看在JMM第一个模型。JVM的划分想必大家都很清楚,这里就不赘述了。下面主要说说JMM中JVM各个区域的分布。JVM中的栈区包括局部变量和操作数栈。局部变量在线程之间独立存在,即线程之间互不干扰,变量的值只受当前线程的影响。叫做闭口。但是,线程间的共享变量是存放在主内存(MainMemory)中的,共享变量是JVM堆区的重要组成部分。那么,共享变量是如何受到影响的呢?其实在操作系统层面是有办法解决进程通信的:共享内存,而主内存其实就是共享内存。共享变量之所以会受到影响,是因为每个Java线程在代码执行过程中都会将主内存中的共享变量副本加载到工作内存中。每个Java线程修改工作内存中共享变量的副本后,会将共享变量存储到主内存中。由于不同线程对共享变量的修改是不同的,并且各个线程对共享变量的修改是相互不可见的,因此,当内存中共享变量的值最终被覆盖时,可能会出现重复覆盖,这也是使共享变量不安全的一个因素。由于JMM的这种设计,存在我们常说的可见性和顺序性的问题。关于可见性以及如何解决Java并发编程中的可见性问题,我们在volatile一文中有详细介绍。事实上,volatile在解决可见性问题的同时,也遵循happens-before原则。happens-before原则JSR-133使用happens-before原则来指定两个操作之间的执行顺序。这两个操作可以在同一个线程内,也可以在不同线程之间。在同一个线程内,可以使用as-if-serial语义来保证可见性,所以happens-before原则更多的是用来解决不同线程之间的可见性。JSR-133对于happens-before关系有如下定义,我们分别解释一下。程序顺序规则线程中的每个动作都先于该线程中的每个后续动作发生。每个线程在执行指令的过程中相当于一个顺序的执行过程:取指令、执行、指向下一条指令、取指令、执行。程序顺序规则是说在同一个顺序执行流程中,代码会按照程序代码编写的先后顺序执行,写在前面的代码操作应该先于写在后面的代码操作发生。这里需要特别注意的一件事是,这些操作的顺序是针对同一个线程的。监视器规则监视器上的解锁发生在该监视器上的每个后续锁定之前。这是monitor监控器的一个规则,主要是lock和unlock,即加锁和解锁。这个规则是针对同一个监视器的,这个监视器的解锁(unlock)应该发生在这个监视器的锁定(lock)之前。例如下面的代码classmonitorLock{privateintvalue=0;publicsynchronizedintgetValue(){returnvalue;}publicsynchronizedvoidsetValue(intvalue){this.value=value;}}在这段代码中,getValue和setValue两个方法使用同一个监视器锁,假设A线程正在执行getValue方法,B线程正在执行setValue方法。monitor的原则会规定线程B对value的修改对线程A是直接可见的,如果getValue和setValue没有用synchronized关键字修饰,不保证线程B对value的修改是可见的到线程A。同步语义的监视器规则与ReentrantLock中的锁定和解锁规则相同。volatile规则对volatile字段的写入发生在该volatile的每次后续读取之前。嗯,这个规则其实就是volatile语义的规则,因为在写volatile和读volatile之间会加一个memorybarrier,也就是内存屏障。内存屏障,也称为栅栏,是一种低级原语。它使CPU或编译器在对内存进行操作时严格按照一定的顺序执行,也就是说内存屏障之前的指令和内存屏障之后的指令不会因为系统优化等原因而乱序原因。线程启动规则线程上对start()的调用happens-before已启动线程中的任何操作。此规则也适用于同一线程。对于同一个线程,调用线程启动方法之前的所有操作都发生在-启动方法之后的任何操作。这个原理也可以这样理解:当调用start方法时,会把start方法之前所有操作的结果同步到主存中。新线程创建后,需要从主存中获取数据。这样,在start方法调用之前的所有操作的结果对于新创建的线程都是可见的。让我为你画一幅画。可以看出线程A在执行ThreadB.start方法前会修改共享变量,修改后的共享变量会直接刷新到内存中,然后线程A执行ThreadB.start方法,然后线程B会从中读取内存获取共享变量。线程连接规则线程中的所有操作发生在任何其他线程从该线程上的join()成功返回之前。此规则适用于多线程:如果线程A执行操作ThreadB.join()并成功返回,则线程B中的任何操作都会发生-在线程A从ThreadB.join操作成功返回之前。假设有两个线程s,t,在线程s中调用t.join()方法。然后线程s会被挂起,等待线程t运行完毕再恢复执行。当t.join()成功返回时,s线程知道t线程已经结束。因此,根据这个原则,t线程中共享变量的修改对s线程是可见的。同样,Thread.isAlive方法也可以检测线程是否已经结束。线程传递规则如果一个动作a发生在动作b之前,并且b发生在动作c之前,那么a发生在c之前。这是happens-before的最后一条规则,主要讲操作之间的传递性,即如果Ahappens-beforeB,Bhappens-beforeC,则Ahappens-beforeC。线程传递规则不和上面的其他规则一样有单独的用法,主要和volatile规则,start规则,join规则一起使用。和volatile规则一起使用,比如现在有四种操作:普通写,易失写,易失读,普通读。线程A执行普通写和volatile写,线程B执行volatile读和普通读。按照程序的顺序,普通Write发生-volatile写之前,volatile读发生-normal读之前,根据volatile规则,线程的volatile写发生-volatile读和normal读之前,并且按照线程传递规则,正常写入也会发生-在正常读取之前。与start()规则和开始规则一起使用。其实上面我们已经描述了启动规则,但是上图中少了一行,就是ThreadB.starthappens-beforethreadBreadsharingVariables,因为ThreadB.start需要happen-beforethreadBstartto执行,但是按照程序定义的顺序,线程B的执行happens-before线程B读取共享变量,所以根据线程传递规则,线程A修改共享变量happens-before线程B读取共享变量变量,如下图所示。与join()规则一起使用时假定线程A正在执行并等待线程B通过执行ThreadB.join终止。另外,假设线程B在终止前修改了一些共享变量,线程A从ThreadB.join返回后将读取这些共享变量。上图中,2happens-before4是由join规则生成的,而4happens-before5是程序顺序规则,所以根据线程传递规则,会出现2happens-before5,这也意味着线程A执行操作ThreadB.join成功返回后,线程B中的任何操作都将对线程A可见。