什么是线程安全?如何保证线程安全?

目录

一、引入线程安全 👇

二、 线程安全👇

1、线程安全概念 🔍

2、线程不安全的原因 🔍

抢占式执行(罪魁祸首,万恶之源)导致了线程之间的调度是“随机的”

多个线程修改同一个变量 

 修改操作,不是原子的(不可分割的最小单位) 

内存可见性,引起的线程不安全 

指令重排序,引起的线程不安全

三、解决之前的线程不安全问题👇

1、synchronized 关键字-监视器锁monitor lock  🔍

1)synchronized 的特性 

(1) 互斥

(2)刷新内存

(3) 可重入

2)synchronized 使用示例 

1) 直接修饰普通方法:

 2) 修饰静态方法:

3) 修饰代码块: 明确指定锁哪个对象.

2、volatile 关键字 (保证内存可见性) 🔍

 1)引入volatile

2)volatile不保证原子性

💡 总结:


一、引入线程安全 👇

执行以下代码:

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

可以观察代码,我们对两个线程分别累加50000次,结果应该为100000,但是大家看运行结果并非如此,而且可以发现,每次运行的结果都不一样,这是什么原因呢? 

答案就是涉及到了线程安全问题

 

 

本质原因:线程在系统中的调度是无序的/随机的(抢占式执行的) 


二、 线程安全👇

1、线程安全概念 🔍

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

2、线程不安全的原因 🔍

  • 抢占式执行(罪魁祸首,万恶之源)导致了线程之间的调度是“随机的”

  • 多个线程修改同一个变量 

一个线程修改同一个变量=>安全

多个线程读取同一个变量=>安全

多个线程修改不同变量=> 安全 

  •  修改操作,不是原子的(不可分割的最小单位) 

什么是原子性🐶

我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。

那我们应该如何解决这个问题呢?🐶

是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进 不来了。这样就保证了这段代码的原子性了。 有时也把这个现象叫做同步互斥,表示操作是互相排斥的。

一条 java 语句不一定是原子的,也不一定只是一条指令 比如刚才我们看到的 n++,其实是由三步操作组成的: 1. 从内存把数据读到 CPU 2. 进行数据更新 3. 把数据写回到 CPU

不保证原子性会给多线程带来什么问题🐶

如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。 这点也和线程的抢占式调度密切相关. 如果线程不是 "抢占" 的, 就算没有原子性, 也问题不大.

  • 内存可见性,引起的线程不安全 

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

  • 指令重排序,引起的线程不安全


三、解决之前的线程不安全问题👇

1、synchronized 关键字-监视器锁monitor lock  🔍

1)对文章初始代码进行修改:既可以保证 ++ 操作就是原子的,不受影响啦

可以将{ }视为厕所,进表示加锁,出表示解锁

void increase() {//锁有俩个核心操作,加锁和解锁//进入该代码块就会触发加锁,出了代码块,就会触发解锁synchronized (this) {count++;}}

 注意:此处的this指的就是counter对象

因此,在上述代码中,两个线程实在竞争同一个锁对象,就会产生锁竞争。

再执行上述代码:就是100000

疑惑为啥以上操作为什么可以解决线程安全问题呢? 🐶

1)synchronized 的特性 

(1) 互斥

      synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到 同一个对象 synchronized 就会阻塞等待.

进入 synchronized 修饰的代码块, 相当于 加锁

退出 synchronized 修饰的代码块, 相当于 解锁 

理解 "阻塞等待". 针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝 试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的 线程, 再来获取到这个锁.

注意: 上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 "唤醒". 这 也就是操作系统线程调度的一部分工作.

假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能 获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则. 

(2)刷新内存

synchronized 的工作过程: 

  1. 获得互斥锁
  2. 从主内存拷贝变量的最新副本到工作的内存
  3. 执行代码
  4. 将更改后的共享变量的值刷新到主内存
  5. 释放互斥锁 所以 synchronized 也能保证内存可见性. 
(3) 可重入

synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题; 

理解 "把自己锁死" :一个线程没有释放锁, 然后又尝试再次加锁. 

按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第 二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无 法进行解锁操作. 这时候就会死锁

当然,Java 中的 synchronized 是 可重入锁, 因此没有上面的问题

 示例:

static class Counter {public int count = 0;synchronized void increase() {count++;}synchronized void increase2() {increase();}
}

increase 和 increase2 两个方法都加了 synchronized,

此处的 synchronized 都是针对 this 当前对象加锁的.

在调用 increase2 的时候, 先加了一次锁, 执行到 increase 的时候, 又加了一次锁. (上个锁还没释 放, 相当于连续加两次锁)

这个代码是完全没问题的. 因为 synchronized 是可重入锁.

注意:在可重入锁的内部, 包含了 "线程持有者" 和 "计数器" 两个信息.

如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取 到锁, 并让计数器自增.

解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到) 

2)synchronized 使用示例 

 synchronized 本质上要修改指定对象的 "对象头". 从使用角度来看, synchronized 也势必要搭配一个具 体的对象来使用.

1) 直接修饰普通方法:

锁的 SynchronizedDemo 对象🐶

public class SynchronizedDemo {public synchronized void methond() {}
}
 2) 修饰静态方法:

锁的 SynchronizedDemo 类的对象🐶

public class SynchronizedDemo {public synchronized static void method() {}
}
3) 修饰代码块: 明确指定锁哪个对象.

锁当前对象🐶

public class SynchronizedDemo {public void method() {synchronized (this) {}}
}

锁类对象🐶

类对象是啥:Counter.class

.java源代码文件,javac =>.class(二进制字节码文件),JVM就可以执行.class文件了,类对象就可以表示这个.class文件的内容~~(描述了类的方方面面的详细信息,比如诶的名字,类的属性,类的方法,)

public class SynchronizedDemo {public void method() {synchronized (SynchronizedDemo.class) {}}
}

2、volatile 关键字 (保证内存可见性)🔍

 1)引入volatile

所谓内存可见性,就是多线程环境下,编译器对于代码优化,产生了误判,从而引起了bug,进一步导致了代码的bug

因此我们可以加上 volatile(让编译器对这个场景暂停优化) , 强制读写内存. 速度是慢了, 但是数据变的更准确了.每次都是从内存中重新读取数据

观察以下代码:

package threading;import java.util.Scanner;public class ThreadDemo24 {public static int flag = 0;public static void main(String[] args) {Thread t1 = new Thread(() -> {while (flag == 0) {}System.out.println("循环结束,t1结束");});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("请输入一个整数");//t2通过控制台输入一个整数,yidanyonghushurule非0的值,此时t1的循环就会立即结束,从而t1线程就会退出flag = scanner.nextInt();});t1.start();t2.start();}
}

 我们预期的效果应该是输入一个非零的数,线程t1就会停止,但实际上仍然在执行,处在RUNNABLE状态

 为什么会出现以上问题呢?内存可见性的锅!!!

让我们分析一下代码:

直接访问工作内存(实际是 CPU 的寄存器或者 CPU 的缓存), 速度 非常快, 但是可能出现数据不一致的情况.

 

volatile public static int flag = 0;

加上volatile关键字,此时编译器就可以保证每次都是重新从内存读取flag变量的值,

此时t2修改flag,t1就可以立即感知到了,t1就可以正确退了

2)volatile不保证原子性

这个是最初的演示线程安全的代码.

给 increase 方法去掉 synchronized

给 count 加上 volatile 关键字.

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

此时可以看到, 最终 count 的值仍然无法保证是 100000 

💡 总结:
  • volatile不保证原子性
  • volatile也能禁止指令重排序
  • volatile 适用一个线程读,一个线程写的情况
  • synchronized则是多个线程写 
  • volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性(也能保证内存可见性), volatile 保证的是内存可见 性
static class Counter {public int flag = 0;
}
public static void main(String[] args) {Counter counter = new Counter();Thread t1 = new Thread(() -> {while (true) {synchronized (counter) {if (counter.flag != 0) {break;}}// do nothing}System.out.println("循环结束!");});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("输入一个整数:");counter.flag = scanner.nextInt();});t1.start();t2.start();
}

上面代码:

去掉 flag 的 volatile

给 t1 的循环内部加上 synchronized, 并借助 counter 对象加锁. 

运行结果是可以正常结束的

因此 synchronized是可以保证内存可见性的

 

 补充: 指令重排序,也是编译器优化的策略,调整了代码的执行顺序,让程序更高效,前提也是保证整体逻辑不变

 💡 总结:(面试题)

线程不安全的原因:

【根本原因】==操作系统上的线程是“抢占式执行”的,线程调度是随机的,==这是线程不安全的一个主要原因。随机调度会导致在多线程环境下,程序的执行顺序不确定,程序员必须确保无论哪种执行顺序,代码都能正常运行。
【代码结构】共享资源:多个线程同时访问并修改共享的数据或资源。当多个线程同时访问和修改共享资源时容易引发竞态条件和数据不一致的问题。
①一个线程修改一个变量是安全的
②多个线程修改一个变量是不安全的
③多个线程修改不同变量是安全的
【直接原因】多线程操作不是“原子的”。多线程操作中的原子性指的是一个操作是不可中断的,要么全部执行完成,要么都不执行,不能被其他线程干扰。这对于并发编程非常重要,因为如果一个操作在执行过程中被中断,可能导致数据不一致或者其他意外情况发生。(在上述多线程操作中,count++操作不是“原子的”,而是由多个CPU指令组成的,一个线程执行这些指令时,可能会在执行过程中被抢占,从而给其他线程“可乘之机”。要保证原子性操作,每个CPU指令都应该是“原子的”,即要么完全执行,要么完全不执行。)
内存可见性问题:在多线程环境下调用不可重入的函数(即不支持多线程调用的函数),可能导致数据混乱或程序崩溃。
指令重排序问题:在多线程环境下,由于编译器或处理器对指令进行重排序优化,可能导致预期之外的程序行为。

 

线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。
线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。


                        

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

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

相关文章

ESP8266实现获取天气情况

利用太极创客提供的ESP8266 心知天气库获取天气情况并显示 心知天气库地址&#xff1a; ESP8266-心知天气: 本库主要功能为使用ESP8266物联网开发板通过心知天气 API 获取天气等信息。 clone到本地: git clone https://gitee.com/taijichuangke/ESP8266-Seniverse.git 安装该…

跟着Kimi学习结构化提示词:19套内置提示词都在这里了!

大家好&#xff0c;我是木易&#xff0c;一个持续关注AI领域的互联网技术产品经理&#xff0c;国内Top2本科&#xff0c;美国Top10 CS研究生&#xff0c;MBA。我坚信AI是普通人变强的“外挂”&#xff0c;所以创建了“AI信息Gap”这个公众号&#xff0c;专注于分享AI全维度知识…

C++ Primer Plus第十六章复习题

1、考虑下面的 类声明 class RQ1 { private:char * st; public:RQ1(){st new char [1]; strcpy(st,"");}RQ1(const RQ1 & rq){st new char [strlen(rq.st)1]; strcpy(st,rq.st);}~RQ1(){delete [] st};RQ & OPERATOR (cosnt RQ &rq); }; 将它转换为使…

【笔记】树(Tree)

一、树的基本概念 1、树的简介 之前我们都是在谈论一对一的线性数据结构&#xff0c;可现实中也有很多一对多的情况需要处理&#xff0c;所以我们就需要一种能实现一对多的数据结构--“树”。 2、树的定义 树&#xff08;Tree&#xff09;是一种非线性的数据结构&#xff0…

作物水文模型AquaCrop---用于评估作物对水的需求、灌溉计划和管理策略

AquaCrop是由世界粮食及农业组织&#xff08;FAO&#xff09;开发的一个先进模型&#xff0c;旨在研究和优化农作物的水分生产效率。这个模型在全球范围内被广泛应用于农业水管理&#xff0c;特别是在制定农作物灌溉计划和应对水资源限制方面显示出其强大的实用性。AquaCrop 不…

Python知识点复习

文章目录 Input & OutputVariables & Data typesPython字符串重复&#xff08;字符串乘法&#xff09;字符串和数字连接在一起print时&#xff0c;要强制类型转换int为str用input()得到的用户输入&#xff0c;是str类型&#xff0c;如果要以int形式计算的话&#xff0c…

SkyWalking 介绍及部署

1、SkyWalking简介2、SkyWalking的搭建 2.1 部署Elasticsearch2.2 部署SkyWalking-Server2.3 部署SkyWalking-UI3、应用接入 3.1 jar包部署方式3.2 dockerfile方式3.3 DockerFile示例4、SkyWalking UI 界面说明 4.1 仪表盘 4.1.1 APM &#xff08;1&#xff09;全局维度&#x…

UBUNTU22.04无法安装nvidia-driver-550 依赖于 nvidia-dkms-550 (<= 550.54.15-1)

类似的报错信息&#xff0c;就是卡在了nvidia-dkms-550无法安装 Loading new nvidia-550.40.07 DKMS files… Building for 6.5.0-15-generic Building for architecture x86_64 Building initial module for 6.5.0-15-generic ERROR: Cannot create report: [Errno 17] File e…

【机器学习】在电子商务(淘*拼*京*—>抖)的应用分析

机器学习与大模型&#xff1a;电子商务的新引擎 一、电子商务的变革与挑战二、机器学习与大模型的崛起三、机器学习与大模型在电子商务中的应用实践个性化推荐精准营销智能客服库存管理与商品定价 四、总结与展望 随着互联网的飞速发展&#xff0c;电子商务已经成为我们生活中不…

【三剑客和正则表达式】

文章目录 学习目标一、什么是三剑客1.三剑客grep2.三剑客sed3.三剑客awk4.正则过滤例子15.正则过滤例子2 总结 学习目标 1.学会使用 grep 2.学会使用 sed 3.学会使用 awk 4.学会使用正则表达式一、什么是三剑客 正则三剑客&#xff1a;grep sed awk 1.三剑客grep # 擅长过滤…

【深度学习】YOLOv8训练,交通灯目标检测

文章目录 一、数据处理二、环境三、训练 一、数据处理 import traceback import xml.etree.ElementTree as ET import os import shutil import random import cv2 import numpy as np from tqdm import tqdmdef convert_annotation_to_list(xml_filepath, size_width, size_he…

海山数据库(He3DB)代理ProxySQL使用详解:(二)功能实测

读写分离实测 ProxySQL官方demo演示了三种读写分离的方式&#xff1a;使用不同的端口进行读写分离、使用正则表达式进行通用的读写分离、使用正则和digest进行更智能的读写分离。最后一种是针对特定业务进行的优化调整&#xff0c;也可将其归结为第二种方式&#xff0c;下边分…

MySQL备份与日志练习

1、创建对mysql数据库test1的定时备份任务&#xff0c;频率是每周一的2点 create database test1;crond -e0 2 * * 1 mysqldump -u root -pAdmin123 --databases test1 > /opt/test1.sql2、test1中有t1、t2、t3三张表&#xff0c;要求只备份t2这张表 mysqldump -u root -pA…

Python 机器学习 基础 之 数据表示与特征工程 【单变量非线性变换 / 自动化特征选择/利用专家知识】的简单说明

Python 机器学习 基础 之 数据表示与特征工程 【单变量非线性变换 / 自动化特征选择/利用专家知识】的简单说明 目录 Python 机器学习 基础 之 数据表示与特征工程 【单变量非线性变换 / 自动化特征选择/利用专家知识】的简单说明 一、简单介绍 二、单变量非线性变换 三、自…

知识图谱数据预处理笔记

知识图谱数据预处理笔记 0. 引言1. 笔记1-1. \的转义1-2. 特殊符号的清理1-3. 检查结尾是否正常1-4. 检查<>是否存在1-5. 两端空格的清理1-6. 检查object内容长时是否以<开始 0. 引言 最近学习知识图谱&#xff0c;发现数据有很多问题&#xff0c;这篇笔记记录遇到的…

软件设计师备考笔记(九):数据库技术基础

文章目录 一、基本概念二、数据模型&#xff08;一&#xff09;基本概念&#xff08;二&#xff09;E-R模型&#xff08;三&#xff09;数据模型 三、关系代数&#xff08;一&#xff09;关系数据库的基本概念&#xff08;二&#xff09;五种基本的关系代数运算&#xff08;三&…

bugku 网络安全事件应急响应

开启靶场&#xff1a; 开始实验&#xff1a; 使用Xshell登录服务器&#xff0c;账号及密码如上图。 1、提交攻击者的IP地址 WP: 找到服务器日志路径&#xff0c;通常是在/var/log/&#xff0c;使用cd /var/log/&#xff0c;ls查看此路径下的文件. 找到nginx文件夹。 进入ng…

【Jenkins】Centos7安装Jenkins(环境:JDK11,tomcat9,maven3.8)

目录 Jenkins部署环境Maven安装1.上传安装包2.解压3.配置Maven环境变量4.使配置文件立即生效5.校验Maven安装6.Maven配置阿里云仓库7.Maven配置依赖下载位置 Git安装安装监测安装 JDK17安装1.查看旧版本JDK2.卸载旧版本JDK3.查看是否卸载干净4.创建java目录5.下载JDK11安装包6.…

Excel中Lookup函数

#Excel查找函数最常用的是Vlookup&#xff0c;而且是经常用其精确查找。Lookup函数的强大之处在于其“二分法”的原理。 LOOKUP&#xff08;查找值&#xff0c;查找区域&#xff08;Vector/Array&#xff09;&#xff0c;[返回结果区域]&#xff09; 为什么查找区域必须升序/…

【UE HTTP】“BlueprintHTTP Server - A Web Server for Unreal Engine”插件使用记录

1. 在商城中下载“BlueprintHTTP Server - A Web Server for Unreal Engine”插件 该插件的主要功能有如下3点&#xff1a; &#xff08;1&#xff09;监听客户端请求。 &#xff08;2&#xff09;可以将文件直接从Unreal Engine应用程序提供到Web。 &#xff08;3&#xff…