对象头和锁
JVM的实现中,每个对象都有一个对象头,用于保存对象的系统信息。对象头中有一个Mark Word部分,里面存放对象的哈希值,对象的年龄,锁的指针,是否占用锁,哪个锁等信息
在32位系统中,Mark Word占32位,这是小端储存,从右往左看。默认状态下,对象前2位总是状态位,第三位表示是否是偏向锁,看得到不是一定有的。无锁,第4-6位会存放对象年龄,8-32位放Hash值。
锁在JVM中的优化
为了避免在操作系统层面的挂起线程,JVM自己优先解决问题,办法有n种
偏向锁
产生的原因是,大多数时候加锁只是一个保护性的措施,大多数时候并不会出现竞态。而真正出现了竞态情况,才会退出偏向模式。
启用参数:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0(表示马上启动,默认在4秒后)。在竞态环境强的时候,频繁进退偏向模式会消耗时间,可以禁用偏向锁优化。
我理解的原因是,当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁,而只需简单的测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果测试成功,表示线程已经获得了锁,如果测试失败,则需要再测试下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁),如果没有设置,则使用CAS竞争锁,如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
偏向锁的撤销:偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态,如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。
轻量锁
如果偏向锁失败,JVM会让线程申请轻量锁。轻量锁在JVM内部是以BasicObjectLock的对象来实现的.其中有lockField和objField,该对象存在于线程私有的Java栈里面。而BasicLock类中存放对象头部的Mark Word的备份。
public class BasicObjectLock extends VMObject {
private static sun.jvm.hotspot.types.Field lockField;
private static sun.jvm.hotspot.types.OopField objField;
private static int size;
}
public class BasicLock extends VMObject {
private static CIntegerField displacedHeaderField;
public Mark displacedHeader() {
return new Mark(addr.addOffsetTo(displacedHeaderField.getOffset()));
}
}
markOop mark = obj->mark();
lock->set_displaced_header(mark);
if (mark == (markOop) Atomic::cmpxchg_ptr(lock,obj()->mark_addr(),mark)){
TEVENT (show_enter: release stacklock);
}
锁膨胀->重量级锁
当轻量级锁失败,会膨胀为重量级锁.第1步是废弃前面BasicLock备份的对象头信息,第2步是通过inflate()方法进行锁膨胀,获取对象的ObjectMoniter,然后再通过enter()尝试进入该锁,在enter()方法中可能会在操作系统层面挂起线程,成本就会比较高。
lock->set_displaced_header(markOopDesc::unused_mark());
ObjectSynchronizer::inflate(THREAD, obj()) -> enter(THREAD)
自旋锁
在锁膨胀后,在操作系统挂起线程之前,JVM会做最后一次争取避免被操作系统挂起,这种操作被称为自旋锁。
自旋锁可以使线程在没有取得锁时,不被挂起,而转为执行一个空循环,执行若干个循环后,能获取锁最好,不能则由操作系统挂起。
在JDK1.6时可以通过-XX:UseSpinning开启自旋锁,使用-XX:PreBlockSpin参数设定自旋锁等待次数。在JDK1.7中完全交给JVM,它自动执行,不能控制。
public class SpinLock {
private AtomicReference<Thread> sign =new AtomicReference<>();
public void lock(){
Thread current = Thread.currentThread();
while(!sign .compareAndSet(null, current)){
}
}
public void unlock (){
Thread current = Thread.currentThread();
sign .compareAndSet(current, null);
}
}
锁消除
这是JVM在JIT编译阶段,通过上下文扫描,和逃逸分析,去除不可能出现竞态的锁。如像Vector,StringBuffer这些类。
参数:逃逸分析:+XX:+DoEscapeAnalysis,锁消除:+XX:EliminateLocks。必须在-server模式下才行。
其他措施
减小锁粒度:ConcurrentHashMap分了16段
锁分离:LinkedBlockingQueue有取放两把锁
锁粗化:减少获取锁的次数
CAS:Compare and Swap,atomic包
LongAddr:分段+CAS,更快的AtomicLong
volatile
保证了像Long,Double这种64位操作的原子性
保证了有序性,指令不会重排序
保证了可见性,强制CPU从内存读,而不是缓存
Happens-Before原则
指令重排序时候,不会违背这些原则
- 程序顺序原则:一个线程内保证语义的串行性
- volatile规则:volatile的写先于读,保证可见性
- 锁规则:解锁必须发生于加锁前
- 传递性:a先于b,b先于c,则a先于c
- 线程的start()先于它其他动作
- 线程所有动作先于线程的结束Thread.join()
- 线程的中断(interrupt())先于被中断的线程的代码
- 对象的constructor()早于finalize()