【从0到1学习Java线程池】Java线程池的简介以及使用

这是【从0到1学习Java线程池】系列文章的第 壹 篇,该系列文章总共三篇,介绍了 Java 线程池的使用以及原理,并且最后会实现一个基本的线程池。本篇文章主要介绍了 Java 线程池以及它的使用。

【从0到1学习Java线程池】系列文章共有3篇,目录如下:

线程池是什么?

线程池用于多线程处理中,它可以根据系统的情况,可以有效控制线程执行的数量,优化运行效果。线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。

线程池的作用

在面向对象的编程过程中,创建对象和销毁对象是非常消耗时间和资源的。因此想要最小化这种消耗的一种思想就是『池化资源』。线程池就是这样的一种思想。我们通过重用线程池中的资源来减少创建和销毁线程所需要耗费的时间和资源。

线程池的一个作用是创建和销毁线程的次数,每个工作线程可以多次使用;另一个作用是可根据系统情况调整执行的线程数量,防止消耗过多内存。另外,通过线程池,能有效的控制线程的最大并发数,提高系统资源利用率,同时避免过多的资源竞争,避免堵塞。

线程池的优点总结如下几个方面:

  • 线程复用
  • 控制最大并发数
  • 管理线程

线程池的组成

一般的线程池主要分为以下4个组成部分:

  1. 线程池管理器:用于创建并管理线程池
  2. 工作线程:线程池中的线程
  3. 任务接口:每个任务必须实现的接口,用于工作线程调度其运行
  4. 任务队列:用于存放待处理的任务,提供一种缓冲机制

线程池的常见应用场景

许多服务器应用常常需要处理大量而短小的请求(例如,Web 服务器,数据库服务器等等),通常它们收到的请求数量很大,一个简单的模型是,当服务器收到来自远程的请求时,为每一个请求开启一个线程,在请求完毕之后再对线程进行销毁。这样处理带来的问题是,创建和销毁线程所消耗的时间往往比任务本身所需消耗的资源要大得多。那么应该怎么办呢?

线程池为线程生命周期开销问题和资源不足问题提供了解决方案。我们可以通过线程池做到线程复用,不需要频繁的创建和销毁线程,让线程池中的线程一直存在于线程池中,然后线程从任务队列中取得任务来执行。而且这样做的另一个好处有,通过适当地调整线程池中的线程数目,也就是当请求的数目超过某个阈值时,就强制其它任何新到的请求一直等待,直到获得一个线程来处理为止,从而可以防止资源不足。

Java线程池的简介

Java中提供了实现线程池的框架Executor,并且提供了许多种类的线程池,接下来的文章中将会做详细介绍。

Java线程池框架

Java中的线程池是通过Executor框架实现的,该框架中用到了ExecutorExecutorsExecutorServiceThreadPoolExecutorCallableFutureFutureTask这几个类。

  • Executor:所有线程池的接口,只有一个方法
  • Executors:Executor 的工厂类,提供了创建各种不同线程池的方法,返回的线程池都实现了ExecutorService 接口
  • ThreadPoolExecutor:线程池的具体实现类,一般所有的线程池都是基于这个类实现的

其中ThreadPoolExecutor的构造方法如下:

1
2
3
4
5
6
7
8
9
10
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {

this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);

}

其中:

  • corePoolSize:线程池的核心线程数,线程池中运行的线程数也永远不会超过 corePoolSize 个,默认情况下会永远存活
  • maximumPoolSize:线程池中允许的最大线程数
  • keepAliveTime:空闲线程结束的超时时间
  • unit:是一个枚举,它表示的是 keepAliveTime 的单位
  • workQueue:工作队列,用于任务的存放

Java线程池的工作过程

Java线程池的工作过程如下:

  1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
  2. 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
  3. 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
  4. 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
  5. 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
  6. 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常RejectExecutionException。
  7. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  8. 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

常见的Java线程池

生成线程池使用的是Executors的工厂方法,以下是常见的 Java 线程池:

SingleThreadExecutor

SingleThreadExecutor是单个线程的线程池,即线程池中每次只有一个线程在运行,单线程串行执行任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

1
2
3
4
5
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0,Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}

FixedThreadPool

FixedThreadPool是固定数量的线程池,只有核心线程,每提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面进入等待队列,直到前面的任务完成才继续执行。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

1
2
3
4
5
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}

CachedThreadPool

CachedThreadPool是可缓存线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。其中,SynchronousQueue是一个是缓冲区为1的阻塞队列。

1
2
3
4
5
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0,Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}

ScheduledThreadPool

ScheduledThreadPool是核心线程池固定,大小无限制的线程池,支持定时和周期性的执行线程。创建一个周期性执行任务的线程池。如果闲置,非核心线程池会在DEFAULT_KEEPALIVEMILLIS时间内回收。

1
2
3
4
5
public static ExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPool(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}

Java 线程池的创建和使用

我们可以通过Executors的工厂方法来创建一个线程池。但是我们该如何让线程池执行任务呢?

线程池最常用的提交任务的方法有两种:

  • execute:

    1
    ExecutorService.execute(Runnable runable);
  • submit:

    1
    2
    3
    FutureTask task = ExecutorService.submit(Runnable runnable);
    FutureTask<T> task = ExecutorService.submit(Runnable runnable,T Result);
    FutureTask<T> task = ExecutorService.submit(Callable<T> callable);

可以看出submit开启的是有返回结果的任务,会返回一个FutureTask对象,这样就能通过get()方法得到结果。submit最终调用的也是execute(Runnable runable)submit只是将Callable对象或Runnable封装成一个FutureTask对象,因为FutureTask是个Runnable,所以可以在execute中执行。

下面的示例代码演示了如何创建一个线程池,并且使用它管理线程:

1
2
3
4
5
6
public class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running.");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class TestSingleThreadExecutor {
public static void main(String[] args) {
//创建一个可重用固定线程数的线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
//创建实现了Runnable接口对象
Thread tt1 = new MyThread();
Thread tt2 = new MyThread();
Thread tt3 = new MyThread();
Thread tt4 = new MyThread();
Thread tt5 = new MyThread();
//将线程放入池中并执行
pool.execute(tt1);
pool.execute(tt2);
pool.execute(tt3);
pool.execute(tt4);
pool.execute(tt5);
//关闭
pool.shutdown();
}
}

运行结果:

1
2
3
4
5
pool-1-thread-1 is running.
pool-1-thread-2 is running.
pool-1-thread-1 is running.
pool-1-thread-2 is running.
pool-1-thread-1 is running.

本文的版权归作者 罗远航 所有,采用 Attribution-NonCommercial 3.0 License。任何人可以进行转载、分享,但不可在未经允许的情况下用于商业用途;转载请注明出处。感谢配合!