JavaEE初阶——多线程(9)JUC的程序类和死锁

news/2025/12/8 21:14:04/文章来源:https://www.cnblogs.com/gccbuaa/p/19323665

目录

一、信号量 Semaphoe

二、CountDownLatch

三、线程安全的集合类

3.1 ArrayList

3.1.1 Collections.synchronizedList(new ArrayList)

3.1.2 CopyOnWriteArrayList

3.2多线程使用队列

3.3 多线程使用哈希表

3.3.1 Hashtable

3.3.2 ConcurrentHashMap

四、死锁

4.1 死锁的情景

4.1.1 一个线程获取一把锁

4.1.2 两个线程获取两把锁

4.1.3 多个线程获取多把锁

4.2 造成死锁的原因

4.3 解决死锁问题


一、信号量 Semaphoe

信号量用来表示“可用资源的个数”,本质上就是一个计数器,控制对共享资源的并发访问数量,本质是 “资源访问许可证”—— 计数器大于 0 时允许访问,等于 0 时阻塞等待,释放资源时计数器递增,唤醒等待线程。

我们用停车场举例:

  • 停车场外面通常会有一个显示牌,牌子上会显示当前停车场中车位的可用个数
  • 一辆车进入停车场,显示牌显示的个数减1,表示停车位资源减少1
  • 一辆车从停车场出来,显示牌显示的个数加1,释放了一份停车场资源,外面等待的车就可以进入
  • 如果停车场的车位占满了,那么显示牌上就显示0,这是外面的车如果要进入停车场则需要阻塞等待

在Java中我们可以用Semaphore类来表示信号量,我们通过传给构造方法一个参数来设定信号量的可用资源有多少

Semaphore semaphore = new Semaphore(5);

按照上述代码,semaphore信号量有5个可用资源

acquire()方法表示申请资源,也就是车辆进入停车场的过程

semaphore.acquire();

release()方法表示释放资源,也就是车辆出停车场的过程

semaphore.release();

我们用代码来测试一下信号量的工作流程

public class Demo_1201 {public static void main(String[] args) {// 初始化一个信号量的对象, 指定系统可用资源的数量, 相当于一个停车场有5个车位Semaphore semaphore = new Semaphore(5);// 定义线程的任务Runnable runnable = new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "开始申请资源...");try {// 申请资源, 相当进入停车场,可用车位数减1semaphore.acquire();System.out.println(Thread.currentThread().getName() + "====== 已经申请到资源 ======");// 处理业务逻辑, 用休眠来模拟, 相当于停车时间TimeUnit.SECONDS.sleep(1);// 释放资源, 相当于出停车场, 可用车位数加1semaphore.release();System.out.println(Thread.currentThread().getName() + "***** 释放资源");} catch (InterruptedException e) {e.printStackTrace();}}};// 创建线程执行任务, 10相当于有20辆车需要进停车场for (int i = 0; i < 10; i++) {// 创建线程并指定任务Thread thread = new Thread(runnable);// 启动线程thread.start();}}
}

我们观察代码

  • 前5个线程都是申请资源之后里面申请到了资源,这是因为信号量有5个可用资源,申请即可获得。
  • 再看信号量5个资源被申请完后的结果,剩下5个线程申请资源都没有获取到,这是因为之前5个线程还没有释放资源,信号量现在没有可用资源
  • 当前五个线程释放资源后,后来申请资源的5个线程陆续获得了资源

我们可以通过信号量限制系统中并发执行的线程个数

二、CountDownLatch

CountDownLatch 是 JUC包的线程同步工具,核心功能是:让一个或多个线程等待 “等待”,直到其他指定数量的线程完成任务后,再继续执行。可以理解为 “倒计时门闩”—— 先设定一个倒计时数,线程完成任务后倒计时减 1,直到倒计时归 0,等待的线程才会被 “放行”。

我们通过传入参数count到构造方法创建一个CountDownLatch对象,下面代码的count就是10

CountDownLatch countDownLatch = new CountDownLatch(10);

调用countDown()方法后count就减1

countDownLatch.countDown();

调用await()方法就会让主线程阻塞等待,直到所有的线程都运行结束,也就是count归0

countDownLatch.await();

我们用跑步比赛举例

  • 组委会说:“10 人参赛,全到齐才算结束”(初始化倒计时 10)。
  • 裁判喊预备,10 名选手同时开跑(线程启动)。
  • 选手们陆续冲线,每到 1 人,倒计时减 1(countDown())。
  • 裁判在终点等待,直到最后 1 人到齐(await() 等待倒计时 0)。
  • 所有人到齐后,裁判宣布结束并颁奖。
public class Demo_1202 {public static void main(String[] args) throws InterruptedException {// 指定参赛选手的个数(线程数)CountDownLatch countDownLatch = new CountDownLatch(10);System.out.println("各就各位,预备...");for (int i = 0; i < 10; i++) {Thread player = new Thread(() -> {try {System.out.println(Thread.currentThread().getName() + "开跑.");// 模拟比赛过程, 休眠2秒TimeUnit.SECONDS.sleep(2);System.out.println(Thread.currentThread().getName() + "到达.");// 标记选手已达到终点,让countDownLatch的计数减1, 当计数到0时,表示所有的选手都到达终点,比赛结束countDownLatch.countDown();} catch (InterruptedException e) {e.printStackTrace();}}, "player" + i);// 启动线程player.start();}TimeUnit.MILLISECONDS.sleep(10);System.out.println("===== 比赛进行中 =====");// 等待比赛结束countDownLatch.await();// 颁奖System.out.println("比赛结束, 进行颁奖");}
}

三、线程安全的集合类

我们知道我们之前使用的很多集合类都是线程不安全的集合类,会产生线程安全问题。

3.1 ArrayList

创建10个线程向list里面写入数据

public class Demo_1203 {public static void main(String[] args) {// 先定义一个集合对象(线程不安全)List list = new ArrayList<>();// 多个线程同时对这个集合进行读写操作for (int i = 0; i < 10; i++) {int j = i + 1;Thread t = new Thread(() -> {// 写list.add(j);// 读System.out.println(list);});// 启动线程t.start();}}
}

我们可以看到执行结果报错,ConcurrentModificationException(并发修改异常)的核心原因是:多个线程同时操作了同一个 ArrayList,其中一个线程在遍历集合,另一个线程在修改集合(添加 / 删除元素),导致遍历过程中集合结构被意外改变,触发了 Java 的并发安全检查

那如果我们需要使用集合类ArrayList,除了可以用我们之前学的synchronized或ReentrantLock同步机制,还可以使用什么方法保证线程安全呢

3.1.1 Collections.synchronizedList(new ArrayList)

synchronizedList是标准库提供的⼀个基于synchronized进⾏线程的List

public class Demo_1204 {public static void main(String[] args) {// 创建一个普通集合对象List arrayList = new ArrayList<>();// 通过工具类把普通集合对象,转换线程安全的集合对象List list = Collections.synchronizedList(arrayList);// 多个线程同时对这个集合进行读写操作for (int i = 0; i < 10; i++) {int j = i + 1;Thread t = new Thread(() -> {// 写list.add(j);// 读System.out.println(list);});// 启动线程t.start();}}
}

我们利用synchronizedList工具类把普通集合类转化成了线程安全的集合类,我们来通过源码来分析这是如何做到的

我们能看到调用synchronizedList方法之后返回了一个SynchronizedList实例对象,我们来看一下这个类的方法

观察SynchronizedList类的方法,发现每个方法都是被synchronized包裹的,这就是为什么synchronizedList可以把线程不安全的集合类转化成线程安全的类

3.1.2 CopyOnWriteArrayList

我们也可以直接使用CopyOnWriteArrayList类,这是一个线程安全的类,基于 “写时复制”(Copy-On-Write)的思想设计,适用于读多写少的场景。

核心原理

  • 当对 CopyOnWriteArrayList 进行修改操作(如添加、删除、修改元素)时
  • 它不会直接修改原数组,而是先复制一份原数组的副本,在副本上执行修改操作
  • 完成后再将原数组的引用指向新副本
  • 读操作则直接访问原数组,无需加锁,因此读操作效率很高,且不会阻塞其他线程的读或写。

我们来读源码,分析一下add方法

可以看到add方法全部上锁,方法中新创建了一个es集合类副本,在es副本中添加元素,随后调用setArray方法,这个方法里让array指向了es副本,最后释放锁。实现了线程安全的ArrayList集合类

3.2多线程使用队列

  • ArrayBlockingQueue:基于数组实现的阻塞队列
  • LinkedBlockingQueue:基于链表实现的阻塞队列
  • LinkedBlockingQueue:基于堆实现的优先级阻塞队列
  • LinkedBlockingQueue:最多只包含一个元素的阻塞队列

3.3 多线程使用哈希表

多线程环境下使用HashMap是不安全的

3.3.1 Hashtable

Hashtable实现线程安全是依靠把关键方法加上synchronized关键字来实现的

但是此时就会出现一个问题,当我们调用put方法时,其实我们只会操作一个hash桶,但是这样整体上锁会把整个哈希表锁住,大大降低了效率。而且⼀旦触发扩容,就由该线程完成整个扩容过程,这个过程会涉及到⼤量的元素拷⻉,效率会⾮常低。

3.3.2 ConcurrentHashMap

ConcurrentHashMap相比于Hashtable做出了一系列的改进和优化

  • 读操作没有加锁(但是使⽤了volatile保证从内存读取结果)
  • 只对写操作进⾏加锁.加锁的⽅式仍然是是⽤synchronized,但是不是锁整个对象,⼤⼤降低了锁冲突的概率

同时,ConcurrentHashMap在扩容上也做了优化

  • 扩容时把数组的容量增加到原来的两倍,但并不是一次性把Map中的数据全部复制到新Map中
  • 而只是复制当前访问的下标的元素,这样的操作会使两个Map同时存在一段时间
  • 当查询的时候同时在两个Map里面进行查询,删除也是在两个Map中删除
  • 写入操作时只往新的Map中写

四、死锁

死锁是这样⼀种情形:多个线程同时被阻塞,它们中的⼀个或者全部都在等待某个资源被释放。由于线程被⽆限期地阻塞,因此程序不可能正常终⽌

4.1 死锁的情景

4.1.1 一个线程获取一把锁

一个线程如果重复获取同一把锁两次以上,如果锁是可重入锁,那就不会出现死锁问题

如果是不可重入锁,就会发生死锁

4.1.2 两个线程获取两把锁

假设有两个线程,线程A和线程B,两把锁,锁A和锁B

此时线程A持有锁A,等待锁B;线程B持有锁B,等待锁A。这样循环等待也会造成死锁

public class Demo_1302 {public static void main(String[] args) {// 定义两个锁对象Object locker1 = new Object();Object locker2 = new Object();// 创建线程1,先获取locker1 再获取locker2Thread t1 = new Thread(() -> {System.out.println("t1申请locker1....");synchronized (locker1) {System.out.println("t1获取到了locker1");// 模拟业务执行时间try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}// 在持有locker1的基础上获取locker2synchronized (locker2) {System.out.println("t1获取了所有的锁资源。");}}});// 创建线程2,先获取locker2 再获取locker1Thread t2 = new Thread(() -> {System.out.println("t2申请locker2....");synchronized (locker2) {System.out.println("t2 获取到了locker2.");// 模拟业务执行时间try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}// 持有locker2 的基础上获取locker1synchronized (locker1) {System.out.println("t2获取了所有的锁资源。");}}});// 启动两个线程t1.start();t2.start();}
}

观察结果显示,两个线程都获取到了一把锁,然后线程想获取另一把锁,代码发生死锁。

4.1.3 多个线程获取多把锁

我们以著名的哲学家就餐问题为例

可以看到有五个座位,每个座位上坐着一个哲学家,他们只有两个状态,一个是就餐状态一个是思考状态。每个哲学家左右都有一只筷子,规定只有获取到了两只筷子才可以用餐

我们可以让哲学家先拿左边的筷子,再拿右边的筷子,用完餐再放回原位,等待下一次用餐,这个模型大多数情况运行良好

  • 但是可能会出现极端情况,这种情况就容易出现死锁问题

当每个哲学家都同时拿起了左手筷子,此时他们都要获取右手筷子,都在等待旁边的哲学家放下筷子,从而发生了死锁问题

4.2 造成死锁的原因

  • 互斥使⽤,即当资源被⼀个线程使⽤(占有)时,别的线程不能使⽤
  • 不可抢占,资源请求者不能强制从资源占有者⼿中夺取资源,资源只能由资源占有者主动释放。
  • 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
  • 循环等待,即存在⼀个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了⼀个等待环路。

当上述四个条件都成⽴的时候,便形成死锁。当然,死锁的情况下如果打破上述任何⼀个条件,便可让死锁消失。

4.3 解决死锁问题

我们根据造成死锁的原因来逐个分析

  • 互斥使用:这是锁自带的特性,我们无法破坏
  • 不可抢占:这也是锁自带的特性,我们无法破坏
  • 保持与请求:这和代码的设计相关,其实我们只需要设计合理的获取锁顺序就可以打破
  • 循环等待:同样与代码设计相关,我们合理设计就可以打破
public class Demo_1303 {public static void main(String[] args) {// 定义两个锁对象Object locker1 = new Object();Object locker2 = new Object();// 所有的线程都是先拿locker1再拿locker2// 创建线程1Thread t1 = new Thread(() -> {System.out.println("t1申请locker1....");synchronized (locker1) {System.out.println("t1获取到了locker1");// 模拟业务执行时间try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}// 在持有locker1的基础上获取locker2synchronized (locker2) {System.out.println("t1获取了所有的锁资源。");}}});// 创建线程2Thread t2 = new Thread(() -> {System.out.println("t2申请locker1....");synchronized (locker1) {System.out.println("t2获取到了locker1");// 模拟业务执行时间try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}// 在持有locker1的基础上获取locker2synchronized (locker2) {System.out.println("t2获取了所有的锁资源。");}}});// 启动两个线程t1.start();t2.start();}
}

我们更换了一下获取锁的顺序,代码此时就执行正确了,两个线程都成功获取了锁资源

我们再回顾上文说的哲学家进餐问题,我们可以怎么设计获取锁策略来解决死锁问题呢?

我们可以给筷子编号,让每个哲学家都先拿编号小的筷子,再拿编号大的筷子

abcd哲学家都获取到了小编号的筷子之后,此时e想要获取编号1的筷子,但是此时筷子被哲学家a持有,所以e获取不到筷子1,也无法获取筷子5。

这个时候哲学家d就可以获取到筷子5,可以开始用餐,用完餐之后放下筷子,哲学家c也可以开始用餐,如此往复,不会出现死锁问题

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/993560.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[智能体设计模式] 第 1 章:提示链(Prompt Chaining) - 实践

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

极速AI助手 - 多AI服务桌面助手, 支持MCP工具调用, 内置免费AI功能

极速AI助手是一款专业的桌面端多AI服务交互程序。支持接入多种主流AI服务(内置AI、DeepSeek、通义千问等),集成MCP工具调用功能,让AI助手能够执行更多实用任务。支持多对话管理、Markdown渲染、流式响应等功能,是…

蓝鲸花呗客服妙招帮你脱困省油大空间低配拆解银河的“水桶车细节值得吵一架

谁能想到,一台8万出头的B级插混轿车,竟能同时做到续航超2100km、百公里油耗2.67L、轴距2845mm? 吉利银河A7的上市,直接撕掉了“低价必低配”的标签,甚至让合资混动车型的定价逻辑彻底崩塌。有车主实测从北京到武汉一箱…

吴恩达深度学习课程四:计算机视觉 第一周:卷积基础知识(一)图像处理基础

此分类用于记录吴恩达深度学习课程的学习笔记。 课程相关信息链接如下:原课程视频链接:[双语字幕]吴恩达深度学习deeplearning.ai github课程资料,含课件与笔记:吴恩达深度学习教学资料 课程配套练习(中英)与答案…

Python函数基础实战教程:从定义调用到参数传值全解析

一、Python函数核心意义:为什么要学函数基础? Python函数是代码复用、逻辑封装的核心载体,也是新手从「线性代码编写」过渡到「模块化编程」的关键。无论是自动化脚本、数据分析还是Web开发,函数都能让代码更简洁、…

索引数组读取修改添加

索引数组读取修改添加1 $xm = array(小明,男,28,5888.88);2 3 //1.读取数据 4 echo $xm[0].同学的工资是:.$xm[3].元人民币。;5 6 //2.修改数据 7 $xm[0] = 小张;8 $xm[1]…

12.08

今天上午统一建模语言上机数构上课,下午Java

zsj_蓝桥python系列二_Python 基础语法 _Python 列表推导式

zsj_蓝桥python系列二_Python 基础语法 _Python 列表推导式Python 基础语法 Python 列表推导式 你有没有写过这样的代码?想生成一个新列表,得先建空列表、再写 for 循环、最后用append()加元素 —— 又长又麻烦。今天…

白带异常用药推荐:科学应对妇科炎症的健康指南

白带是女性生殖系统健康的“晴雨表”,正常情况下呈透明或白色糊状、无异味。当白带出现颜色异常(如黄绿色、灰色)、性状改变(如豆腐渣样、泡沫状)或伴随瘙痒、异味时,可能提示阴道炎、宫颈炎等妇科炎症。世界卫生…

获取数组长度即最大下标

获取数组长度即最大下标$xm = array(小明,男,28,5888.88);//count()函数用于返回数组长度(元素的个数),int(整型)$x = count($xm);echo $x;echo "<br>";var_dump($x);//3.使用数组长度添加数据$xm[c…

第49天

今天学习的java

JAVA学习笔记-DAY3

引用类型 VS 指针在Java中,引用类型的变量非常类似于C/C++的指针。 引用类型指向一个对象,指向对象的变量是引用变量。这些变量在声明时被指定为一个特定的类型,比如 Employee、Puppy 等。变量一旦声明后,类型就不…

北京婚姻家庭法律事务所服务观察:专业机构业务能力解析

在社会关系日益复杂化的当下,婚姻家庭领域的法律需求呈现多样化趋势,涵盖离婚纠纷、财产划分、子女抚育、遗产继承等多个维度。专业的婚姻家庭法律事务所凭借其对细分领域的深耕,为当事人提供法律支持与权益保障,成…

火小兔的两种交互方式与全部命令 - Magic

使用本平台开发脚本 从微软官方渠道安装完毕后,可以从开始菜单中搜索到“机关区”和“火小兔”。机关区——调试窗口(演示):直接双击“机关区”,进入界面之后就可以直接编写,调试,编辑,浏览,运行本平台命令;…

123_尚硅谷_匿名函数

123_尚硅谷_匿名函数1.匿名函数使用方式1:在定义匿名函数时就直接调用 2.匿名函数使用方式2:将匿名函数赋给一个变量,再通过该变量调用匿名函数 3.全局匿名函数

推荐几个模切机品牌:国内实力厂商推荐

模切机作为印后加工和精密制造领域的关键设备,广泛应用于印刷、包装、电子、图文处理等行业,其性能直接影响产品的加工精度、生产效率及成品质量。在各行业对加工工艺要求不断提升的背景下,选择技术成熟、品质可靠的…

白带异常用药品牌排行榜:科学守护女性生殖健康

白带是女性生殖系统健康的“晴雨表”,正常情况下呈无色透明或乳白色糊状,无异味。当出现颜色、质地、气味异常(如黄绿色、豆腐渣样、鱼腥味等)时,可能提示阴道炎症、宫颈病变或盆腔感染等问题。及时识别症状并采取…

洛谷 P3959

NOIP 2017 提高组给定 \(n\) 个点,\(m\) 条边的无向连通图。要选出一棵有根生成树,设 \(u\) 与 \(fa_u\) 之间的边长度为 \(l_u\),总代价为 \(\sum l_u \cdot dis(u, root)\),求最小总代价是多少? \(n \le 12, m …