Day850.GuardedSuspension模式 -Java 性能调优实战

news2024/9/20 16:43:54

GuardedSuspension模式

Hi,我是阿昌,今天学习记录的是关于GuardedSuspension模式的内容。

小灰工作中遇到一个问题,开发了一个 Web 项目:Web 版的文件浏览器,通过它用户可以在浏览器里查看服务器上的目录和文件。

这个项目依赖运维部门提供的文件浏览服务,而这个文件浏览服务只支持消息队列(MQ)方式接入。

消息队列在互联网大厂中用的非常多,主要用作流量削峰系统解耦

在这种接入方式中,发送消息和消费结果这两个操作之间是异步的,可以参考下面的示意图来理解。

消息队列(MQ)示意图
在小灰的这个 Web 项目中,用户通过浏览器发过来一个请求,会被转换成一个异步消息发送给 MQ,等 MQ 返回结果后,再将这个结果返回至浏览器。

小灰同学的问题是:给 MQ 发送消息的线程是处理 Web 请求的线程 T1,但消费 MQ 结果的线程并不是线程 T1,那线程 T1 如何等待 MQ 的返回结果呢?

为了便于理解这个场景,将其代码化了,示例代码如下。


class Message{
  String id;
  String content;
}
//该方法可以发送消息
void send(Message msg){
  //省略相关代码
}
//MQ消息返回后会调用该方法
//该方法的执行线程不同于
//发送消息的线程
void onMessage(Message msg){
  //省略相关代码
}
//处理浏览器发来的请求
Respond handleWebReq(){
  //创建一消息
  Message msg1 = new 
    Message("1","{...}");
  //发送消息
  send(msg1);
  //如何等待MQ返回的消息呢?
  String result = ...;
}

一、Guarded Suspension 模式

上面小灰遇到的问题,在现实世界里比比皆是,只是一不小心就忽略了。

比如,项目组团建要外出聚餐,提前预订了一个包间,然后兴冲冲地奔过去,到那儿后大堂经理看了一眼包间,发现服务员正在收拾,就会告诉:“您预订的包间服务员正在收拾,请您稍等片刻。”过了一会,大堂经理发现包间已经收拾完了,于是马上带去包间就餐。

等待包间收拾完的这个过程和小灰遇到的等待 MQ 返回消息本质上是一样的,都是等待一个条件满足:就餐需要等待包间收拾完,小灰的程序里要等待 MQ 返回消息。

那来看看现实世界里是如何解决这类问题的呢?现实世界里大堂经理这个角色很重要,是否等待,完全是由他来协调的。通过类比,相信也一定有思路了:程序里,也需要这样一个大堂经理。

的确是这样,那程序世界里的大堂经理该如何设计呢?其实设计方案前人早就搞定了,而且还将其总结成了一个设计模式:Guarded Suspension

所谓 Guarded Suspension,直译过来就是“保护性地暂停”。

那下面我们就来看看,Guarded Suspension 模式是如何模拟大堂经理进行保护性地暂停的。

下图就是 Guarded Suspension 模式的结构图,非常简单,一个对象 GuardedObject,内部有一个成员变量——受保护的对象,以及两个成员方法——get(Predicate p)和onChanged(T obj)方法。

其中,对象 GuardedObject 就是前面提到的大堂经理,受保护对象就是餐厅里面的包间;
受保护对象的 get() 方法对应的是我们的就餐,就餐的前提条件是包间已经收拾好了,参数 p 就是用来描述这个前提条件的;
受保护对象的 onChanged() 方法对应的是服务员把包间收拾好了,通过 onChanged() 方法可以 fire 一个事件,而这个事件往往能改变前提条件 p 的计算结果。

下图中,左侧的绿色线程就是需要就餐的顾客,而右侧的蓝色线程就是收拾包间的服务员。

Guarded Suspension 模式结构图
GuardedObject 的内部实现非常简单,是管程的一个经典用法,可以参考下面的示例代码,核心是:

  • get() 方法通过条件变量的 await() 方法实现等待

  • onChanged() 方法通过条件变量的 signalAll() 方法实现唤醒功能。


class GuardedObject<T>{
  //受保护的对象
  T obj;
  final Lock lock = 
    new ReentrantLock();
  final Condition done =
    lock.newCondition();
  final int timeout=1;
  //获取受保护对象  
  T get(Predicate<T> p) {
    lock.lock();
    try {
      //MESA管程推荐写法
      while(!p.test(obj)){
        done.await(timeout, 
          TimeUnit.SECONDS);
      }
    }catch(InterruptedException e){
      throw new RuntimeException(e);
    }finally{
      lock.unlock();
    }
    //返回非空的受保护对象
    return obj;
  }
  //事件通知方法
  void onChanged(T obj) {
    lock.lock();
    try {
      this.obj = obj;
      done.signalAll();
    } finally {
      lock.unlock();
    }
  }
}

二、扩展 Guarded Suspension 模式

Guarded Suspension 模式里 GuardedObject 有两个核心方法:

  • 一个是 get() 方法
  • 一个是 onChanged() 方法

很显然,在处理 Web 请求的方法 handleWebReq() 中,可以调用 GuardedObject 的 get() 方法来实现等待;
在 MQ 消息的消费方法 onMessage() 中,可以调用 GuardedObject 的 onChanged() 方法来实现唤醒。


//处理浏览器发来的请求
Respond handleWebReq(){
  //创建一消息
  Message msg1 = new 
    Message("1","{...}");
  //发送消息
  send(msg1);
  //利用GuardedObject实现等待
  GuardedObject<Message> go
    =new GuardObjec<>();
  Message r = go.get(
    t->t != null);
}
void onMessage(Message msg){
  //如何找到匹配的go?
  GuardedObject<Message> go=???
  go.onChanged(msg);
}

但是在实现的时候会遇到一个问题,handleWebReq() 里面创建了 GuardedObject 对象的实例 go,并调用其 get() 方等待结果,那在 onMessage() 方法中,如何才能够找到匹配的 GuardedObject 对象呢?

这个过程类似服务员告诉大堂经理某某包间已经收拾好了,大堂经理如何根据包间找到就餐的人。

现实世界里,大堂经理的头脑中,有包间和就餐人之间的关系图,所以服务员说完之后大堂经理立刻就能把就餐人找出来。

可以参考大堂经理识别就餐人的办法,来扩展一下 Guarded Suspension 模式,从而使它能够很方便地解决小灰同学的问题。

在小灰的程序中,每个发送到 MQ 的消息,都有一个唯一性的属性 id,所以可以维护一个 MQ 消息 id 和 GuardedObject 对象实例的关系,这个关系可以类比大堂经理大脑里维护的包间和就餐人的关系。有了这个关系,来看看具体如何实现。

下面的示例代码是扩展 Guarded Suspension 模式的实现,扩展后的 GuardedObject 内部维护了一个 Map,其 Key 是 MQ 消息 id,而 Value 是 GuardedObject 对象实例,同时增加了静态方法 create() 和 fireEvent();

create() 方法用来创建一个 GuardedObject 对象实例,并根据 key 值将其加入到 Map 中,而 fireEvent() 方法则是模拟的大堂经理根据包间找就餐人的逻辑。


class GuardedObject<T>{
  //受保护的对象
  T obj;
  final Lock lock = 
    new ReentrantLock();
  final Condition done =
    lock.newCondition();
  final int timeout=2;
  //保存所有GuardedObject
  final static Map<Object, GuardedObject> 
  gos=new ConcurrentHashMap<>();
  //静态方法创建GuardedObject
  static <K> GuardedObject 
      create(K key){
    GuardedObject go=new GuardedObject();
    gos.put(key, go);
    return go;
  }
  static <K, T> void 
      fireEvent(K key, T obj){
    GuardedObject go=gos.remove(key);
    if (go != null){
      go.onChanged(obj);
    }
  }
  //获取受保护对象  
  T get(Predicate<T> p) {
    lock.lock();
    try {
      //MESA管程推荐写法
      while(!p.test(obj)){
        done.await(timeout, 
          TimeUnit.SECONDS);
      }
    }catch(InterruptedException e){
      throw new RuntimeException(e);
    }finally{
      lock.unlock();
    }
    //返回非空的受保护对象
    return obj;
  }
  //事件通知方法
  void onChanged(T obj) {
    lock.lock();
    try {
      this.obj = obj;
      done.signalAll();
    } finally {
      lock.unlock();
    }
  }
}

这样利用扩展后的 GuardedObject 来解决小灰同学的问题就很简单了,具体代码如下所示。


//处理浏览器发来的请求
Respond handleWebReq(){
  int id=序号生成器.get();
  //创建一消息
  Message msg1 = new 
    Message(id,"{...}");
  //创建GuardedObject实例
  GuardedObject<Message> go=
    GuardedObject.create(id);  
  //发送消息
  send(msg1);
  //等待MQ消息
  Message r = go.get(
    t->t != null);  
}
void onMessage(Message msg){
  //唤醒等待的线程
  GuardedObject.fireEvent(
    msg.id, msg);
}

三、总结

Guarded Suspension 模式本质上是一种等待唤醒机制的实现,只不过 Guarded Suspension 模式将其规范化了。

规范化的好处是你无需重头思考如何实现,也无需担心实现程序的可理解性问题,同时也能避免一不小心写出个 Bug 来。

但 Guarded Suspension 模式在解决实际问题的时候,往往还是需要扩展的,扩展的方式有很多,对 GuardedObject 的功能进行了增强,Dubbo 中 DefaultFuture 这个类也是采用的这种方式,可以对比着来看,相信对 DefaultFuture 的实现原理会理解得更透彻。当然,也可以创建新的类来实现对 Guarded Suspension 模式的扩展。

Guarded Suspension 模式也常被称作 Guarded Wait 模式、Spin Lock 模式(因为使用了 while 循环去等待),这些名字都很形象,不过它还有一个更形象的非官方名字:多线程版本的 if。

单线程场景中,if 语句是不需要等待的,因为在只有一个线程的条件下,如果这个线程被阻塞,那就没有其他活动线程了,这意味着 if 判断条件的结果也不会发生变化了。但是多线程场景中,等待就变得有意义了,这种场景下,if 判断条件的结果是可能发生变化的。

所以,用“多线程版本的 if”来理解这个模式会更简单。


用 done.await() 还要加锁,太啰嗦,还不如直接使用 sleep() 方法,下面是他的实现,你觉得他的写法正确吗?


//获取受保护对象  
T get(Predicate<T> p) {
  try {
    while(!p.test(obj)){
      TimeUnit.SECONDS
        .sleep(timeout);
    }
  }catch(InterruptedException e){
    throw new RuntimeException(e);
  }
  //返回非空的受保护对象
  return obj;
}
//事件通知方法
void onChanged(T obj) {
  this.obj = obj;
}

sleep 无法被唤醒,只能时间到后自己恢复运行,当真正的条件满足了,时间未到,接着睡眠,无性能可言


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/142081.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

(原创)Flow数据流的使用

前言 这篇文章主要介绍Flow的一些基础使用方法 同时介绍如何用Flow请求网络数据 下面开始&#xff01; 什么是Flow Flow翻译过来&#xff0c;是“流”的意思 举例说明&#xff0c;在大自然中&#xff0c;常见的如水流 是从高往低流动的 那么在计算机世界里&#xff0c;所谓的…

loss盘点: asl loss (Asymmetric Loss) 代码解析详细版

1. BCE公式部分 可以简单浏览下这篇博客的文章&#xff1a; https://blog.csdn.net/qq_14845119/article/details/114121003 这是多分类 经典 BCELossBCELossBCELoss 公式 L−yL−(1−y)L−L -y L_{} - (1-y) L_{-} L−yL​−(1−y)L−​ 其中&#xff0c;L/−L_{/-}L/−​…

Docker保姆级学习教程

文章目录1、什么是Docker1.1、容器技术1.2、容器与虚拟机比较1.3、Docker特点1、更高效的利用系统资源2、更快速的启动时间3、一致的运行环境4、持续支付和部署5、更轻松的迁移6、更轻松的维护和扩展2、Docker组件学习2.1、Docker客户端和服务器2.2、Docker镜像2.3、Registry&a…

奇怪的知识——Windows下怎么修改进程的名称?

&#x1f4e2;欢迎点赞 &#xff1a;&#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff0c;赐人玫瑰&#xff0c;手留余香&#xff01;&#x1f4e2;本文作者&#xff1a;由webmote 原创&#x1f4e2;作者格言&#xff1a;无尽的折腾后&#xff0c;终于又回到…

element-plus的form表单form-item的prop怎么写才能正确校验,实现逻辑是怎么样的?

不管是element-plus还是上一个版本的element-ui&#xff0c;都是一个使用很广泛的基于csshtmljs的ui组件库&#xff0c;它的form表单自带强大的校验功能&#xff0c;form-item的prop怎么写才正确&#xff0c;实现逻辑是怎么样的&#xff1f;element-plus的form表单的model、for…

聚观早报 | 苹果市值跌破2万亿美元大关;卢伟冰晋升小米集团总裁

今日要闻&#xff1a;苹果市值跌破2万亿美元大关&#xff1b;卢伟冰晋升小米集团总裁&#xff1b;京东方拿下iPhone 15订单&#xff1b;英伟达与富士康达成合作&#xff1b;哪吒汽车旗下车型价格调整苹果市值跌破2万亿美元大关 1 月 4 日消息&#xff0c;据国外媒体报道&#x…

C51单片机连接wifi模块,发送AT指令

一、AT指令AT 指令集是从终端设备&#xff08; Terminal Equipment &#xff0c; TE) 或 数据终端设备 &#xff08; Data TerminalEquipment &#xff0c; DTE) 向终端适配器 (Terminal Adapter &#xff0c; TA) 或 数据电路终端设备 (Data CircuitTerminal Equipment &#…

CDGA|企业数字化转型进展得越快就越好吗?

数据治理并不是一件简单的事情。即使是行业知名公司&#xff0c;在高度重视和确保投入的情况下&#xff0c;完成全公司“数据底座”/“数据中台”的所耗时间也往往以年计。并且&#xff0c;还需要注意到&#xff0c;数据规范只是数字化转型的一个维度而已&#xff1a; 在国标《…

SQL INSERT INTO 语句

INSERT INTO 语句用于向表中插入新记录。 SQL INSERT INTO 语句 INSERT INTO 语句用于向表中插入新记录。 SQL INSERT INTO 语法 INSERT INTO 语句可以有两种编写形式。 第一种形式无需指定要插入数据的列名&#xff0c;只需提供被插入的值即可&#xff1a; INSERT INTO t…

Python爬虫常用哪些库?

经常游弋在互联网爬虫行业的程序员来说&#xff0c;如何快速的实现程序自动化&#xff0c;高效化都是自身技术的一种沉淀的结果&#xff0c;那么使用Python爬虫都会需要那些数据库支持&#xff1f;下文就是有关于我经常使用的库的一些见解。 请求库&#xff1a; 1、urllib&a…

matlab复杂函数多元函数拟合

简介 本文介绍了基于matlab实现的复杂函数以及多元函数的拟合。在工程和研究中偶尔会遇到要用一个非常复杂的数学公式来拟合实验测量数据&#xff0c;对这些复杂的数学公式拟合时&#xff0c;采用常见的拟合方法往往会失败&#xff0c;或者得不到足够精确的结果。本文以笔者多…

AVL树:高度平衡的二叉搜索树

AVL树 AVL树和BST树的联系   答&#xff1a;BST树&#xff08;二叉排序树&#xff09;当节点的数据key有序时是一棵单支树&#xff0c;查找时效率直接降低到O(N)而不是树高&#xff0c;为了使树尽量两边均匀&#xff0c;设计出了AVL树&#xff0c;AVL树的左右高度差不超过1。…

sql语句练习题1

1、选择部门30中的所有员工&#xff1b; 要注意到查的是所有员工 代码如下&#xff1a; mysql> select * from emp where deptno 30;2、列出所有办事员(CLERK)的姓名&#xff0c;编号和部门编号&#xff1b; 注意的是要查的是姓名&#xff0c;编号和部门编号 范围限定的是…

并发编程的原子性 != 事务ACID的原子性

△Hollis, 一个对Coding有着独特追求的人△这是Hollis的第 412 篇原创分享作者 l Hollis来源 l Hollis&#xff08;ID&#xff1a;hollischuang&#xff09;关于原子性&#xff0c;很多人在多个地方都听说过&#xff0c;大家也都背的很熟悉。在事务的ACID中&#xff0c;有原子性…

儒家思想和道家思想的三个主要差异

孔子、孟子、老子、庄子&#xff0c;这四位古代思想家被称为“中国四哲”&#xff0c;他们分别代表了儒家和道家思想。这两大思想流派&#xff0c;是数千年来中国人智慧的结晶和文化的瑰宝。01先秦儒家思想的发展&#xff0c;经过了三个阶段&#xff0c;第一阶段是孔子&#xf…

CHAPTER 7 Ansible playbook(四)

ansible-playbook7.1 roles&#xff08;角色&#xff09;7.1.1 Ansible Roles 介绍7.1.2 Roles结构7.1.3 存储和查找角色7.1.4 制作一个Role7.1.5 使用角色7.1.5.1 经典方法7.1.5.2 import_role7.1.6 如何使用Galaxy7.1 roles&#xff08;角色&#xff09; 7.1.1 Ansible Role…

windows docker安装prometheus和grafana

文章目录docker安装prometheusdocker安装grafanawindows安装windows_exporterprometheus配置新增windows_exporter的job,配置grafana导入windows模板即可出现酷炫大屏出现酷炫画面完成docker安装prometheus 拉取镜像,在D盘下创建prometheus.yml配置文件,映射到docker里面d:/se…

【pandas】教程:8-如何组合多个表格的数据

Pandas 组合多个表格的数据 本节使用的数据为 data/air_quality_no2_long.csv&#xff0c;链接为 pandas案例和教程所使用的数据-机器学习文档类资源-CSDN文库 导入数据 NO2NO_2NO2​ import pandas as pd air_quality_no2 pd.read_csv("data/air_quality_no2_long.cs…

二、python编程进阶02:模块和包

目录 1. python中的模块是什么 2. 导入模块: 学习import语句 2.1 import语句介绍 2.2 import导入模块的语法 2.3 导入自己的模块 2.4 导入数字开头或者带空格的模块 3. 编写自定义模块 3.1 给自定义模块编写测试代码 3.2 给自定义模块模块编写说明文档 4. 模块的搜索…

1215. 小朋友排队(树状数组应用 -- 逆序对个数)

题目如下&#xff1a; 思路 or 题解 我们可以得出交换的次数 > 逆序对个数 kkk 我们可以发现 所有 位置 左边大于它的个数 右边小于它的个数和 kik_iki​ 等于 k∗2k*2k∗2 我们可以简单证明出(感觉出)&#xff1a;答案就是 ∑1n(1ki)∗ki2\sum^n_1 \frac{(1 k_i) * k_i}…