我们知道线程的创建与销毁需要和操作系统进行交互,这需要花费很大的系统开销。所以我们的程序中不应该频繁、无限制地创建线程,而是应该使用线程池。
Java中线程池技术,允许我们事先将一定数量的线程放入线程池中,池中的线程执行使用者提交的任务,并且线程执行任务后不会销毁,而是在池中准备为下一个请求提供服务,从而达到了线程的“复用”,减少并发线程的数目。
在线程池的编程模式下,任务是提交给整个线程池,而不是直接提交给某个线程,线程池在拿到任务后,就在内部寻找是否有空闲的线程,如果有,则将任务交给某个空闲的线程。
线程池的使用
在java.util.concurrent包下有一个静态工厂类Executors可创建JDK中预定义的几个不同配置的线程池:FixedThreadPool、CachedThreadPool、SingleThreadExecutor、ScheduledThreadPool和SingleThreadScheduledExecutor。
其中:
FixedThreadPool是一个可重用固定线程数的线程池,以共享的无界队列(LinkedBlockingQueue)方式来运行这些线程;
CachedThreadPool是一个根据需要创建新线程的线程池,但是在之前创建的线程可用时将重用它们,池中空闲的线程将只会保留“60s”;其内部使用的是SynchronousQueue作为工作队列,对于执行很多短期异步任务的程序而言,这种线程池通常可以提高程序的性能;
SingleThreadExecutor是一个使用单个worker线程的Executor,以无界队列方式来运行该线程。如果因为在关闭前的执行期间出现失败而终止了此单个线程,那么如果需要,一个新的线程将会代替它执行后续的任务,这可以保证顺利地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。
ScheduledThreadPool是一个可安排在给定延迟后运行命令或者定期地执行的线程池。
SingleThreadScheduledExecutor是一个单线程执行的线程池,它可安排在给定延迟后运行命令或者定期地执行。
我们可以使用Executors来为我们的程序创建任意一种
package com.test;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test2 {
public static void main(String[] args) {
// TODO Auto-generated method stub
ExecutorService executor = Executors.newFixedThreadPool(5) ;
for (int i = 0;i < 10;i++) {
executor.execute(new MyTask("" + i));
}
}
/*
* 自定义Runnable类
*/
static class MyTask implements Runnable {
private String name;
public MyTask(String name) {
this.name = name;
}
@Override
public void run() {
try {
System.out.println(this.toString() + " is running!");
Thread.sleep(3000); //让任务执行慢点
}catch (InterruptedException e) {
e.printStackTrace();
}
}
public String getName() {
return name;
}
@Override
public String toString() {
return "MyTask [name=" + name + "]";
}
}
}
上面的示例代码中创建来一个固定大小为5的 线程池,同时在 主线程中循环的往线程池中提交了10个任务,运行结果如下 :
从运行的过程中来看,是先运行了5个 线程 ,后运行了剩下 的5个线程。
使用自定义的线程池
其实,Executors类中的静态方法newFixedThreadPool方法其实是通过ThreadPoolExecutor类创建了一个corePoolSize和maximumPoolSize都等于设置的固定大小nThreads,keepAliveTime为0,工作队列为LinkedBlockingQueue,使用默认线程创建工厂和默认拒绝策略的线程池。源码如下:
其中因为请求队列是LinkedBlockingQueue,基于链表实现的阻塞队列,它允许的请求队列长度为Integer.MAX_VALUE,当线程池中的线程执行的任务耗时久,而又不断地有线程提交到工作队列中,这可能会在队列中堆积大量的请求,从而导致OOM(内存溢出)。在《阿里巴巴Java开发手册》中也明确指出了不允许使用Executors去创建,而是直接通过ThreadPoolExecutor的方式来创建线程池。
为了更安全地使用线程池,我们可以自己通过ThreadPoolExecutor来自定义一个更合理的线程池。
package com.test;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class Test {
public static void main(String[] args) {
//核心线程数量
int corePoolSize = 5;
//最大线程数量
int maximumPoolSize = 8;
//线程活跃时间
long keepAliveTime = 10000;
//keepAliveTime 参数的时间单位
TimeUnit unit = TimeUnit.MILLISECONDS;
//工作队列
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(2);
//线程创建工厂
ThreadFactory threadFactory = new TreadFactoryWithName();
//拒绝服务策略:AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
//预启动所有核心线程
poolExecutor.prestartAllCoreThreads();
for (int i = 0;i < 10;i++) {
MyTask task = new MyTask(String.valueOf(i));
poolExecutor.execute(task);
}
}
/*
* 线程创建工厂类
*/
public static class TreadFactoryWithName implements ThreadFactory {
//原子方式更新的int值(CAS无锁并发)
private final AtomicInteger mThreadNum = new AtomicInteger();
@Override
public Thread newThread(Runnable r) {
// 推荐创建带有名字的线程
Thread t = new Thread(r, "MyThread -- " + mThreadNum.getAndIncrement());
System.out.println(t.getName() + " has been created");
return t;
}
}
/*
* 自定义Runnable类
*/
static class MyTask implements Runnable {
private String name;
public MyTask(String name) {
this.name = name;
}
@Override
public void run() {
try {
System.out.println(this.toString() + " is running!");
Thread.sleep(3000); //让任务执行慢点
}catch (InterruptedException e) {
e.printStackTrace();
}
}
public String getName() {
return name;
}
@Override
public String toString() {
return "MyTask [name=" + name + "]";
}
}
}
上面的示例代码通过ThreadPoolExecutor类创建了一个核心线程数为5、最大线程数为8、线程活跃时间为10s、工作队列为一个容量为2的ArrayBlockingQueue,以及一个自定义的线程工厂。运行结果如下:
在代码中我们先预启动了所有的核心线程,即创建5个核心线程,从运行结果我们可以看到,一共创建了8个线程 ,10个任务都 成功地的执行结束了。
线程池的运行机制
仅仅知道如何使用线程池是远远不够的,想要熟练地运用线程池,我们还必须知道线程池的设计和实现原理,了解它的运行机制。
Executor是一个基础接口,其初衷是将任务的提交与执行细节解耦,可以从它仅有的一个方法execute()中看到这一点。
ExecutorService继承了Executor接口(上图中实线表示继承,虚线表示实现),并进行了拓展,它不仅仅是通常意义上的线程“池”,还提供了强大的线程管理、任务提交等方法。
ThreadPoolExecutor则 是更加具体的线程池的实现,JDK中的预定义的几个线程池都是通过ThreadPoolExecutor创建的。
了解了大致的线程池框架后,我们就需要去理解一下线程池的运行机制,即当任务提交到线程池后会发生什么?
从上图中可以看到,当提交一个新任务到线程池时,线程池的处理流程如下:
(1) 首先判断核心线程是否已满。如果没满,则创建一个新的工作线程(核心线程)来执行任务。如果核心线程已满,则进入下一步。
(2) 判断工作队列(阻塞队列)是否已满。如果没满,则将任务加入到工作队列中,等待线程执行。如果工作队列已满,则进入下一步。
(3) 判断线程池是否已达到最大线程数。如果没有达到最大线程数,则创建一个非核心线程执行任务。
(4) 如果达到了最大线程数,则执行拒绝服务的策略。
以上就是线程池的主要工作机制,其引入阻塞队列,并且在阻塞队列满了之后才会去创建非核心线程的设计思路主要是因为创建一个线程需要额外的开销,且在线程池中创建一个工作线程(Worker)还需要获取加全局锁mainLock,即(1)(3)步都需要获取全局锁,会影响线程池的效率。所以其实当核心线程都启动完后,整个线程池的运行都主要是在执行第二个步骤。
线程池的实现原理及源码分析
在ThreadPoolExecutor线程池中,真正存储线程池中所有的工作线程的数据结构是HashSet。
ThreadPoolExecutor的第一个成员变量是ctl,它是一个AtomicInteger类型的对象,可以通过CAS(compareAndSwap)达到无锁并发,效率很高。
同时,变量ctl还有双重身份,它的高三位表示线程池的状态,而低29位则表示线程池中现有的线程数。
线程池有5种状态,分别是RUNNING、SHUTDOWN、STOP、TIDYING和TERMINATED:
RUNNING, 运行状态,值也是最小的,刚创建的线程池就是此状态;
SHUTDOWN,停工状态,不再接收新任务,已经接收的会继续执行;
STOP,停止状态,不再接收新任务,已经接收正在执行的,也会中断清空状态,所有任务都停止了,工作的线程也全部结束了;
TIDYING,整理状态,线程池STOP状态和SHUTDOWN状态之后都将进入到TIDYING状态,再转变为TERMINATED状态;
TERMINATED,终止状态,线程池已销毁;
线程池的状态迁移如下图:
了解了线程池的主要变量后,我们再来看一下线程池的创建:
上图是ThreadPoolExecutor线程池的构造方法,我们一般也都是通过该方法来构造一个自定义的线程池,在上面的示例中其实已经简单的介绍过了该方法中的几个参数,下面我们需要重点关注一下workQueue和handle。
线程池框架中的工作队列workQueue是使用的阻塞队列,所有的BlockingQueue都可以作为工作队列。在线程池中,一般有三种通用的阻塞策略:
(1) 直接提交。直接将任务提交给线程而不保持它们,如果线程池中不存在可以立即运行任务的线程,则“试图把任务加入队列失败”,因此会构造一个新的线程,当线程池中的线程达到maximumPoolSize,则无法创建线程,触发拒绝策略handle。
(2) 无界队列。无界队列(例如不指定预定容量的LinkedBlockingQueue) 将导致在所有corePoolSize线程都忙时新任务在队列中等待。这样,创建的线程就不会超过corePoolSize(因此,maximumPoolSize的值也就无效了)。
(3) 有界队列。当使用有限的maximumPoolSize时,有界队列(如ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大线程数量需要相互折中:使用大型队列和小型池可以最大限度的降低CPU使用率、操作系统资源和上下文切换开销,但是可能导致导致人工降低吞吐量。如果任务频繁阻塞(如I/O操作),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的maximumPoolSize,CPU使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。
在线程池框架中,对应阻塞策略有以下几种常用的工作队列(阻塞队列):
ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则进行排序。
LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常高于ArrayBlockingQueue。
SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常高于LinkedBlockingQueue。
PriorityBlockingQueue:一个具有优先级的无界阻塞队列。
RejectedExecutionHandler是线程池中定义的拒绝策略,当线程池中的线程数量达到maximumPoolSize,没有可以使用可以使用的空闲线程,且工作队列满时,新提交的任务将会触发RejectedExecutionHandler。
这个策略在默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。线程池框架中提供了以下四种策略:
AbortPolicy:直接抛出异常;
CallerRunsPolicy:只用调用者所在线程来运行任务。
DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
DiscardPolicy:不处理,丢弃掉。
了解了线程池后,继续看线程池的任务提交,我们可以通过execute和submit方法向线程池提交任务。
execute用于提交不需要返回值得任务,所以无法判断任务是否被线程池执行成功。
submit方法用于提交需要返回值得任务,线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以获取任务结果和取消任务。
通过execute方法源码来了解一下线程池任务的提交:
在线程池中,工作线程被封装成Worker,当需要向线程池中添加线程时就需要调用addWorker方法。
addWorker方法源码:
private boolean addWorker(Runnable firstTask, boolean core) {
//相当于goto,虽然不建议滥用,但这里使用又觉得没一点问题
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
//如果线程池的状态到了SHUTDOWN或者之上的状态时候,只有一种情况还需要继续添加线程,
//那就是线程池已经SHUTDOWN,但是队列中还有任务在排队,而且不接受新任务(所以firstTask必须为null)
//这里还继续添加线程的初衷是,加快执行等待队列中的任务,尽快让线程池关闭
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
//传入的core的参数,唯一用到的地方,如果线程数超过理论最大容量,如果core是true跟最大核心线程数比较,否则跟最大线程数比较
if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize))
return false;
//通过CAS自旋,增加线程数+1,增加成功跳出双层循环,继续往下执行
if (compareAndIncrementWorkerCount(c))
break retry;
//检测当前线程状态如果发生了变化,则继续回到retry,重新开始循环
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
//走到这里,说明我们已经成功的将线程数+1了,但是真正的线程还没有被添加
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
//添加线程,Worker是继承了AQS,实现了Runnable接口的包装类
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
//到这里开始加锁
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());
//检查线程状态,还是跟之前一样,只有当线程池处于RUNNING,或者处于SHUTDOWN并且firstTask==null的时候,这时候创建Worker来加速处理队列中的任务
if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) {
//线程只能被start一次
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
//workers是一个HashSet,添加我们新增的Worker
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
//启动Worker
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
当程序中不再使用线程池时,就需要关闭线程池,但是线程池是不会自行停止的,线程池中的线程会循环的接收并执行任务,我们可以通过调用线程池的shutdown或者shutdownNow方法来关闭线程池。
它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。
但是它们存在一定的区别,shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。
而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
合理配置线程池
CPU密集型任务应配置尽可能小的线程,如配置Ncpu+1个线程的线程池。由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*Ncpu。
可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。
线程池的监控
可以通过继承线程池来自定义线程池,重写线程池的beforeExecute、afterExecute和terminated方法,也可以在任务执行前、执行后和线程池关闭前执行一些代码来进行监控。
线程池的优点
总的来说,Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来3个好处。
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。
池化技术
将宝贵的资源放置在一个池子中,每次都从池子中获取资源,用完又放回池子中。可以通过池化技术来减少系统资源消耗,提升系统性能。
常见的池化技术的应用:数据库连接池、Redis连接池、HTTP连接池。