使用信号量同步线程:在 Java 中练习并发 - LeetCode 问题 1115,“交替打印 FooBar”

**并发简介**

在软件开发中,并发允许多个进程或线程同时执行,这通常会缩短执行时间并提高资源利用效率。但是,有效的并发管理对于避免竞争条件、死锁和状态不一致等问题至关重要。这时信号量等同步机制就必不可少了。

**理解信号量**

信号量是一种同步工具,用于控制并发系统中多个线程对公共资源的访问。它的作用类似于计数器,用于控制有多少个线程可以同时访问代码的特定部分。信号量操作包括:

  • 获取:如果信号量计数器大于零,则减少信号量计数器,允许线程继续执行。如果信号量计数器为零,则线程将被阻塞,直到另一个线程释放信号量。
  • 释放:增加信号量的计数器,表示线程已完成使用资源,从而允许其他等待线程继续进行。
  • 信号量对于管理任务之间的依赖关系或确保程序的某些部分按特定顺序运行特别有用。

    **问题描述**

    class FooBar {
      public void foo() {
        for (int i = 0; i < n; i++) {
          print("foo");
        }
      }
    
      public void bar() {
        for (int i = 0; i < n; i++) {
          print("bar");
        }
      }
    }

    我们有一个类“FooBar”,它有两个方法,“foo()”和“bar()”。这两个方法分别用于打印“foo”和“bar”。难点在于修改这个类,以便当两个不同的线程执行这两个方法时,输出始终重复“n”次“foobar”。无论调度程序对线程的执行顺序如何,都必须实现这一点。

    该问题源自 LeetCode,具体来自问题 1115,“交替打印 FooBar”。

    **解决方法**

    为了确保正确的执行顺序(“foo”后跟“bar”),我们可以使用信号量来协调线程。下面是我们如何使用信号量修改现有的“FooBar”类以实现所需的行为:

  • 引入两个信号量:一个信号量(semFoo)将控制 foo() 的执行,另一个信号量(semBar)将控制 bar()。
  • 初始化信号量:semFoo 使用许可证(计数 = 1)进行初始化,从而允许 foo() 首先执行。semBar 未使用许可证(计数 = 0)进行初始化,从而阻止 bar() 执行,直到 foo() 完成为止。
  • 修改方法:每个方法在继续之前必须获取其信号量,并在完成其任务后释放另一个信号量。
  • 以下是“FooBar”类的修改版本:

    class FooBar {
        private int n;
        private Semaphore semFoo = new Semaphore(1); // Initially, allow foo() to proceed
        private Semaphore semBar = new Semaphore(0); // Initially, prevent bar() from proceeding
    
        public FooBar(int n) {
            this.n = n;
        }
    
        public void foo(Runnable printFoo) throws InterruptedException {
            for (int i = 0; i < n; i++) {
                semFoo.acquire(); // Wait until it's allowed to proceed
                printFoo.run();   // This will print "foo"
                semBar.release(); // Allow bar() to proceed
            }
        }
    
        public void bar(Runnable printBar) throws InterruptedException {
            for (int i = 0; i < n; i++) {
                semBar.acquire(); // Wait until it's allowed to proceed
                printBar.run();   // This will print "bar"
                semFoo.release(); // Allow foo() to proceed again
            }
        }
    }

    **运行代码**

    为了演示这个解决方案,我们可以创建一个“FooBar”实例并启动两个线程,每个线程负责以下方法之一:

    public class Main {
        public static void main(String[] args) {
            FooBar fooBar = new FooBar(2); // Example for n = 2
    
            Thread threadA = new Thread(() -> {
                try {
                    fooBar.foo(() -> System.out.print("foo"));
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
    
            Thread threadB = new Thread(() -> {
                try {
                    fooBar.bar(() -> System.out.print("bar"));
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
    
            threadA.start();
            threadB.start();
        }
    }

    此设置确保当 n = 2 时,控制台上的输出为“foobarfoobar”,展示了信号量在控制 Java 应用程序中线程执行顺序方面的强大功能。

    Image description