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

相关推荐
kiracrimson2 分钟前
STL:List的使用和模拟实现
开发语言·c++·list
码智社17 分钟前
Redis 哨兵模式详解:从原理到生产实践
java·redis
救救孩子把26 分钟前
结构化输出为何“格式正确、答案错误”?JSON Schema 的能力边界
android·java·json·agent·agent服务
北冥you鱼38 分钟前
Go 语言 GORM 使用 Preload:从关联查询到嵌套预加载的完整实践
开发语言·jvm·golang
silvamux_43 分钟前
微头条爆款体平台适配实测:把交付要求变成可计算的指标
java·线性代数·算法·决策树·矩阵
论文复现现场43 分钟前
DeepSeek V4.1 Flash 和 V4 Pro 有什么区别?API 价格、性能与迁移建议
java·服务器·人工智能·python·大模型api·deepseek·deepseek v4.1
dingzd951 小时前
TikTok账号登录态失效的Cookie字段排查路径
开发语言·php·跨境电商·营销策略
曹牧1 小时前
C#:The operation has timed out
开发语言·c#
郭小李1 小时前
转发与重定向
java·服务器·数据库
你看我名字1 小时前
数眼智能 Search + Reader 实战:联网问答怎样避免“搜到了,却引用错了”
android·java·开发语言