Juc:锁升级、偏向轻重锁、wait notify notifyall

101 阅读9分钟

进程和线程

锁升级

Java 中存在了三种类型得锁,分别是偏向锁、轻量锁和重量锁。

我们可以用一个门口带有小黑板的房间举例:

  • 偏向锁:
    • 加锁:你进入一个门口带有小黑板的房间。小黑板上是空白一片的,你进入房间之前先把自己的名字写上
    • 释放锁:你直接离开
    • 再次加锁:你进入房间前,看见小黑板上依旧是写着你的名字,你推门就进
  • 轻量锁:
    • 加锁:你进入一个门口带有小黑板的房间。小黑板上是空白一片的。你进入房间之前先把自己的名字写上
    • 释放锁:你把门口小黑板的名字擦掉然后离开
    • 再次加锁:你进入房间前,先看小黑板上是否写着你的名字。如果空白一片则进入,就在进来之前先把你的名字写了
  • 重量锁:
    • 加锁:你进入一个门口带有小黑板的房间,先上去晃动一下门看看有没有被锁起来,没有就进去上锁;有就在门口等待
    • 释放锁:你把门锁打开,出去。
    • 再次加锁:再去看看门有没有被锁起来,没有就去门口等待。

偏向锁

轻量级锁

锁对象中存在这自己的锁记录Mark Word,其中记录了hash、age、状态码 01 等其他信息

  • 上锁:

    1. 企图上锁的线程在自己的栈中创建栈帧。栈帧中包含了锁记录Mark Word 和锁对象地址。Mark Word里记录了线程自己的地址和状态码 00。锁对象地址指向锁对象。

      创建栈帧

    2. 线程指向锁对象,尝试用 cas 替换将锁记录自己的 Mark Word 和锁对象的 Mark Word 进行替换

      Mark Word 交换

      • 获取成功

        1. 检查锁对象状态为是否为 01,如果为 01 则代表可以替换。替换成功,锁对象的 Mark Word 的状态将从 01 转换为 00

      • 获取失败

        1. 如果不为 01,则说明已上锁。这样就代表了资源竞争,会导致锁膨胀。

        2. 不为 01,但是发现锁对象的 Mark Word 指向自己的时候,代表锁重入。这时候线程会创建一个新的栈帧。新栈帧和第一次获取锁的栈帧不同,它的 Mark Word 是 null,但是锁对象地址依旧指向锁对象

          锁重入

  • 解锁:

    1. 对线程的栈进行 pull 操作,发现有 Mark Word 为 null 的栈帧时,代表出现了锁重入现象。只需要把对应的栈帧清除即可。

    2. 当发现 Mark Word 不为 null 的时候,会采用 cas 将对应栈帧 Mark Word 和锁对象的 Mark Word 进行交换。

      解锁

      • 交换成功:锁释放
      • 交换失败:说明在上锁期间进入了锁膨胀或许锁已升级为重量级锁,进入重量级锁解锁流程

重量级锁

  • Monitor

    Monitor

Monitor 就是锁,也是重量级锁里重要的一环。他的 WaitSet 是用来存放调用 Wait 方法的线程;Owner 是用来存在当前上锁的线程;EntryList 用来存放等待释放锁的线程。

每一个 Java 对象都可以关联到一个 Monitor 对象。如果一个锁对象是重量级锁,那么他的 Mark Word 就会指向 Monitor 对象的指针。接下来就通过 线程、锁对象、Monitor 来讲解一下重量级锁的上锁和解锁过程。

  • 上锁:
    • 线程 A 向锁对象进行查询,判断锁对象的 Monitor 对象是否存在
      • 不存在,就为锁对象关联对应的 Monitor 对象,并且在 Monitor 对象中的 Owner 指向自己的地址,即上锁
      • 存在,则判断 Monitor 对象的 Owner 是否存在
        • 不存在,则将 Owner 指向自己的地址,即上锁
        • 存在,则将自己放入 Monitor 对象的 EntryList 当中。
  • 解锁:
    • 线程 A 解锁后,让 Owner 指向 null
    • 通知 Monitor 的 EntryList,让 EntryList 的线程进行争夺,让其成为最后的 Owner

锁膨胀

当我们的偏向锁或者轻量级加锁过程中,cas 操作无法成功,这个时候说明了该资源有竞争。这时候需要进行锁膨胀,将锁向上提升一个量级。我们先用轻量级锁膨胀到重量级锁为例。

线程B 向锁对象加锁,但是锁对象的 Mark Word 的状态已经变成了 00,并且也指向了线程A。这个时候就是加锁操作失败,进入锁膨胀流程

  1. 为锁对象申请 Monitor 对象,让锁对象的 Mark Word 指向 Monitor 地址,状态变为 10。
  2. Monitor 对象的 Owner 指向现在加锁的线程A
  3. 线程B 放入 Monitor 的 EntryList 中等待。
  4. 线程 A 解锁后,让 Owner 指向 null
  5. 通知 Monitor 的 EntryList,让 EntryList 的线程进行争夺,让其成为最后的 Owner

wait、notify、notifyAll

api

[使用wait和notify - 廖雪峰的官方网站]

举例子

  • 为什么调用wait、notify、notifyAll
    • 有的线程进行下去的时候必须要有一些条件去满足。当出现 Owner 线程的条件不满足的时候,可以让该线程调用一个 wait 线程,让其释放锁。当线程满足之后再将其唤醒,让其继续进行未完成的事情。

我们就用一个房间来举例子

  • wait
    • 有一个独立的房间名字叫 Owner 。Owner 用来自习,每次只能进去一个人
    • 小明先在房间的门口那条名字叫 EntryList 的走廊里等待,发现没人他就直接拿进 Owner学习了。
    • 小明进 Owner 本来希望复习【语数英课本】但是发现 Owner 里没有【语数英课本】。这样子他即没办法学习也占用了 Owner 。
    • 小明主动调用了 wait() 方法,申请要出门到旁边的一个叫 WaitSet 的等待室等待,直到有【语数英课本】才能继续自己的学习
    • 这样子这件 Owner 就空闲了,就可以有其他的人进去使用。
  • notify
    • 房间空出来之后,在 EntryList 里排队的小红进入了 Owner。
    • 他是小明的马仔,专门是来为小明放书的。他把【语数英课本】放在 Owner 里,然后调用了 notify() 方法,紧接着他就离开了
    • notify() 方法调用之后,就会在 WaitSet 里随机唤醒一个幸运儿让他去 EntryList 里排队。如果抽中小明,当小明进入 Owner 的时候发现【语数英课本】都在,就能继续复习了。如果发现没有【语数英课本】,小明就会回到 WaitSet 当中
  • notifyAll
    • 有的时候 WaitSet 当中可能是有多个人在等待。
    • 小红就不可以调用 notify() 方法去随机唤醒幸运儿了。为了确保小明能够从 WaitSet 中出来到 EntryList 排队到 Owner 里看到自己放的【语数英课本】,他就可以调用 notifyAll() 方法。
    • notifyAll() 方法调用之后,WaitSet 当中的所有人都会被唤醒到 EntryList 中排队。有的人发现 Owner 中满足了自己的条件就回去完成自己的事情。有的人发现依旧没满足就会继续去 WaitSet 中等待。小明就发现 Owner 满足了自己的条件,就会继续自己的学习了。

wait、notify、notifyAll的使用

  • Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变成 WAITING 状态
  • BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
  • BLOCKED 线程会在 Owner 线程释放锁的时候唤醒
  • WAITING 线程会在 Owner 调用 notify() 或 notifyAll() 时唤醒,但是唤醒后并不意味着立刻获得锁,仍需要进入 EntryList 重新竞争

虚假唤醒

notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线 程,称之为【虚假唤醒】

虚假唤醒的解决方法:notifyAll()

每个需要调用 wait 的线程格式:

synchronized(lock){
    ....
    while(!条件是否满足){
        lock.wait();
    }
    ....
}

唤醒的线程:

synchronized(lock){
    ....
    lock.notifyAll();
    ....
}

保护性暂停

保护性暂停模式就是提供了一种线程间通信能力的模式。

如果有一个线程的执行结果需要传递给另一个线程,就需要使用保护性暂停模式将两条线程关联起来。

JDK 中 join 方法和 Future 就是使用了此模式实现的。

  1. 创建一个类来保存执行结果,里面包含了一个线程安全的存入数据方法和获取方法

    • 存入数据方法

          /**
           * 传入数据
           *
           * @param date 数据
           */
          public void pushData(Object date) {
              synchronized (this) {
                  this.data = date;
                  this.notifyAll();
              }
          }
      
    • 获取数据方法

      	/**
           * 获取数据
           *
           * @param timeout 最长等待时间
           * @return 数据
           */
          public Object pullData(long timeout) {
              synchronized (this) {
                  // 开始时间
                  long begin = System.currentTimeMillis();
                  // 已经使用的时间
                  long passTime = 0;
                  while (data == null) {
                      // 剩余可等待时间
                      long waitTime = timeout - passTime;
                      if (waitTime <= 0) {
                          break;
                      }
                      try {
                          this.wait(waitTime);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                      passTime = System.currentTimeMillis() - begin;
                  }
                  return data;
              }
          }
      

      我们就来重点解释一下这个获取方法。可以看到获取方法当中是使用了时间和 while 循环,这是为了避免出现虚假唤醒和过长等待而设置的。

      • 方法先获取传入的参数最长等待时间 timeout
      • 记录我们的开始时间 begin 和我们已经使用的时间 passTime
      • 然后进入防止虚假唤醒的训话当中。在每次循环开始之前先计算出剩余可等待时间 waitTime
      • 如果 waitTime <= 0 代表无剩余可等待时间了,就退出循环返回结果
      • 如果还有剩余可等待时间,就进入等待,并且传入可剩余等待时间
      • 如果被虚假唤醒,则重新计算 passTime

      pull流程

  2. 创建多线程环境,一个线程负责存入数据,一个线程负责获取数据

    public static void main(String[] args) {
            GuardedObject guardedObject = new GuardedObject();
    
            // 获取数据线程
            new Thread(() -> {
                log.debug("开始等待获取");
                Object data = guardedObject.pullData(2000);
                log.debug("获取结束:{}", data);
            }, "pull").start();
    
            // 存入数据线程
            new Thread(() -> {
                log.debug("开始存入数据");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 测试传入空值,pull线程是否会等待
                guardedObject.pushData(null);
                // 传入数据,pull 线程是否会提前结束
    //            guardedObject.pushData("data");
                log.debug("存入数据");
            }, "push").start();
        }
    

所有代码:

import lombok.extern.slf4j.Slf4j;

import java.security.PrivateKey;
import java.util.concurrent.TimeUnit;

/**
 * @author HGD
 * @date 2022/12/14 23:21
 */
@Slf4j
public class Test7 {
    public static void main(String[] args) {
        GuardedObject guardedObject = new GuardedObject();

        // 获取数据线程
        new Thread(() -> {
            log.debug("开始等待获取");
            Object data = guardedObject.pullData(2000);
            log.debug("获取结束:{}", data);
        }, "pull").start();

        // 存入数据线程
        new Thread(() -> {
            log.debug("开始存入数据");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 测试传入空值,pull线程是否会等待
            guardedObject.pushData(null);
            // 传入数据,pull 线程是否会提前结束
//            guardedObject.pushData("data");
            log.debug("存入数据");
        }, "push").start();
    }
}

/**
 * 增加超时效果
 */
class GuardedObject {
    /**
     * 需要传递的传递的数据
     */
    private Object data;

    /**
     * 获取数据
     *
     * @param timeout 最长等待时间
     * @return 数据
     */
    public Object pullData(long timeout) {
        synchronized (this) {
            // 开始时间
            long begin = System.currentTimeMillis();
            // 循环经历时间
            long passTime = 0;
            while (data == null) {
                // 剩余可等待时间
                long waitTime = timeout - passTime;
                if (waitTime <= 0) {
                    break;
                }
                try {
                    this.wait(waitTime);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                passTime = System.currentTimeMillis() - begin;
            }
            return data;
        }
    }

    /**
     * 传入数据
     *
     * @param date 数据
     */
    public void pushData(Object date) {
        synchronized (this) {
            this.data = date;
            this.notifyAll();
        }
    }
}