一、 产生并发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()的运行过程是这样:

  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规则)。


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的更多相关文章

  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. 【转载】IIS网站配置不带www域名直接跳转带www的域名

    很多时候为了统一网站入口,需要将不带www的主域名解析到带www的域名记录下,当客户访问不带www的域名网址的时候自动跳转到带www的域名,在IIS Web服务器中可以通过URL重写模块来实现此功能, ...

  2. 【日语】日语N5学习

    副词与连接词 ~から: 从-(表示时间.场所起点) ~まで: 到-(表示时间.场所终点) と: 和(并列时用) えーと: 嗯 いっしょに: 一起 ちょっと: 一点儿 いつも: 经常.总是 ときどき: ...

  3. 【日语】【ZZ】日语人称小结

    [ZZ]日语人称小结 日语中有关人称的词很多,也有不少朋友问 现整理了一下,希望能对那些不太清楚的朋友有点帮助 如果您认为在下有写错的地方,或者您有什么高见,请不吝赐教 第一人称 “我” 1.私 わた ...

  4. 如何:确定已安装的 .NET Framework 版本

    用户可在他们的计算机上安装和运行 .NET Framework 的多个版本. 当你开发或部署应用时,你可能需要知道用户的计算机上安装了哪些 .NET Framework 版本. .NET Framew ...

  5. centos8 安装 mongodb 4.2 (使用yum)

    1.制作 repo 文件 参考 mongodb 官方的安装文档,使用下面的脚本制作Yum库安装mongodb4.2,但安装过程提示 "Failed to synchronize cache ...

  6. Spring Boot配置多数据源并实现Druid自动切换

    原文:https://blog.csdn.net/acquaintanceship/article/details/75350653 Spring Boot配置多数据源配置yml文件主数据源配置从数据 ...

  7. ArrayList之foreach循环删除倒数第二个元素,不触发fail-fast机制

    今天一朋友问了个问题,对于如下一段代码,运行后会有怎样的结果? public class ArrayListTest { public static void main(String[] args) ...

  8. Java程序员完美设置,Mac编程指南

        重装了不知道多少次Windows,Linux发行版换来换去总是觉得不满意,终于下定决心在年头买了人生中第一台Mac. 为什么是Mac 现在的移动端.服务器端跑的大多数都是Unix系统,熟悉Un ...

  9. 3星|路骋《用得上的商学院》:100个MBA知识点的简单介绍

    作者在序言中说,放弃了上亿的股票期权去念了两年全脱产的清华-MIT Global MBA.念完后认为课程不错,考虑到这种课本科毕业不能直接念,工作几年后又很难脱产来念,因此办了一个音频课程来讲这个MB ...

  10. Mybatis3.1-[tp_32-33]-_映射文件_select_resultMap关联查询_association分步查询_延迟加载

    笔记要点出错分析与总结 工程组织 1.定义接口 DepartmentMapper package com.dao; import com.bean.Department; public interfa ...