Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM的实现和CPU的指令。
本章我们将深入底层一起探索下Java并发机制的底层实现原理。
volitate
定义
Java语言规范第3版中对volatile的定义如下:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。
Java语言提供了volatile,在某些情况下比锁要更加方便。如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。
原理
- 将当前CPU缓存中的数据写入内存,同时缓存一致性机制会阻止有两个以上CPU缓存的内存区域数据。
- 通过嗅探方式,检测其它CPU是否修改共享内存的数据。如果发现有,则使缓存的数据无效。那么下次访问数据时,则访问的就是最新的数据
synchronized
形式
- 对于普通方法,锁是当前实例对象
- 对于静态同步防范,锁是当前类的class对象
- 对于同步方法块,锁是synchronized括号内的对象
实现方式
JVM基于monitor对象实现方法和代码块的同步;任何对象都有一个monitor与之关联;两者都可以使用monitorenter和monitorexit指令来实现。具体加锁过程如下:
- monitorenter指令插入同步代码开始的部分
- monitorexit插入同步代码结束以及异常部分(如果是运行时异常呢)
- 线程执行到monitorenter指令时,尝试获取被锁对象(
参考形式
)的monitor对象的所有权,剩下的部分就不言而喻了。
存储机制
至此,我们还有一个问题没有解决,那就是被锁对象怎么存储的呢?其实,synchronized对应的锁记录在java对象头中。
长度 | 内容 | 说明 |
---|---|---|
32/64bit | Mark Word | 存储对象的的hashCode或锁信息等 |
32/64bit | Class Meta Address | 存储对象类型数据的指针等 |
32/64bit | Array Length | 数组的长度(如果当前对象是数组) |
锁状态 | 23bit | 2bit | 4bit | 1bit(是否偏向锁) | 2 bit(锁标志位) |
---|---|---|---|---|---|
轻量级锁 | 指向栈中锁记录的指针 | 00 | |||
重量级锁 | 指向互斥量(重量级锁)的指针 | 10 | |||
GC标记 | 空 | 空 | 空 | 11 | |
偏向锁 | 线程ID | Epoch | 对象分代年龄 | 1 | 01 |
锁状态 | 25bit | 31bit | 1bit (cms_free) | 4bit (分代年龄) | 1bit(锁标志位) | 2bit((是否偏向锁) |
---|---|---|---|---|---|---|
无锁 | unused | hashCode | 0 | 01 | ||
偏向锁 | ThreadID(25bit + 29bit) | Epoch(2bit) | 0 | 01 |
升级与优化
为了减少获取锁以及释放锁的性能消耗,java1.6 引入"偏向锁"以及"轻量级锁"。
- 偏向锁
- 轻量级锁
- 重量级锁
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级别的差距 | 如果线程间存在锁竞争,会带来撤销锁的消耗 | 使用只有一个线程访问同步块的场景 |
轻量级锁 | 竞争不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程,使用自旋锁会消耗CPU | 追求响应时间,同步块执行速度非常快 |
重量级锁 | 线程竞争使用阻塞方式,不会消耗CPU | 线程阻塞,响应时间慢 | 追求吞吐量,同步快执行速度较长 |
原子操作
原子操作(atomic operation)意为“不可被中断的一个或一系列操作”。
- 硬件级别的支持:总线锁定(类似锁表,有且只有当前CPU可以和内存通信),缓存锁定(类似锁行,只有当前CPU可以修改对应内存的数据)
- 循环CAS。本质上使用了CMPXCHG指令去实现。但是CAS存在ABA、自旋降低CPU执行效率、无法直接对多个共享变量进行原子操作
- 使用锁机制实现原子操作
疑问
如果有疑问,欢迎讨论。