【多线程带来的的风险-线程安全的问题的简单实例-线程不安全的原因】

news/2024/10/25 18:24:36/

文章目录

  • 前言
  • 线程不安全的5大原因
    • 1. 抢占式执行和随机调度
    • 2. 多个线程同时修改一个变量(共享数据)
    • 3. 修改操作不是原子性的
    • 4. 内存可见性
    • 5. 指令重排序


前言

什么是线程安全?

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

但是在多线程的环境下,我们很难预知线程的调度方法,这就像A和B两个施工队,同时在山的两头开始挖隧道,理想状态下我们希望他们能够在中间相遇,但是也极有可能他们没有相遇,各自挖了一条隧道。


提示:以下是本篇文章正文内容,下面案例可供参考

线程不安全的5大原因

1. 抢占式执行和随机调度

线程的抢占式执行

  • 是指操作系统可以在任何时刻强制暂停当前线程的执行,并将处理器分配给另一个就绪状态的线程。抢占式执行可以保证操作系统的响应能力和调度公平性,避免某个线程长时间占用处理器而导致其他线程无法得到执行的问题。

线程的随机调度

  • 是指操作系统在多个就绪状态的线程中随机选择一个线程来执行。随机调度可以保证线程执行的公平性和可预测性,避免某个线程过度优先导致其他线程无法得到执行的问题。

简单来说就是,线程中的代码执行到任意的一行,都随时可能被切换出去。

代码示例

public class Ceshi {public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i <= 10; i++) {System.out.print(i + " ");}});Thread t2 = new Thread(() -> {for (int i = 11; i <= 20; i++) {System.out.print(i + " ");}});//我们期望能够打印0-20的递增形式t1.start();t2.start();}
}

输出结果 1

在这里插入图片描述

输出结果 2
在这里插入图片描述

输出结果 3
在这里插入图片描述

图解

在这里插入图片描述

可以看出每次执行的结束都不相同,这是由于线程的抢占式执行和随机调度的结果,你无法预测操作系统会如何安排任务的执行顺序。


2. 多个线程同时修改一个变量(共享数据)

代码示例

public class Ceshi {public static int a = 0;//共享数据apublic static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 10; i++) {a += 2;if (a > 0) System.out.print("大 ");}});Thread t2 = new Thread(() -> {for (int i = 0; i < 10; i++) {a -= 3;if (a < 0) System.out.print("小 ");}});t1.start();t2.start();//保证t线程能够执行完毕Thread.sleep(1000);System.out.println();}
}

输出结果 1

在这里插入图片描述

输出结果 2

在这里插入图片描述

输出结果 3

在这里插入图片描述

图解

在这里插入图片描述

t1 t2 这两个线程都能够访问到a,两个线程同时分别对a进行修改和判断,你无法预料到它会被两个线程如何的互相争夺。


3. 修改操作不是原子性的

代码示例

class Counter1 {private int count = 0;public void add() {count++;// ++ 操作就不是原子性的// 它会被操作系统分为三步操作//1. load,把内存中的数据读取到cpu寄存器中//2. add,把寄存器中的值,进行+1运算//3. save,把寄存器中的值写回到内存中}public int get() {return count;}
}public class ThreadDemo {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();//两个线程,分别对count++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();//与预期结果100000不同(线程不安全问题,抢占式执行)System.out.println(counter.get());}
}

输出结果

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

图解

在这里插入图片描述

4. 内存可见性

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

在这里插入图片描述

如果a不是内存可见的,那么t1t2就会同时对a进行修改,可能会对预期的结果产生问题。


5. 指令重排序

指令重排序是现代处理器为了提高指令执行效率所采取的一种优化手段。它可以将指令的执行顺序进行重新排序,以最大程度地利用CPU内部资源,提高CPU的执行效率。

具体来说,指令重排序可以分为两种类型:

  1. 编译器重排序:编译器会将乱序的代码重新排列成一个顺序执行代码,以提高程序执行速度。

  2. 处理器重排序:处理器会按照一定的策略对指令执行顺序进行调整,以最大程度地利用CPU内部的各种功能单元。

代码示例

public class Ceshi {public static int flag = 0;public static void main(String[] args) {Thread t1 = new Thread(() -> {while (flag == 0) {// == 符号分为两部//1. load,开销是cmp的几千倍//2. cmp,很快//load之前,已经cmp了很多次,编译器就认为每次load的值都相同,为了节省时间,所以直接将load优化掉了//就导致结果出错//一般单线程都是正确的,多线程会出错// 空循环,会快速的执行,发现每次flag==0,此时编译器就会动了优化的心思}//导致该句,输出不来System.out.println("循环结束,t1结束");});Thread t2 = new Thread(() -> {while (true) {Scanner scanner = new Scanner(System.in);System.out.println("请输入一个整数改变flag:");//输入的这点时间内,flag==0可能已经比较了无数次flag = scanner.nextInt();}});t1.start();t2.start();}
}

输出结果

在这里插入图片描述

可以看出,即使输入了多个数,也没有输出 循环结束,t1结束这条语句,因为编译器已经认为我比较了那么多次(由于它很快,在输入之前已经比较了无数次),flag就是固定值0,不会变了,以后就不用执行load了。

  • 所以虽然指令重排序可以提高CPU的执行效率,但它也可能会带来一些问题。尤其是在多线程并发执行时,指令重排序可能会导致程序执行结果出现错误,这种问题被称为“内存模型问题”。为了解决这类问题,Java 提供了一些机制,如“volatile”关键字和“synchronized”关键字,用于禁止指令重排序和保证内存可见性。


http://www.ppmy.cn/news/981038.html

相关文章

数据结构【线性表】

数据结构入门级 第二章 线性表 一、线性表的定义和基本操作 线性表的定义&#xff1a;具有相同属性数据类型的数据元素组成的一个有限序列&#xff1b;除第一个元素外的元素都有直接前驱&#xff0c;除最后一个元素外的元素都有直接后继&#xff1b;存在一个唯一被称为“第一个…

【MATLAB第60期】基于MATLAB的ARMAX具有外生回归因子的移动平均自回归模型

【MATLAB第60期】源码分享 | 基于MATLAB的ARMAX具有外生回归因子的移动平均自回归模型 一、简要介绍 ARMAX模型相比ARMA考虑了影响因素 &#xff0c;即可以实现基于时间序列数据的回归预测。目前&#xff0c;ARMAX预测未来功能存在困难&#xff0c;本篇文章不予介绍。大致思路…

刘铁猛C#语言教程——表达式详解1

表达式的定义 对以上文档的翻译&#xff1a; 对以上文档的代码解释&#xff1a;表达式是为了实现具体的算法逻辑并得到一个具体的值&#xff0c;而表达式的返回值可以是一个单值&#xff0c;也可以是实例&#xff0c;方法&#xff0c;或者命名空间&#xff1b;例如&#xff1a;…

EAP设备自动化控制系统在设备数采和控制方面的优势

随着科技的不断进步和工业自动化的发展&#xff0c;EAP&#xff08;Equipment Automation Program&#xff09;设备自动化控制系统在各个行业中扮演着越来越重要的角色。作为连接MES&#xff08;Manufacturing Execution System&#xff09;和设备层的沟通桥梁&#xff0c;EAP系…

【python】在matlab中调用python

参考 Matlab调用Python - 知乎 (zhihu.com) 说一下我犯的错误&#xff1a; 1、电脑上有没有python都可以&#xff0c;我以为anaconda里的python不行&#xff0c;又重新下了一个python3.8 实际上导入的时候可以用 pyversion(D:\myDownloads\anaconda\envs\pytorch38\pytho…

vue3 中的监听器

划重点&#xff1a;用户创建的侦听器回调&#xff0c;都会在 Vue 组件更新之前被调用。这意味着你在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态。如果想在侦听器回调中能访问被 Vue 更新之后的 DOM&#xff0c;你需要指明 flush: ‘post’ 选项&#xff1a; 1.watch …

SpringCloudAlibaba微服务实战系列(一)Nacos服务注册发现

SpringCloudAlibaba微服务实战系列&#xff08;一&#xff09;Nacos服务注册发现 实战前先做一个背景了解。 单体架构、SOA和微服务 单体架构&#xff1a;近几年技术的飞速发展&#xff0c;各种各样的服务已经进入到网络化。单体架构发布时只需要打成一个war或jar包发布即可&a…

页面设计—FixedContainer固定容器组件详解

一、组件介绍 可任意拖动位置&#xff0c;脱离文档流布局&#xff0c;生成绝对定位的元素 二、如何使用 1、找到FixedContainer组件&#xff0c;拖放到页面位置。 2、设置是否拖动&#xff0c;设置高级样式&#xff0c;也可以在自定义样式里编写css样式 操作步骤如下&…