Java多线程详解⑤(全程干货!!!)线程安全问题 || 锁 || synchronized

这里是Themberfue
· 在上一节的最后,我们讨论两个线程同时对一个变量累加所产生的现象
· 在这一节中,我们将更加详细地解释这个现象背后发生的原因以及该如何解决这样类似的现象
线程安全问题
public class Demo15 {private 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();// 改为串行执行t1.start();t1.join();t2.start();t2.join();System.out.println(count);}
}
· 我们先回顾上述代码,如果两个线程并发执行逻辑,同时累加 count 变量 100,000 次后,得到的结果是一个随机值,且这个随机值一定小于 100,000
· 如果改为串行执行,就是 t1 执行完后,t2 再度执行,那么 count 的结果就为 100,000
· 为什么会产生这样的现象?
· 我们先从一行代码入手:count++,有的人就会问:这有什么好分析的,这不就是一个count + 1的操作吗?没错,的确是这样
· 众所周知:CPU(中央处理器)执行的是一系列指令,这些指令定义了它需要执行的逻辑操作,这些指令的集合统称为指令集,指令集有两种,一种是..... (再讲就串台了,这是计算机组成原理的知识哦)
· 常见的指令就有逻辑指令,算术指令等,那么,一个 count++ 其实分为三个指令操作,因为它还设计到变量的修改,而不是单纯地加法
· 我们都知道,把大象放进冰箱分三步:把把冰箱门打开,把大象装进去,再把冰箱门关上
· count++ 也分为三步操作:
1. load:把内存中 count 的值,加载到 cpu 寄存器
2. add:把寄存器中的 count 的值 + 1
3. save:把寄存器中的 count 的值保存到内存中
PS:寄存器就是CPU处理日常任务的小工具,用来存放临时信息
· 操作系统对线程的调度的是随机的,所以在执行这三条指令时,可能不是一口气全部执行完毕,而是执行了一半就不执行了,而后又执行了
· 比如执行 指令1 ,后被调度走,调度回来后执行 指令2 指令3
· 比如执行 指令1 指令2 ,后被调度走,调度回来后执行 指令3
· 比如执行 指令1 ,后被调度走,调度回来后执行 指令 2 ,后被调度走,调度回来后执行指令3
· 多线程的随机调度是造成这个bug出现的原因

· 上述为简单模拟了一遍两次 count++ 的大概流程
· 这是最为理想的情况,就是三条指令一次性执行完毕后再去执行下三条指令,但实际情况却不能保证每次发生这种理想的情况


· 上述情况才是经常发生的,也是导致bug的主要原因
· 尽管执行了两次 count++ 操作,但内存中保存的值为1,结果只增值了一次
· 产生上述问题的原因就是线程安全问题
· 根本原因就是操作系统对于线程的调度是随机的,也就是抢占式执行(这个策略在最初诞生多线程任务操作系统时就诞生了,是非常伟大的发明,后世的操作系统,都是这个策略)
· 第二个原因就是多个线程修改同一个变量
如果是一个线程修改一个变量,不会产生上述问题
如果是多个线程修改不同变量,同样的
如果是多个线程不是同时修改同一个变量,同样的
如果是多个线程同时读取一个变量,同样的
· 第三个原因就是修改的操作,不是原子的,如果是 count++ 是一条指令就可以执行完毕,那么认为该操作就是原子的
· 内存可见性问题
· 指令重排序
· 后续再讨论其细节
加锁
· Java中解决线程安全问题的最主要的方案就是给代码块加锁,通过加锁,可以让不是原子的操作,打包成一个原子的操作
· 计算机中的锁操作,和生活中的加锁区别不大,都是互斥,排他。例如:你上厕所,对当前这个厕所间加锁,那么别人就不能进这个厕所间了,你出厕所门时,此时就是解锁
· 通过使用锁,对先前的 count++ 操作就可以将其变为原子的,在加上锁后,count++ 的三个指令就会完整执行完毕后才可能被调度走
· 加锁操作,不是讲线程锁死到CPU上,禁止这个线程被调度走,是禁止其他线程重新加这个锁,避免其他线程的操作,在这个线程执行过程中插队
· 加锁和解锁这个操作本身是操作系统提供的 api,但是很多语言都对其单独进行了封装,大多数的封装风格都是采取这两个函数:
Object.lock();// 执行的代码逻辑Object.unlock();· 但是这样写的弊端也很大,不能保证每次加上锁后都会记住去解锁,所以 Java 提供了一种更为简洁的方式去给某个代码块上锁:
synchronized {// 执行的代码逻辑}· 只要进入了代码块(进入 '{' 后)就会加上锁,只要出了代码块(出去 '}' 后)就会自动解锁
· 但在上述伪代码中,synchronized 的使用并不正确,单纯地加锁,但是此时另一个线程又要加锁,我们要怎么判断这个锁有没有被使用(锁又不止一个)
· 所以应该这样使用:
synchronized (Object) {// 执行的代码逻辑}· 没错,括号里填写的就是用来加锁的对象,这个对象一般称为锁对象,作为锁的作用去使用
· Object 表示一个类,Java 的所有对象都可以作为锁对象:
Object locker = new Object();synchronized (locker) {count++; }
public class Demo16 {private static int count = 0;public static void main(String[] args) throws InterruptedException {// Java中,任何一个对象都可以作为锁Object locker = new Object();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {// 对count的++操作进行上锁// load,add,save操作执行完才会调度走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 的结果恒为 100,000,不可能出现其他值,也就解决了该代码逻辑的线程安全问题
· 通过加锁操作,count++ 操作的三个指令相当于合并成了一个指令,保证每个线程从内存中获取到的值是正确的

· 并不是加上了 synchronized 就一定保证线程安全,得要正确地使用锁,在该使用的时候使用锁,在对的地方使用锁
· 比如,在这个案例中,不是对 count++ 操作操作,而是在 for 循环开始前就上锁:
synchronized (locker) {for (int i = 0; i < 50000; i++) {count++;} }· 这样虽然也是上了锁,但是没什么意义,就相当于等到 for 循环逻辑全部结束后,再解锁,另一个线程停止等待,拿到锁
· 因为这两个线程就这一个相同逻辑,所以这么写就相当于变成了串行执行,不是并发了
· 采取 synchronized 的加锁方式,就可以确保一定会释放锁,不会遇到加锁后但是没有解锁的情况
· 除此之外,synchronized 还可以修饰方法,对这个方法加锁
class Counter {private int count;// 使用 synchronized 对方法进行上锁,就相当于是针对this上锁synchronized public void addCount() {// synchronized (this) {this.count++;// }}// 使用 synchronized 对静态方法进行上锁,就相当于是针对类对象上锁(反射)public synchronized static void func () throws ClassNotFoundException {synchronized (Class.forName("Counter")) {System.out.println("func");}}public synchronized static void fuc () {synchronized (Counter.class) {System.out.println("fuc");}}public int getCount() {return count;}
}public class Demo17 {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.addCount();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.addCount();}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = " + counter.getCount());}
}
· 我们如果查看 StringBuffer 类的方法,也可以看到类似的操作
· 下一节我们会更加深入多线程,了解到死锁等相关概念
· 毕竟不知后事如何,且听下回分解~~

相关文章:
Java多线程详解⑤(全程干货!!!)线程安全问题 || 锁 || synchronized
这里是Themberfue 在上一节的最后,我们讨论两个线程同时对一个变量累加所产生的现象 在这一节中,我们将更加详细地解释这个现象背后发生的原因以及该如何解决这样类似的现象 线程安全问题 public class Demo15 {private static int count 0;public …...
(已解决)Dependency “ ” not found 细谈
剖析原因:依赖在pom文件中引用后,然后ReLoad,此依赖会在你配置的本地仓库里面找,并下载下来,他报not found就是没有找到。 本地仓库的位置: 进一步深究:在本地仓库找的时候,他又会…...
网络编程、UDP、TCP、三次握手、四次挥手
一、初识网络编程 网络编程的概念:在网络通信协议下,不同计算机上运行的程序,进行的数据传输。 应用场景:即时通信、网游对战、金融证券、国际贸易、邮件等等。 不管是什么场景,都是计算机和计算机之间通过网络进行…...
程序员的生活周刊 #7:耐克总裁被裁记
0. 庙宇 这张图来自 Tianshu Liu, 被树木环绕的宝塔庙宇 1. 耐克总裁 耐克最近的总裁 John Donahoe 干了 5 年,终于被裁掉了。 这位总裁即不了解球鞋文化,也没有零售经验,但不懂事的董事会还是聘用它,寄托把耐克从运…...
sparkSQL的UDF,最常用的regeister方式自定义函数和udf注册方式定义UDF函数 (详细讲解)
- UDF:一对一的函数【User Defined Functions】 - substr、split、concat、instr、length、from_unixtime - UDAF:多对一的函数【User Defined Aggregation Functions】 聚合函数 - count、sum、max、min、avg、collect_set/list - UDTF:…...
【Ubuntu20】VSCode Python代码规范工具配置 Pylint + Black + MyPy + isort
常用工具: 在 Ubuntu20 下,有以下常见的 Python 代码工具: 静态分析工具: Pylint 和 Flake8 功能范围:Pylint功能非常强大,能够检查代码质量、潜在错误、代码风格、复杂度等多个方面, 并生成详细的报…...
游戏提示错误:xinput1_3.dll缺失?四种修复错误的xinput1_3.dll文件
在计算机的运行过程中,我们可能会遇到各种各样的问题,其中与“xinput1_3.dll”相关的问题也并不罕见。“xinput1_3.dll”是一个在许多游戏和多媒体应用程序运行过程中可能会用到的动态链接库文件。当我们启动某些游戏时,可能会突然弹出一个错…...
YOLOv11融合IncepitonNeXt[CVPR2024]及相关改进思路
YOLOv11v10v8使用教程: YOLOv11入门到入土使用教程 一、 模块介绍 论文链接:https://arxiv.org/abs/2303.16900 代码链接:https://github.com/sail-sg/inceptionnext 论文速览:受 ViT 长距离建模能力的启发,大核卷积…...
[Web安全 网络安全]-学习文章汇总导航(持续更新中)
文章目录: 一:学习路线资源 1.路线 2.资源 二:工具 三:学习笔记 1.基础阶段 2.进阶阶段 四:好的参考 五:靶场 博主对网络安全很感兴趣,但是不知道如何取学习,自己一步一步…...
Docker Compose部署Rabbitmq(Docker file安装延迟队列)
整个工具的代码都在Gitee或者Github地址内 gitee:solomon-parent: 这个项目主要是总结了工作上遇到的问题以及学习一些框架用于整合例如:rabbitMq、reids、Mqtt、S3协议的文件服务器、mongodb github:GitHub - ZeroNing/solomon-parent: 这个项目主要是…...
SpringBoot+FileBeat+ELK8.x版本收集日志
一、准备环境 1、ElasticSearch:8.1.0 2、FileBeat:8.1.0 3、Kibana:8.1.0 4、logstach:8.1.0 本次统一版本:8.1.0,4个组件,划分目录,保持版本一致。 说明:elasticsearch和kib…...
本地模型导入ollama
文章目录 Modelfile模板导入到 ollama Modelfile模板 在本地模型目录下创建 Modelfile FROM ./qwen2.5-7b-instruct-q4_k_m.gguf# 设定温度参数为1 [更高的更具有创新性,更低的更富有连贯性] PARAMETER temperature 1 # 将上下文窗口大小设置为4096,这…...
scala Map训练
Map实训内容: 1.创建一个可变Map,用于存储图书馆中的书籍信息(键为书籍编号,值为包含书籍名称、作者、库存数量的元组),初始化为包含几本你喜欢的书籍信息。 2.使用 操作符添加两本新的书籍到图书馆集合中。 3.根据书籍编号查询某一本特定的书籍信息&…...
WorkFlow源码剖析——Communicator之TCPServer(下)
WorkFlow源码剖析——Communicator之TCPServer(下) 前言 系列链接如下: WorkFlow源码剖析——GO-Task 源码分析 WorkFlow源码剖析——Communicator之TCPServer(上) WorkFlow源码剖析——Communicator之TCPServer&…...
数据结构与算法分析:专题内容——动态规划2之例题讲解(代码详解+万字长文+算法导论+力扣题)
一、最长公共子序列 在生物应用中,经常需要比较两个(或多个)不同生物体的 DNA。一个 DNA 串由一串称为碱基(base)的分子组成,碱基有腺嘌呤、鸟嘌呤、胞嘧啶和胸腺嘧啶 4 种类型。我们用英文单词首字母表示 4 种碱基,这样就可以将一个 DNA 串…...
【Qt】QTreeView 和 QStandardItemModel的关系
QTreeView 和 QAbstractItemModel(通常是其子类,如 QStandardItemModel 或自定义模型)是 Qt 框架中的两个关键组件,它们之间存在密切的关系。 关系概述 QTreeView: QTreeView 是一个用于显示和编辑层次数据的视图小部…...
containerd配置私有仓库registry
机器ip端口regtisry192.168.0.725000k8s-*-------k8s集群 1、镜像上传 rootadmin:~# docker push 192.168.0.72:5000/nginx:1.26.1-alpine The push refers to repository [192.168.0.72:5000/nginx] 6961f0b8531c: Pushed 3112cd521249: Pushed d3f50ce9b5b5: Pushed 9efaf2eb…...
深入解析语音识别中的关键技术:GMM、HMM、DNN和语言模型
目录 一、高斯混合模型(GMM)与期望最大化(EM)算法二、隐马尔可夫模型(HMM)三、深度神经网络(DNN)四、语言模型(LM)五、ASR系统的整体工作流程结论 在现代语音…...
C++循环引用
C循环引用指的是两个或多个类之间互相引用对方,形成一个循环的引用关系。 循环引用的问题: 编译错误:编译器在编译过程中会按照包含关系依次编译每个文件,当编译ClassA时,它会尝试包含ClassB.h文件,而…...
dayseven-因果分析-图模型与结构因果模型
在数学上,“图”(graph)是顶点(vertex,也可以称为节点)和边(edge)的集合,表示为图G(V,E),其中V是节点的集合,E是边的集合,图中的节点之间通过边相连(也可以不相连&…...
[特殊字符] 智能合约中的数据是如何在区块链中保持一致的?
🧠 智能合约中的数据是如何在区块链中保持一致的? 为什么所有区块链节点都能得出相同结果?合约调用这么复杂,状态真能保持一致吗?本篇带你从底层视角理解“状态一致性”的真相。 一、智能合约的数据存储在哪里…...
rknn优化教程(二)
文章目录 1. 前述2. 三方库的封装2.1 xrepo中的库2.2 xrepo之外的库2.2.1 opencv2.2.2 rknnrt2.2.3 spdlog 3. rknn_engine库 1. 前述 OK,开始写第二篇的内容了。这篇博客主要能写一下: 如何给一些三方库按照xmake方式进行封装,供调用如何按…...
Java如何权衡是使用无序的数组还是有序的数组
在 Java 中,选择有序数组还是无序数组取决于具体场景的性能需求与操作特点。以下是关键权衡因素及决策指南: ⚖️ 核心权衡维度 维度有序数组无序数组查询性能二分查找 O(log n) ✅线性扫描 O(n) ❌插入/删除需移位维护顺序 O(n) ❌直接操作尾部 O(1) ✅内存开销与无序数组相…...
CocosCreator 之 JavaScript/TypeScript和Java的相互交互
引擎版本: 3.8.1 语言: JavaScript/TypeScript、C、Java 环境:Window 参考:Java原生反射机制 您好,我是鹤九日! 回顾 在上篇文章中:CocosCreator Android项目接入UnityAds 广告SDK。 我们简单讲…...
BCS 2025|百度副总裁陈洋:智能体在安全领域的应用实践
6月5日,2025全球数字经济大会数字安全主论坛暨北京网络安全大会在国家会议中心隆重开幕。百度副总裁陈洋受邀出席,并作《智能体在安全领域的应用实践》主题演讲,分享了在智能体在安全领域的突破性实践。他指出,百度通过将安全能力…...
CRMEB 框架中 PHP 上传扩展开发:涵盖本地上传及阿里云 OSS、腾讯云 COS、七牛云
目前已有本地上传、阿里云OSS上传、腾讯云COS上传、七牛云上传扩展 扩展入口文件 文件目录 crmeb\services\upload\Upload.php namespace crmeb\services\upload;use crmeb\basic\BaseManager; use think\facade\Config;/*** Class Upload* package crmeb\services\upload* …...
【HarmonyOS 5 开发速记】如何获取用户信息(头像/昵称/手机号)
1.获取 authorizationCode: 2.利用 authorizationCode 获取 accessToken:文档中心 3.获取手机:文档中心 4.获取昵称头像:文档中心 首先创建 request 若要获取手机号,scope必填 phone,permissions 必填 …...
面向无人机海岸带生态系统监测的语义分割基准数据集
描述:海岸带生态系统的监测是维护生态平衡和可持续发展的重要任务。语义分割技术在遥感影像中的应用为海岸带生态系统的精准监测提供了有效手段。然而,目前该领域仍面临一个挑战,即缺乏公开的专门面向海岸带生态系统的语义分割基准数据集。受…...
计算机基础知识解析:从应用到架构的全面拆解
目录 前言 1、 计算机的应用领域:无处不在的数字助手 2、 计算机的进化史:从算盘到量子计算 3、计算机的分类:不止 “台式机和笔记本” 4、计算机的组件:硬件与软件的协同 4.1 硬件:五大核心部件 4.2 软件&#…...
day36-多路IO复用
一、基本概念 (服务器多客户端模型) 定义:单线程或单进程同时监测若干个文件描述符是否可以执行IO操作的能力 作用:应用程序通常需要处理来自多条事件流中的事件,比如我现在用的电脑,需要同时处理键盘鼠标…...
