Java并发编程的艺术学习笔记(二) Java并发机制的底层实现原理

目录

 

2.1 volatile的应用

2.1.1 volatile定义与实现原理

2.1.2 volatile的使用优化

2.2 synchronized的实现原理与应用

2.2.1 Java对象头

2.2.2 锁的升级与对比

偏向锁

轻量级锁

2.3 原子操作的实现原理

2.3.1 CPU术语定义

2.3.2 处理器如何实现原子操作

2.3.3 Java如何实现原子操作


2.1 volatile的应用

2.1.1 volatile定义与实现原理

volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”,即一个线程修改共享变量时,另一个线程能读到这个修改的值。它不会引起上下文切换,比synchronized的执行成本更低。

java语言规范第3版对volatile定义如下:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该通过排他锁单独获得这个变量。

如果一个变量被声明为volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。

在对volatile变量进行写操作时,汇编代码中会多出一行lock指令。这条指令会引发两件事情:

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

2.1.2 volatile的使用优化

某个并发编程大师在JDK 7的并发包中新增一个队列集合类LinkedTransferQueue,在使用volatile变量时,用一种追加字节的方式来优化队列出队和入队性能。

一个对象占4字节,追加60字节,凑够64字节。因为英特尔很多处理器缓存的高速缓存行是64字节,不支持部分填充。如果不追加字节,可能头尾节点存在一个缓存行中,此时,当一个处理器试图修改头节点时,会使整个缓存行锁定,由于缓存一致性机制的作用,会导致其他处理器不能访问自己高速缓存中的尾节点。

以下两种场合不应该用这种方式:

  1. 缓存行非64字节宽的处理器
  2. 共享变量不会被频繁地写

2.2 synchronized的实现原理与应用

synchronized一直是元老级角色,被称为重量级锁。

2.2.1 Java对象头

synchronized用的锁是存在Java对象头里的。如果是数组类型,3个自宽存储对象头,非数组类型两个自宽。32为虚拟机1字宽等于4字节

Java对象头的长度
长度内容说明
32/64bitMark Word存储对象的hashCode或锁信息等
32/64bitClass Metadata Address存储到对象类型数据的指针
32/64bitArray Length数组的长度(是数组才有)

其中,Mark Word中存储对象锁相关信息

 

Mark Word的存储结构
锁状态25bit4bit1bit是否偏向锁2bit锁标志位
无锁状态对象的hashCode对象分代年龄001

 

运行期间,Mark Word随着锁标志位变化而变化

64位虚拟机下,MarkWord是64bit大小

2.2.2 锁的升级与对比

在java SE 1.6中,锁一共有4种状态,级别由低到高依次是:无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态。锁可以升级但不能降级,偏向锁升级为轻量级锁后不能降级为偏向锁,这种策略是为了提高获得锁和释放锁的效率。

偏向锁

经过研究发现,大多数情况下,锁不仅存在多线程竞争,而且总是由同一线程多次获得,为了让线程获取锁的代价更低而引入了偏向锁的概念。线程访问同步块并获取锁时,会在对象头和栈帧的锁记录里存储锁偏向的线程ID,以后该线程进入和退出同步块不需进行CAS操作来加锁和解锁,只需简单测试对象头Mark Word中是狗存储着指向当前线程的偏向锁。

偏向锁的撤销   等其他线程竞争偏向锁时,持有偏向锁的线程才会释放锁。撤销需要等待全局安全点(没有正在执行的字节码)。步骤:

  1. 暂停拥有偏向锁的线程
  2. 检查持有偏向锁的线程是否活着
  3. 如果不处于活动状态,则将对象头设置成无锁状态
  4. 如果是获得状态,则先执行拥有偏向锁的栈

轻量级锁

锁的优缺点对比


2.3 原子操作的实现原理

原子操作意为“不可被中断的一个或一系列操作”

2.3.1 CPU术语定义

2.3.2 处理器如何实现原子操作

使用基于缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。

1、使用总线锁保证原子性

所谓总线锁就是使用处理器提供的一个LOCK  #信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存

2、使用缓存锁保证原子性

总线锁将CPU和内存之间的通信锁住了,使得锁定期间,其他处理器不能操作其他内存地址的数据,开销较大

缓存锁定是指内存区域如果被缓存在处理器缓存行中,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时不在总线上声言Lock信号,而是修改内部内存地址。根据缓存一致性机制来保证原子性。缓存一致性机制会阻止同时修改两个以上处理器缓存的内存区域数据。当其他处理器回写以被锁定的缓存行数据时,会使缓存行无效

有两种情况下处理器不会使用缓存锁定

第一种:操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行时

第二种:有些处理器不支持缓存锁定

2.3.3 Java如何实现原子操作

在java中可以通过循环CAS实现原子操作

使用循环CAS实现原子操作

自旋CAS基本思路是循环进行CAS操作直到成功为止。

CAS实现原子操作的三大问题

1、ABA问题

操作值时检查值有没有发生变化,如果A->B->A检查时发现没变化,实际上变化了。解决方式是用版本号。

2、循环时间长开销大

3、只能保证一个共享变量的原子操作

对多个变量操作时,循环CAS无法保证原子性,这个时候可以用锁。

使用锁机制实现原子操作

除了偏向锁,JVM实现锁的方式都用了循环CAS。

 


版权声明:本文为zj574406254原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。