Guarded Suspension(保护性暂挂)模式

思维导图

思维导图

Guarded Suspension 模式简介

Guarded Suspension 模式的核心是一个受保护方法(Guarded Method)。该方法在执行其所需要真正执行的操作时需要满足特定的条件(Predicate,称为保护条件)。当条件状态不满足时,执行受保护方法的线程会被挂起并进入等待状态(WAITING)状态,直到条件满足,该线程才会继续运行。此时,受保护方法才会真正执行其所要执行的操作,所要执行的操作又称为:目标动作。

GuardedObject 角色是一个持有被守护的方法(guardedMethod)的类,当线程执行 guardedMethod 方法时,若守护条件成功,则可以立即执行操作;不成立时,就要进行等待。除了 guardedMethod方法,GuardedObject 角色还应该有持有其他改变实例状态的方法(stateChangingMethod)。stateChangingMethod 通过 notify/notifyAll 方法来实现,guardedMethod 通过 while 语句和 wait 方法来实现。

Guarded Suspension 模式的特点:

  • 存在循环
  • 存在条件检查
  • 因为某种原因而等待

核心代码:

while (守护条件) {
    try {
        wait();
    } catch(InterruptedException e) {
        e.printStackTrace();
    }
}

// 执行操作
todo(); 

应用场景

服务器请求多,但服务器程序不能丢弃任何一个客户的请求。当大量客户请求到来,而我们不能放弃任何一个请求,必须让客户请求排队,由服务器一个一个去处理。

Guarded Suspension模式:既可以最大限度的保护服务器在大量用户访问时不挂掉,同时也能一个一个处理用户的请求 ,保证所有客户端请求均不丢失。

实例代码

Request封装客户端请求。RequestQueue表示客户端请求队列(充当中间缓存,存放未处理的请求),由Client端和Server端共同维护。Client端负责不断发起请求,并将请求对象放入队列中。Server端则根据自身状态,在有能力的情况下,从RequestQueue队列中提取请求对象并进行处理。

时序图,待完善。

名字 说明
Request 表示一个请求的类
RequestQueue 依次存放请求的类
ClientThread 发送请求的类
ServerThrad 接受请求的类
Main 测试类
package com.learn.concurrent.design;

/**
 * @Auther: fc.w
 * @Date: 2021/1/11 22:37
 * @Description:
 */
public class Request {

    private String name;

    public Request(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return "Request{" +
                "name='" + name + '\'' +
                '}';
    }
}
package com.learn.concurrent.design;

import java.util.LinkedList;
import java.util.Queue;

/**
 * 此类用于依次存放请求,RequestQueue 通过 putRequest 放入 Request 实例,并按顺序使用 getRequest 取出 Request 实例。
 * 这种结构通常称为队列(queue)或者 FIFO(First In First Out ,先进先出)
 * @Auther: fc.w
 * @Date: 2021/1/11 22:38
 * @Description:
 */
public class RequestQueue {

    private final Queue<Request> queue = new LinkedList<>();

    /**
     * 取出最先放在RequestQueue的中的一个请求,作为返回值,如果一个请求也没有就一直等待,直到其他线程执行putRequest.
     * @return
     */
    public synchronized Request getRequest() {
        while (queue.peek() == null) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        return queue.remove();
    }

    /**
     * 添加一个请求
     * @param request
     */
    public synchronized void putRequest(Request request) {
        queue.offer(request);
        notifyAll();
    }

}
package com.learn.concurrent.design;

import java.util.Random;

/**
 * 表示发送请求的线程。ClientThread持有RequestQueue的实例,并连续调用该实例的putRequest,放入请求。
 * @Auther: fc.w
 * @Date: 2021/1/11 22:43
 * @Description:
 */
public class ClientThread extends Thread {

    private final Random random;
    private final RequestQueue requestQueue;

    public ClientThread(RequestQueue requestQueue, String name, long seed) {
        super(name);
        this.requestQueue = requestQueue;
        this.random = new Random(seed);
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            Request request  = new Request("No." + i);
            System.out.println(Thread.currentThread().getName() + " requests " + request);
            requestQueue.putRequest(request);
            try {
                // 为了错开发送请求的执行点,使用java.util.Random类随机生成0-1000之间的数
                Thread.sleep(random.nextInt(1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
package com.learn.concurrent.design;

import java.util.Random;

/**
 * 用于表示接收请求的线程,该类也持有RuquestQueue的实例,ServerThread使用该实例的getRequst方法来接受请求。
 * @Auther: fc.w
 * @Date: 2021/1/11 22:48
 * @Description:
 */
public class ServerThread extends Thread{

    private final Random random;
    private final RequestQueue requestQueue;

    public ServerThread(RequestQueue requestQueue,String name ,long seed){
        super(name);
        this.requestQueue = requestQueue;
        this.random = new Random(seed);
    }

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            Request request = requestQueue.getRequest();
            System.out.println(Thread.currentThread().getName() + " handles " + request);
            try {
                Thread.sleep(random.nextInt(1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
package com.learn.concurrent.design;

/**
 * @Auther: fc.w
 * @Date: 2021/1/11 22:51
 * @Description:
 */
public class Main {

    public static void main(String[] args) {
        RequestQueue requestQueue = new RequestQueue();
        new ClientThread(requestQueue,"client-Alice",3141592L).start();
        new ServerThread(requestQueue,"server-Bobby",6535897L).start();
    }

}

运行结果:

client-Alice requests Request{name='No.0'}
server-Bobby handles Request{name='No.0'}
client-Alice requests Request{name='No.1'}
client-Alice requests Request{name='No.2'}
server-Bobby handles Request{name='No.1'}
server-Bobby handles Request{name='No.2'}
client-Alice requests Request{name='No.3'}
server-Bobby handles Request{name='No.3'}
client-Alice requests Request{name='No.4'}
server-Bobby handles Request{name='No.4'}

Java 标准库实例

从JDK 1.5 开始提供的阻塞队列类LinkedBlockingQueue就是使用 Guarded Suspension 模式。 该类的 take 方法用于从队列中取出一个元素。 如果take方法被调用时队列为空,则当前线程会被阻塞; 直到队列不为空时,该方法才返回一个出队列的元素,只不过LinkedBlockingQueue在实现Guarded Suspension 模式时直接使用了Condition。

LinkedBlockingQueue 源码

实现时易出错的重要技术细节

  • 内存可见性和锁泄漏(Lock Leak)
    为了保证保护条件中涉及的变量内存可见性,引入ReentrantLock锁。使用该锁时需要注意临界区中的代码无论执行正常与否,进入临界区前获得的锁实例都应该被释放。否锁就会出现锁泄漏现象;锁对象被某个线程获得,但永远不会被该线程释放,导致其他线程无法获得该锁。为了避免锁泄漏,使用ReentrantLock 的临界区代码需要按照以下格式来编写:
    public E take() throws InterruptedException {
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();
        try {
      
        } finally {
            takeLock.unlock();
        }
    }
  • 线程被过早的唤醒
    为了应对被挂起的线程可能被“过早” 地唤醒的情景,将 take 方法和保护条件的检测对 Contion 实例的 await 方法的调用总是放到一个 while 循环而非 if 语句中。
    public E take() throws InterruptedException {
        takeLock.lockInterruptibly();
        try {
            while (保护条件不成立) {
                notEmpty.await();
            }

            // 执行目标动作
            // TODO
        } finally {
            takeLock.unlock();
        }

参考资料
https://blog.csdn.net/weixin_42245930/article/details/88761176
Java多线程并发编程-设计模式篇

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容