【Java】多线程 - 线程池(Thread Pool)

Posted by 西维蜀黍 on 2019-02-25, Last Modified on 2021-09-21

什么是线程池(Thread Pool)?

为了避免系统频繁地创建和销毁线程,我们可以让创建的线程进行复用。 用线程时从线程池中获取,用完以后不销毁线程,而是归还给线程池(Thread Pool)。

为什么需要使用线程池

减少线程创建与切换的开销

在没有使用线程池的时候,来了一个任务,就创建一个线程,我们知道系统创建和销毁工作线程的开销很大,而且频繁的创建线程也就意味着需要进行频繁的线程切换,这都是一笔很大的开销。

控制线程的数量

使用线程池我们可以有效地控制线程的数量,当系统中存在大量并发线程时,会导致系统性能剧烈下降。

线程池做了什么

重复利用有限的线程

线程池中会预先创建一些空闲的线程,他们不断的从工作队列中取出任务,然后执行,执行完之后,会继续执行工作队列中的下一个任务,减少了创建和销毁线程的次数,每个线程都可以一直被重用,变了创建和销毁的开销。

JDK 对线程池的支持

为了更好的控制多线程,JDK 提供了一套线程池框架。它们都在 java.util.concurrent 包中。

  • Executor 用来执行任务,它提供了 execute() 方法来执行 Runnable 任务;
  • ThreadPoolExecutor 表示一个线程池;
  • Executors 是一个线程池工厂类,该工厂类包含如下集合静态工厂方法来创建线程池:
    • newFixedThreadPool():创建一个可重用的、具有固定线程数的线程池
    • newSingleThreadExecutor():创建只有一个线程的线程池
    • newCachedThreadPool():创建一个具有缓存功能的线程池
    • newWorkStealingPool():创建持有足够线程的线程池来支持给定的并行级别的线程池
    • newScheduledThreadPool():创建具有指定线程数的线程池,它可以在指定延迟后执行任务线程

newFixedThreadPool()

newFixedThreadPool() 返回固定数量的线程池。线程池中的数量始终不变。当有新任务提交时,线程池中若有空闲线程,则立即执行。若没有则放入任务队列中,等待有空闲线程时,处理任务队列中的任务。

实例代码

public class MyFixThreadPool {

    public static void main(String[] args) throws InterruptedException {
        // 创建一个线程数固定为5的线程池
        ExecutorService service = Executors.newFixedThreadPool(5);

        System.out.println("初始线程池状态:" + service);

        for (int i = 0; i < 6; i++) {
            service.execute(() -> {
                try {
                    TimeUnit.MILLISECONDS.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());
            });
        }
        System.out.println("线程提交完毕之后线程池状态:" + service);

        service.shutdown();//会等待所有的线程执行完毕才关闭,shutdownNow:立马关闭
        System.out.println("是否全部线程已经执行完毕:" + service.isTerminated());//所有的任务执行完了,就会返回true
        System.out.println("是否已经执行shutdown()" + service.isShutdown());
        System.out.println("执行完shutdown()之后线程池的状态:" + service);

        TimeUnit.SECONDS.sleep(5);
        System.out.println("5秒钟过后,是否全部线程已经执行完毕:" + service.isTerminated());
        System.out.println("5秒钟过后,是否已经执行shutdown()" + service.isShutdown());
        System.out.println("5秒钟过后,线程池状态:" + service);
    }

}

运行结果

初始线程池状态:[Running, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0]
线程提交完毕之后线程池状态:[Running, pool size = 5, active threads = 5, queued tasks = 1, completed tasks = 0]
是否全部线程已经执行完毕:false
是否已经执行shutdown():true
执行完shutdown()之后线程池的状态:[Shutting down, pool size = 5, active threads = 5, queued tasks = 1, completed tasks = 0]
pool-1-thread-2
pool-1-thread-1
pool-1-thread-4
pool-1-thread-5
pool-1-thread-3
pool-1-thread-2
5秒钟过后,是否全部线程已经执行完毕:true
5秒钟过后,是否已经执行shutdown():true
5秒钟过后,线程池状态:[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 6]

newSingleThreadPool()

newSingleThreadPool() 返回只有一个线程的线程池。若有多余一个任务被提交,则放入任务队列中,等待有空闲线程时,按先入先出的顺序执行队列中的任务。

实例代码

public class SingleThreadPool {

    public static void main(String[] args) {
        ExecutorService service = Executors.newSingleThreadExecutor();

        for (int i = 0; i < 5; i++) {
            final int j = i;
            service.execute(() -> {
                System.out.println(j + " " + Thread.currentThread().getName());
            });
        }
    }

}
复制代码

运行结果

0 pool-1-thread-1 1 pool-1-thread-1 2 pool-1-thread-1 3 pool-1-thread-1 4 pool-1-thread-1

程序分析

可以看到只有pool-1-thread-1一个线程在工作。

newCachedThreadPool()

newCachedThreadPool() 返回一个可根据实际情况调整线程数量的线程池。这个方法创建出来的线程池可以被无限扩展,并且当需求降低时会自动收缩。

实例代码

public class CachePool {

    public static void main(String[] args) throws InterruptedException {
        ExecutorService service = Executors.newCachedThreadPool();
        System.out.println("初始线程池状态:" + service);

        for (int i = 0; i < 12; i++) {
            service.execute(() -> {
                try {
                    TimeUnit.MILLISECONDS.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());
            });
        }
        System.out.println("线程提交完毕之后线程池状态:" + service);

        TimeUnit.SECONDS.sleep(50);
        System.out.println("50秒后线程池状态:" + service);

        TimeUnit.SECONDS.sleep(30);
        System.out.println("80秒后线程池状态:" + service);
    }

}

运行结果

初始线程池状态:[Running, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0] 线程提交完毕之后线程池状态:[Running, pool size = 12, active threads = 12, queued tasks = 0, completed tasks = 0] pool-1-thread-3 pool-1-thread-4 pool-1-thread-1 pool-1-thread-2 pool-1-thread-5 pool-1-thread-8 pool-1-thread-9 pool-1-thread-12 pool-1-thread-7 pool-1-thread-6 pool-1-thread-11 pool-1-thread-10 50秒后线程池状态:[Running, pool size = 12, active threads = 0, queued tasks = 0, completed tasks = 12] 80秒后线程池状态:[Running, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 12]

程序分析

  • 因为我们每个线程任务至少需要500毫秒的执行时间,所以当我们往线程池中提交12个任务的过程中,基本上没有空闲的线程供我们重复使用,所以线程池会创建12个线程。
  • 缓存中的线程默认是60秒没有活跃就会被销毁掉,可以看到在50秒钟的时候回,所有的任务已经完成了,但是线程池线程的数量还是12。
  • 80秒过后,可以看到线程池中的线程已经全部被销毁了。

newScheduledThreadPool()

newScheduledThreadPool() 返回一个 ScheduledExecutorService 对象,并可指定线程数量。

示例代码

下面代码每500毫秒打印一次当前线程名称以及一个随机数字。

public class MyScheduledPool {

    public static void main(String[] args) {
        ScheduledExecutorService service = Executors.newScheduledThreadPool(4);
        service.scheduleAtFixedRate(() -> {
            System.out.println(Thread.currentThread().getName() + new Random().nextInt(1000));
        }, 0, 500, TimeUnit.MILLISECONDS);
    }
}

Reference