你的未来由你定义:不要让别人或环境决定你的命运,勇敢地掌握自己的人生方向。
即使世界抛弃了你,也不要自己放弃自己:任何时候都不要失去对生活的希望和对自己的信心。
目录
上一篇博客习题讲解
使用 synchronized 关键字同步非静态方法
静态同步方法与非静态同步方法的区别
线程安全问题及改进建议
ReentrantLock 相比 synchronized 的优势
第3章 多线程通信
3.1 wait()与notify()
3.1.1 阻塞当前线程
3.1.2 案例分析:厨师与侍者1
3.1.3 案例分析:厨师与侍者2
使用CountDownLatch管理厨师与侍者的协作
使用CyclicBarrier实现循环屏障
3.1.4 案例分析:两个线程交替输出信息
3.2 join()线程排队
3.2.1 加入者与休眠者
3.2.2 案例:紧急任务处理
3.2.3 join()限时阻塞
3.3 线程中断
3.3.1 中断运行态线程
3.3.2 中断阻塞态线程
3.3.3 如何停止线程
3.4 CountDownLatch计数器
3.5 CyclicBarrier屏障
3.5.1 案例:矩阵分行处理
3.5.2 案例:赛马游戏
3.6 Exchanger
3.7 Semaphore信号灯
3.8 死锁
3.8.1 案例:银行转账引发死锁
3.8.2 案例:哲学家就餐死锁
3.9 本章习题
加油加油!
上一篇博客习题讲解
Java多线程与线程池技术详解(二)
https://blog.csdn.net/speaking_me/article/details/144258233?spm=1001.2014.3001.5501
使用 synchronized
关键字同步非静态方法
public class Counter {private int count = 0;// Synchronized method to increment the counterpublic synchronized void increment() {count++;}public int getCount() {return count;} }
这段代码定义了一个简单的计数器类 Counter
,其中 increment()
方法被声明为同步方法。这意味着当一个线程调用此方法时,它会获得该对象的锁(即实例级别的锁),其他尝试访问同一个对象上任何同步方法的线程将被阻塞,直到当前线程释放锁。
静态同步方法与非静态同步方法的区别
- 静态同步方法:使用的是类级别的锁,意味着所有该类的实例共享同一把锁。如果一个线程进入了静态同步方法,那么不仅其他的线程不能进入这个静态同步方法,也不能进入该类中的其他静态同步方法。
- 非静态同步方法:使用的是对象级别的锁,每个对象有自己的锁。因此,不同对象上的非静态同步方法可以同时被执行。
示例代码:
public class Example {private static int staticCount = 0;private int instanceCount = 0;// Static synchronized method affecting all instancespublic static synchronized void incrementStatic() {staticCount++;}// Non-static synchronized method affecting only this instancepublic synchronized void incrementInstance() {instanceCount++;} }
线程安全问题及改进建议
对于提供的 Counter
类,确实存在线程安全问题。由于 count++
操作不是原子操作,它包含读取、增加和写入三个步骤,在高并发环境下可能会出现数据竞争条件,导致不一致的状态。以下是两种改进方案:
- 使用
synchronized
关键字:
public class Counter {private int count = 0;public synchronized void increment() {count++;}public synchronized int getCount() {return count;} }
- 使用
AtomicInteger
:
import java.util.concurrent.atomic.AtomicInteger;public class Counter {private AtomicInteger count = new AtomicInteger(0);public void increment() {count.incrementAndGet();}public int getCount() {return count.get();} }
ReentrantLock 相比 synchronized
的优势
在高并发情况下,ReentrantLock
提供了比 synchronized
更加灵活的锁定机制:
- 公平锁支持:
ReentrantLock
可以配置为公平锁,使得等待时间最长的线程优先获取锁。 - 可中断的锁等待:允许正在等待锁的线程被中断,从而避免死锁或长时间无响应的情况。
- 尝试获取锁:
ReentrantLock
提供了tryLock()
方法,可以让线程尝试获取锁而不必一直等待。 - 超时获取锁:提供了带有超时参数的方法,如
tryLock(long timeout, TimeUnit unit)
,这有助于防止线程无限期地等待锁。 - 更细粒度的控制:
ReentrantLock
允许程序员更精细地控制锁的行为,比如不同的锁条件等。
应用场景示例:
假设我们有一个任务调度系统,需要确保某些关键资源不会被多个线程同时访问。我们可以使用 ReentrantLock
来实现这一点,并且利用它的特性来优化性能和可靠性。例如,如果一个线程正在处理重要任务时突然收到中断请求,它可以立即停止工作并释放资源,而不会因为持有 synchronized
锁而被迫完成整个操作。
import java.util.concurrent.locks.ReentrantLock;public class TaskScheduler {private final ReentrantLock lock = new ReentrantLock();public void scheduleTask(Runnable task) {lock.lock();try {// Critical section: Schedule the task.task.run();} finally {lock.unlock();}} }
通过这种方式,我们能够更好地管理并发访问,同时提供更强大的功能来应对复杂的多线程环境。
第3章 多线程通信
3.1 wait()
与notify()
3.1.1 阻塞当前线程
在Java中,wait()
和notify()
方法是用于线程间通信的关键工具。它们允许一个线程等待另一个线程完成某个特定条件或事件的发生。wait()
方法会让调用它的线程进入等待状态,并释放它所持有的对象锁,直到其他线程调用了同一个对象上的notify()
或者notifyAll()
方法唤醒它。
当一个线程执行了wait()
后,它会放弃对共享资源(即对象)的锁定,并进入等待队列中。只有当其他线程调用了同一对象上的notify()
或notifyAll()
时,该线程才会被重新加入到可运行队列中,等待再次获得锁以继续执行。需要注意的是,wait()
、notify()
必须在同步上下文中使用,也就是说它们应该出现在由synchronized
关键字保护的方法或代码块内。
public class WaitNotifyExample {private static final Object lock = new Object();public static void main(String[] args) throws InterruptedException {Thread waiter = new Thread(() -> {synchronized (lock) {System.out.println("Waiting for notification...");try {lock.wait(); // 线程在此处等待} catch (InterruptedException e) {Thread.currentThread().interrupt();}System.out.println("Notified, continuing execution.");}});waiter.start();// 确保waiter线程先开始并进入等待状态Thread.sleep(100);Thread notifier = new Thread(() -> {synchronized (lock) {System.out.println("Notification sent.");lock.notify(); // 唤醒等待中的线程}});notifier.start();} }
这段代码展示了如何使用wait()
让一个线程暂停执行,直到另一个线程通过调用notify()
来通知它继续。注意这里使用了Thread.sleep()
来确保waiter
线程有足够的时间进入等待状态,然后再启动notifier
线程。
3.1.2 案例分析:厨师与侍者1
接下来我们来看一个实际的例子——厨师和侍者的协作问题。在这个场景中,厨师负责准备食物,而侍者则负责将食物送到顾客面前。为了保证服务的质量,我们需要确保厨师不会过早地准备过多的食物,同时也要避免侍者在没有准备好食物的情况下尝试送餐。这可以通过使用wait()
和notify()
来实现有效的协调。
public class ChefWaiterExample {private static final Object orderLock = new Object();private static Integer ordersToCook = 0; // 订单数量private static Integer dishesReady = 0; // 准备好的菜肴数量public static void main(String[] args) {// 厨师线程Thread chef = new Thread(() -> {while (true) {synchronized (orderLock) {if (ordersToCook == 0) {try {System.out.println("Chef is waiting for orders.");orderLock.wait(); // 如果没有订单,则厨师等待} catch (InterruptedException e) {Thread.currentThread().interrupt();}}System.out.println("Chef starts cooking an order.");ordersToCook--;dishesReady++;orderLock.notify(); // 告知侍者有新菜可以取走try {Thread.sleep(1000); // 模拟烹饪时间} catch (InterruptedException e) {Thread.currentThread().interrupt();}}}});// 侍者线程Thread waiter = new Thread(() -> {while (true) {synchronized (orderLock) {if (dishesReady == 0) {try {System.out.println("Waiter is waiting for dishes.");orderLock.wait(); // 如果没有准备好菜肴,则侍者等待} catch (InterruptedException e) {Thread.currentThread().interrupt();}}System.out.println("Waiter serves a dish.");dishesReady--;ordersToCook++; // 新订单到来orderLock.notify(); // 告知厨师有新订单需要处理try {Thread.sleep(500); // 模拟送餐时间} catch (InterruptedException e) {Thread.currentThread().interrupt();}}}});chef.start();waiter.start();} }
在这个例子中,厨师和侍者分别代表两个不同的线程,它们共享同一个锁对象orderLock
。每当厨师完成了菜肴的制作,就会调用notify()
告知侍者;同样地,当侍者完成了送餐任务,也会调用notify()
来提醒厨师新的订单已经到达。这种机制确保了两者之间的工作流程顺畅且有序。
3.1.3 案例分析:厨师与侍者2
在多线程通信的场景中,当业务逻辑变得更加复杂时,例如存在多个厨师和多个侍者的情况,我们需要确保每个厨师都可以独立工作,并且每个侍者能够从多个厨师那里接收菜品。为了实现这一目标,可以引入CountDownLatch
或CyclicBarrier
等高级同步工具来简化多线程间的协调工作。
使用CountDownLatch
管理厨师与侍者的协作
CountDownLatch
是一个同步辅助类,它允许一个或多个线程一直等待,直到其他线程执行了一组操作。在这个例子中,我们可以用CountDownLatch
来协调厨师准备食物的时间点以及侍者服务顾客的时间点。具体来说,我们可以为每一批订单设置一个CountDownLatch
实例,其计数值等于参与烹饪该批订单的所有厨师的数量。每当一个厨师完成自己的部分任务后,就会调用countDown()
方法减少计数器值;而侍者则会在开始送餐之前调用await()
方法,直到所有厨师都完成了他们的工作。
import java.util.concurrent.CountDownLatch;public class ChefWaiterExampleWithLatch {private static final int NUM_CHEFS = 3; // 假设有3个厨师private static final int NUM_WAITERS = 2; // 假设有2个侍者private static final CountDownLatch chefLatch = new CountDownLatch(NUM_CHEFS);public static void main(String[] args) throws InterruptedException {// 创建并启动厨师线程for (int i = 0; i < NUM_CHEFS; ++i) {new Thread(new Chef(i + 1)).start();}// 创建并启动侍者线程for (int i = 0; i < NUM_WAITERS; ++i) {new Thread(new Waiter()).start();}// 主线程等待所有厨师完成准备工作chefLatch.await();System.out.println("All chefs have finished their preparations.");}static class Chef implements Runnable {private final int id;Chef(int id) {this.id = id;}@Overridepublic void run() {try {// 模拟厨师准备菜品的过程System.out.println("Chef " + id + " is preparing...");Thread.sleep((long) (Math.random() * 5000)); // 随机时间模拟不同的准备速度System.out.println("Chef " + id + " has completed preparation.");// 准备完成后通知CountDownLatchchefLatch.countDown();} catch (InterruptedException e) {Thread.currentThread().interrupt();}}}static class Waiter implements Runnable {@Overridepublic void run() {try {// 等待所有厨师完成准备工作chefLatch.await();// 开始送餐给顾客System.out.println("Waiter serving dishes...");} catch (InterruptedException e) {Thread.currentThread().interrupt();}}} }
在这段代码中,我们创建了三个厨师线程和两个侍者线程。每个厨师线程在完成自己的准备工作之后都会调用chefLatch.countDown()
,而每个侍者线程则会在开始送餐前调用chefLatch.await()
以确保所有的厨师都已经完成了他们的准备工作。这种方式保证了即使有多个厨师同时工作,也能有序地进行菜品准备和服务流程。
使用CyclicBarrier
实现循环屏障
相比之下,CyclicBarrier
也是一种同步辅助类,但它允许多个线程相互等待对方达到某个共同点(称为屏障)。一旦所有参与的线程都达到了屏障,它们就可以一起继续前进。此外,CyclicBarrier
还可以复用,也就是说它可以在一次使用完毕后重置,以便后续再次使用。这非常适合用来处理如厨房中的情况,即每一轮的服务结束后,所有的厨师和侍者都需要重新回到起点,准备下一轮的服务。
import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier;public class ChefWaiterExampleWithBarrier {private static final int NUM_PARTIES = 5; // 总共需要等待的参与者数量(厨师+侍者)private static final CyclicBarrier barrier = new CyclicBarrier(NUM_PARTIES, () -> {System.out.println("All parties have reached the barrier, proceeding to next phase.");});public static void main(String[] args) {// 创建并启动厨师线程for (int i = 0; i < 3; ++i) {new Thread(new Chef(i + 1)).start();}// 创建并启动侍者线程for (int i = 0; i < 2; ++i) {new Thread(new Waiter()).start();}}static class Chef implements Runnable {private final int id;Chef(int id) {this.id = id;}@Overridepublic void run() {try {// 模拟厨师准备菜品的过程System.out.println("Chef " + id + " is preparing...");Thread.sleep((long) (Math.random() * 5000)); // 随机时间模拟不同的准备速度System.out.println("Chef " + id + " has completed preparation.");// 到达屏障点barrier.await();} catch (InterruptedException | BrokenBarrierException e) {Thread.currentThread().interrupt();}}}static class Waiter implements Runnable {@Overridepublic void run() {try {// 等待所有厨师完成准备工作System.out.println("Waiter waiting at the barrier...");barrier.await();// 开始送餐给顾客System.out.println("Waiter serving dishes...");} catch (InterruptedException | BrokenBarrierException e) {Thread.currentThread().interrupt();}}} }
这段代码展示了如何利用CyclicBarrier
来同步厨师和侍者的工作流程。每当一组厨师和侍者完成各自的任务并且到达屏障时,他们会触发barrier.await()
,从而阻塞在那里,直到所有人都到达。之后,他们会一起继续下一步骤。由于CyclicBarrier
支持复用特性,因此它可以被用于多次循环的服务过程中,每次服务结束后的重置使得它可以持续有效地管理工作流。
通过上述两种方式——CountDownLatch
和CyclicBarrier
——我们可以更加灵活地管理和优化多线程环境下的厨师与侍者之间的协作关系,确保即使面对复杂的业务逻辑也能够保持高效稳定的运作状态。这两种工具不仅简化了程序员编写并发程序的工作量,而且提高了系统的可靠性和性能。
3.1.4 案例分析:两个线程交替输出信息
有时候我们需要两个线程按照一定的顺序交替打印消息。例如,一个线程负责打印奇数,另一个线程负责打印偶数。为了实现这一点,我们可以利用wait()
和notify()
来控制线程之间的切换:
public class AlternatingPrint {private static final Object printLock = new Object();private static boolean printOdd = true;public static void main(String[] args) {Thread oddPrinter = new Thread(() -> {for (int i = 1; i <= 10; i += 2) {synchronized (printLock) {while (!printOdd) {try {printLock.wait();} catch (InterruptedException e) {Thread.currentThread().interrupt();}}System.out.println(Thread.currentThread().getName() + ": " + i);printOdd = false;printLock.notify();}}});Thread evenPrinter = new Thread(() -> {for (int i = 2; i <= 10; i += 2) {synchronized (printLock) {while (printOdd) {try {printLock.wait();} catch (InterruptedException e) {Thread.currentThread().interrupt();}}System.out.println(Thread.currentThread().getName() + ": " + i);printOdd = true;printLock.notify();}}});oddPrinter.setName("Odd Printer");evenPrinter.setName("Even Printer");oddPrinter.start();evenPrinter.start();} }
此段代码定义了两个线程,分别用来打印奇数和偶数。通过共享变量printOdd
来决定哪个线程应该打印下一个数字,并且每次打印之后都会改变这个标志位的状态,然后调用notify()
唤醒另一个线程进行下一轮打印。
3.2 join()
线程排队
join()
方法允许一个线程等待另一个线程完成其执行。这意味着调用join()
的线程会被阻塞,直到目标线程结束为止。这对于确保某些任务按顺序完成非常有用,特别是在你需要等待一组子任务全部完成后才能继续下一步操作的时候。
3.2.1 加入者与休眠者
考虑这样一个场景:主线程启动了一系列辅助线程去完成各自的任务,但主线程希望在所有辅助线程都结束后再继续自己的工作。这时就可以使用join()
方法让主线程等待这些辅助线程。
public class JoinExample {public static void main(String[] args) throws InterruptedException {Thread sleeper = new Thread(() -> {try {System.out.println("Sleeper thread starting to sleep.");Thread.sleep(2000);System.out.println("Sleeper thread finished sleeping.");} catch (InterruptedException e) {Thread.currentThread().interrupt();}});sleeper.start();System.out.println("Main thread waits for sleeper thread.");sleeper.join(); // 主线程等待sleeper线程完成System.out.println("Sleeper thread has completed.");} }
在这个例子中,main
线程启动了一个名为sleeper
的线程,并通过调用join()
确保自己会在sleeper
线程结束后才继续执行。这样做的好处是可以保证某些依赖关系得到正确处理,比如文件读写、网络请求等并发操作后的数据一致性检查。
3.2.2 案例:紧急任务处理
在多线程应用程序中,当遇到需要立即响应的紧急情况时,如何优雅地停止当前正在进行的任务,并迅速转向处理新的紧急消息是一个关键问题。Java 提供了 interrupt()
方法作为线程间通信的一种方式,允许一个线程向另一个线程发送中断信号。结合 join()
方法,可以有效地实现对紧急任务的快速响应。
使用 interrupt()
和 join()
实现紧急任务处理
interrupt()
方法的作用是设置目标线程的中断状态位,但不会直接终止该线程。这意味着被中断的线程仍然有机会清理资源或执行其他必要的操作,以确保安全退出。对于正在阻塞(如调用 sleep()
, wait()
, 或 join()
)中的线程,interrupt()
将导致这些方法抛出 InterruptedException
,同时清除中断状态位。因此,在处理紧急消息时,我们可以通过检查中断状态或者捕获异常来决定何时以及如何结束当前任务。
另一方面,join()
方法可以让主线程等待子线程完成其执行。如果我们想要确保所有非紧急任务都已经结束,那么可以在接收到紧急消息后调用 join()
来同步线程的状态。这有助于避免未完成的工作遗留下来,同时也保证了紧急任务能够在尽可能短的时间内得到处理。
下面是一个简单的例子,展示了如何使用 interrupt()
和 join()
来处理紧急消息:
public class EmergencyTaskHandler {public static void main(String[] args) throws InterruptedException {// 创建并启动一个模拟常规工作的线程Thread regularWorker = new Thread(() -> {try {while (!Thread.currentThread().isInterrupted()) {System.out.println("Performing regular task...");Thread.sleep(1000); // 模拟工作延迟}System.out.println("Regular worker interrupted.");} catch (InterruptedException e) {// 清理资源并重置中断标志System.out.println("Caught InterruptedException, cleaning up resources.");Thread.currentThread().interrupt();}});regularWorker.start();// 模拟一段时间后接收到了紧急消息Thread.sleep(5000);System.out.println("Received an emergency message!");// 中断常规工作的线程,并尝试加入它直到完成regularWorker.interrupt();regularWorker.join(); // 等待常规工作者线程结束// 开始处理紧急消息handleEmergencyMessage();}private static void handleEmergencyMessage() {System.out.println("Handling emergency message...");// 这里可以放置紧急任务的具体逻辑} }
在这个示例中,regularWorker
是一个代表常规工作的线程,它会不断地打印消息并休眠一秒。一旦接收到紧急消息,我们会调用 interrupt()
来通知 regularWorker
停止工作,并通过 join()
来确保主线程等待 regularWorker
完成任何必要的清理工作后再继续。如果 regularWorker
在被中断时正处于休眠状态,则会抛出 InterruptedException
,并且我们可以在此处添加适当的清理代码。此外,我们还应该记得重新设置中断标志,以便后续代码能够正确识别到线程已被中断。
值得注意的是,虽然 interrupt()
提供了一种优雅的方式来请求线程停止工作,但它并不强制线程立即停止。实际上,是否以及如何响应中断取决于线程自身的实现。因此,在设计程序时,应当考虑到这一点,并为可能的情况做好准备,例如定期检查中断状态或编写能够妥善处理 InterruptedException
的代码。
总之,通过合理运用 interrupt()
和 join()
,我们可以构建更加健壮和响应迅速的应用程序,尤其是在面对突发性的高优先级任务时。这种方法不仅提高了系统的灵活性,而且也增强了用户体验,因为用户不必长时间等待低优先级任务完成即可获得重要信息或服务。
3.2.3 join()
限时阻塞
有时你可能不想无限期地等待另一个线程完成,而是设置一个超时时间。如果指定时间内目标线程还没有结束,那么调用join(long millis)
的线程将会恢复执行。这对于防止程序陷入长时间等待是非常重要的。
3.3 线程中断
3.3.1 中断运行态线程
在线程正常运行期间,可以通过调用interrupt()
方法来请求中断该线程。然而,是否真的能够成功中断取决于线程本身的行为以及它当时所处的状态。例如,如果线程正处于I/O操作或者处于sleep()
状态下,那么它可以立即响应中断并抛出InterruptedException
异常。
3.3.2 中断阻塞态线程
当线程处于阻塞状态(如等待获取锁、调用wait()
、sleep()
、join()
等),调用interrupt()
会导致线程抛出InterruptedException
,从而提前退出阻塞状态。因此,在编写多线程应用时,应当妥善处理这种异常,以确保程序能够安全地终止线程。
3.3.3 如何停止线程
虽然Java提供了stop()
方法来强制终止线程,但由于这种方法可能会导致不一致的状态或资源泄露,所以并不推荐使用。相反,我们应该采用协作式的方式停止线程,即设置一个标志位,让线程定期检查这个标志位,一旦发现被置为false
就自行退出循环。
3.4 CountDownLatch
计数器
CountDownLatch
是一个同步辅助类,它允许一个或多个线程一直等待,直到其他线程执行了一组操作。它非常适合用来协调多个线程之间的启动和完成。
3.5 CyclicBarrier
屏障
CyclicBarrier
也是一种同步辅助类,但它允许多个线程相互等待对方达到某个共同点(称为屏障)。一旦所有参与的线程都达到了屏障,它们就可以一起继续前进。此外,CyclicBarrier
还可以复用,也就是说它可以在一次使用完毕后重置,以便后续再次使用。
3.5.1 案例:矩阵分行处理
在一个大型矩阵计算任务中,为了提高效率并充分利用多核处理器的能力,我们可以将矩阵分成若干行,然后分配给不同的线程进行并行处理。使用 CyclicBarrier
可以确保所有负责不同行的线程在完成各自的部分之前不会继续前进,从而保证整个矩阵的数据一致性。
矩阵分行处理的具体实现
假设我们有一个 m×nm×n 的矩阵需要执行某种复杂的运算,比如矩阵乘法或者某些特定的变换操作。如果直接在一个单线程环境中处理这样的任务,可能会因为数据量庞大而导致较长的计算时间。为了解决这个问题,可以采用分治策略,即将矩阵按照行分割成多个子任务,并行地交给不同的线程去处理。
每个线程负责处理矩阵的一行或多行,当所有线程都完成了它们的任务后,再合并结果得到最终的答案。这里的关键在于如何协调这些线程的工作,使得它们能够同步地开始和结束各自的计算步骤。CyclicBarrier
正是这样一个理想的工具,它允许一组线程相互等待,直到所有线程都到达了一个共同的屏障点(barrier point),之后它们可以一起继续执行后续的操作。
例如,在矩阵乘法的问题中,如果我们有两个矩阵 A 和 B 需要相乘生成矩阵 C,那么我们可以把矩阵 A 的每一行与矩阵 B 的列向量相乘的结果分别交给不同的线程来计算。一旦某个线程完成了它的部分计算,它就会调用 await()
方法等待其他线程也完成相应的工作。只有当所有的线程都达到了屏障点时,才会触发下一步的动作,如汇总各个线程计算出的部分结果,形成完整的矩阵 C。
import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier;public class MatrixRowProcessor {private static final int ROWS = /* 矩阵A的行数 */;private static final int COLS = /* 矩阵B的列数 */;private static final int THREAD_COUNT = /* 线程数量 */;public static void main(String[] args) throws InterruptedException, BrokenBarrierException {// 初始化矩阵A、B以及结果矩阵Cfloat[][] a = new float[ROWS][/* 矩阵A的列数 */];float[][] b = new float[/* 矩阵B的行数 */][COLS];float[][] c = new float[ROWS][COLS];// 创建CyclicBarrier实例,设置参与线程数及屏障动作CyclicBarrier barrier = new CyclicBarrier(THREAD_COUNT, () -> {System.out.println("All threads have completed their computations.");// 这里可以添加额外的逻辑,例如检查或合并部分结果});// 启动线程池中的线程,每个线程处理一部分矩阵行for (int i = 0; i < THREAD_COUNT; ++i) {new Thread(new RowComputationTask(a, b, c, i, barrier)).start();}}static class RowComputationTask implements Runnable {private final float[][] a;private final float[][] b;private final float[][] c;private final int rowIdx;private final CyclicBarrier barrier;RowComputationTask(float[][] a, float[][] b, float[][] c, int rowIdx, CyclicBarrier barrier) {this.a = a;this.b = b;this.c = c;this.rowIdx = rowIdx;this.barrier = barrier;}@Overridepublic void run() {try {// 计算指定行与其他矩阵列的乘积,并存入结果矩阵for (int col = 0; col < c[rowIdx].length; ++col) {for (int k = 0; k < a[rowIdx].length; ++k) {c[rowIdx][col] += a[rowIdx][k] * b[k][col];}}// 完成计算后等待其他线程barrier.await();} catch (InterruptedException | BrokenBarrierException e) {Thread.currentThread().interrupt();System.err.println("Thread was interrupted or barrier was broken.");}}} }
在这个例子中,我们定义了一个名为 MatrixRowProcessor
的类,其中包含了主方法和一个内部类 RowComputationTask
。主方法负责初始化矩阵和创建 CyclicBarrier
实例,而 RowComputationTask
则实现了具体的矩阵行计算逻辑。每当一个线程完成了它的任务,它就会调用 barrier.await()
来等待其他线程也完成工作。通过这种方式,我们可以确保所有线程都在同一时间点上完成计算,并且只有当所有线程都准备好时才会继续执行下一个阶段的操作。
3.5.2 案例:赛马游戏
赛马游戏中,每匹马都是一个独立的线程,它们同时起跑并在各自的跑道上奔跑。我们可以使用 CyclicBarrier
来模拟比赛规则,即所有马匹必须同时到达终点线,然后才能宣布获胜者。这种方式不仅增加了游戏的真实感,而且也简化了编程逻辑。
赛马游戏的实现细节
为了实现赛马游戏中的同步机制,CyclicBarrier
是一个非常适合的选择。它可以用来确保所有代表马匹的线程在同一时刻开始赛跑,并且只有当所有马匹都到达了终点之后,程序才会继续进行下一步,比如宣布比赛结果。
具体来说,我们可以为每一轮比赛创建一个新的 CyclicBarrier
对象,其计数值等于参赛马匹的数量。每当一匹马到达终点时,它会调用 await()
方法等待其他马匹也达到终点。一旦所有马匹都到达了终点,CyclicBarrier
将解除阻塞状态,允许所有线程继续执行。此外,还可以为 CyclicBarrier
设置一个可选的屏障动作(barrier action),该动作会在所有线程到达屏障点之后自动执行,例如打印比赛结果或重置计数器以便下一轮比赛。
下面是一个简化的代码示例,展示了如何使用 CyclicBarrier
来控制赛马游戏中的同步:
import java.util.Random; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier;public class HorseRaceGame {private static final int HORSE_COUNT = 5; // 假设有5匹马参加比赛public static void main(String[] args) throws InterruptedException, BrokenBarrierException {// 创建CyclicBarrier实例,设置参与线程数及屏障动作CyclicBarrier barrier = new CyclicBarrier(HORSE_COUNT, () -> {System.out.println("All horses have reached the finish line!");// 这里可以添加额外的逻辑,例如确定获胜者});// 启动赛马线程for (int i = 0; i < HORSE_COUNT; ++i) {new Thread(new Horse(i + 1, barrier)).start();}// 主线程等待一段时间让比赛开始Thread.sleep(1000);System.out.println("The race has started...");}static class Horse implements Runnable {private final int id;private final CyclicBarrier barrier;Horse(int id, CyclicBarrier barrier) {this.id = id;this.barrier = barrier;}@Overridepublic void run() {try {Random random = new Random();// 模拟赛马过程,随机花费时间到达终点long startTime = System.currentTimeMillis();Thread.sleep(random.nextInt(5000)); // 模拟赛马所需的时间long endTime = System.currentTimeMillis();System.out.printf("Horse %d finished in %d ms.\n", id, (endTime - startTime));// 到达终点后等待其他马匹barrier.await();} catch (InterruptedException | BrokenBarrierException e) {Thread.currentThread().interrupt();System.err.println("Horse was interrupted or barrier was broken.");}}} }
在这段代码中,我们定义了一个名为 HorseRaceGame
的类,它包含了主方法和一个内部类 Horse
。主方法负责创建 CyclicBarrier
实例,并启动多个代表马匹的线程。每个 Horse
实例在其 run()
方法中模拟了一次赛马的过程,包括随机生成赛马所需的时间,然后调用 barrier.await()
等待其他马匹也到达终点。一旦所有马匹都到达了终点,CyclicBarrier
将解除阻塞状态,允许所有线程继续执行,并可能触发预设的屏障动作,如宣布比赛结果。
通过这种方式,我们可以有效地利用 CyclicBarrier
来实现赛马游戏中所需的同步行为,既保证了比赛的真实性,又简化了编程逻辑。这种方法同样适用于其他需要多个线程协同工作的场景,如多人在线游戏或其他类型的分布式系统。
3.6 Exchanger
Exchanger
是一种特殊的同步工具,它允许一对线程交换它们持有的对象。这对于需要在两个线程之间传递数据的应用非常有用,尤其是在双方都需要更新彼此的状态时。
3.7 Semaphore
信号灯
Semaphore
提供了一种限制访问某种资源数量的方式。它类似于现实生活中的交通信号灯,可以根据设定的许可数目控制多少个线程可以同时访问资源。这对于管理有限资源(如数据库连接池)特别有用。
3.8 死锁
死锁是指两个或更多线程互相持有对方所需要的资源,结果谁也无法继续下去。这种情况通常发生在多个线程试图以不同的顺序获取相同的资源时发生。为了避免死锁,我们应该尽量减少锁的数量,并遵循固定的加锁顺序。
3.8.1 案例:银行转账引发死锁
在设计银行系统时,我们必须小心处理账户之间的转账操作,因为不当的加锁策略可能导致死锁。例如,两个线程分别尝试从A账户转到B账户和从B账户转到A账户,如果它们以相反的顺序获取锁,就可能发生死锁。为了避免这种情况,我们可以规定所有转账操作都必须按照账户ID从小到大的顺序加锁。
3.8.2 案例:哲学家就餐死锁
经典的哲学家就餐问题是用来说明死锁的经典案例之一。五位哲学家围坐在一张圆桌旁,每个人面前有一碗面条和一双筷子。每位哲学家要么思考要么吃饭,但是要吃饭就必须拿到左右两边的筷子。如果每个哲学家都拿起左边的筷子并等待右边的筷子,那么他们将永远无法进食,形成死锁。解决这个问题的一个方法是打破对称性,比如规定只有一位哲学家先拿右边的筷子。
3.9 本章习题
为了巩固所学知识,建议读者完成以下练习:
- 编写一个简单的生产者-消费者模式程序,其中生产者向缓冲区添加元素,而消费者从中移除元素。请确保生产者不会过度填充缓冲区,而消费者也不会从空的缓冲区读取。
- 实现一个多阶段流水线作业,其中每个阶段由一个独立的线程完成,前一阶段的结果作为下一阶段的输入。请使用适当的同步机制保证各个阶段按序执行。
- 设计一个支持动态调整线程数量的线程池框架,并实现基本的任务分发功能。请考虑如何有效管理线程生命周期以及任务队列。
- 分析并修复一段包含潜在死锁风险的代码片段。请解释为什么会出现死锁,并提出改进措施。
- 探索
LockSupport.park()
和unpark()
的用法,了解它们是如何实现低级别的线程阻塞和唤醒机制的。