0

0

Java并发:synchronized机制与wait/notify的深度解析

DDD

DDD

发布时间:2025-11-23 14:07:40

|

819人浏览过

|

来源于php中文网

原创

java并发:synchronized机制与wait/notify的深度解析

本文深入探讨Java并发编程中synchronized方法与synchronized块的使用,特别是涉及共享资源访问和wait()/notify()机制时的最佳实践。文章将分析使用不同锁对象可能导致的并发问题,强调确保内存同步和正确唤醒等待线程的关键原则,并提供避免常见陷阱的指导。

1. synchronized 方法与 synchronized 块基础

在Java中,synchronized关键字是实现线程同步的基本机制,它确保同一时刻只有一个线程可以执行被同步的代码块或方法。理解其工作原理对于编写正确的并发程序至关重要。

1.1 synchronized 方法

当一个方法被声明为 synchronized 时,它会锁定当前实例对象(this)。如果是静态 synchronized 方法,则会锁定该方法所属的 Class 对象。这意味着在任何给定时间,只有一个线程可以执行该对象的任何 synchronized 方法。

示例:

立即学习Java免费学习笔记(深入)”;

class CircularBuffer {
    private byte[] buffer;
    private int size;
    private int head = 0;
    private int tail = 0;
    private int availableObjects = 0;

    public CircularBuffer(int capacity) {
        this.size = capacity;
        this.buffer = new byte[capacity];
    }

    synchronized void add(byte b) throws InterruptedException {
        if (availableObjects == size) { // 这里的 if 存在风险,应为 while
            wait(); // wait() 和 notifyAll() 默认作用于当前对象 (this)
        }
        buffer[tail] = b;
        tail = (tail + 1) % size;
        availableObjects++;
        notifyAll();
    }

    synchronized byte remove() throws InterruptedException {
        if (availableObjects == 0) { // 这里的 if 存在风险,应为 while
            wait();
        }
        byte element = buffer[head];
        head = (head + 1) % size;
        availableObjects--;
        notifyAll();
        return element;
    }
}

在上述实现中,add 和 remove 方法都锁定了 CircularBuffer 实例本身。这意味着在任何给定时间,只有一个线程可以执行 add 或 remove 方法,从而保证了对 buffer、head、tail 和 availableObjects 等共享状态的原子性访问。

1.2 synchronized 块

synchronized (object) 语法允许我们指定一个任意对象作为锁。这提供了更细粒度的控制,可以只同步代码的特定部分,或者使用不同的锁对象来同步不同的资源。

示例(存在问题的设计):

BibiGPT-哔哔终结者
BibiGPT-哔哔终结者

B站视频总结器-一键总结 音视频内容

下载
class CircularBufferProblematic {
    private final Object addLock = new Object();
    private final Object removeLock = new Object();
    private byte[] buffer;
    private int size;
    private int head = 0;
    private int tail = 0;
    private int availableObjects = 0; // 假设这里是 AtomicInteger 或普通 int

    public CircularBufferProblematic(int capacity) {
        this.size = capacity;
        this.buffer = new byte[capacity];
    }

    void add(byte b) throws InterruptedException {
        synchronized (addLock) { // 锁定 addLock
            while (availableObjects == size) { // 使用 while 循环是正确的改进
                addLock.wait();
            }
            buffer[tail] = b;
            tail = (tail + 1) % size;
            availableObjects++;
        }
        // 为什么这里需要第二个 synchronized 块?
        synchronized (removeLock) { // 锁定 removeLock
            removeLock.notifyAll();
        }
    }

    byte remove() throws InterruptedException {
        byte element;
        synchronized (removeLock) { // 锁定 removeLock
            while (availableObjects == 0) { // 使用 while 循环是正确的改进
                removeLock.wait();
            }
            element = buffer[head];
            head = (head + 1) % size;
            availableObjects--;
        }
        // 为什么这里需要第二个 synchronized 块?
        synchronized (addLock) { // 锁定 addLock
            addLock.notifyAll();
        }
        return element;
    }
}

2. wait()/notify() 机制与锁对象

Object 类的 wait()、notify() 和 notifyAll() 方法是Java中实现线程间协作的关键。它们允许线程在特定条件不满足时释放锁并进入等待状态,并在条件满足时被其他线程唤醒。

核心原则: 调用 wait()、notify() 或 notifyAll() 方法时,当前线程必须持有该方法所属对象的监视器(即锁)。如果不在相应的 synchronized 块中调用,会抛出 IllegalMonitorStateException。

这就是 CircularBufferProblematic 示例中额外 synchronized 块的根本原因:

  • 在 add 方法中,removeLock.notifyAll() 必须在 synchronized (removeLock) 块中执行,因为它需要持有 removeLock 的监视器。
  • 在 remove 方法中,addLock.notifyAll() 必须在 synchronized (addLock) 块中执行,因为它需要持有 addLock 的监视器。

这种设计是为了确保 wait() 和 notify() 调用的原子性,并防止在没有正确持有锁的情况下操作等待队列。

3. 使用不同锁对象引发的并发问题

尽管使用不同的锁对象可以实现更细粒度的控制,但当这些锁对象保护的是相同的共享状态时,就会引入严重的并发问题。CircularBufferProblematic 示例就存在这样的缺陷:

  • add 方法在 synchronized (addLock) 块中修改 buffer、tail 和 availableObjects。
  • remove 方法在 synchronized (removeLock) 块中读取 buffer、head 和 availableObjects。

问题分析: 由于 addLock 和 removeLock 是两个独立的锁,一个线程可以持有 addLock 并修改 buffer,而另一个线程同时持有 removeLock 并读取 buffer。这可能导致:

  1. 数据不一致: 读者可能读取到部分更新或过时的数据。例如,add 线程更新了 buffer 中的一个元素并递增了 availableObjects,但 remove 线程在看到 availableObjects 变化之前就读取了 buffer,从而读取到 null 或旧值。
  2. 内存可见性问题: synchronized 关键字除了提供互斥访问外,还保证了内存可见性。当一个线程释放锁时,它所做的所有写入操作都会被刷新到主内存;当一个线程获取锁时,它会从主内存中读取最新值。如果 add 和 remove 使用不同的锁,它们之间的内存同步就无法保证,一个线程对共享变量的修改可能对另一个线程不可见。

解决方案: 当多个操作(如生产者和消费者)需要访问和修改同一组共享状态变量时,它们必须使用同一个锁对象来保护这些共享状态。这样可以确保任何时候只有一个线程能够修改或读取这些变量,从而保证数据的一致性和内存可见性。

改进示例(使用单一锁):

class CircularBufferCorrected {
    private final Object lock = new Object(); // 单一锁对象
    private byte[] buffer;
    private int size;
    private int head = 0;
    private int tail = 0;
    private int availableObjects = 0; // 可以是普通 int,因为有锁保护

    public CircularBufferCorrected(int capacity) {
        this.size = capacity;
        this.buffer = new byte[capacity];
    }

    public void add(byte b) throws InterruptedException {
        synchronized (lock) { // 锁定同一个对象
            while (availableObjects == size) { // 使用 while 循环
                lock.wait();
            }
            buffer[tail] = b;
            tail = (tail + 1) % size;
            availableObjects++;
            lock.notifyAll(); // 唤醒所有等待的线程
        }
    }

    public byte remove() throws InterruptedException {
        byte element;
        synchronized (lock) { // 锁定同一个对象
            while (availableObjects == 0) { // 使用 while 循环
                lock.wait();
            }
            element = buffer[head];
            head = (head + 1) % size;
            availableObjects--;
            lock.notifyAll(); // 唤醒所有等待的线程
            return element;
        }
    }
}

在这个改进版本中,add 和 remove 方法都同步在同一个 lock 对象上。这确保了对 buffer、head、tail 和 availableObjects 的所有访问都是互斥的,并且保证了内存可见性。availableObjects 也不再需要是 AtomicInteger,因为其操作在 synchronized 块内已受保护。

4. wait() 条件检查:while 循环的重要性

在调用 wait() 方法时,必须将其放在 while 循环中检查条件,而不是 if 语句

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
c语言中null和NULL的区别
c语言中null和NULL的区别

c语言中null和NULL的区别是:null是C语言中的一个宏定义,通常用来表示一个空指针,可以用于初始化指针变量,或者在条件语句中判断指针是否为空;NULL是C语言中的一个预定义常量,通常用来表示一个空值,用于表示一个空的指针、空的指针数组或者空的结构体指针。

236

2023.09.22

java中null的用法
java中null的用法

在Java中,null表示一个引用类型的变量不指向任何对象。可以将null赋值给任何引用类型的变量,包括类、接口、数组、字符串等。想了解更多null的相关内容,可以阅读本专题下面的文章。

438

2024.03.01

if什么意思
if什么意思

if的意思是“如果”的条件。它是一个用于引导条件语句的关键词,用于根据特定条件的真假情况来执行不同的代码块。本专题提供if什么意思的相关文章,供大家免费阅读。

776

2023.08.22

while的用法
while的用法

while的用法是“while 条件: 代码块”,条件是一个表达式,当条件为真时,执行代码块,然后再次判断条件是否为真,如果为真则继续执行代码块,直到条件为假为止。本专题为大家提供while相关的文章、下载、课程内容,供大家免费下载体验。

94

2023.09.25

class在c语言中的意思
class在c语言中的意思

在C语言中,"class" 是一个关键字,用于定义一个类。想了解更多class的相关内容,可以阅读本专题下面的文章。

469

2024.01.03

python中class的含义
python中class的含义

本专题整合了python中class的相关内容,阅读专题下面的文章了解更多详细内容。

13

2025.12.06

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

503

2023.08.10

俄罗斯Yandex引擎入口
俄罗斯Yandex引擎入口

2026年俄罗斯Yandex搜索引擎最新入口汇总,涵盖免登录、多语言支持、无广告视频播放及本地化服务等核心功能。阅读专题下面的文章了解更多详细内容。

141

2026.01.28

包子漫画在线官方入口大全
包子漫画在线官方入口大全

本合集汇总了包子漫画2026最新官方在线观看入口,涵盖备用域名、正版无广告链接及多端适配地址,助你畅享12700+高清漫画资源。阅读专题下面的文章了解更多详细内容。

24

2026.01.28

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 3万人学习

C# 教程
C# 教程

共94课时 | 7.8万人学习

Java 教程
Java 教程

共578课时 | 52.5万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号