锁优化的背景
JDK5版本带来了J.U.C包以及其他并发相关的技术,使得Java语言对于并发的支持更加完善。在这个基础上,JDK6为了更加高效的并发,Hotspot虚拟机的开发团队花费了大量的精力去实现各种锁优化的技术:自旋锁、自适应自旋锁、锁消除、锁膨胀、轻量级锁、偏向锁等。
自旋锁与自适应自旋锁
互斥同步对于性能最大的影响点在于线程阻塞导致用户态和内核态切换所带来的的性能消耗。同时一个现状是:多数情况下,共享数据的锁定状态持续的时间都比较短。在这么短的阻塞情况下,去阻塞线程带往往是不值得的,尤其是当今多核处理器的现状下。
因此,当遇到锁竞争的情况下,我们可以暂时不阻塞后面的线程,而是让他们不放弃处理器的资源,进行一个忙循环,来等待锁的释放。这项技术就是所谓的自旋锁。
自旋锁的好处是可以避免线程直接阻塞导致的性能消耗,但是自旋锁并不能代替阻塞。如果锁竞争十分激烈并且锁占用时间过长,线程将一直忙循环从而浪费处理器的资源。因此不能让线程一直处于自旋中,必须有一个限度:线程自旋时间需要有限度;线程自旋次数需要有限度。当一个线程经过数次自旋依然没有获取到锁时,应该进入到阻塞状态。
Java中对于自旋锁的具体优化方式是,线程自旋的时长和次数由前一次获取锁的自旋时间和获取锁的线程状态决定的。如果前一次自旋时间比较短就获得了锁,虚拟机就会认为本次也很有可能在较短的时间内获取到锁,进而允许本次自旋时间等待时间更长。如果通过自旋的方式获取锁的成功机率很低,那么虚拟机也很有可能不让线程进行自旋而是直接进入到阻塞状态,避免白白浪费处理器资源。
锁消除
锁消除是指在程序运行情况下,有些代码要求同步,但是虚拟机检测到不存在共享数据竞争,从而消除掉锁。锁消除的技术实际上是基于逃逸分析的。如果判断到一段代码中涉及到的数据不会逃逸出去被其他线程访问到,就可以认为这些数据是线程私有的,自然就不存在锁竞争的情况。
我们来看这一段代码:
// 这段代码没有涉及到任何共享数据,只是一个普通的虚方法.
public String concatStr(String str1, String str2, String str3) {
synchronized(this) {
synchronized(this) {
return str1 + str2 + str3;
}
}
}
通过反编译来看一下:
public java.lang.String concatStr(java.lang.String, java.lang.String, java.lang.String);
descriptor: (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=2, locals=8, args_size=4
0: aload_0
1: dup
2: astore 4
4: monitorenter
5: aload_0
6: dup
7: astore 5
9: monitorenter
10: new #2 // class java/lang/StringBuilder
13: dup
14: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
17: aload_1
18: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21: aload_2
22: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
25: aload_3
26: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
29: invokevirtual #5 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
32: aload 5
34: monitorexit
35: aload 4
37: monitorexit
38: areturn
39: astore 6
41: aload 5
43: monitorexit
44: aload 6
46: athrow
47: astore 7
49: aload 4
51: monitorexit
52: aload 7
54: athrow
可以看到,通过静态编译之后,有两对monitorenter-monitorexit。并没有进行锁消除啊~是的,静态编译并不会进行锁消除,锁消除是在程序运行时进行的。
锁粗化
如果有一系列的操作都会对同一个对象进行加锁和解锁,那么即使没有锁竞争,频繁的进行互斥同步操作也会导致不必要的性能消耗。我们看这段代码:
public Object lock = new Object();
public void loop(int n) {
for(int i = 0; i< n; i++) {
synchronized(lock) {
System.out.println(i);
}
}
}
以上程序在实际运行中会被虚拟机优化进行锁粗化,等同于一下代码:
public Object lock = new Object();
public void loop(int n) {
synchronized(lock) {
for(int i = 0; i< n; i++) {
System.out.println(i);
}
}
}
轻量级锁
在JDK6之前,基于synchronized关键字进行同步时,是一个“重量级”操作,在JDK6时,引入了“轻量级”的概念,轻量级并不是要取代传统的“重量级”。而是在所竞争没有那么激烈的情况下,采用轻量级锁机制可以降低性能的消耗。
偏向锁
偏向锁也是JDK6引入的优化技术。它的目的是消除无锁竞争时的同步原语,进一步提升程序性能。
关于轻量级锁和偏向锁的原理我们在介绍synchronized时讲解。