多线程设计模式案例:带代码的全景梳理(单例 / 阻塞队列 / 定时器 / 线程池)

0. waitsleep(后面两个案例会用到)

来看这段面试总结:wait 必须配合 synchronized 使用,sleep 不需要;并且 waitObject 的方法,sleepThread 的静态方法。

synchronized (locker) {
    locker.wait();        // Object.wait:需要拿到 locker 这把锁
}
Thread.sleep(1000);        // Thread.sleep:不要求持有任何锁

解释一下:wait() 会让线程进入等待队列,并且释放当前锁sleep() 只是让线程暂停一段时间,不负责线程间协作。而且对于sleep来说,他会“抱着锁睡觉” 这个差异会直接决定“阻塞队列”和“定时器”的写法。


8.1 单例模式:保证“全局只有一个实例”

来看这里对单例的定义:单例模式要保证某个类在程序中只存在唯一一份实例,比如 JDBC 的 DataSource 就常常只需要一个。

class Singleton {
    private static Singleton instance = new Singleton(); // 唯一实例
    private Singleton() {}                               // 禁止外部 new
    public static Singleton getInstance() {
        return instance;
    }
}

解释一下:构造器私有化让外部无法随意 new;唯一实例放在类的静态字段里,统一通过 getInstance() 返回,这就把“唯一入口”钉死了。


8.1.1 饿汉模式:类加载就创建(简单、天然线程安全)

来看这段“饿汉”:类加载时直接把实例建出来。

class Singleton {
    private static Singleton instance = new Singleton(); // 类加载即创建
    private Singleton() {}
    public static Singleton getInstance() { return instance; }
}

解释一下:优点是实现极其简单,并且类初始化过程本身就能保证线程安全;缺点是如果实例很重、但一直用不上,会有资源浪费。


8.1.2 懒汉模式(单线程版):第一次用到才创建(但多线程会翻车)

来看这段“懒汉单线程版”:第一次调用 getInstance() 时发现 instance == null 才创建。

class Singleton {
    private static Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton(); // 第一次用到才创建
        }
        return instance;
    }
}

解释一下:在单线程里没问题;但多线程同时进来都看到 instance == null 时,可能会各自 new 一次,直接破坏“唯一性”。线程安全问题主要发生在首次创建实例这一刻。


8.1.3 懒汉模式(加锁版):用 synchronized 把创建过程互斥掉

来看这段修复:给 getInstance()synchronized,保证同一时间只有一个线程能创建实例。

class Singleton {
    private static Singleton instance = null;
    private Singleton() {}
    public synchronized static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton(); // 互斥区:只会创建一次
        }
        return instance;
    }
}

解释一下:正确性稳了,但每次调用都要竞争锁,实例创建完成后仍然要付出加锁成本。


8.1.4 懒汉模式(改进版):双重检查 + volatile(兼顾性能与正确性)

来看这段“改进版”:外层 if 先挡住大多数已经初始化的情况,只有第一次才会进入锁;并且给 instance 加上 volatile 来处理可见性问题。

class Singleton {
    private static volatile Singleton instance = null;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {                 // 第一次检查:避免无谓加锁
            synchronized (Singleton.class) {
                if (instance == null) {         // 第二次检查:保证只创建一次
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

解释一下:加锁/解锁开销高,而懒汉的不安全只发生在首次创建;外层 if 就是为了减少后续的锁竞争;volatile 用来避免读取到不一致的 instance 值(可见性偏差)。


8.2 阻塞队列:线程安全 + “满/空自动阻塞”

来看这里的定义:阻塞队列仍然遵守 FIFO,但它是线程安全结构;队列满继续入队会阻塞,队列空继续出队也会阻塞。

BlockingQueue<String> q = new LinkedBlockingQueue<>();
q.put("abc");           // 队列满会阻塞
String x = q.take();    // 队列空会阻塞

解释一下:put/take 这类 API 自带阻塞语义;而 offer/poll/peek 这一类不阻塞(更多用于“试一下,不行就算了”的场景)。


8.2.1 生产者-消费者模型:用队列解耦 + 削峰填谷

来看这段模型描述:生产者与消费者不直接通信,而是通过阻塞队列通信;生产者生产完直接扔队列,消费者从队列取,不需要互相等待对方。

BlockingQueue<Integer> q = new LinkedBlockingQueue<>();

Thread producer = new Thread(() -> {
    try {
        while (true) q.put((int)(Math.random() * 1000));
    } catch (InterruptedException ignored) {}
}, "生产者");

Thread consumer = new Thread(() -> {
    try {
        while (true) System.out.println("消费: " + q.take());
    } catch (InterruptedException ignored) {}
}, "消费者");

producer.start();
consumer.start();

解释一下:这类结构最常见的两个收益——缓冲区平衡两端处理能力(削峰填谷),以及解耦。这里举了“秒杀请求先入队,消费者慢慢处理,避免服务被瞬时洪峰冲垮”的例子。


8.2.2 手写阻塞队列:循环数组 + synchronized + while(wait) + notifyAll

来看这里的实现要点:用循环队列存储;用 synchronized 加锁;put 遇到满就 wait(而且要在 while 里等);take 遇到空也 wait;状态变化后用 notifyAll 唤醒等待线程。

class BlockingQueue {
    private final int[] items = new int[1000];
    private int size = 0, head = 0, tail = 0;

    public void put(int value) throws InterruptedException {
        synchronized (this) {
            while (size == items.length) { // 满了就等
                wait();
            }
            items[tail] = value;
            tail = (tail + 1) % items.length;
            size++;
            notifyAll(); // 唤醒可能在等“非空”的线程
        }
    }

    public int take() throws InterruptedException {
        synchronized (this) {
            while (size == 0) {            // 空了就等
                wait();
            }
            int ret = items[head];
            head = (head + 1) % items.length;
            size--;
            notifyAll(); // 唤醒可能在等“非满”的线程
            return ret;
        }
    }
}

解释一下:while 不用 if 的原因也写得很明确——线程被唤醒后不一定立刻抢到锁;等真正抢到锁时,队列状态可能又变回“满/空”,所以需要循环验证条件,条件不满足就继续等。


8.3 定时器:把“到点执行”变成一个组件

来看这里对定时器的定位:像“闹钟”一样,到设定时间就执行指定代码;常见场景包括网络超时重连、Map 的 key 过期自动删除等。

Timer timer = new Timer();
timer.schedule(new TimerTask() {
    @Override
    public void run() {
        System.out.println("hello");
    }
}, 3000);

解释一下:schedule 两个参数——任务代码 + 延迟多久执行(毫秒)。


8.3.1 手写定时器:优先队列 + worker 线程扫队首 + wait(剩余时间)

来看这里的“构成”:核心是一个优先级队列(明确提示不要用 PriorityBlockingQueue,容易死锁)、队列里每个元素是带执行时间戳的 Task,队首永远是最早要执行的任务;再配一个 worker 线程不停扫描队首,时间没到就等待,到了就执行。

class MyTask implements Comparable<MyTask> {
    Runnable runnable;
    long time; // 绝对时间戳

    MyTask(Runnable runnable, long delay) {
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + delay;
    }

    @Override
    public int compareTo(MyTask o) {
        return Long.compare(this.time, o.time); // 时间小的优先
    }
}

解释一下:用“绝对时间戳”而不是“剩余延迟”非常关键——这样每次比较只看 time 大小即可,优先队列 peek() 永远拿到最早要执行的任务。


class MyTimer {
    private final PriorityQueue<MyTask> queue = new PriorityQueue<>();
    private final Object locker = new Object();

    public void schedule(Runnable command, long after) {
        synchronized (locker) {
            queue.offer(new MyTask(command, after));
            locker.notify(); // 新任务来了,叫醒 worker 重新算等待时间
        }
    }

    public MyTimer() {
        Thread t = new Thread(() -> {
            while (true) {
                try {
                    synchronized (locker) {
                        while (queue.isEmpty()) locker.wait();

                        MyTask task = queue.peek();
                        long now = System.currentTimeMillis();

                        if (now >= task.time) {
                            queue.poll();
                            task.runnable.run();
                        } else {
                            locker.wait(task.time - now); // 睡到差不多该执行
                        }
                    }
                } catch (InterruptedException ignored) {}
            }
        });
        t.start();
    }
}

解释一下:worker 线程做的事情非常“机械但正确”——队列空就无限期 wait();队首任务没到点就 wait(差值);到点就 poll() 并执行。这个结构把 CPU 空转降到最低,同时保证始终按最早触发顺序执行。


8.4 线程池:复用线程,限制并发上限,队列缓冲任务

来看这里的类比:每来一个任务就临时招人送快递再解雇,成本很高;于是固定雇几个人,忙不过来就把任务记在本子上排队,这就是线程池模式。它的最大好处是减少线程启动/销毁的损耗。

ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(() -> System.out.println("hello"));

解释一下:Executors.newFixedThreadPool(10) 创建固定 10 线程的线程池,通过 submit 把任务提交进去执行。


8.4.1 Executors 的几种常见线程池

来看这里的四种:固定线程数、动态增长、单线程、以及带延迟/定期执行能力的 scheduled(相当于 Timer 的进阶版)。

Executors.newFixedThreadPool(10);
Executors.newCachedThreadPool();
Executors.newSingleThreadExecutor();
Executors.newScheduledThreadPool(4);

解释一下:这些工厂方法本质上都是对 ThreadPoolExecutor 的封装,提供“开箱即用”的默认参数组合。


8.4.2 ThreadPoolExecutor:把线程池行为参数化(含拒绝策略)

来看这里的参数清单:corePoolSize / maximumPoolSize / keepAliveTime / workQueue / threadFactory / RejectedExecutionHandler,以及四种拒绝策略(直接抛异常、调用者执行、丢最老、丢最新)。

ThreadPoolExecutor pool = new ThreadPoolExecutor(
        4, 8,
        60, TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(1000),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.AbortPolicy()
);

解释一下:核心线程相当于“正式员工”,最大线程数包含“临时工”,临时工空闲超过 keepAliveTime 会被回收;队列负责缓冲任务;拒绝策略负责在系统过载时给出“怎么处理多出来任务”的明确答案。


8.4.3 手写固定线程数线程池:BlockingQueue + N 个 worker 不停 take().run()

来看这里的核心套路:submit 把任务放入阻塞队列;每个 worker 线程做的事就是不断从队列取任务并执行。

class MyThreadPool {
    private final BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();

    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }

    public MyThreadPool(int n) {
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(() -> {
                while (true) {
                    try {
                        Runnable r = queue.take(); // 没任务就阻塞
                        r.run();                   // 有任务就执行
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            t.start();
        }
    }
}

解释一下:阻塞队列把“提交任务”和“执行任务”彻底解耦——提交方只负责 put;worker 只负责 take + run;并发上限由 worker 数量天然限定(n 个线程同时最多跑 n 个任务)。


总结:四个案例其实是同一个并发约束力

来看最后的统一视角:单例用锁/可见性约束“唯一性”;阻塞队列用条件等待约束“生产与消费速率差”;定时器用“时间排序 + 超时等待”约束“到点触发”;线程池用“固定 worker + 队列缓冲”约束“资源上限与吞吐”。这四类积木拼起来,就能覆盖大量真实业务中的并发问题。

// 多线程里最常用的四件套就是:
// 1) Singleton:全局唯一
// 2) BlockingQueue:线程间通信 + 削峰
// 3) Timer/MyTimer:延迟触发
// 4) ThreadPool:线程复用 + 任务队列

本质是把“不确定的线程调度”变成“确定的协议”(锁、队列、时间、worker)


Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐