一、 产生并发Bug的源头

  • 可见性

    • 缓存导致的可见性问题
  • 原子性
    • 线程切换带来的原子性问题
  • 有序性
    • 编译优化带来的有序性问题

上面讲到了 volatile 与可见性,本章再主要讲下原子性、有序性与Happens-Before规则。

二、线程切换带来的原子性问题

count += 1 这一句高级语言的语句,往往需要多条CPU执令。可以分为3步:

  • 将count值加载到寄存器
  • 在寄存器中对count进行+1操作
  • 将count值写回内存

所以,我们需要在高级语言的层面上,确保一些操作是原子性操作。

三、编译优化带来的有序性问题

编译器为了优化性能,有时会改变程序中语句的先后顺序。

  1. a = 6;
  2. b = 7;

经过编译优化后,可能会变成

  1. b = 7;
  2. a = 6

双重检查创建单例对象

  1. public class Singleton{
  2. static Singleton instance;
  3. static Singleton getInstance(){
  4. if(instance == null){
  5. synchronized(Singleton.class){
  6. if(instance == null){
  7. instance = new Singleton();
  8. }
  9. }
  10. }
  11. }
  12. }

这个例子看似很完美,但其实是可能触发空指针异常。

为什么可能会触发空指针异常。

假设getInstance()的运行过程是这样:

  1. 开辟一块M内存空间
  2. 在M内存空间上创建Singleton对象
  3. 将对象赋值给instance

这样的话是没问题的。但编译时并非按这个顺序来的,而是按照下面的顺序来:

  1. 开辟一块M内存空间
  2. 将M内存空间的地址赋值给instance
  3. 在M内存空间创建Singleton对象。

当A线程走到了第2步,将M内空空间的地址赋值给instance时,发生线程切换,则B线程判断instance == null时,结果返回false,则返回null,导致返回空指针。

四、Java内存模型

上面说到产生并发Bug的源头是缓存导致的可见性、编译优化导致的顺序性。如果禁用缓存和编译优化是不是就问题解决了,并不是,将引入最大的问题,程序性能问题。

合理的方案是按需求来禁用缓存和编译优化。

Java内存模型规范了按需禁用缓存和编译优化的方法(volatile、synchronized、final、Happens-Before规则)。


  1. class VolatileExample{
  2. int x = 0;
  3. volatile boolean v = false;
  4. public void writer(){
  5. x = 42;
  6. v = true;
  7. }
  8. publci void reader(){
  9. if(v == true){
  10. //x = ?
  11. sout(x);
  12. }
  13. }
  14. }

上面的例子,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可能会先被执行。


  1. class VolatileExample{
  2. int x = 0;
  3. volatile boolean v = false;
  4. public void writer(){
  5. x = 42;
  6. v = true;
  7. }
  8. publci void reader(){
  9. if(v == true){
  10. //x = ?
  11. sout(x);
  12. }
  13. }
  14. }

规则2:volatile变量规则

对一个volatile变量的写操作,Happens-Before于后续对这个volatile变量的读操作。

规则3:传递性

A Happens-Before B

B Happens-Before C

那么

A Happens-Before C


  1. class VolatileExample{
  2. int x = 0;
  3. volatile boolean v = false;
  4. public void writer(){
  5. x = 42;
  6. v = true;
  7. }
  8. publci void reader(){
  9. if(v == true){
  10. //x = ?
  11. sout(x);
  12. }
  13. }
  14. }

规则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对管程的实现。


  1. synchronized(this){//此处自动加锁
  2. // x 是共享变量,初始值=10
  3. if(this.x < 12){
  4. this.x = 12;
  5. }
  6. }//此处自动解锁

A执行完代码块后x=12,执行完释放锁。线程B进入代码块,能够看到A对x的写操作。

规则5:线程start()规则

主线程A启动子线程B后,子线程B能够看到主线程在启动子线程B之前的操作。

也就是 start()操作 Happens-Before 线程B中的任意操作。

  1. Thread B = new Thread(() -> {
  2. //主线程调用B.start()之前
  3. //所有对共享变量的修改,此处可见
  4. //var == 77
  5. })
  6. var = 77;
  7. B.start();

规则6:线程join()规则

这条是关于线程等待的。主线程A等待子线程B完成(A调用子线程B的join()方法)。当子线程B完成后,主线程能够看到子线程的操作。

  1. Thread B = new Thread(()-> {
  2. var = 66;
  3. })
  4. //一系列操作
  5. B.start();
  6. //进行一系列操作
  7. B.join()

线程B中的任意操作,Happens-Before 于该join()操作。

六、final

final修饰变量时,初衷是告诉编译器:这个变量生而不变,可以尽可能的优化。

那什么时候使用final呢?

一个答案就是“尽可能的使用”。任何你不希望改变的(基本类型,或者指向一个对象,不管该对象是否可变)一般来讲都应该声明为final。

另一种看待此问题的方式是:

如果一个对象将会在多个线程中访问并且你并没有将其成员声明为final,则必须提供其他方式保证线程安全

七、并发编程的学习路线图

参考文档

《Java并发编程实战——王宝令》

JSR 133 (Java Memory Model) FAQ

关于java中final关键字与线程安全性

Java核心复习—— 原子性、有序性与Happens-Before的更多相关文章

  1. Java核心复习——J.U.C AbstractQueuedSynchronizer

    第一眼看到AbstractQueuedSynchronizer,通常都会有这几个问题. AbstractQueuedSynchronizer为什么要搞这么一个类? 这个类是干什么的.有什么用? 这个类 ...

  2. Java核心复习——线程池ThreadPoolExecutor源码分析

    一.线程池的介绍 线程池一种性能优化的重要手段.优化点在于创建线程和销毁线程会带来资源和时间上的消耗,而且线程池可以对线程进行管理,则可以减少这种损耗. 使用线程池的好处如下: 降低资源的消耗 提高响 ...

  3. Java核心复习 —— J.U.C 并发工具类

    一.CountDownLatch 文档描述 A synchronization aid that allows one or more threads to wait until* a set of ...

  4. Java核心复习——synchronized

    一.概念 利用锁机制实现线程同步,synchronized关键字的底层交由了JVM通过C++来实现 Java中的锁有两大特性: 互斥性 同一时间,只允许一个线程持有某个对象锁. 可见性 锁释放前,线程 ...

  5. Java核心复习—— volatile 与可见性

    一.介绍 volatile保证共享变量的"可见性".可见性指的是当一个线程修改变量时,另一个线程能读到这个修改的值. 这里就要提出几个问题. 问题1:为什么一个线程修改时,另一个线 ...

  6. Java核心复习——CompletableFuture

    介绍 JDK1.8引入CompletableFuture类. 使用方法 public class CompletableFutureTest { private static ExecutorServ ...

  7. Java核心复习—— ThreadLocal源码分析

    ThreadLocal,叫做线程本地存储,也可以叫做线程本地变量.ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量. 一.如何使用 class Acce ...

  8. Java核心复习——J.U.C LinkedBlockingQueue源码分析

    参考文档 LinkedBlockingQueue和ArrayBlockingQueue的异同

  9. Java核心复习——J.U.C ArrayBlockingQueue源码分析

    介绍 依赖关系 源码 构造方法 public ArrayBlockingQueue(int capacity) { this(capacity, false);//默认构造非公平的有界队列 } pub ...

随机推荐

  1. code first从入门到伪精通

    新入职一家公司,虽然之前也用ef,但是方式不一样,之前用的db,现在用代码先行的code,基于现有公司基本项目框架都是用的code,所以一步登顶,从最实战的角度去操作code,心颤的很,废话不多说,开 ...

  2. [#Linux] CentOS 7 安装微信详细过程

    微信安装 微信安装过程如下: 1,下载最新版本tar.gz压缩包 wget https://github.com/geeeeeeeeek/electronic-wechat/releases/down ...

  3. Python统计字符出现次数(Counter包)以及txt文件写入

    # -*- coding: utf-8 -*- #spyder (python 3.7) 1. 统计字符(可以在jieba分词之后使用) from collections import Counter ...

  4. 开源框架---tensorflow c++ API中./configure步骤细节

    u@u160406:~/tf1.13/tensorflow$ git checkout r1.13 分支 r1.13 设置为跟踪来自 origin 的远程分支 r1.13.切换到一个新分支 'r1.1 ...

  5. django实现发送邮件功能

    django实现邮件发送功能 1)首先注册一个邮箱,这里以163邮箱为例 2)注册之后登录,进行如下修改 找到设置,设置一个授权码,授权码的目的仅仅是让你有权限发邮件,但是不能登录到邮箱进行修改,发送 ...

  6. JDBC课程4--使用PreparedStatement进行增删查改--封装进JDBCTools的功能中;模拟SQL注入 ; sql的date()传入参数值格式!

    主要内容: /*SQL 的date()需要传入参数值: preparedStatement().setDate(new java.util.Date().getTime()); 熟悉了使用Prepar ...

  7. 【小顶堆的插入构造/遍历】PatL2-012. 关于堆的判断

    L2-012. 关于堆的判断 时间限制   将一系列给定数字顺序插入一个初始为空的小顶堆H[].随后判断一系列相关命题是否为真.命题分下列几种: “x is the root”:x是根结点: “x a ...

  8. 三、vue基础--表单绑定

    表单输入绑定:可以一起使用以下修饰符,都是在v-model里面使用的,有input,radio,textrea,select中都可以使用绑定 1.单选按钮,代码如下: <div id='app' ...

  9. Pthon操作Gitlab API----批量删除,创建,取消保护

    1.需求:大批量的应用上线后合并到Master,其他的分支develop/test/uat等需要同步最新代码的操作. 2.操作:可以通过传参 ,列表 的方式把每个项目的id值填入,才能对相关项目进行批 ...

  10. latex 表格每行设置不同字体

    Each cell of a table is set in a box, so that a change of font style (or whatever) only lasts to the ...