首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【JavaSE】线程同步wait/notify && 单例模式 && 阻塞队列

【JavaSE】线程同步wait/notify && 单例模式 && 阻塞队列

原创
作者头像
lirendada
发布2026-04-06 09:01:06
发布2026-04-06 09:01:06
1300
举报
文章被收录于专栏:JavaJava

线程同步

一、wait && notify

下面的方法,都是 Object 类实现的,所以所有类都存在这些线程同步方法!

方法

描述

说明

void wait()

无限期等待,直到被唤醒

必须在 synchronized 中使用

void wait(long timeout)

最多等待 timeout 毫秒

超时未被唤醒也会继续执行

void notify()

无参数,随机唤醒一个等待线程

只能唤醒一个,不能控制具体唤醒哪个线程

void notifyAll()

无参数,唤醒所有等待该对象锁的线程

所有被唤醒线程会竞争锁,推荐用于并发协作

wait 做的事情如下所示:

  1. 使当前执行代码的线程进行等待,腾出 CPU 使用权,释放当前的锁
  2. 满足一定条件时被唤醒,重新尝试获取这个锁(不一定能拿到锁)

一般 wait() 的使用如下所示:

代码语言:javascript
复制
synchronized (locker) {
    // 使用 while 防止 “虚假唤醒”
    while (!条件成立) {
        locker.wait(); // 🟢 这段逻辑是实现同步的关键一步,应该放在逻辑开头
    }
    // 当条件满足,才继续做事
}

注意事项:

  • wait()notify() 必须在 synchronized 内使用,否则会抛出 IllegalMonitorStateException
  • 一般放在条件不满足的位置(比如队列满或空)
  • while 而非 if 来包裹 wait(),防止虚假唤醒

二、waitsleep 的区别

sleep()

wait()

所属类

Thread 类的静态方法

Object 类的方法

是否需要锁(synchronized)

✅ 需要,且必须在同步代码块中使用

是否释放锁

✅ 释放锁,等待期间不占有对象锁

是否释放CPU

唤醒方式

超时唤醒

被 notify()/notifyAll() 或超时唤醒

是否可中断

✅ 抛出 InterruptedException

✅ 抛出 InterruptedException

是否用于线程间通信

✅ 能,用于线程间协调与同步

主要用途

暂停线程(模拟延迟、限流等)

条件不满足时挂起线程,等待其他线程通知继续执行

单例模式

单例模式:保证一个类在 JVM 中只存在一个实例,并提供一个全局访问点

一、饿汉模式

饿汉模式属于 "急切加载" 的方式,即在类加载时就立即创建单例对象,而不管是否会被使用

  • 优点:
    • 实现简单,代码简洁。
    • 线程安全,不需要额外的同步机制。
  • 缺点:
    • 若单例对象的成员数据过多,那么会导致整个程序启动变慢
    • 如果有多个单例类是相互依赖并且有初始化依赖顺序的,那么饿汉模式在创建的时候是控制不了这种依赖顺序
代码语言:javascript
复制
public class StarvingSingleton {
    // 静态变量,存储单例对象,使用final修饰
    private static final StarvingSingleton instance = new StarvingSingleton();

    // 私有化构造方法,防止外部通过new创建对象
    private StarvingSingleton() {
    }

    // 提供获取单例成员接口
    public static StarvingSingleton getInstance() {
        return instance;
    }
}

💥注意事项:

  • instance 设为 final,是为了防止其它代码意外修改单例对象的引用,从而避免创建多个实例的情况。

二、懒汉模式

懒汉模式的核心思想是 "延迟加载",即只有在第一次使用单例对象时才创建它。这种方式可以节省资源,因为单例对象只有在真正需要时才会被创建!

  • 优点:
    • 因为对象在主程序之后才会创建,所以程序启动比饿汉模式要快
    • 只有在需要时才创建单例对象,避免了不必要的资源占用
    • 可以控制不同的单例类的依赖关系以及控制依赖顺序
  • 缺点:
    • 涉及到多线程安全问题,需要加锁,实现更复杂
代码语言:javascript
复制
public class LazySingleton {
    // 静态变量,存储单例对象,同时使用volatile修饰
    private static volatile LazySingleton instance = null;

    // 私有化构造方法,防止外部通过new创建对象
    private LazySingleton() {
    }

    // 提供一个公共的静态方法,返回单例对象
    public static LazySingleton getInstance() {
        // 第一次检查:提高效率
        if(instance == null) { 
            synchronized(LazySingleton.class) { 
                // 第二次检查:保证单例
                if(instance == null) { 
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }
}

💥注意事项:

  1. instance 设为 volatile,是为了防止编译器优化出现的指令重排序问题,导致上述第 14 行代码拿到的对象是 null(重排序导致先赋值,再初始化,结果拿到的是没初始化前的 null 对象),此时一使用该对象,直接就奔溃了!
  2. 这个双重判断,叫做 "双重检查锁定DCL",第一层检查是为了避免不必要的加锁带来的效率问题第二层检查是为了实现单例对象,目的不同!

阻塞队列

阻塞队列是一种特殊的队列,也遵守 "先进先出" 的原则,并且阻塞队列也是一种线程安全的数据结构,并且具有以下特性:

  • 当队列满的时候,继续入队列就会阻塞,直到有其他线程从队列中取走元素。
  • 当队列空的时候,继续出队列也会阻塞,直到有其他线程往队列中插入元素,

阻塞队列的一个典型应用场景就是 "生产者消费者模型”,这是一种非常典型的开发模型,如下图所示:

生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,而是直接扔给阻塞队列;消费者不找生产者要数据,而是直接从阻塞队列里取。

下面是生产者消费者模型的优缺点:

  1. 优点:
    1. 削峰填谷。减少出现生产者资料生产少了之后消费者也消费的少,或者消费者消费地快导致生产者来不及生产的情况。
    2. 降低耦合度。生产者线程只负责生产资料然后放到阻塞队列中,而不关心消费者的任务;消费者线程只需要从阻塞队列中获取资料进行消费,而不关心生产者的任务。
    3. 减少资源竞争,提升程序效率。可以理解为有了阻塞队列之后,生产者和消费者都只能依次排队操作队列,且队列会自动安排进出顺序,这不就既高效又有秩序了吗?
  2. 缺点:
    1. 系统更加复杂。
    2. 引入队列的层数太多,会增加网络传输的开销。

一、标准库中的阻塞队列 -- BlockingQueue

Java 标准库中,java.util.concurrent 包提供了多种阻塞队列的实现,这些阻塞队列都实现了 BlockingQueue 接口。

以下是 BlockingQueue 接口的主要方法:

方法

描述

void put(E e)

将元素插入队列的尾部,如果队列已满,则阻塞等待,直到有空间可用

E take()

从队列头部取出并移除元素,如果队列为空,则阻塞等待,直到有元素可用

E poll(long timeout, TimeUnit unit)

从队列头部取出并移除元素,如果队列为空,则等待最多指定的时间,如果在指定时间内没有元素可用,则返回 null

E poll()

从队列头部取出并移除元素,如果队列为空,则立即返回 null

E peek()

查看队列头部的元素,但不移除它,如果队列为空,则返回 null

int remainingCapacity()

返回队列剩余的容量,即还可以插入多少元素

boolean offer(E e, long timeout, TimeUnit unit)

将元素插入队列的尾部,如果队列已满,则等待最多指定的时间,如果在指定时间内没有空间可用,则返回 false

boolean offer(E e)

将元素插入队列的尾部,如果队列已满,则立即返回 false

int drainTo(Collection<? super E> c)

将队列中的所有元素转移到指定的集合中,返回转移的元素数量

int drainTo(Collection<? super E> c, int maxElements)

将队列中的最多 maxElements 个元素转移到指定的集合中,返回转移的元素数量

💥注意事项:

  • 插入和弹出队列的操作,只有 put()take() 有阻塞等待的效果,其它方法比如 offer()add()poll() 等是不带阻塞功能的,所以一般只用 put()take() 即可。
  • BlockingQueue 是一个接口,创建对象时候需要用以下常见的阻塞队列实现类:

队列类型

底层结构

有界性

处理优先级

线程安全机制

适用场景

ArrayBlockingQueue

数组(大小固定)

有界(必须指定)

FIFO

ReentrantLock

严格控制任务数量、防止 OOM,适合内存敏感系统(如任务缓冲)

LinkedBlockingQueue

链表

有界或无界

FIFO

插入与移除锁分离(putLock / takeLock)

任务量大但可控时可用无界; 希望高并发性能但需限制任务量时选用有界

SynchronousQueue

无(容量为 0)

无存储

N/A

ReentrantLock

适合高并发短任务、任务直接移交等场景

PriorityBlockingQueue

堆结构

无界

优先级

ReentrantLock

任务需按优先级处理,如支付系统处理高金额、实时任务优先执行

DelayQueue

堆结构

无界

延迟 + 优先级

ReentrantLock

定时任务调度,如订单超时取消、缓存过期处理

二、自主实现阻塞队列(理解原理、细节即可)

💥注意事项:

  • 在判断队列为空或者队列为满的时候,要用 while 判断,而不能用 if 判断。因为比如队列为满的时候被唤醒了,也不一定队列就不满,有可能在唤醒期间有其它线程又插入了数据,此时就会出现问题!
代码语言:javascript
复制
public class MyBlockingQueue {
    // 使用循环队列模拟阻塞队列
    private String[] arr;
    private int head = 0;
    private int tail = 0;
    private int size; // 有效元素个数
    private static final Object lock = new Object();

    public MyBlockingQueue(int capacity) {
        arr = new String[capacity];
    }

    public void put(String value) throws InterruptedException {
        synchronized (lock) {
            // 用 while 来判断队列是否满,看看是否要阻塞
            while(size >= arr.length) {
                lock.wait();
            }

            // 插入数据
            arr[tail] = value;
            tail = (tail + 1) % arr.length;
            size++;

            // 唤醒消费者
            lock.notify();
        }
    }

    public String take() throws InterruptedException {
        synchronized (lock) {
            // 用 while 来判断队列是否空,看看是否要阻塞
            while(size == 0) {
                lock.wait();
            }

            // 拿到数据
            String value = arr[head];
            head = (head + 1) % arr.length;
            size--;

            // 唤醒生产者
            lock.notify();
            return value;
        }
    }
}

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 线程同步
    • 一、wait && notify
    • 二、wait 与 sleep 的区别
  • 单例模式
    • 一、饿汉模式
    • 二、懒汉模式
  • 阻塞队列
    • 一、标准库中的阻塞队列 -- BlockingQueue
    • 二、自主实现阻塞队列(理解原理、细节即可)
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档