利用wait/notify模拟消息队列

一个简易版的消息队列模型:

利用wait/notify模拟消息队列_第1张图片

这里一共有四种角色:

  • queue:消息队列;
  • Publisher:消息发布者,可以向任何一个队列发布消息;
  • Listener:消息监听者,一个 Listener 同时只能监听一个 queue 的消息;但一个 queue 可同时被多个 Listener 监听;
  • MqServer:消息队列服务器,负责管理所有的队列。

首先定义 MqServer,queue 都在 MqServer 里面:

package per.lvjc.concurrent.waitnotify;

import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;

public class MqServer {

    private final Map<String, Queue<String>> queues;

    public MqServer() {
        this.queues = new HashMap<>();
    }

    /**
     * 创建队列
     * @param name 队列名称
     */
    public synchronized void createQueue(String name) {
        queues.put(name, new ArrayDeque<>());
        System.out.println("成功创建队列: " + name);
    }

    /**
     * 根据队列名称获取队列
     * @param name 队列名称
     * @return
     */
    public synchronized Queue<String> getQueueByName(String name) {
        Queue<String> queue = queues.get(name);
        if (queue == null) {
            System.out.println("队列 [" + name + "] 不存在!");
            return null;
        }
        return queue;
    }
}

然后定义 Listener:

package per.lvjc.concurrent.waitnotify;

import java.util.Queue;

public class Listener {

    private final MqServer server;
    private final String name;
    private Thread thread;

    public Listener(MqServer server, String listenerName) {
        this.server = server;
        this.name = listenerName;
    }

    /**
     * 开启一个线程,监听指定的队列
     * @param queueName
     */
    public void listen(String queueName) {
        //一些非空检查,可忽略
        if (thread != null) {
            System.out.println(name + " 已处于监听状态,无法重复监听");
            return;
        }
        final Queue<String> queue = server.getQueueByName(queueName);
        if (queue == null) {
            return;
        }
        //真正的监听操作在这里
        thread = new Thread(() -> {
            System.out.println(name + " 开始监听 " + queueName);
            synchronized (queue) {
                String message;
                //借用 interrupt 标识位作监听状态判断条件
                while (!Thread.currentThread().isInterrupted()) {
                    message = queue.poll();
                    //取到消息,则处理消息
                    if (message != null) {
                        System.out.println("[" + name + "]" + " 在队列 [" + queueName + "] 上监听到消息 [" + message + "]");
                        continue;
                    }
                    //没有消息,则等待其它线程发布消息
                    try {
                        queue.wait();
                    } catch (InterruptedException e) {
                        //被打断则终止监听
                        break;
                    }
                }
            }
            System.out.println(name + " 停止监听 " + queueName);
        });
        //在后台监听
        thread.setDaemon(true);
        //启动监听
        thread.start();
    }

    /**
     * 关闭 Listener
     */
    public void close() {
        if (thread == null) {
            return;
        }
        System.out.println(name + " 被关闭");
        //将 interrupt 标识位置位被打断状态,使 Listener 线程跳出 while 循环
        thread.interrupt();
        thread = null;
    }
}

再定义 Publisher

package per.lvjc.concurrent.waitnotify;

import java.util.Queue;

public class Publisher {

    private MqServer server;

    public Publisher(MqServer server) {
        this.server = server;
    }

    /**
     * 发布消息到指定队列
     * @param queueName
     * @param message
     */
    public void publishMessage(String queueName, String message) {
        final Queue<String> queue = server.getQueueByName(queueName);
        if (queue == null) {
            return;
        }
        synchronized (queue) {
            //向队列发布一条消息
            queue.offer(message);
            System.out.println("成功发布消息 [" + message + "] 到队列 [" + queueName + "]");
            //唤醒在此对象上等待的所有线程,随便唤醒哪个线程都一样,没必要 notifyAll
            queue.notify();
        }
    }
}

到此,所有的角色都已经有了。

再写一个 Main 方法跑一下:

package per.lvjc.concurrent.waitnotify;

import java.util.Scanner;

public class Main {

    private static final String QUEUE_1 = "queue1";
    private static final String QUEUE_2 = "queue2";

    public static void main(String[] args) throws InterruptedException {
        //启动 MqServer,并创建两个队列
        MqServer server = new MqServer();
        server.createQueue(QUEUE_1);
        server.createQueue(QUEUE_2);
        //创建 Listener 1,监听 queue 1
        Listener listener1 = new Listener(server, "listener1");
        listener1.listen(QUEUE_1);
        //创建 Listener 2,监听 queue2
        Listener listener2 = new Listener(server, "listener2");
        listener2.listen(QUEUE_2);
        //创建 Listener 3,监听 queue2
        Listener listener3 = new Listener(server, "listener3");
        listener3.listen(QUEUE_2);
        //创建 Publisher
        Publisher publisher = new Publisher(server);

        //在主线程做 发送消息、关闭 Listener 等操作
        Scanner scanner = new Scanner(System.in);
        while (true) {
            String input = scanner.nextLine();
            if ("0".equals(input)) {
                System.out.println("退出程序");
                //因为 Listener 都是守护线程,主线程结束,程序应该会终止
                break;
            }
            if ("1".equals(input)) {
                listener1.close();
                continue;
            }
            if ("2".equals(input)) {
                listener2.close();
                continue;
            }
            if ("3".equals(input)) {
                listener3.close();
                continue;
            }
            String[] strs = input.split(":");
            if (strs.length != 2) {
                System.out.println("错误输入");
                continue;
            }
            String queueName = strs[0];
            String message = strs[1];
            publisher.publishMessage(queueName, message);
        }
    }
}

执行结果:

成功创建队列: queue1
成功创建队列: queue2
listener1 开始监听 queue1
listener2 开始监听 queue2
listener3 开始监听 queue2
queue1:aaa
成功发布消息 [aaa] 到队列 [queue1]
[listener1] 在队列 [queue1] 上监听到消息 [aaa]
queue2:bbb
成功发布消息 [bbb] 到队列 [queue2]
[listener2] 在队列 [queue2] 上监听到消息 [bbb]
queue3:ccc
队列 [queue3] 不存在!
queue2:ccc
成功发布消息 [ccc] 到队列 [queue2]
[listener3] 在队列 [queue2] 上监听到消息 [ccc]
queue2:ddd
成功发布消息 [ddd] 到队列 [queue2]
[listener2] 在队列 [queue2] 上监听到消息 [ddd]
queue2:fff
成功发布消息 [fff] 到队列 [queue2]
[listener3] 在队列 [queue2] 上监听到消息 [fff]
2
listener2 被关闭
listener2 停止监听 queue2
queue2:ggg
成功发布消息 [ggg] 到队列 [queue2]
[listener3] 在队列 [queue2] 上监听到消息 [ggg]
queue2:hhh
成功发布消息 [hhh] 到队列 [queue2]
[listener3] 在队列 [queue2] 上监听到消息 [hhh]
1
listener1 被关闭
listener1 停止监听 queue1
queue1:xxx
成功发布消息 [xxx] 到队列 [queue1]
0
退出程序

Process finished with exit code 0

这里会看到一个有意思的现象,往 queue2 发布消息时,queue2 上的监听者 listener2 和 listener3 总是轮流被唤醒,可以推断这里的 JVM(Ubuntu 16 系统) 可能把 wait set 实现成了先进先出。

然后在 listener2 被关闭之后,消息就只会被 listener3 消费。

listener1 被关闭后,发布到 queue1 的消息就堆积在队列里没人消费了。

输入 0 退出循环可以终止程序,因为 Listener 开启监听线程的时候做了一个操作:

thread.setDaemon(true);

你可能感兴趣的:(并发,多线程,java,wait,notify,消息队列)