当前位置: 首页 > news >正文

多线程(四):线程安全

在开始讲解线程安全之前我们先来回顾一下我们学了那些东西了:

1. 线程和进程的认识

2. Thread 类的基本用法

3. 简单认识线程状态

4. 初见线程安全

上一章结束时看了一眼线程安全问题,本章将针对这个重点讲解。

一个代码在单线程中能够安全执行,但是在多线程中就容易出现错误;其本质原因就是线程在系统中的调度是无序的 / 抢占式执行的。

再看一眼上一章末尾的题,两个线程各执行 5w 次自增操作,最后的结果为什么是一个小于 10w 的随机数。

上节课也画了图:

 线程不安全的原因

我们在这里讨论一下照成线程不安全的原因有哪些?

  1. 多线程的抢占式执行(罪魁祸首)
  2. 多个线程修改同一个变量 【如果是一个线程修改一个变量 => 安全】【多个线程读取一个变量 => 安全】【多个线程修改不同变量 => 安全】
  3. 修改操作不是原子的 
  4. 内存可见性引起的线程不安全
  5. 指令重排序引起的线程不安全

那么我们就开始本章内容的讲解

对于 多线程的抢占式执行多个线程修改同一个变量 这两点不是我们能够改变的,我们就直接跳过,直接看第三条

修改操作不是原子的 

这里说到的原子性,数据库中 事物的原子性 是一个概念, 原子性意味着不可再分,说明每个操作都是最小单位。

例如上述例题: 每次自增操作都不算是最小操作,我们还可以对其进行划分,将一次 add 操作,分为三个小操作:load 、 add 、 save ;

任意某个操作对应单个 cpu 指令就是原子的, 对应多个 cpu 操作就是非原子的。

正是应该这个操作不是原子的,导致了俩个线程的指令排序存在更多的变数

既然我们发现了这个问题了,我们该如何解决呢?

保证操作的原子性

既然它不是原子的,那么我们就可以通过加锁操作让它变成原子性的。

就比如:

我们要上厕所,为了让别人也进来,所以需要锁门,我们就给门 加了个锁,那么上完厕所以后,就解锁,剩下的两个人就继续 抢占式 上厕所。

那么这个锁呢就可以保证 “原子性” 的效果

锁的核心操作就两个,加锁和解锁。 

对于上述的一个锁,当谁抢到了,其他线程就需要等待,也就发生了 阻塞等待,直到拿到锁的线程释放为止。

那么如何对线程进行加锁呢?

加锁 和 解锁

Java提供了关键字:synchronized,Java直接用 synchronized 这个关键字实现加锁过程。

还是上一章中最后一段的线程自增 5w 次的例子:

代码如下

class Count {private int count = 0;public void add() {synchronized (this) {count++;}}public int get() {return count;}
}
public class demo11 {public static void main(String[] args) throws InterruptedException {Count count = new Count();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {count.add();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {count.add();}});t1.start();t2.start();t1.join();t2.join();System.out.println(count.get());}
}

唯一不同的点在于:

我们加了关键字。

 这里给它加了个代码块,这个代码块有啥用呢?

一旦进入 被 synchronized 修饰的代码块时,就出发加锁机制, 一旦离开了这个代码块就会触发解锁机制。

而且我们在 synchronized 后面加了一个(this)这里的 this 就是锁对象

谁调用 this 就是谁,就对谁进行加锁操作。

例如:

 如果两个线程,针对同一个对象进行加锁,就会造成锁竞争(一个拿到锁,另一个线程阻塞等待)。如果两个对象针对不同的锁竞争就不会照成锁竞争。

现在重点来说一下锁括号里面的东西:

() 里的锁对象,可以是写作任意一个Object 对象,但是不能是 内置类型(内置类型就是基本数据类型)。

这括号主要就是为为了告诉大家,多个线程针对同一个对象加锁就会出现锁竞争,如果针对不同的对象加锁,就不会出现锁竞争了,再也没有别的作用

加锁以后,操作就变成原子的了,原来的操作就变成为了:

 那么再次执行的时候就变成为了:

 由于 t1 已经率先lock 了,t2 再次尝试 lock 就会出现阻塞等待的情况。

此时就可以保证 t2 的load 一定是在 t1 save 之后,此时计算的结果就一定是安全的。

加锁的本质其实就是变成串行化。

那么对比 join 方法,join也是实现串行化,join 方法是让两个线程都是实现串行化,而加锁只是让加锁的部分串行,其他部分还是并发执行的。

无论如何,加锁可能会造成阻塞,代码阻塞,对于程序的效率还是会有影响的。

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

我们先来写个 bug 在来说原因。

看代码:

import java.util.Scanner;public class demo12 {public static boolean flag = false;public static void main(String[] args) {Thread t1 = new Thread(() -> {while (!flag) {}});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);flag = scanner.nextBoolean();});t1.start();t2.start();}
}

我们在来运行一遍:

可以看到输入了true 之后代码还在跑,同样可以在 jconsole 里看到线程还在执行,为什么这一段代码还继续执行呢。

这里就涉及到内存可见性了。

我们在执行这段代码的时候,进入到 while 循环, !flag 为真 在这个过程中又发生了两个 原子性的操作, 一个是 load :从内存读取数据到 cpu 寄存器;一个是 cmp (在cpu中可以叫别的名字):比较寄存器内的值是否为 false 。

这两个操作,load 消耗的时间远远高于 cmp 。

读内存虽然比读硬盘 快个几千倍 ; 读寄存器又要比 读内存快个几千倍

这样换算下来 每秒钟就要执行上亿次。

那么这样看下来,编译器发现 load 的开销很大,并且每次的结果都一样,那么编译器就做了一个非常大胆的操作,直接将 load 优化掉了(去掉了),只有第一次执行的 load 真正执行了,后续只循环 cmp 不执行 load 。

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

那么我们就可以通过 让编译器对这个场景暂停优化 :

这里就需要使用另一个关键字: volatile 

该关键字的含义就是:被它修饰的变量,此时编译器就会停止上述的优化。能够保证每次都是从内存上重新读取数据。

volatile关键字的作用主要有如下两个:

  1. 保证内存可见性:基于屏障指令实现,即当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
  2. 保证有序性:禁止指令重排序。编译时 JVM 编译器遵循内存屏障的约束,运行时靠屏障指令组织指令顺序。

volatile不能保证原子性,volatile 使用的场景是:一个线程读,一个线程写的情况,而 synchronized 则适用于多线程写。 

volatile 的这个效果,称为 “保证内存可见性”。

而 synchronized 不确定是否也能保证内存可见性,网上资料 众说纷纭 。

volatile 还有一个效果,禁止指令重排序。

指令重排序

什么是指令重排序?

这也是编译器优化手段的一种,调整了代码的执行顺序,但是前后的逻辑不改变,效率更高。

如果是单线程的实现逻辑,结果并不会改变,但是在多线程中就会产生问题。

举例:

有个学生对象: Student s;

线程: t1  :s = new Student();

线程: t2  :if (s != null)  s.learn();

大体可以分为三个步骤:

1. 申请内存空间

2. 调用构造方法(初始化内存的数据)

3. 把对象的引用赋值给s (内存地址的赋值)

如果是个单线程,此处可以发生指令重排序, 2 和 3 谁先谁后都可以。

t1执行1和3,即将执行2的时候,t2开始执行,t2拿到的就不是一个空的对象,是一个非空的,他就去调用cow的方法,但是实际上,t1还没有初始化,调用方法,会产生bug,所以我们可以在cow对象前加关键字volatile,保证执行顺序。

那么本章的 线程安全 就到这里,下一章继续多线程内容。

相关文章:

多线程(四):线程安全

在开始讲解线程安全之前我们先来回顾一下我们学了那些东西了&#xff1a; 1. 线程和进程的认识 2. Thread 类的基本用法 3. 简单认识线程状态 4. 初见线程安全 上一章结束时看了一眼线程安全问题&#xff0c;本章将针对这个重点讲解。 一个代码在单线程中能够安全执行&am…...

[ROC-RK3568-PC] [Firefly-Android] 10min带你了解Camera的使用

&#x1f347; 博主主页&#xff1a; 【Systemcall小酒屋】&#x1f347; 博主追寻&#xff1a;热衷于用简单的案例讲述复杂的技术&#xff0c;“假传万卷书&#xff0c;真传一案例”&#xff0c;这是林群院士说过的一句话&#xff0c;另外“成就是最好的老师”&#xff0c;技术…...

C++之模拟实现string

文章目录前言一、包含的相关头文件二、构造和析构1.构造函数2.拷贝构造1.传统写法2.现代写法3.赋值运算符重载1.传统写法2.现代写法4.析构函数三、iterator四、modify1.push_back(尾插一个字符&#xff09;2.append&#xff08;尾插一个字符串&#xff09;3.运算符重载1.尾插字…...

SpringBoot实战(十三)集成 Admin

目录一、简介二、搭建 springboot-admin 管理服务1.Maven 依赖2.application.yml3.添加 EnableAdminServer4.启动服务&#xff0c;查看页面三、搭建 springboot-admin-client 客户端服务1.Maven 依赖2.application.yml3.启动服务&#xff0c;查看页面四、搭配 Eureka 使用1.搭建…...

mke2fs命令:建立ext2文件系统

以下内容源于网络资源的学习与整理&#xff0c;如有侵权请告知删除。 使用格式 mke2fs [options] [设备名称] [区块数] options与含义 -c&#xff1a;检查是否有损坏的区块。-F&#xff1a;不管指定的设备为何&#xff0c;强制执行mke2fs。-M&#xff1a;记录最后一次挂入的…...

免费分享一个springboot+vue的办公系统

springbootvue的OA系统项目介绍项目部署项目特点项目展示项目介绍 这是一个采用前后端分离开发的项目&#xff0c;前端采用 Vue 开发、后端采用 SpringBoot Mybatis 开发。 很适合java初学者练手和学习。 前端技术&#xff1a;Vue3.2 Vue-Router Pinia Ant Design Vue 3.X…...

STM32数据搬运工DMA

DMA的概念DMA&#xff0c;全称为&#xff1a;Direct Memory Access&#xff0c;即直接存储器访问。DMA 传输方式无需 CPU 直接控制传输&#xff0c;也没有中断处理方式那样保留现场和恢复现场的过程&#xff0c;通过硬件为 RAM 与 I/O 设备开辟一条直接传送数据的通路&#xff…...

4、操作系统——进程间通信(2)(system V-IPC介绍)

目录 一、system V-IPC常识 1、key和ID 2、文件描述符 3、函数&#xff08;ftok&#xff09; ftok产生IPC对象的健值key&#xff08;类似文件路径&#xff09; 4、例子 5、使用命令查看或删除当前系统中的IPC对象 一、system V-IPC常识 1、key和ID &#xff08;1&#x…...

基于CentOS Stream 9平台搭建Nacos2.0.4集群以及OpenResty反向代理

目录展示Nacos2.0.4集群搭建1. 下载2. 解压3.修改配置3.1分别修改下启动类中JDK路径以及启动大小3.2 分别配置数据源3.3 创建nacos数据库3.4 修改cluster.conf配置3.4.1 复制并修改3.4.2 编辑文件&#xff0c;修改三台主机地址3.4.3 分别放入另外两个nacos的conf目录下:4. 启动…...

老杜MySQL入门基础 第二天

导入演示数据 1、连接MySQL 2、创建"bjpowernode"数据库 create database bjpowernode;3、选择数据库 use bjpowernode4、导入数据 source D&#xff1a;\bjpowernode.sql(文件的路径)1 去除重复记录(把查询结果去除重复记录)(原表数据不会改变) 使用关键字dist…...

Python深度学习实战:人脸关键点(15点)检测pytorch实现

引言 人脸关键点检测即对人类面部若干个点位置进行检测&#xff0c;可以通过这些点的变化来实现许多功能&#xff0c;该技术可以应用到很多领域&#xff0c;例如捕捉人脸的关键点&#xff0c;然后驱动动画人物做相同的面部表情&#xff1b;识别人脸的面部表情&#xff0c;让机…...

linux简单入门

目录Linux简介Linux目录结构Linux文件命令文件处理命令文件查看命令常用文件查看命令Linux的用户和组介绍Linux权限管理Linux简介 Linux&#xff0c;全称GNU/Linux&#xff0c;是一种免费使用和自由传播的类UNIX操作系统&#xff0c;其内核由林纳斯本纳第克特托瓦兹&#xff0…...

给准备面试网络工程师岗位的应届生一些建议

你听完这个故事&#xff0c;应该会有所收获。最近有一个23届毕业的大学生和我聊天&#xff0c;他现在网络工程专业大四&#xff0c;因为今年6、7月份的时候毕业&#xff0c;所以现在面临找工作的问题。不管是现在找一份实习工作&#xff0c;还是毕业后找一份正式工作&#xff0…...

主线程与子线程之间相互通信(HandlerThread)

平时&#xff0c;我们一般都是在子线程中向主线程发送消息&#xff08;要在主线程更新UI&#xff09;&#xff0c;从而完成请求的处理。那么如果需要主线程来向子线程发送消息&#xff0c;希望子线程来完成什么任务。该怎么做&#xff1f;这就是这篇文章将要讨论的内容。 一、…...

13基于双层优化的电动汽车日前-实时两阶段市场竞标

MATLAB代码&#xff1a;基于双层优化的电动汽车日前-实时两阶段市场竞标 关键词&#xff1a;日前-实时市场竞标 电动汽车 双层优化 编程语言&#xff1a;MATLAB平台 参考文献&#xff1a;考虑电动汽车可调度潜力的充电站两阶段市场投标策略_詹祥澎 内容简介&#xff1a;…...

REDIS19_zipList压缩列表详解、快递列表 - QuickList、跳表 - SkipList

文章目录①. 压缩列表 - zipList②. 快递列表 - QuickList③. 跳表 - SkipList①. 压缩列表 - zipList ①. ZipList是一种特殊的"双端链表",由一系列特殊编码的连续内存块组成。可以在任意一端进行压入/弹出操作,并且该操作的时间复杂度为O(1) (oxff:11111111) type…...

JavaScript 基础 - 第3天

文章目录JavaScript 基础 - 第3天笔记数组数组的基本使用定义数组和数组单元数据单元值类型数组长度属性操作数组JavaScript 基础 - 第3天笔记 数组 数组的基本使用 定义数组和数组单元 <script>// 1. 语法&#xff0c;使用 [] 来定义一个空数组// 定义一个空数组let…...

23.3.26总结

康托展开 是一个全排列与自然数的映射关系&#xff0c;康托展开的实质是计算当前序列在所有从小到大的全排列中的顺序&#xff0c;跟其逆序数有关。 例如&#xff1a;对于 1,2,3,4,5 来说&#xff0c;它的康托展开值为 0*4&#xff01;0*3&#xff01;0*2&#xff01;0*1&…...

【Java学习笔记】37.Java 网络编程

Java 网络编程 网络编程是指编写运行在多个设备&#xff08;计算机&#xff09;的程序&#xff0c;这些设备都通过网络连接起来。 java.net 包中 J2SE 的 API 包含有类和接口&#xff0c;它们提供低层次的通信细节。你可以直接使用这些类和接口&#xff0c;来专注于解决问题&…...

Azure OpenAI 官方指南03|DALL-E 的图像生成功能与安全过滤机制

2021年1月&#xff0c;OpenAI 推出 DALL-E。这是 GPT 模型在图像生成方面的人工智能应用。其名称来源于著名画家、艺术家萨尔瓦多 • 达利&#xff08;Dal&#xff09;和机器人总动员&#xff08;Wall-E&#xff09;。DALL-E 图像生成器&#xff0c;能够直接根据文本描述生成多…...

【数据结构】堆

文章目录前言堆的概念及结构堆初始化堆的判空堆的销毁插入数据删除数据堆的数据个数获取堆顶数据用数组创建堆对数组堆排序有关topk问题整体代码展示写在最后前言 &#x1f6a9;前面了解了树&#xff08;-> 传送门 <-&#xff09;的概念后&#xff0c;本章带大家来实现一…...

电脑硬盘文件数据误删除/格式化为什么可以恢复? 怎么恢复?谈谈文件删除与恢复背后的原理

Hello 大家好&#xff0c; 我是元存储~ 主页&#xff1a;元存储的博客_CSDN博客 1. 硬盘数据丢失场景 我们在每天办公还是记录数据的时候&#xff0c;文件存储大多数都是通过硬盘进行存储的&#xff0c;因此&#xff0c;使用多了&#xff0c;各种问题就会出现&#xff0c;比如…...

Gateway服务网关

Spring Cloud Gateway为微服务架构提供一种简单有效的统一的 API 路由管理方式。Gateway网关是所有微服务的统一入口。网关的核心功能特性&#xff1a;请求路由和负载均衡&#xff1a;一切请求都必须先经过gateway&#xff0c;但网关不处理业务&#xff0c;而是根据某种规则&am…...

K8S + GitLab + Jenkins自动化发布项目实践(一)

K8S GitLab Jenkins自动化发布项目实践&#xff08;一&#xff09;发布流程设计安装Docker服务部署Harbor作为镜像仓库部署GitLab作为代码仓库常用Git命令发布流程设计 #mermaid-svg-pe9VmFytb9GmqMvG {font-family:"trebuchet ms",verdana,arial,sans-serif;font-…...

【数据结构篇C++实现】- 堆

文章目录&#x1f680;一、堆的原理精讲⛳&#xff08;一&#xff09;堆的概念⛳&#xff08;二&#xff09;看图识最大堆⛳&#xff08;三&#xff09;详解堆是用数组表示的树&#x1f680;二、堆的向下调整算法&#x1f680;三、堆的向上调整算法&#x1f680;四、将任意一棵…...

C++笔试题

作用域运算符(::)的作用&#xff1a;1.存在具有相同名称的局部变量时&#xff0c;访问全局变量。2.在类之外定义类相关函数。3.访问类的静态变量。4.在多重继承的情况下&#xff0c;如果两个基类中存在相同的变量名&#xff0c;可以使用作用域运算符来进行区分。5.限定成员函数…...

【Python】基本语法

数据类型 通过 print(type(x)) 可以输出 x 的数据类型&#xff0c;type() 函数可以获取数据类型 整数 a 10 print(type(a)) 浮点数 a 0.5 print(type(a)) 字符串 a hello print(type(a)) 获取字符串长度 a hello print(len(a))字符串拼接 a hello b world prin…...

用栈实现队列(图示超详解哦)

全文目录引言用栈实现队列题目介绍思路简述实现栈的部分队列的部分创建队列判断队列是否为空在队列尾入在队列头出访问队头元素释放队列总结引言 在上一篇文章中&#xff0c;我们了解了用两个队列实现栈。在这篇问章中将继续介绍用两个栈实现队列的OJ练习&#xff1a; 用栈实现…...

Spring - Spring 注解相关面试题总结

文章目录01. Spring 配置方式有几种&#xff1f;02. Spring 如何实现基于xml的配置方式&#xff1f;03. Spring 如何实现基于注解的配置&#xff1f;04. Spring 如何基于注解配置bean的作用范围&#xff1f;05. Spring Component, Controller, Repository, Service 注解有何区别…...

【数据结构】实现二叉树的基本操作

目录 1. 二叉树的基本操作 2. 具体实现 2.1 创建BinaryTree类以及简单创建一棵树 2.2 前序遍历 2.3 中序遍历 2.4 后序遍历 2.5 层序遍历 2.6 获取树中节点的个数 2.7 获取叶子节点的个数 2.8 获取第K层节点的个数 2.9 获取二叉树的高度 2.10 检测值为val的元素是否…...

php动态网站开发简答题/百度seo推广怎么收费

测试原理&#xff0c;停掉源端和目标端的OGG&#xff0c; 在源端进行DML操作&#xff0c;然后切换日志。然后开启源端和目标端的OGG。看看是否会同步数据到目标端。另外查看源端的ext进程日志&#xff0c;看里面的提示。 结论&#xff1a; OGG 在停掉后&#xff0c;进行大批量的…...

深圳网站制作与建设公司/免费网络营销软件

####定义AOP&#xff0c;面向领域编程&#xff0c;是在不修改源代码的情况下&#xff0c;通过编译时或者运行时的代码修改来实现改变程序功能的目的。####问题和实现例如如何在c#中实现类似于python的方法装饰器的功能呢&#xff1f;fody这个库&#xff0c;通过修改编译好的dll…...

政府网站站群建设方案/搜索引擎技巧

Spring-Boot-2.0.0-M1版本将默认的数据库连接池从tomcat jdbc pool改为了hikari&#xff0c;这里主要研究下hikari的默认配置 0. 创建Spring Boot项目&#xff0c;选中 Web、MySQL、JDBC 依赖 1. 启动类默认加载了DataSourceAutoConfiguration&#xff0c;默认数据源是HikariD…...

怎么创建网络平台/seo网站推广教程

mysqldump -u用戶名 -p密码 -d 数据库名 表名 > 脚本名; #导出整个数据库结构和数据 mysqldump -h localhost -uroot -p123456 database > dump.sql #导出整个多个数据库结构和数据 mysqldump -h localhost -uroot -p123456 --databases database1 database2> dump.sq…...

体育网站怎样做香功/谷歌推广怎么样

本文与大家分享一些Python编程语言的入门书籍&#xff0c;其中不乏经典。我在这里分享的&#xff0c;大部分是这些书的英文版&#xff0c;如果有中文版的我也加上了。有关书籍的介绍&#xff0c;大部分截取自是官方介绍。Python基础教程&#xff08;Begining Python&#xff09…...

网站建设的开多少税率/seo优化培训课程

问题&#xff1a; 给定一个栈&#xff0c;逆置栈中的内容&#xff0c;要求只能只用栈操作push和pop&#xff0c;不能用数组、栈、队列等做过渡。 基本思路&#xff1a; 用递归&#xff0c;先将原来栈中的元素递归出栈&#xff0c;直至栈为空&#xff0c;然后在每次递归向上步…...