何为线程安全?

要谈及何为线程安全,总得说来,我们可以用一句话来概况:

如果在多线程环境下代码运行结果和我们预期是相符的,即和单线程环境下的运行结果相同,那么我们就称这个程序是线程安全的,反之则不安全,即和预期不符;

为了大家更好地理解这句话,大家可以看一下下面这个例子

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

对于上述代码,我们预期的结果应该是输出10000,因为count经过了10000次的累加,然而事实上结果是....?

7019...??这样一个令人摸不着头脑的数字,甚至每次运行的结果都不一样,这是为什么呢?

原因分析:

实际上,count++的操作是分为三步的

  1. load从内存中读取数据到cpu的寄存器
  2. add把寄存器中的值+1
  3. save把寄存器中的值写回内存中

而由于线程是随机调度的,所以有可能t1刚执行到第1一步,cpu资源就被调度走了,那么count值就不会和预想结果一样,也可能t1和t2同时执行第一步,那么它们读取到的数据都是count = 0,而事实上count应该执行的操作是 + 2,因为这样随机调度的不确定性,就使得这样的代码是线程不安全的!!

线程不安全的原因

1) 根本原因

线程不安全的根本原因就是线程的随机调度,这样的随机带来了很多不确定性,使得线程的执行顺序是不确定的~~

2) 多个线程修改同一个变量

通过例子我们可以发现,t1和t2都在针对count这一个变量进行修改的操作,这样的操作就会引起线程安全问题,为了解决这样的问题,我们就会引入"锁"这样的概念,具体的解释我们会在解决安全问题篇讲述~~

3) 修改操作不是原子的

何为原子的?

原子的即原子性的操作,即这个操作是不可再分的。

我们上文提到了,虽然我们肉眼看起来count++这个操作就是对count进行了一个加法操作,但事实上,count++这个操作是包含了三部分的,所以这个操作并不是一个原子性的操作~~因此引发了线程安全问题

4) 内存可见性问题

在讲述这个原因之前,我们要先引入另外一个例子

import java.util.Scanner;public class Demo22 {public static int flg = 0;public static void main(String[] args) {Thread t1 = new Thread(() -> {while (flg == 0) {}System.out.println("t1线程结束");});Thread t2 = new Thread(() -> {System.out.println("请输入flg的值:");Scanner scan = new Scanner(System.in);flg = scan.nextInt();});t1.start();t2.start();}
}

上述代码我们想实现的结果是,用户输入一个非0的数字后t1线程结束,然而真正的运行结果却无法结束t1线程

原因分析:

造成这样结果的原因就是因为t2修改了这个变量内存,但t1内却没有接收到这个变量的变化,这样的问题我们就称为 "内存可见性" 问题,造成这样的问题主要有以下两个要点:

  1. JVM识别到load加载的flg的值几百万次都一样[while循环的执行速度是很快的,可能一秒几百万次]
  2. load操作的花费开销是很大的,远远超过了其它操作

因此在很多次的执行之后,JVM就会觉得,反之每次结果都一样,那还有什么执行的必要吗??因此JVM就自动地优化了代码,将load操作变成了直接使用寄存器中之前"缓存"的值,而非每次去内存中重新获取,大大降低了花费,因此就造成了即使后面修改了flg的值也无法被t2感知到的结果

5) 指令重排序问题

指令重排序实际上也是编译器优化代码的一种方式,保证逻辑不变的前提下,调整原有代码的执行顺序,提高程序的效率,自然,指令顺序都发生了改变,安全也无法保证

在描述解决这些问题之前,我们先来讲一下"锁"的概念

线程加锁

加锁的关键字

形如上图的由synchronized关键字修饰的代码块就是相当于对一个Object对象加锁,当两个线程竞争同一把锁的时候,就会引发阻塞,一个进程拿到锁之后,另外一个进程就会因为拿不到锁而陷入阻塞状态,这样就不会因为随机的变化而造成线程安全问题

我们举个例子来理解一下~~

比如你想要追求你的crush~~她有对象的时候,就相当于她加上锁了,按理来说,你就不能再追求她了,但是如果她分手了,又回归了单身状态,那就是锁解除啦,然后你就可以追求她了,你们在一起之后,就相当于你竞争到了这把锁,那其它人就不能追求你的crush啦,除非你俩又分手了,她又回归了单身状态,那么别人就可以又来竞争这把锁,此时"男朋友"这个身份就是那把锁~~~

加锁例子

我们来完善一下开头的例子,看下我们加上锁之后的结果

public class Demo21 {public static int count = 0;public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(() -> {for(int i = 0; i < 5000; i++) {synchronized (locker) {      //共同竞争Locker这把锁count++;}}});Thread t2 = new Thread(() -> {for(int i = 0; i < 5000; i++) {synchronized (locker) {count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
}

可以看到,此时结果就是10000了

提示

当对同一个线程多次加同一把锁的时候,是无效的,只会算一把锁,因为锁有可重入性~~

解决线程安全问题

对于问题1)

这个问题是无法解决的,这是系统的底层逻辑

对于问题2)、 3)

想要解决问题2和问题3,就是要对操作加锁,详情请参看锁篇章~~

对于问题4)、5)

想要解决这两个问题,我们要引入另一个关键字 volatile

volatile可以强制关闭JVM的代码优化机制,确保每次循环都要重新从内存中读取数据,虽然这增加了开销,但可以增加代码的准确性~~

同样的例子,我们对flg加上volatile关键字

import java.util.Scanner;public class Demo22 {public static volatile int flg = 0;        //加上volatilepublic static void main(String[] args) {Thread t1 = new Thread(() -> {while (flg == 0) {}System.out.println("t1线程结束");});Thread t2 = new Thread(() -> {System.out.println("请输入flg的值:");Scanner scan = new Scanner(System.in);flg = scan.nextInt();});t1.start();t2.start();Object locker = new Object();synchronized (locker) {}}}

运行则可以发现,t1此时就可以正常结束了~~


❤❤ 觉得博主写的有帮助的话,请点个赞 b( ̄▽ ̄)d ,谢谢~~~ ❤❤

❤❤ 你的喜欢是我更新的最大动力~~~ ❤❤

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

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

相关文章

水印消失术!JavaAI深度学习去水印技术深度剖析

一、飞算JavaAI平台概述1.1 飞算JavaAI定位与技术特色 飞算JavaAI是国内领先的智能化Java开发平台&#xff0c;通过AI技术赋能软件开发全流程&#xff0c;特别针对小程序、Web应用等轻量级开发场景提供*零基础编程→高质量交**的一站式解决方案。其核心优势体现在&#xff1a; …

醋酸钆:医学影像与科技创新中的重要角色

醋酸钆是一种由钆元素和醋酸根离子组成的化合物。钆是稀土金属之一&#xff0c;常常用于医学影像、核磁共振成像&#xff08;MRI&#xff09;以及某些工业应用。醋酸钆作为钆的盐之一&#xff0c;具有许多独特的性质&#xff0c;尤其在医学和科学研究领域表现突出。一、醋酸钆的…

插入排序专栏

插入排序&#xff08;Insertion Sort&#xff09;是一种简单直观的排序算法&#xff0c;其思想源于我们日常生活中整理扑克牌的方式。本文将详细解析插入排序的工作原理&#xff0c;通过 Java 实现代码进行分析&#xff0c;深入探讨其时间复杂度的计算过程&#xff0c;并阐述其…

高效Unicode字符表示:一种创新的词表构建策略分析

在自然语言处理中&#xff0c;处理多语言和特殊字符的表示始终是一项挑战。本文将分析一种创新的词表构建策略&#xff0c;该策略通过数学优化和双token机制&#xff0c;在保持词表紧凑的同时实现了对Unicode字符的全面覆盖。 词表构建的核心逻辑 该策略包含四个关键步骤&#…

python与物联网基础知识

软件准备&#xff1a;软件&#xff1a;thonny-4.0.1-windows-portable(win10,11系统64位)驱动&#xff1a;CP210x_Windows_Drivers固件&#xff1a;esp8266-1m-20220618-v1.19.1.bin物料准备&#xff1a;面包板、开发板、电源线一、安装与调试&#xff1a;1.在软件文件中找到th…

SVN提交服务器拒绝访问的问题

SVN提交服务器拒绝访问的问题 介绍 分析 1.服务器的SVN没有开启 2.服务器的网络端口除了问题没有开放端口 3.客户端的SVN配置除了问题刷新一下数据 4.客户端的SVN重装 找原因 1.初步以为是**防火墙**的问题 2.网络运营商的问题 总结 介绍 SVN相信大家都用过,今天反馈一个比较…

【Linux】库制作与原理

前言 本篇博客我们来认识下库方面的知识 &#x1f493; 个人主页&#xff1a;zkf ⏩ 文章专栏&#xff1a;Linux 若有问题 评论区见&#x1f4dd; &#x1f389;欢迎大家点赞&#x1f44d;收藏⭐文章 目录 1.什么是库 2.静态库 2.1静态库的生成 2.2静态库的使用 3.动态库 …

Android ADB 常用指令全解析

ADB&#xff08;Android Debug Bridge&#xff09;是 Android 开发和测试不可或缺的调试工具&#xff0c;它建立了电脑与 Android 设备之间的通信桥梁&#xff0c;通过命令行指令可实现对设备的全方位控制。掌握 ADB 指令能大幅提升开发效率&#xff0c;解决各类调试难题。本文…

使用 Rust 创建 32 位 DLL 的完整指南

使用 Rust 创建 32 位 DLL 的完整指南 在 Rust 中创建 32 位 DLL 需要特定的工具链配置和编译选项。以下是详细步骤和最佳实践&#xff1a; 环境准备 1. 安装 Rust 工具链 # 安装 Rust curl --proto https --tlsv1.2 -sSf https://sh.rustup.rs | sh# 安装 32 位目标 rustu…

算法基础 第3章 数据结构

1.单调栈 1.什么是单调栈 单调栈&#xff0c;即具有单调性的栈。 实现 #include <iostream> #include <stack> using namespace std; const int N 3e6 10; int a[N], n; void test1() {stack<int> st; // 维护⼀个单调递增的栈for(int i 1; i < n; i…

[机器学习]08-基于逻辑回归模型的鸢尾花数据集分类

使用sklearn的LogisticRegression多分类模型程序代码&#xff1a;import numpy as np from sklearn.linear_model import LogisticRegression import matplotlib.pyplot as plt import matplotlib as mpl from sklearn import datasets from sklearn import preprocessing impo…

【STM32入门教程】stm32简介

一、STM32简介二、ARM三、stm32f103c8t6四、命名规则五、系统结构六、引脚定义七、启动配置一般情况下&#xff0c;都是在flash开始程序&#xff0c;而启动程序也可以进行配置在其他地方启动程序&#xff0c;通过配置boot0和boot1来进行配置八、最小系统电路

SAE J2716多协议网关的硬件架构与实时协议转换机制解析

本文解析符合SAE J2716标准的工业级协议转换设备技术架构&#xff0c;通过拆解其四路双向SENT通道与多总线&#xff08;CANFD/Ethernet/USB&#xff09;的实时交互机制、MicroSD独立日志系统设计及模拟量动态映射方案&#xff0c;为汽车电子与工业通信开发者提供可复用的技术参…

VS2022+QT5.15.2+OCCT7.9.1的开发环境搭建流程

以下是VS2022 QT5.15.2 OCCT7.9.1开发环境搭建的完整流程&#xff1a; 一、安装Visual Studio 2022 下载安装程序 访问VS官网下载Community版安装组件 选择"使用C的桌面开发"工作负载勾选&#xff1a; MSVC v143 - VS 2022 C x64/x86生成工具Windows 10 SDK (建议…

数据库访问模式详解

数据库访问模式详解数据库访问模式是软件架构中数据访问层&#xff08;Data Access Layer&#xff09;设计的核心&#xff0c;它定义了应用程序如何与数据库进行交互的策略和方法。选择合适的访问模式对于系统的性能、可维护性、可扩展性、事务一致性和开发效率至关重要。不同的…

BGE向量算法

一、是什么 什么是BGE向量算法&#xff1f;先说说网上的概念吧。本文不讲解太深的算法知识&#xff0c;主要讲解如何用&#xff01; BGE&#xff08;BAAI General Embedding&#xff09;是北京智源研究院开源的“通用语义向量模型”。一句话&#xff1a;把中文或英文句子变成…

AI数据仓库的核心优势解析

内容概要本文旨在全面解析AI数据仓库的核心优势&#xff0c;为读者提供清晰的框架。文章首先从基础定义出发&#xff0c;探讨其如何高效整合多源数据&#xff0c;并支持人工智能与机器学习应用。随后&#xff0c;将详细阐述处理TB级数据的能力&#xff0c;包括兼容结构化和非结…

具身智能Scaling Law缺失:机器人界的“摩尔定律“何时诞生?

8月9日&#xff0c;在世界机器人大会的演讲台上&#xff0c;宇树科技创始人王兴兴谈论到目前机器人运动控制领域存在的RL Scaling Law问题&#xff0c;他认为现在的机器人在学习一项新的技能时&#xff0c;往往都是需要从头开始研究以及教学。而在未来更加希望的是能够在原有的…

【跨越 6G 安全、防御与智能协作:从APT检测到多模态通信再到AI代理语言革命】

跨越 6G 安全、防御与智能协作&#xff1a;从APT检测到多模态通信再到AI代理语言革命引言单篇总结**2. Integrated Multimodal Sensing and Communication: Challenges, Technologies, and Architectures****3. Why do AI agents communicate in human language?**引言 在迈向…

微前端-解决MicroApp微前端内存泄露问题

前言 之前使用京东微前端框架MicroApp集成10个微前端的页面到AngularJs的后台管理系统中&#xff0c;每个微前端做成一个菜单&#xff0c;一共10个&#xff0c;每次打开都是一个新的微前端&#xff0c;但是发现打开的微前端越多&#xff0c;容易造成内存泄露&#xff0c;下面讲…