0

0

Java并发编程:深入理解synchronized关键字与线程安全实践

霞舞

霞舞

发布时间:2025-11-23 15:11:52

|

1033人浏览过

|

来源于php中文网

原创

Java并发编程:深入理解synchronized关键字与线程安全实践

本文深入探讨java中synchronized关键字在方法和代码块层面的应用,重点解析wait()和notify()系列方法的使用规范及其必须在同步块内调用的原因。通过分析循环缓冲区的并发实现案例,文章揭示了分离锁可能导致的严重线程安全问题,强调了统一锁机制的重要性,并阐述了在并发编程中,wait()条件判断使用while循环而非if的必要性,旨在指导读者构建健壮的并发程序。

在Java并发编程中,synchronized关键字是实现线程同步和互斥的核心机制之一。它不仅能够保证代码块或方法的原子性,还能确保内存可见性,即一个线程对共享变量的修改对其他线程是可见的。本文将通过一个经典的生产者-消费者模型(循环缓冲区)的并发实现案例,详细剖析synchronized方法与synchronized代码块的区别、wait()/notify()机制的使用细节以及并发编程中常见的陷阱与最佳实践。

synchronized方法与synchronized代码块:基础与选择

synchronized关键字可以用于修饰方法或代码块,以实现对共享资源的互斥访问。

  • synchronized方法: 当synchronized修饰一个实例方法时,锁对象是当前实例对象(this);当修饰一个静态方法时,锁对象是当前类的Class对象。这种方式简单直观,但锁的粒度较大,可能会限制并发性。

    class ExampleBuffer {
        // ... 共享资源
        synchronized void addElement(byte b) {
            // 访问和修改共享资源
        }
    
        synchronized byte removeElement() {
            // 访问和修改共享资源
            return 0; // 示例返回值
        }
    }
  • synchronized代码块: synchronized代码块允许我们指定任意对象作为锁。这提供了更细粒度的控制,我们可以根据需要选择不同的锁对象,或者只同步代码的关键部分,从而在某些场景下提高并发度。

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

    class ExampleBuffer {
        private final Object lock = new Object(); // 显式声明锁对象
        // ... 共享资源
        void addElement(byte b) {
            synchronized (lock) {
                // 访问和修改共享资源
            }
        }
    
        void removeElement() {
            synchronized (lock) {
                // 访问和修改共享资源
            }
        }
    }

在实际开发中,应根据共享资源的访问模式和对并发性能的要求,合理选择synchronized方法或synchronized代码块,并确定合适的锁对象。

wait()、notify()和notifyAll()的同步要求

Object类提供的wait()、notify()和notifyAll()方法是Java中实现线程间协作(如生产者-消费者模式)的关键。然而,使用这些方法有一个严格的规定:任何调用wait()、notify()或notifyAll()的方法,都必须在持有该对象监视器(即该对象的锁)的synchronized代码块或方法内部。 否则,会抛出IllegalMonitorStateException运行时异常。

考虑案例中的第二个实现片段:

// 在add方法中
synchronized (removeLock){ // 为什么这里又加了一个同步块?
    removeLock.notifyAll();
}

// 在remove方法中
synchronized (addLock){ // 为什么这里又加了一个同步块?
    addLock.notifyAll();
}

这里添加第二个synchronized代码块的原因,正是为了满足notifyAll()方法的调用要求。当需要调用removeLock.notifyAll()时,线程必须先获得removeLock对象的锁;同理,调用addLock.notifyAll()时,线程必须获得addLock对象的锁。synchronized关键字不仅提供了互斥访问,还保证了内存同步,使得wait()和notify()能够正确地协调线程状态,确保线程在等待或被唤醒时,其状态转换是原子且可见的。

并发编程中的陷阱:分离锁与共享资源

尽管第二个实现为了满足notifyAll()的调用要求而添加了额外的同步块,但它引入了一个更严重的并发问题:对同一个共享资源使用了不同的锁。

腾讯交互翻译
腾讯交互翻译

腾讯AI Lab发布的一款AI辅助翻译产品

下载

在第二个实现中,add方法在addLock上同步以修改buffer和availableObjects,而remove方法在removeLock上同步以读取buffer和修改availableObjects。

// add方法片段
void add (byte b) throws InterruptedException{
    synchronized (addLock){ // 保护对buffer的写入
        // ... 修改 buffer ...
        // ... 修改 availableObjects ...
    }
    // ...
}

// remove方法片段
byte remove () throws InterruptedException{
    byte element;
    synchronized (removeLock){ // 保护对buffer的读取
        // ... 读取 buffer ...
        // ... 修改 availableObjects ...
    }
    // ...
    return element;
}

这种分离锁的策略是极其危险的。当一个线程在addLock的保护下修改buffer时,另一个线程可能在removeLock的保护下读取buffer。由于它们使用的是不同的锁,synchronized机制无法保证对buffer的互斥访问和内存可见性。这会导致:

  • 数据不一致: 读取线程可能读取到写入线程修改前的旧数据。
  • 内存可见性问题: 写入线程对buffer的修改可能对读取线程不可见,即使写入操作已经完成。
  • 读取到null或其他错误值: 在极端情况下,读取线程可能读取到部分写入的数据,导致错误。

核心原则:所有访问(读或写)共享可变状态的操作,都必须通过同一个锁来保护。

对于循环缓冲区这样的共享数据结构,无论是添加元素(生产者)还是移除元素(消费者),它们都在操作同一个buffer数组、head、tail指针以及availableObjects计数器。因此,正确的做法是使用一个统一的锁对象来保护所有对这些共享状态的访问。

修正方案示例:

以下是一个使用统一锁对象实现线程安全循环缓冲区的示例:

import java.util.concurrent.atomic.AtomicInteger; // 在这里可以简化为普通int

class ThreadSafeCircularBuffer {
    private final byte[] buffer;
    private int head;
    private int tail;
    private int availableObjects; // 在统一锁保护下,可以是普通int
    private final int size;
    private final Object bufferLock = new Object(); // 统一的锁对象

    public ThreadSafeCircularBuffer(int size) {
        if (size <= 0) {
            throw new IllegalArgumentException("Buffer size must be positive.");
        }
        this.size = size;
        this.buffer = new byte[size];
        this.head = 0;
        this.tail = 0;
        this.availableObjects = 0;
    }

    public void add(byte b) throws InterruptedException {
        synchronized (bufferLock) { // 使用统一的锁
            while (availableObjects == size) { // 使用while循环判断条件
                bufferLock.wait(); // 缓冲区满,等待消费者消费
            }
            buffer[tail] = b;
            tail = (tail + 1) % size;
            availableObjects++;
            bufferLock.notifyAll(); // 唤醒等待的消费者(或生产者,如果他们有其他条件)
        }
    }

    public byte remove() throws InterruptedException {
        byte element;
        synchronized (bufferLock) { // 使用统一的锁
            while (availableObjects == 0) { // 使用while循环判断条件
                bufferLock.wait(); // 缓冲区空,等待生产者生产
            }
            element = buffer[head];
            head = (head + 1) % size;
            availableObjects--;
            bufferLock.notifyAll(); // 唤醒等待的生产者(或消费者,如果他们有其他条件)
        }
        return element;
    }
}

在这个修正后的实现中,add和remove方法都通过bufferLock来同步,确保了对buffer、head、tail和availableObjects的访问是线程安全的,并且内存可见性得到了保证。

wait()条件判断:if vs. while

原始的第一个实现中使用了if(condition) wait();,而第二个实现则改为了while(condition) wait();。这是一个非常重要的改进,也是并发编程中的一个常见陷阱。

为什么必须使用while循环来判断wait()的条件?

  1. 虚假唤醒(Spurious Wakeups): JVM规范允许线程在没有收到notify()或notifyAll()调用时被“虚假唤醒”。如果使用if,被虚假唤醒的线程会直接执行后续代码,而此时它等待的条件可能并未满足,导致逻辑错误。
  2. 多个生产者/消费者: 在多生产者-多消费者场景中,一个notifyAll()可能会唤醒所有等待的线程。当这些线程逐一获得锁并检查条件时,可能发现条件已不满足。例如,多个消费者被唤醒,第一个消费者获取到元素后,第二个消费者再检查时可能发现缓冲区又空了。
  3. 条件变更: 即使没有虚假唤醒,一个线程被唤醒后,它所等待的条件可能在它获得锁并重新检查之前,被另一个(更快的)线程再次改变。

热门AI工具

更多
DeepSeek
DeepSeek

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

豆包大模型
豆包大模型

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

WorkBuddy
WorkBuddy

腾讯云推出的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语言中的一个预定义常量,通常用来表示一个空值,用于表示一个空的指针、空的指针数组或者空的结构体指针。

254

2023.09.22

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

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

1089

2024.03.01

if什么意思
if什么意思

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

847

2023.08.22

while的用法
while的用法

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

106

2023.09.25

treenode的用法
treenode的用法

​在计算机编程领域,TreeNode是一种常见的数据结构,通常用于构建树形结构。在不同的编程语言中,TreeNode可能有不同的实现方式和用法,通常用于表示树的节点信息。更多关于treenode相关问题详情请看本专题下面的文章。php中文网欢迎大家前来学习。

549

2023.12.01

C++ 高效算法与数据结构
C++ 高效算法与数据结构

本专题讲解 C++ 中常用算法与数据结构的实现与优化,涵盖排序算法(快速排序、归并排序)、查找算法、图算法、动态规划、贪心算法等,并结合实际案例分析如何选择最优算法来提高程序效率。通过深入理解数据结构(链表、树、堆、哈希表等),帮助开发者提升 在复杂应用中的算法设计与性能优化能力。

30

2025.12.22

深入理解算法:高效算法与数据结构专题
深入理解算法:高效算法与数据结构专题

本专题专注于算法与数据结构的核心概念,适合想深入理解并提升编程能力的开发者。专题内容包括常见数据结构的实现与应用,如数组、链表、栈、队列、哈希表、树、图等;以及高效的排序算法、搜索算法、动态规划等经典算法。通过详细的讲解与复杂度分析,帮助开发者不仅能熟练运用这些基础知识,还能在实际编程中优化性能,提高代码的执行效率。本专题适合准备面试的开发者,也适合希望提高算法思维的编程爱好者。

44

2026.01.06

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

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

870

2024.01.03

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号