前两天博主写过一编名为《Synchronized原理详解》)的文章,里面有对synchronized有过比较详细的分析。它其实是基于jvm的Monitor对象锁实现,而底层则是调用操作系统互斥量Mutex。在jdk1.6之前,其实使用synchronized性能是不怎么好的,因为操作系统互斥量其实是一种重量级锁,由用户态切换到内核态来完成。而到了jdk1.6,jvm的专家们采用锁膨胀升级机制对它进行了优化,今天我们就一起来研究下这块内容。
偏向锁
Java偏向锁(Biased Locking)顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁(对象markword中保存)。
如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
它通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。
偏向锁获取过程:
- 访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态。
- 如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤3。
- 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行5;如果竞争失败,执行4。
- 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会导致stop the word)
- 执行同步代码。
轻量级锁
轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。
上图展示了自旋的一个场景示意。T1线程在执行同步代码块,这时T2线程获得了CPU时间片,黄色为两个线程执行的重叠时间区域。由于T1线程拿着Minotor对象锁的,按道理,这时T2线程应该进入阻塞状态。但实际虚拟机并没有这么做。
虚拟机为了避免线程真实地在操作系统层面挂起,采用了自旋锁的优化机制。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对 比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式。
在jdk1.7之前,我们可以直接设置虚拟机自旋的次数。显然,这种设置毕竟带有人为主观性,见仁见智,很容易设置不恰当。在1.7之后,则变成了自适应自旋。自适应自旋锁的自适应反映在自旋的时间不在固定了。如果在同一个锁对象上,自旋线程之前刚刚获得过锁,且现在持有锁的线程正在运行中,那么虚拟机会认为这次自旋也很有可能会成功,进而允许该线程等待持续相对更长的时间,比如100个循环。反之,如果某个锁自旋很少获得过成功,那么之后再获取锁的时候将可能省略掉自旋过程,以避免浪费处理器资源。
轻量级锁所适合于线程交替执行同步块的场合,如果存在多个线程同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁,在操作系统层面直接挂起。