【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 就是联合文件系统,将多个文件联合在一起成为一个统一的…...
vue3 字体颜色设置的多种方式
在Vue 3中设置字体颜色可以通过多种方式实现,这取决于你是想在组件内部直接设置,还是在CSS/SCSS/LESS等样式文件中定义。以下是几种常见的方法: 1. 内联样式 你可以直接在模板中使用style绑定来设置字体颜色。 <template><div :s…...
ffmpeg(四):滤镜命令
FFmpeg 的滤镜命令是用于音视频处理中的强大工具,可以完成剪裁、缩放、加水印、调色、合成、旋转、模糊、叠加字幕等复杂的操作。其核心语法格式一般如下: ffmpeg -i input.mp4 -vf "滤镜参数" output.mp4或者带音频滤镜: ffmpeg…...
Swagger和OpenApi的前世今生
Swagger与OpenAPI的关系演进是API标准化进程中的重要篇章,二者共同塑造了现代RESTful API的开发范式。 本期就扒一扒其技术演进的关键节点与核心逻辑: 🔄 一、起源与初创期:Swagger的诞生(2010-2014) 核心…...
GitHub 趋势日报 (2025年06月06日)
📊 由 TrendForge 系统生成 | 🌐 https://trendforge.devlive.org/ 🌐 本日报中的项目描述已自动翻译为中文 📈 今日获星趋势图 今日获星趋势图 590 cognee 551 onlook 399 project-based-learning 348 build-your-own-x 320 ne…...
日常一水C
多态 言简意赅:就是一个对象面对同一事件时做出的不同反应 而之前的继承中说过,当子类和父类的函数名相同时,会隐藏父类的同名函数转而调用子类的同名函数,如果要调用父类的同名函数,那么就需要对父类进行引用&#…...
【SpringBoot自动化部署】
SpringBoot自动化部署方法 使用Jenkins进行持续集成与部署 Jenkins是最常用的自动化部署工具之一,能够实现代码拉取、构建、测试和部署的全流程自动化。 配置Jenkins任务时,需要添加Git仓库地址和凭证,设置构建触发器(如GitHub…...
Matlab实现任意伪彩色图像可视化显示
Matlab实现任意伪彩色图像可视化显示 1、灰度原始图像2、RGB彩色原始图像 在科研研究中,如何展示好看的实验结果图像非常重要!!! 1、灰度原始图像 灰度图像每个像素点只有一个数值,代表该点的亮度(或…...
用js实现常见排序算法
以下是几种常见排序算法的 JS实现,包括选择排序、冒泡排序、插入排序、快速排序和归并排序,以及每种算法的特点和复杂度分析 1. 选择排序(Selection Sort) 核心思想:每次从未排序部分选择最小元素,与未排…...
OpenGL-什么是软OpenGL/软渲染/软光栅?
软OpenGL(Software OpenGL)或者软渲染指完全通过CPU模拟实现的OpenGL渲染方式(包括几何处理、光栅化、着色等),不依赖GPU硬件加速。这种模式通常性能较低,但兼容性极强,常用于不支持硬件加速…...
Ray框架:分布式AI训练与调参实践
Ray框架:分布式AI训练与调参实践 系统化学习人工智能网站(收藏):https://www.captainbed.cn/flu 文章目录 Ray框架:分布式AI训练与调参实践摘要引言框架架构解析1. 核心组件设计2. 关键技术实现2.1 动态资源调度2.2 …...
