深入学习并发编程中的synchronized

第一章:并发编程中的三个问题

可见性

可见性概念
可见性( 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:I
12: iconst_1
13: iadd
14: putstatic #12 // Field number:I
public 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.java
package 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" )
@State
public class Test03Orderliness {
int num = 0 ;
boolean ready = false ;
// 线程一执行的代码
@Actor
public void actor1 ( I_Result r ) {
if ( ready ) {
r . r1 = num + num ;
} else {
r . r1 = 1 ;
}
}
// 线程 2 执行的代码
@Actor
public 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 ( 锁对象 ) {
// 受保护资源
  • 17
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值