两个线程交替顺序打印

24 阅读9分钟

Java两个线程交替顺序打印,一个顺序打印12345,一个顺序打印ABCDE

public class AlternatingPrinter {

    private final Object lock = new Object();
    private volatile boolean printNumber = true; // Start with printing numbers
    private int number = 1;
    private char letter = 'A';

    public void printNumbers() {
        synchronized (lock) {
            try {
                for (int i = 0; i < 5; i++) {
                    while (!printNumber) {
                        lock.wait(); // Wait if it's not this thread's turn
                    }
                    System.out.print(number++);
                    printNumber = false; // Switch turn
                    lock.notifyAll();    // Notify the other thread
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                System.err.println("Number printing thread interrupted: " + e.getMessage());
            }
        }
    }

    public void printLetters() {
        synchronized (lock) {
            try {
                for (int i = 0; i < 5; i++) {
                    while (printNumber) {
                        lock.wait(); // Wait if it's not this thread's turn
                    }
                    System.out.print(letter++);
                    printNumber = true; // Switch turn
                    lock.notifyAll();   // Notify the other thread
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                System.err.println("Letter printing thread interrupted: " + e.getMessage());
            }
        }
    }

    public static void main(String[] args) {
        AlternatingPrinter printer = new AlternatingPrinter();

        Thread t1 = new Thread(printer::printNumbers);
        Thread t2 = new Thread(printer::printLetters);

        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.err.println("Main thread interrupted: " + e.getMessage());
        }
        System.out.println(); // For a new line after printing
    }
}

核心概念

  • synchronized (lock):这确保了在任何给定时间只有一个线程可以执行代码块(printNumbersprintLetters)。lock 对象在两个线程之间共享。
  • volatile boolean printNumber: volatile 确保了 printNumber 的更改对所有线程立即可见。 这个布尔标志控制轮到哪个线程执行:true 表示轮到打印数字的线程,false 表示轮到打印字母的线程。
  • lock.wait():此方法使当前线程释放锁并进入等待状态,直到另一个线程对同一锁对象调用 notify()notifyAll()。线程在一个 while 循环内等待,以防止虚假唤醒(即线程可能在没有被明确通知的情况下唤醒)。
  • lock.notifyAll():此方法唤醒所有在同一锁对象上等待的线程。在这种情况下,它会唤醒另一个线程,以便它可以检查是否轮到自己打印。
  • Thread.join():在 main 方法中,使用 t1.join()t2.join() 来使主线程等待,直到 t1t2 完成执行后,才打印最后的换行符并退出。

为什么加synchronized?synchronized wait时能解锁吗

来解释一下为什么在之前的Java代码中需要使用 synchronized,以及 wait() 方法在 synchronized 块中的行为。

为什么加 synchronized

在多线程编程中,synchronized 关键字主要用于解决两个核心问题:

  1. 互斥 (Mutual Exclusion)

    • 当多个线程可能同时访问和修改共享数据时,就会产生竞争条件 (Race Condition)。这可能导致数据不一致或程序行为不可预测。
    • synchronized 可以确保在任何时刻,只有一个线程能够执行被 synchronized 关键字保护的代码块或方法。这个被保护的区域被称为“临界区”(Critical Section)。
    • 在我们的例子中,printNumbernumberletter 是共享数据。synchronized(lock) 确保了 printNumbers()printLetters() 方法中的临界区代码(检查 printNumber 标志、打印、更新计数器、切换标志、通知)不会被两个线程同时执行,从而避免了打印混乱或计数错误。
  2. 可见性 (Visibility)

    • 当一个线程修改了共享变量的值后,synchronized 可以确保这个修改对于其他后续获取同一个锁的线程是可见的。
    • 如果没有 synchronized(或者其他内存同步机制如 volatile),一个线程对共享变量的修改可能只存在于该线程的本地缓存中,其他线程可能看不到最新的值。
    • 在例子中,当一个线程修改 printNumber 标志后,synchronized 保证了另一个线程在获取锁之后能看到这个最新的 printNumber 值,从而正确判断是否轮到自己执行。

简单来说,不加 synchronized 的话:

  • 两个线程可能同时进入打印逻辑,导致输出顺序混乱,例如输出 12AB... 而不是 1A2B...
  • 一个线程修改了 printNumber 标志,另一个线程可能因为缓存原因看不到这个修改,导致它错误地继续等待或错误地开始打印。

synchronized wait() 时能解锁吗?

是的,wait() 方法在被调用时会释放它所持有的对象锁 (monitor lock)。

这是 wait() 方法一个非常关键且核心的行为,也是实现线程间协作(如生产者-消费者模式、交替打印)的基础。

具体来说,当一个线程在 synchronized(lock) 块中调用 lock.wait() 时,会发生以下事情:

  1. 释放锁:该线程会立即释放它在 lock 对象上持有的锁。这一点非常重要,因为如果不释放锁,其他线程就永远无法进入 synchronized(lock) 块,也就无法调用 lock.notify()lock.notifyAll() 来唤醒等待的线程,从而导致死锁。

  2. 进入等待状态:线程进入与 lock 对象关联的等待队列 (wait set) 中,并暂停执行。

  3. 等待唤醒:线程会一直保持等待状态,直到以下情况之一发生:

    • 另一个线程调用了同一个 lock 对象的 notify() 方法,并且等待的这个线程被选中唤醒。
    • 另一个线程调用了同一个 lock 对象的 notifyAll() 方法。
    • 线程被中断 (Thread.interrupt())。
    • (不太常见)发生“伪唤醒”(spurious wakeup),即线程在没有被明确通知的情况下被唤醒。这就是为什么 wait() 通常要放在一个 while 循环中进行条件检查。
  4. 重新获取锁:当线程被唤醒后,它并不会立即继续执行。它会尝试重新获取之前释放的 lock 对象上的锁。只有当它成功重新获取到锁之后,才能从 wait() 方法返回,并继续执行 synchronized 块中 wait() 调用之后的代码。

总结一下 wait() 和锁的关系:

  • 必须在 synchronized 块或方法中调用 wait() : 如果一个线程没有持有对象锁就调用该对象的 wait() 方法,会抛出 IllegalMonitorStateException
  • 调用 wait() 时自动释放锁:这是为了让其他线程有机会获取锁并改变条件,然后唤醒等待的线程。
  • wait() 返回前自动重新获取锁:确保线程在继续执行临界区代码时,依然持有锁,保证了数据的一致性。

在我们的交替打印例子中:

  • 当数字线程打印完一个数字后,它把 printNumber 置为 false,然后调用 lock.notifyAll() 唤醒可能在等待的字母线程。
  • 如果字母线程此时在 lock.wait() 处等待,它被唤醒后会尝试重新获取 lock。一旦获取成功,它会检查 printNumber(现在是 false),发现轮到自己了,于是打印字母,然后把 printNumber 置为 true,并调用 lock.notifyAll() 唤醒数字线程。
  • 数字线程接着做类似的事情。

这个机制保证了两个线程能够安全、有序地交替执行。

为何要捕获 Thread.interrupt()

Thread.interrupt() 是 Java 中用来中断一个线程的方法。然而,理解“中断”在这里的含义非常重要,它并不像听起来那样是强制停止一个线程。

Thread.interrupt() 的核心作用是设置目标线程的中断状态位 (interrupted status flag) 为 true

它本身并不会直接停止线程的执行。线程是否以及如何响应这个中断请求,取决于线程当前正在做什么以及它是如何编码来处理中断的。

以下是关于 Thread.interrupt() 的关键点:

  1. 设置中断状态

    • 调用 targetThread.interrupt() 会将 targetThread 的内部中断标志设置为 true
  2. 对阻塞操作的影响

    • 如果目标线程因为调用了某些特定的阻塞方法(如 Object.wait(), Thread.sleep(), Thread.join(), 以及 java.nio.channels.InterruptibleChannel 上的 I/O 操作,java.util.concurrent.locks.Lock.lockInterruptibly(), java.util.concurrent.BlockingQueue.take()/put() 等)而处于阻塞状态,那么:

      • 该线程的中断状态将被清除 (设置为 false)。
      • 该阻塞方法会立即抛出 InterruptedException
    • 这是处理中断最常见和推荐的方式。捕获 InterruptedException 后,线程可以决定如何响应,例如清理资源、提前结束任务等。

  3. 对正在运行或非阻塞I/O的线程的影响

    • 如果线程正在执行计算任务,或者正在执行不会抛出 InterruptedException 的阻塞 I/O 操作 (例如传统的 java.io 包中的流操作),那么调用 interrupt() 仅仅是设置了中断状态位。

    • 线程需要主动检查自己的中断状态,才能响应该中断请求。这可以通过以下两个方法实现:

      • Thread.currentThread().isInterrupted() : 返回当前线程的中断状态,但不清除中断状态。可以多次调用以检查状态。
      • Thread.interrupted() (静态方法) : 返回当前线程的中断状态,并且会清除中断状态 (将其重置为 false)。如果连续调用两次,第二次通常会返回 false (除非在两次调用之间线程再次被中断)。
  4. 不是强制停止

    • Thread.interrupt() 不像已废弃的 Thread.stop() 方法那样会粗暴地终止线程。Thread.stop() 非常危险,因为它会释放线程持有的所有锁,可能导致对象状态不一致。
    • Thread.interrupt() 是一种协作机制。被中断的线程有机会自行决定如何以及何时停止。

如何正确处理中断:

  • catch (InterruptedException e) 块中

    • 恢复中断状态 (推荐) :通常,当你捕获 InterruptedException 但不能立即完全处理它时(例如,在一个底层的库方法中),你应该通过调用 Thread.currentThread().interrupt() 来重新设置中断状态。这允许调用栈上更高层的方法也能感知到中断的发生。

      Java

      try {
          // some blocking operation like sleep() or wait()
          Thread.sleep(10000);
      } catch (InterruptedException e) {
          // Clean up if necessary
          System.out.println("Thread was interrupted during sleep/wait.");
          // Restore the interrupted status because this method is not the ultimate handler
          Thread.currentThread().interrupt();
          // Optionally rethrow or handle further
      }
      
    • 完成清理并退出:如果当前方法是任务的顶层控制逻辑,它可以执行必要的清理工作然后结束线程的执行(例如通过从 run() 方法返回)。

  • 在循环或长时间运行的任务中主动检查

    Java

    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted() && moreWorkToDo) {
            // do some work
            // ...
    
            // If no InterruptedException is thrown by methods like sleep(),
            // periodically check the interrupt status
            if (Thread.currentThread().isInterrupted()) {
                System.out.println("Thread has been interrupted, cleaning up and exiting.");
                // Perform cleanup
                break; // or return
            }
        }
        System.out.println("Thread finishing.");
    }
    

    或者使用 Thread.interrupted() 如果你希望在检查后清除状态:

    Java

    @Override
    public void run() {
        while (moreWorkToDo) {
            // do some work
            // ...
    
            if (Thread.interrupted()) { // Checks and clears the flag
                System.out.println("Thread has been interrupted, cleaning up and exiting.");
                // Perform cleanup
                break; // or return
            }
        }
        System.out.println("Thread finishing.");
    }
    

总结:

Thread.interrupt() 是一种礼貌的请求,通知一个线程它应该停止正在做的事情。线程可以通过响应 InterruptedException 或定期检查其中断状态来配合这个请求。它提供了一种比 Thread.stop() 更安全、更灵活的线程终止机制。