从JDK5到JDK6HotSpot虚拟机开发团队花费了大量的资源实现了各种锁优化技术,如适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁膨胀(Lock Coarsening)、轻量级锁(LightEight Locking)、偏向锁(Biased Locking)等,这些技术都是胃了在线程之间更高效地共享数据及解决竞争问题,从而提供程序的执行效率。
自旋锁与自适应锁在Java中锁起到的作用是互斥同步,而互斥同步对性的影响最大的是阻塞,阻塞是通过挂起线程和恢复线程来实现的,这个操作是很昂贵的,消耗的服务器资源比较大。针对于此虚拟机开发团队发明了自旋锁,因为在共享数据的锁定状态只会持续很短一段时间,为了这段时间去挂起和恢复线程很不值得。所以在一个线程获得锁的同时另一个线程可以先“稍等一会儿”,但并不放弃处理器执行时间,为了让线程等待,只须让线程执行一个忙循环(自旋),这就是自旋锁。
那么这个自旋锁的自旋时间多久比较合适呢?
如自旋时间太短那就起不到自旋的作用了,太长又会占用过多的处理器资源。所以在JDK1.4.2中引入自旋锁的时候,就提供了自旋次数为10默认值以及可以自行配置的参数-XXPreBlockSpin。
在JDK1.6中对自旋锁进行了优化,引入了自适应自旋。它可以根据前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。如果上一次获得了锁,那么下一次就会被认为也会获得锁,进而自旋时间会加长;如果这个锁很少被成功获得,那么有可能就直接省略掉自旋锁,避免处理器资源浪费。
锁消除锁消除是指:虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除。
锁消除是虚拟机自行判断的,开发人员,在编写代码的时候并不用刻意的去规避这些问题,因为有些同步措施都是Java本身自己实现的。
例如如下代码:
因为String是被final修饰的类,所以每次变动都是会产生新的String对象来进行的,因此在编译时会对String连接做自动优化。在JDK5之前会转成StringBuffer对象进行append()操作,在JDK5以后会转为StringBuilder对象进行append()操作。
这样JDK5之前编译器就会把代码变成如下形式:
因为StringBuffer::append()方法就涉及到同步块,锁的就是sb对象。所以发现sb的动态作用域在concatString()方法内部,其他线程又无法访问到它,因此这里的锁就可以被安全的消除。
锁粗化我们在编写代码的时候,一般会遵循一个原则,就是尽量将同步块的作用范围限制的最小,只在共享数据的实际作用域中才进行同步,这样同步操作数量会变得更少,即使存锁竞争,等待锁的线程也能尽可能快地拿到锁。
但是实际情况,在一系列连续操作都对同一个对象反复加锁和解锁,甚至加锁操作时出现在循环体之中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。
上面的代码中concatString()方法就是频繁的堆sb对象进行加锁,虚拟机会探测到这种情况,将锁的范围扩展到整个系列操作的外部。就是在第一个append()操作之前到最后一个append()操作之后,只需要加一次锁就可以了。
总结一下锁粗化:虚拟机探测到有一系列零碎的操作都对同一个对象加锁,将会加锁的同步范围扩展(粗化)到整个系列的操作外部。
轻量级锁是相对于操作系统互斥量来实现的“重量级”锁而言的,但是轻量级锁并不用来替代重量级锁的,它是指在没有多线程竞争的前提下,减少重量级锁使用操作系统互斥量产生的性能消耗。
要理解轻量级锁,必须要对虚拟机对象的内存布局(尤其是对象头部分)。
HotSpot虚拟机的对象头分为两部分:第一部门用户存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄(Generational GC Age)等。这部分数据的长度咋32位和64位的虚拟机中分别会占用32个或64个比特,官方称它为“Mark Word”,它是实现轻量级锁和偏向锁的关键。
第二部分是用于存储指向方法区对象类型数据的指针,如果是数组对象,还会有一个额外的部分用户存储数组长度。
由于对象头信息是与对象自身定义的数据无关的额外存储成本,Mark Word被设计成一个非固定的动态数据结构,以便在极小的空间内存储尽量多的信息。
Mark Word会根据对象的状态复用自己的存储空间。下面是对象的状态对应的对象头的存储内容表