synchronized(偏向锁和轻量级锁)(TODO)
synchronized
JDK1.6对synchronized进行了各种优化,性能已经和ReentrantLock差不多了。
Java中的每一个对象都可以作为锁。具体表现为以下3种形式。
- 对于普通同步方法,锁是当前实例对象。
- 对于静态同步方法,锁是当前类的Class对象。
- 对于同步方法块,锁是Synchonized括号里配置的对象。
JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。
代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有详细说明。
但是,方法的同步同样可以使用这两个指令来实现。
Java对象头
如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,1字宽等于4字节,即32bit。
Java对象头的长度
Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位。
Mark Word的存储结构
Mark Word的状态变化
在64位虚拟机下,Mark Word是64bit大小
64bit的Mark Word的存储结构
锁升级与对比
JDK1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。
锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级,锁可以升级但不能降级。
偏向锁
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出
同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否
存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需
要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则
使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
偏向锁撤销
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,
持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正
在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,
如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈
会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他
线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。
偏向锁初始化
书上说了一大堆,简单来说就是分两种场景,一种是有竞争,一种是无竞争
无竞争:
无竞争的场景可以假设是一个单线程请求synchronized锁,当第一次请求锁时,锁的对象头中的偏向锁线程ID是空,状态是0。
锁请求成功后,JVM会将这个线程ID写入对象头,偏向锁线程ID不为空,状态是1,然后该线程执行完同步代码后释放锁,接着又继续请求获取锁。
然后判断锁的请求头中的偏向锁状态是否为1,为1则再判断偏向锁的线程ID是否是该线程ID,如果是则直接进入临界区执行同步代码,无需加锁和解锁的操作。
有竞争:
有竞争的场景的第一个请求锁的线程同无竞争场景一样,假设线程A请求锁成功,设置偏向锁状态为1,记录偏向锁线程ID,但由于存在锁竞争,第二个线程B请求锁时,发现偏向锁状态是1,
但是锁的对象头中偏向锁记录的线程ID并不是自己,这时就需要撤销偏向锁,撤销之前必须要检查线程A是不是还活着,因为此时线程A可能正在临界区内执行同步代码,也可能线程A活的很滋润,在另外一把锁的临界区内执行同步代码,也可能代码执行完毕已经释放锁然后线程池中等待复用,也可能代码执行完毕且线程已经死亡。
所以线程B请求锁时发现偏向锁的线程ID不是自己,JVM会暂停对象头中记录的偏向锁ID。
如果线程A暂停失败说明线程死亡,线程B直接将对象头设置成无锁状态
如果线程A暂停成功说明线程A活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,
要么锁状态恢复到无锁(锁不升级)或者标记锁对象不适合作为偏向锁(升级为轻量级锁),最后唤醒暂停的线程。
这里有个疑惑,TODO 一下,什么情况锁不升级,什么情况锁才升级?
我的想法是,由于锁只能升级不能降级,那么盲目升级锁势必会增大锁开销而降低性能。所以线程B发现锁的偏向线程ID不是自己还需要一些判断再决定是否需要升级锁。
即如果线程A活着,但是线程A处于挂起状态或者当前线程A在活动中且栈信息中的锁信息不是线程B请求的锁,则锁状态置位无效,不升级锁
如果线程A正在锁的临界区执行同步代码,则需要锁升级,升级为轻量级锁。
所以总结下来,偏向锁请求过程如下:(这里只说偏向锁的请求,即所标志为=01的情况)
判断锁对象的对象头中的Mark Word是否为无锁状态(锁状态=0)
a.是,CAS设置锁状态为1,偏向锁ID为当前线程ID
b.否:判断锁状态是否为1
a.是,继续判断偏向锁ID是否为当前请求线程ID
a.是,无需加锁和解锁,直接进入临界区执行同步代码
b.否,判断偏向锁ID对应的线程是否存活
a.是,暂停线程,判断线程栈中的锁对象信息是否为当前请求的锁对象信息
a.是,存在竞争,当前线程请求的锁已被其他线程占用且正在临界区执行同步代码,锁升级为轻量级锁,在暂停的线程栈中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到 锁记录中,使用CAS将对象头中的Mark Word替换为指向锁记录的指针,恢复之前暂停的线程
b.否,没有线程在当前锁对象的临界区内,修改锁的偏向线程ID为当前线程或重置为无锁状态,CAS设置锁状态为1,偏向锁ID为当前线程ID。恢复之前暂停的线程
b.否,锁已升级,不使用偏向锁
轻量级锁
1)加锁
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并
将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用
CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失
败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
2)解锁
轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成
功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
锁膨胀流程
因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级
成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,
都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮
的夺锁之争。
总结轻量级锁请求过程:
判断锁对象的对象头中的Mark Word是否为无锁状态(锁状态=0)
a.是,在栈帧中创建锁记录空间,复制锁对象头的Mark Word到锁记录中,CAS将锁对象的对象头中的Mark Word替换为指向锁记录的指针。
a.替换成功,则加锁成功,执行同步代码,执行完毕后CAS操作将Displaced Mark Word替换回到对象头进行解锁
a.解锁成功,此时锁状态为无锁状态
b.解锁失败,说明当前线程在执行同步代码的时候有其他线程来请求锁资源,且请求失败将锁升级为重量级锁
b.替换失败
自旋,走下面的自旋流程
b.否,检查对象的Mark Word是否指向当前线程的栈帧的锁记录
a.是,说明当前线程已持有锁,直接进入同步代码
b.否,自旋,空循环自旋一段时间
a.自旋成功,获取锁成功
b.自旋失败,升级锁为重量级锁,修改锁对象的Mark Word指针指向重量级锁
锁的优缺点对比
参考:
《Java编发变成艺术》