【JAVA语言-第20话】多线程详细解析(二)——线程安全,非线程安全的集合转换成线程安全
目录
线程安全
1.1 概述
1.2 案例分析
1.3 解决线程安全问题
1.3.1 使用synchronized关键字
1.3.1.1 同步代码块
1.3.1.2 同步方法
1.3.2 使用Lock锁
1.3.2.1 概述
代码示例:
1.4 线程安全的类
1.4.1 非线程安全集合转换成线程安全集合
1.5 总结
线程安全
1.1 概述
指如果有多个线程在同时运行,而这些线程可能会同时运行某段代码,程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全。
1.2 案例分析
那什么情况下会导致【线程不安全】呢?看如下案例:
假设:有三家电影院,卖票形式分别为以下A、B、C三种。

思考:哪一种卖票形式会出现问题呢?
- 第1种:开一个窗口,卖100张票,不会出现问题,单线程程序不存在线程安全问题。
- 第2种:开三个窗口,但是每个窗口票的号码不冲突,也不会出现问题,属于线程安全。
- 第3种:开三个窗口,但是每个窗口票的号码一样。如果1,2,3三个窗口访问同一张票,那进入的结果和返回的结果很有可能不一致。这就出现了线程安全问题。

结论:
买票出现了线程安全问题,可能会出现重复的票和不存在的票,但是线程安全问题是不允许出现的。
1.3 解决线程安全问题
那怎么解决线程安全问题呢?
我们可以让一个线程在访问共享数据的时候,无论是否失去了CPU的执行权,让其他的线程只能等待,等待当前线程买完票,其他线程在进行买票。保证同时只有一个线程在买票。
1.3.1 使用synchronized关键字
在Java中,synchronized是一个关键字,用于控制多个线程对 对象或方法 的访问。当一个代码块被标记为synchronized时,只允许一个线程在同一时间执行该代码块。这样做是为了防止并发访问和潜在的数据损坏或不一致。该关键字可以使用在同步代码块或者同步方法用来解决线程安全问题。
1.3.1.1 同步代码块
一个同步代码块一次只允许一个线程进入,并确保它完成执行后其他线程才能进入。这是通过使用与同步代码块关联的对象的内在锁(或监视器)来实现的。
格式:
synchronized(锁对象){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
注意事项:
1.同步代码块中的锁对象,可以使用任意的对象。
2.必须保证多个线程使用的锁对象是同一个。
3.锁对象作用:把同步代码块锁住,只让一个线程在同步代码块中执行。
代码示例:
RunnableImpl.java:多线程的实现类
package com.zhy.multiplethread;public class RunnableImpl implements Runnable{/*** 共享票数*/private int ticket = 10;/*** 设置线程任务:卖票*/@Overridepublic void run() {//使用死循环,让卖票操作重复执行while (true){//同步代码块,保证每次只有一个线程占用锁对象synchronized (this){//当存在余票时,进行卖票操作if (ticket > 0){//为了表示卖票需要时间,暂停10毫秒try {Thread.sleep(10);}catch (InterruptedException e){e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " 正在卖第 " + ticket + "张票");ticket--;}}//出了同步代码块,归还锁对象,供线程重新抢占}}
}
TestThread.java:线程测试类
package com.zhy.multiplethread;public class TestThread {public static void main(String[] args) {RunnableImpl impl = new RunnableImpl();Thread t1 = new Thread(impl);Thread t2 = new Thread(impl);Thread t3 = new Thread(impl);//开启3个线程一起抢夺CPU的执行权,谁抢到谁执行t1.start();t2.start();t3.start();}
}
输出结果:多个线程共同抢占CPU进行卖票操作,不会出现线程安全问题。

1.3.1.2 同步方法
当一个方法被声明为synchronized时,即使有多个线程同时访问该方法,也只允许一个线程执行。在这种情况下使用的锁是调用该方法的对象实例。
格式:
修饰符 synchronized 返回值类型 方法名(参数列表){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
使用步骤:
1.把访问了共享数据的代码抽取出来,放到一个方法中。
2.在方法上添加synchronized修饰符
代码示例:
RunnableImpl.java:多线程的实现类
package com.zhy.multiplethread;public class RunnableImpl implements Runnable{/*** 共享票数*/private int ticket = 10;/*** 设置线程任务:卖票*/@Overridepublic void run() {//使用死循环,让卖票操作重复执行while (true){payTicket();}}/*** 同步方法:卖票*/public synchronized void payTicket(){//当存在余票时,进行卖票操作if (ticket > 0){//为了表示卖票需要时间,暂停10毫秒try {Thread.sleep(10);}catch (InterruptedException e){e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " 正在卖第 " + ticket + "张票");ticket--;}}
}
结论:
通过使用synchronized关键字,我们可以确保在多线程环境中共享资源的安全访问。
1.3.2 使用Lock锁
1.3.2.1 概述
java.util.concurrent.locks.Lock接口:实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作。
Lock接口中的方法:
void lock():获取锁
void unlock():释放锁
实现类:
java.util.concurrent.locks.ReentrantLock implements Lock接口
使用步骤:
1.在成员位置创建一个ReentrantLock对象。
2.在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁。
3.在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁。一般放在finally里面执行。
代码示例:
RunnableImpl.java:多线程实现类
package com.zhy.multiplethread;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class RunnableImpl implements Runnable{/*** 共享票数*/private int ticket = 10;Lock l = new ReentrantLock();/*** 设置线程任务:卖票*/@Overridepublic void run() {//使用死循环,让卖票操作重复执行while (true){//获取锁:当存在余票时,进行卖票操作l.lock();try {if (ticket > 0) {//为了表示卖票需要时间,暂停10毫秒Thread.sleep(10);System.out.println(Thread.currentThread().getName() + " 正在卖第 " + ticket + "张票");ticket--;}}catch (InterruptedException e){e.printStackTrace();}finally {//释放锁:为了避免忘记释放或者出现异常,造成死锁,该操作放在finally中执行l.unlock();}}}
}
结论:
同步保证了只能有一个线程在同步中执行共享数据,保证率安全,但是程序频繁的判断锁、获取锁、释放锁、程序的效率会降低。
1.4 线程安全的类
如果一个类,所有的方法都是有synchronized修饰的,那么该类就叫做线程安全的类。保证同一时间,只有一个线程能够进入 这种类的一个实例 的去修改数据,进而保证了这个实例中的数据的安全,不会同时被多个线程修改而变成脏数据。
- 操作集合的线程安全的类:Vector,Hashtable
- 操作字符串的线程安全的类:StringBuffer
1.4.1 非线程安全集合转换成线程安全集合
ArrayList是非线程安全的,如果多个线程可以同时进入一个ArrayList对象的add/remove方法。那会造成什么后果呢,我们先看一个案例。
场景:
定义一个List集合,初始化5个元素。定义一个增加线程(往集合的头部持续插入1000个元素)和减少线程(从集合的头部持续移除1000个元素)同时操作该集合,我们最终想要的效果是:增加和减少的次数一致,最终集合内的元素仍然是初始化的元素。
代码示例:
package com.zhy.multiplethread;import com.zhy.thread.RunnableImpl;import java.util.ArrayList;
import java.util.List;public class TestThread {public static void main(String[] args) {//初始化List集合List<Integer> nonThreadSafeList = new ArrayList<Integer>();for (int i = 0; i < 5; i++){nonThreadSafeList.add(i + 3);}System.out.println("初始化List集合:" + nonThreadSafeList);//验证:使用两个线程同时往集合中插入1000个元素,在删除1000个元素int n = 1000;Thread[] addThreads = new Thread[n];Thread[] reduceThreads = new Thread[n];//将所有 增加线程 加入到addThreads数组中for (int i = 0; i < n; i++){Thread addThread = new Thread(){@Overridepublic void run() {nonThreadSafeList.add(0,1);try {//暂停1000毫秒,给其他线程抢占CPU的时间Thread.sleep(1000);}catch (InterruptedException e){e.printStackTrace();}}};addThread.start();addThreads[i] = addThread;}//将所有 减少线程 加入到reduceThreads数组中for (int i = 0; i < n; i++){Thread reduceThread = new Thread(new RunnableImpl(){@Overridepublic void run() {if (nonThreadSafeList.size() > 0){nonThreadSafeList.remove(0);}try {//暂停1000毫秒,给其他线程抢占CPU的时间Thread.sleep(1000);}catch (InterruptedException e){e.printStackTrace();}}});reduceThread.start();reduceThreads[i] = reduceThread;}//等待所有增加线程执行完成for (Thread addThread : addThreads){try {//将 增加线程 加入到主线程中addThread.join();}catch (InterruptedException e){e.printStackTrace();}}//等待所有 减少线程 执行完成for (Thread reduceThread : reduceThreads){try {//将 减少线程 加入到主线程中reduceThread.join();}catch (InterruptedException e){e.printStackTrace();}}//所有增加线程 和 减少线程 执行完毕后,List集合中的数据:正确应该为初始数据System.out.println("所有增加线程 和 减少线程 执行完毕后,List集合中的数据:" + nonThreadSafeList);}
}
输出结果:
使用非线程安全的集合进行多线程处理,很显然最终的结果并不是我们想要的,出现了null的元素,且集合内的元素也不是初始化的元素。

注:并不是每一次执行都会出现错误的结果,多执行几次,会发现执行结果并不一致。
那如何把非线程安全的集合转换成线程安全的呢?
以ArrayList为例,使用Collections工具类中的synchronizedList,可以把ArrayList转换为线程安全的List。
源码:
public static <T> List<T> synchronizedList(List<T> list) ;
使用:Collections.synchronizedList(list);
改造上述代码,变成线程安全,只需加入如下代码,然后将多线程中操作的集合换成转换后的集合即可:
//将List转换成线程安全的类List<Integer> threadSafeList = Collections.synchronizedList(nonThreadSafeList);
最终的执行结果如下,执行多次,结果一致。

与此类似的,还有HashSet,LinkedList,HashMap等等非线程安全的类,具体类型如下,都可以通过Collections工具类转换为线程安全的

1.5 总结
在多线程中,线程安全问题是不允许被出现的。所以我们在使用多线程时,对于共享数据,可以通过synchronized关键字和Lock锁来处理,保证线程安全。 synchronized使用简单但灵活性较差;而Lock是一个更灵活的同步方式,可以实现更复杂的同步需求,但需要手动管理锁的获取和释放。在实际开发中,可以根据具体需求进行选择。
相关文章:
【JAVA语言-第20话】多线程详细解析(二)——线程安全,非线程安全的集合转换成线程安全
目录 线程安全 1.1 概述 1.2 案例分析 1.3 解决线程安全问题 1.3.1 使用synchronized关键字 1.3.1.1 同步代码块 1.3.1.2 同步方法 1.3.2 使用Lock锁 1.3.2.1 概述 代码示例: 1.4 线程安全的类 1.4.1 非线程安全集合转换成线程安全集合 1.5 总结 …...
区块链中的加密算法及其作用
区块链技术以其去中心化、不可篡改、透明公开的特性,在全球范围内引发了广泛的关注和讨论。其中,加密算法作为区块链技术的核心组成部分,对于维护区块链网络的安全、确保数据的完整性和真实性起到了至关重要的作用。本文将详细介绍区块链中常…...
微信小程序跳转微信管理平台配置的客服及意见页面
<button open-type"contact" bindcontact"handleContact" session-from"sessionFrom">帮助与客服</button> 不需要路径 在当前小程序中会自动进入 open-type"contact" 其他参数不用修改 只修改这个参数对应表单组件 /…...
灌溉机器人 状压dp
灌溉机器人 题目描述 农田灌溉是一项十分费体力的农活,特别是大型的农田。小明想为农民伯伯们减轻农作负担,最近在研究一款高科技——灌溉机器人。它可以在远程电脑控制下,给农田里的作物进行灌溉。 现在有一片 N 行 M 列的农田。农田的土…...
用于接收参数的几个注解
了解四种主要请求方法的传参格式 GET方法: 参数通常通过URL的查询字符串(query string)传递,形式为key1value1&key2value2。示例:http://example.com/api/resource?key1value1&key2value2 POST方法…...
Flask-Login 实现用户认证
Flask-Login 实现用户认证 Flask-Login 是什么 Flask-Login 是 Flask 中的一个第三方库,用于处理用户认证和管理用户会话,它提供了一组工具和功能,使得在 Flask 应用程序中实现用户认证变得更加简单和方便。 如何使用 Flask-Login 1.安装…...
基于WPF的DynamicDataDisplay曲线显示
一、DynamicDataDisplay下载和引用 1.新建项目,下载DynamicDataDisplay引用: 如下图: 二、前端开发: <Border Grid.Row"0" Grid.Column"2" BorderBrush"Purple" BorderThickness"1"…...
股票问题(至多两次购买
class Solution {public int maxProfit(int[] prices) {int[] dpnew int[4];dp[0]-prices[0];//第一次持有dp[1]0;dp[2]-prices[0];//第二次持有dp[3]0;for(int i1;i<prices.length;i){dp[0]Math.max(dp[0],-prices[i]);dp[1]Math.max(dp[1],dp[0]prices[i]);dp[2]Math.max(…...
车辆运动模型中LQR代码实现
一、前言 最近看到关于架构和算法两者关系的一个描述,我觉得非常认同,分享给大家。 1、好架构起到两个作用:合理的分解功能、合理的适配算法; 2、好的架构是好的功能的必要条件,不是充分条件,一味追求架构…...
Springboot集成feign远程调用
需求:在leadnews-wemedia微服务里需要调用leadnews-article微服务的接口。新建一个支持feign调用的名为heima-leadnews-feign-api的模块 heima-leadnews-feign-api的pom文件里导入openfeign依赖 <dependency><groupId>org.springframework.cloud</g…...
构建NFS远程共享存储
nfs-server:10.1.59.237 nfs-web:10..159.218 centos7,服务端和客户端都关闭防火墙和selinux内核防火墙,如果公司要求开启防火墙,那需要放行几个端口 firewall-cmd --add-port2049/tcp --permanent firewall-cmd --add-port111/tcp --permanent firew…...
X9C103SIZT1 数字电位计 IC 10K SOIC-8 参数 应用案例
X9C103SIZT1 是一款数字电位器,属于 X9C103 系列。它是一款100抽头的非易失性数字电位器,阻值为 10 kOhm,封装形式为 SOIC-8。这款器件常用于需要调整电子设备阻值的应用中,如音频设备、电源管理以及传感器校准等。 X9C103SIZT1 的…...
redis深入理解之数据存储
1、redis为什么快 1)Redis是单线程执行,在执行时顺序执行 redis单线程主要是指Redis的网络IO和键值对读写是由一个线程来完成的,Redis在处理客户端的请求时包括获取(socket 读)、解析、执行、内容返回 (socket 写)等都由一个顺序串行的主线…...
用20行python写一个最简单的网站
先安装flask框架,cmd命令行 pip install flask,或pycharm -> setting -> project -> python interpreter 搜索安装 # 引入Flask框架 from flask import Flask# 实例化Flask应用 app Flask(__name__)# 定义一个路由,当用户访问网站…...
零基础入门篇①③ Python可变序列类型--列表
Python从入门到精通系列专栏面向零基础以及需要进阶的读者倾心打造,9.9元订阅即可享受付费专栏权益,一个专栏带你吃透Python,专栏分为零基础入门篇、模块篇、网络爬虫篇、Web开发篇、办公自动化篇、数据分析篇…学习不断,持续更新,火热订阅中🔥专栏限时一个月(5.8~6.8)重…...
微服务项目 - SpringBoot 2.x 升级到 SpringBoot 3.2.5,保姆级避坑
目录 一、前言 二、取经之路 2.1、依赖版本情况 2.2、MyBatis-Plus 依赖改变...
【2024亚马逊云科技峰会】Amazon Bedrock + Llama3 生成式AI实践
在 4 月 18 日,Meta在官网上公布了旗下最新大模型Llama 3。目前,Llama 3已经开放了80亿(8B)和700亿(70B)两个小参数版本,上下文窗口为8k,据称,通过使用更高质量的训练数据…...
ApacheCordova 12 +Vs 2022 项目搭建教程_开发环境搭建教程
一、安装 cordova cli 并使用命令创建项目 npm install –g cordova 详细参考: Apache Cordova开发环境搭建(二)VS Code_天马3798-CSDN博客_cordova vscode 二、 Vs 2022 Android 开发搭建+调试 .Net MAUI 搭建Android 开发环境-CSDN博客 三、配置 JDK 环境变量、配置…...
地磁暴红色预警来袭,普通人该如何应对?绝绝子的防护指南来了
近日,国家空间天气监测预警中心发布了一则令人瞩目的消息——地磁暴红色预警。这一预警不仅提醒我们地磁暴即将影响我国的电离层和低轨卫星,更让我们深刻认识到地球空间环境的脆弱性和复杂性。对于普通公众而言,地磁暴的概念可能相对陌生&…...
从零自制docker-12-【overlayfs】
文章目录 overlayfsexec.Command("tar", "-xvf", busyboxTarURL, "-C", busyboxURL).CombinedOutput()exec.Command格式差异 挂载mount卸载unmount代码地址结果演示 overlayfs 就是联合文件系统,将多个文件联合在一起成为一个统一的…...
K8S认证|CKS题库+答案| 11. AppArmor
目录 11. AppArmor 免费获取并激活 CKA_v1.31_模拟系统 题目 开始操作: 1)、切换集群 2)、切换节点 3)、切换到 apparmor 的目录 4)、执行 apparmor 策略模块 5)、修改 pod 文件 6)、…...
智慧工地云平台源码,基于微服务架构+Java+Spring Cloud +UniApp +MySql
智慧工地管理云平台系统,智慧工地全套源码,java版智慧工地源码,支持PC端、大屏端、移动端。 智慧工地聚焦建筑行业的市场需求,提供“平台网络终端”的整体解决方案,提供劳务管理、视频管理、智能监测、绿色施工、安全管…...
UE5 学习系列(三)创建和移动物体
这篇博客是该系列的第三篇,是在之前两篇博客的基础上展开,主要介绍如何在操作界面中创建和拖动物体,这篇博客跟随的视频链接如下: B 站视频:s03-创建和移动物体 如果你不打算开之前的博客并且对UE5 比较熟的话按照以…...
学校招生小程序源码介绍
基于ThinkPHPFastAdminUniApp开发的学校招生小程序源码,专为学校招生场景量身打造,功能实用且操作便捷。 从技术架构来看,ThinkPHP提供稳定可靠的后台服务,FastAdmin加速开发流程,UniApp则保障小程序在多端有良好的兼…...
在四层代理中还原真实客户端ngx_stream_realip_module
一、模块原理与价值 PROXY Protocol 回溯 第三方负载均衡(如 HAProxy、AWS NLB、阿里 SLB)发起上游连接时,将真实客户端 IP/Port 写入 PROXY Protocol v1/v2 头。Stream 层接收到头部后,ngx_stream_realip_module 从中提取原始信息…...
Frozen-Flask :将 Flask 应用“冻结”为静态文件
Frozen-Flask 是一个用于将 Flask 应用“冻结”为静态文件的 Python 扩展。它的核心用途是:将一个 Flask Web 应用生成成纯静态 HTML 文件,从而可以部署到静态网站托管服务上,如 GitHub Pages、Netlify 或任何支持静态文件的网站服务器。 &am…...
【Go】3、Go语言进阶与依赖管理
前言 本系列文章参考自稀土掘金上的 【字节内部课】公开课,做自我学习总结整理。 Go语言并发编程 Go语言原生支持并发编程,它的核心机制是 Goroutine 协程、Channel 通道,并基于CSP(Communicating Sequential Processes࿰…...
聊一聊接口测试的意义有哪些?
目录 一、隔离性 & 早期测试 二、保障系统集成质量 三、验证业务逻辑的核心层 四、提升测试效率与覆盖度 五、系统稳定性的守护者 六、驱动团队协作与契约管理 七、性能与扩展性的前置评估 八、持续交付的核心支撑 接口测试的意义可以从四个维度展开,首…...
RNN避坑指南:从数学推导到LSTM/GRU工业级部署实战流程
本文较长,建议点赞收藏,以免遗失。更多AI大模型应用开发学习视频及资料,尽在聚客AI学院。 本文全面剖析RNN核心原理,深入讲解梯度消失/爆炸问题,并通过LSTM/GRU结构实现解决方案,提供时间序列预测和文本生成…...
排序算法总结(C++)
目录 一、稳定性二、排序算法选择、冒泡、插入排序归并排序随机快速排序堆排序基数排序计数排序 三、总结 一、稳定性 排序算法的稳定性是指:同样大小的样本 **(同样大小的数据)**在排序之后不会改变原始的相对次序。 稳定性对基础类型对象…...
