# 1.轻量级锁

  • 轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。

image.png 轻量级锁执行流程:在轻量级锁中不是采用monitor,而是采用一种锁记录(LockRecord)的机制,首先在锁记录中有俩部分组成:对象指针(Object reference)和MarkWord。对象指针指向的就是我们需要加锁对象地址,MarkWord中存储的就是对象对象头的信息(具体可以看我上一节的内容,已经为什么要存储,是为了后期锁恢复用的),而对象的对象头中的MrakWord就编程了锁地址(00代表轻量级锁,01代表无锁状态).当有其他线程来进行加锁时,他可以通过对象头中的锁地址判断是不是同一把锁,如果是同一把锁的话不会发生锁膨胀现象,而是增加一条锁记录。当线程执行完,也是先消除掉刚刚的锁记录,然后在消除第一条锁记录。

# 2.锁膨胀

  • 如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

image.png 锁膨胀情况的发生,就是Thread-0上锁以后,此时因为没有其他的锁来竞争,导致现在是轻量级锁,当有其他线程来竞争的时候,刚开始也想给这个对象加轻量级锁,但发现这个对象头中的MarkWord中的地址已经有了别的锁,加轻量级锁失败,进入锁膨胀流程。即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址。然后自己进入 Monitor的EntryList 阻塞

  • 当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置Owner为null(代表当时没有人持有这把锁),唤醒EntryList中BLOCKED醒 EntryList 中 BLOCKED 线程

# 3.自旋优化

  • 重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

image.png 解释下自旋优化的原理:简单点来说就是线程1已经获取了锁(重量级锁的情况下),此时这个线程正在执行同步代码块,此时如果有线程2来访问同步代码块,肯定要先获取锁,这时候因为线程1还没有执行完,线程2肯定拿不到锁,这个时候不会立即进入阻塞状态,而是会进行自旋重试,有俩种情况,

  • 一种情况就是尝试了几次自旋重试后,在自旋重试过程中恰好线程1执行完了,线程2就直接获取到了锁,执行相应内容
  • 另一种情况就是线程2自旋了好多次,都没有拿到锁,因为自旋模式会导致线程2一直占用cpu资源,线程2就会进入阻塞状态 为什么会有自旋重试,因为有的时候同步代码块的内容比较少,执行起来比较快,可能自旋一俩次就成功了,这种情况下就不用进入阻塞状态。这种情况下就会大大减少cpu的使用资源,因为线程上下文状态的切换,浪费的资源还是挺多的。
  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
  • 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
  • Java 7 之后不能控制是否开启自旋功能

# 4.偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。 Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。

偏向锁存在的意义简单来说,如果一直是轻量级锁,岂不是每次冲入,都要增加一条锁记录,这样太麻烦了。现在我们只需要去判断对象头中的MarkWord的ThreadId 就好了。

# 5.锁的变化流程

image.png

简单点来说就是刚开始只有Thread-0线程持有这个对象的锁,这个时候就是轻量级锁。然后当偶尔有其他线程来访问这个对象的同步代码块的时候且是在Thread-0执行结束以后。这个时候就会变成轻量级锁。当有很多线程都来访问这个对象的同步代码块,且造成了锁竞争,这个时候就升级成了重量级锁

# 6.各个锁状态下对象头中MrakWord的信息

image.png