多线程并发 之 锁消除与锁粗化

前言

锁消除与锁粗化是虚拟机针对低效的锁操作而进行的一个优化。

锁消除

简单来说锁消除是虚拟机根据一个对象是否真正存在同步情况,若不存在同步情况,则对该对象的访问无需经过加锁解锁的操作。

你可能会有疑问,对象有没有同步我还不知道吗?有同步我就用synchronize,没有就不用了。但是实际情况是,有许多同步措施并不是程序员自己加入的。看下面的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {

public static void main(String[] args) {
createStringBuffer("Test1", "Test2");
}

public static String createStringBuffer(String str1, String str2) {
StringBuffer sBuf = new StringBuffer();
sBuf.append(str1);// append方法是同步操作
sBuf.append(str2);
return sBuf.toString();
}
}

通过调用createStringBuffer方法,将两个字符串拼接起来,然后返回。了解过StringBuffer的同学一定清楚它是线程安全的,从append方法就可以知道:

1
2
3
4
5
6
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}

上面的代码是不存在锁的竞争的,那么如果在append的时候还需要去判断对象是否被占用,这部分的性能消耗是无意义的,因为根本不存在同步情况。于是虚拟机在即时编译的时候就会将上面代码进行优化,也就是锁消除。那么虚拟机什么时候才会使用锁消除呢?通过逃逸分析。那么什么是逃逸分析呢?还是看上面的代码:

1
2
3
4
5
6
public static String createStringBuffer(String str1, String str2) {
StringBuffer sBuf = new StringBuffer();
sBuf.append(str1);// append方法是同步操作
sBuf.append(str2);
return sBuf.toString();
}

首先 sBuf 对象使用的范围仅仅只在这个方法栈中,因为return回去的对象是一个的String对象。

1
2
3
4
5
6
7
@Override
public synchronized String toString() {
if (toStringCache == null) {
toStringCache = Arrays.copyOfRange(value, 0, count);
}
return new String(toStringCache, true);
}

也就是说 sBuf 对象是不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上的数据对待,认为它们是线程私有,同步锁无须进行。关于逃逸分析还不清楚的,可以看这篇博客

注意锁消除的前提是:要运行在server模式下,且开启了逃逸分析。

锁粗化

锁粗化比较好理解,如下面的代码:

1
2
3
4
5
6
7
public static StringBuffer createStringBuffer(String str1, String str2) {
StringBuffer sBuf = new StringBuffer();
sBuf.append(str1);// append方法是同步操作
sBuf.append(str2);
sBuf.append("abc");
return sBuf;
}

当频繁对 sBuf 进行加锁、解锁,会造成性能上的损失。如果虚拟机探测到有一串零碎操作都是对同一对象加锁,将会把加锁同步的范围扩展到整个操作序列的外部,也就是在第一个和最后一个append操作之后。