并发编程(二)
并发机制的底层实现
Java代码编译成Java字节码,字节码被类加载器加载到JVM中,并转换成汇编指令在CPU上执行;而Java的并发机制就依赖于JVM的实现与CPU指令。
volatile关键字
多线程并发编程synchronized和volatile都扮演者重要的角色,其中可以将volatile视为保证共享变量“可见性”的轻量级synchronized,其不会引起上下文的切换和调度;
定义与实现原理
了解volatile实现原理之前,先了解其实现原理相关的CPU术语和说明;
Lock前缀的指令在多核处理器下引发两件事情,(1)将当前处理器缓存行数据写回系统内存;(2)写回内存的操作使得其他CPU内缓存该内存地址的数据无效;
处理器不与内存通信,而是先将内存数据读取到内部缓存,但是对该数据操作完成后不知道何时写回到内存中,而volatile修饰的变量进行写操作,JVM则会向CPU发送Lock前缀指令(上述所写的),数据写回了内存;但是其他处理器的缓存还是旧值,此时依靠缓存一致性协议,每个处理器嗅探总线上传播的数据查看自身缓存数据是否过期,如过期,则设置为无效,且当下次该处理器需要进行数据修改的时候,再从内存中读取。
volatile实现原则
(1)Lock前缀指令引起处理器缓存写回内存,其中主要通过LOCK#信号确保在声言该信号的过程中,只有单一处理器独占共享内存(处理器老版本中通过锁住主线进行的,当前版本使用的锁住内存区域的缓存直至写回内存,并使用缓存一致性确保修改的原子性,此操作又称为“缓存锁定”)
(2)一个处理器的缓存写回内存将导致其他处理器的缓存无效;使用MESI协议维护内部缓存与其他处理器缓存及系统内存之间的关系,MESI相当于建立了之间的连接总线,使得嗅探能够时刻了解当前总线的情况,如果一处理器的缓存数据进行了修改,其他的处理器的相同缓存数据则使得无效,当下次刚无效的CPU重新获取地址,连接到MESI总线,将修改的数据直接赋予。
synchronized的实现原理与应用
Java中的每一个对象都可以作为锁,具体表现为3中形式:(1)对于普通同步方法,琐是当前实例对象;(2)对于静态同步方法,锁是当前类的Class对象;(3)对于同步方法块,锁是Synchronized括号内配置的对象
Synchronized的实现主要分为两类(本质都是基于Monitor对象实现):(1)锁代码块的时候,使用monitorenter和monitorexit实现;(2)锁方法的时候,使用ACC_SYNCHRONIZED指令执行。
对象头
对象如果是数组类型,虚拟机使用3字宽存储对象头,如果非数组类型使用2字宽存储对象头;
其中运行期间,Mark Word中存储的数据会随着锁标志位的变换而变换
锁升级与对比
(1)偏向锁,synchronized开始就是偏向锁(此时某个时间段内只有一个线程争夺资源);
步骤:
a.头次进入将Mark Word的锁标志位设置为01,进入偏向模式
b.如果判定为偏向状态,查看Mark Word中的线程ID是否与外部线程ID一致,如一致则进入同步块
c.如果线程ID不同,那么使用CAS进行竞争锁操作,如果竞争成功将Mark Word中线程ID修改为当前线程ID,进入同步块。
d.如果修改失败,必须等到其他线程竞争偏向锁,才能开始释放,进而等待全局安全点(时间点上没有执行的字节码),暂停所有偏向锁的线程,然后依据线程的活跃程度进一步确定。
(2)轻量级锁,当偏向锁处于运行状态的时候,此时出现了第二个线程进行锁竞争,这时偏向锁升级为轻量级锁(两个线程的竞争并非同时)
步骤:
a.代码进入同步块中,如果同步对象是无锁状态(是否偏向锁“0”,锁标志位“01“),当前线程的栈创建存储锁记录的空间(Lock Record),存储对象Mark Word的拷贝
b.CAS尝试将对象Mark Word更新为指向Lock Record的指针,如果成功标识线程拥有该对象的锁,并将Mark Word的锁标志位设置为”00“;
c.如果失败,检查Mark Word中线程ID是否指向线程ID,如果则进入同步块,否则说明多个线程正在竞争(2个或多个锁同时竞争),升级为重量级锁,标志位设置为”10“并开始自旋以获取锁。
原子操作的实现原理
原子本意是“不能被进一步分割的最小粒子”,而原子操作意为“不可被中断的一个或一系列操作”;
处理器通过两种方式保证原子操作:(1)使用总线锁保证原子性,某处理器使用LOCK#信号时,其他处理器请求将被阻塞;(2)使用缓存锁保证原子性,其本质是保证内存地址的原子性,将频繁使用的内存缓存到处理器的高速缓存中,在Lock操作期间被锁定,执行锁操作回写到内存时,不声明LOCK#信号,而是修改内部内存地址,通过缓存一致性机制保证操作的原子性。注:两种情况使得处理器不会使用缓存锁定,(1)操作的数据不能被缓存到处理器内部或操作的数据跨多个缓存行时,处理器会调用总线锁定;(2)某些处理器不支持缓存锁定。
Java实现原子操作
Java通过锁和循环CAS的方式实现原子操作;(1)使用循环CAS实现原子操作,循环进行CAS操作直至成功,其中JDK的并发包提供了一些类来支持原子操作,如AutomicBoolean;(2)CAS实现原子操作的三大问题,即ABA问题、循环时间开销大的问题以及只能保证一个共享变量的原子操作
a)ABA问题,A->B->A导致误认为没有修改操作,其实有修改操作但是最终变回了原本值,导致没有觉察导致出现错误;解决方法就是在变量前面添加版本号,如A -> B -> A最终变成1A -> 2A -> 3A;代码实现就是通过JDK的Atomic包提供的AtomicStampedReference解决问题,该类的compareAndSet方法的作用a1)检查当前引用是否等于预期引用;(a2)检查当前标志是否等于预期标志;(a3)如果全部相等,通过原子方式将引用和标志值设置为给定的更新值。
b)循环开销大的问题,自旋CAS如果长时间不成功,将会给CPU带来很大的执行开销,如果JVM支持处理器提供的pause指令可以实现两方面优化(1)延迟流水线指令(de-pipeline),使CPU不会消耗过多的执行资源;(2)避免退出循环时因内存顺序冲突导致CPU流水线被清空,进而提高CPU执行效率
c)只能保证一个共享变量的问题,多个变量时CAS无法保证操作的原子性,但可以通过(1)加锁方式,synchronized(2)多个共享变量合并称为一个共享变量进行操作,JDK中的AtomicReferen ce类保证对象之间的原子性。
(3)使用锁机制实现原子机制,只有获得锁的线程才能操作锁定的内存区域,JVM的锁机制除了偏向锁,其他都是斗个循环CAS实现的锁,当一个线程想进入同步块的时候使用循环CAS获得锁,同理想退出的时候使用循环CAS释放锁。