高效并发是JDK5升级到JDK6之后的一项重要改进。
讨论互斥同步的时候,我们说到了互斥同步里对性能最大的影响就是阻塞,因为会不断地在用户态和内核态之间切换。而假如我们的物理机的CPU是双核及以上的处理器,可以同时让多个线程并行执行,那么我们就可以让后面请求锁的线程“等待一会儿”,但不放弃处理器的执行时间,看看是否有线程释放锁。
为了让线程等待,我们设置一个忙循环(自旋),这就是自旋锁。
自选等待不能代替阻塞,自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的。如果在短时间内另一个线程释放了锁,那么效果就会很好;反之长时间都获取不到锁,那么就会白白浪费CPU的运行时间。
因此,自旋等待必须有一定的限度,如果自旋次数超过了限定的次数仍然没有获取到锁,就应该用传统方式去挂起线程。自旋默认次数是10次,可以使用参数-XX:PreBlockSpin修改。
但对于JDK6来讲,对于自旋锁引入了自适应自旋,于是自旋的次数就不是固定的了。例如对于同一个锁对象,自旋等待刚刚获得过这个锁,并且持有锁的线程正在运行中,那么虚拟机认为这次自旋也很有可能再成功,进而允许自旋相对更长的时间;但对于这个锁自旋很少成功获得锁的话,就可能直接忽略自旋过程,避免浪费处理器资源。
锁消除是指虚拟机在即时编译器在运行时检测到某段需要同步的代码不可能存在共享数据竞争,而实施对锁进行消除的优化策略。
锁消除的主要判定依据是逃逸分析技术(to be waited)
在编写代码的时候,我们对需要同步操作的代码块进行同步,都是尽量在共享数据的实际作用域中进行。但如果一系列的连续操作都对同一个对象反复加锁和解锁,即使没有线程竞争,频繁地进行互斥同步也会导致不必要的损耗。
例如下面一段代码,虚拟机就会把加锁范围扩大(粗化)到整个操作序列外部。
- public String concatString(String s1,String s2,String s3){
- StringBuilder sb = new StringBuilder();
- sb.append(s1);
- sb.append(s2);
- sb.append(s3);
- return sb.toStirng();
- }
轻量级锁是JDK6加入的新型锁机制,它的轻量级是相对于传统的互斥的“重量级锁而言的”。不过轻量级锁不是用来代替重量级锁的,它设计的初衷是在没有线程竞争的情况下减少重量级锁带来的性能消耗。
要了解轻量级锁和偏向锁,就要了解HotSpot虚拟机对象的内存布局。
HotSpot虚拟机的对象头(Object Header)分为两个部分:
1)第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄(Generational GC Age)等。这部分数据在32位和64位的Java虚拟机中分别占用32和64个bit,官方称为Mark Word。
2)另一部分用于存储指向方法区对象类型数据的指针,如果是数组对象,还会有额外一部分用于存放数组长度。
对象的对象头信息是与对象自身定义的数据无关的额外存储成本,所以Mark Word被设计成一个非固定的动态数据结构。

现在让我们来看看轻量级锁的加锁过程:
1)代码即将进入同步块的时候,如果同步对象没有被锁定(标志位是01)
虚拟机首先在当前线程的栈帧用建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝。如图所示:

2)加锁操作
虚拟机会使用CAS操作来尝试把当前对象的Mark Word更新为指向Lock Record的指针,如果这个操作成功了,就代表该线程拥有了这个对象的锁,并且对象Mark Word的锁标志位变成“00”,表示对象处于轻量级锁定状态。
这个时候的线程栈和对象头的状态如图所示:

如果这个时候CAS更新失败了,虚拟机会检查对象的Mark Word是否指向当前线程的栈帧,如果是,说明当前线程已经拥有了这个对象的锁,直接进入同步块就行了;如果不是,说明这个锁对象以及被其他线程抢占了。
如果出现两条以上线程争用一个对象锁的情况,那么轻量级锁就不再有效,必须膨胀为重量级锁,锁标志位变为“10”,此时Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程必须进入阻塞状态。
3)解锁
轻量级锁的解锁也是通过CAS来进行的,如果对象的Mark Word仍然指向线程的锁记录,那就用CAS操作把对象当前的Mark Word替换回来。
轻量级锁是针对“对于绝大部分锁,在整个同步周期内都是不存在竞争的”。如果没有竞争,轻量级锁通过CAS操作成功避免了使用互斥量的开销;但如果存在竞争,除了互斥量本身的开销外,还额外发生了CAS操作,开销比传统的重量级锁大。
如果说轻量级锁是在无竞争的情况下使用CAS去消除同步使用的互斥量,那么偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作也不做了。
偏向锁的意思就是:这个锁会偏向于第一个获取它的线程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程永远不需要进行同步。
理解了上述轻量级锁的加锁过程,偏向锁也很好理解。
假设当前虚拟机开启了偏向锁(-XX:+UseBiasedLocking),那么锁对象第一次被线程获取的时候,虚拟机就会把对象头中的标志位设置为"01",把偏向锁标志位设置为"1",并使用CAS操作把获取到这个锁的线程ID记录在Mark Word中。如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,都不再进行任何同步操作。
一旦出现另一个线程去尝试获取这个锁的情况,偏向模式马上结束。
根据锁对象目前是否处于被锁定的状态决定是否撤销偏向(偏向标志设置为"0"),撤销后标志位恢复到未锁定("01")或轻量级锁定("00"),后续同步操作同轻量级锁执行。

拓展:偏向锁的HashCode在记录线程ID后,HashCode记录在哪呢?
Java里,一个对象计算过HashCode,那么就希望这个对象的HashCode保持不变,否则很多依赖于HashCode的API都会有风险。而绝大多数对象的哈希码来源Object::hashCode()返回的是对象的一致性哈希码(Identity Hash Code),这个值是强制不变的,它通过在对象头中存储计算结果来保证再次调用这个方法能获得相同的HashCode。
所以,一旦有对象计算过一致性哈希码后,它就再也无法进入偏向锁模式了。
当对象正处于偏向锁又需要获得一致性哈希码时,它的偏向状态会被立即取消,变为重量级锁。