【JavaEE】线程安全性问题,线程不安全是怎么产生的,该如何应对

产生线程不安全的原因

在Java多线程编程中,线程不安全通常是由于多个线程同时访问共享资源而引发的竞争条件。以下是一些导致线程不安全的常见原因:

  1. 共享可变状态:当多个线程对共享的可变数据进行读写时,如果没有适当的同步机制,可能导致数据的不一致性。例如,两个线程同时修改一个共享变量,最终的结果可能取决于线程的执行顺序。

  2. 缺乏同步:在没有使用synchronized关键字或其他同步机制(如Lock)进行保护的情况下,多个线程可以同时进入临界区,从而导致线程安全问题。

  3. 指令重排序:为了提高执行效率,Java虚拟机和处理器可能会对指令进行重排序,这种行为在多线程环境中可能导致不可预期的结果,尤其是在多个线程依赖某些变量的状态时。

  4. 原子性问题:某些操作在Java中并不是原子的,例如对对象属性的读-改-写操作。在多线程环境下,这类操作必须通过同步处理以确保原子性。

  5. 死锁:尽管死锁本身不直接导致线程不安全,但在复杂的同步情况下,死锁可能导致某些线程无法继续执行,从而影响整体程序的正确性与稳定性。

  6. 不可见性:当一个线程对共享变量的修改在其他线程中不可见时,可能导致一些线程读取到过时的值。这通常可以通过使用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关键字来确保对共享资源的访问是线程安全的。我们可以对depositwithdraw方法加锁,使得同一时间只有一个线程能够执行其中一个方法。

以下是修改后的代码:

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());}
}

结果

在修改后的代码中,由于对depositwithdraw方法加了synchronized修饰,确保任何时刻只有一个线程可以执行这两个方法,从而避免了由于竞争条件导致的不一致性。最终输出的余额将与预期结果相一致。

指令重排序案例

指令重排序是指在编译、优化或CPU执行过程中,代码的执行顺序被改变。

count++ 操作并不是一个原子操作,它是由三个步骤组成的:

  1. 读取当前的值。
  2. 对值加1。
  3. 将新值写回。

在多线程环境中,多个线程可能会同时对同一变量进行 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。

解决方法

为了解决这个问题,可以使用以下几种方法:

  1. 使用synchronized关键字:将increment方法同步,以确保同一时刻只有一个线程能执行该操作。

  2. **使用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 线程陷入死循环,程序执行不会继续下去。

解决方法

为了解决这个线程不可见性的问题,可以使用以下两种常见方法:

  1. 使用 volatile 关键字:将 fag 声明为 volatile,这样可以确保任何线程对 fag 的写入都会立即对其他线程可见。
  2. 使用同步机制:使用 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 关键字有效地解决了线程间的可见性问题。

 死锁

在多线程编程中,死锁是一种非常严重的问题,它会导致程序无法继续执行。产生死锁的典型条件通常可以归纳为以下四个必要条件:

  1. 互斥条件:至少有一个资源必须被一个线程持有,并且在该资源被其他线程请求时,该线程不能被剥夺,即资源只能被一个线程使用。

  2. 保持并等待条件:一个线程至少持有一个资源,并且正在等待获取其他资源。在这个状态下,线程不会释放它已持有的资源。

  3. 不剥夺条件:一旦资源被分配给某个线程,其他线程不能强制剥夺该资源,只有线程在完成其任务后才能释放它所持有的资源。

  4. 循环等待条件:存在一个线程集合 {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。这样就形成了循环等待,导致两个线程相互阻塞,从而发生死锁。

解决方法

为了避免这种死锁情况,可以使用以下解决方案:

  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)来请求锁,由此避免了死锁情况的发生。

通过这些方法,可以有效减少多线程程序中的死锁风险,保证程序的稳定性。

为了有效避免死锁,可以考虑以下策略:

  • 资源有序分配:为所有资源定义一个全局的获取顺序,线程在请求资源时,按照这个顺序获取,从而避免循环等待的情况。

  • 使用超时机制:在尝试获取锁时,可以设定一个超时时间,若超时则放弃锁的请求,减少潜在的死锁情况。

  • 避免保持并等待:可以在开始线程时一次性请求所有所需资源,成功则继续执行,失败则释放所有已获得的资源。

  • 检测与恢复:定期检查系统中是否存在死锁,如果发现可以中断某些线程或者释放某些资源来解除死锁。

通过合理的设计与计划,可以有效减少死锁的可能性,提高系统的稳定性和可靠性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/52794.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

鸿蒙Next 单元测试框架——hypium

一 框架概述 单元测试框架(hypium)是HarmonyOS上的测试框架&#xff0c;提供测试用例编写、执行、结果显示能力&#xff0c;用于测试系统或应用接口。 表1 单元测试框架功能特性 二 安装使用 目前hypium以npm包的形式发布, 因此需要在Deveco Studio 工程级package.json内配…

CSS-常用属性【看这一篇就够了】

目录 前言文章 常用属性 cursor鼠标样式 outline外轮廓 border与outline的区别 overflow超出部分隐藏 overflow属性值 overflow-x和overflow-y vertical-align属性 应用案例 常用的a标签布局按钮 水平居中的轮播图按钮 产品展示效果&#xff1a; 小米商城菜单 前…

【C#】属性的声明

在面向对象程序设计中,属性是访问对象存储数据的首选方式。 一般不要直接公开类的变量成员,即便是get访问器和set访问器并无数据访问规则。 属性的声明 1. 完整声明 在代码中输入propfull &#xff0c;并连续按两下tab键 高亮的部分是可以修改的部分&#xff0c;按tab键可以…

FPGA上板项目(四)——FIFO测试

目录 实验内容实验原理FIFO IP 核时序绘制HDL 代码仿真综合实现上板测试 实验内容 理解 FIFO 原理调用 FIFO IP 核完成数据读写 实验原理 FIFO&#xff1a;First In First Out&#xff0c;先入先出式数据缓冲器&#xff0c;用来实现数据先入先出的读写方式。可分类为同步 FI…

一个php快速项目搭建框架源码,带一键CURD等功能

介绍&#xff1a; 框架易于功能扩展&#xff0c;代码维护&#xff0c;方便二次开发&#xff0c;帮助开发者简单高效降低二次开发成本&#xff0c;满足专注业务深度开发的需求。 百度网盘下载 图片&#xff1a;

科研绘图系列:R语言多组极坐标图(grouped polar plot)

介绍 Polar plot(极坐标图)是一种二维图表,它使用极坐标系统来表示数据,而不是像笛卡尔坐标系(直角坐标系)那样使用x和y坐标。在极坐标图中,每个数据点由一个角度(极角)和一个半径(极径)来确定。角度通常从水平线(或图表的某个固定参考方向)开始测量,而半径则是…

【网络安全】服务基础第一阶段——第六节:Windows系统管理基础---- DNS部署与安全

计算机智能识别并用IP地址定位&#xff0c;例如我们想要访问一个网页&#xff0c;其实是只能使用这个网页的IP地址&#xff0c;即四位的0&#xff5e;255来访问&#xff0c;但这一串数字难以记忆&#xff0c;于是就有了DNS&#xff0c;将难以记忆的数字转化为容易记忆的域名&am…

合宙LuatOS产品规格书——Air700EAQ

Luat Air700EAQ是合宙的LTE Cat.1bis通信模块&#xff0c;采用移芯EC716E平台&#xff0c;支持LTE 3GPP Rel.13技术。 该模块专为满足小型化、低成本需求而设计&#xff0c;具备超小封装和极致成本优势。 Air700EAQ支持移动双模&#xff0c;内置丰富的网络协议&#xff0c;集…

基于AI大模型开发上层应用常见的技术栈

基于AI大模型的上层应用开发&#xff0c;技术栈要求通常包括以下几个方面&#xff1a; 编程语言&#xff1a;Python是AI领域的主要编程语言&#xff0c;具有大量的库和框架支持&#xff0c;是大模型开发的首选语言 。TypeScript也是不错的选择&#xff0c;很多模型对外提供类似…

Java使用POI创建带样式和公式的Excel文件

这篇文章将演示如何使用POI 创建带样式和公式的Excel文件。 代码 import org.apache.poi.ss.usermodel.*; import org.apache.poi.xssf.usermodel.XSSFWorkbook;import java.io.FileOutputStream; import java.io.IOException;public class ExcelDemo {public static void mai…

FPGA第 5 篇,FPGA技术优略势,FPGA学习方向,FPGA学习路线(FPGA专业知识的学习方向,FPGA现场可编程门阵列学习路线和方向)

前言 前几篇讲了一下FPGA的发展和应用&#xff0c;以及未来前景。具体详细&#xff0c;请看 FPGA发展和应用&#xff0c;以及未来前景https://blog.csdn.net/weixin_65793170/category_12665249.html 这里我们来&#xff0c;记录一下&#xff0c;FPGA专业知识的学习路线 一.…

Python(C++)自动微分导图

&#x1f3af;要点 反向传播矢量化计算方式前向传递和后向传递计算方式图节点拓扑排序一阶二阶前向和伴随模式计算二元分类中生成系数高斯噪声和特征二元二次方程有向无环计算图超平面搜索前向梯度下降算法快速傅里叶变换材料应力和切线算子GPU CUDA 神经网络算术微分 Pytho…

C语言阴阳迷宫

目录 开头程序程序的流程图程序游玩的效果下一篇博客要说的东西 开头 大家好&#xff0c;我叫这是我58。 程序 #define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <Windows.h> enum WASD {W…

设计模式 -- 外观模式(Facade Pattern)

1 问题引出 组建一个家庭影院 DVD 播放器、投影仪、自动屏幕、环绕立体声、爆米花机,要求完成使用家庭影院的功能&#xff0c;其过程为&#xff1a; 直接用遥控器&#xff1a;统筹各设备开关 开爆米花机&#xff0c;放下屏幕 &#xff0c;开投影仪 &#xff0c;开音响&#xf…

图像金字塔的作用

1. 概述 图像金字塔是图像多尺度表达的一种&#xff0c;主要应用与图像分割&#xff0c;是一种以多分辨率来解释图像的有效但概念简单的结构。图像金字塔实际上是一张图片在不同尺度下的集合&#xff0c;即原图的上采样和下采样集合。金字塔的底部是高分辨率图像&#xff0c;而…

C++学习/复习补充记录 --- 图论(深搜,广搜)

数据结构与算法 | 深搜&#xff08;DFS&#xff09;与广搜&#xff08;BFS&#xff09;_深搜广搜算法-CSDN博客 深度优先搜索理论基础 深搜和广搜的区别&#xff1a; &#xff08;通俗版&#xff09; dfs是可一个方向去搜&#xff0c;不到黄河不回头&#xff0c;直到遇到绝境了…

Netty 学习笔记

Java 网络编程 早期的 Java API 只支持由本地系统套接字库提供的所谓的阻塞函数&#xff0c;下面的代码展示了一个使用传统 Java API 的服务器代码的普通示例 // 创建一个 ServerSocket 用以监听指定端口上的连接请求 ServerSocket serverSocket new ServerSocket(5000); //…

android13 隐藏状态栏里面的飞行模式 隐藏蓝牙 隐藏网络

总纲 android13 rom 开发总纲说明 目录 1.前言 2.问题分析 3.代码分析 4.代码修改 5.编译运行 6.彩蛋 1.前言 android13 隐藏状态栏里面的飞行模式,或者其他功能,如网络,蓝牙等等功能,隐藏下图中的一些图标。 2.问题分析 这里如果直接找这个布局的话,需要跟的逻…

nefu暑假acm集训1 构造矩阵 个人模板+例题汇总

前言&#xff1a; 以下都是nefu暑假集训的训练题&#xff0c;我在此把我的模板和写的一些练习题汇总一下并分享出来&#xff0c;希望在能满足我复习的情况下能帮助到你。 正文&#xff1a; 模板&#xff1a; #include<bits/stdc.h> using namespace std; typedef long…

Qt 学习第7天:Qt核心特性

元对象系统Meta-object system 来自AI生成&#xff1a; Qt中的元对象系统&#xff08;Meta-Object System&#xff09;是Qt框架的一个核心特性&#xff0c;它为Qt提供了一种在运行时处理对象和类型信息的能力。元对象系统主要基于以下几个关键概念&#xff1a; 1. QObject&a…