Java两个线程交替顺序打印,一个顺序打印12345,一个顺序打印ABCDE
Java
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
}
}
Key Concepts 🔑
-
synchronized (lock)
: This ensures that only one thread can execute the code block (printNumbers
orprintLetters
) at any given time. Thelock
object is shared between the two threads. -
volatile boolean printNumber
:volatile
ensures that changes toprintNumber
are immediately visible to all threads.- This boolean flag controls whose turn it is:
true
for the number-printing thread,false
for the letter-printing thread.
-
lock.wait()
: This method causes the current thread to release the lock and go into a waiting state until another thread callsnotify()
ornotifyAll()
on the same lock object. The thread waits inside awhile
loop to guard against spurious wakeups (where a thread might wake up without being explicitly notified). -
lock.notifyAll()
: This method wakes up all threads that are waiting on the same lock object. In this case, it wakes up the other thread so it can check if it's its turn to print. -
Thread.join()
: In themain
method,t1.join()
andt2.join()
are used to make the main thread wait untilt1
andt2
have finished their execution before printing the final newline and exiting.
When you run this code, the output will be: 1A2B3C4D5E
为什么加synchronized?synchronized wait时能解锁吗
Show thinking
好的,我们来解释一下为什么在之前的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()
Show thinking
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
scsstry { // 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
csharp@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
csharp@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()
更安全、更灵活的线程终止机制。