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

线程安全

目录

1、线程安全

1.1、案例引入

1.2、线程安全的概念

2、线程安全问题分析

2.1、线程安全问题的原因

2.2、解决线程安全问题

3、synchronized 关键字

3.1、修饰代码块

3.2、修饰普通方法

3.3、修饰静态方法


1、线程安全

1.1、案例引入

执行结果:

原因分析:

线程是并发执行的,调度是随机的。结果是 0,说明main线程先被执行打印了

我们希望先把 t1 和 t2 执行完,再执行 main 的打印,可以做出优化:

先让两个线程执行完,再执行main线程

这两个线程,谁先 join,谁后 join 都可以

分析:两种情况

1)t1 先结束,t2 后结束
main 先在 t1.join 阻塞等待
t1 结束
main 再在 t2.join 阻塞等待
t2 结束

main继续执行后续打印
最终打印的值,就是 t1 和 t2 都执行完的值

2)t2 先结束,t1 后结束
main 先在 t1.join 阻塞
t2 结束,t1.join 继续阻塞
t1 结束
main执行到 t2.join,由于 t2 已经结束了,此处的 t2.join 是不会阻塞的
main 继续执行后续打印
最终打印的值,还是 t1 和 t2 都执行完的值

^

这两种方式总的阻塞时间都是一样的,都是 t1 和 t2 较长的时间。区别在于是分两个 join 各自阻塞一会还是在一个 join 全都阻塞完

优化后发现结果还是不对,并且每次执行出现的结果都不相同

但如果把两个线程变成串行执行(一个执行完了,再执行另一个),就没问题了

1.2、线程安全的概念

通过上述分析可以看出,当前 bug 是由于多线程的并发执行代码引起的 bug,这样的 bug 就称为 “线程安全问题” 或者叫做 “线程不安全”;反之,如果一个代码在多线程并发执行的环境下,不会出现类似于上述的 bug 的代码就叫 “线程安全”

2、线程安全问题分析

bug 分析:

count++;  看似是一行代码,实际上对应到 3 个 cpu 指令

1)load:把内存中 count 的值,加载到 cpu 的寄存器
2)add:把寄存器中的内容+1
3)save:把寄存器中的内容保存回内存上

而操作系统,对于线程的调度,是随机的。执行 1 2 3 三个指令的时候,不一定是 “一口气执行完”,很可能是执行到其中的一部分,该线程就被调度走了

这种抢占式执行就是线程安全问题的罪魁祸首

^

例如:t1 线程 先执行了第一个指令,然后调度到 t2 线程并且把三个指令执行完了,再回过头来执行 t1 线程的后两个指令。

t1 执行第一个指令后,寄存器中的值是 0,执行完 t2 的三个指令后,内存的数字是 1,再执行 t1 线程的后两个指令,0+1 后把 1 写回到内存上。最终内存上的结果是 1。两次 ++,最终只相当于只 + 了一次

^

实际在循环5w次过程中,不知道有多少次是正确的,多少次是错误的,所以每次重新执行代码出现的结果都各不相同,但最终执行的结果,一定是小于等于 10w 的

如果把执行 5w 次改成 50 次,尝试多次运行,发现大部分情况都是正确的结果(100),只有偶尔才会出现错误的结果

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 < 50; i++) {count++;}System.out.println("t1 结束");});Thread t2 = new Thread(() -> {for (int i = 0; i < 50; i++) {count++;}System.out.println("t2 结束");});t1.start();t2.start();t1.join();t2.join();// 一个线程自增 5w 次, 两个线程, 总共自增 10w 次. 预期结果, count = 100000System.out.println(count);}
}

原因是,循环 50 次很可能在执行 t2.start 之前 t1 就已经执行完了,后续 t2 再执行,就变成纯串行了。但还是有可能出现 t1,t2 交替执行的情况,问题仍然存在,只是概率变低了

2.1、线程安全问题的原因

线程安全问题产生原因总结: 

1. 根本原因:操作系统对线程的调度是随机的,抢占式执行
2. 多个线程同时修改同一个变量

例如上述的案例,t1 和 t2 都在修改同一个内存空间(count)

3. 修改操作不是原子的

如果修改操作只是对应到一 个cpu 指令,就可以认为是原子的(cpu不会出现 “一条指令执行一半” 这样的情况)如果对应到多个 cpu 指令就不是原子的


像 ++、--、+=、-= 等操作都不是原子的

赋值操作 “=” 在 Java 中是原子的,但在 C++ 就不一定了,因为C++涉及到 “运算符重载”

4. 内存可见性问题
5. 指令重排序

以下几种情况不会出现线程安全问题:

1. 一个线程只修改一个变量
2. 多个线程,不同时修改同一个变量
3. 多个线程修改不同变量
4. 多个线程读取(取值操作)同一个变量

2.2、解决线程安全问题

1. 抢占式执行的问题无法解决,因为是操作系统的底层设定

2. 多个线程同时修改同一个变量的解决方法 和代码的结构直接相关,只需要调整代码结构,规避一些线程不安全的代码的。但是这样的方案不够通用,有些情况下,需求上就是需要多线程修改同一个变量的,例如超买/超卖的问题

3. 修改操作不是原子的 解决方法:

在 Java 中解决线程安全问题,最主要的方案:加锁(互斥 / 排他)。通过加锁操作,让不是原子的操作,打包成一个原子的操作。

案例中的线程安全问题,就可以使用锁,把不是原子的 count++ 包裹起来。在 count++ 之前先加锁,然后再进行count++,计算完毕之后,再解锁。这样在执行 3 个指令的过程中,其他线程就没法插队了

加锁 / 解锁本身是操作系统提供的 api,很多编程语言都对这个 api 进行封装了

大多数的封装风格,都是采取两个函数:

Java中,是使用 synchronized 关键字,搭配代码块,来实现类似的效果的

3、synchronized 关键字

synchronized:同步的

在计算机中,同步这个术语有多种含义,这里同步指的是 “互斥”

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

3.1、修饰代码块

明确指定锁哪个对象

1. synchronized 用的锁是存在 Java 对象里的:

尝试给两个线程的 count++ 加锁:

1. synchronized 后的小括号填写的是 用来加锁的对象。要加锁,要解锁,前提是要先有一个锁。锁在 Java 中,任何一个对象,都可以用作 “锁”。这里使用 Object 对象

2. 这个对象的类型是什么,不重要。重要的是,是否有多个线程尝试针对同一个对象加锁(多个线程是否涉及到互斥)

3. 两个线程,针对同一个对象加锁,才会产生互斥效果。一个线程加上锁了,另一个线程就要阻塞等待,等到第一个线程释放锁,才有机会

4. 如果是不同的锁对象,不会有互斥效果,线程安全问题没有得到改变

5. 上一个线程解锁之后,下一个线程并不是立即就能获取到锁,而是要靠操作系统来 “唤醒”

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

7. 把对象作为锁对象,不影响对象的其他使用。但一般来说,一个对象只有一个用途是比较好的。因此更推荐创建一个专门的对象只用于加锁操作

2. 解决线程安全问题,不是写了加了锁就可以,而是要正确的使用锁

1) synchronized { } 代码块位置要合适
2) synchronized ( ) 指定的锁对象也要合适

例如,把代码块加到循环外面:

这种写法意味着整个 for 循环,i<50000,i++,count++ 都是“互斥”的方式执行的,而 for 循环里的条件判断(i<50000)和 i++ 这两个操作不涉及到互斥

只有当第一个线程中的循环全部执行完,第二个线程才能拿到锁,相当于完全串行执行了。虽然最终结果是正确的,但效率低很多


而代码块加在 count++ 外面,只是每次 count++ 之间是串行的,for 中的 i<5w 和 i++ 是并发的,执行速度更快

3.2、修饰普通方法

针对 this 加锁

把案例的代码改一下,把 count++ 操作封装到一个类中:

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

此时对 counter.add(); 加锁的操作,可以直接在 add 方法内对 this 对象加锁:

进一步变成,可以写成这种形式:


像 StringBuffer、Vector 等对象,方法上就是带有 synchronized(针对 this 加锁)

3.3、修饰静态方法

针对类对象加锁

static 修饰的方法不存在 this。synchronized 修饰 static方法,相当于针对类对象加锁

等价与:

http://www.dtcms.com/a/303602.html

相关文章:

  • C++常见的仿函数,预定义函数,functor,二元操作函数(对vector操作,加减乘除取余位运算等 )
  • 异步通讯组件MQ
  • HTML应用指南:利用GET请求获取全国小米之家门店位置信息
  • 基于深度学习的医学图像分析:使用3D CNN实现肿瘤检测
  • hot100——第九周
  • 在Linux上使用DuckCP实现从csv文件汇总数据到SQLite数据库的表
  • 数据开源 | “白虎”数据集首批开源,迈出百万数据征途第一步
  • Zynq SOC FPGA嵌入式裸机设计和开发教程自学笔记:硬件编程原理、基于SDK库函数编程、软件固化
  • 2.DRF 序列化器-Serializer
  • 第五章:进入Redis的Hash核心
  • 小架构step系列28:自定义校验注解
  • 【算法训练营Day17】二叉树part7
  • 【VASP】二维材料杨氏模量与泊松比的公式
  • OpenLayers 综合案例-信息窗体-弹窗
  • 打卡day5
  • C++面试5题--5day
  • C++中的“对象切片“:一场被截断的继承之痛
  • 【SpringMVC】MVC中Controller的配置 、RestFul的使用、页面重定向和转发
  • rhel9.1配置本地源并设置开机自动挂载(适用于物理光驱的场景)
  • c++ 基础
  • windows内核研究(异常-CPU异常记录)
  • 嵌入式分享合集186
  • STM32时钟源
  • JavaScript手录09-内置对象【String对象】
  • 第一章:Go语言基础入门之函数
  • wrk 压力测试工具使用教程
  • 屏幕晃动机cad【4张】三维图+设计说明书
  • 多信号实采数据加噪版本
  • 详解 Electron 应用增量升级
  • 轻量级远程开发利器:Code Server与cpolar协同实现安全云端编码