当前位置: 首页 > news >正文

Java EE - 线程安全的产生及解决方法

目录

  • 1.线程安全
    • 1.1 线程安全例子
    • 1.2 出现不安全的原因
  • 2.解决方法
    • 2.1 串行执行
    • 2.2 加锁
  • 3.小结

1.线程安全

1.1 线程安全例子

线程安全问题是在多线程并发的情况下,程序执行的结果与预期的结果不一致导致出现的问题,可以先看以下例子:创建两个线程t1 和 t2,定义一个变量count,在两个线程中对变量count分别执行累加操作(count++)5000次,然后输出count变量,预期输出结果是10000.

public class Main{//定义变量static int count = 0;public static void main(String[] args) throws InterruptedException {//创建线程t1Thread t1 = new Thread(() -> {for (int i = 0; i < 5000; i++) {count++;}});//创建线程t2Thread t2 = new Thread(() -> {for (int i = 0; i < 5000; i++) {count++;}});//启动线程t1.start();t2.start();//确保累计操作完成执行完成:两个线程都销毁才执行输出的操作while(t1.isAlive() && t2.isAlive());//输出System.out.println("count = " + count);}
}

程序多次输出的结果如下:7764,7337,8377.

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
通过程序的执行结果可以看出,每一次的输入都是不一样的,此时的线程就是不安全的。

1.2 出现不安全的原因

为什么会程序这种情况呢?

1.操作系统在给线程分配资源的时候是随机的,可能先一段时间执行t1线程,再一段时间执行t2线程,也有可能t1执行完在分配给t2线程,或者t2先执行完后执行t1线程,两个线程并发的过程中就可能存在时间上的间隙(不在同一时间上执行)。

2.两个线程都是执行count++操作,而count++虽然是一条语句,但是在真正执行过程中,++操作对应的是3条指令,1.load指令(从内存中将变量count读取到cup中);2.add指令(在cpu中对count变量执行+1操作);3.save指令(将count变量重新存储到内存中);

由于线程的执行时间可能存在间隙,而count++操作对应的是多个指令,所以在并发执行过程中就有可能出现错误的执行结果,以下通过图解来做具体展示。

在并发程序执行的过程中可能出现的几种情况:1,2,3,4

在这里插入图片描述
在这里插入图片描述
观察情况1和情况2可知,两种情况都是先执行完一个线程后再执行另一个线程,以情况1做具体的介绍;在执行load操作时,本质是将变量从内存读取后存放在cup的寄存器中,以下通过寄存器1和寄存器2作为t1 和 t2线程读取变量存储的位置,如下图:

在这里插入图片描述

开始执行语句:执行开启线程t1语句,操作系统先给t1线程分配资源;

在这里插入图片描述

执行load指令,将count从内存中读取到寄存器1中。

在这里插入图片描述
执行add指令,将寄存器1中的值累加1,值由0变为1.

在这里插入图片描述
执行save指令,将寄存器1的值存放到内存中,count = 1.

在这里插入图片描述
执行t1.interupt()(不是真的调用中断线程方法,此处是表示累加操作完成),完成累加操作。

在这里插入图片描述
执行启动线程t2,为t2线程分配资源。

在这里插入图片描述
执行load指令,将count变量从内存中读取到寄存器2中。

在这里插入图片描述
执行add指令,将寄存器2中的值累加,由1变为2.

在这里插入图片描述
执行save语句,将寄存器2中的值加载到内存中,count = 2.

在这里插入图片描述
通过以上执行操作是可以得到正确的结果的,但是如果执行的是3和4情况,就会出现错误的结果,以情况3作为具体的展示。

在这里插入图片描述
执行t1线程开启,为线程分配资源,执行load语句,将count从内存读取到寄存器1中。

在这里插入图片描述
执行t2线程的启动和load指令,将count从内存读取到寄存器2中。

在这里插入图片描述
执行线程t2的add操作,将寄存器2中的值累加,由0变为1.

在这里插入图片描述
执行线程2的save指令,将寄存器2的值加载到内存中,count = 1.

在这里插入图片描述

执行t1线程的add操作,将寄存器1的值累加,由0变为1.

在这里插入图片描述
执行t1线程的save操作,将寄存器1的值加载到内存中,此时count = 1.

在这里插入图片描述
最后执行t1线程和t2线程的终止,可以看出经过两次累加操作操作,count 最终的结果是1,与预计出现的2不同,此时就发生了线程不安全问题。

线程安全产生的原因:
1)操作系统对线程的分配是随机调度的,即抢占式执行;
2)多个线程对同一个变量进行修改;
3)修改的操作不是原子性的;

在以上累加的例子中可以看出,count++这个操作是可以分为多条指令的,因此累加操作不是原子性的,同时t1和t2线程都是在修改count变量,同时在抢占式执行系统资源,所以导致累加的操作是不安全的。

2.解决方法

2.1 串行执行

将并发执行的过程改为串行执行,即先执行一个线程,等线程执行完成后再执行下一个线程;

例如:先执行线程t1的累加操作后,再执行t2线程的累加操作,最终可以得到10000.

class Main{//定义变量static int count = 0;public static void main(String[] args) throws InterruptedException {//创建线程t1Thread t1 = new Thread(() -> {for (int i = 0; i < 5000; i++) {count++;}});//创建线程t2Thread t2 = new Thread(() -> {for (int i = 0; i < 5000; i++) {count++;}});//启动线程t1.start();t1.join();//等t1执行完,再执行下面操作t2.start();t2.join();//等t2执行完,再执行下面操作//确保累计操作完成执行完成:两个线程都销毁才执行输出的操作while(t1.isAlive() && t2.isAlive());//输出System.out.println("count = " + count);}
}

2.2 加锁

第一种方法虽然能解决问题,但是在多线程中还是希望能够并发执行的,所以就可以使用锁来解决,可以创建一个锁对象locker,对线程中的累加操作加锁,就能解决累加过程中出现的线程安全问题。

class Main{//定义变量static int count = 0;//    class Count{
//        synchronized  static void add(){
//            count++;
//        }
//    }//锁对象static Object locker = new Object();public static void main(String[] args) throws InterruptedException {//创建线程t1Thread t1 = new Thread(() -> {//加锁synchronized (locker){for (int i = 0; i < 5000; i++) {count++;//or Count.add(),此时就不需要加锁,默认锁对象是Count}}});//创建线程t2Thread t2 = new Thread(() -> {//加锁synchronized (locker){for (int i = 0; i < 5000; i++) {count++;//or Count.add(),此时就不需要加锁}}});//启动线程t1.start();t2.start();//确保累计操作完成执行完成:两个线程都销毁才执行输出的操作//while(t1.isAlive() && t2.isAlive());//加锁后不能使用这个条件,因为可能其中一个线程先执行完,导致提前输出Thread.sleep(1000);//累加10000是不需要消耗1000毫秒时间//输出System.out.println("count = " + count);}
}

在这里插入图片描述

3.小结

线程安全问题通常出现在多线程并发执行的过程中,由于多个线程同时修改一个变量,且修改操作不是原子性的导致出现错误的情况,除了以上情况外,内存可见性问题和指令重排序也会使线程出现不安全的情况,如果需要解决线程安全问题可以通过加锁操作来解决或者将线程改为串行执行。

加锁操作不一定能解决线程安全的问题,而且可能导致程序一直进入阻塞等待,即死锁,下一篇文章将讲解死锁的产生和如何解决和预防出现死锁。

欢迎在评论区分享你的想法或问题,我们将在后续内容中继续探讨。

期待在评论区看到你的见解,下期内容更精彩。

http://www.dtcms.com/a/601624.html

相关文章:

  • wordpress 迁移网站怎样在电脑登录wordpress
  • JavaEE进阶——Spring Boot项目
  • 供应商网络安全风险评估方法
  • 硅基计划6.0 伍 JavaEE 网络原理
  • 使用vLLM与Docker在Ubuntu 22.04上离线部署Qwen3-4B模型:多卡配置完整指南
  • 南京建设网站制作巧克力网站模板
  • 2019年的阜南县建设修路网站Net网站开发多少钱
  • 收集系统资源使用情况
  • 100v转12v芯片,100V转-12V负压方案AH7691D
  • 没有网站怎么做cpa赚钱网站设计论文的参考文献
  • Docker 部署 MySQL 5.7
  • 【图像处理基石】如何对图像畸变进行校正?
  • Step by Step Configuration Of DataGuard Broker for Oracle 19C
  • 阿里云服务器网站备案工程造价材料信息网
  • 做底单的网站wordpress oauth
  • mkcert 自签证书以及 jssip
  • 新出土的古陶瓷碎片的图片并根据碎口尝试进行拼接用什么模型算法比较合适?古陶瓷碎片拼接算法选型
  • 网站建设人员需求化妆品网站的建设方案
  • Flink原理与实战(java版)#第2章 Flink的入门(第一节大数据架构的演变)
  • Python好玩的算法库
  • 银河麒麟V10下使用virt-manager安装Windows虚拟机
  • 插值——牛顿插值
  • 【稳定性】system_app_anr@1760693457221.txt和anr_2025-10-17-17-30-35-009有什么区别
  • 网站建设 教材 推荐免费网站提供
  • Java ee初阶——定时器
  • 【JavaEE初阶】网络层-IP协议
  • tomcat/idea打包部署报错,RUN 可以 DEBUG 不行
  • 地方网站还有得做吗永嘉做网站
  • 防滑齿位置与牙根断裂风险的相关性分析
  • Lua学习记录(1) --- Lua中的条件分支语句和循环语句