Java-多线程并发-7.线程的包装


📌 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<>();
    

🔹 使用方法

  1. 设置一个实例与 ThreadLocal 关联。
  2. 在移除之前,所有方法都可以获取该实例。
  3. 在同一个线程中,各个方法获取的实例都是同一个。

🔹 原理

  • 可以把 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

  • 📍 定义: ThreadPoolExecutorExecutorService 接口的直接实现,它提供了更为灵活和强大的线程池配置选项。
  • 📍 主要参数:
    • 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

  • 📍 定义: ScheduledThreadPoolExecutorThreadPoolExecutor 的一个扩展,它可以执行定时或周期性任务。
  • 📍 主要方法:
    • 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中的FutureCallable是为了解决多线程编程中的一个关键问题:如何获取异步执行任务的结果。传统的线程的Runnable接口任务是没有返回值的,但是有时我们确实需要获取异步执行任务的结果,这时FutureCallable就派上了用场。

📕 Callable

Callable是一个泛型接口,与Runnable接口相似,但有两个主要区别:

  1. 返回值Callablecall()方法有返回值。
  2. 异常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

🔍 示例

假设我们有一个计算任务,它需要一些时间来计算结果。我们可以使用CallableFuture来异步执行任务并获取结果。

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()方法会阻塞,直到任务完成并返回结果。

📌 详解 FutureCompletableFuture

在Java中,FutureCompletableFuture都是与并发相关的类,它们提供了一种表示异步计算结果的机制。下面我们将深入探讨这两个类及其之间的关系。

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

  • 📍 定义: CompletableFutureFuture的一个扩展,它提供了更丰富、更强大的功能,如流式操作、组合、等待多个计算完成等。

  • 📍 主要方法:

    • 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上执行,这为任务的并行执行提供了更细粒度的控制。

📌 详解 ForkJoinPoolRecursiveTask/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 任务,因为这可能会导致大量线程阻塞,从而降低整体的并行性能。

总的来说,ForkJoinPoolRecursiveTask/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. 确保应用在继续执行前完成必要的初始化

🔍 场景描述:
假设我们有一个应用,需要在启动时完成以下初始化工作:

  1. 连接到数据库
  2. 加载配置文件
  3. 初始化一个缓存

我们可以使用 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 的同步点。

🔹 工作原理

  • 当一个线程调用 Exchangerexchange() 方法后,它会被阻塞,等待另一个线程也调用 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,它将输出交换给消费者线程进行进一步的处理。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yueerba126

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值