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

JavaEE——线程安全

目录

  • 前言
  • 1.线程安全的定义
  • 2.线程安全问题产生的原因
    • 2.1 多个线程修改一个变量
    • 2.2 修改操作不是原子的
    • 2.3 内存可见性引起的线程安全问题
  • 3.解决线程安全问题的方法
    • 3.1 通过synchronized关键字加锁
    • 3.2 使用volatile关键字
  • 总结

前言

在使用多线程的时候,难免会出现一些问题,会产生线程安全问题,本篇文章就来简单讲述一下线程安全问题产生的原因和对应的解决方案。

1.线程安全的定义

线程安全是指当多个线程同时访问某个对象或方法时,不会出现数据不一致、逻辑错误或者意外结果的情况。即当把一个单线程执行的程序修改成一个多线程程序,产生的结果要和原来一样,如果不一样,则可以认为出现了线程安全问题。

2.线程安全问题产生的原因

造成线程不安全有各种各样的原因,但是导致线程不安全的根本原因就是线程的调度是随机的。下面还有一些常见的原因。

2.1 多个线程修改一个变量

多个线程同时访问共享资源(比如说同一个变量)可能会产生竞争状态,比如一个线程读取的是1,然后改成2,还没有保存,另一个线程也来读取,此时读取的还是1,这就会导致每次多线程执行完毕的时候,得出的结果可能都不一样。

2.2 修改操作不是原子的

就比如说对变量进行++操作,在java语句中是一行代码,实际上是三个操作:读取内存数据到CPU;对数据进行更新;然后将数据写回CPU。
在进行多线程操作的时候,可能就会因为操作顺序的抢占而产生线程安全问题

2.3 内存可见性引起的线程安全问题

下面先给出一段代码:

public class Demo17 {private static int flag = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {while(flag == 0){//do nothing}System.out.println("t1线程结束");});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("请输入一个数字:");flag = scanner.nextInt();System.out.println("t2线程结束");});t1.start();t2.start();t1.join();t2.join();}
}

我们来尝试运行一下
我们输入一个非零数字,理论上应该弹出t1线程结束和t2线程结束,我们来看结果:
在这里插入图片描述
我们发现并没有显示预期的结果,这是为什么?
这里就是由于”内存可见性“。
意思就是这里t2对flag进行修改,但是t1没有感知到,就是t1”没有看见“,一直认为flag还是0。
这就是内存可见性问题。
这是由于编译器优化所产生的问题,在代码编译时,编译器会进行一些优化策略,对代码进行优化,来提高程序的效率,这里是由于编译器优化出现了误判,从而导致代码的逻辑发生了改变,上面的程序就是由于此原因而出现问题,在t1的反复循环中,flag会被存进寄存器里来提高读取效率,不通过内存来读,而t2把通过内存把flag的数值变化后,t1仍然读的是原来存取到寄存器中的‘0’的值,这就是t1线程“看不见”flag的值变化的原因。

3.解决线程安全问题的方法

上述的根本原因不好解决,我们可以通过把修改操作变成”原子的“ 来解决线程安全问题

3.1 通过synchronized关键字加锁

可以通过加锁的方式将多段代码变成一个整体,让其它的线程无法进行干扰。我们通过synchronized关键字来实现锁。
比如我们对如下代码进行分析:

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

可以看出这里的操作并不是原子的,所以并不能如愿获得100000的结果。
我们就需要使用synchronized关键字来进行加锁,方法如下:

package thread;/*** @author Wind* @date 2025-04-13*/
public class Demo14 {public static int count = 0;private static Object locker = new Object();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {synchronized (locker) {count++;}}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {synchronized (locker){count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = " + count);}
}

将count++操作进行加锁,在执行count++前,后台会将语句lock,在调用完count++操作后,进行unlock操作,假设当t1加锁后,t2要进行count++前,要执行加锁,此时加锁不成功,会阻塞等待,知道t1进行unlock,这样就不会导致多个线程对同一个变量进行修改。现在观看结果,可以发现结果正确:
在这里插入图片描述
synchronized还可以用来修饰方法,同时也能起到加锁的作用:

public class Demo14 {static class Counter{private int count = 0;private Object locker = new Object();synchronized public void add() {count++;}}public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.add();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.add();}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = " + counter.count);}
}

这里实际上就是对add方法加锁。

synchronized也可以对静态方法加锁,此时可以认为是对类对象进行加锁。

3.2 使用volatile关键字

使用volatile关键字可以解决上面所出现的内存可见性问题。
volatile关键字用于修饰变量,用于告诉编译器,这个变量是经常性变化的,使编译器不对变量进行上面内存可见性中所出现的优化。
修改后的代码(将flag用volatile修饰):

public class Demo17 {private static volatile int flag = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {while(flag == 0){//do nothing}System.out.println("t1线程结束");});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("请输入一个数字:");flag = scanner.nextInt();System.out.println("t2线程结束");});t1.start();t2.start();t1.join();t2.join();}
}

下面看运行结果:
在这里插入图片描述
可以看到,程序正常结束。
此外还需要注意的是,volatile虽然可以解决内存可见性问题,但它本身不具有原子性,无法解决不是原子性的问题。

总结

以上就是对线程安全的简单介绍,希望这篇文章能帮助你更加熟练的运用多线程。

相关文章:

  • Java工具类——实体类列表写入excel
  • Java 工厂设计模式详解:用统一入口打造灵活可扩展的登录系统----掌握 Spring 源码的基础第一步
  • 【Semantic Kernel核心组件】Plugin:连接AI与业务逻辑的桥梁
  • EmbeddingBag介绍与案例
  • Android问题整理
  • 数据加盐/加密
  • CentOS 中安装 vim
  • qt中关于思源雅黑字体的使用
  • OpenCV 图形API(43)颜色空间转换-----将 BGR 图像转换为 LUV 色彩空间函数BGR2LUV()
  • 《vue3学习手记4》
  • 《前端面试题之 Vue 篇(第二集)》
  • await 在多线程,子线程中的使用
  • 大模型赋能工业制造革新:10个显效可落地的应用场景
  • 字符设备驱动程序的另一种注册方法
  • IoT FEM射频前端模组芯片(2.4G PA)三伍微电子GSR2401 兼容替代RFX2401
  • 408数据结构绪论刷题001
  • 3.Rust + Axum 提取器模式深度剖析
  • 红宝书第四十九讲:XSS/CSRF攻击防御策略解析
  • 从零上手GUI Guider学习LVGL——Button
  • AI与思维模型——耗散结构思维模型【64】
  • ​中国超大规模市场是信心所在——海南自贸港建设一线观察
  • 习近平会见古巴国家主席迪亚斯-卡内尔
  • 850亿元!2025年中央金融机构注资特别国债(一期)拟第一次续发行
  • 中消协点名新能源汽车行业:定金退款争议频发
  • 国防部:正告菲方停止以任何方式冲撞中方核心利益
  • 常州市委原常委、组织部部长陈翔调任江苏省民宗委副主任