【并发编程】- JMM和happens-before规则 - Go语言中文社区

【并发编程】- JMM和happens-before规则


涉及到并发,我们通常会想到两方面,一方面是使用多线程实现程序并发,另一方面是使用锁防止程序并发。不管是哪一方面都涉及到两大核心,三大性质。

两大核心分别是Java内存模型、Happen-Before原则。三大性质分别是原子性、可见性、有序性。

在并发编程中主要需要解决两个问题:一、线程之间如何通信;二、线程之间如何完成同步。通信是指线程之间以何种机制来交换信息,主要有两种方式:共享内存和消息传递。而Java内存模型就是共享内存的并发模型,也可以说Java内存模型是共性内存方式的基础,线程之间主要通过读-写共享变量来完成隐式通信。

Java内存模型

Java内存模型,即JMM(Java Memory Model),规定了所有的共享变量都存储在主内存(Main Memory)中,每个线程拥有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的主内存中变量的的拷贝副本,线程改变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接操作主内存中的变量。JMM就从抽象层次定义了这种方式,并且JMM决定了一个线程对共享变量的写入何时对其他线程是可见的

结构

Java内存模型包括主内存和工作内存。

目标

Java内存模型的主要目标是定义程序中各个变量的访问规则,即在JVM中将变量存储到内存和从内存中取出变量这样的底层细节。

作用

在并发编程中,我们通常需要考虑线程间如何通信。线程之间的通信机制有两种:共享内存消息传递。

而JMM的目标正是共享内存通信方式的基础,JMM决定一个线程对共享变量的写入何时对另一个线程可见,所以说JMM控制了变量在主内存和工作内存读写方式,也就控制了共享数据的公共状态,保证了共享数据的安全。

共性内存方式通信步骤

  1. 线程A从主内存中将共享变量读入线程A的工作内存后进行操作,之后将数据重新写会到主内存
  2. 线程A将数据写回主内存后,其他线程的本地变量会失效,然后线程B从主内存中读取最新的共享变量,从而保证共享变量的安全性

隐式通信

上边提到了隐式通信,何为隐式通信?线程A和线程B就好像通过共享变量在进行隐式通信。这其中有很有意思的问题,如果线程A更新后数据并没有及时写回到主存,而此时线程B读到的是过期的数据,这就出现了“脏读”现象。可以通过同步机制(控制不同线程间操作发生的相对顺序)来解决或者通过volatile关键字使得每次volatile变量都能够强制刷新到主存,从而对每个线程都是可见的。

重排序

java内存模型为了提高性能,提供并发性,会运行编译器和处理区常常会对指令进行重排序,从而减少等待,提高性能。一般重排序可以分为如下三种:

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
  2. 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果**不存在数据依赖性**,处理器可以改变语句对应机器指令的执行顺序;
  3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。

如图,1属于编译器重排序,而2和3统称为处理器重排序。这些重排序会导致线程安全的问题,一个很经典的例子就是单例双重锁问题。针对编译器重排序,JMM的编译器重排序规则会禁止一些特定类型的编译器重排序;针对处理器重排序,编译器在生成指令序列的时候会通过插入内存屏障指令来禁止某些特殊的处理器重排序。

Happens-Before

重排序确实可以提升性能,但是有时候会造成实际结果有我们所期望的结果不符,所以,JMM又提供了happers-before。而happens-before所做的事就是,每一条规则对应一个或多个编译器和处理器重排序规则,对于程序员而言,happens-before相当于对于重排序规则以及具体实现做了封装,方便程序员使用,程序员只需要理解happens-before规则就好,不需要去理解复杂的重排序相关的事儿。

定义

happens-before指定两个操作之间的执行顺序。具体定义为

  1. 如果一个操作Happens-Before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前;
  2. 两个操作之间存在Happens-Before关系,并不意味着Java平台的具体实现必须要按照Happens-Before关系指定的顺序来执行。如果重排序之后的结果,与按照Happens-Before关系来执行的结果一致,那么这种重排序并不非法。

第一条是对程序员而言,方便程序员使用,可以这样理解happens-before关系:如果A happens-before B,那么Java内存模型将向程序员保证——A操作的结果将对B可见,且A的执行顺序排在B之前。注意,这只是Java内存模型向程序员做出的保证!实际并不是真正的不可重排序。

第二条是编译器和处理器重排序而言的约束原则。只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。所以说,第一条只是方便程序员使用时方便,程序员关系的只是结果,只要结果不变,是否真的被重排序并不重要。

happens-before的具体规则

  1. 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  2. 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  3. volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  4. 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
  5. start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
  6. join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
  7.  程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。
  8. 对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。

 

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/ldb987/article/details/90346385
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-02-25 01:17:15
  • 阅读 ( 763 )
  • 分类:

0 条评论

请先 登录 后评论

官方社群

GO教程

推荐文章

猜你喜欢