为什么要优化?
为什么阻塞线程和挂起线程的操作都需要从用户态转入内核态,这是要耗费很多处理器时间的;并且常见的同步场景有如下几种情况
- 共享数据的锁定状态只会持续很短一段时间,可能必挂起线程唤醒线程的操作时间还短
- 大部分情况下,同一时刻只有一个线程会获得该对象的锁
自旋锁与自适应锁
为了不让获取不到锁的线程陷入阻塞,可以让该线程陷入自旋,即让他不断的重试获取锁,执行一个忙循环。这种操作再前面提到的线程获取锁的状态只会持续很短一段时间时是能提升效率的,因为它避免了线程的阻塞和唤醒的操作。不过自旋不能代替阻塞,原因也很直白,如果持有锁的线程久久不释放,那么自旋只会白白浪费处理器资源。所以,一般自旋都是有限制的,默认次数是10次;可以通过参数-XX:PreBlockSpin来修改
而自适应锁的自旋次数是根据前一次再同一个锁上的自旋时间及锁的拥有者状态来决定的;如果前一个刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机会认为自旋成功的几率很高,会运行自旋的更长的时间;如果自旋很少成功,那么就会减少自旋次数,甚至直接省略自旋,以避免浪费处理器资源
锁消除
即使编译器利用逃逸分析判断代码是否存在共享数据竞争,如果不存在则会进行锁消除;因为有时候我们的代码并没有使用同步互斥,但是再其他我们不知道的地方,可能就被加入了同步;
锁的不断升级
单向的,只能往上升级,不能降级;
偏向锁
当一个线程访问同步代码块并获取锁时,会在Mark Word里存储锁偏向的线程ID。在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Mark Word里是否存储着指向当前线程的偏向锁。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可。
一旦出现另外一个线程尝试去获取这个锁时,偏向锁马上结束,根据锁对象目前是否被锁定决定是否撤销偏向锁或是升级为轻量级锁;
这里注意一个点,因为线程Id是存储在Mark Word 存放hashCode值的地方,所以如果该对象曾经调用过一次计算该对象的hashCode值,那么该值会被存储在Mark Word中,即偏向锁就无法使用了,如果此时该位置存放的是线程Id,则会理解撤销偏向锁状态;
轻量级锁
当前锁是偏向锁时,被另外的线程访问,偏向锁就会升级为轻量级锁,其他线程会通过自选的形式尝试获取锁,不会阻塞,从而提高性能;
轻量级是相对于操作系统互斥量来实现的传统锁而言的;轻量级锁所适应的场景是线程交替执行同步块的情况
若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。
轻量级锁的加锁过程
- 虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。
- 拷贝对象头中的Mark Word复制到锁记录中。
- 拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针;并将Lock record里的owner指针指向object mark word;如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,
- 如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。
轻量级锁的解锁过程
- 通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。
- 如果替换成功,整个同步过程就完成了。
- 如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。
重量级锁
升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。
参考
不可不说的Java“锁”事
Java并发编程:Synchronized及其实现原理