在JDK1.6之前synchronized关键属于一种重量级的锁,性能能略显不足,从JDK1.6开始对synchronized做了一些优化,感觉主要就是针对到底要不要加锁,加什么锁做了一些判断,下面一起来看一下吧。
1、锁消除
最大程度的优化就是没有锁,那么如何判断能不能消除synchronized呢?在编译期间有一种优化叫做逃逸分析。
分析对象的作用域,如果证明一个对象不会逃逸到方法外,也就是说对象不会被当前方法外的其他任何地方访问到了,则可针对此对象进行优化。
public class ClearLock {
public Object o;
public void m1() {
o = new Object();//方法外的对象
}
public Object m2() {
Object o = new Object();
return o;//对象被发布出去了
}
public void m3() {
Object o = new Object();//对象无逃逸
synchronized (new Object()) {//对象无逃逸,此处可以优化删除synchronized
}
}
}
2、锁粗化
如果发现有对同一个对象连续的加锁、解锁的操作,那么可以将锁扩大到几步连续操作的最外面,粗化锁的范围,减少多次加锁、解锁带来的性能消耗。
下面示例中m4的方法可以优化成m5
public Object m4() {
Object o = new Object();
synchronized (o) {
//do something 1
}
synchronized (o) {
//do something 2
}
synchronized (o) {
//do something 3
}
return o;
}
public Object m5() {
Object o = new Object();
synchronized (o) {
//do something 1
//do something 2
//do something 3
}
return o;
}
3、偏向锁
经过分析大多数情况下可能都不存在锁之间的竞争,而是由同一线程多次获得,那么在这种情况下为了减少线程获取锁的CAS操作带来的性能损失就引入了偏向锁。
我们知道锁的信息就是存放在对象头的MarkWord中的。
那么当第一个线程获取锁时,就变成了偏向锁,之后有线程再次获取时就从对象头的MarkWord中判断当前是否为偏向锁,如果是再判断线程ID是否为同一个,如果是则直接访问即可,就不需要加锁、解锁的操作了。
当然偏向锁也不是万能的,当线程之间存在频繁竞争时,对偏向锁来说就不合适了,因为需要不断的设置偏向锁和撤销偏向锁,这带来额外的操作。
开启偏向锁:
-XX:+UseBiasedLocking
-XX:BiasedLockingStartupDelay=0设置偏向锁的启动延迟为0(默认为5秒)
关闭偏向锁:
-XX:-UseBiasedLocking
4、轻量级锁
当偏向锁被撤销后,下一步就升级为轻量级锁,轻量级锁比较偏向于认为大部分同步状态下还是不存在线程之间竞争的,那么就由CAS+自旋的方式。
轻量级锁的加锁过程:
在代码进入同步块的时候,如果同步对象锁状态为无锁状态且不允许进行偏向(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。
拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。
如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态。
如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,当竞争线程尝试占用轻量级锁失败多次之后,轻量级锁就会膨胀为重量级锁,重量级线程指针指向竞争线程,竞争线程也会阻塞,等待轻量级线程释放锁后唤醒他。锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。
锁的对比