【线程安全篇】
线程安全之原子性问题
x++ ,在字节码文件中对应多个指令,多个线程在运行多个指令时,就存在原子性、可见性问题
赋值
多线程场景下,一个指令如果包含多个字节码指令,那么就不再是原子操作。因为赋值的同时,读到的x的值可能已经发生变化,被其他线程修改了。
x = 10; //原子操作,只有一个操作,10赋值给x,之后写入内存y = x; //非原子操作,1、先从内存读x的值 2、x的值赋值给y,再写入内存x++; //非原子操作,同上
count++
模拟多个线程count++,最终count不一定等于1000。
public class Demo{ private static int count=0; public static void inc(){ try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } count++; } public static void main(String[] args) throws InterruptedException {for(int i=0;i<1000;i++){ new Thread(()->Demo.inc()).start(); } Thread.sleep(3000); System.out.println("运行结果"+count); }
}
线程安全之可见性问题
多个线程访问同一变量,一个线程修改了该变量的值,其他线程能立刻看到修改的最新值。
CPU缓存不一致问题
计算机核心组件:CPU、内存、I/O设备
计算速度对比:CPU > 内存 > I/O设备
为了提升计算性能,CPU从单核升级到了多核,以及超线程技术。但后两者的处理性能并没有跟上。为了平衡三者的速度差异,做了很多优化:
1.CPU增加了高速缓存,很好的解决了CPU和内存的速度矛盾。
2.操作系统增加了进程、线程。通过CPU时间片切换最大化提高CPU录用率。
3.编译器指令优化。
CPU高速缓存
线程是CPU调度的最小单元。
主内存 、总线 、CPU多级缓存
CPU先在L1找数据,L1没有去L2,L2没有去L3,L3没有去内存找。
CPU计算时,直接从缓存中读取数据,计算完成后再写入缓存中,最后再把缓存中的数据同步到内存。
缓存不一致问题
每个CPU拥有自己的缓存,如果同一数据在不同缓存中,缓存值不一样,就存在缓存不一致的问题。
解决方案: 总线锁、缓存锁
总线锁
当一个CPU要对共享变量操作时,在总线上发出LOCK#信号,锁住CPU和内存的通信,锁住期间,其他CPU不能操作缓存了该数据内存地址的缓存。
总线锁开销比较大,所以这种机制显然不合适。
缓存锁
基于缓存一致性协议
缓存一致性协议(MESI)
M(Modify)
被修改的。该数据只在当前CPU的缓存中有,且与主内存不一致。
E(Exclusive)
独占的。该数据只在当前CPU缓存中,且没有被修改过。
S(Shared)
共享的。该数据被多个CPU缓存,且各缓存中的数据与主内存一直。
I(Invalid)
失效的。当前CPU中缓存的该数据失效。
i=1,该CPU独占且与内存数据一致,此时处于E状态,如果i变成了2,则状态变为M。
CPU只能从缓存中读取M、E、S状态的数据,I状态的数据要到内存中读取。
CPU可以直接写M、E状态的数据。S状态的数据,需要先将其他CPU中缓存行设置为无效才能写。
Store Bufferes
CPU0对缓存中的共享变量写入时,先发送一个失效的消息给到缓存了该共享变量的CPU,并且要等到它们的确认回执。这个过程中,CPU0处于阻塞状态。为了避免浪费资源,所以引入Store Bufferes
1.CPU0将数据写入Store Bufferes中,同时发送invalidate消息给CPU1,之后就可以继续处理其他指令。
2.CPU1收到invalidate消息后,将要修改的变量i放入invalidate queue(失效队列中),并且给一个ACK应答。
3.CPU0收到CPU1的invalidate acknowledge之后,将Store Bufferes中的数据存储至缓存行(cache line),最后再从缓存行同步到内存。
内存屏障(memory barrier)
内存屏障就是把Store Bufferes中的指令写入内存,内存屏障之前的内存访问操作先于其后的操作完成。保证共享变量对其他线程的可见性。
写屏障(store memory barrier)
store之前的所有已经存储在Sotre Bufferes中的数据同步到内存。即将Sotre Bufferes中的a==1同步到内存后,才能执行后面的b=1。
读屏障(load memory barrier)
load之后的读操作,都在load屏障之后执行。配合store屏障,使得store之前的写操作对load之后的读操作是可见的。
全屏障(full memory barrier)
full前的读写操作同步到内存后,才能执行full之后的操作。
重排序问题
为了提升性能,编译器和CPU会对指令做重排序,源码到最终执行,会经过三种重排序
注:2、3属于CPU重排序
JMM(Java Memory Model)
JMM定义了共享内存中,多线程的读写操作规范。实现了将共享变量存储到内存、从内存中取出共享变量的底层细节。从而解决CPU多级缓存、处理器优化、指令重排序导致的内存访问问题。保证了并发场景下的可见性。
缓存一致性问题,有总线索、缓存锁,缓存锁基于MESI协议。
指令重排序问题,硬件层面提供了内存屏障。
JMM在此基础上提供了volatile、final等关键字,来解决可见性、重排序问题。
内存屏障分4类
HappenBefore
如果前一个操作的结果需要另一个操作时可见,那么这两个操作之间必须存在happens-before关系。这两个操作可以是同一个线程,也可以是不同线程。
1、程序顺序规则(as-if-serial语义)==
单个线程中的代码顺序不管怎么变,对于结果来说是不变的。
依赖问题,如果两个指令存在依赖关系,不许重排序。
1 happenns-before 2,3 happens-before 4
2、volatile变量规则
volatile修饰的变量,写操作一定happens-before读操作。
2 happens-before 3
3.传递性规则
如果1 happenns-before 2,3 happens-before 4,那么1 happenns-before 4。
4.Start规则
线程A 中ThreadB.start()操作happenns-before线程B中的任意操作。
public StartDemo{
int x=0;
Thread t1 = new Thread(()->{
// 子线程中,x==10
});
x = 10; // 此处对共享变量 x 修改,此操作对于子线程可见。
t1.start(); // 主线程启动子线程
}
5.join规则
线程A中ThreadB.join(),那么线程B的所有操作happenns-before线程的ThreadB.join()操作。
public StartDemo{ int x=0; Thread t1 = new Thread(()->{ // 子线程中,x==10 x=100; //修改X}); x = 10; // 此处对共享变量 x 修改,此操作对于子线程可见。t1.start(); // 主线程启动子线程t1.join(); //子线程的修改,在主线程执行t1.join()之后皆可见。X==100
}
6.监视器锁的规则
解锁happenns-before下一个加锁。
synchronized (this) { // 此处自动加锁 if (this.x < 12) { // x 是共享变量, 初始值 =10 this.x = 12; }
} // 此处自动解锁
线程A中x = 12,那么线程B拿到锁之后,能看到x == 12。
Synchronized
synchronized可以解决线程原子性问题,synchronized块之间的操作具备原子性。
Java SE 1.6优化了synchronized,引入了偏向锁、轻量级锁,减少获得锁、释放锁带来的性能开销。
public class Demo{ private static int count=0; public static void inc(){ synchronized (Demo.class) { //基于Demo对象的生命周期来控制锁粒度try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } count++; }} public static void main(String[] args) throws InterruptedException {for(int i=0;i<1000;i++){ new Thread(()->Demo.inc()).start(); } Thread.sleep(3000); System.out.println("运行结果"+count); }
}
对象锁
synchronized 修饰方法
synchronized 修饰代码块 this/Synchronized_demo.this
多线程跑同一个对象
全局锁/类锁
synchronized 修饰 static 方法
各线程之间 抢锁
synchronized 修饰代码块 Synchronized_demo.class
各线程之间 抢锁
对象
对象的存储布局
对象头
包含了Mark Word、class指针、数组的长度(对象为数据时才有)
Mark Word(自身运行时数据)记录了对象和锁有关的信息。
32位操作系统为例:
synchronized 锁升级
所以在JDK1.6之后,synchronized中,锁存在4种状态:无锁、偏向锁、轻量级锁、重量级锁,锁状态由低到高不断升级。
偏向锁
大部分情况下,锁总被同一个线程多次获得,所以引入偏向锁。
对象头中存储线程ID,从而避免同一个线程再次进入、退出时获取锁、释放锁的操作。
如果多个线程竞争该锁,那么偏向锁就是一种累赘,可通过JVM参数UseBiasedLocking 来设置开启或关闭偏向锁。
偏向锁获取逻辑
1.获取锁对象的Mark Word,判断是否处于可偏向状态。
(biased_lock=1且 ThreadID 为空,则表示可偏向)
2.如果是可偏向状态,则通过CAS操作,把当前线程ID写入锁对象的Mark Word。
1)CAS成功,则获得偏向锁
2)CAS失败,说明偏向锁被其他线程占有,当前锁存在竞争,则撤销偏向锁,升级成轻量级锁。
3.如果是已偏向状态,则检查锁对象的Mark Word中的ThreadID 与当前线程的 ThreadID 是否相等。
1)如果相等,则无需再获得锁。
2)如果不相等,说明当前锁偏向于其他线程,要么重新偏向,要么撤销偏向锁,升级成轻量级锁。
偏向锁撤销逻辑
1.如果原获得偏向锁的线程同步代码块执行完了,那么锁对象设置成无锁状态,再重新偏向。
如果没有执行完,则在一个安全点停止拥有锁的线程A,修复锁记录和Mark Word,使其变成无锁状态,再唤醒线程A,将当前锁升级成轻量级锁。
轻量级锁
升级为轻量级锁之后,对象的Mark Word也会相应的变化。
轻量级锁的加锁逻辑
1.线程在自己的栈帧中创建锁记录LockRecord。
2.将锁对象 对象头中的MarkWord复制到线程刚刚创建的LockRecord。
3.将锁记录中的owner指针指向锁对象。
将锁对象对象头的MarkWord替换为指向锁记录的指针。
轻量级锁的解锁
锁释放逻辑其实就是获得锁的逆向逻辑。
通过CAS操作把线程栈帧中的LockRecord替换回到锁对象的MarkWord中,如果成功,表示没有竞争。如果失败,表示当前锁存在竞争,膨胀称为重量级锁。
自旋锁
轻量级锁在加锁的过程中,使用了自旋锁。
当一个线程来竞争锁时,会原地循环等待,直到锁被释放后,该线程直接获得锁,所以轻量级锁适用于同步代码块执行很快的场景。
自旋必须要一定的条件限制,否则不断循环,反而消耗CPU资源。默认情况下,自旋次数10次,可以通过preBlockSpin修改。
JDK1.6之后,引入自适应自旋锁,可根据前一次自旋时间以及锁拥有者的状态来觉得自旋次数。
重量级锁
当轻量级锁膨胀到重量级锁,未抢到锁的线程只能被挂起阻塞,等待被唤醒。
重量级锁是依赖对象内部的monitor锁来实现的,而monitor锁又依赖操作系统的MutexLock(互斥锁),所以重量级锁又称互斥锁。
当线程要去执行一段被synchronize修饰的方法或代码块时,需要先获得被synchronize修饰的对象的monitor监视器(monitorenter),获取失败,线程进入同步队列,变成blocked状态,直到锁被释放之后,当前线程会被唤醒,重新尝试对monitorenter的获取。
synchronized的执行过程
- 检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁 。
- 如果不是,则使用CAS将当前线程的ID替换Mard Word里的线程ID,如果成功则表示当前线程获得偏向锁,置偏向标志位1 ,如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。
- 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁 ,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
- 如果自旋成功则依然处于轻量级,如果自旋失败,则升级为重量级锁。
sleep
Thread.sleep(1000)
阻塞1秒,期间不释放锁
wait
wait()阻塞当前线程,释放锁,并把当前线程放入等待队列,等待被唤醒。
wait()前提是必须先获得锁,这样才能释放锁,一般配合synchronized 关键字使用,即一般在synchronized 同步代码块里使用 wait()、notify/notifyAll() 方法。
public class ThreadA extends Thread{private Object lock;public ThreadA(Object lock) {this.lock = lock;}@Overridepublic void run() {synchronized (lock){System.out.println("start ThreadA");try {lock.wait(); //实现线程的阻塞,并且释放锁} catch (InterruptedException e) {e.printStackTrace();}System.out.println("end ThreadA");}}
}
notify
notify()是将锁交给含有wait()方法的线程,让其继续执行下去,所以必须先持有锁
public class ThreadB extends Thread{private Object lock;public ThreadB(Object lock) {this.lock = lock;}@Overridepublic void run() {synchronized (lock){System.out.println("start ThreadB");lock.notify(); //唤醒被阻塞的线程System.out.println("end ThreadB");}}
}
notifyAll
notifyAll()唤醒等待队列里的线程,等待队列并没有资格竞争锁,而是线程被移到同步队列后,再竞争锁。
jion
主线程合并子线程。join底层是使用wait()来实现,所以会释放锁。
Volatile
Vloatile遵循HappenBefore规则,能保证新值在修改后立即同步回主内存,每次使 用前从主内存刷新。
普通变量无法保证这一点,因为普通的共享变量修改后,什么时候同步写回主内存是不确定的,其他线程读取时,内存中可能还是原来的旧值。
public class App {public volatile static boolean stop=false;public static void main( String[] args ) throws InterruptedException {Thread t1=new Thread(()->{int i=0;while(!stop){ //condition 不满足i++;}System.out.println(i);});t1.start();Thread.sleep(10);stop=true; //true 主线程设置stop为true,对子线程可见。}
}
final关键字提供了内存屏障的规则
相关文章:

【线程安全篇】
线程安全之原子性问题 x ,在字节码文件中对应多个指令,多个线程在运行多个指令时,就存在原子性、可见性问题 赋值 多线程场景下,一个指令如果包含多个字节码指令,那么就不再是原子操作。因为赋值的同时,…...

错误:EfficientDet网络出现“No boxes to NMS“并且mAP:0.0的解决方案
近日,在使用谷歌新推出来的一个网络EfficientDet进行目标检测训练自己的数据集的时候,出现了如下错误: 其中项目开源地址是:https://github.com/toandaominh1997/EfficientDet.Pytorch 上面截图中的1和2代表我的类别名称。读者可…...

python的opencv操作记录13——区域生长及分水岭算法
文章目录图像区域基本算法——形态学运算腐蚀与膨胀开运算与闭运算opencv中的形态学运算距离计算——distanceTransform函数连通域连通的定义计算连通域——connectedComponents连通域实验基于区域的分割区域生长算法自定义一个最简单区域生长算法实现区域分割一般区域分割open…...

一文看懂网上下单的手机流量卡为什么归属都是随机的!
最近很多网上下单的小伙伴们心中似乎都有一个疑问。那就是网上很多手机卡、流量卡都不能自选号码和归属地,就算能自选号码,归属地也是随机的而且很多都不会跟你说具体的城市,这是为什么呢?莫非其中有什么不可告人的秘密吗?小伙伴…...

python Pytest生成alluer测试报告的完整教程
1.下载allure包到本地,解压 网上很多资料,这边不提供了 2.配置环境变量 将上面解压后bin文件的路径复制,添加到环境变量Path下 3.验证环境变量配置是否功 在cmd中输入allure,回车 。查看allure是否成功: 4.pyc…...

4-spring篇
ApplicationContext refresh的流程 12个步骤 prepareRefresh 这一步创建和准备了Environment对象,并赋值给了ApplicationContext的成员变量 要理解Environment对象的作用 obtainFreshBeanFactory ApplicationContext 里面有一个成员变量,Beanfactory b…...
提升 Web 应用程序的性能:如何使用 JavaScript 编写缓存服务
缓存是一种重要的优化技术,用于加速数据访问和降低服务器负载。缓存存储经常访问的数据,以便在需要时可以快速检索。在本文中,我们将探索如何使用简单的数据结构在 JavaScript 中编写缓存服务。 编码缓存服务的第一步是定义将用于访问缓存的…...

供应商绩效管理指南:挑战、考核指标与管理工具
管理和优化供应商绩效既关键又具有挑战性。要知道价格并不是一切,如果你的供应商在商定的价格范围内向你开具发票,但服务达不到标准或货物不合格,你也无法达到节约成本的目标。 供应商绩效管理可以深入了解供应商可能带来的风险,…...

干货文稿|详解深度半监督学习
分享嘉宾 | 范越文稿整理 | William嘉宾介绍Introduction to Semi-Supervised Learning传统机器学习中的主流学习方法分为监督学习,无监督学习和半监督学习。这里存在一个是问题是为什么需要做半监督学习?首先是希望减少标注成本,因为目前可以…...

信箱|邮箱系统
技术:Java、JSP等摘要:在经济全球化和信息技术飞速发展的今天,通过邮件收发进行信息传递已经成为主流。目前,基于B/S(Browser/Server)模式的MIS(Management information system)日益…...
JS数组拓展
1、Array.from Array.from 方法用于将两类对象转为真正的数组: 类似数组的对象,所谓类似数组的对象,本质特征只有一点,即必须有length属性。 因此,任何有length属性的对象,都可以通过Array.from方法转为数组 和 可遍历…...
一道很考验数据结构与算法的功底的笔试题:用JAVA设计一个缓存结构
我在上周的笔试中遇到了这样一道题目,觉得有难度而且很考验数据结构与算法的功底,因此Mark一下。 需求说明 设计并实现一个缓存数据结构: 该数据结构具有以下功能: get(key) 如果指定的key存在于缓存中,则返回与该键关联的值&am…...
(10)C#传智:命名空间、String/StringBuilder、指针、继承New(第10天)
内容开始多了,慢品慢尝才有滋味。 一、命名空间namespace 用于解决类重名问题,可以看作类的文件夹. 若代码与被使用的类,与当前的namespace相同,则不需要using. 若namespace不同时,调用的方法:…...

基于Jetson Tx2 Nx的Qt、树莓派等ARM64架构的Ptorch及torchvision的安装
前提 已经安装好了python、pip及最基本的依赖库 若未安装好点击python及pip安装请参考这篇博文 https://blog.csdn.net/m0_51683386/article/details/129320492?spm1001.2014.3001.5502 特别提醒 一定要先根据自己板子情况,找好python、torch、torchvision的安…...

MySQL存储引擎详解及对比和选择
什么是存储引擎? MySQL中的数据用各种不同的技术存储在文件(或者内存)中。这些技术中的每一种技术都使用不同的存储机制、索引技巧、锁定水平并且最终提供广泛的不同的功能和能力。通过选择不同的技术,你能够获得额外的速度或者功能,从而改善…...

【推拉框-手风琴】vue3实现手风琴效果的组件
简言 在工作时有时会用到竖形手风琴效果的组件。 在此记录下实现代码和实现思路。 手风琴实现 结构搭建 搭建结构主要实现盒子间的排列效果。 用flex布局或者其他布局方式将内容在一行排列把每一项的内容和项头用盒子包裹, 内容就是这一项要展示的内容…...

滑动窗口最大值:单调队列
239. 滑动窗口最大值 难度困难2154收藏分享切换为英文接收动态反馈 给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。 返回 滑动窗口中的最大值 。 示例…...
负载均衡算法
静态负载均衡 轮询 将请求按顺序轮流地分配到每个节点上,不关心每个节点实际的连接数和当前的系统负载。 优点:简单高效,易于水平扩展,每个节点满足字面意义上的均衡; 缺点:没有考虑机器的性能问题&…...

C语言数组二维数组
C 语言支持数组数据结构,它可以存储一个固定大小的相同类型元素的顺序集合。数组是用来存储一系列数据,但它往往被认为是一系列相同类型的变量。 数组的声明并不是声明一个个单独的变量,比如 runoob0、runoob1、…、runoob99,而是…...

7年测试工程师,裸辞掉17K的工作,想跳槽找更好的,还是太高估自己了....
14年大学毕业后,在老师和朋友的推荐下,进了软件测试行业,这一干就是7年时间,当时大学本来就是计算机专业,虽然专业学的一塌糊涂,但是当年的软件测试属于新兴行业,人才缺口比较大,而且…...

19c补丁后oracle属主变化,导致不能识别磁盘组
补丁后服务器重启,数据库再次无法启动 ORA01017: invalid username/password; logon denied Oracle 19c 在打上 19.23 或以上补丁版本后,存在与用户组权限相关的问题。具体表现为,Oracle 实例的运行用户(oracle)和集…...

Lombok 的 @Data 注解失效,未生成 getter/setter 方法引发的HTTP 406 错误
HTTP 状态码 406 (Not Acceptable) 和 500 (Internal Server Error) 是两类完全不同的错误,它们的含义、原因和解决方法都有显著区别。以下是详细对比: 1. HTTP 406 (Not Acceptable) 含义: 客户端请求的内容类型与服务器支持的内容类型不匹…...

树莓派超全系列教程文档--(61)树莓派摄像头高级使用方法
树莓派摄像头高级使用方法 配置通过调谐文件来调整相机行为 使用多个摄像头安装 libcam 和 rpicam-apps依赖关系开发包 文章来源: http://raspberry.dns8844.cn/documentation 原文网址 配置 大多数用例自动工作,无需更改相机配置。但是,一…...

《Qt C++ 与 OpenCV:解锁视频播放程序设计的奥秘》
引言:探索视频播放程序设计之旅 在当今数字化时代,多媒体应用已渗透到我们生活的方方面面,从日常的视频娱乐到专业的视频监控、视频会议系统,视频播放程序作为多媒体应用的核心组成部分,扮演着至关重要的角色。无论是在个人电脑、移动设备还是智能电视等平台上,用户都期望…...

Debian系统简介
目录 Debian系统介绍 Debian版本介绍 Debian软件源介绍 软件包管理工具dpkg dpkg核心指令详解 安装软件包 卸载软件包 查询软件包状态 验证软件包完整性 手动处理依赖关系 dpkg vs apt Debian系统介绍 Debian 和 Ubuntu 都是基于 Debian内核 的 Linux 发行版ÿ…...
【磁盘】每天掌握一个Linux命令 - iostat
目录 【磁盘】每天掌握一个Linux命令 - iostat工具概述安装方式核心功能基础用法进阶操作实战案例面试题场景生产场景 注意事项 【磁盘】每天掌握一个Linux命令 - iostat 工具概述 iostat(I/O Statistics)是Linux系统下用于监视系统输入输出设备和CPU使…...
Java多线程实现之Callable接口深度解析
Java多线程实现之Callable接口深度解析 一、Callable接口概述1.1 接口定义1.2 与Runnable接口的对比1.3 Future接口与FutureTask类 二、Callable接口的基本使用方法2.1 传统方式实现Callable接口2.2 使用Lambda表达式简化Callable实现2.3 使用FutureTask类执行Callable任务 三、…...

2021-03-15 iview一些问题
1.iview 在使用tree组件时,发现没有set类的方法,只有get,那么要改变tree值,只能遍历treeData,递归修改treeData的checked,发现无法更改,原因在于check模式下,子元素的勾选状态跟父节…...

ETLCloud可能遇到的问题有哪些?常见坑位解析
数据集成平台ETLCloud,主要用于支持数据的抽取(Extract)、转换(Transform)和加载(Load)过程。提供了一个简洁直观的界面,以便用户可以在不同的数据源之间轻松地进行数据迁移和转换。…...

10-Oracle 23 ai Vector Search 概述和参数
一、Oracle AI Vector Search 概述 企业和个人都在尝试各种AI,使用客户端或是内部自己搭建集成大模型的终端,加速与大型语言模型(LLM)的结合,同时使用检索增强生成(Retrieval Augmented Generation &#…...