
本文深入探讨了在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()时会一直阻塞,导致整个程序无法继续交替执行。同步机制失效,线程之间无法进行有效的协作。
行者AI
行者AI绘图创作,唤醒新的灵感,创造更多可能
100 查看详情
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(); // 给线程命名方便调试 }}
代码解析:
共享Semaphore实例: sem1和sem2被定义为main方法内的final局部变量。由于匿名内部类(Runnable的lambda表达式)可以访问final或“effectively final”的局部变量,这两个Semaphore实例被两个线程共享和操作。初始化许可:sem1 = new Semaphore(1);:sem1初始有一个许可。这意味着第一个线程(打印“1”的线程)可以立即获取许可并开始执行。sem2 = new Semaphore(0);:sem2初始没有许可。这意味着第二个线程(打印“2”的线程)在尝试获取sem2的许可时会立即阻塞,直到有其他线程释放了sem2的许可。线程1 (Thread-1) 的逻辑:sem1.acquire();:获取sem1的许可。由于初始有1个许可,线程1可以顺利通过。System.out.print(“1”);:打印“1”。sem2.release();:释放sem2的一个许可。此时sem2的许可数变为1,允许等待中的线程2继续执行。线程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并发:使用Semaphore实现线程交替执行的精确同步的详细内容,更多请关注创想鸟其它相关文章!
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/571223.html
微信扫一扫
支付宝扫一扫