1、synchronized初识

:::info syncronized是通过互斥同步实现线程安全的常见手段。 :::

syncronized关键字经过编译后会形成monitorenter和monitorexit两个字节码指令,这两个字节码指令都需要reference类型的参数指明要锁定和解锁的对象。如果syncronized没有明确指定对象参数,那么根据syncronized修饰的是实例方法或者静态方法,取实例对象或者类对象作为锁对象。

syncronized在jdk1.6之前是属于重量级的锁,syncronized是需要阻塞或者唤醒线程的,而这需要操作系统从用户态切换到核心态,状态切换需要耗费许多的处理器时间,对于简单的同步块,状态转换的时间可能比代码执行的时间长。
jdk 1.6 中为了减少获得锁和释放锁带来的性 能消耗而引入的偏向锁和轻量级锁。

2、HotSpot虚拟机对象头Mark World

synchronized详解 - 图1 HotSpot虚拟机中,对象头包括两部分数据,第一部分用于存储自身运行时数据,官方称为Mark World,如哈希码,Gc分代年龄,锁状态标志,线程持有的锁,偏向线程id,偏向时间戳等,,这部分数据的长度在32位虚拟机和64位虚拟机中分别为32位和64位。第二部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定对象是哪个类的实例。

在32位的虚拟机中:

synchronized详解 - 图2

在64位的虚拟机中:

synchronized详解 - 图3

3、锁的升级

(1)偏向锁

因为经过HotSpot的作者大量的研究发现,大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁。

:::info 偏向锁的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。 :::

如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那么偏向锁就是在无竞争情况下把整个同步都消除掉,连CAS都不做了。

如果当前虚拟机启用了偏向锁,当锁对象第一次被线程获取的时候,虚拟机会将对象头中Mark World锁标志位置为01,是否偏向锁标志位置为1,即偏向模式。同时使用CAS将获取到锁的线程id记录到锁对象的Mark World中,如果CAS成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不做任何同步操作。
当另一个线程去尝试获取锁时,偏向模式结束。偏向锁不会主动释放锁,所以当获取锁的线程与锁对象对象头中记录的线程id不一致时,需要查看锁对象记录的线程是否存活,如果不在存活,锁对象被重置位无锁状态,其它线程可以将其设置为偏向锁。如果存活,则立刻查找该线程的栈帧信息,如果还需要继续持有锁对象。那么暂停当前持有锁的线程,撤销偏向锁,升级位轻量级锁,如果持有锁的线程不再使用该锁对象,则锁对象被重置位无锁状态,重新偏向其它线程。

(2)轻量级锁

:::info 轻量级锁不是用来代替重量级锁的,轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,可能阻塞不久锁就释放了,所以干脆不阻塞线程,自旋等待锁释放。 :::

轻量级锁加锁:

在代码进入同步块的时候,如果同步对象没有被锁定,虚拟机首先在当前线程的栈帧中创建一个名为锁记录的空间(DisplacedMarkWord,用于存储对象目前的mark world拷贝。然后使用CAS将对象头中的Mark world替换为线程中存储的锁记录的地址。

轻量级锁解锁:
轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁

如果在线程1复制对象头的同时(在线程1CAS之前),线程2也准备获取锁,复制了锁对象的mark World到自身线程的锁空间中,但是线程2 CAS操作时失败,那么线程2就尝试自旋锁来等待线程1释放锁。
当自旋到一定次数时,如果还没有获取到锁,或者说线程1在执行,线程2在自旋等待锁,此时线程3来竞争锁,那么轻量级锁会膨胀为重量级锁.

注意:
** :::danger 锁可以升级但是不能降级,但是偏向锁可以被重置为无锁状态

为了避免无用的自旋,轻量级锁一旦膨胀为重量级锁就不会再降级为轻量级锁了;偏向锁升级为轻量级锁也不能再降级为偏向锁 :::


synchronized详解 - 图4