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,最后不要重复造轮子,多用标准库提供的线程池、阻塞队列等组件。

相关推荐
笨笨饿30 分钟前
#135_代码中的类型重定义艺术:从基础封装到函数类型设计
开发语言·stm32·单片机·嵌入式硬件·mcu·物联网·嵌入式实时数据库
统计学小王子33 分钟前
R语言入门——ggplot2图例增加公式
开发语言·r语言
牛油果子哥q35 分钟前
C++内存池与对象池精讲:内存碎片、自定义分配器、对象池实现、STL allocator原理、工程落地与性能对比
java·开发语言·c++
CodeStats44 分钟前
《源纹天书》第三百六十三章至第三百六十四章
java·开发语言·源纹天书
万法若空1 小时前
C/C++ 类型别名定义方式对比
java·c语言·c++
Java后端的Ai之路1 小时前
03、Python - 抽象工厂模式
开发语言·python·设计模式·抽象工厂模式·通用智慧
吴声子夜歌1 小时前
Java——开发中通用的方法和准则(一)
java·开发语言
mmsy10241 小时前
缓存知识点总结
java·spring·缓存
运行时异常1 小时前
【WMS 仓储系统集成 AI Agent 实战】第 3 讲:Spring Security 6 + JWT——addFilterBefore 一词之差,全站 401
java·后端