Java线程池技术一:入门篇

Published on 2025-01-22 11:06 in 分类: 博客 with 狂盗一枝梅
分类: 博客

Java线程的创建非常昂贵,需要JVM和OS(操作系统)配合完成大量的工作:

(1)必须为线程堆栈分配和初始化大量内存块,其中包含至少1MB的栈内存。

(2)需要进行系统调用,以便在OS(操作系统)中创建和注册本地线程。

Java高并发应用频繁创建和销毁线程的操作是非常低效的,而且是不被编程规范所允许的。如何降低Java线程的创建成本?必须使用到线程池。线程池主要解决了以下两个问题:

(1)提升性能:线程池能独立负责线程的创建、维护和分配。在执行大量异步任务时,可以不需要自己创建线程,而是将任务交给线程池去调度。线程池能尽可能使用空闲的线程去执行异步任务,最大限度地对已经创建的线程进行复用,使得性能提升明显。

(2)线程管理:每个Java线程池会保持一些基本的线程统计信息,例如完成的任务数量、空闲时间等,以便对线程进行有效管理,使得能对所接收到的异步任务进行高效调度。

一、使用Executors快捷创建线程池

Executors类提供了五种(原来是四种,java1.8中新增了一种newWorkStealingPool)快捷创建线程池的方式:newSingleThreadExecutor、newFixedThreadPool、newCachedThreadPool、newScheduledThreadPool、newWorkStealingPool。

1、newSingleThreadExecutor

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(
            1, //corePoolSize
            1, //maximumPoolSize
            0L, //keepAliveTime 
            TimeUnit.MILLISECONDS, //TimeUnit
            new LinkedBlockingQueue<Runnable>() //BlockingQueue
        ));
}

该方法用于创建一个“单线程化线程池”,也就是只有一个线程的线程池,所创建的线程池用唯一的工作线程来执行任务,使用此方法创建的线程池能保证所有任务按照指定顺序(如FIFO)执行。

案例

public static void newSingleThreadExecutorDemo() {
    AtomicInteger count = new AtomicInteger(0);
    ExecutorService executor = Executors.newSingleThreadExecutor();
    for (int i = 0; i < 5; i++) {
        executor.submit(() -> {
            log.info("任务[{}]开始执行", count.incrementAndGet());
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                log.error("", e);
            }
            log.info("任务[{}]结束执行任务", count.get());
        });
    }
    executor.shutdown();
}

以上代码运行结果如下:

image-20250120183403482

可以看到所有任务都是由一个线程执行,而且所有任务按照入队的顺序依次执行。

特点

从以上输出中可以看出,该线程池有以下特点:

(1)单线程化的线程池中的任务是按照提交的次序顺序执行的。

(2)池中的唯一线程的存活时间是无限的。

(3)当池中的唯一线程正繁忙时,新提交的任务实例会进入内部的阻塞队列中,并且其阻塞队列是无界的(LinkedBlockingQueue)。

2、newFixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(
        nThreads, //corePoolSize
        nThreads,//maximumPoolSize
        0L, //keepAliveTime
        TimeUnit.MILLISECONDS,//TimeUnit
        new LinkedBlockingQueue<Runnable>()//BlockingQueue
    );
}

该方法用于创建一个“固定数量的线程池”,其唯一的参数用于设置池中线程的“固定数量”。

案例

public static void newFixedThreadPoolDemo() {
    AtomicInteger count = new AtomicInteger(0);
    ExecutorService executor = Executors.newFixedThreadPool(2);
    for (int i = 0; i < 10; i++) {
        executor.submit(() -> {
            log.info("任务[{}]开始执行", count.incrementAndGet());
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                log.error("", e);
            }
        });
    }
    executor.shutdown();

}

以上代码运行结果如下所示:

动画10

在测试用例中,创建了一个线程数为2的“固定数量线程池”,然后向其中提交了10个任务。从输出结果可以看到,该线程池同时只能执行2个任务,剩余的任务会排队等待。

特点

“固定数量的线程池”的特点大致如下:

(1)如果线程数没有达到“固定数量”,每次提交一个任务线程池内就创建一个新线程,直到线程达到线程池固定的数量。

(2)线程池的大小一旦达到“固定数量”就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

(3)在接收异步任务的执行目标实例时,如果池中的所有线程均在繁忙状态,新任务会进入阻塞队列中(无界的阻塞队列)。

“固定数量的线程池”的适用场景:需要任务长期执行的场景。“固定数量的线程池”的线程数能够比较稳定地保证一个数,能够避免频繁回收线程和创建线程,故适用于处理CPU密集型的任务,在CPU被工作线程长时间占用的情况下,能确保尽可能少地分配线程。

“固定数量的线程池”的弊端:内部使用无界队列来存放排队任务,当大量任务超过线程池最大容量需要处理时,队列无限增大,使服务器资源迅速耗尽。

3、newCachedThreadPool

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(
        0,//corePoolSize
        Integer.MAX_VALUE,//maximumPoolSize
        60L, //keepAliveTime
        TimeUnit.SECONDS,//TimeUnit
        new SynchronousQueue<Runnable>()//BlockingQueue
    );
}

该方法用于创建一个“可缓存线程池”,如果线程池内的某些线程无事可干成为空闲线程,“可缓存线程池”可灵活回收这些空闲线程。

案例

public static void newCachedThreadPoolDemo() {
    AtomicInteger count = new AtomicInteger(0);
    ExecutorService executor = Executors.newCachedThreadPool();
    for (int i = 0; i < 10; i++) {
        executor.submit(() -> {
            log.info("任务[{}]开始执行", count.incrementAndGet());
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                log.error("", e);
            }
        });
    }
    executor.shutdown();
}

以上代码运行结果如下所示:

动画11

特点

“可缓存线程池”的特点大致如下:

(1)在接收新的异步任务target执行目标实例时,如果池内所有线程繁忙,此线程池就会添加新线程来处理任务。

(2)此线程池不会对线程池大小进行限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

(3)如果部分线程空闲,也就是存量线程的数量超过了处理任务数量,就会回收空闲(60秒不执行任务)线程。

“可缓存线程池”的适用场景:需要快速处理突发性强、耗时较短的任务场景,如Netty的NIO处理场景、REST API接口的瞬时削峰场景。“可缓存线程池”的线程数量不固定,只要有空闲线程就会被回收;接收到的新异步任务执行目标,查看是否有线程处于空闲状态,如果没有就直接创建新的线程。

“可缓存线程池”的弊端:线程池没有最大线程数量限制,如果大量的异步任务执行目标实例同时提交,可能会因创建线程过多而导致资源耗尽。

4、newScheduledThreadPool

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(
        corePoolSize, //corePoolSize
        Integer.MAX_VALUE, //maximumPoolSize
        0, //keepAliveTime
        NANOSECONDS,//TimeUnit
        new DelayedWorkQueue()//BlockingQueue
    );
}

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

newScheduledThreadPool方法用于创建一个“可调度线程池”,即一个提供“延时”和“周期性”任务调度功能的ScheduledExecutorService类型的线程池。

案例

ScheduledExecutorService有两个重要的方法:scheduleAtFixedRate以及scheduleWithFixedDelay方法。

scheduleAtFixedRate:以固定时间间隔执行任务,如果前次任务未执行完,等待前次任务执行完毕后执行下次任务

scheduleWithFixedDelay:前次任务执行完毕之后,间隔固定时间后执行下一次任务

下面通过scheduleWithFixedDelay演示ScheduledExecutorService用法:

 public static void newScheduledThreadPoolDemo() throws InterruptedException {
    AtomicInteger count = new AtomicInteger(0);
    ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
    for (int i = 0; i < 2; i++) {
        int finalI = i;
        executor.scheduleWithFixedDelay(
                () -> {
                    log.info("{}任务[{}]开始执行", finalI, count.incrementAndGet());
                    try {
                        TimeUnit.SECONDS.sleep(2);
                    } catch (InterruptedException e) {
                        log.error("", e);
                    }
                },
                0,
                1,//任务执行完成以后等待一秒钟执行下一个任务
                TimeUnit.SECONDS
        );
    }
    TimeUnit.SECONDS.sleep(600);
    executor.shutdown();
}

上述代码运行结果:

动画12

可以看到,执行完scheduleWithFixedDelay方法,任务和任务之间间隔了3秒钟,其中2秒钟是线程执行任务的时间,1秒钟是设置的间隔时间。

特点

“可调度线程池”的适用场景:周期性地执行任务的场景。

5、newWorkStealingPool

newWorkStealingPool方法创建线程池是Java8 新增的,内部使用了ForkJoinPool线程池,它通过“分而治之”的思想运作,并有独特的“工作窃取”算法优化任务排队逻辑。

newWorkStealingPool方法支持传入“并行度”参数,并行度默认值是Runtime.getRuntime().availableProcessors()也就是当前处理器的核心数,为了维持该并行度,线程数量会动态的增加或者减少。

案例

public static void newWorkStealingPoolDemo() throws InterruptedException {
    AtomicInteger count = new AtomicInteger(0);
    //这里设置并行度为2,表示设置同时让两个任务并行运行
    ExecutorService executor = Executors.newWorkStealingPool(2);
    for (int i = 0; i < 100; i++) {
        executor.submit(() -> {
            log.info("任务[{}]开始执行", count.incrementAndGet());
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                log.error("", e);
            }
        });
    }
    TimeUnit.SECONDS.sleep(600);
    executor.shutdown();
}

其结果运行如下:

动画13

特点

适合处理I/O密集型任务,因为当某个线程在执行I/O操作时,其他线程可以窃取任务执行,减少了线程的等待时间。

二、线程池分类

1、线程池类图

上面讲了五种快捷创建线程池的方式,五种创建线程池的方式之间是否存在关联关系?先看五个线程池的类图

线程池.drawio

2、线程池分类

从线程池类图中可以看到,五种快捷创建的线程池共分为四大类:

ThreadPoolExecutor、FinallzableDelegatedExecutorService、ScheduledThreadPoolExecutor、ForkJoinPool,每一种都比较复杂,都值得仔细探究下其实现。

从下一章开始将会仔细分析每一种线程池实现。



注:本篇文章大量节选自《Java高并发核心编程 卷2:多线程、锁、JMM、JUC、高并发设计模式》一书中内容。

END.
#java #多线程编程
复制 复制成功