多线程—线程安全集合类与死锁

ops/2025/4/2 4:00:02/

上篇文章:

多线程—JUChttps://blog.csdn.net/sniper_fandc/article/details/146713322?fromshare=blogdetail&sharetype=blogdetail&sharerId=146713322&sharerefer=PC&sharesource=sniper_fandc&sharefrom=from_link

目录

线程安全的集合类

1.1 ArrayList确保线程安全

1.2 Queue确保线程安全

1.3 哈希表确保线程安全

2 死锁


线程安全的集合类

        前面讲过,Vector、Stack(Stack线程安全的原因是因为继承Vector实现)、HashTable、ConcurrentHashMap、StringBuffer是线程安全的,其他的集合类不是线程安全的,如果想要在多线程环境下使用集合类确保线程安全,就要结合确保线程安全的机制来使用:

1.1 ArrayList确保线程安全

        1.使用synchronized或ReentrantLock。

        2.Collections.synchronizedList(new ArrayList):synchronizedList是标准库提供的一个基于synchronized进行线程同步List,对可能影响到线程安全的操作上使用synchronized关键字。

        3.CopyOnWriteArrayList:当多线程同时修改ArrayList时,会把容器拷贝一份,往拷贝的容器修改数据,在把拷贝容器替换掉原来的容器。但是引发缺点:当容器数据很多时,拷贝操作开销大;同时新修改的数据无法及时读取到。因此这个类适合读多写少的场景,读可以并发读,不用加锁,性能高。(实际上,在CopyOnWriteArrayList源码中,对于修改操作的拷贝是加了ReentrantLock锁的,但是加了锁再拷贝,拷贝的意义也就丢失了)

1.2 Queue确保线程安全

        1.ArrayBlockingQueue:基于数组实现的阻塞队列。

        2.LinkedBlockingQueue:基于链表实现的阻塞队列。

        3.PriorityBlockingQueue:优先级阻塞队列(堆实现)。

        4.TransferQueue:最多只包含一个元素的阻塞队列。

1.3 哈希表确保线程安全

        1.Hashtable:内部实现把关键的方法都加了synchronized关键字,这等于对哈希表加了一把锁(锁对象是当前Hashtable对象),因此如果要并发访问不同位置的数据,也会产生锁竞争。

        对于哈希表的size修改,也是使用synchronized控制。同时一旦触发哈希表扩容操作,就会导致当前线程的put方法负责完成扩容,此过程涉及大量元素拷贝,导致此次操作时间很长。解决方案:在Java 8中,ConcurrentHashMap对Hashtable做了一些优化。

        2.ConcurrentHashMap:synchronized的锁粒度进行了细化,对每个哈希桶都加了一把锁,同一个哈希桶的元素修改才会涉及锁竞争问题,不同哈希桶的元素修改不涉及锁竞争。同时读操作不加锁(使用volatile关键字保证内存可见性),写操作加锁。

        对于哈希表的size修改,使用CAS机制,避免使用重量级锁。对于扩容操作,采用“化整为零”策略:一旦触发扩容,首先创建一个新的更大容量的哈希表,插入元素时,插入到新的哈希表,查找元素时,新旧哈希表一起查询。对于扩容时的每一个操作,都参与元素移动的过程,即每次操作移动一小批元素(重新哈希),直到所有的元素都搬运完,旧的哈希表删除,完成扩容过程。这种扩容操作避免了扩容时某次操作时间过长的问题。

        注意1:在Java 7时,ConcurrentHashMap的线程安全的实现是使用“分段锁”,即把若干哈希桶分为一个“段”,每个段共享一把锁,每个段的若干哈希桶的修改涉及锁竞争。底层实现是数组+链表(哈希桶)。而在Java 8,ConcurrentHashMap的线程安全的实现优化成上述每个桶一把锁的机制,底层实现是数据+链表/红黑树(链表元素>=8个,链表转化为红黑树)。

        注意2:HashMap、Hashtable和ConcurrentHashMap的区别?从线程安全角度来讲:HashMap线程不安全,Hashtable和ConcurrentHashMap线程安全从加锁粒度角度来讲:Hashtable的锁对象是当前Hashtable对象,ConcurrentHashMap的锁对象是每个链表的头结点,ConcurrentHashMap还利用了CAS机制保证Size的线程安全和使用“化整为零”的扩容机制。从Key是否为空角度来讲:HashMap的Key允许为null,Hashtable和ConcurrentHashMap的Key不允许为null。

2 死锁

        死锁是多个线程中一个或多个同时等待某个资源的释放,因为线程之间相互阻塞,于是程序无法正常结束,这种现象就被称为死锁。

        死锁的常见场景有:

        1.一个线程一把锁:不可重入锁,当一个线程已经持有一把锁时,线程任务的执行逻辑还需要对该锁对象加锁,由于当前锁对象未被释放,因此就会导致自己等待自己释放资源的死锁现象。

        2.两个线程两把锁:线程需要同时持有两把锁才能进行任务的执行,但是由于一个线程持有一把锁,另一个线程持有另一把锁,两个线程都等待对方的另一把锁的释放,因此导致了线程相互阻塞等待对方的锁的死锁现象。

java">Object lock1 = new Object();Object lock2 = new Object();Thread t1 = new Thread() {@Overridepublic void run() {synchronized (lock1) {synchronized (lock2) {//...}}}};t1.start();Thread t2 = new Thread() {@Overridepublic void run() {synchronized (lock2) {synchronized (lock1) {//...}}}};
t2.start();

        3.多个线程多把锁:以哲学家就餐问题为例,一群哲学家在餐桌旁就餐,每个人左手边和右手边各一根筷子,两个人之间只有一根筷子,只有同时拿到两根筷子组成一双才能吃饭,如果每个哲学家都同时拿左边的筷子或右边的筷子,就会导致每个人都只拿到了一根筷子,从而每个人都无法就餐:

        要解决这个死锁问题,可以为每根筷子编号,规定哲学家只能按编号从小到大的顺序取筷子,比如只有先拿到筷子1才能拿筷子2。也可以为哲学家编号,每次满足部分哲学家的就餐需求,比如奇数同时拿起左右两边的筷子(如果出现筷子不足的情况,就让某位哲学家放弃),待奇数放下筷子偶数再拿起左右两边的筷子就餐。

        上述分析过程也体现了死锁的四个必要条件:

        1.互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用。

        2.不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。

        3.请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。

        4.循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源,这样就形成了一个等待环路。

        这四个条件同时存在就构成了死锁,要解决死锁,破坏其中任何一个条件即可,最好的手段是破坏循环等待条件,即开发人员从代码逻辑入手:

java">Object lock1 = new Object();Object lock2 = new Object();Thread t1 = new Thread() {@Overridepublic void run() {synchronized (lock1) {synchronized (lock2) {//...}}}};t1.start();Thread t2 = new Thread() {@Overridepublic void run() {synchronized (lock1) {synchronized (lock2) {//...}}}};t2.start();

        约定好获取锁的顺序,破坏了t1等t2,t2等t1的等待环路,从而解决了死锁问题。

        多线程系列到此基本就结束了,下个系列更新文件IO的相关文章。


http://www.ppmy.cn/ops/171239.html

相关文章

【Prompt实战】邮件意图分类助手

本文原创作者:姚瑞南 AI-agent 大模型运营专家,先后任职于美团、猎聘等中大厂AI训练专家和智能运营专家岗;多年人工智能行业智能产品运营及大模型落地经验,拥有AI外呼方向国家专利与PMP项目管理证书。(转载需经授权&am…

parallelStream线程问题及解决方案

parallelStream可以在多个线程中并行处理流数据,提高性能。然而,如果在处理过程中涉及共享的可变状态,可能会导致线程不安全的问题。以下是一个简单的示例演示了如何在不正确的使用情况下导致线程安全问题: import java.util.Arr…

豪越HYCloud消防一体化安全管控平台:融合创新,重塑格局

在当今科技飞速发展的时代,消防行业正经历着前所未有的融合创新变革。豪越消防一体化平台作为这一变革中的佼佼者,以其先进的理念和技术应用,为消防管理格局带来了全新的重塑机遇。 消防行业融合创新的趋势日益明显。随着物联网、大数据、人工…

深入探索Node.js Koa框架:构建现代化Web应用的2000字实践指南

引言:Koa的演进与核心设计哲学 在Node.js后端开发领域,Koa作为Express原班人马打造的新一代Web框架,以其轻量级架构和创新的中间件处理机制,正在重塑服务端开发范式。本指南将深度解析Koa的核心技术,从基础搭建到企业…

从入门到精通:SQL注入防御与攻防实战——红队如何突破,蓝队如何应对!

引言:为什么SQL注入攻击依然如此强大? SQL注入(SQL Injection)是最古老且最常见的Web应用漏洞之一。尽管很多公司和组织都已经采取了WAF、防火墙、数据库隔离等防护措施,但SQL注入依然在许多情况下能够突破防线&#…

奇怪的电梯问题优化建议

文章目录 奇怪的电梯问题优化建议1. 双向BFS优化2. 迭代加深DFS (IDDFS)**原理****实现步骤****伪代码示例****适用场景****优缺点对比****关键点** 3. 预处理优化4. 启发式搜索 (A*算法)优化策略对比实际应用建议进一步优化思路 奇怪的电梯问题优化建议 针对"奇怪的电梯…

Ubuntu 24.04 安装 Docker 详细教程

前言 Docker 是目前最流行的容器化技术,它可以帮助开发者快速部署和运行应用程序。本文将详细介绍在 Ubuntu 24.04 (Noble Numbat) 上安装 Docker 的完整步骤,包括配置镜像加速等实用技巧。 一、准备工作 1.1 系统要求 Ubuntu 24.04 LTS 具有 sudo 权…

基于 GEE 的 2010—2020 年归一化植被指数 NDVI 与核植被指数 kNDVI 年度变化分析

目录 1 前言 2 代码解析 2.1 定义感兴趣区域并居中显示 2.2 加载数据集并过滤 2.3 定义 kNDVI 计算函数 2.4 生成年度影像集合 2.5 计算 NDVI 和 kNDVI 的时间序列 2.6 可视化时间序列 2.7 导出影像到 Google Drive 3 完整代码 4 运行结果 1 前言 在遥感领域&#…