农行1面:Java如何保证线程T1,T2,T3 顺序执行?
作者:猿java(同公众号)
线程是 Java执行的最小单元,通常意义上来说,多个线程是为了加快速速且无需保序,这篇文章,我们来分析一道农行的面试题目:如要保证线程T1, T2, T3顺序执行?
考察意图
在面试中出现这道问题,通常是为了考察候选人的以下几个知识点:
1. 多线程基础知识: 希望了解候选人是否熟悉Java多线程的基本概念,包括线程的创建、启动和同步机制。
2. 同步机制的理解:候选人需要展示对Java中各种同步工具的理解,如join()、CountDownLatch、Semaphore、CyclicBarrier等,并知道如何在不同场景下应用这些工具。
3. 线程间通信:希望候选人理解线程间通信的基本原理,例如如何使用wait()和notify()来协调线程。
4. 对Java并发包的熟悉程度: 希望候选人了解Java并发包(java.util.concurrent)中的工具和类,展示其对现代Java并发编程的掌握。
保证线程顺序执行的方法
在分析完面试题的考察意图之后,我们再分析如何保证线程顺序执行,这里列举了几种常见的方式。
<顺便吆喝一句,技术大厂,前后端测试捞人,感兴趣可试试>
join()
join()方法是Thread类的一部分,可以让一个线程等待另一个线程完成执行。 当你在一个线程T上调用T.join()时,调用线程将进入等待状态,直到线程T完成(即终止)。因此,可以通过在每个线程启动后调用join()来实现顺序执行。
如下示例代码,展示了join()如何保证线程顺序执行:
Thread t1 = new Thread(() -> {// 线程T1的任务}); Thread t2 = new Thread(() -> {// 线程T2的任务}); Thread t3 = new Thread(() -> {// 线程T3的任务}); t1.start();t1.join(); // 等待t1完成 t2.start();t2.join(); // 等待t2完成 t3.start();t3.join(); // 等待t3完成
CountDownLatch
CountDownLatch通过一个计数器来实现,初始时,计数器的值由构造函数设置,每次调用countDown()方法,计数器的值减1。当计数器的值变为零时,所有等待在await()方法上的线程都将被唤醒,继续执行。
CountDownLatch是Java并发包(java.util.concurrent)中的一个同步辅助类,用于协调多个线程之间的执行顺序。它允许一个或多个线程等待另外一组线程完成操作。
如下示例代码,展示了CountDownLatch如何保证线程顺序执行:
CountDownLatch latch1 = new CountDownLatch(1); CountDownLatch latch2 = new CountDownLatch(1); Thread t1 = new Thread(() -> { // 线程T1的任务 latch1.countDown(); // 完成后递减latch1 }); Thread t2 = new Thread(() -> { try { latch1.await(); // 等待T1完成 // 线程T2的任务 latch2.countDown(); // 完成后递减latch2 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); Thread t3 = new Thread(() -> { try { latch2.await(); // 等待T2完成 // 线程T3的任务 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); t1.start(); t2.start(); t3.start();
CountDownLatch关键方法解析:
CountDownLatch(int count) : 构造函数,创建一个CountDownLatch实例,计数器的初始值为count。void await() : 使当前线程等待,直到计数器的值变为零。boolean await(long timeout, TimeUnit unit) : 使当前线程等待,直到计数器的值变为零或等待时间超过指定的时间。void countDown() : 递减计数器的值。当计数器的值变为零时,所有等待的线程被唤醒。
Semaphore
Semaphore通过一个计数器来管理许可,计数器的初始值由构造函数指定,表示可用许可的数量。线程可以通过调用acquire()方法请求许可,如果许可可用则授予访问权限,否则线程将阻塞。使用完资源后,线程调用release()方法释放许可,从而允许其他阻塞的线程获取许可。
如下示例代码,展示了Semaphore如何保证线程顺序执行:
Semaphore semaphore1 = new Semaphore(0); Semaphore semaphore2 = new Semaphore(0); Thread t1 = new Thread(() -> { // 线程T1的任务 semaphore1.release(); // 释放一个许可 }); Thread t2 = new Thread(() -> { try { semaphore1.acquire(); // 获取许可,等待T1完成 // 线程T2的任务 semaphore2.release(); // 释放一个许可 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); Thread t3 = new Thread(() -> { try { semaphore2.acquire(); // 获取许可,等待T2完成 // 线程T3的任务 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); t1.start(); t2.start(); t3.start();
Semaphore关键方法分析:
- Semaphore(int permits) :构造一个具有给定许可数的Semaphore。
- Semaphore(int permits, boolean fair) :构造一个具有给定许可数的Semaphore,并指定是否是公平的。公平性指的是线程获取许可的顺序是否是先到先得。
- void acquire() :获取一个许可,如果没有可用许可,则阻塞直到有许可可用。
- void acquire(int permits) :获取指定数量的许可。
- void release() :释放一个许可。
- void release(int permits) :释放指定数量的许可。
- int availablePermits() :返回当前可用的许可数量。
- boolean tryAcquire() :尝试获取一个许可,立即返回true或false。
- boolean tryAcquire(long timeout, TimeUnit unit) :在给定的时间内尝试获取一个许可。
单线程池
单线程池(Executors.newSingleThreadExecutor())可以确保任务按提交顺序依次执行。所有任务都会在同一个线程中运行,保证了顺序性。
如下示例代码展示了单线程池如何保证线程顺序执行:
ExecutorService executor = Executors.newSingleThreadExecutor(); executor.submit(new T1()); executor.submit(new T2()); executor.submit(new T3()); executor.shutdown();
单线程这种方法简单易用,适合需要顺序执行的场景。
synchronized
synchronized 是Java中的一个关键字,用于实现线程同步,确保多个线程对共享资源的访问是互斥的。它通过锁机制来保证同一时刻只有一个线程可以执行被Synchronized保护的代码块,从而避免数据不一致和线程安全问题。
如下示例代码,展示了synchronized如何保证线程顺序执行:
class Task { synchronized void executeTask(String taskName) { System.out.println(taskName + " 执行"); } } public class Main { public static void main(String[] args) { Task task = new Task(); new Thread(() -> task.executeTask("T1")).start(); new Thread(() -> task.executeTask("T2")).start(); new Thread(() -> task.executeTask("T3")).start(); } }
CompletableFuture
CompletableFuture是 Java 8 引入的类,属于 java.util.concurrent 包。它是一个功能强大的工具,用于处理异步编程。CompletableFuture 允许创建、操作和组合任务,可以说是处理异步任务的一个灵活和强大的解决方案。
如下示例代码,展示了Semaphore如何保证线程顺序执行:
public static void main(String[] args) { // 创建第一个任务T1 CompletableFuture<Void> future = CompletableFuture.runAsync(() -> { System.out.println("T1 is running on thread: " + Thread.currentThread().getName()); // 模拟任务运行 sleep(1000); }); // 链接任务T2到T1之后 future = future.thenRunAsync(() -> { System.out.println("T2 is running on thread: " + Thread.currentThread().getName()); sleep(1000); }); // 链接任务T3到T2之后 future = future.thenRunAsync(() -> { System.out.println("T3 is running on thread: " + Thread.currentThread().getName()); sleep(1000); }); // 等待所有任务完成 future.join(); } // 辅助方法用于模拟延迟 private static void sleep(long milliseconds) { try { Thread.sleep(milliseconds); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }
代码解释:
- runAsync 方法:使用 CompletableFuture.runAsync() 方法来异步执行任务。runAsync 不会返回任何结果,因此只在任务结束时传递控制权。
- 链式调用 thenRunAsync:使用 thenRunAsync 方法来在前一个任务完成后启动下一个任务。这样确保了任务按顺序执行。
- join 方法:join 方法等待 CompletableFuture 的计算完成。这相当于调用 get,但不会抛出检查异常。
总结
本文,我们分析了6种保证线程T1,T2,T3顺序执行的方法,依次如下:
- join()
- CountDownLatch
- Semaphore
- 单线程池
- synchronized
- CompletableFuture
在实际开发中,这种需要在业务代码中去保证线程执行顺序的情况几乎不会出现,因此,这个面试题其实缺乏实际的应用场景,纯粹是为了面试存在。尽管是面试题,还是可以帮助我们更好地去了解和掌握线程。