线程安全-搞清synchronized的真面目

       多线程编程中,最难的地方,也是最重要的一个地方,还是一个最容易出错的地方,更是一个特别爱考的地方,就是线程安全问题

万恶之源,罪魁祸首,多线程的抢占式执行,带来的随机性.
如果没有多线程,此时程序代码执行顺序就是固定的.(只有一条路)﹒代码顺序固定,程序的结果就是固定的.[单线程的情况下,只需要理清楚这一条路即可)
如果有了多线程,此时抢占式执行下,代码执行的顺序,会出现更多的变数。代码执行顺序的可能性就从一种情况变成了无数种情况。
所以就需要保证这无数种线程调度顺序的情况下,代码的执行结果都是正确的。
只要有一种情况下,代码结果不正确,就都视为是有bug,线程不安全。

目录

线程安全

原因

synchronized

synchronized使用方法

1.修饰方法

2.修饰代码块             

3.可重入

4.其他的锁

5.Java标准库中的线程安全类

死锁

死锁的三种典型情况

1.一个线程一把锁

2.两个线程两把锁

3.多个线程多把锁

 死锁的四个必要条件

1.互斥使用

2.不可抢占

3.请求和保持

4.循环等待

如何避免死锁

内存可见性

​编辑

volatile

wait notify


线程安全

class Counter{public int count = 0;public void add(){count++;}
}public class demo1 {public static void main(String[] args) {Counter counter = new Counter();Thread t1 = new Thread(() -> {for(int i = 0 ; i < 50000 ; i++){counter.add();}});Thread t2 = new Thread(() -> {for(int i = 0 ; i < 50000 ; i++){counter.add();}});t1.start();t2.start();try {t1.join();t2.join();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("coount=" + counter.count);}
}count=59005进程已结束,退出代码0coount=75148进程已结束,退出代码0count=67437进程已结束,退出代码0

我们先来看到这样一个代码:

两个线程各自自增5w次,一共自增10w次,预期结果count是10w,但是实际结果并不是10w,而且每一次都不一样,这个就称为bug。

为什么会出现这样的情况?

count++;

对于count++这个操作本质上要分为三步:

1.把内存中的值,读取到CPU的寄存器中去  load

2.把CPU寄存器里的数值进行+1运算            add

3.把得到的结果写到内存中去                        save

如果是两个线程并发的执行count++,此时就相当于两组load,add,save进行执行,此时不同的线程调度顺序就可能会产生一些结果上的差异~

 但是那么多种情况,只有这种情况才是我们所需的正确的情况(t1 t2可以交换)

 下面这种情况就是一个不正确的,类似于事务中的读到了一个脏数据。t1读到的是一个t2还没来得及提交的脏数据,于是就出现了脏读问题~

此处讲的多线程,和前面的并发事务,本质上都是“并发编程”问题,并发处理事务,底层也是基于多线程这样的方式来实现的 。

一个线程是完成一个任务,要做一些工作,你这个工作是可以分解成一个一个的小步骤的,每一个小步骤就是一个指令。由于线程的抢占式执行,导致当前执行到任意一个指令的时候,线程都可能被调度走CPU让别的线程来执行。

当前这个代码,是否可能结果正好是10w呢?是有可能的,只是概率非常小,假设两个线程的每次调度顺序都是先t1再t2或者先t2再t1,那么还是有可能的~

同时也有可能最后的结果小于5w,可能t1先加载,t2连续执行三次,最后的结果count只加1。

原因

到底是什么样的情况会出现线程安全问题?

1.[根本原因] 抢占式执行,随机调度

2.代码结构:多个线程同时修改一个变量(注意,这里说的是修改,也就是写)

        一个线程修改一个变量,没事

        多个线程读取一个变量,没事

        多个线程修改多个不同的变量,也没事

3.原子性:如果修改操作是原子的,那么不会有事

   但是如果是非原子的,出现问题的概率就非常高了

count++可以拆分成 load add save 三个操作

我们需要通过操作把这个非原子的操作变成原子的:加锁

4.内存可见性问题

5.指令重排序(本质上是编译器优化出bug了)

以上分析出的是五个典型的原因,不是全部

一个代码究竟是线程安全还是不安全,都得具体问题 具体分析

如果一个代码踩中了上面的原因,也可能线程安全
如果一个代码没踩中上面的原因,也可能线程不安全.......

结合原因,结合需求,具体问题具体分析.
最终抓住的原则:多线程运行代码,不出bug,就是安全的!!!

如何从原子性入手,来解决线程安全问题呢?

synchronized

这是一个关键字,表示加锁

 加了synchronized之后,进入方法就会加锁,出了方法就会解锁

如果两个线程同时尝试加锁,此时一个能获取成功,另一个只能阻塞等待(BLOCKED),一直阻塞到刚才的线程释放锁(解锁),当前线程才能加锁成功。

引出之前介绍的线程的几种状态之一:BLOCKED 等待另一个线程解锁的状态

加锁之后,代码执行速度一定是大打折扣的,但是仍然是比单线程要快。

刚刚的例子中,加锁只是针对了count++加锁了,但是除了count++之外,还有for循环的代码,for循环是可以并行的,只是count++串行了。一个任务中,一部分并发,一部分串行,仍然是比所有的代码串行要快~

synchronized使用方法

1.修饰方法

1)修饰普通方法        修饰普通方法,锁对象就是this

2)修饰静态方法        修饰静态方法,锁对象就是类对象(Counter.class)

2.修饰代码块             

修饰代码块,显示\手动指定锁对象

所以加锁是要明确执行对哪个对象加锁的

如果两个线程针对同一个对象加锁,会产生阻塞等待(锁竞争、锁冲突)

如果两个线程针对同一个对象进行加锁,就会出现锁竞争/锁冲突,一个线程能够获取到锁(先到先得)另一个线程阻塞等待,等待到上一个线程解锁,它才能获取锁成功~~否则就不会
如果两个线程针对不同对象加锁,此时不会发生锁竞争/锁冲突.这俩线程都能获取到各自的锁.不会有阻塞等待了.
还是两个线程,一个线程加锁,一个线程不加锁这个时候是否有锁竞争呢??没有的!!!

eg1:

public synchronized void add(){count++;
}这里直接把synchronized修饰到方法上了,此时相当于针对this加锁

eg2:

 eg3:

public void add(){synchronized(this){count++;}
}
进入代码块就解锁
出了代码块就解锁

这里的this可以指定任意你想指定的对象(不一定非要是this)

3.可重入

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

一个线程针对同一个对象,连续加锁两次,是否会有问题~~如果没问题,就叫可重入的。如果有问题,就叫不可重入的。

synchronized public void add(){synchronized(this){count++;}
}

 在这个代码块中,锁对象是this,只要有线程调用add,进入add方法的时候,就会先加锁(能够加锁成功),紧接着又遇到了代码块,再次尝试加锁。

站在this(锁对象)的视角,它认为自己已经被线程占用了,这里的第二次加锁要不要阻塞等待呢?

这里的第二个线程和第一个线程,其实是同一个线程

在是相同线程的前提下如果允许第二个锁不用阻塞等待,那么就说这个锁是可重入的

反之(第二次加锁会阻塞等待),就说是不可重入的

(就是在锁对象里面记录一下,当前的锁是哪个线程持有的,如果加锁线程和持有线程是同一个,就直接放过,否则就阻塞)

因为Java代码中很容易出现死锁,所以Java就把synchronized设定成可重入的了

4.其他的锁

除了Java的synchronized之外,很多别的语言别的库,加锁解锁往往是两个分开的操作,比如:加锁lock(),解锁unlock(),但是这样分开写容易忘记写unlock

所以synchronized基于代码块的方式,就有效的解决了上述问题

5.Java标准库中的线程安全类

死锁

       死锁是一个非常影响程序员幸福感的问题,一但程序出现死锁,就会导致无法执行后续工作,程序就会有严重bug。并且死锁是非常隐蔽的,开发阶段不经意间就会写出死锁代码,不容易测试出来。

死锁的三种典型情况

1.一个线程一把锁

连续加锁两次

如果锁是不可重入锁,就会死锁。

Java中synchronized和ReentrantLock都是可重入锁,C++,Python,操作系统原生的加锁API都是不可重入的,就会在这种情况下出现死锁。

2.两个线程两把锁

t1,t2各自先针对锁A,锁B加锁,再尝试获取对方的锁

(在这段代码中要加入sleep,否则会出现线程执行速度差别较大从而能够获取到对方的锁)

locker1和locker2分别加锁,再申请对方的锁,这样就会进入死锁,结果什么也没有,于是我们可以运用jconsole来看一下线程的情况:

可以很清楚的看到,两个线程都进入了BLOCKED状态,表示获取锁,获取不到的阻塞状态。

针对这样的死锁问题,也是需要借助像jconsole这样的工具来进行定位的。看线程的状态和调用栈,就可以分析出代码是在哪里死锁了。

3.多个线程多把锁

 死锁的四个必要条件

1.互斥使用

线程1拿到了锁,线程2就得等着(锁的基本特性)

2.不可抢占

线程1拿到锁之后,必须是线程1主动释放,不能说是线程2就把锁给强行获取到。

3.请求和保持

线程1拿到锁A之后,再次尝试获取锁B,A这把锁没有释放,就仍然是保持的。

4.循环等待

线程1尝试获取到锁A和锁B;线程2尝试获取到锁B和锁A。

线程1在获取B的时候等待线程2释放B;同时线程2在获取A的时候等待线程1释放A。

只有这四个条件同时具备,才出现死锁。

循环等待是这四个条件里唯一一个和代码结构相关的,也是我们可以控制的。

如何避免死锁

避免死锁,突破点就是循环等待

方法:给锁编号,然后指定一个固定的顺序(比如从小到大)来加锁,任意线程加多把锁的时候,都让线程遵守上述顺序,此时循环等待自然破除。

内存可见性

class Mycounter{int flag = 0;
}
public class demo1 {public static void main(String[] args) {Mycounter mycounter = new Mycounter();Thread t1 = new Thread(() -> {while(mycounter.flag == 0){            t1这里要快速重复的读取flag的值}System.out.println("循环结束");});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("请输入值");mycounter.flag = scanner.nextInt();});}
}

线程2修改了flag的值,理论上线程1应该会打印循环结束,但是实际上并不会。当输入1的时候,这个线程并不会结束循环。

这个问题就叫做:内存可见性问题

这是一个bug,也是一个线程安全问题

while(mycounter.flag == 0)

这里用汇编来理解,就是两步操作:

1.load,把内存中flag的值,读取到寄存器中

2.cmp,把寄存器的值,和0进行比较,根据比较结果再进行下一步的执行

上述是一个循环,这个循环执行速度极快,一秒钟执行百万次以上。

循环执行这么多次,在t2真正修改之前,load得到的结果都是一样的,另一方面,load操作和cmp操作相比,执行速度慢非常非常多~

由于load执行的速度太慢(相比于cmp来说),再加上反复的load到的结果都一样,JVM就做出了一个大胆的决定:不再真正的重复load,判定好像flag的值不会被修改,干脆就只读取一次就好了。

因为CPU针对寄存器的操作,要比内存快很多!于是通过编译器优化,从而导致了这样的结果。

内存可见性问题:

一个线程对一个变量进行读取操作,同时另一个线程针对这个变量进行修改,此时读到的值,不一定是修改过后的值。

volatile

这时候就需要我们手动干预,需要用到的关键字是violatile。

volatile关键字的作用主要有如下两个:
1. 线程的可见性:当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
2. 顺序一致性:禁止指令重排序。

同时volatile只能修饰变量,synchronized可以修饰方法,静态方法,代码块。

这就相当于告诉编译器,这个变量是易变的,你要每次都重新读取这个变量的内容。

一个变量在两个线程中,一个读,一个写就需要考虑violatile了。

wait notify

现在有一个场景:t1 t2俩线程,希望t1先干活,干的差不多了,再让t2来干。就可以让t2先wait (阻塞,主动放弃cpu)等t1干的差不多了,再通过notify通知t2,把t2唤醒,让t2接着干。

是不是这个场景和join有点类似,也是让其中一个线程等待另一个线程。但是如果我们想先让t1执行50%,再执行t2,join就做不到了。

这个时候就需要用到wait和notify

当t1执行到50%时,手动让其wait,让其进入WAITING状态,然后等待t2执行完毕再执行t1,仅需要用notify唤醒就行了。

 但是报错了

 为什么会有这个异常?先来了解一下wait的操作:

1.先释放锁

2.进行阻塞等待

3.收到通知之后,重新尝试获取锁,并且在获取锁后,继续往下执行。

因此wait操作要搭配synchronized来使用

public class demo2 {public static void main(String[] args) throws InterruptedException {Object object = new Object();System.out.println("t1 wait之前");Thread t1 = new Thread(() -> {synchronized (object){try {object.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("t1 wait之后");});Thread t2 = new Thread(() -> {System.out.println("t2 notif之前");synchronized (object){object.notify();}System.out.println("t2 notif之后");});t1.start();t2.start();}
}

同时要注意,只有object四次引用的对象是同一个对象,那么这里的结果才是我们想要的。

wait的带有等待时间的版本,看起来就和sleep有点像,其实还是有本质差别的
虽然都是能指定等待时间,虽然也都能被提前唤醒(wait是使用notify唤醒, sleep使用interrupt唤醒)但是这里表示的含义截然不同。
notify唤醒wait,这是不会有任何异常的。(正常的业务逻辑)interrupt唤醒sleep 则是出异常了。(表示一个出问题了的逻辑)

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

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

相关文章

可拖动表格

支持行拖动&#xff0c;列拖动 插件&#xff1a;sortablejs UI: elementUI <template><div><hr style"margin: 30px 0;"><div><!-- 数据里面要有主键id&#xff0c; 否则拖拽异常 --><h2 style"margin-bottom: 30px&qu…

python遍历文件夹下的所有子文件夹,并将指定的文件复制到指定目录

python遍历文件夹下的所有子文件夹&#xff0c;并将指定的文件复制到指定目录 需求复制单个文件夹遍历所有子文件夹中的文件&#xff0c;并复制代码封装 需求 在1文件夹中有1&#xff0c;2两个文件夹 将这两个文件夹中的文件复制到 after_copy中 复制单个文件夹 # coding: ut…

【跨域异常】

想在前端使用vue获取后端接口的数据&#xff0c;但是报了跨域异常&#xff0c;如下图所示。 一种解决的方式是&#xff0c;在后端Controller接口上加上CrossOrigin&#xff0c;从后端解决跨域问题。 还要注意前端请求的url要加上协议&#xff0c;比如http://

Excel:通过Lookup函数提取指定文本关键词

函数公式&#xff1a;LOOKUP(9^9,FIND($G 2 : 2: 2:G 6 , C 2 ) , 6,C2), 6,C2),G 2 : 2: 2:G$6) 公式解释&#xff1a; lookup第一参数为9^9&#xff1a;代表的是一个极大值的数据&#xff0c;查询位置里面最接近这一个值的数据&#xff1b;lookup第二参数用find函数代替&am…

80. 删除有序数组中的重复项 II

【中等题】 题目&#xff1a; 给你一个有序数组 nums &#xff0c;请你 原地 删除重复出现的元素&#xff0c;使得出现次数超过两次的元素只出现两次 &#xff0c;返回删除后数组的新长度。 不要使用额外的数组空间&#xff0c;你必须在 原地 修改输入数组 并在使用 O(1) 额…

string类中的一些问题

前言&#xff1a;C中的string类是继承C语言的字符数组的字符串来实现的&#xff0c;其中包含许多C的字符串的相关知识的同时&#xff0c;也蕴含很多的类与对象的相关知识&#xff0c;在面试中&#xff0c;面试官总喜欢让学生自己来模拟实现string类&#xff0c;最主要是实现str…

RK3568 安卓源码编译

一.repo安卓编译工具 项目模块化/组件化之后各模块也作为独立的 Git 仓库从主项目里剥离了出去&#xff0c;各模块各自管理自己的版本。Android源码引用了很多开源项目&#xff0c;每一个子项目都是一个Git仓库&#xff0c;每个Git仓库都有很多分支版本&#xff0c;为了方便统…

request+python操作文件导入

业务场景&#xff1a; 通常我们需要上传文件或者导入文件如何操作呢&#xff1f; 首先通过f12或者通过抓包查到请求接口的参数&#xff0c;例如&#xff1a; 图中标注的就是我们需要的参数&#xff0c;其中 name是参数名&#xff0c;filename是文件名&#xff0c;Content-Type是…

微信小程序开发教学系列(4)- 数据绑定与事件处理

4. 数据绑定与事件处理 在微信小程序中&#xff0c;数据绑定和事件处理是非常重要的部分。数据绑定可以将数据和页面元素进行关联&#xff0c;实现数据的动态渲染&#xff1b;事件处理则是响应用户的操作&#xff0c;实现交互功能。本章节将详细介绍数据绑定和事件处理的基本原…

【C++】C++11的新特性(上)

引入 C11作为C标准的一个重要版本&#xff0c;引入了许多令人振奋的新特性&#xff0c;极大地丰富了这门编程语言的功能和表达能力。本章将为您介绍C11的一些主要变化和改进&#xff0c;为接下来的章节铺垫。 文章目录 引入 一、列表初始化 1、1 {} 初始化 1、2 std::initiali…

RISC-V IOPMP实际用例-Rapid-k模型在NVIDIA上的应用

安全之安全(security)博客目录导读 2023 RISC-V中国峰会 安全相关议题汇总 说明&#xff1a;本文参考RISC-V 2023中国峰会如下议题&#xff0c;版权归原作者所有。

C语言:指针数组

一、指针数组介绍 指针数组本质是数组&#xff0c;是一个存放指针的数组 代码如下&#xff1a; arr1和arr2就是指针数组 int main() {int a 1; int *pa &a;int b 2; int *pb &b;int c 3; int *pc &c;int d 4; int *pd &d;int e 5; int *pe &e;in…

websocket和uni-app里使用websocket

一、HTTP是无状态协议 特点&#xff1a; 1、浏览器发送请求时&#xff0c;浏览器和服务器会建立一个连接。完成请求和响应。在http1.0之前&#xff0c;每次请求响应完毕后&#xff0c;会立即断开连接。在http1.1之后&#xff0c;当前网页的所有请求响应完毕后&#xff0c;才断…

ZLMediaKit 各种推拉流

1 用ffmpeg 推音视频流 ./ffmpeg -f dshow -i video"HP Wide Vision HD Camera" -f dshow -i audio"麦克风阵列 (Realtek High Definition Audio)" -rtbufsize 100M -max_delay 100 -pix_fmt yuv420p -tune zerolatency -c:v libx264 -crf 18 -s 1280x720…

如何使用敏捷开发方法管理项目

敏捷开发方法是一种灵活且高效的项目管理方法&#xff0c;旨在应对不断变化的需求和快速发展的项目环境。使用敏捷开发方法可以帮助团队更好地应对不确定性&#xff0c;提高项目的质量和效率。以下是使用敏捷开发方法管理项目的具体步骤&#xff1a; 明确项目目标和范围 在项…

科技资讯|苹果Vision Pro头显申请游戏手柄专利和商标

苹果集虚拟现实和增强现实于一体的头戴式设备 Vision Pro 推出一个月后&#xff0c;美国专利局公布了两项苹果公司申请的游戏手柄专利&#xff0c;其中一项的专利图如下图所示。据 PatentlyApple 报道&#xff0c;虽然专利本身并不能保证苹果公司会推出游戏手柄&#xff0c;但是…

【OpenCV入门】第一部分——图像处理基础

本文结构 图像处理的基本操作读取图像imread() 显示图像imshow()waitKey()destroyAllWindows() 保存图像imwrite() 复制图像copy() 获取图像属性 像素确定像素的位置获取像素的BGR值修改像素的BGR值 色彩空间GRAY色彩空间cvtColor()——从BGR色彩空间转换到GRAY色彩空间 HSV色彩…

JavaScript 执行上下文和作用域链

执行上下文 执行上下文决定了变量和函数可以访问哪些数据。 一个执行上下文就对应一个仅后台可访问的变量对象&#xff0c;其中保存有该上下文的局部变量、参数和函数声明。 最外层的上下文称为全局上下文。宿主环境不同&#xff0c;全局上下文的关联对象就不同。在浏览器中…

Java 程序打印 OpenCV 的版本

我们可以使用 Java 程序来使用 OpenCV。 OpenCV 的使用需要动态库的加载才可以。 加载动态库 到 OpenCV 的官方网站上下载最新的发布版本。 Windows 下载的是一个可执行文件&#xff0c;没关系&#xff0c;这个可执行文件是一个自解压程序。 当你运行以后会提示你进行解压。…

spring打入filter内存马+冰蝎成功

环境&#xff1a; springboot版本2.4.5 <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.4.5</version><relativePath/> <!-- lookup parent from r…