Java多线程初阶(下)—— synchronized、volatile、wait/notify与经典并发工具

Java多线程初阶(下)------ synchronized、volatile、wait/notify与经典并发工具

一、解决线程不安全的神器:synchronized(监视器锁)

synchronized能保证原子性可见性

1. 核心特性:互斥与可重入

  • 互斥 :当线程A进入synchronized代码块(相当于加锁),其他试图进入该锁保护的代码块的线程(如线程B)必须阻塞等待,直到A执行完毕释放锁(解锁)。
  • 可重入 :同一个线程可以多次获取同一把锁,不会把自己锁死。synchronized内部记录了"锁的持有者"和"计数器",每次重入计数器+1,完全退出时计数器归零才真正释放锁。
java 复制代码
synchronized (locker) {
    synchronized (locker) { // 同一个线程可以再次进入,不会死锁
        count++;
    }
}

2. 三种使用方式

  • 修饰代码块 :需要手动指定锁对象(synchronized(locker) {})。
  • 修饰普通方法 :锁的是当前实例对象 this
  • 修饰静态方法 :锁的是当前类的 Class 对象(全局锁)。

二、保证可见性的轻量级方案:volatile

  • 作用 :被volatile修饰的变量,读写时强制绕过工作内存,直接操作主内存。保证一个线程修改后,其他线程立刻可见。
  • 误区volatile不保证原子性 。比如volatile int count,两个线程执行count++依然会数据错乱,因为"读-改-写"不是原子操作。它适合作为标志位 (如boolean isQuit)使用。

三、线程间的协作:wait() 与 notify()

synchronized解决了竞争问题,但有时我们需要线程间主动协调顺序(如线程A传完球,线程B才能扣篮)。wait/notify定义在Object类中,必须配合synchronized使用。

1. wait() 做了什么?

  • 释放当前对象锁。
  • 让当前线程进入等待队列(WAITING状态)。
  • 直到被notify唤醒或超时,才重新竞争锁。

2. notify() 与 notifyAll()

  • notify():随机唤醒一个在该对象上等待的线程。
  • notifyAll():唤醒所有等待的线程,它们再去竞争锁。

经典生产者消费者模型示例(伪代码)

java 复制代码
synchronized (lock) {
    while (队列已满) { lock.wait(); } // 生产者等待
    生产数据;
    lock.notifyAll(); // 唤醒消费者
}
synchronized (lock) {
    while (队列为空) { lock.wait(); } // 消费者等待
    消费数据;
    lock.notifyAll(); // 唤醒生产者
}

注意wait必须在while循环中调用,因为被唤醒后不一定满足条件(可能又被别的线程抢占了资源)。

wait 与 sleep 的区别(高频面试题)

  • wait 属于Object,必须配合synchronized,会释放锁;sleep 属于Thread,不涉及锁释放。
  • wait 需要被显式唤醒;sleep 到时间自动醒来。

四、必会的并发编程案例

1. 单例模式(双重检查锁 DCL)

懒汉模式(单线程版)线程不安全,多线程版加上synchronized后虽然安全但效率低。最终优化版(面试必考)

java 复制代码
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;
    }
}

为什么加volatile 因为new Singleton()这一步可能会发生指令重排序(分配内存、初始化、赋值),加上volatile禁止重排序,保证其他线程看到的是完整的对象。

2. 阻塞队列(BlockingQueue)

Java标准库提供了LinkedBlockingQueueArrayBlockingQueue。核心方法:

  • put(e):队列满则阻塞等待。
  • take():队列空则阻塞等待。
    利用阻塞队列可以轻松实现"生产者-消费者模型",起到削峰填谷和解耦的作用。

3. 定时器(Timer)

java 复制代码
Timer timer = new Timer();
timer.schedule(new TimerTask() {
    @Override
    public void run() {
        System.out.println("3秒后执行");
    }
}, 3000);

原理:内部维护一个优先级队列(按执行时间排序),后台线程不断扫描队首元素,判断当前时间是否达到执行时间。

4. 线程池(ThreadPoolExecutor)

频繁创建/销毁线程开销大,线程池复用线程。

  • 标准库创建Executors.newFixedThreadPool(10)(固定数量)等。
  • 核心参数(线程池7大参数)
    • corePoolSize:核心线程数(正式工)。
    • maximumPoolSize:最大线程数(正式工 + 临时工)。
    • keepAliveTime:临时工空闲多久会被辞退。
    • workQueue:任务阻塞队列。
    • handler:拒绝策略(当队列满且线程数达到最大时执行)。
  • 自定义线程池:核心逻辑就是维护一个阻塞队列存放任务,多个Worker线程不断从队列取任务执行。

总结 :多线程编程的核心是访问共享资源时保证线程安全 。优先考虑volatile(轻量级标志位),其次使用synchronized保证原子性,线程间协调使用wait/notify,最后不要重复造轮子,多用标准库提供的线程池、阻塞队列等组件。

相关推荐
Joy T4 小时前
Spring AI 2.0 Agent 进阶:Memory、State 与 Context Engineering 常见技术全景
java·人工智能·后端·spring·agent入门·agent state
估值探索者4 小时前
【Python量化系统工程化 #08】关了 SSH 就停?systemd 让脚本开机自启 + 异常自动拉起
java·c++·人工智能·分类·数据挖掘
RAN11426760084 小时前
1.3第一个程序的介绍
开发语言·qt
天若有情6734 小时前
C++花式魔改!手写头文件复刻Java语法,彻底打破编码思维定式
java·c++·语言做语言·jac++
blue_ice .5 小时前
Verilog DDS 数字信号发生器:DAC 输出模拟波形与 ADC 回采观测
开发语言·单片机·嵌入式硬件·fpga开发
远翔调光芯片^138287988725 小时前
ECP5702能芯科技PD取电芯片在市场上的优势有哪些?
开发语言·人工智能·单片机·嵌入式硬件·智能家居
西峰u6 小时前
Java单例模式|从基础到多线程安全
java·java单例模式
ClinicTech6 小时前
实验室洗瓶机的清洗系统架构与自动化控制解析
java·python
linmengmeng_13146 小时前
【总结】MyBatis-Plus批量插入与清表的正确姿势
java·spring boot·mysql·mybatis
木井巳6 小时前
【记忆化搜索】不同路径
java·算法·leetcode·深度优先·剪枝·推荐算法