为了换取性能,JVM在内置锁上做了非常多的优化,膨胀式的锁分配策略就是其一。这里只讲这几种锁的概念,并不讲解锁的细节和详细的膨胀过程。
1.偏向锁
在某些时候,对于某个锁而言,可能并不存在多个锁来对他进行竞争。也就是说,申请该锁的始终都是同一个线程,那么这种情况下,就完全没有必要进行复杂的获取锁的操作。
就比如说我们可能在方法中创建了一个StringBuffer类型的示例,局部对象是线程安全的,其append()方法会进行同步,但是其实这种同步是没有必要的,这种情况下,偏向锁就能对性能有很多的帮助了。
偏向锁假定将来只有第一个申请锁的线程会使用锁(不会再有任何其他线程再来申请锁),因此,只需要在Mark Word中CAS记录owner(本质上也是更新,但初始值为空),如果记录成功,则偏向锁获取成功,记录锁状态为偏向锁,以后当前线程等于owner就可以零成本的直接获得锁;否则,说明有其他线程尝试获取锁,锁就要进行膨胀了,膨胀为轻量级锁。
总结:始终是同一个线程来申请锁,没有其他线程来申请锁
2.轻量级锁(无锁,自旋锁)
当一个线程来申请锁时,发现这个锁是偏向锁,但是偏向的不是自己。这时候其就会尝试去撤销偏向锁,转换为轻量级锁。
轻量级锁是对偏向锁的更进一步的优化。
轻量级锁的目标是,减少无激烈竞争情况下,使用重量级锁产生的性能消耗,包括系统调用引起的内核态与用户态切换。
使用轻量级锁时,不需要申请互斥量,仅仅将Mark Word中的部分字节利用CAS更新指向线程栈中的Lock Record,如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁;否则,说明线程之前产生了竞争,这时候还不会升级到重量级锁。线程还会进行自旋(自旋的次数有限制,JDK1.6之后都是自适应自旋次数,一般不需要我们自己设定)
,等待其他线程释放偏向锁,其再去申请。自旋的思想就是,上一个线程在获取了锁之后,可能很快就结束了操作,然后就释放掉了锁,可能他只执行了几千个时钟周期。这种情况下另外一个获取锁的线程完全没有必要因为获取不到锁而阻塞。因为执行的时间很短,我们可以让另外的获取锁的线程先进行自旋(执行循环语句)等待,而不是直接因为获取锁失败而直接升级到重量级锁,待拥有锁的线程释放掉锁了之后,当前自选的锁就可以获取到锁了。这样操作有什么好处?因为大多数情况下,一个线程占有锁的时间是很短的,这种情况下系统调用相比较于自旋更加浪费时间,自旋在JDK1.8中是默认开启的。自选锁在合适的场景下能显著的提高吞吐量。比如JDK1.8中的ConcurrentHashMap中就利用到了自旋的思想。
当自旋到一定的次数之后,如果还是没有获取到锁,就会升级为重量级锁了。自旋时利用的是CAS来保证。
总结:锁竞争不激烈,每个线程占用锁的时间比较短
3.重量级锁
内置锁在Java中被抽象为监视器锁(monitor)。监视器锁可以认为直接对应底层操作系统中的互斥量(mutex)(互斥量是存储在操作系统内核中的,这是操作系统提供的系统调用,操作系统来管理多个线程之前的唤醒和阻塞)
。这种同步方式的成本非常高,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。因此,后来称这种锁为“重量级锁”。
JVM在内核的互斥量的基础上实现了管程机制
总结:重量级锁就是利用操作系统提供的互斥量实现
锁消除
public String connect(String args1,String args2){
StringBuffer buffer = new StringBuffer(args1);
return buffer.append(args2).toString();
}
我们都知道StringBuffer是线程安全的,其提供的方法都会上锁,但是这里,因为buffer是一个局部变量,根本不可能有其他线程能访问到他,这时候他加的锁也没有意义,这种情况下JVM会将这个锁进行消除
锁粗化
public String connect(StringBuffer args){
for(int i=0;i<100;i++){
args.append("1");
}
return args.toString();
}
循环内一直获取锁释放锁,非常消耗性能,JVM会粗化锁,直接加到for循环上,就只用获取一次了。
注意
当开启了偏向锁之后,我们在新建的每一个对象的markword默认都是匿名偏向状态。什么意思呢?即该对象的锁偏向标志位是1,而不是0,但是此时的threadId还没有值,所以是匿名偏向状态。
如果没有开启偏向锁,新建的对象默认都是无锁状态。
利用JOL来观察对象头的变化
public static void main(String[] args) throws InterruptedException {
// 启动时先等10毫秒,因为偏向锁不是立即启动,而是等待几秒中才启动
// 因为JVM启动是内部也会大量用到锁,而且这些锁都是有激烈的竞争的
// 偏向锁在明确锁的竞争很强的情况下,完全是一个多余的东西,因为肯定会锁升级
TimeUnit.SECONDS.sleep(10);
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
结果
可以发现,默认情况下锁的偏向锁的标志位为1,锁标志位为01,这时候就是匿名偏向状态。获取一次锁之后,还是偏向锁状态,只是此时有了threadId。
还有一点需要注意,就是hashcode也会带来锁升级问题,直接就跳过偏向锁升级到了轻量级锁
java中的Identity hashcode带来的偏向锁膨胀