一、锁优化
概念
1.1自旋锁
互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都不可避免地需要在用户态和内核态进行切换,这些操作给JAVA虚拟地并发性能带来很大压力。一般情况下,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。在多核处理器并行的情况下,我们可以让请求锁的那个线程稍微等待一下,不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放。为了让线程等待,我们只需要让线程执行忙循环(自旋),这就是所谓的自旋锁。
1.2自适应自旋锁
JDK1.6以后自旋锁默认开启,自旋锁等待虽然可以避免线程切换带来的开销,但是要占用处理器的执行时间,如果占用时间很短,那么自旋等待的效果会很好,反之如果锁占用时间很长,那么自旋的线程只会白白消耗处理器资源,自旋锁默认超过10次如果还没获取到锁,那么便会挂起线程,可以通过 -XX:PreBlockSpin 参数设置自旋的次数。JDK1.6后对自旋锁进行了优化,可以根据上次自旋获取到锁的概率,来决定这次自旋的次数,或者是否有必要进行自旋。有了自适应自旋,随着程序运行时间的增长,及性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越精准。
1.3锁消除
锁消除是指虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除。如果判断到一段代码中,在堆上所有的数据都不会逃逸出去被其他线程锁访问到,那就可以把它们当成栈上的数据对待,认为它们是线程私有的,同步枷锁自然也无须再进行,因此在经过服务端编译器的即时编译后,这段代码就会忽略所有的同步措施而直接执行。如一下代码:
public void metod(){
DemoX demoX = new DemoX();
synchronized (demoX){
//.....
}
}
1.4锁粗化(没复现,有待验证)
一般写代码的时候,尽量只把同步代码块的作用范围缩小到共享数据的实际作用域,这样是为了使得同步的操作尽可能变少,即使存在锁竞争,等待锁的线程也能尽快地释放和获取。
但是如果一系列的连续操作都是对同一个对象反复加锁解锁,甚至加锁操作是出现在一个循环体中,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要地性能损耗。
对于这种类似的情况,虚拟机会把加锁同步的范围扩展到整个操作序列的外部,如一下代码 (理论上是这样,但是实际操作并没有被粗化):
//锁粗化前
public void method(String str){
for (int i = 0; i < 10; i++) {
synchronized ("lock"){
System.out.println(str);
}
}
}
//锁粗化后
public void method(String str) {
synchronized ("lock") {
for (int i = 0; i < 10; i++) {
System.out.println(str);
}
}
}
PS:对于锁的粗化,目前只在StringBuffer验证出来过,至于自己写的demo,通过打印出来的结果来看,并没有锁粗化,目前还未找到具体的问题,猜测锁的粗化并不作用于所有的类。

1.5轻量级锁
锁采用 CAS 操作,将锁对象的标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对象原本的标记字段。它针对的是多个线程在不同时间段申请同一把锁的情况。
当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,然后拷贝对象头中的Mark Word复制到锁记录中。
拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向对象的Mark Word。
如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,表示此对象处于轻量级锁定状态。
如果轻量级锁的更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁。
若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。
多个线程在不同的时间段请求同一把锁,也就是说没有锁竞争。针对这种情形,Java 虚拟机采用了轻量级锁,来避免重量级锁的阻塞以及唤醒。
1.6偏向锁
只会在第一次请求时采用 CAS 操作,在锁对象的标记字段中记录下当前线程的地址。在之后的运行过程中,持有该偏向锁的线程的加锁操作将直接返回。它针对的是锁仅会被同一线程持有的情况。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。
在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能。
当一个线程访问同步代码块并获取锁时,会在Mark Word里存储锁偏向的线程ID。在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Mark Word里是否存储着指向当前线程的偏向锁。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可。
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。
偏向锁在JDK 6及以后的JVM里是默认启用的。一般偏向锁适用于同步但无竞争的程序,如果程序中大多数的锁都总是被多个不同的线程访问,那么偏向锁模式就是多余的,可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。
1.7重量级锁
锁会阻塞、唤醒请求加锁的线程。它针对的是多个线程同时竞争同一把锁的情况。Java 虚拟机采取了自适应自旋,来避免线程在面对非常小的 synchronized 代码块时,仍会被阻塞、唤醒的情况。
2 锁膨胀的过程: