目 录CONTENT

文章目录

第二章 Java并发机制的底层原理

FatFish1
2024-10-21 / 0 评论 / 0 点赞 / 68 阅读 / 0 字 / 正在检测是否收录...

volatile的作用和原理

如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值一样的。

首先了解cpu和内存交互模型,可参考三级缓存部分。此外再理解几个术语:

  • 内存屏障:本质是一组处理器指令,用于对内存操作的顺序限制

  • 缓冲行:缓存行,缓存中可分配的最小存储单位

  • 缓存行填充:当处理器识别到从内存中读取操作数是可缓存的,处理器读取整个缓存行到适当的缓存(L1、L2、L3或所有)

  • 缓存命中:高速缓存有对应值,则下次操作从高速缓存中取值,不需要从内存读取

  • 写命中:CPU写处理结果时,先不直接往内存写,而是看下三级缓存里面有没有对应缓存行,如果有,优先往高速缓存里面写

  • 写缺失:一个有效的缓存行被写入到不存在的内存区域

volatile的字节码原理

对于创建一个volatile变量,字节码如下:

0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);

即有一个lock指令,这个指令的作用是:

  • 强制处理器缓存行的数据写回系统内存

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

这里使其他CPU无效的方法就是总线嗅探和MESI原则,在写回的时候通过总线发送消息变更其他CPU里面对应数据的状态

MESI=modified+exclusive+shared+invalidated,具体可以参考三级缓存部分

这里其实是volatile支持了内存模型的可见性。此外volatile还支持有序性。但是volatile并不支持原子性。

synchronized的作用和原理

synchronized是重量级锁,锁机制:

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

  • 对于静态同步方法,锁是当前类class对象

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

synchronized对同步方法块的锁是通过monitor字节码实现的。monitor的方法块前面加一个monitorenter,后面加一个monitorexit,线程执行到monitorenter,将持有下面代码块的monitor,且必须先有一个monitorexit与之对应

java对象头

java对象头有三部分:Mark Word、Class Metadata Address、Array length

在32位/64位JVM中Mark Word分别为32bit/64bit,Class Metadata Address一样,而Array length固定32bit

锁类型与升级

锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级,但是不能降级

不同状态的锁,java对象头的Mark Word就会有差别:

偏向锁

获取:适应竞争极少的情况,这种情况一般都是一个线程持续获取锁,为了节省开销

释放:这个级别的锁一般不自行释放,除非有人竞争,下次来只需要判断一下Mark Word中偏向锁的线程id是自己,就可以继续用,也省去了CAS的加锁释放锁流程。

轻量级锁

获取:JVM会在当前线程栈帧中创建专门的空间用于存储锁记录,同时执行CAS操作将对象头中的Mark Word进行指针替换到当前线程,成功才能获取锁,失败的话表示有竞争,失败的线程开始自旋,但是对cpu性能有一定影响

释放:轻量级锁通过CAS操作回滚对象头,如果发生竞争,膨胀为重量级锁,因为轻量级锁要自旋,有性能影响,如果锁竞争激烈,升级到重量级了,就不自旋了,直接阻塞。

重量级锁

获取:获取不到锁的,不再自旋,也不消耗CPU,而是直接阻塞线程,响应时间就变慢了

锁抢占与Mark Word变化

  1. 当没有被当成锁时,这就是一个普通的对象,Mark Word记录对象的HashCode,锁标志位是01,是否偏向锁那一位是0。

  2. 当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1,前23bit记录抢到锁的线程id,表示进入偏向锁状态。

  3. 当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,Mark Word中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码。

  4. 当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是Mark Word中的线程id记录的不是B,那么线程B会先用CAS操作试图获得锁,这里的获得锁操作是有可能成功的,因为线程A一般不会自动释放偏向锁。如果抢锁成功,就把Mark Word里的线程id改为线程B的id,代表线程B获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,则继续执行步骤5。

  5. 偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁Mark Word中保存指向这片空间的指针。上述两个保存操作都是CAS操作,如果保存成功,代表线程抢到了同步锁,就把Mark Word中的锁标志位改成00,可以执行同步锁代码。如果保存失败,表示抢锁失败,竞争太激烈,继续执行步骤6。

  6. 轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。从JDK1.7开始,自旋锁默认启用,自旋次数由JVM决定。如果抢锁成功则执行同步锁代码,如果失败则继续执行步骤7。

  7. 自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10。在这个状态下,未抢到锁的线程都会被阻塞。

原子操作的原理

原子操作就是不能被分割的一个或一系列操作。原子操作的相关术语包括:

  • 比较并交换:Compare and Swap,即CAS操作,两个数值,先比较操作期间旧值是否有变化,没有才交换成新值

  • 内存顺序冲突:由假共享引起的,即多个CPU同时修改一个缓存行的不同部分而引起其中一个CPU的操作无效。当出现内存顺序冲突,CPU必须清空流水线。

处理器如何实现原子操作

总线锁

例如多线程同时对一个变量i,执行i++操作,是非原子的,两个CPU可能产生冲突,不加锁可能就得到一个错误数据

这个时候可以使用volatile,使i增加一个LOCK指令,要求CPU改写前先进行总线广播

缓存锁

三级缓存里面,L1、L2、L3中,L3是核心间共享的,L1和L2是核心独享的。如果只针对缓存做锁定,可以减少对内存和总线锁定产生的压力。缓存锁阻止同时修改两个以上处理器缓存的内存区域数据。其他处理器写回操作会使缓存行无效。

java如何实现原子操作

JVM使用CAS实现原子操作,例如AtomicInteger,是通过处理器的CMPXCHG指令+循环自旋实现的。例如CompareAndSet,不断判断老值未发生变化才做修改,如果变化了,不断自旋获取。

但是CAS实现原子操作也有问题:

  • ABA问题:比如老值从A变成B变成A,也会判断不变。

  • 循环时间长开销大:CAS自旋不成功,会给CPU产生压力

0

评论区