文章目录
- 前言
- 为什么要用Synchronized关键字
- 并发编程中的三个问题
- 可见性
- 原子性
- 有序性
- Synchronized保证三大特性
- 使用synchronized保证可见性
- 使用synchronized保证原子性
- 用synchronized保证有序性
- Synchronized的特征
- 可重入特征
- 不可中断特征
前言
synchronized 关键字,代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。即synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized 翻译为中文的意思是同步,也称之为”同步锁“。它包括两种用法:synchronized 方法和 synchronized 块。
为什么要用Synchronized关键字
在使用多线程进行并发编程的时候,如果有多个线程来操作共享数据,很有可能共享数据的值会出现错乱,我们称之为线程安全问题。导致出现问题的原因有:可见性问题;原子性问题;有序性问题。
并发编程中的三个问题
可见性
- 可见性(Visibility):是指一个线程对共享变量进行修改,另一个线程立即得到修改后的新值。
- 出现可见性问题的两个前提:至少有两个线程、有个共享变量
可见性问题演示:
/**
* 目标:演示可见性问题
* 1.创建一个共享变量
* 2.创建一条线程不断读取共享变量
* 3.创建一条线程修改共享变量
*/
public class Test01Visibility {
// 1. 创建一个共享变量
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
// 2. 创建一条线程不断读取共享变量
new Thread(() -> {
while (flag) {
}
}).start();
Thread.sleep(2000);
// 3. 创建一条线程修改共享变量
new Thread(() -> {
flag = false;
System.out.println("线程修改了变量的值为false");
}).start();
}
}
当打印 “另一个线程修改了flag:false”,程序并没有停止,也就是说,线程1获取的flag还是初始获取的true,并没有立即得到修改后的值。
并发编程时,会出现可见性问题,一个线程对共享变量进行修改,另一个线程不能立即得到修改后的值(获取变量的值还是旧的值)
原子性
- 原子性(Atomicity):在一次或多次操作中,要么所有的操作都执行并且不会受其他因素干扰而中断,要么所有的操作都不执行。
- 出现原子性问题的两个前提:至少有两个线程、有个共享变量。
原子性问题演示:
/**
* 目标:演示原子性问题
* 1.定义一个共享变量number
* 2.对number进行1000的++操作
* 3.使用5个线程来进行
*/
public class Test02Atomictity {
// 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 t : list) {
// 等到线程结束,再运行其他的,为了让5个线程都运行结束,再让main线程打印结果
t.join();
}
System.out.println("number = " + number);
}
}
5个线程,每个线程执行1000次number++,最终结果应该是5000,但打印结果是“number:4130”。说明number++操作并不是原子性操作。
使用javap反汇编class文件(命令为:javap -p -v 文件名.class),得到下面的字节码指令:
其中,对于 number++ 而言(number 为静态变量),实际会产生如下的 JVM 字节码指令:
9: getstatic #12 // 获取静态字段number的值并将其推送到操作数栈中
12: iconst_1 // 将整数常量1推送到操作数栈中
13: iadd // 将操作数栈顶的两个整数相加,并将结果推送到操作数栈中
14: putstatic #12 // 将操作数栈顶的值存储到静态字段number中
注释:这段代码的作用是将静态字段number的值增加1。
由此可见number++是由多条语句组成,以上多条指令在一个线程的情况下是不会出问题的,但是在多线程情况下就可能会出现问题。比如一个线程在执行13: iadd时,另一个线程又执行9: getstatic。会导致两次number++,实际上只加了1。
并发编程时,会出现原子性问题,当一个线程对共享变量操作到一半时,另外的线程也有可能来操作共享变量,干扰了前一个线程的操作。
有序性
- 有序性:程序执行的顺序按照代码的先后顺序执行。编译器为了优化性能,有时候会改变程序中语句的先后顺序。
例如程序中:“a=6;b=7;”编译器优化后可能变成“b=7;a=6;”,在这个例子中,编译器调整了语句的顺序,但是不影响程序的最终结果。不过有时候编译器及解释器的优化可能导致意想不到的Bug。
有序性问题代码演示:
jcstress是java并发压测工具:https://wiki.openjdk.java.net/display/CodeTools/jcstress
修改pom文件,添加依赖:
<dependency>
<groupId>org.openjdk.jcstress</groupId>
<artifactId>jcstress-core</artifactId>
<version>0.3</version>
</dependency>
@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger")
@State
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;
}
}
I_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在编译期以及运行期的优化,导致了代码的执行顺序未必就是开发者编写代码时的顺序。
Synchronized保证三大特性
使用synchronized保证可见性
package com.itheima.demo02_concurrent_problem;
/**
案例演示:
一个线程根据boolean类型的标记flag, while循环,另一个线程改变这个flag变量的值,
另一个线程并不会停止循环.
*/
public class Test01Visibility {
// 多个线程都会访问的数据,我们称为线程的共享数据
private static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (run) {
// 增加对象共享数据的打印,println是同步方法
System.out.println("run = " + run);
}
});
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(() -> {
run = false;
System.out.println("时间到,线程2设置为false");
});
t2.start();
}
}
synchronized保证可见性的原理,执行synchronized时,会对应lock原子操作会刷新工作内存中共享变量的值。那加了Synchronized关键字后,就可保证共享资源的可见性。
使用synchronized保证原子性
public class Test02Atomictity {
// 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++) {
synchronized (Test01Atomicity.class) {
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 t : list) {
// 等到线程结束,再运行其他的,为了让5个线程都运行结束,再让main线程打印结果
t.join();
}
System.out.println("number = " + number);
}
}
对number++;增加同步代码块后,保证同一时间只有一个线程操作number++;。就不会出现安全问题。
还是javap反汇编class文件(命令为:javap -p -v 文件名.class),得到下面的字节码指令:
保证一个线程把这几步执行完之后,另一个线程才能执行不会中途被抢占,从而保证原子性。
用synchronized保证有序性
重排序:为了提高程序的执行效率,编译器和CPU会对程序中代码进行重排序。
编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。
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;
使用synchronized保证有序性代码示例:
@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger")
@State
class Test03Orderliness {
private Object obj = new Object();
int num = 0;
boolean ready = false;
// 线程一执行的代码
@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;
}
}
}
synchronized后,虽然进行了重排序,保证只有一个线程会进入同步代码块,也能保证有序性。
Synchronized的特征
可重入特征
可重入:一个线程可以多次执行synchronized,重复获取同一把锁。
可重入特征代码示例:
public class Demo01 {
public static void main(String[] args) {
Runnable sellTicket = new Runnable() {
@Override
public void run() {
synchronized (Demo01.class) {
System.out.println("我是run");
test01();
}
}
public void test01() {
synchronized (Demo01.class) {
System.out.println("我是test01");
}
}
};
new Thread(sellTicket).start();
new Thread(sellTicket).start();
}
}
可重入原理:synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁。
synchronized是可重入锁,内部锁对象中会有一个计数器记录线程获取几次锁啦,在执行完同步代码块时,计数器的数量会-1,直到计数器的数量为0,就释放这个锁。
不可中断特征
不可中断:一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直阻塞或等待,不可被中断。
Synchronized不可中断代码演示:
/**
* 目标:演示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();
// 5.停止第二个线程
System.out.println("停止线程前");
t2.interrupt();
System.out.println("停止线程后");
System.out.println(t1.getState());
System.out.println(t2.getState());
}
}