Juc:锁升级、偏向轻重锁、wait notify notifyall「终于解决」

Juc:锁升级、偏向轻重锁、wait notify notifyall「终于解决」进程和线程 锁升级 Java 中存在了三种类型得锁,分别是偏向锁、轻量锁和重量锁。 我们可以用一个门口带有小黑板的房间举例: 偏向锁: 加锁:你进入一个门口带有小黑板的房间。小黑板上是空白一片的,你进

进程和线程

锁升级

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

          Juc:锁升级、偏向轻重锁、wait notify notifyall「终于解决」

      • 获取失败

        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();
        }
    }
}

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
转载请注明出处: https://daima100.com/13298.html

(0)

相关推荐

  • 以高反差保留

    以高反差保留高反差保留(High-Pass Retention)是一种图像处理技术,它在保留图像的细节同时去除低频细节,使得图像更加锐利。该技术可以应用于图像增强、降噪、边缘检测等领域,同时也是数字图像处理入门知识。

    2024-05-28
    61
  • 用python批量重命名linux目录

    用python批量重命名linux目录在Linux系统中,经常需要对大量的文件夹进行重命名操作,手动修改文件夹名称效率较低,需要使用自动化工具进行批量重命名。Python是一种灵活的脚本语言,可以方便地进行批量操作。本篇文章将介绍如何使用Python对Linux系统中的文件夹进行批量重命名。

    2024-01-30
    114
  • Electron中使用sql.js操作SQLite数据库「建议收藏」

    Electron中使用sql.js操作SQLite数据库「建议收藏」推荐sql.js——一款纯js的sqlite工具。 一、关于sql.js sql.js(https://github.com/kripken/sql.js)通过使用Emscripten编译SQLite

    2022-12-23
    141
  • 挖矿设备(比特币挖矿设备)

    挖矿设备(比特币挖矿设备)

    2023-08-28
    142
  • 美团大脑百亿级知识图谱的构建及应用进展[亲测有效]

    美团大脑百亿级知识图谱的构建及应用进展[亲测有效]分享嘉宾:张鸿志博士 美团 算法专家 编辑整理:廖媛媛 美的集团 出品平台:DataFunTalk **导读:**美团作为中国最大的在线本地生活服务平台,连接着数亿用户和数千万商户,其背后蕴含着丰富的

    2023-05-28
    137
  • sqlserver修改数据库文件路径_sql server图形化界面

    sqlserver修改数据库文件路径_sql server图形化界面第一步: 将所有副本可读设置为 “否” 第二步: 在主副本上设置挂起 ALTER DATABASE Erp_Wygl_6008 SET HADR SUSPEND 第三步: 设置迁移后的文件路径 SE…

    2023-04-13
    153
  • Python二维字典操作

    Python二维字典操作字典是Python语言中最常用的一种数据类型,它可以存储键值对的数据,例如一个人的姓名和年龄。而二维字典则是指在字典中再嵌套一个字典,即将一个二维坐标用键值对的方式进行存储。例如,可以用字典存储多个城市的经纬度,其中经纬度又用键值对进行存储。

    2024-05-12
    73
  • 怎样看电脑系统版本具体型号_计算机版本在哪里看

    怎样看电脑系统版本具体型号_计算机版本在哪里看1、打开电脑,点击电脑左下角的开始菜单,在弹出的菜单选项中选择“控制面板”。 2、打开控制面板,点击“系统和安全”。 3、进入系统和安全页面,点击系统下面的“查看该计算机的名称”。 4、打开新页面,…

    2023-04-13
    160

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注