Java并发编程(二)——Java并发底层实现原理

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

volatile

在多线程并发编程中,synchronized和volatile都很重要,volatile是轻量级的synchronized,它在多处理器的开发中,保证了共享变量的可见性。*可见性指一个线程修改这个共享变量时,另外的线程能够读到这个修改的值。*volatile的成本更低,不会引起线程上下文的切换和调度。

volatile的定义和实现原理

Java语言规范中定义:Java编程语言允许线程访问共享变量,为了确保共享变量能被准备和一致地更新,线程应该确保通过排他锁单独获取这个变量。

要了解volatile的实现原理,首先要了解一些CPU的术语和说明

术语英文描述
内存屏障Memory barriers是一组处理器指令,用于实现对内存操作的顺序限制
缓存行cache lineCPU高速缓存中可以分配的最小存储单位,处理器填写缓冲行时会加载整个缓存行,现在CPU需要执行几百次CPU指令
原子操作atomic operation不可中断的一个或一系列操作
缓存行填充cache line fill当处理器识别到从内存中读取操作数是可缓存的,处理器读取整个高速缓存行到适当的缓存
缓存命中cache hit如果进行高速缓存行填充操作的内存位置仍然是下次处理器访问的地址时,处理器冲缓存中读取操作数,而不是内存
写命中Write hit当处理器将操作数写回一个内存缓存的区域时,它首先会检查这个缓存的内存地址是否在缓存行中,如果存在一个有效的缓存行,则处理器将这个操作数回写到缓存,而不是内存
写缺失Write misses the cache一个有效的缓存行被写入到不存在的内存区域

有volatile修饰的变量,进行写操作的时候会出现lock指令,lock指令在多核处理器下引发两件事情

  1. 将当前处理器缓存行的数据写回到系统内存
  2. 这个写回内存的操作会使其它CPU里缓存了该内存地址的数据无效

为了提供处理速度,处理器不直接和内存通信,而是通过高速缓存工作。如果对声明了volatile的变量进行了写操作,JVM就会向处理器发送一条lock前缀的指令,将这个变量所在的缓存行的数据写回到系统内存。
就算写会了系统内存,其它处理器缓存的值还是旧的。所以在多处理器下,为保证缓存的一致性,会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效,当处理器对这个数据进行操作时,会重新从系统内存中把数据读到处理器缓存里。

synchronized的实现原理

synchronized是Java的重量级锁,但是JDK1.6优化之后,也不是那么重量级了。

synchronized实现同步的基础:Java中的每一个对象都可以作为锁

  1. 对于普通同步方法,锁是当前实例对象
  2. 对于静态同步方法,锁是当前类的Class对象
  3. 对于同步方法块,锁是synchronized括号里配置的对象

JVM基于进入和退出monitor对象来实现方法同步和代码块的同步。代码块的同步是基于monitorenter和monitorexit指令实现的,而方法同步是使用另一种实现的。

monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit指令是插入到方法结束处和异常处,JVM保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之相关,当一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令,就会尝试去获取对象所对应的monitor的所有权,即锁。

Java对象头

synchronized用的锁存在Java对象头里面。如果对象是数组类型,则用3个字宽;如果是非数组则2个字宽。在32位虚拟机里,一个字宽等于4个字节,即32Bit。

长度内容说明
32/64Mark Word存储对象的hashcode和锁信息等
32/64Class Metadata Address存储到对象类型数据的指针
32/64Array length数组的长度(如果对象是数组的话)

Mark Word是定长但是非结构的,会随着锁标志位的变化而变化
默认存储结构

锁状态25Bit4Bit1Bit是否是偏向锁2Bit锁标志位
无锁状态对象的HashCode对象GC分代年龄001

运行期间

锁状态25Bit4Bit1Bit是否是偏向锁2Bit锁标志位
轻量级锁指向栈中锁记录的指针(30Bit)000
重量级锁指向互斥量(重量级锁)的指针(30Bit)010
GC标志11
偏向锁线程ID(23Bit)+epoch(2Bit)对象GC分代年龄101

锁升级与对比

Java为了减少获取锁和释放锁的性能消耗,引入偏向锁,轻量级锁。锁一共有四种状态,级别从低到高依次是:无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态。这几种状态会随竞争情况升级,但是不能降级。

偏向锁

HotSpot的作者发现,锁不仅不存在多线程的竞争,而且总是由同一个线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存放锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁或解锁,只需要简单测试一下对象头的Mark Word是否存储着指向当前线程的偏向锁。如果成功,则线程已经获得了锁;如果失败,需要在测试Mark Word中偏向锁标志是否为1。如果没有设置,采用CAS竞争锁;如果设置了,采用CAS将对象头的偏向锁指向当前线程。

偏向锁的撤销:偏向锁是一种等到竞争出现才释放锁的机制,所以当其它线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。

偏向锁在Java6和Java7中默认是启动的

轻量级锁

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

解锁
解锁时,会使用原子的CAS操作将Displaced Mark Word替换回对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁会膨胀为重量级锁。

因为自旋会消耗CPU,为了避免无用的自旋,一旦锁升级为重量级锁就不会恢复到轻量级锁状态。当锁处于重量级状态时,其它线程试图获取锁时,都会被阻塞,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程会开始新一轮竞争。

锁的优缺点对比

优点缺点场景
偏向锁加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距如果线程间存在锁竞争,就会带来额外的锁撤销的消耗适合只有一个线程访问同步块的场景
轻量级锁竞争的线程不会阻塞,提高了程序的响应速度如果始终得不到锁竞争的线程,使用自旋会消耗CPU追求响应时间,同步块执行速度非常快
重量级锁线程竞争不使用自旋,不会消耗CPU线程阻塞,响应时间缓慢追求吞吐量,同步块执行速度较长
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值