当前位置: 首页 > news >正文

【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 解决的是 一个线程读,一个线程修改。

http://www.dtcms.com/a/335539.html

相关文章:

  • 已开源:Highcharts.NET,Highcharts Android,与Highcharts iOS集成
  • VS Code配置MinGW64编译Apache Arrow C++库
  • 2025年服装工厂生产管理系统软件推荐
  • 远程桌面管理工具 - 交互式版本
  • 达梦数据库常见的重要巡检语句
  • Qt5 的基础知识
  • 【UEFI系列】ACPI
  • 51单片机-驱动独立按键模块教程
  • 类的静态成员的定义、调用及继承详解【C++每日一学】
  • AI+预测3D新模型百十个定位预测+胆码预测+去和尾2025年8月17日第163弹
  • 深度学习-计算机视觉-数据增广/图像增广
  • 《MATLAB绘图进阶教程》主要内容与专栏目录(持续更新中。。。)
  • GitHub 热榜项目 - 日榜(2025-08-17)
  • 智能体与MCP的核心流程和差异点(适合初学者)
  • IDEA飞算插件测评:重塑AI编码价值的实战体验
  • 【IDEA】设置Debug调试时调试器不进入特定类(Spring框架、Mybatis框架)
  • GEO(生成引擎优化)是什么?GEO优化怎么做
  • 在QML中使用Chart组件
  • Java Stream ForEach算子实现:ForEachOps
  • 半敏捷卫星观测调度系统的设计与实现
  • Git登录配置的详细方法
  • CSS中linear-gradient 的用法
  • Python字符串净化完全指南:专业级字符清理技术与实战
  • 开发者说 | EmbodiedGen:为具身智能打造可交互3D世界生成引擎
  • 区块链练手项目(持续更新)
  • Linux入门指南:基础开发工具---vim
  • 飞算AI 3.2.0实战评测:10分钟搭建企业级RBAC权限系统
  • ZKmall开源商城的移动商城搭建:Uni-app+Vue3 实现多端购物体验
  • PostgreSQL——用户管理
  • 轻松配置NAT模式让虚拟机上网