0

0

深入理解Java并发:同步机制与线程间通信

花韻仙語

花韻仙語

发布时间:2025-11-11 15:51:30

|

546人浏览过

|

来源于php中文网

原创

深入理解java并发:同步机制与线程间通信

本文旨在全面解析Java中的同步机制,重点探讨`synchronized`关键字在保护共享资源方面的应用,以及`wait()`, `notify()`, `notifyAll()`方法实现线程间通信的原理与实践。我们将通过具体场景分析,阐明同步访问共享可变状态的重要性、`wait()`方法释放锁的机制,并提供使用lambda表达式创建线程时进行有效通信的示例,旨在帮助开发者构建健壮、高效的并发应用。

Java并发编程中的同步机制

在多线程环境下,当多个线程访问和修改同一个共享资源时,如果不采取适当的同步措施,就可能导致数据不一致、竞态条件等问题。Java提供了多种同步机制来确保线程安全,其中synchronized关键字是最基础也是最常用的工具

1. synchronized关键字:保护共享可变状态

synchronized关键字可以用于修饰方法或代码块,它确保在任何时刻,只有一个线程可以执行被同步的代码。当一个线程进入synchronized代码块或方法时,它会获取一个锁(也称为监视器锁或内部锁),其他试图进入相同锁保护的代码的线程将被阻塞,直到持有锁的线程释放锁。

1.1 synchronized代码块与共享资源的可见性

考虑一个常见的场景:一个List作为共享资源,多个线程对其进行添加元素和查询大小的操作。

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

示例代码:不安全的并发访问

import java.util.ArrayList;
import java.util.List;

public class UnsafeListAccess {
    private List<Integer> a = new ArrayList<>();

    // foo 方法通过 synchronized(a) 保护了对 a 的添加操作
    public void foo(int i) {
        synchronized (a) {
            a.add(i);
            System.out.println(Thread.currentThread().getName() + " added " + i + ", current size: " + a.size());
        }
    }

    // goo 方法没有同步,直接访问 a 的大小
    public int goo() {
        return a.size();
    }

    public static void main(String[] args) throws InterruptedException {
        UnsafeListAccess demo = new UnsafeListAccess();

        // 启动多个线程并发调用 foo
        for (int i = 0; i < 5; i++) {
            final int value = i;
            new Thread(() -> demo.foo(value), "Producer-" + value).start();
        }

        // 等待一段时间,确保 some foo calls might have happened
        Thread.sleep(100);

        // 在另一个线程中调用 goo
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " queried size: " + demo.goo());
        }, "Checker").start();
    }
}

问题分析: 在上述代码中,foo方法使用了synchronized(a)来确保对a.add(i)操作的原子性和可见性。这意味着在任何给定时刻,只有一个线程可以执行foo方法中synchronized(a)块内的代码。然而,goo方法没有使用任何同步机制来访问a.size()。

  1. 线程A执行foo时,线程B能否访问goo? 是的,可以。因为foo方法同步在对象a上,而goo方法没有同步。synchronized关键字只锁定指定的监视器对象,不影响其他未同步的代码块或方法。因此,当线程A持有a的锁并执行foo时,线程B可以自由地进入并执行goo方法。

  2. 线程A通过goo访问a时,线程B能否进入foo的同步块? 不能。如果线程A正在执行goo(未同步),而线程B试图进入foo的synchronized(a)块,线程B将需要获取对象a的锁。由于goo没有持有a的锁,线程B可以立即尝试获取锁。一旦线程B成功获取锁,它就可以进入foo的同步块。

数据一致性问题: 尽管线程B可以访问goo,但这种设计存在严重的数据一致性问题。goo()方法返回的a.size()值可能不是最新的。当foo方法向a中添加元素时,这些修改可能不会立即对goo方法可见。这是因为Java内存模型(JMM)规定,没有同步的读写操作不能保证内存可见性。一个线程对共享变量的修改,可能不会立即刷新到主内存,另一个线程从主内存读取时可能读到旧值。

正确做法:同步所有对共享可变状态的访问

为了确保数据一致性和可见性,所有访问共享可变状态的代码都必须进行同步。这意味着goo方法也应该同步在同一个监视器对象上。

import java.util.ArrayList;
import java.util.List;

public class SafeListAccess {
    private final List<Integer> a = new ArrayList<>(); // 使用 final 确保引用不变

    // foo 方法同步在 a 上
    public void foo(int i) {
        synchronized (a) {
            a.add(i);
            System.out.println(Thread.currentThread().getName() + " added " + i + ", current size: " + a.size());
        }
    }

    // goo 方法也同步在 a 上,确保可见性和原子性
    public int goo() {
        synchronized (a) { // 必须同步在同一个对象上
            return a.size();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SafeListAccess demo = new SafeListAccess();

        // 启动多个线程并发调用 foo
        for (int i = 0; i < 5; i++) {
            final int value = i;
            new Thread(() -> demo.foo(value), "Producer-" + value).start();
        }

        // 等待一段时间,确保 some foo calls might have happened
        Thread.sleep(100);

        // 在另一个线程中调用 goo
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " queried size: " + demo.goo());
        }, "Checker").start();
    }
}

通过同步goo方法,我们确保了在foo对a进行修改后,goo能够看到最新的a的状态。

2. wait(), notify(), notifyAll():线程间协作

除了保护共享资源,线程间经常需要进行协作,例如一个线程需要等待某个条件满足后才能继续执行,或者一个线程通知其他线程某个条件已经满足。Object类提供的wait(), notify(), notifyAll()方法是实现这种协作的基础。

TTSMaker
TTSMaker

TTSMaker是一个免费的文本转语音工具,提供语音生成服务,支持多种语言。

下载

重要原则:

  • 这三个方法都必须在synchronized代码块或方法中调用,并且必须作用于当前线程所持有的监视器对象上。
  • wait()方法会释放当前线程持有的监视器锁,并使线程进入等待状态,直到被notify()或notifyAll()唤醒,或者被中断。
  • notify()方法会唤醒一个在当前对象上等待的线程。
  • notifyAll()方法会唤醒所有在当前对象上等待的线程。

2.1 wait()方法释放锁的机制

示例场景:

public class WaitNotifyDemo {
    public synchronized void foo() {
        System.out.println(Thread.currentThread().getName() + " entered foo, holding lock.");
        // do stuff before wait
        notifyAll(); // 尝试唤醒其他线程
        // do stuff after notifyAll
        if (Thread.currentThread().getName().equals("Thread-1")) { // 假设某个条件
            System.out.println(Thread.currentThread().getName() + " going to wait().");
            try {
                wait(); // 线程1进入等待状态并释放锁
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        System.out.println(Thread.currentThread().getName() + " resumed from wait() or finished.");
        // do stuff after wait
    }

    public static void main(String[] args) throws InterruptedException {
        WaitNotifyDemo demo = new WaitNotifyDemo();

        Thread t1 = new Thread(() -> demo.foo(), "Thread-1");
        Thread t2 = new Thread(() -> demo.foo(), "Thread-2");

        t1.start();
        Thread.sleep(50); // 确保t1先进入
        t2.start();

        t1.join();
        t2.join();
    }
}

问题分析: 假设Thread-1先进入foo方法,执行到wait()。

  1. Thread-1进入foo方法,获取WaitNotifyDemo实例的锁。
  2. Thread-1执行notifyAll()。此时,如果还没有其他线程在等待,这个notifyAll()将不起作用。
  3. Thread-1执行wait()方法。关键点:wait()方法会立即释放WaitNotifyDemo实例的锁,然后Thread-1进入等待状态。
  4. 此时,WaitNotifyDemo实例的锁被释放,Thread-2有机会获取到锁并进入foo方法。
  5. Thread-2进入foo方法,获取WaitNotifyDemo实例的锁。
  6. Thread-2执行notifyAll()。由于Thread-1正在等待,它会被这个notifyAll()唤醒。
  7. Thread-1被唤醒后,它并不能立即继续执行,因为它需要重新获取WaitNotifyDemo实例的锁。
  8. Thread-2继续执行foo方法中剩余的代码(如果if条件不满足,或者直接执行到方法结束)。
  9. 当Thread-2退出foo方法时,它会释放WaitNotifyDemo实例的锁。
  10. Thread-1现在可以尝试重新获取锁。一旦获取成功,它将从wait()方法处继续执行。

总结: wait()方法会释放锁,允许其他线程进入同步块。被notify()/notifyAll()唤醒的线程,必须在重新获得锁之后才能继续执行。

3. Lambda表达式创建线程的通知机制

当使用Lambda表达式创建线程时,我们通常会得到一个Thread对象的引用。要通知这个线程,我们不能直接对Thread对象调用wait()或notify(),因为wait()/notify()是Object类的方法,它们操作的是监视器锁,而不是线程本身。

核心思想: 线程间通信需要一个共享的监视器对象。

示例场景: 如何通知一个由Lambda表达式创建的线程?

public class LambdaThreadNotify {
    // 定义一个共享的监视器对象
    private final Object monitor = new Object();
    private volatile boolean conditionMet = false; // 共享条件

    Thread workerThread;

    public void startWorker() {
        workerThread = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + ": Worker started.");
            synchronized (monitor) { // 线程需要同步在同一个监视器对象上
                while (!conditionMet) { // 使用循环防止虚假唤醒
                    try {
                        System.out.println(Thread.currentThread().getName() + ": Worker waiting for condition...");
                        monitor.wait(); // 释放 monitor 的锁并等待
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        System.out.println(Thread.currentThread().getName() + ": Worker interrupted.");
                        return;
                    }
                }
                System.out.println(Thread.currentThread().getName() + ": Worker condition met, continuing...");
                // 执行后续任务
            }
            System.out.println(Thread.currentThread().getName() + ": Worker finished its task.");
        }, "LambdaWorker");

        workerThread.start();
    }

    public void notifyWorker() {
        synchronized (monitor) { // 通知方也需要同步在同一个监视器对象上
            System.out.println(Thread.currentThread().getName() + ": Notifier setting condition and notifying.");
            conditionMet = true; // 改变共享条件
            monitor.notifyAll(); // 唤醒所有在 monitor 上等待的线程
        }
    }

    public static void main(String[] args) throws InterruptedException {
        LambdaThreadNotify demo = new LambdaThreadNotify();

        demo.startWorker(); // 启动工作线程

        Thread.sleep(1000); // 模拟主线程做一些其他工作

        demo.notifyWorker(); // 通知工作线程

        demo.workerThread.join(); // 等待工作线程结束
        System.out.println("Main thread finished.");
    }
}

实现方法:

  1. 共享监视器对象: 创建一个所有相关线程都能访问的共享Object实例(例如monitor)。
  2. 工作线程等待: 在Lambda线程内部,在synchronized (monitor)块中,使用monitor.wait()来等待某个条件。wait()会释放monitor的锁。
  3. 通知方唤醒: 在需要通知工作线程的方法中(例如notifyWorker()),同样在synchronized (monitor)块中,修改共享条件(如conditionMet = true),然后调用monitor.notifyAll()来唤醒等待的线程。
  4. 循环检查条件: 被wait()唤醒的线程,在继续执行前,应再次检查等待条件是否满足(通常在一个while循环中)。这是为了处理虚假唤醒(spurious wakeups)和多个线程被唤醒但条件只满足一个的情况。

总结与最佳实践

  • 同步所有访问: 任何对共享可变状态的读写操作都必须通过同步机制进行保护,且所有相关操作应同步在同一个监视器对象上,以确保数据一致性和内存可见性。
  • 理解synchronized锁定的对象: synchronized代码块锁定的是括号中的对象,synchronized方法锁定的是方法所属的实例对象(对于静态方法,锁定的是类的Class对象)。
  • wait(), notify(), notifyAll()的正确使用: 它们必须在synchronized块中调用,并且作用于当前线程持有的锁对象。wait()会释放锁,而notify()/notifyAll()不会释放锁,直到同步块结束。
  • 使用while循环检查等待条件: 永远不要在if语句中调用wait(),因为线程可能被虚假唤醒或被其他条件不满足的notify()唤醒。
  • 选择合适的同步工具: 对于更复杂的并发场景,Java并发包(java.util.concurrent)提供了更高级、更灵活的工具,如ReentrantLock, Semaphore, CountDownLatch, CyclicBarrier, BlockingQueue等,它们通常比裸露的synchronized和wait/notify组合更易于使用和管理。例如,BlockingQueue可以优雅地解决生产者-消费者问题,而无需手动实现wait/notify。

深入理解和正确应用Java的同步机制是编写高性能、高可靠性并发应用程序的关键。通过遵循上述原则和最佳实践,开发者可以有效地管理线程间的协作与竞争,避免常见的并发陷阱。

热门AI工具

更多
DeepSeek
DeepSeek

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

豆包大模型
豆包大模型

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

WorkBuddy
WorkBuddy

腾讯云推出的AI原生桌面智能体工作台

腾讯元宝
腾讯元宝

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

文心一言
文心一言

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

讯飞写作
讯飞写作

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

即梦AI
即梦AI

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

ChatGPT
ChatGPT

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

相关专题

更多
if什么意思
if什么意思

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

847

2023.08.22

while的用法
while的用法

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

107

2023.09.25

lambda表达式
lambda表达式

Lambda表达式是一种匿名函数的简洁表示方式,它可以在需要函数作为参数的地方使用,并提供了一种更简洁、更灵活的编码方式,其语法为“lambda 参数列表: 表达式”,参数列表是函数的参数,可以包含一个或多个参数,用逗号分隔,表达式是函数的执行体,用于定义函数的具体操作。本专题为大家提供lambda表达式相关的文章、下载、课程内容,供大家免费下载体验。

215

2023.09.15

python lambda函数
python lambda函数

本专题整合了python lambda函数用法详解,阅读专题下面的文章了解更多详细内容。

192

2025.11.08

Python lambda详解
Python lambda详解

本专题整合了Python lambda函数相关教程,阅读下面的文章了解更多详细内容。

61

2026.01.05

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

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

871

2024.01.03

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

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

32

2025.12.06

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

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

765

2023.08.10

C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

76

2026.03.11

热门下载

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

精品课程

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

共23课时 | 4.3万人学习

C# 教程
C# 教程

共94课时 | 11.2万人学习

Java 教程
Java 教程

共578课时 | 81.1万人学习

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

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