您的位置:首页 > 编程语言 > Java开发

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

2018-02-10 22:45 507 查看
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。

重排序分3种类型。

编译器优化的重排序。编译器在不改变单线程寓意的前提下,重新安排语句的执行顺序。

指令级并行的重排序。cpu将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应的机器指令的执行顺序。

内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
重排序既然是优化,在单线程下是不会影响宏观上的代码执行顺序的。但是在多线程并发的情况下就不能保障了。因为宏观上的一行代码,对cpu来说对应很多个指令行。

这些重排序可能会导致线程程序出现内存可见性问题。

  比方,一个线程执行两行代码:a = 1; flag = true; 而另外一个线程执行if(flag){ a = 2 }。如果前者线程发生重排序,并发的时候后者线程就可能发生线程安全问题。(本来前者线程执行到a=1的时候flag还是false呢,结果由于重排序先执行了,就导致后者线程进入了if(){}中)

第1点由编译器引起,JMM的重排序规则会禁止特定类型的编译器重排序。

针对第2点,JMM要求编译后的指令对要求禁止重排序的地方插入特性类型的内存屏障(memory barriers/fence)来禁止特性类型的处理器重排序。

针对第3点,JMM提出了内存一致性模型

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

顺序一致性内存模型有两大特性。

一个线程中的所有操作必须按照程序的顺序来执行。

(不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。

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

需要重点理解的happens-before

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

happens-before规则:

程序顺序规则:在一个单独的线程中,按照程序代码的执行顺序,先执行的操作happens—before后执行的操作。

管理锁定规则:一个unlock操作happens—before后面对同一个锁的lock操作。

volatile变量规则:对一个volatile变量的写操作happens—before后面对该变量的读操作。

传递性:如果A happens-before B 、B happens-before C 那么A happens-before C 。

start()规则:线程A内执行ThreadB.start(),那么 A线程的ThreadB.start()操作happens-before于线程B中的任意操作。

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。

cpu原子性的实现方式:

总线锁。要操作的内存区域与cpu之间的通道被锁住,其他处理器不能操作该区域。效率低。

缓存锁。如果要操作的数据在CPU的高速缓存中。使用 缓存一致性 来保证各处理器缓存的一致。效率高。

java原子性的实现方式:

循环CAS。比如AtomicXxx类的加减和赋值操作。

锁机制

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

ABA的问题,即使引入版本比较实现比较复杂;

循环时间长的话开销大,白白浪费了cpu资源。

只能保障一个共享变量的原子操作。

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

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

保证有序性的关键字有:voliatile、synchronized(lock)

参考资料:

《Java并发编程的艺术》

《深入理解Java虚拟机:JVM高级特性与最佳实践》

以上内容为笔者日常琐屑积累,已无从考究引用。如果有,请站内信提示。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: