《java并发编程的艺术》第二章读书笔记

volatile

关于volatile的介绍
volatile比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。

Cpu术语

在这里插入图片描述

深入底层了解volatile

在X86处理器下通过工具获取JIT编译器生成的汇编指令来查看对volatile进行写操作时,CPU会做什么事情

java代码

instance=new Singleton(); //instance是volatile变量

汇编代码

Ox01a3deld: movb $0X0,0X1104800(%esi);Ox01a3de24:lock addl $0X0(%esp);

有volatile变量修饰的共享变量进行写操作的时候会多出第二句汇编代码,Lock前缀的指令在多核处理器下会引发两件事

1.将当前处理器缓存行的数据写回到系统内存
2.这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。(所以就需要它们又重新会内存拿新数据)

synchronized的实现原理与应用

人们通常称呼它为重量级锁,但jdk6以后进行了各种优化之后,有些情况它也并不那么重了。

基础
java每一个对象都可以作为锁。具体表现为3种形式
(1)对于普通同步方法,锁是当前实例对象。
(2)对于静态同步方法,锁是当前类的Class对象。
(3)对于同步方法块,锁是Synchonized括号里配置的对象

原理
参考文章
参考文章

Java对象头

synchronized用的锁是存放在Java对象头里的。如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,1字宽等于4字节即32bit。
在这里插入图片描述

锁的升级与对比

锁的级别从低到高依次是:无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态。锁只能升级不能降级,目的是为了提高获得锁和释放锁的效率。

偏向锁
参考文章

轻量级锁
线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。(CAS参考文章 //////CAS参考文章

轻量级锁解锁
轻量级解锁时,会使用原子的CAS操作将栈帧中存储的锁记录替换回到对象头中,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

自旋
自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。

优缺点对比
在这里插入图片描述

原子操作的实现原理

原子操作:不可被中断的一个或一系列操作。
在这里插入图片描述
处理器如何实现原子操作
处理器通过缓存加锁总线加锁来实现多处理器之间的原子操作,处理器会自动保证基本的内存操作的原子性。

总线加锁
总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。

缓存加锁
在同一时刻,我们只需保证对某个内存地址的操作是原子性即可,但总线锁定把CPU和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,目前处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。
频繁使用的内存会缓存在处理器的高速缓存里,那么原子操作就可以直接在处理器内存缓存中进行。并不需要声明总线锁。
缓存锁定是指内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效。

两种情况处理器不会使用缓存锁定
1.当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行时,则处理器会调用总线锁定。
2.有些处理器不支持缓存锁定。
针对以上两个机制,可以通过Intel处理器提供的Lock前缀的指令来实现。例如位测试和修改指令:BTS,BTR,BTC;交换指令XADD,CMPXCHG,以及其他一些操作数和逻辑指令(如ADD,OR)等,被这些指令操作的内存区域就会加锁。

java是如何实现原子操作的
java中可以通过锁和循环CAS的方式实现原子操作

(1)使用循环CAS实现原子操作
JVM中的CAS操作正是利用了处理器提供的CMPXCHG指令实现的。

(2)使用锁机制实现原子操作
锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。JVM内部实现了很多种锁机制,有偏向锁,轻量级锁和互斥锁。除了偏向锁,JVM实现锁的方式都用了循环CAS,即当一个线程想进如同不快的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁。

CAS三大问题
(1)ABA问题
CAS在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,实际发生了变化。解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本加1,那么A-B-A就会变成1A-2B-3A。

(2)循环时间开销大。
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升。pause指令有两个作用:第一,它可以延迟流水线执行指令,使CPU不会消耗过多的执行资源,第二,它可以避免在退出循环的时候因内存顺序冲突而引起CPU流水线被清空,从而提高CPU的执行效率。

(3)只能保证一个共享变量的原子操作。
CAS只能保证对一个共享变量执行操作的时候为原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性。这个时候可以用锁。

猜你喜欢

转载自blog.csdn.net/weixin_45593271/article/details/107541394