什么是锁优化
- 为了线程之间更高效的共享数据,以及解决竞争问题,在JDK1.5之后,对锁进行了大量的优化,由此衍生出(自适应)自旋锁/轻量级锁/偏向级锁/锁消除/锁粗化等技术
锁消除
- 锁消除是指虚拟机即时编译器在运行时,对一些代码上的同步要求,检测到是不可能存在共享数据竞争的,这时就会对锁进行清除,这里就好比我们去火车站买票需要排队,为了保证秩序,一般都会有一些围挡限制值人,但是现在不是高峰期,发现根本不需要做一些围栏来限制秩序,这个时候车站安保人员就会把围栏撤掉,这里这个围挡就相当于我们的锁,会根据实际情况来进行判断
- 锁消除主要源于逃逸分析的数据支持,如果判断一段代码中,堆上的所有数据都不会逃逸出去被其他线程访问,那么可以把它当做栈上数据来对待,认为他是线程私有的,那么就不用加锁了
- 要注意的是,不仅仅是我们在开发中手动加的锁,在有些场景中,同步代码也是普遍存在的
1 | public String concat(String s1,String s2,String s3){ |
- 我们知道,由于String是一个不可变类,对字符串的操作是转换成新的String对象来进行的,在jdk1.5及以前的版本,这个字符串拼接会被javac编译成
1
2
3
4
5
6
7
8
9
10
11
12
13
14 //javac编译后
public String concat(String s1,String s2,String s3){
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
//StringBuffer的append方法
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
- 但是我们看到StringBuffer的append方法是一个同步方法,但是在该方法中,并不需要加锁,所以在jdk1.5以后的版本, StringBuffer会被优化成StringBuilder,而StringBuilder是不加锁的
锁粗化
- 一般我们在开发过程中,推荐将同步块的作用域限制的尽量小,最好是只在共享数据的实际操作的作用域加锁,这样使得需要同步的操作数量尽可能小,如果存在锁竞争,那等待的线程也能够尽快拿到锁
- 在大部分情况中上述原则是正确的,但是如果一些列连续都操作都是对同一个对象进行反复的加锁解锁操作,甚至在循环中出现这样的问题,那样既是没有线程的竞争,也会频繁的触发互斥同步的操作,对性能是有巨大损耗的
- 如何解决这类问题呢,那么就是锁粗化,顾名思义,就是将细粒度的锁变成粗粒度的锁,如果虚拟机探测到有这样一系列零零碎碎的操作都对同一个对象加锁,会把这个锁同步范围扩大到整个操作序列的外部,以上面的concat方法为例,把原来每个append方法上的锁变成对concat方法的锁
自旋锁/自适应自旋锁
- 提到自旋锁,我们先回到互斥同步本身,我们知道互斥同步有个很大的问题,就是阻塞实现,挂起线程和恢复线程都是内核态的操作,这些操作会给系统并发性能带来很大的压力,而且在很多场景中,共享数据的锁定只会持续很短的时间,而为了这段时间去恢复和挂起线程是非常不值得的,所以就有了自旋锁
- 自旋锁,前提是机器有一核以上的处理器(一核的机器现在已经很少了,可以忽略这个前提),能让两个以上的线程并行执行,这个时候我们可以让后面请求锁的线程稍微等一会,看看持有锁的线程能否快速的释放这个锁,为了让线程陷入等待,就出现了自旋(说白了就是循环),在JUC包中我们可以看到AQS中出现的下面代码,就是基于CAS实现的自旋锁
1
2
3 for(;;){
doSomething();
}
- 自旋锁虽然避免了线程切换带来的开销,但是在处理时间上却变得更长,如果锁占用的时间很短,那自旋等待的效果就很好,如果锁占用的时间长,那么自旋的线程只会白白浪费处理器的资源,而不会做其他有用的工作,反而会带来性能上的浪费,因此,在自旋到一定时间后还没有获得锁,就会将线程挂起,自旋的默认次数是10
- 自适应自旋锁,顾名思义就是自动适配的自旋锁,它的自旋时间不在固定,而是由上一次同一个锁上的自旋时间和锁的拥有者的状态来决定的,由此可见JVM更加智能了,对于经常获得锁的,自旋的时间会尽可能的长,而对于自旋经常获取不到锁的线程,就直接挂起线程,避免资源的浪费
轻量级锁
- 轻量级锁也是jdk1.6之后引入的新型锁机制,轻量级是相对于使用操作系统的互斥量来实现的传统锁而言,因此传统的锁机制就被称为重量级锁,这里要强调一点的是,轻量级锁不是用来代替重量级锁的,它的本意是避免在没有锁竞争的情况下,使用重量级锁造成的资源浪费
- 说到轻量级锁,首先要了解对象头的内容对象在JVM中的储存,对象在没有进入同步块的时候,如果此同步对象没有被锁定(锁标识为”01”的状态), jvm将在栈中建立一个LockRecord的空间,用于存储当前对象MarkWord的拷贝(Displaced MarkWord),如上图所示,然后虚拟机将使用CAS操作尝试对对象头的MarkWord更新为指向LockRecord的指针,如果更新成功,则对象拥有对该对象的锁,并且将锁标识转变成”00”
- 如果更新失败了,jvm会首先检查对象的MarkWord是否指向当前的栈,如果指向当前线程的栈,则说明已经获得锁,那就可以进入同步块去执行,如果没有指向,则说明当前线程的锁已经被其他线程抢占了,如果有两条以上的线程去争夺同一个锁,那么轻量级锁就会失效,膨胀为重量级锁,锁标识的状态值也会变为”10”, MarkWord指向的就是重量级锁(互斥量的指针),后面等待锁的线程也要进入阻塞状态
- 轻量级锁解锁也是通过CAS进行的,如果对象的MarkWord仍然指向着线程的锁记录,那就用CAS把对象当前的MarkWord替换回来,如果替换成功,说明整个同步过程就完成了,如果替换失败,说明其他线程尝试获取过锁,那么在释放锁的时候还要唤起其他被挂起的线程
- 轻量级锁提升性能的依据在于,对于大部分的锁,在整个同步周期内都是不存在竞争的,这只是一个经验数据,如果没有竞争,轻量级锁使用CAS操作避免了使用互斥线程的开销,但是如果存在竞争,那么除了互斥开销外,还有CAS的开销,在有竞争的情况下,轻量级锁会比传统的重量级锁更慢
偏向锁
- 偏向锁和轻量级锁类似,也是对于MarkWord的一系列操作,顾名思义,偏向锁,是指偏向某一个线程的锁,偏向锁会偏向第一个获取他的线程,如果在接下来的执行当中都没有其他线程获取,那么持有偏向锁的线程将永远不用在进行同步
- 当锁第一次被对象获取时,在将锁标识为”01”,即偏向锁模式,然后使用CAS操作把线程ID记录到MarkWord中,如果更新成功,持有偏向锁的线程,每次进入同步块时都不需要重新加锁
- 当有另一个线程尝试获得锁时,偏向模式宣告结束,根据锁对象目前是否处于被锁定的状态,决定撤销偏向,还是升级到轻量级锁
- 偏向锁可以提高带有同步但是无竞争的程序性能,同样是有利有弊,如果程序中大部分锁是要被多个不同线程访问,偏向锁肯定是有负担的