【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 就是联合文件系统,将多个文件联合在一起成为一个统一的…...

docker详细操作--未完待续
docker介绍 docker官网: Docker:加速容器应用程序开发 harbor官网:Harbor - Harbor 中文 使用docker加速器: Docker镜像极速下载服务 - 毫秒镜像 是什么 Docker 是一种开源的容器化平台,用于将应用程序及其依赖项(如库、运行时环…...

MODBUS TCP转CANopen 技术赋能高效协同作业
在现代工业自动化领域,MODBUS TCP和CANopen两种通讯协议因其稳定性和高效性被广泛应用于各种设备和系统中。而随着科技的不断进步,这两种通讯协议也正在被逐步融合,形成了一种新型的通讯方式——开疆智能MODBUS TCP转CANopen网关KJ-TCPC-CANP…...

新能源汽车智慧充电桩管理方案:新能源充电桩散热问题及消防安全监管方案
随着新能源汽车的快速普及,充电桩作为核心配套设施,其安全性与可靠性备受关注。然而,在高温、高负荷运行环境下,充电桩的散热问题与消防安全隐患日益凸显,成为制约行业发展的关键瓶颈。 如何通过智慧化管理手段优化散…...
JAVA后端开发——多租户
数据隔离是多租户系统中的核心概念,确保一个租户(在这个系统中可能是一个公司或一个独立的客户)的数据对其他租户是不可见的。在 RuoYi 框架(您当前项目所使用的基础框架)中,这通常是通过在数据表中增加一个…...

CVE-2020-17519源码分析与漏洞复现(Flink 任意文件读取)
漏洞概览 漏洞名称:Apache Flink REST API 任意文件读取漏洞CVE编号:CVE-2020-17519CVSS评分:7.5影响版本:Apache Flink 1.11.0、1.11.1、1.11.2修复版本:≥ 1.11.3 或 ≥ 1.12.0漏洞类型:路径遍历&#x…...

推荐 github 项目:GeminiImageApp(图片生成方向,可以做一定的素材)
推荐 github 项目:GeminiImageApp(图片生成方向,可以做一定的素材) 这个项目能干嘛? 使用 gemini 2.0 的 api 和 google 其他的 api 来做衍生处理 简化和优化了文生图和图生图的行为(我的最主要) 并且有一些目标检测和切割(我用不到) 视频和 imagefx 因为没 a…...
纯 Java 项目(非 SpringBoot)集成 Mybatis-Plus 和 Mybatis-Plus-Join
纯 Java 项目(非 SpringBoot)集成 Mybatis-Plus 和 Mybatis-Plus-Join 1、依赖1.1、依赖版本1.2、pom.xml 2、代码2.1、SqlSession 构造器2.2、MybatisPlus代码生成器2.3、获取 config.yml 配置2.3.1、config.yml2.3.2、项目配置类 2.4、ftl 模板2.4.1、…...

【C++进阶篇】智能指针
C内存管理终极指南:智能指针从入门到源码剖析 一. 智能指针1.1 auto_ptr1.2 unique_ptr1.3 shared_ptr1.4 make_shared 二. 原理三. shared_ptr循环引用问题三. 线程安全问题四. 内存泄漏4.1 什么是内存泄漏4.2 危害4.3 避免内存泄漏 五. 最后 一. 智能指针 智能指…...
C#学习第29天:表达式树(Expression Trees)
目录 什么是表达式树? 核心概念 1.表达式树的构建 2. 表达式树与Lambda表达式 3.解析和访问表达式树 4.动态条件查询 表达式树的优势 1.动态构建查询 2.LINQ 提供程序支持: 3.性能优化 4.元数据处理 5.代码转换和重写 适用场景 代码复杂性…...
学习一下用鸿蒙DevEco Studio HarmonyOS5实现百度地图
在鸿蒙(HarmonyOS5)中集成百度地图,可以通过以下步骤和技术方案实现。结合鸿蒙的分布式能力和百度地图的API,可以构建跨设备的定位、导航和地图展示功能。 1. 鸿蒙环境准备 开发工具:下载安装 De…...