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标准库提供了LinkedBlockingQueue、ArrayBlockingQueue。核心方法:

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

相关推荐
Sarvartha2 小时前
final 关键字
java·开发语言
2601_952047792 小时前
单一策略深度教程:用轻易云把集成任务的报错实时推送到钉钉机器人
java·机器人·钉钉
程序员Sunday3 小时前
JavaScript 事件循环面试题,宏任务与微任务怎么执行|Sunday面试指南
开发语言·javascript·面试·校招·事件循环·程序员sunday
数据狐(Datafox)3 小时前
淘宝图片搜索 API 落地实战:基于以图搜货搭建跨境电商选品系统
java·大数据·微服务
zhangzeyuaaa4 小时前
Ruby 多线程、GVL(GIL)与 Mutex 完全指南
开发语言·前端·ruby
谢亮_vipxieliang5 小时前
用 PHP 构建轻量级 REST API:从路由到鉴权的完整实践
开发语言·后端·php
Escalating_xu5 小时前
【C 语言】数据在内存中的存储:补码、大小端、整型陷阱与 IEEE 754 全解析
java·c语言·网络
思无邪665 小时前
用 AI 做 JS 逆向:从抓包到复现的完整方法论
开发语言·javascript·人工智能