Java多线程同步:使用Semaphore实现交替执行与共享资源管理

Java多线程同步:使用Semaphore实现交替执行与共享资源管理

本教程探讨了在java多线程环境中,如何利用`semaphore`实现线程间的交替执行同步,以达到特定序列输出(如“121212”)。文章分析了初学者常犯的错误——为每个线程创建独立的同步器实例,导致线程间无法通信。核心解决方案在于确保所有相关线程共享同一个`semaphore`实例,从而正确协调它们的执行顺序。通过示例代码,详细演示了如何正确配置和使用`semaphore`来构建高效且无竞争的多线程协作机制。

并发编程中,线程间的协作与同步是构建健壮系统的关键。当多个线程需要按照特定顺序或互斥地访问资源时,同步机制显得尤为重要。本教程将以一个典型的场景为例:两个线程(或两个不同的操作)需要交替执行,一个打印“1”,另一个打印“2”,最终输出“121212…”。我们将重点介绍如何使用Java的Semaphore(信号量)来实现这一目标,并分析常见的陷阱及正确的实践方法。

理解问题:实现线程交替执行

我们的目标是让两个线程T1和T2协同工作。T1负责打印数字“1”,T2负责打印数字“2”。要求是:

T1先打印“1”。T1完成后,T2才能打印“2”。T2完成后,T1才能再次打印“1”。以此类推,实现“121212…”的输出序列。

这种场景非常适合使用信号量来协调。信号量可以控制对共享资源的访问,通过许可(permit)的数量来限制同时访问的线程数。

初始尝试与常见陷阱分析

许多初学者在尝试解决此类问题时,可能会遇到程序无法按预期执行,甚至出现死锁的情况。一个常见的错误模式是为每个线程创建独立的同步器实例。

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

考虑以下一个有问题的代码示例:

import java.util.concurrent.Semaphore;public class SemTestProblem {    Semaphore sem1 = new Semaphore(1); // 为线程1准备的信号量    Semaphore sem2 = new Semaphore(0); // 为线程2准备的信号量    public static void main(String args[]) {        // 错误:创建了两个SemTestProblem实例        final SemTestProblem instance1 = new SemTestProblem();        final SemTestProblem instance2 = new SemTestProblem();        new Thread(() -> {            try {                instance1.numb1(); // 线程1操作instance1的信号量            } catch (Exception e) {                throw new RuntimeException(e);            }        }).start();        new Thread(() -> {            try {                instance2.numb2(); // 线程2操作instance2的信号量            } catch (Exception e) {                throw new RuntimeException(e);            }        }).start();    }    private void numb1() {        while (true) {            try {                sem1.acquire(); // 获取sem1许可                System.out.print(" 1");                sem2.release(); // 释放sem2许可                Thread.sleep(100); // 模拟工作            } catch (InterruptedException e) {                Thread.currentThread().interrupt();            }        }    }    private void numb2() {        while (true) {            try {                sem2.acquire(); // 获取sem2许可                System.out.print(" 2");                sem1.release(); // 释放sem1许可                Thread.sleep(100); // 模拟工作            } catch (InterruptedException e) {                Thread.currentThread().interrupt();            }        }    }}

运行上述代码,你会发现程序可能只打印了一个“1”就停止了,或者行为异常。问题出在main方法中:final SemTestProblem instance1 = new SemTestProblem();final SemTestProblem instance2 = new SemTestProblem();

这里创建了SemTestProblem的两个独立实例。每个实例都拥有自己独立的sem1和sem2信号量。Thread-1调用instance1.numb1(),操作的是instance1内部的sem1和sem2。而Thread-2调用instance2.numb2(),操作的是instance2内部的sem1和sem2。

这意味着两个线程之间并没有共享任何信号量实例。它们各自在自己的“沙盒”中运行,彼此无法通过信号量进行通信和协调。Thread-1释放的instance1.sem2许可,对Thread-2尝试获取的instance2.sem2许可没有任何影响。这就是导致同步失败的根本原因。

TextCortex TextCortex

AI写作能手,在几秒钟内创建内容。

TextCortex 62 查看详情 TextCortex

正确的解决方案:共享信号量实例

要解决上述问题,核心思想是确保所有参与同步的线程都操作同一个信号量实例。这可以通过几种方式实现:

使用静态(static)信号量: 将信号量声明为类的静态成员,所有实例和线程都将共享这些静态成员。传递信号量实例: 在创建线程或Runnable时,将共享的信号量实例作为参数传递进去。单例模式: 确保只有一个包含信号量的SemTest实例被创建,并由所有线程共享。

在这里,我们采用将信号量声明为静态成员的方式,这通常是最简洁明了的解决方案之一。

import java.util.concurrent.Semaphore;public class SemTestCorrect {    // 将信号量声明为静态成员,确保所有线程共享同一组信号量    private static Semaphore sem1 = new Semaphore(1); // 初始许可1,允许Thread-1先执行    private static Semaphore sem2 = new Semaphore(0); // 初始许可0,Thread-2需等待Thread-1释放    public static void main(String args[]) {        // 只需一个SemTestCorrect实例(如果方法不是静态的)        // 或者直接在main方法中创建并启动线程,让他们访问静态信号量        // 创建并启动线程1        new Thread(() -> {            try {                // 直接调用静态方法或通过任意实例调用,都会访问静态信号量                numb1();             } catch (Exception e) {                throw new RuntimeException(e);            }        }).start();        // 创建并启动线程2        new Thread(() -> {            try {                // 直接调用静态方法或通过任意实例调用,都会访问静态信号量                numb2();            } catch (Exception e) {                throw new RuntimeException(e);            }        }).start();    }    // 将方法也改为静态,以便直接在静态上下文中调用    private static void numb1() {        while (true) {            try {                sem1.acquire(); // 获取sem1许可,如果无许可则阻塞                System.out.print(" 1");                sem2.release(); // 释放sem2许可,唤醒等待sem2的线程                Thread.sleep(100); // 模拟工作,可观察到交替效果            } catch (InterruptedException e) {                Thread.currentThread().interrupt(); // 重新设置中断状态                System.err.println("Thread 1 interrupted.");                break; // 退出循环            }        }    }    private static void numb2() {        while (true) {            try {                sem2.acquire(); // 获取sem2许可,如果无许可则阻塞                System.out.print(" 2");                sem1.release(); // 释放sem1许可,唤醒等待sem1的线程                Thread.sleep(100); // 模拟工作            } catch (InterruptedException e) {                Thread.currentThread().interrupt(); // 重新设置中断状态                System.err.println("Thread 2 interrupted.");                break; // 退出循环            }        }    }}

代码解析:

静态信号量声明: private static Semaphore sem1 = new Semaphore(1); 和 private static Semaphore sem2 = new Semaphore(0);sem1初始化许可为1,这意味着numb1方法(或负责打印“1”的线程)可以在程序启动时立即获取许可并执行。sem2初始化许可为0,这意味着numb2方法(或负责打印“2”的线程)在程序启动时将无法获取许可,必须等待。static关键字确保了sem1和sem2是类级别的,所有SemTestCorrect的实例(即使我们只创建了一个或没有创建实例,直接通过类名访问)都共享这两个信号量。numb1()方法逻辑:sem1.acquire();:尝试获取sem1的许可。如果当前有许可(例如初始时),则获取成功并继续执行。如果没有,则线程阻塞,直到有其他线程释放sem1的许可。System.out.print(” 1″);:打印数字“1”。sem2.release();:释放sem2的一个许可。这会使得等待sem2许可的线程有机会获取许可并继续执行。numb2()方法逻辑:sem2.acquire();:尝试获取sem2的许可。由于sem2初始为0,numb2线程会在此处阻塞,直到numb1线程执行sem2.release()。System.out.print(” 2″);:打印数字“2”。sem1.release();:释放sem1的一个许可。这会使得等待sem1许可的线程有机会获取许可并继续执行。

通过这种“一放一取”的机制,两个线程得以严格按照“121212…”的顺序交替执行。

替代方案:使用synchronized或ReentrantLock

虽然Semaphore非常适合这种交替执行的场景,但Java中还有其他同步机制可以实现类似功能,例如:

synchronized关键字: 可以用于方法或代码块,实现互斥访问。结合wait()和notify()(或notifyAll())方法,可以实现线程间的协调和通信。ReentrantLock与Condition: ReentrantLock提供了比synchronized更灵活的锁定机制,而其伴随的Condition接口则提供了更精细的线程等待/通知控制。对于复杂的等待条件,Condition比wait/notify更强大。

对于本例中的简单交替打印,Semaphore的实现简洁而直观。

注意事项与总结

共享性是关键: 无论使用何种同步原语(Semaphore、Lock、synchronized),确保所有参与同步的线程都操作同一个实例是至关重要的。否则,同步机制将失效。初始许可数量: Semaphore的初始许可数量决定了哪个线程可以先开始执行,以及有多少个线程可以同时访问。在本例中,sem1为1,sem2为0,精确控制了启动顺序。死锁风险: 不当的acquire()和release()顺序或不匹配的操作可能导致死锁。例如,如果一个线程acquire()了许可却未能release(),其他等待该许可的线程将永远阻塞。中断处理: acquire()方法会抛出InterruptedException,在实际应用中应妥善处理,通常是捕获异常并重新设置线程的中断状态,或者根据业务逻辑决定是继续还是退出。公平性: Semaphore可以创建为公平(new Semaphore(permits, true))或非公平(默认)模式。公平信号量会按请求顺序授予许可,非公平信号量则可能允许“插队”,非公平通常吞吐量更高,但可能导致饥饿。

通过本教程,我们深入理解了如何利用Java Semaphore实现多线程间的交替执行同步,并强调了共享同步器实例的重要性。掌握这些概念对于编写高效、正确且无竞争的并发程序至关重要。

以上就是Java多线程同步:使用Semaphore实现交替执行与共享资源管理的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/982636.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
晨光海韵试听指南
上一篇 2025年12月1日 20:42:10
Win7文件属性怎么批量更改
下一篇 2025年12月1日 20:42:15

相关推荐

  • 如何用SumoPaint的AI裁剪图片?快速完成智能图片裁剪教程

    如何用SumoPaint的AI裁剪图片?快速完成智能图片裁剪教程如何用SumoPaint的AI裁剪图片?快速完成智能图片裁剪教程如何用SumoPaint的AI裁剪图片?快速完成智能图片裁剪教程如何用SumoPaint的AI裁剪图片?快速完成智能图片裁剪教程

    答案:SumoPaint虽无AI裁剪功能,但可通过魔棒、套索工具精确选区,结合图层蒙版与羽化、反选等操作实现智能裁剪效果,最后按需导出PNG或JPG高质量文件。 ☞☞☞AI 智能聊天, 问答助手, AI 智能搜索, 免费无限量使用 DeepSeek R1 模型☜☜☜ 在SumoPaint中,虽然它不…

    2026年9月21日 用户投稿
    000
  • Java OOP如何使用内部类提高代码组织性

    内部类提升Java代码组织性与封装性,成员内部类增强封装,静态内部类分离逻辑,局部与匿名内部类简化回调,私有内部类隐藏实现细节。 内部类在Java面向对象编程中是一种有效提升代码组织性和封装性的工具。通过将一个类定义在另一个类的内部,可以更好地表达类之间的逻辑关系,控制访问权限,并减少命名冲突。合理…

    2026年9月21日
    000
  • VSCode中竖线怎么设置_VSCode编辑区竖线(标尺)显示与配置教程

    在VSCode中启用垂直标尺需修改settings.json文件中的editor.rulers属性,如设置{ “editor.rulers”: [80, 120] }可在第80和120列显示竖线,提升代码对齐与可读性;虽原生不支持自定义颜色样式,但可通过安装Guides或In…

    2026年9月21日
    100
  • PHP 数组值比较与嵌套数组过滤教程

    本教程详细讲解如何在 PHP 中比较一个简单数组与一个复杂嵌套数组,并根据特定条件(如文件名匹配)过滤嵌套数组中的所有相关子数组。我们将通过识别非匹配项的索引,然后从所有子数组中移除这些项并重新索引,实现精确的数据筛选。 问题背景 在 php 开发中,我们经常会遇到需要处理结构复杂的数组数据。例如,…

    2026年9月21日
    100
  • Java集合框架在数据处理中的应用实例

    使用Set去重:通过LinkedHashSet去除标签重复并保持顺序;2. Map统计频次:利用HashMap统计单词出现次数;3. List结合Comparator排序:按年龄升序、姓名降序排列用户;4. 集合嵌套处理数据:用Map组织部门与员工列表。集合框架提升数据处理效率与代码可读性。 Jav…

    2026年9月21日
    000
  • Chrome浏览器怎么开启数据同步功能_Chrome浏览器跨设备数据同步设置教程

    首先登录Google账户启用Chrome同步功能,确保书签、历史记录、密码等数据跨设备一致;接着在设置中自定义同步内容类型以满足隐私需求;然后通过Google账户密钥或自定义密码加密同步数据,提升安全性;最后在新设备登录同一账户,自动接收已同步的浏览数据,实现无缝体验。 如果您希望在不同设备间无缝使…

    2026年9月21日
    000
  • 如何使用XGBoost训练AI大模型?优化机器学习模型的步骤

    XGBoost并非用于训练GPT类大模型,而是擅长处理结构化数据的高效梯度提升算法,其优势在于速度快、准确性高、支持并行计算、内置正则化与缺失值处理,适用于表格数据建模;通过分阶段超参数调优(如学习率、树深度、采样策略)、结合贝叶斯优化与交叉验证,并配合特征工程、数据预处理和集成学习等关键步骤,可显…

    2026年9月21日
    000
  • MySQL全文搜索如何与外部引擎结合_提升搜索体验?

    MySQL全文搜索如何与外部引擎结合_提升搜索体验?MySQL全文搜索如何与外部引擎结合_提升搜索体验?MySQL全文搜索如何与外部引擎结合_提升搜索体验?MySQL全文搜索如何与外部引擎结合_提升搜索体验?

    mysql 的全文搜索在中文分词和复杂查询上存在局限,常结合外部引擎提升性能。1. 使用 elasticsearch,通过 logstash 或 canal 同步数据,安装中文分词插件并利用布尔查询等优化搜索。2. 利用 sphinx,从 mysql 直接构建索引,通过 sql-like 接口和中文…

    2026年9月21日 用户投稿
    000
  • VSCode远程开发:配置容器与SSH连接的最佳实践解析

    使用VSCode远程开发提升效率,通过Remote-Containers和Remote-SSH实现环境标准化。1. 配置.devcontainer文件夹,用devcontainer.json定义容器环境,推荐自定义Dockerfile并预装工具;2. SSH连接需配置公钥认证、~/.ssh/conf…

    2026年9月21日
    100
  • 如何在Java中配置与数据库连接环境

    答案:Java中配置数据库连接需引入JDBC驱动,如MySQL在Maven中添加对应依赖;通过DriverManager或连接池(如HikariCP)获取Connection,使用try-with-resources管理资源;建议将连接参数存入properties文件,并处理常见问题如驱动加载、权限…

    2026年9月21日
    000
  • PHP错误日志怎么查看_PHP错误日志定位与查看方法

    要查看PHP错误日志,首先确定php.ini中error_log路径,若未设置则检查Web服务器(如Apache/Nginx)错误日志;确保log_errors=On、error_reporting合理配置,并通过tail、grep等工具分析日志,结合框架日志和系统日志(如syslog)全面定位问题…

    2026年9月21日
    200
  • VSCode怎么运行全部代码_VSCode批量执行代码教程

    在VSCode里“运行全部代码”或“批量执行代码”,其实很少是一个单一的、所有语言通用的按钮。它更多的是指根据你项目的具体需求,通过配置任务(Tasks)、使用集成终端(Integrated Terminal)配合脚本,或者利用特定语言的运行/调试配置(Launch Configurations)来…

    2026年9月21日
    100
  • TuxPaint的AI工具怎么裁剪图片?教你轻松完成图片裁剪步骤

    TuxPaint的AI工具怎么裁剪图片?教你轻松完成图片裁剪步骤TuxPaint的AI工具怎么裁剪图片?教你轻松完成图片裁剪步骤TuxPaint的AI工具怎么裁剪图片?教你轻松完成图片裁剪步骤TuxPaint的AI工具怎么裁剪图片?教你轻松完成图片裁剪步骤

    TuxPaint没有AI裁剪工具,只能通过橡皮擦或填充工具手动模拟裁剪效果,适合儿童创意绘画但不适合精确图像编辑。 ☞☞☞AI 智能聊天, 问答助手, AI 智能搜索, 免费无限量使用 DeepSeek R1 模型☜☜☜ TuxPaint作为一个面向儿童的绘画软件,其实并没有专门的“AI工具”来执行…

    2026年9月21日 用户投稿
    100
  • Java Executors类提供哪些线程池方法

    Executors类提供创建线程池的静态方法:newFixedThreadPool创建固定大小线程池,适用于稳定负载;newCachedThreadPool创建可缓存线程池,适合短期异步任务;newSingleThreadExecutor创建单线程池,保证任务顺序执行;newScheduledThr…

    2026年9月21日
    200
  • Windows&Linux双系统安装流程

    Windows&Linux双系统安装流程Windows&Linux双系统安装流程Windows&Linux双系统安装流程Windows&Linux双系统安装流程

    大家好,很高兴再次见到大家,我是你们的朋友全栈君。 注意事项:在安装Windows与Linux双系统时,建议先安装Windows系统,否则可能会导致grub引导被覆盖的问题。 Windows 10系统安装 制作启动盘(优启通链接)https://www.php.cn/link/219b87ff108…

    2026年9月21日 用户投稿
    200
  • MySQL性能模式监控资源_MySQL瓶颈定位精确工具

    MySQL性能模式监控资源_MySQL瓶颈定位精确工具MySQL性能模式监控资源_MySQL瓶颈定位精确工具MySQL性能模式监控资源_MySQL瓶颈定位精确工具MySQL性能模式监控资源_MySQL瓶颈定位精确工具

    mysql性能模式通过事件记录精准定位瓶颈,核心步骤包括:1.启用并配置performance schema,选择性开启消费者和仪器;2.监控等待事件、sql语句、阶段、i/o、内存及锁等关键指标;3.分析events_waits_summary_global_by_event_name等表识别资源…

    2026年9月21日 用户投稿
    000
  • 三星在电视端首发Perplexity AI应用程序,带来更具创新性AI体验

    10 月 23 日消息,三星电子于美国当地时间 21 日宣布,率先在电视终端推出 perplexity ai 应用程序,为三星电视用户带来更富创新的 ai 使用体验。 借助该应用程序,用户在安排日常生活、查找特定影视内容、创建梦幻体育联赛阵容或策划万圣节活动等场景中,可获得 AI 以卡片式回复框形式…

    2026年9月21日
    500
  • 帕鲁高管回应《幻兽帕鲁:帕鲁农场》疑似碰瓷《宝可梦 pokopia》:乱讲阴谋论

    帕鲁高管回应《幻兽帕鲁:帕鲁农场》疑似碰瓷《宝可梦 pokopia》:乱讲阴谋论帕鲁高管回应《幻兽帕鲁:帕鲁农场》疑似碰瓷《宝可梦 pokopia》:乱讲阴谋论帕鲁高管回应《幻兽帕鲁:帕鲁农场》疑似碰瓷《宝可梦 pokopia》:乱讲阴谋论帕鲁高管回应《幻兽帕鲁:帕鲁农场》疑似碰瓷《宝可梦 pokopia》:乱讲阴谋论

    在不久前的任天堂直面会上,官方公布了一款宝可梦ip的衍生新作——《宝可梦 pokopia》。这款作品让玩家化身一只能够变身成人类训练家的百变怪,主打种田与建造玩法,属于模拟经营类游戏。 视频欣赏: 无独有偶,几天后,《幻兽帕鲁》的开发商PocketPair也正式公布了他们的全新衍生作《幻兽帕鲁:帕鲁…

    2026年9月21日 用户投稿
    000
  • 如何使用mysql设计客户信息管理项目

    答案:设计客户信息管理系统需先明确功能需求,再合理规划数据库结构。1. 根据客户需求划分模块,包括客户基本信息、分类、状态、跟进记录等;2. 创建核心表如customers、company_info、follow_ups和users,确保字段完整且符合业务逻辑;3. 在关键字段上建立索引以提升查询效…

    2026年9月21日
    400
  • 夸克Ai搜索如何设置默认_夸克Ai搜索默认引擎更改

    首先在夸克APP中将默认搜索引擎设为AI引擎,再开启相关AI功能开关以启用AI搜索服务。具体步骤:1、打开夸克APP,点击右下角菜单进入设置;2、选择“通用”选项,点击“搜索引擎”;3、选择“AI引擎”或“夸克AI搜索”作为默认服务;4、返回主界面测试搜索关键词,确认AI结果是否展示;5、进入“AI…

    2026年9月21日
    400

发表回复

登录后才能评论
关注微信