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”后就停止了。

问题根源: 核心问题在于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()时会一直阻塞,导致整个程序无法继续交替执行。同步机制失效,线程之间无法进行有效的协作。

Teleporthq
Teleporthq

一体化AI网站生成器,能够快速设计和部署静态网站

下载

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的共享性原则和许可机制,是编写健壮、高效并发程序的关键。

相关专题

更多
java
java

Java是一个通用术语,用于表示Java软件及其组件,包括“Java运行时环境 (JRE)”、“Java虚拟机 (JVM)”以及“插件”。php中文网还为大家带了Java相关下载资源、相关课程以及相关文章等内容,供大家免费下载使用。

835

2023.06.15

java正则表达式语法
java正则表达式语法

java正则表达式语法是一种模式匹配工具,它非常有用,可以在处理文本和字符串时快速地查找、替换、验证和提取特定的模式和数据。本专题提供java正则表达式语法的相关文章、下载和专题,供大家免费下载体验。

740

2023.07.05

java自学难吗
java自学难吗

Java自学并不难。Java语言相对于其他一些编程语言而言,有着较为简洁和易读的语法,本专题为大家提供java自学难吗相关的文章,大家可以免费体验。

736

2023.07.31

java配置jdk环境变量
java配置jdk环境变量

Java是一种广泛使用的高级编程语言,用于开发各种类型的应用程序。为了能够在计算机上正确运行和编译Java代码,需要正确配置Java Development Kit(JDK)环境变量。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

397

2023.08.01

java保留两位小数
java保留两位小数

Java是一种广泛应用于编程领域的高级编程语言。在Java中,保留两位小数是指在进行数值计算或输出时,限制小数部分只有两位有效数字,并将多余的位数进行四舍五入或截取。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

399

2023.08.02

java基本数据类型
java基本数据类型

java基本数据类型有:1、byte;2、short;3、int;4、long;5、float;6、double;7、char;8、boolean。本专题为大家提供java基本数据类型的相关的文章、下载、课程内容,供大家免费下载体验。

446

2023.08.02

java有什么用
java有什么用

java可以开发应用程序、移动应用、Web应用、企业级应用、嵌入式系统等方面。本专题为大家提供java有什么用的相关的文章、下载、课程内容,供大家免费下载体验。

430

2023.08.02

java在线网站
java在线网站

Java在线网站是指提供Java编程学习、实践和交流平台的网络服务。近年来,随着Java语言在软件开发领域的广泛应用,越来越多的人对Java编程感兴趣,并希望能够通过在线网站来学习和提高自己的Java编程技能。php中文网给大家带来了相关的视频、教程以及文章,欢迎大家前来学习阅读和下载。

16926

2023.08.03

高德地图升级方法汇总
高德地图升级方法汇总

本专题整合了高德地图升级相关教程,阅读专题下面的文章了解更多详细内容。

43

2026.01.16

热门下载

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

精品课程

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

共23课时 | 2.6万人学习

C# 教程
C# 教程

共94课时 | 6.9万人学习

Java 教程
Java 教程

共578课时 | 47.1万人学习

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

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