我理解的Java并发基础(二):happens-before、可见性与原子性

重排序是指编译器和处理器为了优化程序性能而对指令序列进行从新排序的一种手段。
重排序分3种类型。java

  1. 编译器优化的重排序。编译器在不改变单线程寓意的前提下,从新安排语句的执行顺序。
  2. 指令级并行的重排序。cpu将多条指令重叠执行。若是不存在数据依赖性,处理器能够改变语句对应的机器指令的执行顺序。
  3. 内存系统的重排序。因为处理器使用缓存和读/写缓冲区,这使得加载和存储操做看上去多是在乱序执行。 重排序既然是优化,在单线程下是不会影响宏观上的代码执行顺序的。可是在多线程并发的状况下就不能保障了。由于宏观上的一行代码,对cpu来讲对应不少个指令行。

这些重排序可能会致使线程程序出现内存可见性问题。
  比方,一个线程执行两行代码:a = 1; flag = true; 而另一个线程执行if(flag){ a = 2 }。若是前者线程发生重排序,并发的时候后者线程就可能发生线程安全问题。(原本前者线程执行到a=1的时候flag仍是false呢,结果因为重排序先执行了,就致使后者线程进入了if(){}中)程序员

  • 第1点由编译器引发,JMM的重排序规则会禁止特定类型的编译器重排序。
  • 针对第2点,JMM要求编译后的指令对要求禁止重排序的地方插入特性类型的内存屏障(memory barriers/fence)来禁止特性类型的处理器重排序。
  • 针对第3点,JMM提出了内存一致性模型

顺序一致性内存模型是一个被计算机科学家理想化了的理论参考模型,它为程序员提供了极强的内存可见性保证。
顺序一致性内存模型有两大特性。数据库

  1. 一个线程中的全部操做必须按照程序的顺序来执行。
  2. (无论程序是否同步)全部线程都只能看到一个单一的操做执行顺序。在顺序一致性内存模型中,每一个操做都必须原子执行且马上对全部线程可见。

  在概念上,顺序一致性模型有一个单一的全局内存,这个内存经过一个左右摆动的开关能够链接到任意一个线程,同时每个线程必须按照程序的顺序来执行内存读/写操做,在任意时间点最多只能有一个线程能够链接到内存。当多个线程并发执行时,全部线程的全部内存读/写操做按照调度执行串行化编程

须要重点理解的happens-before
  为了不java程序员理解复杂的跟cpu指令相关的内存屏障来保证重排序规则,java使用了happens-before的概念来阐述操做之间的内存可见性。在JMM中,若是一个操做执行的结果须要对另外一个操做可见,那么两个操做之间必需要存在happens-before的关系。这两个操做能够在同一个线程内,也能够在不一样线程内。缓存

happens-before规则:安全

  1. 程序顺序规则:在一个单独的线程中,按照程序代码的执行顺序,先执行的操做happens—before后执行的操做。
  2. 管理锁定规则:一个unlock操做happens—before后面对同一个锁的lock操做。
  3. volatile变量规则:对一个volatile变量的写操做happens—before后面对该变量的读操做。
  4. 传递性:若是A happens-before B 、B happens-before C 那么A happens-before C 。
  5. start()规则:线程A内执行ThreadB.start(),那么 A线程的ThreadB.start()操做happens-before于线程B中的任意操做。
  6. join()规则: 若是线程A执行操做ThreadB.join()并成功返回, 那么线程B中的任意操做happens-before于线程A从ThreadB.join()操做成功返回。

怎么理解呢?
  把A happens-before B 当作 A的发生B必定是知道的。(不是说多线程中A必定要在B以前发生)多线程

什么是可见性
  各个线程虽然有本身的缓存,但各个线程在使用同一个变量进行运算以前以及运算完成以后,该变量在各个线程中的数据是一致的。并发

什么是原子性
  原子性其实就是告诉cpu不能中断,直到执行完一段指令集以后才能切换。cpu执行完时间片后的任意一个原子指令集以后,都有可能被调度器切换到去执行其余线程。属于执行的最小单元。相似于数据库事务的原子性。
  数据在主内存与线程工做内存的交互,Java虚拟机规范定义了8种原子操做:锁定(lock)、解锁(unlock)、读取(read)、载入(load)、使用(use)、赋值(assign)、存储(store)、写入(write)。
  读取(read)、载入(load)、使用(use)、赋值(assign)、存储(store)、写入(write)这6中操做是最基本的原子性操做。 若是想要实现更多操做组成的原子性操做,可使用关键字lock和unlock或者synchronized。app

cpu原子性的实现方式:性能

  1. 总线锁。要操做的内存区域与cpu之间的通道被锁住,其余处理器不能操做该区域。效率低。
  2. 缓存锁。若是要操做的数据在CPU的高速缓存中。使用 缓存一致性 来保证各处理器缓存的一致。效率高。

java原子性的实现方式:

  1. 循环CAS。好比AtomicXxx类的加减和赋值操做。
  2. 锁机制

循环CAS方式的特色,在低争抢的场景下,操做效率高。缺点也很明显:

  1. ABA的问题,即便引入版本比较实现比较复杂;
  2. 循环时间长的话开销大,白白浪费了cpu资源。
  3. 只能保障一个共享变量的原子操做。

voliatile关键字只保证变量的可见性,禁止指令重排序优化,不是原子性的。

实现可见性的关键字有:voliatile、synchronized(lock)、final
保证有序性的关键字有:voliatile、synchronized(lock)

参考资料:

  • 《Java并发编程的艺术》
  • 《深刻理解Java虚拟机:JVM高级特性与最佳实践》
  • 以上内容为笔者平常琐屑积累,已无从考究引用。若是有,请站内信提示。
相关文章
相关标签/搜索