java并发编程的艺术之并发机制的底层实现原理

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对象头中。

java对象头存储说明
长度 内容 说明
32/64bit Mark Word 存储对象的的hashCode或锁信息等
32/64bit Class Meta Address 存储对象类型数据的指针等
32/64bit Array Length 数组的长度(如果当前对象是数组)
Mark Word的状态变化
锁状态 23bit 2bit 4bit 1bit(是否偏向锁) 2 bit(锁标志位)
轻量级锁 指向栈中锁记录的指针 00
重量级锁 指向互斥量(重量级锁)的指针 10
GC标记 11
偏向锁 线程ID Epoch 对象分代年龄 1 01
Mark Word的存储结构(64bit)
锁状态 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执行效率、无法直接对多个共享变量进行原子操作
  • 使用锁机制实现原子操作

疑问

如果有疑问,欢迎讨论。

你可能感兴趣的:(读书笔记)