synchronized锁优化
1.自旋锁和自适应自旋锁
sync在JDK1.6之前之所以被称为重量级锁,是因为对于互斥同步的性能来说,影响最大的就是阻塞的实现。挂起线程与恢复线程的操作都需要转入内核态中完成。从用户态转入内核态是比较耗费系统性能的。
研究表明,大多数情况下,线程持有锁的时间都不会太长,如果直接挂起OS层面的线程可能会得不偿失,毕竟OS实现线程之间的切换需要从用户态转换为核心态。这个转换时间成本相对较高。自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环,使当前线程不放弃处理器的执行时间(这也是称为自旋的原因),在经过若干循环后,如果得到锁,就顺利进入临界区。
但是阿, 自旋不能替代阻塞。首先,自旋需要多处理器或者一个处理器多个核心的CPU环境。这样才能保证两个及以上的线程并行执行(一个是获取锁的执行线程,一个是进行自旋的线程)除了对处理器数量有要求外,自旋虽然避免了线程切换的开销,但他是要占用处理器时间的,因此,如果锁被占用的时间短,自旋的效果才好,否则只是白白占用了CPU资源,带来性能的浪费。
那么自旋需要有一定的限度,如果自旋超过了一定的次数之后,还没有成功获取锁,就只能进行挂起了,这个次数默认是10.
在JDK1.4.2 引入了自旋锁,在JDK1.6引入了适应性自旋锁。自适应意味着自旋的时间不再固定。
适应性自旋锁:
如果同一个锁对象上,自旋等待刚刚成功获取锁,并且持有锁的线程正在运行,那么虚拟机就会认为此次自旋也很有可能成功,进而它将允许自旋等待持续相对更长的时间,比如100个循环。如果对于某个锁,自旋很少成功获取过,那么在以后获取这个锁时将可能自动省略掉自旋过程,以免浪费处理器资源。有了自适应自旋,随着程序运行和性能监控不断完善,虚拟机对程序锁的状况预测就会越来越精准,虚拟机也会越来越聪明。
锁消除
消除锁是更加彻底的锁优化。JVM在JIT编译时通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没必要的锁。
锁消除的主要判断依据是逃逸分析技术的支持。
也许你疑惑,变量是否逃逸,程序员本身应该可以判断,怎么会存在明明知道不存在数据竞争的情况下还是用同步?
public String concatString(String s1, String s2, String s3) { return s1 + s2 + s3; }
由于String是一个不可变类,因此字符串的连接操作总是通过新生成的String对象来进行。在JDK1.5之前,javac编译器会对String连接进行自动优化,将连接转换为StringBuffer对象的连续append操作,在JDK1.5之后,会转化为StringBuilder对象的append操作,也就是说,上述代码经过javac优化之后,会变成下面这样:
public String concatString(String s1, String s2, String s3) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); sb.append(s3); return sb.toString(); }
StringBuffer是一个线程安全的类,在它的append方法中有一个同步块,锁对象就是sb,但是JVM观察变量sb,发现他是一个局部变量,本身线程安全,并不需要额外的同步机制。因此,这里虽有锁,但可以被安全的清除,在JIT编译之后,这段代码就会忽略所有的同步而执行。这就是锁清除。
锁粗化
原则上,我们在使用同步块,总是建议将同步快的作用范围限制的尽量小——使需要同步的操作数量尽可能变小,在存在锁竞争的情况下,等待锁的线程可以尽快的拿到锁。
大部分情况下,上述原则都正确,但是存在特殊情况下,如果一系列下来,都对同一个对象反复加锁、解锁,甚至加锁与解锁操作出现在循环体,那即使没有线程竞争,频繁的进行互斥同步操作也会导致不必要的性能损耗。
如上述代码中的append方法。如果虚拟机探测到了这样的操作,就会把加锁的同步范围扩展(粗化)到整个操作序列的外部。
以上述代码为例,就是拓展到第一个append操作之前直至最后一个append操作之后,这样只需要加锁一次。
偏向锁会偏向第一个获取它的线程,如果在接下来的执行过程中,该锁没有被其他线程获取,则持有偏向锁的线程将永远不需要进行同步。
HotSpot作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得(比如在单线程中使用StringBuffer类),为了让线程获得锁的代价更低而引入了偏向锁。当锁对象第一次被线程获取的时候,虚拟机把对象头中的标志位设为“01”,即偏向模式。同时使用CAS操作把获取这个锁的线程ID记录在对象的Mark Word中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不用进行任何同步操作。
当有另一个线程去尝试获取这个锁时,偏向模式就宣告结束。