【JavaEE】多线程初阶3:死锁 + 线程安全 + volatile 关键字
【JavaEE】多线程初阶3:死锁 + 线程安全类 + volatile 关键字
- 一、死锁
- 1.1 死锁是什么?
- 1.2 死锁的3个场景
- 1.2.1 上面提到的,一个线程一把锁,连续加锁两次。
- 1.2.2 两个线程两把锁,每个线程先获取到一把锁,再尝试获取对方的锁。
- 1.2.3 N 个线程,M 把锁,也会构成死锁。
- 1.3 死锁的危害
- 1.4 死锁的4个必要条件,如何解决死锁?
- 1.4.1 锁是互斥的
- 1.4.2 锁不可被抢占
- 1.4.3 请求和保持
- 1.4.4 循环等待
- 二、Java 标准库中的线程安全类
- 三、 volatile 关键字
- 3.1 内存可见性 引起的线程安全问题
- 3.2 volatile 能保证内存可见性
- 3.3 内存可见性 扩展:Java 内存模型(内存和寄存器 或者 主内存加工作内存)
- 3.4 volatile 不保证原子性
一、死锁
因为 synchronized 的 可重入特性,引入死锁的概念 。
1.1 死锁是什么?
————————————————————————————————————————————
————————————————————————————————————————————
package thread;
//1.1 死锁是什么?
public class Demo19 {public static void main(String[] args) {Object locker = new Object();//下面的代码。由于可重入特性,不会出现死锁。synchronized (locker){synchronized (locker){synchronized (locker){synchronized (locker){}//执行到这里,出了大括号,是否要真正释放锁?//不能。假设再上面释放锁了,意味着:外面三层锁对应的{ }就失效了。//dosomething(); 这里的逻辑不再线程安全了}}}//应该在这个地方,才能真正释放锁。}
}
————————————————————————————————————————————
可重入锁:1. 需要在锁里记录是哪个线程持有的这把锁;2. 给锁安排一个计数器。
1.2 死锁的3个场景
1.2.1 上面提到的,一个线程一把锁,连续加锁两次。
通过可重入锁 解决,但它只能处理死锁中的一种情况,没法处理其他情况。
1.2.2 两个线程两把锁,每个线程先获取到一把锁,再尝试获取对方的锁。
————————————————————————————————————————————
package thread;//两个线程两把锁,每个线程先获取到一把锁,再尝试获取对方的锁
public class Demo20 {public static void main(String[] args) throws InterruptedException {Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(() -> {synchronized (locker1){System.out.println("t1 拿到了 locker1");//此处的sleep 目的是:让t1 和 t2确实都已经拿到各自的 Locker1 和 locker2//然后再进行后续操作try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker2){//获取到 locker1的{ }里面 去获取locker2System.out.println("t1 拿到了 locker2");}}});Thread t2 = new Thread(() -> {synchronized (locker2){System.out.println("t2 拿到了 locker2");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker1){//获取到 locker2的{ }里面 去获取locker1System.out.println("t2 拿到了 locker1");}}});//上面 构成死锁:先拿到一把锁,不释放;再去拿另一把锁t1.start();t2.start();t1.join();t2.join();}/* 输出结果:t1 拿到了 locker1t2 拿到了 locker2*/
}
————————————————————————————————————————————
根据输出的结果,发现后面的代码没有执行?
1.2.3 N 个线程,M 把锁,也会构成死锁。
1.3 死锁的危害
1.4 死锁的4个必要条件,如何解决死锁?
————————————————————————————————
1.4.1 锁是互斥的
1.4.2 锁不可被抢占
————————————————————————————————————————————
1.4.3 请求和保持
拿到第一把锁的情况下,不去释放第一把锁,再尝试请求第二把锁。
package thread;public class Demo20 {public static void main(String[] args) throws InterruptedException {Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(() -> {synchronized (locker1){System.out.println("t1 拿到了 locker1");//此处的sleep 目的是:让t1 和 t2确实都已经拿到各自的 Locker1 和 locker2//然后再进行后续操作try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}//打破请求和保持,解决死锁: 把第二把锁的加锁操作,放到第一把锁外。先释放第一把锁,再获取第二把锁synchronized (locker2){System.out.println("t1 拿到了 locker2");}});Thread t2 = new Thread(() -> {synchronized (locker2){System.out.println("t2 拿到了 locker2");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}synchronized (locker1){System.out.println("t2 拿到了 locker1");}});t1.start();t2.start();t1.join();t2.join();}/* 输出结果:t1 拿到了 locker1t2 拿到了 locker2t2 拿到了 locker1t1 拿到了 locker2*/
}
————————————————————————————————————————————
但是:
1.4.4 循环等待
等待锁释放,等待的关系(顺序),构成了循环。
————————————————————————————————————————————
package thread;public class Demo20 {public static void main(String[] args) throws InterruptedException {Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(() -> {synchronized (locker1){System.out.println("t1 拿到了 locker1");//此处的sleep 目的是:让t1 和 t2确实都已经拿到各自的 Locker1 和 locker2//然后再进行后续操作try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker2){System.out.println("t1 拿到了 locker2");}}//等待});Thread t2 = new Thread(() -> {synchronized (locker1){//调整加锁的顺序,先locker1 后 locker2System.out.println("t2 拿到了 locker1");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker2){System.out.println("t2 拿到了 locker2");}}});t1.start();t2.start();t1.join();t2.join();}/* 输出结果:t1 拿到了 locker1t1 拿到了 locker2t2 拿到了 locker1t2 拿到了 locker2*/
}
————————————————————————————————————————————
二、Java 标准库中的线程安全类
Java 标准库中很多都是线程不安全的。这些类可能会涉及到多线程修改共享数据,⼜没有任何加锁措
施。
- ArrayList
- LinkedList
- HashMap
- TreeMap
- HashSet
- TreeSet
- StringBuilder
这些常用的集合类,大多是线程不安全的。他们把加锁的决策交给程序员。
————————————————————————————————————————————
但是还有⼀些是线程安全的, 使⽤了⼀些锁机制来控制。
- Vector (不推荐使⽤)
- HashTable (不推荐使⽤)
- ConcurrentHashMap
- StringBuffer
- String
三、 volatile 关键字
3.1 内存可见性 引起的线程安全问题
举例:
package thread;import java.util.Scanner;//内存可见性 引起的线程安全问题
public class Demo21 {private static int flag = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {//此处创建一个循环while(flag == 0){//啥都不做}System.out.println(" t1 结束");});Thread t2 = new Thread(() -> {//修改flag的值Scanner scanner = new Scanner(System.in);System.out.println("请输入一个 flag 的值:");flag = scanner.nextInt();System.out.println("t2 结束");});t1.start();t2.start();t2.join();t2.join();}
/*
请输入一个 flag 的值:1
t2 结束*/
}
这个问题产生的原因,就是“内存可见性”。flag 变量的修改,对于 t1 线程 “不可见”了,t2 改了 flag,但是 t1 没看见。
————————————————————————————————————————————
t1 没看见的原因是 编译器优化 造成的。
————————————————————————————————————————————
上述内存可见性问题,是编译器优化机制,自身出现的。
3.2 volatile 能保证内存可见性
通过这个关键字,提醒编译器,某个变量是“易变”的。此时就不要针对这种“易变”的变量,进行上述优化。
package thread;import java.util.Scanner;public class Demo21 {private static volatile int flag = 0;//volatile关键字 修饰 flag 变量public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {//此处创建一个循环while(flag == 0){//啥都不做}System.out.println("t1 结束");});Thread t2 = new Thread(() -> {//修改flag的值Scanner scanner = new Scanner(System.in);System.out.println("请输入一个 flag 的值:");flag = scanner.nextInt();System.out.println("t2 结束");});t1.start();t2.start();t2.join();t2.join();}
/*
请输入一个 flag 的值:
1
t2 结束
t1 结束*/
}
3.3 内存可见性 扩展:Java 内存模型(内存和寄存器 或者 主内存加工作内存)
(1)主内存(平时所说的内存)+工作内存(CPU上的寄存器和缓存)
————————————————————————————————————————————
(2)内存和寄存器
————————————————————————————————————————————
编译器优化, 并非是 100% 触发,根据不同的代码结构,可能产生出不同的优化效果。
(有优化/没有优化/优化方式.….)
package thread;import java.util.Scanner;public class Demo21 {private static int flag = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {//此处创建一个循环while(flag == 0){//加上sleeptry {Thread.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("t1 结束");});Thread t2 = new Thread(() -> {//修改flag的值Scanner scanner = new Scanner(System.in);System.out.println("请输入一个 flag 的值:");flag = scanner.nextInt();System.out.println("t2 结束");});t1.start();t2.start();t2.join();t2.join();}
/*
请输入一个 flag 的值:
1
t2 结束
t1 结束*/
}
3.4 volatile 不保证原子性
volatile 这个关键字, 能够解决内存可见性问题引起的线程安全问题,但是不具备原子性这样的特点。
package thread;
//volatile 不保证原⼦性
public class Demo22 {private static volatile int count = 0;//volatile 解决的是内存可见性问题,不是原子问题public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {count++;}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {count++;}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);//多次执行,count 为随机值//存在线程安全问题}
}
【synchronized 和 volatile 差异】
synchronized 和 volatile 是两个不同的维度;
synchronized 解决的是 两个线程修改;
volatile 解决的是 一个线程读,一个线程修改。