一. volatile
volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。
实现原理
volatile修饰的共享变量,在进行写操作的时候,会多出一条lock前缀的汇编指令lock addl $0x0,(%esp)
。
lock前缀的汇编指令的作用:
- 将当前处理器缓存回写到内存。
- 上述回写操作会导致其他处理器的缓存失效。
正是这两条特性保证了volatile共享变量的“可见性”。
二. synchronized
Java中的每一个对象都可以作为锁。
- 对于普通同步方法,锁的是当前实例对象(this)
- 对于静态同步方法,锁的是当前类的Class对象(java.lang.Class)
- 对于同步方法块,锁的是synchronized括号里配置的对象(实例对象 / Class对象)
实现原理
synchronized是JVM层面的同步方法。
在JVM里,使用monitorenter
和monitorexit
指令实现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随锁标志位的变化而变化
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来实现原子操作。
网友评论