剖析 Java 并发工具类:探索 ExecutorService 线程池、Future 异步结果获取以及 CountDownLatch、CyclicBarrier 等同步工具,提高并发编程效率。

好的,各位观众老爷,欢迎来到“Java并发工具箱:玩转线程,告别阻塞”讲堂!我是你们的老朋友,人称“并发小能手”的码农老王。今天,咱们不讲高深的理论,就聊聊实战,手把手教你玩转Java并发工具类,让你的程序跑得飞起,告别“假并发”,拥抱真高效!😎

开场白:并发编程,痛并快乐着?

说起并发编程,相信很多小伙伴都是又爱又恨。爱的是它可以让程序充分利用多核CPU,提升性能,让你的服务器不再“卡成PPT”。恨的是,并发编程就像潘多拉的魔盒,一不小心就会冒出各种诡异的问题,比如死锁、活锁、数据竞争等等,让你debug到怀疑人生。😩

但是,别怕!Java为我们准备了一整套强大的并发工具类,就像武侠小说里的各种神兵利器,只要掌握了它们,就能轻松驾驭并发,化险为夷。今天,我们就来逐一剖析这些“神兵利器”,让你也能成为并发编程的高手!

第一章:ExecutorService线程池:让线程“劳有所得”

首先,我们来聊聊ExecutorService线程池。想象一下,你要开一家餐厅,如果每次来一个客人就临时招一个服务员,那效率得多低?线程池就像一个预先准备好的服务员队伍,来一个任务就分配一个服务员,任务完成服务员也不会立刻走人,而是等待下一个任务。这样就避免了频繁创建和销毁线程的开销,大大提升了效率。

1.1 线程池的优势:

  • 降低资源消耗: 重用已创建的线程,避免频繁创建和销毁线程的开销。
  • 提高响应速度: 任务到达时,无需等待线程创建即可立即执行。
  • 提高线程的可管理性: 可以统一管理和监控线程,例如设置最大线程数、空闲线程存活时间等。

1.2 常见的线程池类型:

Java提供了多种线程池类型,每种类型都有不同的特点和适用场景。

线程池类型 特点 适用场景
FixedThreadPool 固定大小的线程池,核心线程数和最大线程数相等。 适合任务量比较稳定,需要保证响应速度的场景。
CachedThreadPool 线程数可以动态增长的线程池,没有核心线程数,最大线程数为Integer.MAX_VALUE。 适合任务量波动较大,需要快速响应的场景。
SingleThreadExecutor 只有一个线程的线程池,所有任务按照提交顺序依次执行。 适合需要保证任务顺序执行的场景。
ScheduledThreadPool 可以执行定时任务和周期性任务的线程池。 适合需要执行定时任务和周期性任务的场景。

1.3 如何使用ExecutorService:

使用ExecutorService非常简单,只需要以下几个步骤:

  1. 创建线程池: 使用Executors工厂类创建不同类型的线程池。
  2. 提交任务: 使用submit()execute()方法提交任务。
  3. 关闭线程池: 使用shutdown()shutdownNow()方法关闭线程池。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorServiceDemo {

    public static void main(String[] args) {
        // 创建一个固定大小的线程池
        ExecutorService executor = Executors.newFixedThreadPool(5);

        // 提交10个任务
        for (int i = 0; i < 10; i++) {
            final int taskNum = i;
            executor.submit(() -> {
                System.out.println("Thread " + Thread.currentThread().getName() + " is running task " + taskNum);
                try {
                    Thread.sleep(1000); // 模拟任务执行时间
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        // 关闭线程池
        executor.shutdown();
    }
}

这段代码创建了一个包含5个线程的线程池,并提交了10个任务。每个任务会打印出当前线程的名称和任务编号,并模拟执行1秒钟。最后,我们关闭了线程池,不再接受新的任务。

第二章:Future:让线程“未雨绸缪”

有时候,我们需要知道异步任务的执行结果,或者判断任务是否完成。Future接口就是用来解决这个问题的。它可以让你在任务执行过程中获取任务的状态和结果,就像一个“未来预言家”,提前告诉你任务的命运。🔮

2.1 Future的常用方法:

方法 作用
get() 获取任务的执行结果,如果任务尚未完成,则会阻塞等待。
get(timeout, unit) 在指定时间内获取任务的执行结果,如果超时则抛出TimeoutException
isDone() 判断任务是否完成。
isCancelled() 判断任务是否被取消。
cancel(mayInterruptIfRunning) 取消任务的执行,如果mayInterruptIfRunning为true,则会尝试中断正在执行的任务。

2.2 如何使用Future:

import java.util.concurrent.*;

public class FutureDemo {

    public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException {
        ExecutorService executor = Executors.newFixedThreadPool(1);

        // 提交一个Callable任务,返回一个Future对象
        Future<String> future = executor.submit(() -> {
            System.out.println("Task is running...");
            Thread.sleep(2000); // 模拟任务执行时间
            return "Task completed!";
        });

        // 检查任务是否完成
        System.out.println("Task is done: " + future.isDone());

        // 获取任务的执行结果,最多等待1秒
        try {
            String result = future.get(1, TimeUnit.SECONDS);
            System.out.println("Task result: " + result);
        } catch (TimeoutException e) {
            System.out.println("Task timeout!");
        }

        // 关闭线程池
        executor.shutdown();
    }
}

这段代码提交了一个Callable任务,并使用future.get(1, TimeUnit.SECONDS)方法获取任务的执行结果。由于任务需要执行2秒钟,而我们只等待1秒钟,所以会抛出TimeoutException

第三章:CountDownLatch:让线程“守株待兔”

CountDownLatch是一个计数器,它可以让一个或多个线程等待其他线程完成操作后再执行。就像赛跑比赛前的倒计时,只有所有运动员都准备好了,才能鸣枪起跑。🏃‍♀️🏃‍♂️

3.1 CountDownLatch的原理:

CountDownLatch维护一个计数器,初始值为指定的值。每当一个线程完成操作后,就调用countDown()方法将计数器减1。当计数器变为0时,所有等待的线程都会被唤醒。

3.2 CountDownLatch的常用方法:

方法 作用
countDown() 将计数器减1。
await() 阻塞当前线程,直到计数器变为0。
await(timeout, unit) 阻塞当前线程,直到计数器变为0,或者超时。

3.3 如何使用CountDownLatch:

import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo {

    public static void main(String[] args) throws InterruptedException {
        // 创建一个CountDownLatch,初始值为3
        CountDownLatch latch = new CountDownLatch(3);

        // 创建3个线程,每个线程完成操作后调用countDown()
        for (int i = 0; i < 3; i++) {
            final int threadNum = i;
            new Thread(() -> {
                System.out.println("Thread " + Thread.currentThread().getName() + " is running...");
                try {
                    Thread.sleep(1000); // 模拟任务执行时间
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread " + Thread.currentThread().getName() + " finished.");
                latch.countDown(); // 计数器减1
            }).start();
        }

        // 主线程等待,直到计数器变为0
        latch.await();
        System.out.println("All threads finished!");
    }
}

这段代码创建了一个CountDownLatch,初始值为3。然后创建3个线程,每个线程完成操作后调用latch.countDown()方法将计数器减1。主线程调用latch.await()方法等待,直到计数器变为0,才继续执行。

第四章:CyclicBarrier:让线程“集体行动”

CyclicBarrier也叫做循环栅栏,它可以让一组线程互相等待,直到所有线程都到达某个屏障点,然后才能继续执行。就像团队旅游,大家都要在集合地点等待,人齐了才能一起出发。 ✈️

4.1 CyclicBarrier的原理:

CyclicBarrier维护一个计数器,初始值为指定的值。每当一个线程到达屏障点时,就调用await()方法,计数器减1。当计数器变为0时,所有等待的线程都会被唤醒,并且计数器会被重置为初始值,可以再次使用。

4.2 CyclicBarrier的常用方法:

方法 作用
await() 阻塞当前线程,直到所有线程都到达屏障点。
await(timeout, unit) 阻塞当前线程,直到所有线程都到达屏障点,或者超时。
reset() 重置计数器,将所有等待的线程抛出BrokenBarrierException

4.3 如何使用CyclicBarrier:

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierDemo {

    public static void main(String[] args) throws InterruptedException {
        // 创建一个CyclicBarrier,初始值为3
        CyclicBarrier barrier = new CyclicBarrier(3, () -> {
            System.out.println("All threads arrived at the barrier!");
        });

        // 创建3个线程,每个线程到达屏障点后调用await()
        for (int i = 0; i < 3; i++) {
            final int threadNum = i;
            new Thread(() -> {
                System.out.println("Thread " + Thread.currentThread().getName() + " is running...");
                try {
                    Thread.sleep(1000); // 模拟任务执行时间
                    System.out.println("Thread " + Thread.currentThread().getName() + " is waiting at the barrier...");
                    barrier.await(); // 等待其他线程到达屏障点
                    System.out.println("Thread " + Thread.currentThread().getName() + " continues to run.");
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

这段代码创建了一个CyclicBarrier,初始值为3。当所有线程都到达屏障点时,会执行一个Runnable任务,打印出“All threads arrived at the barrier!”。然后,每个线程继续执行后面的代码。

第五章:Semaphore:让线程“排队入场”

Semaphore信号量,它可以控制同时访问某个资源的线程数量。就像停车场,只有一定数量的停车位,当车位满了,其他车辆就需要在外面等待。 🚗

5.1 Semaphore的原理:

Semaphore维护一个计数器,初始值为指定的值,表示可用的资源数量。每当一个线程需要访问资源时,就调用acquire()方法获取一个许可,计数器减1。当线程释放资源时,就调用release()方法释放一个许可,计数器加1。如果计数器为0,表示资源已用完,其他线程需要等待。

5.2 Semaphore的常用方法:

方法 作用
acquire() 获取一个许可,如果许可不可用,则阻塞当前线程。
acquire(permits) 获取指定数量的许可,如果许可不可用,则阻塞当前线程。
tryAcquire() 尝试获取一个许可,如果许可可用则返回true,否则返回false,不会阻塞当前线程。
tryAcquire(timeout, unit) 尝试在指定时间内获取一个许可,如果超时则返回false,不会阻塞当前线程。
release() 释放一个许可。
release(permits) 释放指定数量的许可。

5.3 如何使用Semaphore:

import java.util.concurrent.Semaphore;

public class SemaphoreDemo {

    public static void main(String[] args) {
        // 创建一个Semaphore,初始值为3,表示有3个许可可用
        Semaphore semaphore = new Semaphore(3);

        // 创建5个线程,每个线程尝试获取一个许可
        for (int i = 0; i < 5; i++) {
            final int threadNum = i;
            new Thread(() -> {
                try {
                    semaphore.acquire(); // 获取一个许可
                    System.out.println("Thread " + Thread.currentThread().getName() + " acquired a permit.");
                    Thread.sleep(1000); // 模拟任务执行时间
                    System.out.println("Thread " + Thread.currentThread().getName() + " released a permit.");
                    semaphore.release(); // 释放一个许可
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

这段代码创建了一个Semaphore,初始值为3,表示有3个许可可用。然后创建5个线程,每个线程尝试获取一个许可。由于只有3个许可可用,所以只有3个线程可以立即获取到许可并执行,其他线程需要等待。

第六章:总结与展望:并发编程,永无止境

好了,各位小伙伴,今天我们一起学习了Java并发工具箱中的几个重要成员:ExecutorService线程池、Future异步结果获取、CountDownLatch、CyclicBarrier和Semaphore。掌握了它们,你就可以更加轻松地编写高效、稳定的并发程序,让你的程序跑得更快,更稳!🚀

但是,并发编程的世界是广阔而深邃的,还有很多值得我们探索的地方。比如,更加高级的并发容器、原子类、锁等等。希望大家在学习并发编程的道路上不断进步,成为真正的并发编程高手!💪

最后,送给大家一句并发编程的真理:“并发虽好,可不要贪杯哦!” 😉

感谢大家的收听,我们下期再见!👋

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注