📌 ThreadLocal
🔹 基础知识
-
多线程是Java实现多任务的基础。
-
Thread
对象代表一个线程。 -
在代码中可以调用
Thread.currentThread()
来获取当前线程。public class Main { public static void main(String[] args) throws Exception { log("start main..."); new Thread(() -> { log("run task..."); }).start(); new Thread(() -> { log("print..."); }).start(); log("end main."); } static void log(String s) { System.out.println(`Thread`.currentThread().getName() + ": " + s); } }
🔹 线程池和任务
-
Java标准库提供的线程池方便地执行多任务并复用线程。
-
Web应用程序是典型的多任务应用。
public void process(User user) { checkPermission(); doWork(); saveStatus(); sendResponse(); }
🔹 上下文问题
- 如何在一个线程内传递状态?例如:
User
实例。 - 给每个方法增加一个上下文参数可能很麻烦。
- 如果调用链包含无法修改源码的第三方库,
User
对象就传不进去了。
🔹 ThreadLocal
介绍
-
Java标准库提供了特殊的
ThreadLocal
。 -
它可以在一个线程中传递同一个对象。
-
ThreadLocal
实例通常作为静态字段初始化。static ThreadLocal<User> threadLocalUser = new ThreadLocal<>();
🔹 使用方法
- 设置一个实例与
ThreadLocal
关联。 - 在移除之前,所有方法都可以获取该实例。
- 在同一个线程中,各个方法获取的实例都是同一个。
🔹 原理
- 可以把
ThreadLocal
看作一个全局Map<Thread, Object>
。 - 每个线程获取
ThreadLocal
变量时,总是使用Thread
自身作为key。 - 因此,它给每个线程都提供了一个独立的存储空间。
🔹 注意事项
-
ThreadLocal
一定要在finally
中清除。try { threadLocalUser.set(user); ... } finally { threadLocalUser.remove(); }
⚠️ 注意: 如果不清除 ThreadLocal
,当线程重新放入线程池执行其他代码时,会携带上一次的状态。
🔹 自动关闭
-
为了确保释放
ThreadLocal
关联的实例,可以使用AutoCloseable
接口配合try (resource) {...}
结构。public class UserContext implements AutoCloseable { static final ThreadLocal<String> ctx = new ThreadLocal<>(); public UserContext(String user) { ctx.set(user); } public static String currentUser() { return ctx.get(); } @Override public void close() { ctx.remove(); } }
🔹 如何使用
try (var ctx = new UserContext("Bob")) {
String currentUser = UserContext.currentUser();
}
📢 小结
ThreadLocal
是线程的“局部变量”,确保每个线程的ThreadLocal
变量都是独立的。ThreadLocal
适用于线程的处理流程中保持上下文。- 使用
ThreadLocal
时,务必在finally
中清除。
📌 线程池
在Java中,线程池是使用预先创建的线程集来执行任务的一种机制。它提供了更好的资源管理和响应能力,特别是在大量的短生命周期任务需要执行时。下面我们将深入探讨与线程池相关的三个关键类。
1. 📜 Executors
- 📍 定义:
Executors
是一个工厂类,提供了一系列静态方法来创建不同类型的线程池。 - 📍 主要方法:
newFixedThreadPool(int nThreads)
: 创建一个固定大小的线程池。newCachedThreadPool()
: 创建一个根据需要创建新线程的线程池。newSingleThreadExecutor()
: 创建一个只有一个线程的线程池。newScheduledThreadPool(int corePoolSize)
: 创建一个可以调度命令在给定延迟后运行或定期执行的线程池。
- 📍 示例:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5); fixedThreadPool.execute(() -> { System.out.println("Task executed in fixed thread pool."); });
2. 📜 ThreadPoolExecutor
- 📍 定义:
ThreadPoolExecutor
是ExecutorService
接口的直接实现,它提供了更为灵活和强大的线程池配置选项。 - 📍 主要参数:
corePoolSize
: 核心线程数,即始终存在的线程数(除非设置了allowCoreThreadTimeOut
)。maximumPoolSize
: 线程池中允许的最大线程数。keepAliveTime
: 当线程数超过核心线程数时,这是多余的空闲线程等待新任务的最长时间。workQueue
: 用于保存等待执行的任务的阻塞队列。
- 📍 示例:
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 5, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10)); executor.execute(() -> { System.out.println("Task executed in ThreadPoolExecutor."); });
3. 📜 ScheduledThreadPoolExecutor
- 📍 定义:
ScheduledThreadPoolExecutor
是ThreadPoolExecutor
的一个扩展,它可以执行定时或周期性任务。 - 📍 主要方法:
schedule(Runnable command, long delay, TimeUnit unit)
: 在给定的延迟后执行任务。scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
: 在给定的初始延迟后开始,然后以指定的周期执行任务。scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
: 在给定的初始延迟后开始,然后在每次执行结束和下次执行开始之间都使用给定的延迟。
- 📍 示例:
ScheduledThreadPoolExecutor scheduledExecutor = new ScheduledThreadPoolExecutor(5); scheduledExecutor.schedule(() -> { System.out.println("Task executed after a delay."); }, 3, TimeUnit.SECONDS);
⚠️ 注意: 使用线程池时,应该始终通过 ExecutorService.shutdown()
或 ExecutorService.shutdownNow()
方法关闭线程池。这些方法会停止接受新的任务,并尝试结束所有正在执行的任务。如果不关闭线程池,它可能会阻止应用程序的正常终止。
📌 虚拟线程 (Virtual Thread
)
🔹 介绍
- Java 19引入的轻量级线程。
- 在其他编程语言中可能被称为协程、纤程、绿色线程或用户态线程。
🔹 线程 vs 虚拟线程
- 线程:
- 由操作系统创建和调度。
- 切换线程消耗大量CPU时间。
- 同时调度的线程数量有限。
- 虚拟线程:
- 不由操作系统调度,而是由普通线程调度。
- 当虚拟线程等待IO时会被挂起,其他虚拟线程可以继续运行。
- 虽然代码看起来是同步的,但实际上是异步执行。
🔹 IO密集型任务的问题
- 使用传统线程处理大量IO请求是低效的。
- IO密集型任务中,线程大部分时间都在等待IO。
🔹 虚拟线程的优势
- 为了高效地执行IO密集型任务,Java引入了虚拟线程。
- 虚拟线程代码在执行到涉及IO的部分时会自动挂起并切换到其他虚拟线程。
📌 如何使用虚拟线程
🔹 方法一:直接创建并运行
Thread vt = Thread.startVirtualThread(() -> {
System.out.println("Start virtual thread...");
Thread.sleep(10);
System.out.println("End virtual thread.");
});
🔹 方法二:创建但不立即运行
Thread vt = Thread.ofVirtual().unstarted(() -> {
System.out.println("Start virtual thread...");
Thread.sleep(1000);
System.out.println("End virtual thread.");
});
vt.start();
🔹 方法三:使用ThreadFactory创建
ThreadFactory tf = Thread.ofVirtual().factory();
Thread vt = tf.newThread(() -> {
System.out.println("Start virtual thread...");
Thread.sleep(1000);
System.out.println("End virtual thread.");
});
vt.start();
🔹 高级:使用调度器
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
ThreadFactory tf = Thread.ofVirtual().factory();
for (int i=0; i<100000; i++) {
Thread vt = tf.newThread(() -> { ... });
executor.submit(vt);
executor.submit(() -> {
System.out.println("Start virtual thread...");
Thread.sleep(1000);
System.out.println("End virtual thread.");
return true;
});
}
⚠️ 注意: 虚拟线程在Java 19中是预览功能,默认关闭,需要添加参数--enable-preview
启用。
📢 小结
- Java 19的虚拟线程为IO密集型任务提供了优化的解决方案。
- 通过虚拟线程,可以高效地使用少数线程去调度大量的任务。
- 虚拟线程提供了与普通线程相同的接口,但允许代码异步执行,从而提高吞吐能力。
- 对于计算密集型任务,应该使用传统的多线程方法。
📌 详解 Future & Callable
Java中的Future
和Callable
是为了解决多线程编程中的一个关键问题:如何获取异步执行任务的结果。传统的线程的Runnable
接口任务是没有返回值的,但是有时我们确实需要获取异步执行任务的结果,这时Future
和Callable
就派上了用场。
📕 Callable
Callable
是一个泛型接口,与Runnable
接口相似,但有两个主要区别:
- 返回值:
Callable
的call()
方法有返回值。 - 异常:
call()
方法可以抛出受检异常。
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
📗 Future
当你提交一个Callable
任务给ExecutorService
时,你会得到一个Future
对象。Future
表示异步计算的结果。
主要方法包括:
get()
: 等待计算完成并检索其结果。get(long timeout, TimeUnit unit)
: 最大等待给定的时间,然后尝试检索结果。isDone()
: 返回任务是否完成。cancel(boolean mayInterruptIfRunning)
: 尝试取消此任务。isCancelled()
: 如果在任务正常完成之前取消,则返回true
。
🔍 示例
假设我们有一个计算任务,它需要一些时间来计算结果。我们可以使用Callable
和Future
来异步执行任务并获取结果。
import java.util.concurrent.*;
public class FutureCallableExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(1);
Callable<Integer> task = () -> {
try {
System.out.println("Executing task...");
TimeUnit.SECONDS.sleep(2);
return 123;
} catch (InterruptedException e) {
throw new IllegalStateException("Task interrupted", e);
}
};
Future<Integer> future = executor.submit(task);
System.out.println("Task is submitted.");
try {
// Future.get() will block until the result is available
Integer result = future.get(3, TimeUnit.SECONDS);
System.out.println("Task result: " + result);
} catch (TimeoutException ex) {
System.out.println("Task timed out.");
} catch (InterruptedException e) {
System.out.println("Task interrupted.");
} catch (ExecutionException e) {
System.out.println("Exception occurred while executing task.");
} finally {
executor.shutdown();
}
}
}
示例解释:
在上述代码中,我们创建了一个ExecutorService
并提交了一个Callable
任务。该任务将休眠2秒,然后返回一个整数。我们使用Future
对象的get()
方法等待任务完成并获取结果。我们还使用了一个超时版本的get()
方法,该方法将在3秒后超时,确保我们的程序不会被永久阻塞。
📢 小结:
Callable
允许有返回值的任务,并且可以抛出异常。Future
提供了检索Callable
任务计算结果的方法,或取消任务,或检查任务是否完成。- 使用
ExecutorService
提交Callable
任务时,将返回一个Future
对象。 Future.get()
方法会阻塞,直到任务完成并返回结果。
📌 详解 Future
和 CompletableFuture
在Java中,Future
和CompletableFuture
都是与并发相关的类,它们提供了一种表示异步计算结果的机制。下面我们将深入探讨这两个类及其之间的关系。
1. 📜 Future
-
📍 定义:
Future
是Java并发包中的一个接口,它代表了一个异步计算的结果。 -
📍 主要方法:
get()
: 等待异步计算完成并获取其结果。这个方法会阻塞直到结果可用。get(long timeout, TimeUnit unit)
: 等待异步计算完成一段时间,并在给定的超时时间内获取结果。isDone()
: 判断异步计算是否完成。isCancelled()
: 判断此任务是否被取消。cancel(boolean mayInterruptIfRunning)
: 尝试取消此任务的执行。
-
📍 示例:
ExecutorService executor = Executors.newFixedThreadPool(1); Future<String> future = executor.submit(() -> { Thread.sleep(2000); return "Task completed"; }); try { String result = future.get(); System.out.println(result); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); }
2. 📜 CompletableFuture
-
📍 定义:
CompletableFuture
是Future
的一个扩展,它提供了更丰富、更强大的功能,如流式操作、组合、等待多个计算完成等。 -
📍 主要方法:
supplyAsync(Supplier<U> supplier)
: 异步地执行给定的供应函数。thenApply(Function<? super T,? extends U> fn)
: 当CompletableFuture
的计算结果完成时,应用给定的函数处理该结果。thenCombine(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn)
: 当两个CompletableFuture
都完成时,使用两者的结果执行给定的函数。allOf(CompletableFuture<?>... cfs)
: 返回一个新的CompletableFuture
,它在所有给定的CompletableFuture
都完成时完成。anyOf(CompletableFuture<?>... cfs)
: 返回一个新的CompletableFuture
,它在任何给定的CompletableFuture
完成时完成。
-
📍 示例:
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> { return "Hello"; }); CompletableFuture<String> future2 = future1.thenApply(s -> { return s + " World"; }); future2.thenAccept(s -> { System.out.println(s); });
3. 📜 主要区别
-
📍 阻塞性:
Future
: 为了获得异步操作的结果,您通常需要使用get()
方法,这会阻塞当前线程直到结果可用。CompletableFuture
: 提供了非阻塞性的方式来处理异步操作的结果,如thenApply()
,thenAccept()
等。
-
📍 链式操作:
Future
: 不支持对结果进行链式操作。CompletableFuture
: 支持对结果进行链式操作,允许你在一个异步计算完成后立即启动另一个异步计算。
-
📍 组合性:
Future
: 缺乏方便的方法来组合多个异步计算的结果。CompletableFuture
: 提供了如thenCombine()
,allOf()
,anyOf()
等方法,使得组合多个异步计算变得简单。
-
📍 异常处理:
Future
: 异常处理相对困难,通常需要配合isDone()
和get()
方法。CompletableFuture
: 提供了更为灵活的异常处理机制,如handle()
,exceptionally()
等方法。
-
📍 灵活性:
Future
: 功能相对有限。CompletableFuture
: 提供了大量方法来支持更复杂的异步流程和组合,以及明确的线程执行控制。
⚠️ 注意:
CompletableFuture
提供了更为灵活的异常处理机制,如handle()
、exceptionally()
等方法。CompletableFuture
还提供了其他便利方法,使您可以更轻松地组合和链接多个异步计算。CompletableFuture
允许显式地定义异步任务应在哪个Executor
上执行,这为任务的并行执行提供了更细粒度的控制。
📌 详解 ForkJoinPool
与 RecursiveTask/RecursiveAction
1. 📜 ForkJoinPool
-
📍 定义:
ForkJoinPool
是 Java 7 引入的一个专门为分治任务设计的线程池。它旨在充分利用多核处理器的优势,提高并行执行任务的性能。 -
📍 工作原理:
- 使用工作窃取(work-stealing)算法,允许空闲的线程窃取其他线程的任务。
- 使用双端队列存储任务,从而支持工作窃取。
-
📍 主要方法:
invoke(ForkJoinTask<T> task)
: 同步执行指定的任务。submit(ForkJoinTask<T> task)
: 异步提交指定的任务。execute(ForkJoinTask<T> task)
: 执行指定的任务,但不返回结果。
2. 📜 RecursiveTask & RecursiveAction
-
📍 定义:
RecursiveTask
: 用于有返回值的分治任务。RecursiveAction
: 用于没有返回值的分治任务。
-
📍 工作原理:
- 划分任务:将任务划分为更小的子任务。
- 执行子任务:并行或递归执行子任务。
- 合并结果:对于
RecursiveTask
,合并子任务的结果。
-
📍 主要方法:
compute()
: 主要的执行方法,需要在子类中重写。用于执行任务的主要逻辑。
-
📍 示例:
class Fibonacci extends RecursiveTask<Integer> { final int n; Fibonacci(int n) { this.n = n; } @Override protected Integer compute() { if (n <= 1) return n; Fibonacci f1 = new Fibonacci(n - 1); f1.fork(); // 创建子任务 Fibonacci f2 = new Fibonacci(n - 2); return f2.compute() + f1.join(); // 等待子任务结果并合并 } } public static void main(String[] args) { ForkJoinPool pool = new ForkJoinPool(); int result = pool.invoke(new Fibonacci(5)); System.out.println(result); // 输出5 }
⚠️ 注意:
- 在设计分治任务时,任务的大小应足够小以确保并行执行的效率。如果任务太大,则工作窃取的效率会降低。
ForkJoinPool
不适用于 IO-bound 任务,因为这可能会导致大量线程阻塞,从而降低整体的并行性能。
总的来说,ForkJoinPool
和 RecursiveTask/RecursiveAction
为 Java 提供了一种强大的并行处理框架,特别适用于计算密集型的分治任务。
📌 详解 CountDownLatch
🔹 什么是 CountDownLatch?
CountDownLatch
是 Java 并发库中的一个同步工具类。- 它允许一个或多个线程等待其他线程完成操作。
🔹 工作原理
CountDownLatch
主要维护一个计数器,初始化时设定一个值。countDown()
方法使计数器减 1。await()
方法会阻塞,直到计数器的值为0。
🔹 基本使用
// 初始化一个计数器为 3 的 CountDownLatch
CountDownLatch latch = new CountDownLatch(3);
// 在 3 个线程中使用
Thread t1 = new Thread(() -> {
// 执行任务...
latch.countDown(); // 减少计数器
});
Thread t2 = new Thread(() -> {
// 执行任务...
latch.countDown(); // 减少计数器
});
Thread t3 = new Thread(() -> {
// 执行任务...
latch.countDown(); // 减少计数器
});
t1.start();
t2.start();
t3.start();
// 主线程等待三个线程完成
latch.await();
System.out.println("All tasks are done!");
🔹 注意事项
CountDownLatch
的计数器不能重置。一旦计数器到达 0,CountDownLatch
就不再有用。如果需要重置计数器,应考虑使用CyclicBarrier
。await()
方法还有一个带超时参数的版本,它会在给定的时间内等待。
🔹 常见应用场景
⚡️ 1. 确保应用在继续执行前完成必要的初始化
🔍 场景描述:
假设我们有一个应用,需要在启动时完成以下初始化工作:
- 连接到数据库
- 加载配置文件
- 初始化一个缓存
我们可以使用 CountDownLatch
确保所有这些初始化任务都完成后,应用才继续执行。
import java.util.concurrent.CountDownLatch;
public class AppInitializationExample {
private static final CountDownLatch latch = new CountDownLatch(3);
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.println("Connecting to database...");
// 模拟数据库连接过程
try { Thread.sleep(2000); } catch (InterruptedException ignored) {}
System.out.println("Database connected!");
latch.countDown();
}).start();
new Thread(() -> {
System.out.println("Loading configuration...");
// 模拟加载配置过程
try { Thread.sleep(1500); } catch (InterruptedException ignored) {}
System.out.println("Configuration loaded!");
latch.countDown();
}).start();
new Thread(() -> {
System.out.println("Initializing cache...");
// 模拟缓存初始化过程
try { Thread.sleep(1000); } catch (InterruptedException ignored) {}
System.out.println("Cache initialized!");
latch.countDown();
}).start();
// 等待所有初始化工作完成
latch.await();
System.out.println("Application is ready to proceed!");
}
}
示例解释:
上述代码模拟了一个应用的启动过程。每个初始化任务在一个独立的线程中执行。CountDownLatch
用于确保所有这些线程都完成其工作后,主线程才继续执行并输出 “Application is ready to proceed!”。
⚡️ 2. 等待所有任务完成后再继续执行
🔍 场景描述:
假设我们需要计算一个大数组的总和,可以将数组分成几个部分并行计算,然后在所有计算都完成后,再合并这些部分的结果。
import java.util.concurrent.CountDownLatch;
public class ParallelComputationExample {
private static final int[] data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
private static final CountDownLatch latch = new CountDownLatch(2);
private static int partialSum1 = 0;
private static int partialSum2 = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
for (int i = 0; i < data.length / 2; i++) {
partialSum1 += data[i];
}
latch.countDown();
}).start();
new Thread(() -> {
for (int i = data.length / 2; i < data.length; i++) {
partialSum2 += data[i];
}
latch.countDown();
}).start();
latch.await();
int totalSum = partialSum1 + partialSum2;
System.out.println("Total sum: " + totalSum);
}
}
示例解释:
上述代码将一个整数数组分成两部分,并使用两个线程并行计算各部分的和。CountDownLatch
确保了只有当这两个线程都完成其计算后,主线程才继续执行并计算总和。
📌 详解 CyclicBarrier
🔹 什么是 CyclicBarrier?
CyclicBarrier
是 Java 并发库中的一个同步工具类。- 它允许一组线程相互等待,直到所有线程都达到某个屏障点,然后所有线程都可以继续执行。
🔹 工作原理
CyclicBarrier
初始化时需要指定一个整数参数,表示要等待的线程数。- 当线程达到屏障点,它们会调用
await()
方法。 - 当所有线程都达到屏障点时,
CyclicBarrier
执行一个可选的屏障操作并释放所有等待的线程。 - 与
CountDownLatch
不同,CyclicBarrier
是可重用的。
🔹 基本使用
// 创建一个要等待3个线程的CyclicBarrier,并指定一个屏障操作
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("All tasks are done. Merging results!");
});
Thread t1 = new Thread(() -> {
// 执行任务...
try {
barrier.await(); // 到达屏障点
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
Thread t2 = new Thread(() -> {
// 执行任务...
try {
barrier.await(); // 到达屏障点
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
Thread t3 = new Thread(() -> {
// 执行任务...
try {
barrier.await(); // 到达屏障点
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
t1.start();
t2.start();
t3.start();
🔹 注意事项
- 如果线程在调用
await()
时被中断,或者等待时屏障被其他线程打破,那么await()
方法会抛出异常。 CyclicBarrier
的名字“cyclic”意味着它可以在释放等待线程后被重置,并再次使用。
🔹 常见应用场景
⚡️ 1. 并行计算
🔍 场景描述:
假设我们要计算一个大数组的总和。我们可以将该数组分成几个部分,使用多个线程并行计算每个部分的和。当所有部分都计算完成后,我们合并这些部分的结果。
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.BrokenBarrierException;
public class ParallelComputationExample {
private static final int[] data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
private static final CyclicBarrier barrier = new CyclicBarrier(2, () -> {
int result = partialSum1 + partialSum2;
System.out.println("Total sum: " + result);
});
private static int partialSum1 = 0;
private static int partialSum2 = 0;
public static void main(String[] args) {
new Thread(() -> {
for (int i = 0; i < data.length / 2; i++) {
partialSum1 += data[i];
}
try {
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
for (int i = data.length / 2; i < data.length; i++) {
partialSum2 += data[i];
}
try {
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
示例解释:
上述代码将一个整数数组分成两部分。两个线程并行计算各部分的和。当这两个线程都完成其计算后,由 CyclicBarrier
指定的屏障操作(在这里是合并结果并打印总和)将被执行。
⚡️ 2. 复杂业务流程控制
🔍 场景描述:
假设我们有一个复杂的业务流程,其中两个线程必须完成各自的子流程,然后在进入下一个流程前同步。
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.BrokenBarrierException;
public class ComplexBusinessProcessExample {
private static final CyclicBarrier barrier = new CyclicBarrier(2, () -> {
System.out.println("Both sub-processes are done. Proceeding to the next step.");
});
public static void main(String[] args) {
new Thread(() -> {
System.out.println("Thread 1: Executing sub-process 1...");
// Simulate some work
try { Thread.sleep(2000); } catch (InterruptedException ignored) {}
System.out.println("Thread 1: Sub-process 1 done.");
try {
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
System.out.println("Thread 2: Executing sub-process 2...");
// Simulate some work
try { Thread.sleep(1500); } catch (InterruptedException ignored) {}
System.out.println("Thread 2: Sub-process 2 done.");
try {
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
示例解释:
上述代码展示了两个线程执行各自的子流程。一旦两个子流程都完成,CyclicBarrier
中的屏障操作将被执行,它在这里是打印一条消息表示两个子流程都已完成,并可以继续进行下一步。
📌 详解 Semaphore
🔹 什么是 Semaphore?
Semaphore
是 Java 并发库中的一个同步工具类。- 它维护了一个许可集合,用于限制同时访问某个特定资源的线程数量。
🔹 工作原理
Semaphore
初始化时设置一个许可的数量。- 线程可以通过
acquire()
方法获取一个许可,如果没有许可可用,线程将被阻塞。 - 线程使用完许可后,可以通过
release()
方法释放许可,供其他线程使用。
🔹 基本使用
// 创建一个允许3个许可的Semaphore
Semaphore semaphore = new Semaphore(3);
Thread t1 = new Thread(() -> {
try {
semaphore.acquire(); // 获取许可
// 执行任务...
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放许可
}
});
Thread t2 = new Thread(() -> {
try {
semaphore.acquire(2); // 获取2个许可
// 执行任务...
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(2); // 释放2个许可
}
});
t1.start();
t2.start();
🔹 注意事项
- 确保在使用许可后始终释放它,通常在
finally
块中进行,以避免资源泄漏。 Semaphore
还提供了其他有用的方法,如availablePermits()
(返回当前可用的许可数)和hasQueuedThreads()
(返回是否有线程正在等待许可)。
🔹 常见应用场景
⚡️ 1. 资源池限制
🔍 场景描述:
假设我们有一个数据库连接池,允许的最大连接数为3。当多个线程试图建立连接时,我们可以使用 Semaphore
确保同时只有3个线程能获得连接。
import java.util.concurrent.Semaphore;
public class DatabaseConnectionPoolExample {
private static final Semaphore semaphore = new Semaphore(3); // 最多3个连接
public static void main(String[] args) {
for (int i = 0; i < 10; i++) { // 创建10个线程,模拟10个请求
new Thread(() -> {
try {
semaphore.acquire(); // 获取连接
System.out.println(Thread.currentThread().getName() + " acquired connection");
// 模拟数据库操作
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + " released connection");
semaphore.release(); // 释放连接
}
}).start();
}
}
}
示例解释:
上述代码模拟了10个线程尝试从数据库连接池中获取连接。由于我们限制了同时只能有3个连接,所以只有3个线程能同时获得连接。其他线程需要等待,直到一个连接被释放。
⚡️ 2. 流量控制
🔍 场景描述:
假设我们有一个服务,它限制了每次只能有5个客户端进行访问。当超过此数量的客户端尝试访问时,它们需要等待直到一个客户端完成并释放其访问权限。
import java.util.concurrent.Semaphore;
public class TrafficControlExample {
private static final Semaphore semaphore = new Semaphore(5); // 最多5个客户端
public static void main(String[] args) {
for (int i = 0; i < 20; i++) { // 创建20个线程,模拟20个客户端
new Thread(() -> {
try {
semaphore.acquire(); // 获取访问权限
System.out.println(Thread.currentThread().getName() + " started accessing the server");
// 模拟服务访问
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + " finished accessing the server");
semaphore.release(); // 释放访问权限
}
}).start();
}
}
}
示例解释:
上述代码模拟了20个客户端尝试访问服务器。由于我们限制了同时只能有5个客户端访问,所以只有5个线程能同时访问。其他线程需要等待,直到一个客户端完成访问并释放其访问权限。
📌 详解 Exchanger
🔹 什么是 Exchanger?
Exchanger
是 Java 并发库中的一个同步工具类。- 它提供了一个同步点,在这个同步点,两个线程可以交换其各自的数据。
- 这两个线程会在交换数据时互相阻塞,直到两者都达到
Exchanger
的同步点。
🔹 工作原理
- 当一个线程调用
Exchanger
的exchange()
方法后,它会被阻塞,等待另一个线程也调用exchange()
方法。 - 一旦两个线程都到达了交换点,它们的数据就会交换,并且两个线程都将继续执行。
🔹 基本使用
// 创建一个Exchanger用于交换String类型的数据
Exchanger<String> exchanger = new Exchanger<>();
Thread producer = new Thread(() -> {
String data = "Data produced by producer thread";
try {
// 交换数据
String receivedData = exchanger.exchange(data);
System.out.println("Producer received: " + receivedData);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread consumer = new Thread(() -> {
String data = "Data produced by consumer thread";
try {
// 交换数据
String receivedData = exchanger.exchange(data);
System.out.println("Consumer received: " + receivedData);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
producer.start();
consumer.start();
🔹 常见应用场景
🌟 Exchanger 示例
⚡️ 1. 数据交换
🔍 场景描述:
假设我们有一个线程负责加载数据,称为加载线程,和另一个线程负责处理这些数据,称为处理线程。加载线程从某个数据源加载数据,并将其交换给处理线程进行处理。加载线程完成一批数据的加载后,与处理线程交换数据,然后继续加载下一批数据。
import java.util.concurrent.Exchanger;
import java.util.List;
import java.util.ArrayList;
public class DataLoaderProcessorExample {
private static final Exchanger<List<String>> exchanger = new Exchanger<>();
public static void main(String[] args) {
new Thread(() -> {
try {
while (true) {
// 模拟数据加载
List<String> data = loadData();
System.out.println("Loader: Loaded data: " + data);
// 交换数据
List<String> processedData = exchanger.exchange(data);
System.out.println("Loader: Received processed data: " + processedData);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Loader").start();
new Thread(() -> {
try {
while (true) {
// 获取待处理数据
List<String> dataToProcess = exchanger.exchange(null);
System.out.println("Processor: Received data to process: " + dataToProcess);
// 模拟数据处理
List<String> processedData = process(dataToProcess);
System.out.println("Processor: Processed data: " + processedData);
// 交换处理过的数据
exchanger.exchange(processedData);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Processor").start();
}
private static List<String> loadData() {
// 模拟从数据源加载数据
List<String> data = new ArrayList<>();
data.add("data-" + Math.random());
return data;
}
private static List<String> process(List<String> data) {
// 模拟数据处理
data.replaceAll(d -> d + "-processed");
return data;
}
}
示例解释:
上述代码中,加载线程从数据源加载数据,并通过 Exchanger
与处理线程交换数据。处理线程接收数据,进行处理,并将处理过的数据交换回加载线程。
⚡️ 2. 流水线计算
🔍 场景描述:
假设我们有两个线程,一个负责计算数据并生成输出,另一个线程获取这些输出并进行进一步的处理。
import java.util.concurrent.Exchanger;
public class PipelineComputationExample {
private static final Exchanger<String> exchanger = new Exchanger<>();
public static void main(String[] args) {
new Thread(() -> {
try {
while (true) {
// 模拟数据计算
String output = compute();
System.out.println("Producer: Produced output: " + output);
// 交换输出
exchanger.exchange(output);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Producer").start();
new Thread(() -> {
try {
while (true) {
// 获取输出
String receivedOutput = exchanger.exchange(null);
System.out.println("Consumer: Received output: " + receivedOutput);
// 进一步处理
String result = furtherProcess(receivedOutput);
System.out.println("Consumer: Processed result: " + result);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Consumer").start();
}
private static String compute() {
// 模拟数据计算
return "output-" + Math.random();
}
private static String furtherProcess(String output) {
// 模拟进一步的数据处理
return output + "-processed";
}
}
示例解释:
在上述代码中,生产者线程进行数据计算并生成输出。通过 Exchanger
,它将输出交换给消费者线程进行进一步的处理。