一、 产生并发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. JavaScript时钟效果

    在JavaScript中,有一个内置对象Date,它重要的一个作用就是实现了时间的时刻更新,通过代码来创造一个实实在在的时间表. 代码例子: <!DOCTYPE html> <htm ...

  2. window.postMessage()实现(iframe嵌套页面)跨域消息传递

    window.postMessage()方法可以安全地实现Window对象之间的跨域通信.例如,在页面和嵌入其中的iframe之间. 不同页面上的脚本允许彼此访问,当且仅当它们源自的页面共享相同的协议 ...

  3. Fortify漏洞之Cross-Site Scripting(XSS 跨站脚本攻击)

    书接上文,继续对Fortify漏洞进行总结,本篇主要针对XSS跨站脚步攻击漏洞进行总结,如下: 1.Cross-Site Scripting(XSS 跨站脚本攻击) 1.1.产生原因: 1. 数据通过 ...

  4. sed 追加文件内容

    追加用法总结 1.a 在匹配行后面追加 2.i 在匹配行前面追加 3.r 将文件内容追加到匹配行后面 4.w 将匹配行写入指定文件 在匹配行后面追加 a passwd文件第10行后面追加"A ...

  5. 流程控制 while for

    循环执行 计算机最擅长的功能之一就是按照规定的条件,重复执行某些操作,这是程序设计中最能发挥计算机特长的程序结构. 1.while语句 while(表达式){ 各种语句.... } 当表达式的值为tr ...

  6. Linux 之 文件

    文件名称 在linux中,windows概念中的文件夹和文件是没有区别的,都是统称为文件. 1.Linux中文件的名称大小写是敏感的 2.名称最多可以为255个字符 3.除了正斜线以外,都是有效字符 ...

  7. MySql 8.0服务端安装后,用navicat12连接时报2059错误_解决

    先看连接错误 连接失败:2059 - Authentication plugin 'caching_sha2_password' cannot be loaded: .... 解决方法: 进入MySQ ...

  8. How to parse unix timestamp to time.Time

      The time.Parse function does not do Unix timestamps. Instead you can use strconv.ParseInt to parse ...

  9. Ajax的简单例子——PHP

    PHP PHP是一种创建动态交互性站点的服务器端脚本语言 PHP能够生成动态页面内容 PHP能够创建.打开.读取.写入.删除以及关闭服务器上的文件 PHP能够接收表单数据 PHP能够发送并取回cook ...

  10. BZOJ3331 压力 (圆方树+树上差分)

    题意 略 题解 求路径上的割点. 然后就直接圆方树上差分 CODE #include <bits/stdc++.h> using namespace std; inline void rd ...