synchronized、偏向锁和轻量级锁

synchronized的基本认识

静心、砥砺前行

使用synchronized修饰符可以达到线程安全的效果
synchronized有三种使用方式

  1. 修饰方法,作用于当前实例,进入同步代码需要获得当前实例的锁
  2. 静态方法,作用于当前对象(相当于class锁),进入同步代码需要获取当前对象锁
  3. 修饰代码块,指定加锁对象,进入同步代码块需要用获得给定对象锁

Mark word

Mark word记录了对象和锁的相关信息,当某个对象被synchronized关键字指定为加锁对象时,那么围绕这个锁的一些列操作道和Mark word有关系。Mark word在32位虚拟机长度就是32bit、64位虚拟机长度就是64bit。
synchronized、偏向锁和轻量级锁

偏向锁、轻量级锁

虚拟机在升级过程中发现,大部分情况下;加锁的代码不仅仅没有线程竞争,而且总是由同一个线程多次获得。所以基于这样的一个概率在jdk1.6之后做了一些优化,为了减少锁带来的性能开销引入了偏向锁、轻量级锁的概念。
因此在synchronized中锁存在四种状态,分别是无锁、偏向锁、轻量级锁、重量级锁;锁的状态根据竞争激烈的程度从低到高不断升级。

偏向锁的获取逻辑

1.首先获取锁对象的Markword,判断是否处于可偏向状态。即标志位=1,且线程id为空;
2.如果是可偏向状态,则通过CAS操作,把当前线程的ID写入到MarkWord

  • 如果 cas 成功,那么markword就会变成这样。
    表示已经获得了锁对象的偏向锁,接着执行同步代码
  • 如果 cas失败,说明有其他线程已经获得了偏向锁,这种情况说明当前锁存在竞争,需要撤销已获得偏向锁的线程,并且把它持有的锁升级为轻量级锁(这个操作需要等到全局安全点,也就是没有线程在执行字节码)才能执行

3.如果是已偏向状态,需要检查 markword 中存储的
ThreadID 是否等于当前线程的 ThreadID

  • 如果相等,不需要再次获得锁,可直接执行同步代码
  • 如果不相等,说明当前锁偏向于其他线程,需要撤销偏向锁并升级到轻量级锁

偏向锁的撤销

偏向锁的撤销并不是把对象恢复到无锁可偏向状态(因为偏向锁并不存在锁释放的概念),而是在获取偏向锁的过程中,发现cas失败也就是存在线程竞争时,直接把被偏向的锁对象升级到被加了轻量级锁的状态。对原持有偏向锁的线程进行撤销时,原获得偏向锁的线程有两种情况:

  1. 原获得偏向锁的线程如果已经退出了临界区,也就是同步代码块执行完了,那么这个时候会把对象头设置成无锁状态并且争抢锁的线程可以基于CAS重新偏向当前线程
  2. 如果原获得偏向锁的线程的同步代码块还没执行完,处于临界区之内,这个时候会把原获得偏向锁的线程升级为轻量级锁后继续执行同步代码块
    synchronized、偏向锁和轻量级锁

轻量级锁的基本原理

锁升级为轻量级锁之后,对象的Markword也会进行相应的的变化。升级为轻量级锁的过程:

  1. 线程在自己的栈桢中创建锁记录 LockRecord。
  2. 将锁对象的对象头中的MarkWord复制到线程的刚刚创建的锁记录中。
  3. 将锁记录中的 Owner 指针指向锁对象。
  4. 将锁对象的对象头的MarkWord替换为指向锁记录的指针

自旋锁

轻量级锁在加锁过程中,用到了自旋锁所谓自旋,就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的。注意,锁在原地循环的时候,是会消耗cpu的,就相当于在执行一个啥也没有的for循环。所以,轻量级锁适用于那些同步代码块执行的很快的场景,这样,线程原地等待很短的时间就能够获得锁了。自旋锁的使用,其实也是有一定的概率背景,在大部分同步代码块执行的时间都是很短的。所以通过看似无异议的
循环反而能提升锁的性能。但是自旋必须要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,那么这个线程不断的循环反而会消耗 CPU 资源。默认情况下自旋的次数是 10 次,可以通过 preBlockSpin来修改在JDK1.6之后,引入了自适应自旋锁,自适应意味着自旋的次数不是固定不变的,而是根据前一次在同一个锁上自旋的时间以及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

轻量级锁的解锁

轻量级锁的锁释放逻辑其实就是获得锁的逆向逻辑,通过CAS 操作把线程栈帧中的LockRecord替换回到锁对象的MarkWord中,如果成功表示没有竞争。如果失败,表示当前锁存在竞争,那么轻量级锁就会膨胀成为重量级锁

synchronized、偏向锁和轻量级锁

总结和示例线程的竞争机制

有这样一个同步代码块,存在 Thread#1、Thread#2 等多个线程
synchronized (lock) {
// do something
}
情况一:只有 Thread#1 会进入临界区;
情况二:Thread#1和Thread#2交替进入临界区,竞争不激烈;
情况三:Thread#1/Thread#2/Thread3…同时进入临界区,竞争激烈;

进入偏向锁

此时当 Thread#1 进入临界区时,JVM会将lockObject 的对象头 Mark Word 的锁标志位设为“01”,同时会用 CAS 操作把 Thread#1 的线程 ID 记录到 Mark Word 中,此时进入偏向模式。所谓“偏向”,指的是这个锁会偏向于 Thread#1,若接下来没有其他线程进入临界区,则 Thread#1再出入临界区无需再执行任何同步操作。也就是说,若只有Thread#1会进入临界区,实际上只有 Thread#1初次进入临界区时需要执行CAS操作,以后再出入临界区都不会有同步操作带来的开销。

进入轻量级锁

偏向锁的场景太过于理想化,更多的时候是 Thread#2 也会尝试进入临界区,如果Thread#2也进入临界区但是Thread#1 还没有执行完同步代码块时,会暂停Thread#1并且升级到轻量级锁。Thread#2通过自旋再次尝试以轻量级锁的方式来获取锁。

进入重量级锁

如果 Thread#1和Thread#2正常交替执行,那么轻量级锁基本能够满足锁的需求。但是如果Thread#1和Thread#2同时进入临界区,那么轻量级锁就会膨胀为重量级锁,意味着 Thread#1线程获得了重量级锁的情况下,Thread#2就会被阻塞。

Synchronized 结合 Java Object 对象中的wait,notify,notifyAll

前面我们在讲synchronized的时候,发现被阻塞的线程什么时候被唤醒,取决于获得锁的线程什么时候执行完同步代码块并且释放锁。那怎么做到显示控制呢?我们就需要借助一个信号机制:在Object对象中,提供了wait/notify/notifyall,可以用于控制线程的状态;
synchronized、偏向锁和轻量级锁

本文如有错误或不足之处望指出,感谢浏览!