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 类的方法,也可以看到类似的操作


· 下一节我们会更加深入多线程,了解到死锁等相关概念

· 毕竟不知后事如何,且听下回分解~~

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

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

相关文章

React前端框架:现代网页开发的基石(附带构建简单任务管理应用案例代码)

&#x1f4dd;个人主页&#x1f339;&#xff1a;一ge科研小菜鸡-CSDN博客 &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; React 是由 Facebook 开发并开源的前端框架&#xff0c;用于构建用户界面。它通过虚拟DOM、高效的渲染机制和组件化的开发模式&am…

在Zetero中调用腾讯云API的输入密钥的问题

也是使用了Translate插件了&#xff0c;但是需要调用腾讯云翻译&#xff0c;一直没成功。 第一步就是&#xff0c;按照这上面方法做&#xff1a;百度、阿里、腾讯、有道各平台翻译API申请教程 之后就是&#xff1a;Zotero PDF translat翻译&#xff1a;申请腾讯翻译接口 主要是…

kelp protocol

道阻且长,行而不辍,未来可期 有很长一段时间我都在互联网到处拾金,but,东拼西凑的,总感觉不踏实,最近在老老实实的看官方文档 & 阅读白皮书 &看合约,挑拣一些重要的部分配上官方的证据,和过路公主or王子分享一下,愿我们早日追赶上公司里那些可望不可及大佬们。…

如何以开源加速AI企业落地,红帽带来新解法

CSDN 看到&#xff0c;生成式 AI 的火爆正在引发计算、开发、交互三大范式全面的升级和转换&#xff0c;全行业或将迎来一次全新的科技变革周期&#xff0c;可能比移动与云计算变革更加剧烈。不过 AI 经历了追求千亿模型效果和芯片、集群硬件的军备竞赛后&#xff0c;如何真正落…

vue的组件使用

1.安装element plus组件库 npm install element-plus --save 2. 3.在页面使用

人保财险(外包)面试分享

前言&#xff1a; 这是本月面的第三家公司&#xff0c;太难了兄弟们&#xff0c;外包都不好找了&#xff0c;临近年底&#xff0c;金九银十已经错过了&#xff0c;金三银四虽然存在&#xff0c;但按照这几年的行情&#xff0c;金九银十和金三银四其实已经是不复存在了&#xf…

Hive 查询各类型专利 top10 申请人及专利申请数

Hive 查询各类型专利 top10 申请人及专利申请数 一、背景 在专利数据处理中&#xff0c;我们常常需要分析不同类型专利下申请人的活跃度。例如&#xff0c;给定一个专利明细表 t_patent_detail&#xff0c;其中包含专利号、专利名称、专利类型、申请时间、授权时间和申请人等…

C++ STL标准模板库详解:深入探索算法、容器与迭代器

C的STL&#xff08;Standard Template Library&#xff0c;即标准模板库&#xff09;是C最重要的组成部分之一&#xff0c;它为开发者提供了一组高效的算法和数据结构&#xff0c;极大地简化了代码的开发。STL主要由三大部分构成&#xff1a;算法、容器和迭代器。以下&#xff…

pytest插件精选:提升测试效率与质量

pytest作为Python生态系统中备受推崇的测试框架&#xff0c;以其简洁、灵活和可扩展性赢得了广泛的认可。通过合理使用pytest的各种插件&#xff0c;可以显著提升测试效率、增强测试的可读性和可维护性。 pytest-sugar&#xff1a;提升测试体验 pytest-sugar是一款增强版的py…

低代码环境中的领域与根实体解析

在现代软件开发中&#xff0c;低代码平台的兴起使得开发者能够更快速地构建应用程序。在这一过程中&#xff0c;理解领域&#xff08;Domain&#xff09;和根实体&#xff08;Root Entity&#xff09;的概念变得尤为重要。这些概念不仅帮助开发者组织和管理复杂系统中的数据和业…

Linux-c TCP服务模型

1、TCP模型&#xff0c;服务端与客户端的搭建时序图 2、TCP模型&#xff0c;在创建阶段和通信阶段&#xff0c;对套接字的理解 2.1、tcp连接阶段 2.2、tcp通信状态 一个服务端与多个客户端的通信状态 TCP与UDP的对比 &#xff08;下图是笔者理解所画&#xff0c;可能也许有错…

卡码网KamaCoder 127. 骑士的攻击

题目来源&#xff1a;127. 骑士的攻击 C题解&#xff08;来源A * 算法精讲 &#xff08;A star算法&#xff09; | 代码随想录&#xff09;&#xff1a;Astar Astar 是一种 广搜的改良版。 有的是 Astar是 dijkstra 的改良版。 其实只是场景不同而已 我们在搜索最短路的时候&…

浅谈语言模型推理框架 vLLM 0.6.0性能优化

在此前的大模型技术实践中&#xff0c;我们介绍了加速并行框架Accelerate、DeepSpeed及Megatron-LM。得益于这些框架的助力&#xff0c;大模型的分布式训练得以化繁为简。 然而&#xff0c;企业又该如何将训练完成的模型实际应用部署&#xff0c;持续优化服务吞吐性能&#xf…

软著补正有哪些类型

软著补正是指在软著申请过程中&#xff0c;版权局在审核软著申请材料时&#xff0c;发现一些部分不符合规定的内容&#xff0c;从而对申请材料提出补正要求&#xff0c;要求申请人修改或补充申请材料。软著补正通常涉及以下几种类型&#xff1a; 一、申请材料填写有误 软件名称…

闯关leetcode——3222. Find the Winning Player in Coin Game

大纲 题目地址内容 解题代码地址 题目 地址 https://leetcode.com/problems/find-the-winning-player-in-coin-game/description/ 内容 You are given two positive integers x and y, denoting the number of coins with values 75 and 10 respectively. Alice and Bob a…

GitLab 中文发行版最新版重点功能解读

GitLab 是一个全球知名的一体化 DevOps 平台&#xff0c;很多人都通过私有化部署 GitLab 来进行源代码托管。极狐GitLab 是 GitLab 在中国的发行版&#xff0c;专门为中国程序员服务。可以一键式部署极狐GitLab。 学习极狐GitLab 的相关资料&#xff1a; 极狐GitLab 官网极狐…

大数据数据存储层MemSQL, HBase与HDFS

以下是对 MemSQL、HBase 和 HDFS 的详细介绍,这些工具在分布式数据存储和处理领域有着重要作用。 1. MemSQL MemSQL(现称为 SingleStore)是一种分布式内存数据库,兼具事务处理(OLTP)和分析处理(OLAP)的能力,专为高性能实时数据处理设计。 1.1 核心特点 内存优先存储…

斗破QT编程入门系列之二:GUI应用程序设计基础:UI文件(四星斗师)

斗破Qt目录&#xff1a; 斗破Qt编程入门系列之前言&#xff1a;认识Qt&#xff1a;Qt的获取与安装&#xff08;四星斗师&#xff09; 斗破QT编程入门系列之一&#xff1a;认识Qt&#xff1a;初步使用&#xff08;四星斗师&#xff09; 斗破QT编程入门系列之二&#xff1a;认识…

《二分查找算法:在有序数组中搜索目标值》

目录 一、问题分析 二、二分查找算法原理 三、代码实现 给定一个 n 个元素有序的&#xff08;升序&#xff09;整型数组 nums 和一个目标值 target&#xff0c;我们要写一个函数来搜索 nums 中的 target&#xff0c;如果目标值存在就返回它的下标&#xff0c;否则返回 -1。 …

【Qt】QTreeView 和 QStandardItemModel的关系

QTreeView 和 QAbstractItemModel&#xff08;通常是其子类&#xff0c;如 QStandardItemModel 或自定义模型&#xff09;是 Qt 框架中的两个关键组件&#xff0c;它们之间存在密切的关系。 关系概述 QTreeView&#xff1a; QTreeView 是一个用于显示和编辑层次数据的视图小部…