【Java 线程池】【一】线程池介绍和基本使用

1  前言

这节开始我们主要是攻克一下Java中的线程池,来深入的分析一下线程池是怎么设计的,线程池的原理是什么等,只有更好的理解原理,才能很好的使用并且规避掉一些问题,那么本节我们先简单介绍下线程池是什么以及平时大家的使用方法。

2  认识线程池

首先大家要知道为什么要有线程池?

Java线程的创建非常昂贵,需要JVM和OS(操作系统)配合完成大量的工作: 1)必须为线程堆栈分配和初始化大量内存块,其中包含至少1MB的栈内存。 2)需要进行系统调用,以便在OS(操作系统)中创建和注册本地线程。

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

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

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

在多线程编程中,任务都是一些抽象且离散的工作单元,而线程是使任务异步执行的基本机 制。随着应用的扩张,线程和任务管理也变得非常复杂,为了简化这些复杂的线程管理模式,我们 需要一个“管理者”来统一管理线程及任务分配,这就是线程池。

在JUC中有关线程池的类与接口的架构图大致如图所示。

 (1)Executor

Executor是Java异步目标任务的“执行者”接口,其目标是来执行目标任务。“执行者”Executor 提供了execute()接口来执行已提交的Runnable执行目标实例。Executor作为执行者的角色,其目的 是“任务提交者”与“任务执行者”分离开来的机制。它只包含一个函数式方法:

void execute(Runnable command)

 (2)ExecutorService

ExecutorService继承于Executor。它是Java异步目标任务的“执行者服务”接口,对外提供异 步任务的接收服务,ExecutorService提供了“接收异步任务并转交给执行者”的方法,如submit系 列方法、invoke系列方法等。具体如下:

//向线程池提交单个异步任务
<T> Future<T> submit(Callable<T> task);
 //向线程池提交批量异步任务
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;

 (3)AbstractExecutorService

AbstractExecutorService是一个抽象类,它实现了ExecutorService接口。AbstractExecutorService 存在的目的是为ExecutorService中的接口提供默认实现。

 (4)ThreadPoolExecutor

ThreadPoolExecutor就是大名鼎鼎的“线程池”实现类,它继承于AbstractExecutorService抽 象类。

ThreadPoolExecutor是JUC线程池的核心实现类。线程的创建和终止需要很大的开销,线程池中预先提供了指定数量的可重用线程,所以使用线程池会节省系统资源,并且每个线程池都维护了 一些基础的数据统计,方便线程的管理和监控。

 (5)ScheduledExecutorService

ScheduledExecutorService是一个接口,它继承于ExecutorService。它是一个可以完成“延时” 和“周期性”任务的调度线程池接口,其功能和Timer/TimerTask类似。

(6)ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor继承于ThreadPoolExecutor,它提供了ScheduledExecutorService线 程池接口中“延时执行”和“周期执行”等抽象调度方法的具体实现。

ScheduledThreadPoolExecutor类似于Timer,但是在高并发程序中,ScheduledThreadPoolExecutor 的性能要优于Timer。

(7)Executors

Executors 是个静态工厂类 , 它通过静态工厂方法返回 ExecutorService 、 ScheduledExecutorService等线程池实例对象,这些静态工厂方法可以理解为一些快捷的创建线程池的方法。

newSingleThreadExecutor() 创建只有一个线程的线程池

newFixedThreadPool(int nThreads) 创建固定大小的线程池

newCachedThreadPool() 创建一个不限制线程数量的线程池,任何提交的任务都将立即执行, 但是空闲线程会得到及时回收

newScheduledThreadPool() 创建一个可定期或者延时执行任务的线程池

3  线程池的使用

大都是为了提升效率,并发的做某些事情;或者是将一个任务拆分成多个小任务,每个小任务都交给线程池去完成,之后将每个任务的结果合并。
或者是一些异步的场景,将任务直接交给线程池去做...等等,使用的场景非常多。先举个例子,我们是怎么使用线程池的:

public class ThreadPoolTest {

    public static ExecutorService createThreadPool() {
        // 创建一个线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                // 核心线程数设置为2
                2,
                // 最大线程数设置为5
                5,
                // 当线程池中的线程数量大于2,这多出的线程在没有任务执行的最大空闲时间,60
                60,
                // 空闲时间的单位,这里设置秒
                TimeUnit.SECONDS,
                // 线程池的阻塞队列使用LinkedBlockingQueue无界阻塞队列
                new LinkedBlockingQueue(),
                // 创建线程的线程工厂,这里test-pool是设置这个线程池的名字
                new DefaultThreadFactory("test-pool"),
                // 当任务太多,线程池线程不足、阻塞队列满时候采取什么拒绝策略
                new ThreadPoolExecutor.CallerRunsPolicy()

        );
        // 返回创建的线程池
        return executor;
    }
    static class DefaultThreadFactory implements ThreadFactory {
        private String name;
        private final AtomicInteger nextId = new AtomicInteger();
        DefaultThreadFactory(String name) {
            this.name = name;
        }
        @Override
        public Thread newThread(Runnable r) {
            return new Thread(null, r, name + "-" + nextId.incrementAndGet());
        }
    }

    public static void main(String[] args) {
        // 创建一个线程池
        ExecutorService executor = createThreadPool();
        for (int i = 0; i < 1000; i++) {
           final int num = i;
           // 封装任务,每个任务就是打印自己是当前第几个号
           Runnable task = () -> {
               System.out.println(num);
           };
           // 往线程池提交任务
           executor.execute(task);
       }
    }
}

上面就是一个线程池使用的小例子:
(1)createThreadPool方法创建出来一个线程池。
(2)然后有1000个任务要跑,每个任务要做的事就是打印自己当前是第几号
(3)封装1000个任务,调用execute方法提交1000个任务

上面的例子是ThreadPoolExecutor线程池,这种线程池是提交任务之后,如果有线程资源空闲会立即执行你提交的任务。
但是其实Java提供的还有另外一种线程池,支持任务的延迟调度、或者任务的周期性的调度的线程池,叫做ScheduledExecutorService,它的使用例子如下:

public class ScheduledThreadPoolTest {
    // 创建一个调度线程池
    public static ScheduledExecutorService createThreadPool() {
        ScheduledExecutorService executor = new ScheduledThreadPoolExecutor(
                // 线程池的核心线程数,配置为3
                3,
                // 创建线程的工厂,test-schedule-pool为线程池的名称
                new DefaultThreadFactory("test-schedule-pool"),
                // 任务拒绝策略,线程资源不足的时候,策略是“使用调用线程池的线程来执行”
                new ThreadPoolExecutor.CallerRunsPolicy()
        );
        return executor;
    }

    static class DefaultThreadFactory implements ThreadFactory {
        private String name;
        private final AtomicInteger nextId = new AtomicInteger();
        DefaultThreadFactory(String name) {
            this.name = name;
        }
        @Override
        public Thread newThread(Runnable r) {
            return new Thread(null, r, name + "-" + nextId.incrementAndGet());
        }
    }


    public static void main(String[] args) {
        // 获取调度线程池
        ScheduledExecutorService executor = createThreadPool();
        // 打印当前时间
        System.out.println("当前时间:" + new Date());
        // 立即执行的任务
        Runnable nowTask = () -> {
            System.out.println("当前时间:" + new Date() + "------执行【立即】任务");
        };
        // 延迟执行的任务
        Runnable delayTask = () -> {
            System.out.println("当前时间:" + new Date() + "------执行【延迟】任务");
        };
        // 周期性执行的任务
        Runnable periodTask = () -> {
            System.out.println("当前时间:" + new Date() + "------执行【周期性】任务");
        };

        // 提交立即任务,有线程空闲立即执行
        executor.execute(nowTask);
        // 提交一次性延迟任务,延迟2秒执行
        executor.schedule(delayTask, 2, TimeUnit.SECONDS);
        // 提交周期性的延迟任务,10秒后每3秒执行一次
        executor.scheduleWithFixedDelay(periodTask, 10, 3, TimeUnit.SECONDS);
    }
}

如上图所示,任务的提交时间都是在07:33:39这个时间
(1)其中【立即】任务调用execute方法的任务立即执行了,事实上ScheduleExecutorService继承了ExecutorService接口,具有和他一样的功能,也就是说具有提交任务,线程空闲时立即执行的功能
(2)同时看【延迟】任务,在2秒之后执行了一次,说明具有延迟任务的功能
(3)【周期性】任务,每隔3秒执行一次,说明具有定时任务的功能,是一个定时调度器
上面就是Java提供的两种类型的线程池了,一种是ExecutorService类型的线程池,代表性的子类是ThreadPoolExecutor;
另外一种是ScheduledExecutorService调度类型的线程池,代表性的子类是ScheduledThreadPoolExecutor。
同时JDK为了我们平时线程池的使用方便,提供了一个工具类Executors,里面封装了这两个类型的线程池的创建方法,如下:

public class ExecutorsTest {
    public static void main(String[] args) {
        // 创建一个缓存型的线程池,这种线程池每来一个任务就会创建一个线程来执行
        Executors.newCachedThreadPool();
        // 创建一个固定线程数量的线程池
        Executors.newFixedThreadPool(3);
        // 创建一个单线程的线程池
        Executors.newSingleThreadExecutor();
        // 创建一个单线程的调度线程池
        Executors.newSingleThreadScheduledExecutor();
        // 创建一个固定线程数量的调度线程池
        Executors.newScheduledThreadPool(3);
    }
}

如上代码所示:Executors这个工具类里面封装了蛮多创建线程的方法,来简化我们平时对线程池的创建。
但是平时我们一般是不用Executors这个工具类来创建线程池的,因为Executors里面创建的线程池,参数设置得很不合理,直接使用可能会有很大问题。

4  小结

本节我们简单先回顾下我们平时线程池的使用,下节我们会开始看线程池的实现细节,有理解不对的地方欢迎指正哈。

posted @ 2023-04-10 07:39  酷酷-  阅读(162)  评论(0编辑  收藏  举报