# 《并发设计模式》第13章-保护性暂挂模式-保护性暂挂模式在JDK中的应用

作者:冰河
星球:http://m6z.cn/6aeFbs (opens new window)
博客:https://binghe.gitcode.host (opens new window)
文章汇总:https://binghe.gitcode.host/md/all/all.html (opens new window)
源码获取地址:https://t.zsxq.com/0dhvFs5oR (opens new window)

沉淀,成长,突破,帮助他人,成就自我。

  • 本章难度:★★☆☆☆
  • 本章重点:掌握保护性暂挂模式在BlockingQueue阻塞队列中的应用,理解保护性暂挂模式的核心原理,能够结合自身实际项目将保护性暂挂模式灵活应用到自身实际项目中。

大家好,我是冰河~~

对于Java程序员来说,一种提升编程能力非常好的方式就是学习JDK中的源码,学习JDK中提供的一些实用工具类的源码不仅能够学习到高并发的编程技巧,还能学习到从源码层面如何避免线程安全问题,并且JDK中的工具类还会使用各种实用的设计模式,有助于理解各种设计模式的使用场景和在源码中的落地方案。

# 一、故事背景

小菜学完保护性暂挂模式之后,在老王的指导下,基于保护性暂挂模式对交易系统进行了优化。但是,小菜并没有止步于此,在老王的耐心讲解下,小菜学习了如何基于保护性暂挂模式实现报警监控系统。

这天,小菜心里非常高兴,因为不仅仅是学习了保护性暂挂模式,也学习了保护性暂挂模式在实际项目场景中的应用。小菜下班回到家后,在思考一个问题:像保护性暂挂模式这种非常优秀的并发设计模式,JDK中的源码应该使用了这个设计模式吧?于是,小菜便打开开发环境,认真看起了JDK中的源码。

时间过的真快,不知不觉已到深夜,尽管小菜一直在JDK的源码中寻找自己心中问题的答案,但在看JDK源码的过程中,出现了问题:无论小菜怎么看,怎么思考,都无法清晰的找到使用了保护性暂挂模式的类源码。

这不禁让小菜新生疑惑:难道JDK的源码中没有使用保护性暂挂模式吗?明天还是问问老大吧!于是,小菜便关上了电脑,去洗漱准备睡觉。。。

# 二、探究真理

第二天,小菜早早的带着心里的疑问来到了公司,坐到工位上,打开开发环境继续寻找JDK中哪些类使用了保护性暂挂模式。无意中听到了老王的声音。于是,小菜真起身:“老大,早啊”。

“早,小菜”。

“老大,我有个问题,就是像保护性暂挂模式这种并发设计模式,JDK中的源码应该使用了这种设计模式吧,但是我没有在JDK中找到使用了保护性暂挂模式的源码,老大,是不是JDK的源码中没有使用这种设计模式呢?”。

老王笑了笑,说:“JDK中使用了保护性暂挂模式,只是你没找到,来,我给你讲讲”。

“好的,谢谢老大”。

“我们还是去会议室吧”。

于是,二人一起走进了会议室。。。

# 三、实用案例

“为了让你更好的理解JDK中使用了保护性暂挂模式的源码,在讲解JDK源码之前,我再给你讲一个非常实用的案例”,老王说道。于是,老王便给小菜讲起了实用的案例。

在开发服务端程序时,需要接收多个客户端发送过来的请求,为了避免丢失请求,在服务端程序中,需要维护一个请求的缓冲区,客户端发送过来的请求首先会缓冲到请求的缓冲区。随后,服务端程序会从缓冲区中取出请求信息进行处理。如果缓冲区中没有请求信息,则服务端程序阻塞等待,直到缓冲区中缓冲了新的请求信息,则阻塞等待的线程会被唤醒,继续从缓冲区中获取请求信息进行处理。

说着,老王便写了一段RequestCacheBuffer类的代码来表示这段逻辑,RequestCacheBuffer类的源码详见:concurrent-design-patterns-guarded-suspension工程下的io.binghe.concurrent.design.guarded.suspension.buffer.RequestCacheBuffer。

public class RequestCacheBuffer {

    private static final int LIMIT = 1024;
    private final Queue<Request> queue = new ArrayBlockingQueue<>(LIMIT);
    private final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();

    public Request get() {
        Request result = null;
        lock.lock();
        try {
            while (queue.isEmpty()) {
                notEmpty.await();
            }
            result = queue.poll();
            notFull.signalAll();
        } catch (InterruptedException e) {
            notFull.signalAll();
        } finally {
            lock.unlock();
        }
        return result;
    }

    public void put(Request request) {
        lock.lock();
        try {
            while (queue.size() >= LIMIT) {
                notFull.await();
            }
            queue.offer(request);
            notEmpty.signalAll();
        } catch (InterruptedException e) {
            notEmpty.signalAll();
        } finally {
            lock.unlock();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

从上述代码可以看出,使用ArrayBlockingQueue缓冲请求时,会有如下业务逻辑:

(1)当从缓冲队列中获取请求信息时,如果缓冲队列为空,则阻塞获取请求信息的线程。

(2)当从缓冲队列中获取请求信息时,如果缓冲队列不为空,则从缓冲队列中获取请求信息,并唤醒向缓冲队列中添加请求信息的线程。

(3)当向缓冲队列中添加请求信息时,如果缓冲队列已满,则阻塞添加请求信息的线程。

(4)当向缓冲队列中添加请求信息时,如果缓冲队列未满,则向缓冲队列中添加请求信息,并唤醒从缓冲队列中获取请求信息的线程。

也就是说,当队列已满进行入队列操作,或者当队列为空进行出队列操作时,都会阻塞线程。

说的再直白些,就是当一个线程对一个已满的队列进行入队操作时,它将会被阻塞,除非有另一个线程做了出队操作,同理,当一个线程对一个已空的队列进行出队操作时,它将会被阻塞,除非有另一个线程进行了入队操作。

# 查看全文

加入冰河技术 (opens new window)知识星球,解锁完整技术文章与完整代码