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

JAVA:线程安全问题及解决方案

线程安全的概念:

如果在多线程环境下代码运行结果是符合我们预期的,就是如果在单线程环境应该的结果,那我们就说这个程序是线程安全的。

问题:

1、操作系统对线程的调度是随机的,抢占式执行

2、多个线程同时修改同一变量

3、修改操作不是原子的

4、指令重排序

5、内存可见性 

解决方案:

针对问题1:

随机调度是系统设置,我们无法对此进行修改,但是也不是放任这种问题不管了,随机调度使我们代码的执行顺序存在很多变数,不一定会按照我们想要的顺序执行,程序员就需要保证在不同执行顺序条件下,代码都能正常执行

针对问题2、3:

问题2可以说是问题3的一个具体案例

public class ThreadDemo {public static int count=0;public static void main(String[] args) throws InterruptedException {Thread thread1=new Thread(()->{for(int i=0;i<50000;i++){count++;}});Thread thread2=new Thread(()->{for(int i=0;i<50000;i++){count++;}});thread1.start();thread2.start();thread1.join();thread2.join();System.out.println(count);}
}

这里预期运行的结果应该是10W,但是 实际上不是,这里就涉及到多个线程同时修改count变量,结果也不是我们的预期结果。

count++:这一步其实需要3个CPU指令完成,分别是将count内存中的值加载到寄存器;把寄存器中的内容+1;把寄存器中的内容保存回内存里

我们知道操作系统对线程的调度是随机的。可能出现  在 thread1 中进行到第2步,还没保存到内存里呢,操作系统又调走执行 thread2 ,本来应该count已经加过1了但是没有保存到内存里就被覆盖了,就出现运行结果少于10W的情况,当然还会有其他调度的顺序,这里就不在一一列举了。

这也是因为修改操作不是原子的,什么是原子性?

原子性在计算机中是指一个操作或者一系列操作要么全部执行成功,要么全部不执行,不存在部分执行的情况。

例如上面代码中  count++  操作在CPU指令中分三步执行,中途突然不执行了,就不是原子性的。

怎么保证java的一串代码或者一条代码是原子性的?

Java中提供了加锁操作(synchronized),这样运行结果就是正确的。

public class ThreadDemo {public static int count=0;public static void main(String[] args) throws InterruptedException {Object lock=new Object();Thread thread1=new Thread(()->{for(int i=0;i<50000;i++){synchronized (lock){count++;}}});Thread thread2=new Thread(()->{for(int i=0;i<50000;i++){synchronized (lock){count++;}}});thread1.start();thread2.start();thread1.join();thread2.join();System.out.println(count);}
}
针对问题4:

在执行程序时为了提高性能,编译器与处理器通常会给代码进行优化,做指令重排序

重排序分三种类型:
编译器优化的重排序 编译器在不改变单线程程序语义的前提下(代码中不包含synchronized),可以重新安排语句的执行顺序。
指令级并行的重排序 现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
内存系统的重排序 由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行

如果是单线程,JVM与CPU指令会进行优化,并不会影响什么

如果在多线程的情况下,代码执行复杂度很高,如果编译器优化把执行逻辑变了,很可能会导致与原本我们想要的结果发生变化,得出我们并不想要的结果,导致线程不安全。

重排序会带来的问题就是可能会导致出现内存可见性问题。

 

 针对问题5:

可见性是说,一个线程对共享变量值的修改能够及时的被其他线程看到

JVM中规范定义了Java内存模型,我们都知道Java有个特点:一次编译,到处运行

Java内存模型就屏蔽了各种硬件与操作系统的内存访问差异,Java程序在各种平台下都能达到一致的并发效果。

 线程的共享变量存在主内存

每个线程都有自己的工作内存

Load是把内存中的变量值加载到寄存器

Save是把寄存器中的内容保存回内存中

当线程要读取一个共享变量时,会先把主变量拷贝到工作内存中,再从工作内存读取数据

当线程要修改共享变量的值时,会先修改工作内存拷贝的变量值,再同步到主内存

了解以上以后,我们来具体看一下内存可见性为什么会导致线程不安全

我们了解到每个线程都有自己的工作内存,工作内存的内容相当于共享变量的副本,假设有两个线程,线程1修改工作内存中的共享变量值还未同步到主内存,线程2进行读取或者修改操作还是原本没有修改的变量(线程2从主内存中读取,线程1把副本修改了但是没有同步到主内存中,共享变量值没被修改),这就导致了线程不安全

可以通过以下来解决:  

        1、volitale关键字

public class ThreadDemo1 {public static volatile boolean flag=false;public static void main(String[] args) {new Thread(()->{try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}flag=true;System.out.println("线程修改了flag值为true");}).start();while(!flag){//主线程不断检查flag的值}System.out.println("主线程检测到flag值变为true,程序结束");}
}

如果不加 volatile 关键字,主线程就一直无法感知到flag被修改为true,陷入死循环。加上 volatile 关键字后就能保证主线程可以及时获取flag的最新值  

        2、使用synchronized关键字

public class ThreadDemo2 {private int count=0;public synchronized void increment(){count++;}public static void main(String[] args) throws InterruptedException {ThreadDemo2 threadDemo2=new ThreadDemo2();Thread thread1=new Thread(()->{for(int i=0;i<50000;i++){threadDemo2.increment();}});Thread thread2=new Thread(()->{for(int i=0;i<50000;i++){threadDemo2.increment();}});thread1.start();thread2.start();thread1.join();thread2.join();System.out.println("count="+threadDemo2.count);}
}

 把 synchronized 修饰 increment 方法,保证同一个时刻只能有一个线程进入方法修改count值,避免了多个线程同时修改count变量导致的内存可见性与数据不一致问题。

        3、使用并发工具类

Java并发包提供了一些工具类,类似 AtomicInteger、AtomicLong 等原子类,他们通过硬件级别的原子操作来保证变量的可见性与原子性,从而避免了内存可见性问题

import java.util.concurrent.atomic.AtomicInteger;public class ThreadDemo3 {private AtomicInteger count=new AtomicInteger(0);public void increment(){count.incrementAndGet();}public static void main(String[] args) throws InterruptedException {ThreadDemo3 threadDemo3=new ThreadDemo3();Thread thread1=new Thread(()->{for(int i=0;i<50000;i++){threadDemo3.increment();}});Thread thread2=new Thread(()->{for(int i=0;i<50000;i++){threadDemo3.increment();}});thread1.start();thread2.start();thread1.join();thread2.join();System.out.println("count="+threadDemo3.count);}
}

 incrementAndGet 方法是对一个数值进行原子性的递增操作并返回递增后的值

使用 AtomicInteger 类来管理计数器 atomicCount , incrementAndGet 方法是原子操作,保证了多线程环境下对 atomicCount 的修改是线程安全的,避免了内存可见性带来的问题。 

http://www.xdnf.cn/news/164485.html

相关文章:

  • Centos7系统防火墙使用教程
  • 【JavaScript】自增和自减、逻辑运算符
  • 五年经验Java开发如何破局创业
  • L1-5 这是字符串题
  • # **DeepSeek 保姆级使用教程**
  • Redis数据结构SDS,IntSet,Dict
  • Java—— 五道算法水题
  • 强化学习基础
  • Python AI图像生成方案指南
  • Axure疑难杂症:全局变量典型应用及思考逻辑(玩转全局变量)
  • 剑指offer经典题目(六)
  • 做的一些题目的答案和自己的一些思考
  • LangChain 中的 Task(任务) 主要通过 生成器(Generator) 实现,而非传统的迭代器(Iterator)
  • Ardunio学习
  • 推论阶梯——AI与思维模型【81】
  • Redis 数据分片三大方案深度解析与 Java 实战
  • JavaScript原生实现简单虚拟列表(列表不定高)
  • 【Agent python实战】ReAct 与 Plan-and-Execute 的融合之道_基于DeepSeek api
  • 快速上手c语言
  • 栈与堆的演示
  • C++ 为什么建议类模板定义在头文件中,而不定义在源文件中
  • 对卡尔曼滤波的理解和简单示例实现
  • 数据库原理(1)
  • N字形上升形态选股代码如何编写?
  • 平面连杆机构(上)
  • 定制一款国密浏览器(11):SM2算法的椭圆曲线参数定义
  • 4月25日日记(补)
  • 6.Geometric Intersection (几何求交)- Preliminary
  • 用高德API提取广州地铁线路(shp、excel)
  • Docker Compose--在Ubuntu中安装Docker compose