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)
:这确保了在任何给定时间只有一个线程可以执行代码块(printNumbers
或printLetters
)。lock
对象在两个线程之间共享。volatile boolean printNumber
:volatile
确保了printNumber
的更改对所有线程立即可见。 这个布尔标志控制轮到哪个线程执行:true
表示轮到打印数字的线程,false
表示轮到打印字母的线程。lock.wait()
:此方法使当前线程释放锁并进入等待状态,直到另一个线程对同一锁对象调用notify()
或notifyAll()
。线程在一个while
循环内等待,以防止虚假唤醒(即线程可能在没有被明确通知的情况下唤醒)。lock.notifyAll()
:此方法唤醒所有在同一锁对象上等待的线程。在这种情况下,它会唤醒另一个线程,以便它可以检查是否轮到自己打印。Thread.join()
:在main
方法中,使用t1.join()
和t2.join()
来使主线程等待,直到t1
和t2
完成执行后,才打印最后的换行符并退出。
为什么加synchronized?synchronized wait时能解锁吗
来解释一下为什么在之前的Java代码中需要使用 synchronized
,以及 wait()
方法在 synchronized
块中的行为。
为什么加 synchronized
?
在多线程编程中,synchronized
关键字主要用于解决两个核心问题:
-
互斥 (Mutual Exclusion) :
- 当多个线程可能同时访问和修改共享数据时,就会产生竞争条件 (Race Condition)。这可能导致数据不一致或程序行为不可预测。
synchronized
可以确保在任何时刻,只有一个线程能够执行被synchronized
关键字保护的代码块或方法。这个被保护的区域被称为“临界区”(Critical Section)。- 在我们的例子中,
printNumber
、number
和letter
是共享数据。synchronized(lock)
确保了printNumbers()
和printLetters()
方法中的临界区代码(检查printNumber
标志、打印、更新计数器、切换标志、通知)不会被两个线程同时执行,从而避免了打印混乱或计数错误。
-
可见性 (Visibility) :
- 当一个线程修改了共享变量的值后,
synchronized
可以确保这个修改对于其他后续获取同一个锁的线程是可见的。 - 如果没有
synchronized
(或者其他内存同步机制如volatile
),一个线程对共享变量的修改可能只存在于该线程的本地缓存中,其他线程可能看不到最新的值。 - 在例子中,当一个线程修改
printNumber
标志后,synchronized
保证了另一个线程在获取锁之后能看到这个最新的printNumber
值,从而正确判断是否轮到自己执行。
- 当一个线程修改了共享变量的值后,
简单来说,不加 synchronized
的话:
- 两个线程可能同时进入打印逻辑,导致输出顺序混乱,例如输出
12AB...
而不是1A2B...
。 - 一个线程修改了
printNumber
标志,另一个线程可能因为缓存原因看不到这个修改,导致它错误地继续等待或错误地开始打印。
synchronized wait()
时能解锁吗?
是的,wait()
方法在被调用时会释放它所持有的对象锁 (monitor lock)。
这是 wait()
方法一个非常关键且核心的行为,也是实现线程间协作(如生产者-消费者模式、交替打印)的基础。
具体来说,当一个线程在 synchronized(lock)
块中调用 lock.wait()
时,会发生以下事情:
-
释放锁:该线程会立即释放它在
lock
对象上持有的锁。这一点非常重要,因为如果不释放锁,其他线程就永远无法进入synchronized(lock)
块,也就无法调用lock.notify()
或lock.notifyAll()
来唤醒等待的线程,从而导致死锁。 -
进入等待状态:线程进入与
lock
对象关联的等待队列 (wait set) 中,并暂停执行。 -
等待唤醒:线程会一直保持等待状态,直到以下情况之一发生:
- 另一个线程调用了同一个
lock
对象的notify()
方法,并且等待的这个线程被选中唤醒。 - 另一个线程调用了同一个
lock
对象的notifyAll()
方法。 - 线程被中断 (
Thread.interrupt()
)。 - (不太常见)发生“伪唤醒”(spurious wakeup),即线程在没有被明确通知的情况下被唤醒。这就是为什么
wait()
通常要放在一个while
循环中进行条件检查。
- 另一个线程调用了同一个
-
重新获取锁:当线程被唤醒后,它并不会立即继续执行。它会尝试重新获取之前释放的
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()
的关键点:
-
设置中断状态:
- 调用
targetThread.interrupt()
会将targetThread
的内部中断标志设置为true
。
- 调用
-
对阻塞操作的影响:
-
如果目标线程因为调用了某些特定的阻塞方法(如
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
后,线程可以决定如何响应,例如清理资源、提前结束任务等。
-
-
对正在运行或非阻塞I/O的线程的影响:
-
如果线程正在执行计算任务,或者正在执行不会抛出
InterruptedException
的阻塞 I/O 操作 (例如传统的java.io
包中的流操作),那么调用interrupt()
仅仅是设置了中断状态位。 -
线程需要主动检查自己的中断状态,才能响应该中断请求。这可以通过以下两个方法实现:
Thread.currentThread().isInterrupted()
: 返回当前线程的中断状态,但不清除中断状态。可以多次调用以检查状态。Thread.interrupted()
(静态方法) : 返回当前线程的中断状态,并且会清除中断状态 (将其重置为false
)。如果连续调用两次,第二次通常会返回false
(除非在两次调用之间线程再次被中断)。
-
-
不是强制停止:
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()
更安全、更灵活的线程终止机制。