Java核心复习—— 原子性、有序性与Happens-Before
一、 产生并发Bug的源头
- 可见性
- 缓存导致的可见性问题
- 原子性
- 线程切换带来的原子性问题
- 有序性
- 编译优化带来的有序性问题
上面讲到了 volatile 与可见性,本章再主要讲下原子性、有序性与Happens-Before规则。
二、线程切换带来的原子性问题

count += 1 这一句高级语言的语句,往往需要多条CPU执令。可以分为3步:
- 将count值加载到寄存器
- 在寄存器中对count进行+1操作
- 将count值写回内存

所以,我们需要在高级语言的层面上,确保一些操作是原子性操作。
三、编译优化带来的有序性问题
编译器为了优化性能,有时会改变程序中语句的先后顺序。
a = 6;
b = 7;
经过编译优化后,可能会变成
b = 7;
a = 6
双重检查创建单例对象
public class Singleton{
static Singleton instance;
static Singleton getInstance(){
if(instance == null){
synchronized(Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
}
}
这个例子看似很完美,但其实是可能触发空指针异常。
为什么可能会触发空指针异常。
假设getInstance()的运行过程是这样:
- 开辟一块M内存空间
- 在M内存空间上创建Singleton对象
- 将对象赋值给instance
这样的话是没问题的。但编译时并非按这个顺序来的,而是按照下面的顺序来:
- 开辟一块M内存空间
- 将M内存空间的地址赋值给instance
- 在M内存空间创建Singleton对象。
当A线程走到了第2步,将M内空空间的地址赋值给instance时,发生线程切换,则B线程判断instance == null时,结果返回false,则返回null,导致返回空指针。

四、Java内存模型
上面说到产生并发Bug的源头是缓存导致的可见性、编译优化导致的顺序性。如果禁用缓存和编译优化是不是就问题解决了,并不是,将引入最大的问题,程序性能问题。
合理的方案是按需求来禁用缓存和编译优化。
Java内存模型规范了按需禁用缓存和编译优化的方法(volatile、synchronized、final、Happens-Before规则)。
class VolatileExample{
int x = 0;
volatile boolean v = false;
public void writer(){
x = 42;
v = true;
}
publci void reader(){
if(v == true){
//x = ?
sout(x);
}
}
}
上面的例子,A线程调用writer(),B线程调用reader(),B看到的x是多少?JDK1.5以前是0,JDK1.5及以上是42。
原因是JDK1.5对volatile进行增强,新增了Happens-Before规则。
五、Happens-Before规则
规则1:程序的顺序性规则
意思就是:前面一个操作的结果对后续操作是可见的。
x = 42 ; Hapens-Before于 v = true;
如果是在JDK1.5以前,v = true可能会先被执行。
class VolatileExample{
int x = 0;
volatile boolean v = false;
public void writer(){
x = 42;
v = true;
}
publci void reader(){
if(v == true){
//x = ?
sout(x);
}
}
}
规则2:volatile变量规则
对一个volatile变量的写操作,Happens-Before于后续对这个volatile变量的读操作。
规则3:传递性
A Happens-Before B
B Happens-Before C
那么
A Happens-Before C
class VolatileExample{
int x = 0;
volatile boolean v = false;
public void writer(){
x = 42;
v = true;
}
publci void reader(){
if(v == true){
//x = ?
sout(x);
}
}
}
规则2结合规则3和规则1来一起看。
x = 42 Happens-Before v = true,这是规则1
A线程写变量v=true Happens-Before B线程读变量v=true,这是规则2
由规则1、2结合规则3的传递性,得出x = 42 Happens-Before B线程读变量v=true

规则4:管程中锁的规则
对一个锁的解锁 Happens-Before于后续对这个锁的加锁。
管程是一种通用的同步原语。同步原语是什么。。。。synchronized是Java对管程的实现。
synchronized(this){//此处自动加锁
// x 是共享变量,初始值=10
if(this.x < 12){
this.x = 12;
}
}//此处自动解锁
A执行完代码块后x=12,执行完释放锁。线程B进入代码块,能够看到A对x的写操作。
规则5:线程start()规则
主线程A启动子线程B后,子线程B能够看到主线程在启动子线程B之前的操作。
也就是 start()操作 Happens-Before 线程B中的任意操作。
Thread B = new Thread(() -> {
//主线程调用B.start()之前
//所有对共享变量的修改,此处可见
//var == 77
})
var = 77;
B.start();
规则6:线程join()规则
这条是关于线程等待的。主线程A等待子线程B完成(A调用子线程B的join()方法)。当子线程B完成后,主线程能够看到子线程的操作。
Thread B = new Thread(()-> {
var = 66;
})
//一系列操作
B.start();
//进行一系列操作
B.join()
线程B中的任意操作,Happens-Before 于该join()操作。
六、final
final修饰变量时,初衷是告诉编译器:这个变量生而不变,可以尽可能的优化。
那什么时候使用final呢?
一个答案就是“尽可能的使用”。任何你不希望改变的(基本类型,或者指向一个对象,不管该对象是否可变)一般来讲都应该声明为final。
另一种看待此问题的方式是:
如果一个对象将会在多个线程中访问并且你并没有将其成员声明为final,则必须提供其他方式保证线程安全
七、并发编程的学习路线图

参考文档
《Java并发编程实战——王宝令》
JSR 133 (Java Memory Model) FAQ
关于java中final关键字与线程安全性
Java核心复习—— 原子性、有序性与Happens-Before的更多相关文章
- Java核心复习——J.U.C AbstractQueuedSynchronizer
第一眼看到AbstractQueuedSynchronizer,通常都会有这几个问题. AbstractQueuedSynchronizer为什么要搞这么一个类? 这个类是干什么的.有什么用? 这个类 ...
- Java核心复习——线程池ThreadPoolExecutor源码分析
一.线程池的介绍 线程池一种性能优化的重要手段.优化点在于创建线程和销毁线程会带来资源和时间上的消耗,而且线程池可以对线程进行管理,则可以减少这种损耗. 使用线程池的好处如下: 降低资源的消耗 提高响 ...
- Java核心复习 —— J.U.C 并发工具类
一.CountDownLatch 文档描述 A synchronization aid that allows one or more threads to wait until* a set of ...
- Java核心复习——synchronized
一.概念 利用锁机制实现线程同步,synchronized关键字的底层交由了JVM通过C++来实现 Java中的锁有两大特性: 互斥性 同一时间,只允许一个线程持有某个对象锁. 可见性 锁释放前,线程 ...
- Java核心复习—— volatile 与可见性
一.介绍 volatile保证共享变量的"可见性".可见性指的是当一个线程修改变量时,另一个线程能读到这个修改的值. 这里就要提出几个问题. 问题1:为什么一个线程修改时,另一个线 ...
- Java核心复习——CompletableFuture
介绍 JDK1.8引入CompletableFuture类. 使用方法 public class CompletableFutureTest { private static ExecutorServ ...
- Java核心复习—— ThreadLocal源码分析
ThreadLocal,叫做线程本地存储,也可以叫做线程本地变量.ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量. 一.如何使用 class Acce ...
- Java核心复习——J.U.C LinkedBlockingQueue源码分析
参考文档 LinkedBlockingQueue和ArrayBlockingQueue的异同
- Java核心复习——J.U.C ArrayBlockingQueue源码分析
介绍 依赖关系 源码 构造方法 public ArrayBlockingQueue(int capacity) { this(capacity, false);//默认构造非公平的有界队列 } pub ...
随机推荐
- 【转载】IIS网站配置不带www域名直接跳转带www的域名
很多时候为了统一网站入口,需要将不带www的主域名解析到带www的域名记录下,当客户访问不带www的域名网址的时候自动跳转到带www的域名,在IIS Web服务器中可以通过URL重写模块来实现此功能, ...
- 【日语】日语N5学习
副词与连接词 ~から: 从-(表示时间.场所起点) ~まで: 到-(表示时间.场所终点) と: 和(并列时用) えーと: 嗯 いっしょに: 一起 ちょっと: 一点儿 いつも: 经常.总是 ときどき: ...
- 【日语】【ZZ】日语人称小结
[ZZ]日语人称小结 日语中有关人称的词很多,也有不少朋友问 现整理了一下,希望能对那些不太清楚的朋友有点帮助 如果您认为在下有写错的地方,或者您有什么高见,请不吝赐教 第一人称 “我” 1.私 わた ...
- 如何:确定已安装的 .NET Framework 版本
用户可在他们的计算机上安装和运行 .NET Framework 的多个版本. 当你开发或部署应用时,你可能需要知道用户的计算机上安装了哪些 .NET Framework 版本. .NET Framew ...
- centos8 安装 mongodb 4.2 (使用yum)
1.制作 repo 文件 参考 mongodb 官方的安装文档,使用下面的脚本制作Yum库安装mongodb4.2,但安装过程提示 "Failed to synchronize cache ...
- Spring Boot配置多数据源并实现Druid自动切换
原文:https://blog.csdn.net/acquaintanceship/article/details/75350653 Spring Boot配置多数据源配置yml文件主数据源配置从数据 ...
- ArrayList之foreach循环删除倒数第二个元素,不触发fail-fast机制
今天一朋友问了个问题,对于如下一段代码,运行后会有怎样的结果? public class ArrayListTest { public static void main(String[] args) ...
- Java程序员完美设置,Mac编程指南
重装了不知道多少次Windows,Linux发行版换来换去总是觉得不满意,终于下定决心在年头买了人生中第一台Mac. 为什么是Mac 现在的移动端.服务器端跑的大多数都是Unix系统,熟悉Un ...
- 3星|路骋《用得上的商学院》:100个MBA知识点的简单介绍
作者在序言中说,放弃了上亿的股票期权去念了两年全脱产的清华-MIT Global MBA.念完后认为课程不错,考虑到这种课本科毕业不能直接念,工作几年后又很难脱产来念,因此办了一个音频课程来讲这个MB ...
- Mybatis3.1-[tp_32-33]-_映射文件_select_resultMap关联查询_association分步查询_延迟加载
笔记要点出错分析与总结 工程组织 1.定义接口 DepartmentMapper package com.dao; import com.bean.Department; public interfa ...