0

0

Java并发:使用Semaphore实现线程交替执行的精确同步

花韻仙語

花韻仙語

发布时间:2025-11-09 14:35:01

|

743人浏览过

|

来源于php中文网

原创

java并发:使用semaphore实现线程交替执行的精确同步

本文深入探讨了在Java中利用`Semaphore`实现线程交替执行特定方法的同步机制。我们将分析一个常见的同步问题,即如何确保两个线程严格按照1-2-1-2的顺序打印输出,并详细解释原始代码中导致同步失败的陷阱——`Semaphore`实例的错误管理。最终,我们将提供一个经过优化的解决方案,并通过代码示例和最佳实践,指导开发者正确使用`Semaphore`进行精细化的线程协作。

1. 理解线程交替执行的需求

在多线程编程中,有时需要强制多个线程按照特定的顺序执行操作。一个典型的场景是,线程A执行完某项任务后,才允许线程B开始其任务;线程B完成后,再轮到线程A,如此循环往复。例如,我们希望两个线程分别打印数字“1”和“2”,最终输出序列为“121212...”。

实现这种精细的线程协作,Java提供了多种并发工具,其中Semaphore(信号量)是一种强大的选择,它通过管理许可数量来控制对共享资源的访问。

2. 初始尝试及问题分析

考虑以下使用Semaphore尝试实现“1212...”序列的代码:

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

import java.util.concurrent.Semaphore;

public class SemTest {
    Semaphore sem1 = new Semaphore(1); // 实例变量
    Semaphore sem2 = new Semaphore(0); // 实例变量

    public static void main(String args[]) {
        final SemTest semTest1 = new SemTest(); // 第一个实例
        final SemTest semTest2 = new SemTest(); // 第二个实例

        new Thread() {
            @Override
            public void run() {
                try {
                    semTest1.numb1(); // 线程1操作semTest1的实例变量
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }.start();

        new Thread() {
            @Override
            public void run() {
                try {
                    semTest2.numb2(); // 线程2操作semTest2的实例变量
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }.start();
    }

    private void numb1() {
        while (true) {
            try {
                sem1.acquire(); // 获取semTest1的sem1许可
                System.out.print("1");
                sem2.release(); // 释放semTest1的sem2许可
                Thread.sleep(100); // 适当缩短休眠时间,方便观察
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    private void numb2() {
        while (true) {
            try {
                sem2.acquire(); // 获取semTest2的sem2许可
                System.out.print("2");
                sem1.release(); // 释放semTest2的sem1许可
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

这段代码的预期是线程1打印“1”,然后通知线程2打印“2”,线程2打印“2”后,再通知线程1打印“1”,如此循环。然而,实际运行时,程序通常只打印一个“1”后就停止了。

codingM
codingM

AI智能体协作软件开发平台

下载

问题根源: 核心问题在于SemTest类中的sem1和sem2是实例变量。在main方法中,我们创建了两个SemTest的实例:semTest1和semTest2。

  • 第一个线程调用semTest1.numb1(),它操作的是semTest1实例内部的sem1和sem2。
  • 第二个线程调用semTest2.numb2(),它操作的是semTest2实例内部的sem1和sem2。

这意味着两个线程各自拥有独立的、不共享的Semaphore对象集合。线程1释放的semTest1.sem2的许可,并不能被线程2在semTest2.sem2上获取。由于semTest2.sem2初始许可为0,线程2尝试acquire()时会一直阻塞,导致整个程序无法继续交替执行。同步机制失效,线程之间无法进行有效的协作。

3. 正确的Semaphore同步方案

为了实现线程间的协作,Semaphore对象必须是共享的。这意味着所有需要通过这些Semaphore进行协调的线程,都必须引用同一个Semaphore实例。

以下是修正后的代码示例,它通过将Semaphore实例在main方法中创建并作为局部变量传递给线程(或通过匿名内部类捕获),确保了所有线程都操作相同的Semaphore对象。

import java.util.concurrent.Semaphore;

public class CorrectedSemTest {

    public static void main(String[] args) {
        // 声明并初始化两个共享的Semaphore实例
        // sem1初始许可为1,允许第一个线程立即执行
        final Semaphore sem1 = new Semaphore(1); 
        // sem2初始许可为0,阻止第二个线程在第一个线程完成前执行
        final Semaphore sem2 = new Semaphore(0); 

        // 线程1:负责打印"1"
        new Thread(() -> {
            try {
                while (true) {
                    sem1.acquire(); // 等待获取sem1的许可
                    System.out.print("1"); // 打印"1"
                    sem2.release(); // 释放sem2的许可,允许线程2执行
                    Thread.sleep(100); // 模拟工作
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                System.out.println("Thread 1 interrupted.");
            }
        }, "Thread-1").start(); // 给线程命名方便调试

        // 线程2:负责打印"2"
        new Thread(() -> {
            try {
                while (true) {
                    sem2.acquire(); // 等待获取sem2的许可
                    System.out.print("2"); // 打印"2"
                    sem1.release(); // 释放sem1的许可,允许线程1再次执行
                    Thread.sleep(100); // 模拟工作
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                System.out.println("Thread 2 interrupted.");
            }
        }, "Thread-2").start(); // 给线程命名方便调试
    }
}

代码解析:

  1. 共享Semaphore实例: sem1和sem2被定义为main方法内的final局部变量。由于匿名内部类(Runnable的lambda表达式)可以访问final或“effectively final”的局部变量,这两个Semaphore实例被两个线程共享和操作。
  2. 初始化许可:
    • sem1 = new Semaphore(1);:sem1初始有一个许可。这意味着第一个线程(打印“1”的线程)可以立即获取许可并开始执行。
    • sem2 = new Semaphore(0);:sem2初始没有许可。这意味着第二个线程(打印“2”的线程)在尝试获取sem2的许可时会立即阻塞,直到有其他线程释放了sem2的许可。
  3. 线程1 (Thread-1) 的逻辑:
    • sem1.acquire();:获取sem1的许可。由于初始有1个许可,线程1可以顺利通过。
    • System.out.print("1");:打印“1”。
    • sem2.release();:释放sem2的一个许可。此时sem2的许可数变为1,允许等待中的线程2继续执行。
  4. 线程2 (Thread-2) 的逻辑:
    • sem2.acquire();:等待获取sem2的许可。在线程1释放许可后,sem2有了1个许可,线程2可以获取并继续。
    • System.out.print("2");:打印“2”。
    • sem1.release();:释放sem1的一个许可。此时sem1的许可数变为1,允许等待中的线程1再次执行。

通过这种精确的acquire()和release()交替操作,两个线程得以严格按照“121212...”的顺序协作执行。

4. 注意事项与最佳实践

  • 共享性原则: 任何用于线程间同步的工具(如Semaphore、Lock、Condition等)都必须是所有参与同步的线程都能访问到的同一个实例。这是多线程协作的基础。
  • 初始许可的重要性: Semaphore的初始许可数量决定了哪个线程或多少个线程可以首先进入临界区。在本例中,sem1的1个许可确保了线程1优先开始。
  • acquire()与release()的配对: 务必确保acquire()和release()操作是成对出现的,并且逻辑正确。错误的配对会导致死锁或意外的并发行为。
  • 异常处理: 在acquire()方法中,通常需要捕获InterruptedException。当线程被中断时,应根据业务逻辑决定是重新尝试、清理资源还是退出。通常的最佳实践是重新设置中断标志:Thread.currentThread().interrupt();。
  • 可读性与维护性: 对于更复杂的同步场景,可以考虑将同步逻辑封装到独立的类或方法中,以提高代码的可读性和可维护性。例如,可以创建Worker1和Worker2类,并在其构造函数中传入共享的Semaphore实例。

5. 总结

通过本教程,我们深入探讨了如何利用Java的Semaphore实现线程间的精确交替执行。关键在于确保所有参与同步的线程都操作同一个共享的Semaphore实例。错误的Semaphore实例化(如每个线程拥有独立的Semaphore实例)是导致同步失败的常见陷阱。理解并正确应用Semaphore的共享性原则和许可机制,是编写健壮、高效并发程序的关键。

热门AI工具

更多
DeepSeek
DeepSeek

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

豆包大模型
豆包大模型

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

通义千问
通义千问

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

腾讯元宝
腾讯元宝

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

文心一言
文心一言

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

讯飞写作
讯飞写作

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

即梦AI
即梦AI

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

ChatGPT
ChatGPT

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

相关专题

更多
python中print函数的用法
python中print函数的用法

python中print函数的语法是“print(value1, value2, ..., sep=' ', end=' ', file=sys.stdout, flush=False)”。本专题为大家提供print相关的文章、下载、课程内容,供大家免费下载体验。

192

2023.09.27

python print用法与作用
python print用法与作用

本专题整合了python print的用法、作用、函数功能相关内容,阅读专题下面的文章了解更多详细教程。

18

2026.02.03

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

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

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

764

2023.08.10

Python 多线程与异步编程实战
Python 多线程与异步编程实战

本专题系统讲解 Python 多线程与异步编程的核心概念与实战技巧,包括 threading 模块基础、线程同步机制、GIL 原理、asyncio 异步任务管理、协程与事件循环、任务调度与异常处理。通过实战示例,帮助学习者掌握 如何构建高性能、多任务并发的 Python 应用。

376

2025.12.24

java多线程相关教程合集
java多线程相关教程合集

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

30

2026.01.21

Go高并发任务调度与Goroutine池化实践
Go高并发任务调度与Goroutine池化实践

本专题围绕 Go 语言在高并发任务处理场景中的实践展开,系统讲解 Goroutine 调度模型、Channel 通信机制以及并发控制策略。内容包括任务队列设计、Goroutine 池化管理、资源限制控制以及并发任务的性能优化方法。通过实际案例演示,帮助开发者构建稳定高效的 Go 并发任务处理系统,提高系统在高负载环境下的处理能力与稳定性。

4

2026.03.10

热门下载

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

精品课程

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

共23课时 | 4.3万人学习

C# 教程
C# 教程

共94课时 | 11.1万人学习

Java 教程
Java 教程

共578课时 | 80.3万人学习

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

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