问答文章1 问答文章501 问答文章1001 问答文章1501 问答文章2001 问答文章2501 问答文章3001 问答文章3501 问答文章4001 问答文章4501 问答文章5001 问答文章5501 问答文章6001 问答文章6501 问答文章7001 问答文章7501 问答文章8001 问答文章8501 问答文章9001 问答文章9501

并发编程底层实现原理,搞懂了并发编程就盘透了!

发布网友 发布时间:2024-09-15 08:23

我来回答

1个回答

热心网友 时间:2024-11-14 06:13

写在前面

Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM中,JVM执行字节码,最终需要转换为汇编指令在CPU上执行,Java中所有的并发机制依赖于JVM的实现和CPU的指定。

1、volatile的应用

在并发编程中synchronized和volatile关键字都扮演着重要的角色,volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改了一个共享变量时,另一个线程能读到这个修改的值,如果volatile关键字使用得当的话,它会比synchronized使用的成本更低,因为它不会引起上下文切换和调度

1.1连接CPU的相关术语与说明术语英文单词术语描述内存屏障memorybarriers是一组处理器指令,用于实现对内存操作的顺序*缓冲行cachelineCPU高速缓存中可以分配的最小单位。处理器填写缓存行时会加载整个缓存行,现代CPU需要执行几百次CPU指令原子操作atomicoperations不可中断的一个或者一系列操作缓存行填充cachelinehit当处理器识别到从内存中读取操作数是缓存的,处理器读取整个缓存行到适当的缓存(L1\L2\L3)缓存命中cachehit如果进行高速缓存行填充操作的内存位置仍然是下次处理器访问的地址时,处理器从缓存中读取操作数,而不是内存写命中writehit当处理器将操作数协会到一个内存缓存的区域时,他会首先检查这个缓存的内存地址是否在缓存行中,如果存在一个有效的缓存行,则处理器会将这个操作数写回到缓存,而不是写回到内存,这个操作就是写命中写缺失writemissesthecache一个有效的缓存行被写入到不存在的内存区域1.2volatile是如何保证内存可见性的

原理:被volatile修饰的变量,通过jvm最终生成的汇编指令会多出一行汇编代码,这行代码是Lock前缀的。

//java代码//instance被volatile关键字修饰instance=newSingleton();//通过工具获取的JIT编译器生成汇编指令如下0x01a3deld:movb$0.....:lockadd1$x0,(%esp);

Lock前缀的指令在多核处理器下会引发两件事情

将处理器缓存行的数据写回到系统内存

这个写回内存的操作会使在其他CPU缓存了该内存地址的数据无效

为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存中的数据读到内存缓存(L1L2或其他)后在进行操作,但操作完不知道何时写会内存。如果对申明了volatile的变量进行操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧值,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线传播的数据来检查自己的缓存是否过期了,当处理器发现自己缓存的数据对应的内存地址被修改,就会将当前处理器的缓存行设置为无效,当处理器对这个数据进行修改时,就会重新从内存中读取数据到缓存中。

2、synchronized的实现原理与应用

在多线程并发编程中synchronized一直是元老角色,很多人称呼它为重量级索。但是随着JavaSE1.6对synchronized进行了各种优化之后,有些情况synchronized它不在那么重了。接下来阐述的知识点是关于偏向锁、轻量级锁,以及锁的存储结构和升级过程。

synchronized在Java中三种表现形式

对于普通同步方法,锁的是当前实例对象

对于静态同步方法,锁的是类的class对象

对于同步方法块,锁的是synchronized括号里配置的对象

当一个线程试图访问同步代码块时,它首先必须得到锁,退出或者抛出异常时,必须释放锁。name锁到底存在哪里呢?锁里面存的又是什么信息呢?

在JVM规范中可以看到synchronized在JVM中的实现原理,JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,两者实现的细节不一样。代码块使用monitorenter和monitorexit指令实现,而方法同步使用的是另外一种情况,这个在JVM规范中并没有讲解。但是,方法的同步也可使用这两个指令来实现。

monitorenter指令是在编译后插入到同步代码块的开始位置

monitorexit指令插入到方法结束的位置和异常处

JVM要保证每个monitorenter必须有与之对应的monitorexit配对

任何对象都有一个monitor与之关联

当一个monitor被持有后,它将处于锁定状态

线程执行monitorenter指令时,将会尝试获取对象对应的monitor的所有权,即尝试获得对象的锁

2.1Java对象头

synchronize用的锁存在Java对象头里。如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果对象是非数据类型,则虚拟机用2个字宽存储对象头。在32位虚拟机中1字宽等于4字节,即32bit。

Java对象头的长度

长度内容说明32/64bitMarkWord存储对象的hashCod或锁信息32/64bitClassMetadataAddress存储到对象类型数据的指针32/64bitArraylength数组的长度(如果当前对象是数组)

Java对象头的MarkWord默认存储的是对象的hashCode、分代年龄和锁标志位。32位JVM的MarkWord的默认存储结构如表

Java对象头存储结构

锁状态25bit4bit1bit是否是偏向锁2bit锁标志位无锁状态对象的hashCode对象的分代年龄001

在运行期间,MarkWord里存储的数据会随着锁标志位的变化而变化,内容较为复杂,图先不讲了,有兴趣的可以查看相关书籍信息

2.2锁升级与对比

JavaSE1.6为了减少获得锁和释放锁带来性能消耗,引入了“偏向锁”和“轻量级锁”,在JavaSE1.6中,锁一共有四种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,这几个状态会随着竞争情况逐渐升级。注意:锁的升级是不可逆的,意味着偏向锁升级为轻量级锁之后是不能降级为偏向锁的。

2.1.1偏向锁

HotSpot的作者研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获取的,为了让线程获得锁的代价更低,引入了偏向锁。注意:这个是设计偏向锁的原因和解决思路

2.1.1.1偏向锁的获取

当一个线程访问同步代码块并获取锁时,会在对线头和栈帧中的锁记录中存储锁偏向的线程ID,以后该线程在进入和退出同步代码块的时候,不需要进行CAS操作来加锁和解锁,只需要简单的测试一下对线头MarkWord里是否存储了当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要在测试一下MarkWord中偏向锁标识是否被设置成立1(表示当前是偏向锁):如果没有设置则使用CAS竞争锁;如果这事了,则尝试使用CAS将当前对象头的偏向锁指向当前线程。

2.1.1.2偏向锁的撤销(非常妙这里)

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁是,持有偏向锁的线程才会释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它首先会暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的MarkWord要么重新偏向其他线程,要么恢复到无锁标记活着标记该对象不适合作为偏向锁,最后唤醒暂停的线程

2.1.1.3关闭偏向锁

Java1.61.7偏向锁是默认开启的,但是它在应用程序启动几秒钟后才会激活,我们可以修改JVM参数来关闭延迟,或者确定应用程序里所有的锁通常情况下都是出于竞争状态,可以直接关闭偏向锁

#关闭偏向锁延迟-XX:BiasedLockingStartupDelay=0#关闭偏向锁程序默认进入轻量级锁状态-XX:-UseBiasedLocking=false2.1.2轻量级锁2.1.2.1轻量级锁加锁

线程在执行同步代码块之前,JVM会在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中MarkWord复制到锁记录中,官方称为DisplacedMarkWord。然后线程尝试使用CAS将对象头中的MarkWord替换为指向锁记录的指针。如果成功,当前线程获得锁;如果失败,表示其他线程竞争锁,当前线程便尝试自旋获得锁。

2.1.2.2轻量级锁解锁

轻量级锁解锁时,使用的是CAS操作将DisplacedMarkWord替换回对象头,如果成功则表示没有竞争;如果失败,表示当前锁存在竞争,锁就会膨胀为重量级锁。注意:由于自旋过程消耗CPU,为了避免无用的自旋,一当升级为重量级锁,那么就不会再恢复到轻量级锁状态。当前锁出于重量级锁状态时,其他线程尝试获取锁,都会被阻塞,当持有锁的线程释放锁后会唤醒这些线程,重新进行锁的争夺。

2.1.3锁的优缺点对比

锁的优缺点对比

锁优点缺点适用场景偏向锁加锁和解锁过程不需要额外的消耗,和执行非同步方法相比仅存在纳秒级别的差异如果线程存在锁竞争,会带来额外的锁撤销开销适用于只有一个线程访问同步代码块轻量级锁竞争的线程不会阻塞,提高了程序的响应速度如果始终得不到锁竞争的线程,使用自旋会消耗CPU追求响应时间,同步代码块执行速度非常快重量级锁线程竞争不适用自旋,不会消耗CPU线程阻塞,响应时间慢追求吞吐量,同步块执行速度较长3、Java中如何实现原子操作

在Java中可以通过锁和循环CAS的方式实现原子操作

3.1使用循环CAS实现原子操作

JVM中的CAS操作利用了处理器提供的CMPXCHG指令实现。自旋CAS实现的基本思路就是循环进行CAS操作知道成功为止,示例代码实现一个安全的计数器和非安全的计数器。

packagecom.liziba;importjava.util.ArrayList;importjava.util.List;importjava.util.concurrent.atomic.AtomicInteger;/***@autherLiZiBa*@date2021/2/2817:39*@description:计数器实现**/publicclassCounter{//安全计数器统计数privateAtomicIntegeratomicInteger=newAtomicInteger(0);//非安全计数器统计数privateinti=0;publicstaticvoidmain(String[]args){finalCountercas=newCounter();List<Thread>threads=newArrayList<>(600);longstart=System.currentTimeMillis();for(intj=0;j<100;j++){Threadt=newThread(()->{for(inti=0;i<10000;i++){//非安全计数器cas.count();//安全计数器cas.safeCount();}});threads.add(t);}//启动线程threads.forEach(t->t.start());//join等待所有线程执行完毕for(Threadt:threads){try{t.join();}catch(InterruptedExceptione){e.printStackTrace();}}//输出不安全计数器结果、安全计数器结果、程序执行时间System.out.println(cas.i);System.out.println(cas.atomicInteger.get());System.out.println(System.currentTimeMillis()-start);}/***安全计数器*/privatevoidsafeCount(){i++;}/***非安全计数器*/privatevoidcount(){for(;;){inti=atomicInteger.get();//CAS增加//注意使用++ibooleanset=atomicInteger.compareAndSet(i,++i);//设置成功退出死循环if(set){break;}}}}#运行结果7113191000000265Processfinishedwithexitcode0

JDK1.5开始,JDK并发包提供了一些类支持原子操作,如AtomicBoolean,AtomicInteger,AtomicLong,对应不同类型的原子操作,这些类提供了非常有用的工具方法,比如原子自增和自减等等。

3.1.1CAS实现原子操作的三大问题

ABA问题:因为CAS需要在操作值的时候,检查值是否发生了变化,如果没有变化则更新,但是如果一个值从A修改为B又修改为A,那么使用CAS就无法发现值发生了变化,但实际上发生了变化。解决方案如下

使用版本号解决,将原本的A-->B-->A问题变成1A-->2B-->3A则可以解决

使用JDKAtomic包里提供的AtomicStampedReference来解决ABA问题,这个类compareAndSet方法会首先比较当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等则以原子方式替换,源码如下。

publicbooleancompareAndSet(VexpectedReference,VnewReference,intexpectedStamp,intnewStamp){Pair<V>current=pair;returnexpectedReference==current.reference&&expectedStamp==current.stamp&&((newReference==current.reference&&newStamp==current.stamp)||casPair(current,Pair.of(newReference,newStamp)));}

循环时间长开销大:自旋CAS如果长时间不成功,会给CPU带来非常大的开销。解决这个问题需要JVM能支持处理器提供的pause指令,效率会有一定的提升。pause指令的两个作用如下

延迟流水线执行指令(de-pipeline),使CPU不会消耗过多执行资源

避免在退出循环时内存顺序冲突(MemoryOrderViolation)而引起CPU流水线被清空

只能保证一个共享变量的原子操作:对多个共享变量进行操作的时候CAS无法保证原子性,解决方案如下

使用锁

变量合并

使用JDK提供的AtomicReference类来保证引用对象之间的原子性,将多个变量放置于对象中

3.2使用锁机制实现原子操作

锁机制保证了只有获得了锁的线程才能操作锁定的内存区域。JVM内部实现了很多锁,偏向锁、轻量级锁、重量级锁。但是除了偏向锁,JVM实现锁的方式都使用了循环CAS机制,即一个线程想进入同步代码块的时候,使用循环CAS的方式获取锁,当它退出同步代码块的时候使用循环CAS来释放锁

参考资料

《Java并发编程的艺术》--方腾飞魏鹏程晓明著

百度百科

CSDN部分博客

声明声明:本网页内容为用户发布,旨在传播知识,不代表本网认同其观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:11247931@qq.com
苹果电脑电池充不进电苹果电脑充不进去电是怎么回事 苹果电脑不充电没反应苹果电脑充电指示灯不亮充不了电怎么办 狗狗更加忠诚护家、善解人意,养一只宠物陪伴自己,泰迪能长多大... 描写泰迪狗的外形和特点的句子 国外留学有用吗 花钱出国留学有用吗 !这叫什么号 百万医疗赔付后是否可以续保 前一年理赔过医疗险还能续保吗? 医疗住院险理赔后还能购买吗? 同态加密的实现原理是什么?在实际中有何应用? 家居绿植搭配也是要有讲究的,绿植搭配4个原则要知道 父亲的信作者以什么为线索组织材料时间顺序和空间顺序结合 肺炎不发烧只咳嗽怎么治疗 肺炎不发烧只咳嗽怎么回事 我儿子得的肺炎现在不发烧了,白天不怎么咳嗽了,就是晚上一躺下就咳嗽的... 线索细胞 _是什么意思? 肺炎吃中药好了,不发烧了,就是咳嗽得厉害,是什么情况 肺炎不发烧咳嗽严重是怎么回事 白血病的饮食建议 在就中国国民政府修约前,外国商品进入内地,还征收“厘金”吗?_百度知 ... 《谁动了我的奶酪》中说,随着改变而改变,而“以不变应万变”,哪个是... 艾灸可以艾去耳聋耳鸣吗 有人骂你沙逼怎么怼回去 怎样把电脑上的文件刻录在cd上 什么定位软件是最好的? SGI映像总裁下载,v4.8.91.0官方版软件介绍_SGI映像总裁下载,v4.8.91.... 大家,英语二的作文一般两篇一共能拿10分到12分吗? 拉克人俄罗斯少数民族 中国工匠的人文精神 技术原理是什么意思 voldemort 底层实现原理是什么?求大神告知。 无线电通信实现原理是什么 月经周期什么意思 生理周期什么意思 ...据说交满几个月之后才能使用医保。想知道具体需要交满几个月?请帮... ...离职两个月了了,社保医保都没有自行缴纳,医保卡还能去医院用吗_百... 最新二手房交易费用湛江麻章二手房01年房对方纯收8万过户需多少钱_百度... 湛江市奇绩汽车用品有限公司怎么样? ps如何修改gif动态图上的字ps如何修改gif动态图上的字样 你知道哪些关于爱的教育故事? Excel怎样双屏分开显示? 在excel中如何实现两个文件分开显示? 怎样让一个Excel文档分屏显示 800米跑步技巧是什么800米跑步技巧具体有哪些建议呢 华为p40pro可以装内存卡吗? ...都没治好怎么办,不知道在成都青羊区这里有治疗抑郁的偏方吗... 驿字五行属什么 在《说文解字》中"驿"字五行属什么 聊聊2023年奥斯卡提名最佳动画长片