Java并发(二):底层实现原理

一. volatile

volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”

实现原理

volatile修饰的共享变量,在进行写操作的时候,会多出一条lock前缀的汇编指令lock addl $0x0,(%esp)

lock前缀的汇编指令的作用:

  1. 将当前处理器缓存回写到内存。
  2. 上述回写操作会导致其他处理器的缓存失效。

正是这两条特性保证了volatile共享变量的“可见性”。

二. synchronized

Java中的每一个对象都可以作为锁。

  • 对于普通同步方法,锁的是当前实例对象(this)
  • 对于静态同步方法,锁的是当前类的Class对象(java.lang.Class)
  • 对于同步方法块,锁的是synchronized括号里配置的对象(实例对象 / Class对象)

实现原理

synchronized是JVM层面的同步方法

在JVM里,使用monitorentermonitorexit指令实现synchronized同步。

任何对象都有一个monitor与之关联,在编译时,会在同步代码开始的位置加入monitorenter指令,在同步代码结束处和异常处加入monitorexit指令。

当线程执行到monitorenter指令时,将尝试获取要锁住的对象的monitor所有权,如果成功则锁住该对象。

1. Java对象头

synchronized用的锁保存在Java对象头里。

  • 对象头结构
长度(32/64位JVM) 内容 说明
32/64bit Mark Word 存储对象的hashCode或锁信息等
32/64bit Class Metadata 存储对象类型数据的指针
32/32bit Array length 数组的长度(当前对象是数组才有)
  • 32位JVM的Mark Word的默认存储结构
锁状态 25bit 4bit 1bit (是否为偏向锁) 2bit (锁标志位)
无锁 对象的hashCode 对象分代年龄 0 01
  • Mark Word随锁标志位的变化而变化
Java并发(二):底层实现原理_第1张图片
2. 锁的升级与对比

为了减少获得锁和释放锁带来的性能消耗,Java SE 1.6引入了偏向锁和轻量级锁。

锁的级别:无锁 -- 偏向锁 -- 轻量级锁 -- 重量级锁。

  • 偏向锁

经研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得。

当一个线程访问同步块并获取锁时,会在对象头和栈帧的锁记录里保存锁偏向的线程ID,以后该线程进入退出同步块时无需通过CAS操作来加锁解锁,只需要测试一下对象头的Mark Word里是否保存了当前线程的偏向锁。

只有当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。此时会执行偏向锁的撤销操作。

  • 轻量级锁

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

轻量级锁解锁时,线程会尝试用CAS将Displaced Mark Word替换回对象头中,如果成功,则解锁成功;如果失败,则表示锁存在竞争,膨胀为重量级锁。

优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 适用于只有一个线程访问同步块的场景
轻量级锁 竞争的线程不会阻塞,提高程序响应速度 如果始终得不到锁,自旋会消耗CPU 追求响应速度,同步块执行速度非常快
重量级锁 线程竞争不适用自旋,不会消耗CPU 线程阻塞,响应时间慢 追求吞吐量,同步块执行速度慢

三. 原子操作

原子操作是指“不可被中断的一个或一系列操作”。

1. 处理器实现原子操作

处理器会自动保证基本的内存操作的原子性,处理器保证从系统内存中读取或写入一个字节是原子的。

对于复杂的内存操作,处理器提供总线锁定缓存锁定来保证复杂内存操作的原子性。

总线锁定

使用处理器提供的一个LOCK # 信号,当一个处理器在总线上传输此信号时,其他处理器的请求将被阻塞,那么当前处理器就可以独占共享内存了。

缓存锁定

内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,会使其他处理器的缓存行失效。

2. Java实现原子操作

Java中通过锁和循环CAS来实现原子操作。

你可能感兴趣的:(Java并发(二):底层实现原理)