【JavaEE】线程安全性问题,线程不安全是怎么产生的,该如何应对
产生线程不安全的原因
在Java多线程编程中,线程不安全通常是由于多个线程同时访问共享资源而引发的竞争条件。以下是一些导致线程不安全的常见原因:
-
共享可变状态:当多个线程对共享的可变数据进行读写时,如果没有适当的同步机制,可能导致数据的不一致性。例如,两个线程同时修改一个共享变量,最终的结果可能取决于线程的执行顺序。
-
缺乏同步:在没有使用
synchronized
关键字或其他同步机制(如Lock
)进行保护的情况下,多个线程可以同时进入临界区,从而导致线程安全问题。 -
指令重排序:为了提高执行效率,Java虚拟机和处理器可能会对指令进行重排序,这种行为在多线程环境中可能导致不可预期的结果,尤其是在多个线程依赖某些变量的状态时。
-
原子性问题:某些操作在Java中并不是原子的,例如对对象属性的读-改-写操作。在多线程环境下,这类操作必须通过同步处理以确保原子性。
-
死锁:尽管死锁本身不直接导致线程不安全,但在复杂的同步情况下,死锁可能导致某些线程无法继续执行,从而影响整体程序的正确性与稳定性。
-
不可见性:当一个线程对共享变量的修改在其他线程中不可见时,可能导致一些线程读取到过时的值。这通常可以通过使用
volatile
关键字来解决。
产生线程不安全的案例以及应对方法
共享可变状态案例
我们将创建一个简单的银行账户类,多个线程并发访问该账户进行存款和取款操作。假设我们有两个线程同时对账户进行操作,可能会出现余额计算错误的情况。
class BankAccount {private int balance = 100; // 初始余额为100public void deposit(int amount) {balance += amount; // 存款}public void withdraw(int amount) {balance -= amount; // 取款}public int getBalance() {return balance; // 返回当前余额}
}public class UnsafeBank {public static void main(String[] args) {BankAccount account = new BankAccount();// 创建两个线程同时操作Thread t1 = new Thread(() -> {account.withdraw(50);System.out.println("Thread 1 withdrew 50, balance: " + account.getBalance());});Thread t2 = new Thread(() -> {account.deposit(30);System.out.println("Thread 2 deposited 30, balance: " + account.getBalance());});t1.start();t2.start();}
}
运行情况:
我们期望的运行结果是:取款50,余额50、存款30,余额80。但是上述结果并不是我们想要的
分析
在上述代码中,两个线程同时对balance
变量进行操作,可能导致不一致的余额输出。例如,假设Thread 1
先读取了余额为100,然后进行了取款操作,但在它更新余额之前,Thread 2
可能已经读取了余额并进行了存款操作。最终的结果可能不符合预期。
解决方法
为了解决这个线程不安全的问题,我们可以使用synchronized
关键字来确保对共享资源的访问是线程安全的。我们可以对deposit
和withdraw
方法加锁,使得同一时间只有一个线程能够执行其中一个方法。
以下是修改后的代码:
class BankAccount {private int balance = 100; // 初始余额为100// 存款操作public synchronized void deposit(int amount) {balance += amount; // 存款}// 取款操作public synchronized void withdraw(int amount) {balance -= amount; // 取款}// 返回当前余额public int getBalance() {return balance; // 返回当前余额}
}public class SafeBank {public static void main(String[] args) throws InterruptedException {BankAccount account = new BankAccount();// 创建两个线程同时操作Thread t1 = new Thread(() -> {account.withdraw(50);System.out.println("Thread 1 withdrew 50, balance: " + account.getBalance());});Thread t2 = new Thread(() -> {account.deposit(30);System.out.println("Thread 2 deposited 30, balance: " + account.getBalance());});t1.start();t2.start();// 等待两个线程结束t1.join();t2.join();// 输出最终余额System.out.println("Final balance: " + account.getBalance());}
}
结果
在修改后的代码中,由于对deposit
和withdraw
方法加了synchronized
修饰,确保任何时刻只有一个线程可以执行这两个方法,从而避免了由于竞争条件导致的不一致性。最终输出的余额将与预期结果相一致。
指令重排序案例
指令重排序是指在编译、优化或CPU执行过程中,代码的执行顺序被改变。
count++
操作并不是一个原子操作,它是由三个步骤组成的:
- 读取当前的值。
- 对值加1。
- 将新值写回。
在多线程环境中,多个线程可能会同时对同一变量进行 count++
操作,导致结果不正确。这种情况下,指令重排序可能导致某些操作无法达到预期结果。
以下是一个示例代码,演示了这个问题:
class Counter {private int count = 0;public void increment() {count++; // 不安全的操作}public int getCount() {return count;}
}public class CountExample {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread[] threads = new Thread[10];// 创建10个线程for (int i = 0; i < 10; i++) {threads[i] = new Thread(() -> {for (int j = 0; j < 1000; j++) {counter.increment(); // 增加计数}});}// 启动所有线程for (Thread thread : threads) {thread.start();}// 等待所有线程结束for (Thread thread : threads) {thread.join();}// 输出最终计数System.out.println("Final count: " + counter.getCount());}
}
运行结果:
我们的预期结果是:10000
分析
在上述代码中,我们创建了10个线程,每个线程执行1000次 increment()
方法,从而期望最终的计数是10000。然而,由于 count++
操作的非原子性,在多个线程并发执行时,可能会导致某些增量操作丢失,最终结果可能小于10000。
解决方法
为了解决这个问题,可以使用以下几种方法:
-
使用
synchronized
关键字:将increment
方法同步,以确保同一时刻只有一个线程能执行该操作。 -
**使用
AtomicInteger
**:Java提供了原子类AtomicInteger
,能够保证对整数操作的原子性。
我们将采用第二种方法,即使用 AtomicInteger
来解决这个问题。
以下是修改后的代码:
import java.util.concurrent.atomic.AtomicInteger;class Counter {private AtomicInteger count = new AtomicInteger(0); // 使用AtomicIntegerpublic void increment() {count.incrementAndGet(); // 原子性增加}public int getCount() {return count.get(); // 获取当前值}
}public class SafeCountExample {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread[] threads = new Thread[10];// 创建10个线程for (int i = 0; i < 10; i++) {threads[i] = new Thread(() -> {for (int j = 0; j < 1000; j++) {counter.increment(); // 增加计数}});}// 启动所有线程for (Thread thread : threads) {thread.start();}// 等待所有线程结束for (Thread thread : threads) {thread.join();}// 输出最终计数System.out.println("Final count: " + counter.getCount());}
}
不可见性案例
我们使用了两个线程 t1
和 t2
。线程 t1
负责不停地检查一个共享变量 fag
,而线程 t2
则在休眠1秒后将 fag
设为1。
public class Main {public static int fag = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {while (fag == 0) {}});Thread t2 = new Thread(() -> {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}fag = 1;});t1.start();t2.start();t1.join();t2.join();System.out.println("主线程结束");}
}
分析
在Java中,fag
是一个共享的静态变量,初始值为0。线程 t1
在一个循环中不断检查 fag
的值,而线程 t2
在休眠1秒后将 fag
更新为1。根据Java内存模型的规定,线程可以在运行过程中缓存某些变量,以提高性能。这意味着,线程 t1
可能在自己的工作内存中读取到fag
的值,并且不会每次都去主内存中检查当其值变化时。
因此,虽然 t2
可能已经将 fag
设置为1,但如果 t1
线程没有看到这个变化,它仍然可能会在其循环中继续查看到 fag
为0,导致 t1
线程陷入死循环,程序执行不会继续下去。
解决方法
为了解决这个线程不可见性的问题,可以使用以下两种常见方法:
- 使用
volatile
关键字:将fag
声明为volatile
,这样可以确保任何线程对fag
的写入都会立即对其他线程可见。 - 使用同步机制:使用
synchronized
关键字来确保对fag
的读取和写入操作是安全的。
在这里,我们选择使用 volatile
关键字来解决这个问题。
public class Main {public static volatile int fag = 0; // 使用volatile关键字public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {while (fag == 0) {// Busy wait: 这里循环等待fag变为1}});Thread t2 = new Thread(() -> {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}fag = 1; // 将fag设置为1});t1.start();t2.start();t1.join();t2.join();System.out.println("主线程结束");}
}
结果
通过将 fag
声明为 volatile
,确保了对该变量的写入会使得线程 t1
线程能看到 fag
的最新值。即使线程 t2
在将 fag
改为1后,其他线程(如 t1
)也能及时看到这一变化,而不会出现不可见性的问题,从而避免了 t1
进入死循环的情况。
在程序运行结束后,您将看到"主线程结束"的输出,表明所有线程都能正常结束。使用 volatile
关键字有效地解决了线程间的可见性问题。
死锁
在多线程编程中,死锁是一种非常严重的问题,它会导致程序无法继续执行。产生死锁的典型条件通常可以归纳为以下四个必要条件:
-
互斥条件:至少有一个资源必须被一个线程持有,并且在该资源被其他线程请求时,该线程不能被剥夺,即资源只能被一个线程使用。
-
保持并等待条件:一个线程至少持有一个资源,并且正在等待获取其他资源。在这个状态下,线程不会释放它已持有的资源。
-
不剥夺条件:一旦资源被分配给某个线程,其他线程不能强制剥夺该资源,只有线程在完成其任务后才能释放它所持有的资源。
-
循环等待条件:存在一个线程集合 {T1, T2, ..., Tn},其中 T1 等待 T2 持有的资源,T2 等待 T3 持有的资源,以此类推,直至 Tn 等待 T1 持有的资源。形成一种循环等待的关系。
死锁案例
假设有两个线程,线程A和线程B,它们分别需要获取两个锁,锁1和锁2。以下是代码示例:
class Lock {private final String name;public Lock(String name) {this.name = name;}public String getName() {return name;}
}public class DeadlockExample {private static final Lock lock1 = new Lock("Lock1");private static final Lock lock2 = new Lock("Lock2");public static void main(String[] args) {Thread threadA = new Thread(() -> {synchronized (lock1) {System.out.println("Thread A: Holding lock 1...");// Simulate some worktry { Thread.sleep(100); } catch (InterruptedException e) {}System.out.println("Thread A: Waiting for lock 2...");synchronized (lock2) {System.out.println("Thread A: Acquired lock 2!");}}});Thread threadB = new Thread(() -> {synchronized (lock2) {System.out.println("Thread B: Holding lock 2...");// Simulate some worktry { Thread.sleep(100); } catch (InterruptedException e) {}System.out.println("Thread B: Waiting for lock 1...");synchronized (lock1) {System.out.println("Thread B: Acquired lock 1!");}}});threadA.start();threadB.start();}
}
分析
在上面的代码中,线程A首先持有锁1,然后尝试去获取锁2。同时,线程B首先持有锁2,之后尝试获取锁1。这样就形成了循环等待,导致两个线程相互阻塞,从而发生死锁。
解决方法
为了避免这种死锁情况,可以使用以下解决方案:
- 按照固定顺序获取锁: 我们可以定义一个顺序,确保所有线程都按照相同的顺序获取锁,从而避免循环等待。
public class DeadlockPrevention {private static final Lock lock1 = new Lock("Lock1");private static final Lock lock2 = new Lock("Lock2");public static void main(String[] args) {Thread threadA = new Thread(() -> {Lock firstLock = lock1;Lock secondLock = lock2;acquireLocks(firstLock, secondLock);});Thread threadB = new Thread(() -> {Lock firstLock = lock1;Lock secondLock = lock2;acquireLocks(firstLock, secondLock);});threadA.start();threadB.start();}private static void acquireLocks(Lock firstLock, Lock secondLock) {synchronized (firstLock) {System.out.println(Thread.currentThread().getName() + ": Holding " + firstLock.getName() + "...");// Simulate some worktry { Thread.sleep(100); } catch (InterruptedException e) {}synchronized (secondLock) {System.out.println(Thread.currentThread().getName() + ": Acquired " + secondLock.getName() + "!");}}}
}
在这个示例中,无论线程A还是线程B,都会按照同样的顺序(首先获取lock1
,然后是lock2
)来请求锁,由此避免了死锁情况的发生。
通过这些方法,可以有效减少多线程程序中的死锁风险,保证程序的稳定性。
为了有效避免死锁,可以考虑以下策略:
-
资源有序分配:为所有资源定义一个全局的获取顺序,线程在请求资源时,按照这个顺序获取,从而避免循环等待的情况。
-
使用超时机制:在尝试获取锁时,可以设定一个超时时间,若超时则放弃锁的请求,减少潜在的死锁情况。
-
避免保持并等待:可以在开始线程时一次性请求所有所需资源,成功则继续执行,失败则释放所有已获得的资源。
-
检测与恢复:定期检查系统中是否存在死锁,如果发现可以中断某些线程或者释放某些资源来解除死锁。
通过合理的设计与计划,可以有效减少死锁的可能性,提高系统的稳定性和可靠性。
相关文章:

【JavaEE】线程安全性问题,线程不安全是怎么产生的,该如何应对
产生线程不安全的原因 在Java多线程编程中,线程不安全通常是由于多个线程同时访问共享资源而引发的竞争条件。以下是一些导致线程不安全的常见原因: 共享可变状态:当多个线程对共享的可变数据进行读写时,如果没有适当的同步机制&…...

低代码-赋能新能源汽车产业加速前行
在“双碳”战略目标的引领下,全球新能源汽车产业正经历着前所未有的发展和变革,新能源汽车整车制造成为绿色低碳转型的重要领域。在政府的大力扶持下,新能源整车制造领域蓬勃发展,已成为全球汽车产业不可逆转的重要趋势。新能源汽…...

基于UDP的简易网络通信程序
目录 0.前言 1.前置知识 网络通信的大致流程 IP地址 端口号(port) 客户端如何得知服务器端的IP地址和端口号? 服务器端如何得知客户端的IP地址和端口号? 2.实现代码 代码模块的设计 服务器端代码 成员说明 成员实现 U…...

AI大模型在知识管理平台上的应用:泛微·采知连实现自动采集.精准搜索.智能问答.主动推荐
AI技术的发展,正在推动组织知识管理模式发生变革。知识管理系统通过各种应用实现知识体系落地,当前聚焦于整合生成式AI技术,以提升业务效率。 组织在数字化进程中面临着知识增量增多、知识更新频率变快、知识与业务结合更紧密等挑战ÿ…...

JavaEE:文件内容操作(一)
文章目录 文件内容的读写---数据流字节流和字符流打开和关闭文件文件资源泄漏try with resources 文件内容的读写—数据流 文件内容的操作,读文件和写文件,都是操作系统本身提供了API,在Java中也进行了封装. Java中封装了操作文件的这些类,我们给它们起了个名字,叫做"文…...

无人机视角下落水救援检测数据集
无人机视角下落水救援检测数据集,利用无人机快速搜索落水者对增加受害者的生存机会至关重要,该数据集共收集12万帧视频图像,涵盖无人机高度从10m-60m高度,检测包括落水者(11万标注量)、流木(900…...

openssl+keepalived安装部署
文章目录 OpenSSL安装下载地址编译安装修改系统配置版本 Keepalived安装下载地址安装遇到问题安装完成配置文件 keepalived运行检查运行状态查看系统日志修改服务service重新加载systemd检查配置文件语法错误 OpenSSL安装 下载地址 考虑到后面设备可能没法连接到外网&…...

float存储原理
float存储原理基于IEEE 754标准,主要包括符号位、指数位和有效数字位三部分。以下是对其存储原理的具体介绍: 符号位:符号位是浮点数中用于表示正负的位。在单精度浮点数(32位)中,最左边的第1位是符号位&a…...

DAY 9 - 10 : 树
树的概念 定义 树(Tree)是n(n≥0)个节点的有限集合T,它满足两个条件 : 1.有且仅有一个特定的称为根(Root)的节点。 2.其余的节点可以分为m(m≥0)个互不相交的…...

【python计算机视觉编程——9.图像分割】
python计算机视觉编程——9.图像分割 9.图像分割9.1 图割安装Graphviz下一步:正文9.1.1 从图像创建图9.1.2 用户交互式分割 9.2 利用聚类进行分割9.3 变分法 9.图像分割 9.1 图割 可以选择不装Graphviz,因为原本觉得是要用,后面发现好像用不…...

北斗赋能万物互联:新质生产力的强劲驱动力
在数字化转型的大潮中,中国自主研制的北斗卫星导航系统,作为国家重大空间基础设施,正以前所未有的力量推动着万物互联时代的到来,成为新质生产力发展的重要基石。本文将深入剖析北斗系统如何以其独特的技术优势和广泛应用场景&…...

时序预测 | Matlab实现GA-CNN遗传算法优化卷积神经网络时间序列预测
时序预测 | Matlab实现GA-CNN遗传算法优化卷积神经网络时间序列预测 目录 时序预测 | Matlab实现GA-CNN遗传算法优化卷积神经网络时间序列预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 时序预测 | Matlab实现GA-CNN遗传算法优化卷积神经网络时间序列预测ÿ…...

如何保证消息不重复消费
在使用消息队列(Message Queue, MQ)时,确保消息不被重复消费是非常重要的,因为重复消费可能导致数据不一致或者业务逻辑出错。要保证消息不被重复消费,可以采取以下几种策略: 1. 消息确认机制 大多数消息…...

HTTP请求工具类
HTTP请求工具类 import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL;public class HttpUtils {/*** 发送GET请求并获取响应结果* * param url 请求的URL* return 响应结果…...

谷歌的 DataGemma 人工智能是一个统计精灵
谷歌正在扩大其人工智能模型家族,同时解决该领域的一些最大问题。 今天,该公司首次发布了 DataGemma,这是一对开源的、经过指令调整的模型,在缓解幻觉挑战方面迈出了一步,幻觉是指大型语言模型(LLM…...

【Python爬虫系列】_021.异步请求aiohttp
课 程 推 荐我 的 个 人 主 页:👉👉 失心疯的个人主页 👈👈入 门 教 程 推 荐 :👉👉 Python零基础入门教程合集 👈👈虚 拟 环 境 搭 建 :👉👉 Python项目虚拟环境(超详细讲解) 👈👈PyQt5 系 列 教 程:👉👉 Python GUI(PyQt5)文章合集 👈👈...

源码运行springboot2.2.9.RELEASE
1 环境要求 java 8 maven 3.5.2 2 下载springboot源码 下载地址 https://github.com/spring-projects/spring-boot/releases/tag/v2.2.9.RELEASE 3 修改配置 修改spring-boot-2.2.9.RELEASE/pom.xml 修改spring-boot-2.2.9.RELEASE/spring-boot-project/spring-boot-tools…...

王者荣耀改重复名(java源码)
王者荣耀改重复名 项目简介 “王者荣耀改重复名”是一个基于 Spring Boot 的应用程序,用于生成王者荣耀游戏中的唯一名称。通过简单的接口和前端页面,用户可以输入旧名称并获得一个新的、不重复的名称。 功能特点 生成新名称:提供一个接口…...

Python 全栈系列271 微服务踩坑记
说明 这个坑花了10个小时才爬出来 碰到一个现象:将微服务改造为并发后,请求最初很快,然后就出现大量的失败,然后过一会又能用。 过去从来没有碰到这个问题,要么是一些比较明显的资源,或者逻辑bug࿰…...

环境搭建2(游戏逆向)
#include<iostream> #include<windows.h> #include<tchar.h> #include<stdio.h> #pragma warning(disable:4996) //exe应用程序 VOID PrintUI(CONST CHAR* ExeName, CONST CHAR* UIName, CONST CHAR* color, SHORT X坐标, SHORT y坐标, WORD UIwide, W…...

快手自研Spark向量化引擎正式发布,性能提升200%
Blaze 是快手自研的基于Rust语言和DataFusion框架开发的Spark向量化执行引擎,旨在通过本机矢量化执行技术来加速Spark SQL的查询处理。Blaze在快手内部上线的数仓生产作业也观测到了平均30%的算力提升,实现了较大的降本增效。本文将深入剖析blaze的技术原…...

用网卡的ap模式抓嵌入式设备的网络包
嵌入式设备不像pc上,有一些专门的工具比如wareshark来抓包,嵌入式设备中,有的可能集成了tcpdump,可以用来进行简单的抓包,但是不方便分析,况且有的嵌入式设备不一定就集成了tcpdump工具。 关于tcpdump工具…...

centos 7 升级Docker 与Docker-Compose 到最新版本
一 升级docker 可参考docker官方升级 1, 查看docker 信息 docker info 2,查看docker 版本 docker --version 3 升级前 可停止docker : sudo systemctl stop docker 4 查看已安装的docker 并卸载 [rootlocalhost docker]# yum list installed | grep docker docker.x86…...

Docker_启动redis,容易一启动就停掉
现象以及排查过程 最近在使用docker来搭建redis服务,但是在启动redis哨兵容器时,总是发现这个容器启动后立马就停止了。首先想到的是不是服务器资源不够用了导致的这个现象,排查后发现不是资源问题。再者猜测是不是启动报错了,查看…...

微服务中间件之Nacos
Nacos(Dynamic Naming and Configuration Service)是阿里巴巴开源的一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。它提供了服务注册与发现、配置管理以及服务健康监测等核心功能,旨在帮助开发人员更轻松地构建和管理微服…...

C++: 类和对象(上)
📔个人主页📚:秋邱-CSDN博客☀️专属专栏✨:C🏅往期回顾🏆:从C语言过渡到C🌟其他专栏🌟:C语言_秋邱 面向过程和面向对象 C 语言被认为是面向过程的编程…...

Unity程序基础框架
概述 单例模式基类 没有继承 MonoBehaviour 继承了 MonoBehaviour 的两种单例模式的写法 缓存池模块 (确实挺有用) using System.Collections; using System.Collections.Generic; using UnityEngine;/// <summary> /// 缓存池模块 /// 知识点 //…...

TiDB 数据库核心原理与架构_Lesson 01 TiDB 数据库架构概述课程整理
作者: 尚雷5580 原文来源: https://tidb.net/blog/beeb9eaf 注:本文基于 TiDB 官网 董菲老师 《TiDB 数据库核心原理与架构(101) 》系列教程之 《Lesson 01 TiDB 数据库架构概述》内容进行整理和补充。 课程链接:…...

计算机毕业设计Python深度学习垃圾邮件分类检测系统 朴素贝叶斯算法 机器学习 人工智能 数据可视化 大数据毕业设计 Python爬虫 知识图谱 文本分类
基于朴素贝叶斯的邮件分类系统设计 摘要:为了解决垃圾邮件导致邮件通信质量被污染、占用邮箱存储空间、伪装正常邮件进行钓鱼或诈骗以及邮件分类问题。应用Python、Sklearn、Echarts技术和Flask、Lay-UI框架,使用MySQL作为系统数据库,设计并实…...

多核DSP(6000系列)设计与调试技巧培训
课程介绍: 为帮助从事DSP开发工程师尽快将DSP技术转化为产品,在较短时间内掌握DSP设计技术和问题的解决方法,缩短产品开发周期、增强产品竞争力、节省研发经费。我们特组织了工程实践和教学经验丰富的专家连续举办了多期DSP C6000的培训&a…...