第一章:并发编程中的三个问题
可见性
可见性概念
可见性(
Visibility
):是指一个线程对共享变量进行修改,另一个先立即得到修改后的最新值。
可见性演示
案例演示:一个线程根据
boolean
类型的标记
flag
,
while
循环,另一个线程改变这个
flag
变量的值,另 一个线程并不会停止循环。
package com . itheima . concurrent_problem ;/**案例演示 :一个线程对共享变量的修改 , 另一个线程不能立即得到最新值*/public class Test01Visibility {// 多个线程都会访问的数据,我们称为线程的共享数据private static boolean run = true ;public static void main ( String [] args ) throws InterruptedException {Thread t1 = new Thread (() -> {while ( run ) {}});t1 . start ();Thread . sleep ( 1000 );Thread t2 = new Thread (() -> {run = false ;System . out . println ( " 时间到,线程 2 设置为 false" );});t2 . start ();}}
小结
并发编程时,会出现可见性问题,当一个线程对共享变量进行了修改,另外的线程并没有立即看到修改后的最新值。
原子性
原子性概念
原子性(
Atomicity
):在一次或多次操作中,要么所有的操作都执行并且不会受其他因素干扰而中
断,要么所有的操作都不执行。
原子性演示
案例演示 :5 个线程各执行 1000 次 i++;使用 javap 反汇编 class 文件,得到下面的字节码指令:package com . itheima . demo01_concurrent_problem ;import java . util . ArrayList ;/**案例演示 :5 个线程各执行 1000 次 i++;*/public class Test02Atomicity {private static int number = 0 ;public static void main ( String [] args ) throws InterruptedException {Runnable increment = () -> {for ( int i = 0 ; i < 1000 ; i ++ ) {number ++ ;}};ArrayList < Thread > ts = new ArrayList <> ();for ( int i = 0 ; i < 5 ; i ++ ) {Thread t = new Thread ( increment );t . start ();ts . add ( t );}for ( Thread t : ts ) {t . join ();}System . out . println ( "number = " + number );}}
其中,对于
number++
而言(
number
为静态变量),实际会产生如下的
JVM
字节码指令:
由此可见
number++
是由多条语句组成,以上多条指令在一个线程的情况下是不会出问题的,但是在多线程情况下就可能会出现问题。比如一个线程在执行13: iadd
时,另一个线程又执
9:getstatic
。会导致两次number++
,实际上只加了
1
。
小结
并发编程时,会出现原子性问题,当一个线程对共享变量操作到一半时,另外的线程也有可能来操作共享变量,干扰了前一个线程的操作。
有序性
有序性概念
有序性(
Ordering
):是指程序中代码的执行顺序,
Java
在编译时和运行时会对代码进行优化,会导致程序最终的执行顺序不一定就是我们编写代码时的顺序。
有序性演示
jcstress 是 java 并发压测工具。 https://wiki.openjdk.java.net/display/CodeTools/jcstress修改 pom 文件,添加依赖:9: getstatic #12 // Field number:I12: iconst_113: iadd14: putstatic #12 // Field number:Ipublic static void main ( String [] args ) {int a = 10 ;int b = 20 ;} <dependency><groupId> org.openjdk.jcstress </groupId><artifactId> jcstress-core </artifactId><version> ${jcstress.version} </version></dependency>代码Test03Orderliness.javapackage com . itheima . concurrent_problem ;import org . openjdk . jcstress . annotations . * ;import org . openjdk . jcstress . infra . results . I_Result ;@JCStressTest@Outcome ( id = { "1" , "4" } , expect = Expect . ACCEPTABLE , desc = "ok" )@Outcome ( id = "0" , expect = Expect . ACCEPTABLE_INTERESTING , desc = "danger" )@Statepublic class Test03Orderliness {int num = 0 ;boolean ready = false ;// 线程一执行的代码@Actorpublic void actor1 ( I_Result r ) {if ( ready ) {r . r1 = num + num ;} else {r . r1 = 1 ;}}// 线程 2 执行的代码@Actorpublic void actor2 ( I_Result r ) {num = 2 ;ready = true ;}}
Result
是一个对象,有一个属性
r1
用来保存结果,在多线程情况下可能出现几种结果?
情况
1
:线 程1
先执行
actor1
,这时
ready = false
,所以进入
else
分支结果为
1
。
情况
2
:线程
2
执行到
actor2
,执行了
num = 2;
和
ready = true
,线程
1
执行,这回进入
if
分支,结果为 4
情况
3
:线程
2
先执行
actor2
,只执行
num = 2
;但没来得及执行
ready = true
,线程
1
执行,还是进入else分支,结果为
1
。
还有一种结果
0
。
运行测试:
mvn clean install
java
-
jar target
/
jcstress
.
jar
小结
程序代码在执行过程中的先后顺序,由于
Java
在编译期以及运行期的优化,导致了代码的执行顺序未必就是开发者编写代码时的顺序。
第二章:Java内存模型(JMM)
在介绍
Java
内存模型之前,先来看一下到底什么是计算机内存模型。
计算机结构简介
冯诺依曼,提出计算机由五大组成部分,输入设备,输出设备存储器,控制器,运算器。
CPU
中央处理器,是计算机的控制和运算的核心,我们的程序最终都会变成指令让
CPU
去执行,处理程序中的数据。
内存
我们的程序都是在内存中运行的,内存会保存程序运行时的数据,供
CPU
处理。
缓存
CPU
的运算速度和内存的访问速度相差比较大。这就导致
CPU
每次操作内存都要耗费很多等待时间。内 存的读写速度成为了计算机运行的瓶颈。于是就有了在CPU
和主内存之间增加缓存的设计。最靠近
CPU 的缓存称为L1
,然后依次是
L2
,
L3
和主内存,
CPU
缓存模型如图下图所示。
CPU Cache
分成了三个级别
: L1
,
L2
,
L3
。级别越小越接近
CPU
,速度也更快,同时也代表着容量越小。
小结
计算机的主要组成
CPU
,内存,输入设备,输出设备。
Java内存模型
Java
内存模型的概念
Java Memory Molde (Java
内存模型
/JMM)
,千万不要和
Java
内存结构混淆
关于
“Java
内存模型
”
的权威解释,请参考
https://download.oracle.com/otnpub/jcp/memory_model-
1.0-pfd-spec-oth-JSpec/memory_model-1_0-pfd-spec.pdf
。
Java
内存模型,是
Java
虚拟机规范中所定义的一种内存模型,
Java
内存模型是标准化的,屏蔽掉了底层 不同计算机的区别。
Java
内存模型是一套规范,描述了
Java
程序中各种变量
(
线程共享变量
)
的访问规则,以及在
JVM
中将变量存储到内存和从内存中读取变量这样的底层细节,具体如下。
主内存
主内存是所有线程都共享的,都能访问的。所有的共享变量都存储于主内存。
工作内存
每一个线程有自己的工作内存,工作内存只存储该线程对共享变量的副本。线程对变量的所有的操
作
(
读,取
)
都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接
访问对方工作内存中的变量。
Java
内存模型的作用
Java
内存模型是一套在多线程读写共享数据时,对共享数据的可见性、有序性、和原子性的规则和保障。
CPU
缓存,内存与
Java
内存模型的关系
通过对前面的
CPU
硬件内存架构、
Java
内存模型以及
Java
多线程的实现原理的了解,我们应该已经意识 到,多线程的执行最终都会映射到硬件处理器上进行执行。
但
Java
内存模型和硬件内存架构并不完全一致。对于硬件内存来说只有寄存器、缓存内存、主内存的概 念,并没有工作内存和主内存之分,也就是说Java
内存模型对内存的划分对硬件内存并没有任何影响,因为JMM
只是一种抽象的概念,是一组规则,不管是工作内存的数据还是主内存的数据,对于计算机硬 件来说都会存储在计算机主内存中,当然也有可能存储到CPU
缓存或者寄存器中,因此总体上来说,Java内存模型和计算机硬件内存架构是一个相互交叉的关系,是一种抽象概念划分与真实物理硬件的交叉。
JMM
内存模型与
CPU
硬件内存架构的关系:
小结
Java
内存模型是一套规范,描述了
Java
程序中各种变量
(
线程共享变量
)
的访问规则,以及在
JVM
中将变量 存储到内存和从内存中读取变量这样的底层细节,Java
内存模型是对共享数据的可见性、有序性、和原子性的规则和保障。
主内存与工作内存之间的交互目标
了解主内存与工作内存之间的数据交互过程
Java
内存模型中定义了以下
8
种操作来完成,主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,虚拟机实现时必须保证下面 提及的每一种操作都是原子的、不可再分的。
对应如下的流程图:
注意
:
1.
如果对一个变量执行
lock
操作,将会清空工作内存中此变量的值
2.
对一个变量执行
unlock
操作之前,必须先把此变量同步到主内存中
小结
lock
->
read
->
load
->
use
->
assign
->
store
->
write
->
unlock
主内存与工作内存之间的数据交互过程
第三章:synchronized保证三大特性
synchronized
能够保证在同一时刻最多只有一个线程执行该段代码,以达到保证并发安全的效果。
synchronized ( 锁对象 ) {// 受保护资源