文章目录
0、前言概述
一、并发编程中的三个问题
二、Java内存模型(JMM)
三、synchronized保证三大特性
四、synchronized的特性
五、synchronized底层原理
六、JDK6 synchronized优化
本文为 1~4小节,5、6节请查阅【并发编程】2 synchronized底层实现原理、Java内存模型JMM;monitor、CAS、乐观锁和悲观锁;对象的内存结构、Mark Word、锁升级
在Java中,如果一个方法是native的,那Java就不负责具体实现它,而是交给底层的JVM使用c或者c++去实现。
~
1.并发编程中存在3个问题:可见性、原子性、有序性
~
2.计算机结构
~
3.Java内存模型(JMM)
和Java内存结构不同,Java内存模型是一套规范、是标准化的,屏蔽掉了底层不同计算机的区别(Java内存模型对内存的划分对硬件内存并没有任何影响,因为JMM只是一种抽象的概念,是一组规则)。它描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节,Java内存模型是对共享数据的可见性、有序性、和原子性的规则和保障。细节如下。
主内存与工作内存之间的数据交互过程:lock -> read -> load -> use -> assign -> store -> write -> unlock
~
4.synchronized可保证三大特性:可见性、原子性、有序性
补充volatile:volatile可以保证可见性、有序性,无法保证原子性(它对任意单个变量的读/写具有原子性,但是类似于i++这种复合操作不具有原子性)。【synchronized可以保证可见性、原子性、有序性,因为锁的互斥执行的特性可以确保对整个临界区代码执行具有原子性】
~
5.synchronized的特性:可重入、不可中断
补充ReentrantLock:与synchronized一样 支持可重入;可中断(有两种方式,可中断、不可中断);可设置超时时间;可设置为公平锁
~
6.synchronized底层原理
synchronized的锁对象会关联一个monitor,这个monitor不是我们主动创建的,是JVM的线程执行到这个同步代码块,发现锁对象没有monitor就会创建monitor,monitor内部有两个重要的成员变量owner:拥有这把锁的线程,recursions会记录线程拥有锁的次数,当一个线程拥有monitor后其他线程只能等待
synchronized使用了monitorentor和monitorexit两个指令。每个锁对象都会关联一个monitor(监视器,它才是真正的锁对象),它内部有两个重要的成员变量owner会保存获得锁的线程,recursions会保存线程获得锁的次数,当执行到monitorexit时,recursions会-1,当计数器减到0时 这个线程就会释放锁。
同步方法会增加 ACC_SYNCHRONIZED 修饰,会隐式调用monitorenter和monitorexit。在执行同步方法前会调用monitorenter,在执行完同步方法后会调用monitorexit。
~
7.Monitor结构、竞争、等待、释放
monitor竞争、等待、释放 具体见正文解释
~
8.synchronized与Lock的区别
lock.tryLock()
方法返回boolean),而synchronized不能。 ~
9.CAS
CAS 底层依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令
~
10.乐观锁和悲观锁
CAS是乐观锁
~
11.对象的内存结构
对象头由两部分组成,一部分用于存储自身的运行时数据,称之为 Mark Word,另外一部分是类型指针,及对象指向它的类元数据的指针。
~
12.Mark Word
在64位虚拟机下,Mark Word是64bit大小的,其存储结构如下:
在32位虚拟机下,Mark Word是32bit大小的,其存储结构如下:
现在绝大多数用的64位虚拟机,因此我们重点关注64位Mark Word。
~
13.synchronized锁升级:无锁--》偏向锁--》轻量级锁–》重量级锁
偏向锁:大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中记录存储锁偏向的线程ID,以后该线程在进入同步块时先判断对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果存在就直接获取锁,虚拟机都可以不再进行任何同步操作,偏向锁的效率高 偏向锁的原理是什么? 当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为“01”、偏向锁设为1,即偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中 ,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作,偏向锁的效率高。 偏向锁的好处是什么? 偏向锁是在只有一个线程执行同步块时进一步提高性能,适用于一个线程反复获得同一锁的情况。偏向锁可以提高带有同步但无竞争的程序性能。轻量级锁:当其他线程尝试竞争偏向锁时,锁升级为轻量级锁。线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的MarkWord替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,标识其他线程竞争锁,当前线程便尝试使用自旋来获取锁。 轻量级锁的原理是什么? 将对象的Mark Word复制到栈帧中的Lock Recod中。Mark Word更新为指向Lock Record的指针。 轻量级锁好处是什么? 在多线程交替执行同步块的情况下,可以避免重量级锁引起的性能消耗。
自旋锁、自适应自旋锁
重量级锁:锁在原地循环等待的时候,是会消耗CPU资源的。所以自旋必须要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,那么等待锁的线程会不断的循环反而会消耗CPU资源。默认情况下锁自旋的次数是10 次,可以使用-XX:PreBlockSpin参数来设置自旋锁等待的次数。10次后如果还没获取锁,则升级为重量级锁。
~
14.锁消除、锁粗化
锁消除是指虚拟机即时编译器(JIT)在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。
什么是锁粗化?
JVM会探测到一连串细小的操作都使用同一个对象加锁,将同步代码块的范围放大,放到这串操作的外面,这样只需要加一次锁即可。
~
15.平时写代码如何对synchronized优化
类名.class
作为锁,降低锁的粒度,提高并发性能。并发情况下,尽量不要使用Hashtable,推荐使用ConcurrentHashMap、LinkedBlockingQueue参考黑马程序员synchronized相关课程
提前说明:由于System.out.println()函数中用到了synchronized关键字,此处我们只分析 无synchronized情况下的并发问题,故不用System.out.println()打印输出结果,选择LOG.info()。需要在pom文件中引入以下依赖
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.2</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.2</version>
</dependency>
println源码
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
概念:当一个线程对共享变量进行了修改,另外的线程立即得到修改后的最新值
演示:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
案例演示: 一个线程对共享变量的修改,另一个线程不能立即得到最新值
*/
public class Test01Visibility {
private final static Logger LOG = LoggerFactory.getLogger(Test01Visibility.class);
// 多个线程都会访问的数据,我们称为线程的共享数据
//1.创建一个共享变量
private static boolean run = true;
public static void main(String[] args) throws InterruptedException {
//2.创建一个线程不断读取共享变量
new Thread(() -> {
while(run) {
}
LOG.info("{} 读取run:{},线程结束", Thread.currentThread().getName(), run);
// System.out.println(Thread.currentThread().getName() + " 读取run:" + run + ",线程结束");
}, "Thread1").start();
Thread.sleep(1000);
//3.创建一个线程修改共享变量
new Thread(() -> {
run = false;
LOG.info( "{} 修改run值为:{}", Thread.currentThread().getName(), run);
// System.out.println(Thread.currentThread().getName() + " 修改run值为:" + run);
}, "Thread2").start();
}
}
由输出可知,虽然Thread2已经将run修改为false,但线程Thread1没有读取到run修改后的值,Thread1一直在运行。
小结:并发编程时,会出现可见性问题,当一个线程对共享变量进行了修改,另外的线程并没有立即看到修改后的最新值。
概念:在一次或多次操作中,要么所有的操作都执行并且不会受其他因素干扰而中断,要么所有的操作都不执行。
演示:5个线程各执行1000次 i++;
/**
* 目标:演示原子性问题
* 1.定义一个共享变量number
* 2.对number进行1000的++操作
* 3.使用5个线程来进行
*/
public class Test02Atomicity {
//1.定义一个共享变量number
private static int number = 0;
public static void main(String[] args) throws InterruptedException {
//2.对number进行1000的++操作
Runnable increment = () -> {
for (int i = 0; i < 1000; i++) {
number++;
}
};
List<Thread> list = new ArrayList<>();
//3.使用5个线程来进行
for (int i = 0; i < 5; i++) {
Thread t = new Thread(increment);
t.start();
list.add(t);
}
for (Thread thread : list) {
//让主线程等待自己创建的线程执行完毕,5个线程执行完 再取number的值
thread.join();
}
System.out.println("number=" + number); //值有多种可能,2677、4202、4722、4910、5000
}
}
分析:使用javap反汇编class文件(找到target下面的 类名.class 文件,cmd打开,控制台输入 javap -p -v .\Test02Atomicity.class),得到下面的字节码指令
其中对于number++而言(number为静态变量),实际会产生如下的JVM字节码指令:
9: getstatic #18 // Field number:I
12: iconst_1
13: iadd
14: putstatic #18 // Field number:I
number++执行过程为:
以线程1、线程2 两个线程为例,当线程1走到iadd
值为1但暂未赋给number,线程2也走到 iadd
。此时两个线程无论谁先执行putstatic
,最终number的值均为1。因为number++的4条语句没有保证原子性 导致数据出现错误。
小结:并发编程时,会出现原子性问题,当一个线程对共享变量操作到一半时,另外的线程也有可能来操作共享变量,干扰了前一个线程的操作。
概念:指程序中代码的执行顺序。我们一般认为,编写代码的顺序 就是程序最终的执行顺序,这种说法不对。Java在编译时和运行时会对代码进行优化,会导致程序最终的执行顺序不一定就是我们编写代码时的顺序。
演示:
jcstress是java并发压测工具,使用方法如下:
1)修改pom文件,添加依赖
<dependency>
<groupId>org.openjdk.jcstress</groupId>
<artifactId>jcstress-core</artifactId>
<version>0.14</version>
</dependency>
2)编写代码 TestOrdering.java
import org.openjdk.jcstress.annotations.*;
import org.openjdk.jcstress.infra.results.I_Result;
@JCStressTest //用并发压缩工具jcstress 测试类方法
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok") //@Outcome 对输出结果进行处理, 1 4 打印ok,0 打印danger
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger")
@State
public class TestOrdering {
int num = 0;
boolean ready = false;
//线程1执行的代码
@Actor //@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;
}
}
3)说明:
I_Result 是一个对象,有一个属性 r1 用来保存结果,在多线程情况下可能出现几种结果?
num = 2
和ready = true
,线程1执行,这回进入 if 分支,结果为4ready=true
** 后**num=2
**。线程2先执行actor2,只执行ready=true
,但没来得及执行num=2
(此时num仍为0),线程1执行,进入if分支,结果为0@Actor
public void actor2(I_Result r) {
ready = true;
num = 2;
}
4)执行过程:idea打开Terminal,输入 mvn clean install
mvn clean install成功后,在terminal控制台执行 java -jar target/jcstress.jar,可以获取相应的并发压测结果。发现其中确实出现 0、1、4这三种结果,其中0的占比为0.09%(java对actor2方法重排序)。
注:如果mvn clean install 无法生成jcstress.jar包,可在pom文件中加入以下插件,重新mvn clean install,即可生成jcstress.jar
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.2</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.2</version>
</dependency>
<dependency>
<groupId>org.openjdk.jcstress</groupId>
<artifactId>jcstress-core</artifactId>
<version>0.14</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<!-- jar生成路径 -->
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.2</version>
<executions>
<execution>
<id>main</id>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<!-- 打包的名字 jcstress.jar -->
<finalName>jcstress</finalName>
<!-- 调用下面两个类 -->
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.openjdk.jcstress.Main</mainClass>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/TestList</resource>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
小结:程序代码在执行过程中的先后顺序,由于Java在编译期以及运行期的优化,导致了代码的执行顺序未必就是开发者编写代码时的顺序。
在介绍Java内存模型之前,我们先看一下 计算机内存模型。
学习计算机的主要组成、以及缓存的作用。
冯诺依曼提出计算机由五大组成部分,分别为:输入设备、输出设备、存储器、控制器、运算器。
中央处理器,是计算机的控制和运算的核心,我们的程序最终都会变成指令让CPU去执行,处理程序中的数据。
我们的程序都是在内存中运行的,内存会保存程序运行时的数据,供CPU处理。
CPU的运算速度和内存的访问速度相差比较大。这就导致CPU每次操作内存都要耗费很多等待时间。内存的读写速度成为了计算机运行的瓶颈。于是就有了在CPU和主内存之间增加缓存的设计。最靠近CPU的缓存称为L1,然后依次是 L2,L3和主内存,CPU缓存模型如图下图所示。
CPU Cache分成了三个级别: L1, L2, L3。级别越小越接近CPU,速度也更快,同时也代表着容量越小。
Cache的出现是为了解决CPU直接访问内存效率低下问题的,程序在运行的过程中,CPU接收到指令后,它会最先向CPU中的一级缓存(L1 Cache)去寻找相关的数据,如果命中缓存,CPU进行计算时就可以直接对CPU Cache中的数据进行读取和写人,当运算结束之后,再将CPUCache中的最新数据刷新到主内存当中,CPU通过直接访问Cache的方式替代直接访问主存的方式极大地提高了CPU 的吞吐能力。但是由于一级缓存(L1 Cache)容量较小,所以不可能每次都命中。这时CPU会继续向下一级的二级缓存(L2 Cache)寻找,同样的道理,当所需要的数据在二级缓存中也没有的话,会继续转向L3 Cache、内存(主存)和硬盘。
冯-诺依曼计算机的特点
Java Memory Molde (Java内存模型/JMM),千万不要和Java内存结构混淆。
关于“Java内存模型”的权威解释,请参考 https://download.oracle.com/otn-pub/jcp/memory_model-1.0-pfd-spec-oth-JSpec/memory_model-1_0-pfd-spec.pdf。
Java内存模型,是Java虚拟机规范中所定义的一种内存模型,Java内存模型是标准化的,屏蔽掉了底层不同计算机的区别。
Java内存模型是一套规范,描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节,具体如下。
Java内存模型是一套在多线程读写共享数据时,对共享数据的可见性、有序性、和原子性的规则和保障。
synchronized, volatile
通过对前面的CPU硬件内存架构、Java内存模型以及Java多线程的实现原理的了解,我们应该已经意识到,多线程的执行最终都会映射到硬件处理器上进行执行。
但Java内存模型和硬件内存架构并不完全一致。对于硬件内存来说只有寄存器、缓存内存、主内存的概念,并没有工作内存和主内存之分,也就是说Java内存模型对内存的划分对硬件内存并没有任何影响,因为JMM只是一种抽象的概念,是一组规则,不管是工作内存的数据还是主内存的数据,对于计算机硬件来说都会存储在计算机主内存中,当然也有可能存储到CPU缓存或者寄存器中,因此总体上来说,Java内存模型和计算机硬件内存架构是一个相互交叉的关系,是一种抽象概念划分与真实物理硬件的交叉。
JMM内存模型与CPU硬件内存架构的关系:
Java内存模型是一套规范,描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节,Java内存模型是对共享数据的可见性、有序性、和原子性的规则和保障。
Java内存模型中定义了以下8种操作来完成,主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的。对应流程图如下:
Read——读取共享变量x;Load——将共享变量加载到工作内存中;Use——线程对共享变量x副本进行操作;Assign——操作得到新结果,赋值给x副本;Store——保存共享变量x副本及新值;Write——将最新值同步回主内存中。
Lock、Unlock——锁操作,与锁有关,譬如加了synchronized 才会有lock、unlock;如果共享变量没有加锁,则没有lock、unlock
注意:
主内存与工作内存之间的数据交互过程:
lock -> read -> load -> use -> assign -> store -> write -> unlock
synchronized能够保证在同一时刻最多只有一个线程执行该段代码,以达到保证并发安全的效果。
synchronized (锁对象) {
// 受保护资源;
}
针对1.1中的可见性问题(当一个线程对共享变量进行了修改,另外的线程并没有立即看到修改后的最新值),有两种方案可以解决:volatile、synchronized
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
案例演示: 一个线程对共享变量的修改,另一个线程不能立即得到最新值
*/
public class Test01Visibility {
private final static Logger LOG = LoggerFactory.getLogger(Test01Visibility.class);
//1.创建一个共享变量,
//加volatile:当修改的值同步回主内存,有个缓存一致性协议,会把其他工作内存的值全部设置为失效,线程会重新读取共享内存的值
private static volatile boolean run = true;
public static void main(String[] args) throws InterruptedException {
//2.创建一个线程不断读取共享变量
new Thread(() -> {
while(run) {
}
LOG.info("{} 读取run:{},线程结束", Thread.currentThread().getName(), run);
}, "Thread1").start();
Thread.sleep(1000);
//3.创建一个线程修改共享变量
new Thread(() -> {
run = false;
LOG.info( "{} 修改run值为:{}", Thread.currentThread().getName(), run);
}, "Thread2").start();
}
}
有两种输出结果。但无论是哪种,Thread1都能读取到最新run值、结束线程。
public class Test01Visibility {
private final static Logger LOG = LoggerFactory.getLogger(Test01Visibility.class);
private static boolean run = true;
private static Object obj = new Object();
public static void main(String[] args) {
//2.创建一个线程不断读取共享变量
new Thread(() -> {
while(run) {
synchronized (obj ) {
}
}
LOG.info("{} 读取run:{},线程结束", Thread.currentThread().getName(), run);
}, "Thread1").start();
//3.创建一个线程修改共享变量
new Thread(() -> {
run = false;
LOG.info( "{} 修改run值为:{}", Thread.currentThread().getName(), run);
}, "Thread2").start();
}
}
输出结果同volatile,表明Thread1能读取到最新run值、结束线程。
synchronized保证可见性的原理,执行synchronized时,对应lock原子操作会刷新工作内存中共享变量的值,获取主内存中共享变量的最新值。
演示:5个线程各执行1000次 i++;
public class Test02Atomicity {
//1.定义一个共享变量number
private static int number = 0;
private static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
//2.对number进行1000的++操作
Runnable increment = () -> {
for (int i = 0; i < 1000; i++) {
//需要锁对象,故创建一把锁
synchronized (obj) { //obj为锁对象,也可使用synchronized (Test02Atomicity.class)
number++; //synchronized保证 number++ 为原子操作。线程获取不到锁 就会等待
}
}
};
List<Thread> list = new ArrayList<>();
//3.使用5个线程来进行
for (int i = 0; i < 5; i++) {
Thread t = new Thread(increment);
t.start();
list.add(t);
}
for (Thread thread : list) {
//让主线程等待自己创建的线程执行完毕
thread.join();
}
System.out.println("number=" + number); //5000
}
}
相比于1.2中的案例,仅在number++
外面添加了synchronized (obj)
代码。程序无论运行多少次,number的值均为5000,证明synchronized能保证原子性。【如果只给number加volatile关键字,无法保证原子性,仍可能输出多种不同的值。volatile对任意单个变量的读/写具有原子性,但是类似于i++这种复合操作不具有原子性】
分析:使用javap反汇编class文件(找到target下面的 类名.class 文件,cmd打开,控制台输入 javap -p -v Test02Atomicity.class),得到下面的字节码指令
有了synchronized同步代码块,当第一个线程执行到一半,就算切到第二个线程 第二个线程没有锁也进不来,能保证第一个线程执行4条指令的时候 不会收到干扰,即能保证同步代码块中的代码是原子操作。
与1.2中(无synchronized)的字节码指令做对比
synchronized使用监视器锁monitor保证原子性
对number++
增加同步代码块后,保证同一时间只有一个线程操作number++;。就不会出现安全问题。
synchronized保证原子性的原理,synchronized保证只有一个线程拿到锁,能够进入同步代码块。
为了提高程序的执行效率,编译器和CPU会对程序中代码进行重排序。但CPU和编译器不能瞎排序,需满足一些规则,即as-if-serial语义。
as-if-serial语义的意思是:不管编译器和CPU如何重排序,必须保证在单线程情况下程序的结果是正确的。注意,重排序只能保证单线程情况下结果的正确性,多线程结果可能有问题。
我们看一下,什么时候能进行重排序、什么情况下不能进行重排序。
以下数据有依赖关系,不能重排序:写后读、写后写、读后写
//写后读
int a = 1;
int b = a;
//写后写
int a = 1;
int a = 2;
//读后写 如果重排序将 b=a放到最后,b的值可能会不同,1、2
int a = 1;
int b = a;
int a = 2;
编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。
int a = 1;
int b = 2;
int c = a + b;
上面3个操作的数据依赖关系如图所示:
a和c之间存在数据依赖关系,同时b和c之间也存在数据依赖关系。因此在最终执行的指令序列中,c不能被重排序到a和b的前面。但a和b之间没有数据依赖关系,编译器和处理器可以重排序a和b之间的执行顺序。下图是该程序的两种执行顺序。
//可以这样
int a = 1;
int b = 2;
int c = a + b;
//也可重排序为如此,两种方式操作的结果相同
int b = 2;
int a = 1;
int c = a + b;
针对1.3中的有序性问题(由于Java在编译期以及运行期的优化,导致了代码的执行顺序未必就是开发者编写代码时的顺序),有两种方案可以解决:volatile、synchronized
另起一个类,复制1.3中的代码。先把之前1.3中TestOrdering的注解全部注释掉,以防压力测试的时候也跑这份代码。
@JCStressTest //用并发压缩工具jcstress 测试类方法
@Outcome(id = {"1"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = {"4"}, expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger1")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger")
@State
public class TestOrdering {
volatile int num = 0;
volatile boolean ready = false;
@Actor //@Actor 表示有多个线程来操作这两个方法
public void actor1(I_Result r) {
if (ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
@Actor
public void actor2(I_Result r) {
num = 2;
ready = true;
}
}
每一轮可能出现1、4,但不会出现0,证明volatile能保证有序性
@JCStressTest //用并发压缩工具jcstress 测试类方法
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok") //@Outcome 对输出结果进行处理, 1 4 打印ok
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger")
@State
public class TestOrdering {
private static Object obj = new Object();
int num = 0;
boolean ready = false;
//线程1执行的代码
@Actor //@Actor 表示有多个线程来操作这两个方法
public void actor1(I_Result r) {
synchronized (obj) {
if (ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
}
//线程2执行的代码 对两个变量进行修改
@Actor
public void actor2(I_Result r) {
synchronized (obj) {
num = 2;
ready = true;
}
}
}
操作方法同1.3。发现本次输出结果与之前不一样,也不会出现0,证明synchronized能保证有序性。
如果想看输出结果,可将@Outcome注解为以下。重新执行,即可查看压力测试结果。发现每一轮可能出现1、4,但不会出现0,因此synchronized能保证有序性。
@JCStressTest
@Outcome(id = {"1"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = {"4"}, expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger1")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger")
@State
public class TestOrdering {
//...
}
加入synchronized后,虽然进行了重排序,保证只有一个线程会进入同步代码块,也能保证有序性。
使用javap反汇编class文件(找到target下面的 类名.class 文件,cmd打开,控制台输入 javap -v -p TestOrdering.class),得到下面的字节码指令。
当java对actor2方法重排序,先**ready=true
** 后**num=2
**。就算线程2先执行actor2,只执行ready=true
,但没来得及执行num=2
(此时num仍为0),cpu切给其他线程。其他线程没锁,也无法进入actor1方法,保证了有序性。
synchronized保证有序性的原理,我们加synchronized后,依然会发生重排序,只不过我们有同步代码块,可以保证只有一个线程执行同步代码中的代码。保证有序性
了解什么是可重入、以及可重入的原理
一个线程可以多次执行synchronized,重复获取同一把锁。
/*
目标:演示synchronized可重入
1.自定义一个线程类
2.在线程类的run方法中使用嵌套的同步代码块
3.使用两个线程来执行
*/
public class Demo01 {
public static void main(String[] args) {
new MyThread().start();
new MyThread().start();
}
}
//1.自定义一个线程类
class MyThread extends Thread {
@Override
public void run() {
synchronized (MyThread.class) {
System.out.println(getName() + "进入了同步代码块1");
synchronized (MyThread.class) {
System.out.println(getName() + "进入了同步代码块2");
}
}
}
}
说明同一个线程可以进入多个同步代码块,拿到同一把锁。锁里面有一个计数器,记录自己被拿几次,结束同步代码块会释放锁 计数器会减1。
上面演示的情况比较简单,两个同步代码块嵌套。我们也可以不用两个同步代码块嵌套,把它放到同一个类的两个不同方法中;也可放到其他类的方法中,以下两种输出结果同上。说明锁重入跟调用哪个对象、哪个方法无关,主要看的是线程、锁。
//同一个类的两个不同方法
public class Demo01 {
public static void main(String[] args) {
new MyThread().start();
new MyThread().start();
}
}
class MyThread extends Thread {
@Override
public void run() {
synchronized (MyThread.class) {
System.out.println(getName() + "进入了同步代码块1");
//锁重入跟调用哪个对象、哪个方法无关,主要看哪个线程、哪个锁
test01();
}
}
//方式二 放同一个类的其他方法中
public void test01() {
synchronized (MyThread.class) {
System.out.println(getName() + "进入了同步代码块2");
}
}
}
//其他类的方法
public class Demo01 {
public static void main(String[] args) {
new MyThread().start();
new MyThread().start();
}
//方式三 放其他类的方法中
public static void test01() {
synchronized (MyThread.class) {
String name = Thread.currentThread().getName();
System.out.println(name + "进入了同步代码块2");
}
}
}
class MyThread extends Thread {
@Override
public void run() {
synchronized (MyThread.class) {
System.out.println(getName() + "进入了同步代码块1");
//锁重入跟调用哪个对象、哪个方法无关,主要看哪个线程、哪个锁
Demo01.test01();
}
}
}
synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁。
synchronized是可重入锁,内部锁对象中会有一个计数器记录线程获取几次锁,在执行完同步代码块时,计数器的数量会-1,直到计数器的数量为0,就释放这个锁。
了解习synchronized不可中断特性、习Lock的可中断特性
一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直阻塞或等待,不可被中断。
/*
目标:演示synchronized不可中断
1.定义一个Runnable
2.在Runnable定义同步代码块
3.先开启一个线程来执行同步代码块,保证不退出同步代码块
4.后开启一个线程来执行同步代码块(阻塞状态)
5.停止第二个线程
*/
public class Demo02_Uninterruptible {
private static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
//1.定义一个Runnable
Runnable run = () -> {
//2.在Runnable定义同步代码块
synchronized (obj) {
String name = Thread.currentThread().getName();
System.out.println(name + "进入同步代码块");
//保证不退出代码块
try {
Thread.sleep(888888);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
//3.先开启一个线程来执行同步代码块,保证不退出同步代码块
Thread t1 = new Thread(run);
t1.start();
Thread.sleep(1000);
//4.后开启一个线程来执行同步代码块(阻塞状态)
Thread t2 = new Thread(run);
t2.start(); //t2处于阻塞状态
//5.停止第二个线程
System.out.println("停止线程前");
t2.interrupt();
System.out.println("停止线程后");
System.out.println(t1.getState());
System.out.println(t2.getState());
}
}
查看结果,t2仍处于BLOCKED状态,我们使用t2.interrupt()
强行中断 并未成功。由于synchronized不可中断,处于阻塞状态的线程无法被中断、会一直等
ReentrantLock有两种方式,一种可中断、一种不可中断。
测试使用lock.lock()不可以从阻塞队列中打断, 一直等待别的线程释放锁
/*
目标:演示Lock不可中断和可中断
*/
public class Demo03_Interruptible {
private static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
// test01();
test02();
}
//演示Lock不可中断 lock()方法
public static void test01() throws InterruptedException {
Runnable run = () -> {
String name = Thread.currentThread().getName();
try {
lock.lock(); //该方式不可被中断,lock()方法无返回值
System.out.println(name + "获得锁,进入锁执行");
Thread.sleep(88888);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); //释放锁,如果不释放其他线程就获取不到锁
System.out.println(name + "释放锁");
}
};
Thread t1 = new Thread(run);
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(run);
t2.start();
System.out.println("停止t2线程前");
t2.interrupt();
System.out.println("停止t2线程后");
Thread.sleep(1000);
System.out.println(t1.getState());
System.out.println(t2.getState());
}
//Thread-0获得锁,进入锁执行;Thread-1在指定时间没有得到锁,做其他操作
//演示Lock可中断
public static void test02() throws InterruptedException {
Runnable run = () -> {
String name = Thread.currentThread().getName();
boolean b = false;
try {
//tryLock在指定时间内 会尝试能否得到锁,能得到 返回true,不能得到 返回false
b = lock.tryLock(3, TimeUnit.SECONDS);
if (b) {
System.out.println(name + "获得锁,进入锁执行");
Thread.sleep(88888);
} else {
System.out.println(name + "在指定时间没有得到锁,做其他操作");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (b) {
lock.unlock();
System.out.println(name + "释放锁");
}
}
};
Thread t1 = new Thread(run);
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(run);
t2.start();
// System.out.println("停止t2线程前");
// t2.interrupt();
// System.out.println("停止t2线程后");
//
// Thread.sleep(1000);
// System.out.println(t1.getState());
// System.out.println(t2.getState());
}
}
执行test01(),发现t2不可被中断。t1已获得锁,t2线程就需要一直等待下去,不能中断,直到获得锁才运行
过了一会输出
执行test02()
过了一会儿输出
不可中断是指,当一个线程获得锁后,另一个线程一直处于阻塞或等待状态,前一个线程不释放锁,后一个线程会一直阻塞或等待,不可被中断。 synchronized属于不可被中断,Lock的lock方法是不可中断的,Lock的tryLock方法是可中断的
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。