文章目录
引言
对于线程,最主要的用途就是处理异步任务。现在假设有一个异步任务要处理,使用线程处理,我们简单的用时间来衡量这个任务的执行过程:
T1:创建线程的时间;
T2:线程执行任务的时间;
T3:线程销毁的时间;
所以整个任务所需的时间就是T = T1 + T2 + T3;从T的时间成分来看,T1和T3是线程本身带来的开销,从使用者的角度看,如果使用线程,这个时间是无法避免的,在当前场景下,这个时间似乎还是可以接受的。
再假设一个较为复杂的场景,有N个异步任务要处理,完成任务的时间就是NT,但是,这里面就包含了NT1 + N*T2,在这种场景下,使用者就有点无法接受这种时间开销了;
上面的例子仅仅是从时间这一条线来分析的,线程的创建同时会消耗内存和CPU,多线程场景下,使用线程对移动端设备会产生很多不利影响:
- 线程的创建和销毁都需要时间,当有大量的线程创建和销毁时,那么这些时间的销毁会变得很明显,导致性能上的缺失。同时线程的大量创建和销毁会导致GC频繁执行,会引起内存抖动,进而导致移动端界面显示卡顿;
- 线程的大量创建和销毁时非常耗费内存和CPU的,这会影响系统吞吐量,导致性能急剧下降,变现为手机卡顿,如果内存占用过多,还有可能会导致OOM;
这里暴露出了线程的弱点,而解决的办法就是从T1和T2入手,可以通过重用已有的线程,来减少甚至去掉这个开销。这里就引入了线程池(ExecutorService)的概念了,线程池的基本作用就是复用线程。
线程池(ExecutorService)
通过上述分析,我们知道了通过创建线程去处理任务的弊端,而为了解决这些问题,Java提供了ExecutorService线程池来优化和管理线程的使用。
线程池的优点
- 线程的创建和销毁由线程池来维护,线程池的线程复用机制,能减少线程创建和销毁,节约系统开销;
- 使用线程池,我们能控制线程池的并发数,制定线程定时任务,确定单线程的执行顺序等,避免因为大量线程争夺CPU资源造成阻塞;
ExecutorService简介
ExecutorService其实是一个接口,他提供了众多接口API来控制线程池中的线程,而真正意义上的线程池是:ThreadPoolExecutor,他实现了ExecutorService接口,并封装了一系列的API使得他具有线程池的特征,其中包括工作队列,核心线程数,最大线程数等。
线程池:ThreadPoolExecutor
既然线程池就是ThreadPoolExecutor,所以要创建一个线程池只需要new ThreadPoolExecutor(….); 就可以创建一个线程池了,他的构造方法如下:
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {...}
其构造方法参数说明如下:
- corePoolSize:线程池中的核心线程数量;
- maximumPoolSize:线程池中的最大线程数量;
- keepAliveTime:线程池中线程“保持活动时间“,它起作用必须在一个前提下,就是当线程池中的线程数量超过了corePoolSize时,它表示多余的空闲线程的存活时间,即:多余的空闲线程在超过keepAliveTime时间内没有任务的话则被销毁。而这个主要应用在缓存线程池中;
- unit:它是一个枚举类型,表示keepAliveTime的单位,常用的如:TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒);
- workQueue:任务队列,主要用来存储已经提交但未被执行的任务,不同的线程池采用的排队策略不一样;
- threadFactory:线程工厂,用来创建线程池中的线程,通常用默认的即可;
- handler:通常叫做拒绝策略。
1、在线程池已经关闭的情况下
2、任务太多导致最大线程数和任务队列已经饱和,无法再接收新的任务。
在上面两种情况下,只要满足其中一种时,在使用execute()来提交新的任务时将会拒绝,而默认的拒绝策略是抛一个RejectedExecutionException异常;
这里可以看出使用这种方法创建线程需要配置很多参数,所以官方不推荐使用这种方法,而是推荐使用Executors的工厂方法来创建线程池,Executors类是官方提供的一个工厂类,其中封装了很多功能不同的线程池,这使得创建线程池变得很简单,日常开发主要使用如下四种功能不同的线程池:
1.newFixedThreadPool():
作用:该方法返回一个固定线程数量的线程池,该线程池中的线程数量始终不变,即不会再创建新的线程,也不会销毁已经创建好的线程,自始自终都是那几个固定的线程在工作,所以该线程池可以控制线程的最大并发数。其任务队列为:LinkedBlockingQueue(无界队列)。实例:假如有一个新任务提交时,线程池中如果有空闲的线程则立即使用空闲线程来处理任务,如果没有,则会把这个新任务存在一个任务队列中,一旦有线程空闲了,则按FIFO方式处理任务队列中的任务。
获取方法:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
工厂方法内部实现:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
2.newCachedThreadPool():
作用:该方法返回一个可以根据实际情况调整线程数量的线程池。即该线程池中的线程数量不确定,是根据实际情况动态调整的。其任务队列为:SynchronousQueue(直接提交的队列)。实例:假如该线程池中的所有线程都正在工作,而此时有新任务提交,那么将会创建新的线程去处理该任务,而此时假如之前有一些线程完成了任务,现在又有新任务提交,那么将不会创建新线程去处理,而是复用空闲的线程去处理新任务。线程池中的线程都有一个“保持活动时间”的参数,通过配置它,如果线程池中的空闲线程的空闲时间超过该“保存活动时间”则立刻停止该线程,而该线程池默认的“保持活动时间”为60s。
获取方法:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
工厂方法内部实现:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
3.newSingleThreadExecutor():
作用:该方法返回一个只有一个线程的线程池,即每次只能执行一个线程任务,多余的任务会保存到一个任务队列中,等待这一个线程空闲,当这个线程空闲了再按FIFO方式顺序执行任务队列中的任务。其任务队列为:LinkedBlockingQueue(无界队列)。
获取方法
ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
工厂方法内部实现:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
4.newScheduledThreadPool():
作用:该方法返回一个可以控制线程定时或周期性执行某任务的线程池。和定时任务相关的线程池比较特殊(newScheduledThreadPool()、newSingleThreadScheduledExecutor()),它们创建的线程池内部实现是由ScheduledThreadPoolExecutor这个类实现的,而ScheduledThreadPoolExecutor是继承于ThreadPoolExecutor扩展而成的,所以本质还是一样的,只不过多封装了一些定时任务相关的API。其任务队列为:DelayedWorkQueue(等待队列)。
获取方法:
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
工厂方法内部实现:
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
线程池ThreadPoolExecutor的使用
使用线程池,其中涉及到一个极其重要的方法,即:
execute(Runnable command);
该方法意为执行给定的任务,该任务处理可能在新的线程、已入池的线程或者正调用的线程,这由ThreadPoolExecutor的实现决定。
1.使用newFixedThreadPool
创建一个固定线程数量的线程池,示例为:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 1; i <=10; i++) {
final int index = i;
fixedThreadPool.execute(new Runnable() {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
Log.v("zxy", "线程:"+threadName+",正在执行第" + index + "个任务");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
效果:上述代码创建了一个线程数为3的固定线程数量的线程池,同理该线程池支持的线程最大并发数也是3,模拟了10个任务让它处理,执行的情况则是首先执行前三个任务,后面7个则依次进入任务队列进行等待,执行完前三个任务后,再通过FIFO的方式从任务队列中取任务执行,直到最后任务都执行完毕。
2.使用newSingleThreadExecutor
创建一个只有一个线程的线程池,每次只能执行一个线程任务,多余的任务会保存到一个任务队列中,等待线程处理完再依次处理任务队列中的任务,示例为:
ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
for (int i = 1; i <=10; i++) {
final int index = i;
singleThreadPool.execute(new Runnable() {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
Log.v("zxy", "线程:"+threadName+",正在执行第" + index + "个任务");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
效果:即依次一个一个的处理任务,而且都是复用一个线程。其实通过newSingleThreadExecutor()和newFixedThreadPool()的方法发现,创建一个singleThreadExecutorPool实际上就是创建一个核心线程数和最大线程数都为1的fixedThreadPool。
4.使用newCachedThreadPool
创建一个可以根据实际情况调整线程池中线程的数量的线程池,示例为:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 1; i <= 10; i++) {
final int index = i;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
cachedThreadPool.execute(new Runnable() {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
Log.v("zxy", "线程:" + threadName + ",正在执行第" + index + "个任务");
try {
long time = index * 500;
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
效果:为了体现该线程池可以自动根据实现情况进行线程的重用,而不是一味的创建新的线程去处理任务,设置了每隔1s去提交一个新任务,这个新任务执行的时间也是动态变化的。
5.使用newScheduledThreadPool
创建一个可以定时或者周期性执行任务的线程池,示例为:
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
//延迟2秒后执行该任务
scheduledThreadPool.schedule(new Runnable() {
@Override
public void run() {
}
}, 2, TimeUnit.SECONDS);
//延迟1秒后,每隔2秒执行一次该任务
scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
}
}, 1, 2, TimeUnit.SECONDS);
6.newSingleThreadScheduledExecutor
创建一个可以定时或者周期性执行任务的线程池,该线程池的线程数为1,示例为:
ScheduledExecutorService singleThreadScheduledPool = Executors.newSingleThreadScheduledExecutor();
//延迟1秒后,每隔2秒执行一次该任务
singleThreadScheduledPool.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
Log.v("zxy", "线程:" + threadName + ",正在执行");
}
},1,2,TimeUnit.SECONDS);
实际上二者没什么太大区别,只不过是线程池内线程数量的不同,都是每隔2秒就会执行一次该任务。
自定义线程池ThreadPoolExecutor
Java内置只提供了五种常用的线程池,一般来说这足够用了,不过有时候也可以根据需求来自定义线程池,而要自定义不同功能的线程池,上面说了线程池功能的不同归根到底还是内部的BlockingQueue实现不同,所以,要实现自定义的线程池,就必须从BlockingQueue的实现上做手脚,而上面也说了BlockingQueue的实现类有多个,下面的例子中就选用PriorityBlockingQueue来实现一个功能是按任务的优先级来处理的线程池。
1、首先创建一个基于PriorityBlockingQueue实现的线程池,为了测试方便,把核心线程数量设置为3,如下:
ExecutorService priorityThreadPool =newThreadPoolExecutor(3,3,0L,TimeUnit.SECONDS,newPriorityBlockingQueue());
2、然后创建一个实现Runnable接口的类,并向外提供一个抽象方法供实现自定义功能,并实现Comparable接口,实现这个接口主要就是进行优先级的比较,代码如下:
public abstract class PriorityRunnable implements Runnable, Comparable {
private int priority;
public PriorityRunnable(int priority) {
if (priority 0)
throw new IllegalArgumentException();
this.priority = priority;
}
@Override
public int compareTo(PriorityRunnable another) {
int my = this.getPriority();
int other = another.getPriority();
return my 1 : my > other ? -1 : 0;
}
@Override
public void run() {
doSth();
}
public abstract void doSth();
public int getPriority() {
return priority;
}
}
3、使用我们自己的PriorityRunnable提交任务,整体代码如下:
ExecutorService priorityThreadPool = new ThreadPoolExecutor(3, 3, 0L, TimeUnit.SECONDS, new PriorityBlockingQueue());
for (int i = 1; i <=10; i++) {
final int priority = i;
priorityThreadPool.execute(new PriorityRunnable(priority) {
@Override
public void doSth() {
String threadName = Thread.currentThread().getName();
Log.v("zxy", "线程:" + threadName + ",正在执行优先级为:" + priority + "的任务");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
由于核心线程数设置为3,刚开始时,系统有3个空闲线程,所以无须使用任务队列,而是直接运行前三个任务,而后面再提交任务时由于当前没有空闲线程所以加入任务队列中进行等待,此时,由于任务队列实现是由PriorityBlockingQueue实现的,所以进行等待的任务会经过优先级判断,优先级高的放在队列前面先处理。从效果图中也可以看到后面的任务是先执行优先级高的任务,然后依次递减。
4、优先级线程池的优点
从上面可以得知,创建一个优先级线程池非常有用,它可以在线程池中线程数量不足或系统资源紧张时,优先处理我们想要先处理的任务,而优先级低的则放到后面再处理,这极大改善了系统默认线程池以FIFO方式处理任务的不灵活
扩展线程池ThreadPoolExecutor
除了内置的功能外,ThreadPoolExecutor也向外提供了三个接口供我们自己扩展满足我们需求的线程池,这三个接口分别是:
- beforeExecute()–任务执行前执行的方法;
- afterExecute() -任务执行结束后执行的方法;
- terminated() -线程池关闭后执行的方法;
这三个方法在ThreadPoolExecutor内部都没有实现。
前面两个方法我们可以在ThreadPoolExecutor内部的runWorker()方法中找到,而runWorker()是ThreadPoolExecutor的内部类Worker实现的方法,Worker它实现了Runnable接口,也正是线程池内处理任务的工作线程,而Worker.runWorker()方法则是处理我们所提交的任务的方法,它会同时被多个线程访问,所以我们看runWorker()方法的实现,由于涉及到多个线程的异步调用,必然是需要使用锁来处理,而这里使用的是Lock来实现的,我们来看看runWorker()方法内主要实现:
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
可以看到在task.run()之前和之后分别调用了beforeExecute和afterExecute方法,并传入任务Runnable对象。而terminated()则是在关闭线程池的方法中调用,而关闭线程池有两个方法:
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(SHUTDOWN);
interruptIdleWorkers();
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
tryTerminate();
}
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(STOP);
interruptWorkers();
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}
所以,要扩展线程池,只需要重写这三个方法,并实现自己的功能即可,这三个方法分别都会在任务执行前调用、任务执行完成后调用、线程池关闭后调用。这里验证一下,继承自ThreadPoolExecutor并实现那三个方法:
public class MyThreadPoolExecutor extends ThreadPoolExecutor {
public MyThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit, BlockingQueue workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
protected voidbeforeExecute(Thread t, Runnable r){
super.beforeExecute(t, r);
String threadName = t.getName();
Log.v("zxy", "线程:" + threadName + "准备执行任务!");
}
@Override
protected voidafterExecute(Runnable r, Throwable t){
super.afterExecute(r, t);
String threadName = Thread.currentThread().getName();
Log.v("zxy", "线程:" + threadName + "任务执行结束!");
}
@Override
protected voidterminated(){
super.terminated();
Log.v("zxy", "线程池结束!");
}
}
而运行后的结果则是,这正符合刚刚说的:
05-17 05:47:51.184 1602-1619/? V/zxy:线程:pool-6-thread-1准备执行任务!
05-17 05:47:51.184 1602-1619/? V/zxy:线程:pool-6-thread-1正在执行任务!
05-17 05:47:53.184 1602-1619/? V/zxy:线程:pool-6-thread-1任务执行结束!
05-17 05:47:58.896 1602-1619/? V/zxy:线程池结束!
所以,在上面我们的优先级线程池的代码上,我们再扩展一个具有暂停功能的优先级线程池,代码如下:
public class PausableThreadPoolExecutor extends ThreadPoolExecutor {
private boolean isPaused;
private ReentrantLock pauseLock = new ReentrantLock();
private Condition unpaused = pauseLock.newCondition();
public PausableThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
pauseLock.lock();
try {
while (isPaused) unpaused.await();
} catch (InterruptedException ie) {
t.interrupt();
} finally {
pauseLock.unlock();
}
}
public void pause() {
pauseLock.lock();
try {
isPaused = true;
} finally {
pauseLock.unlock();
}
}
public void resume() {
pauseLock.lock();
try {
isPaused = false;
unpaused.signalAll();
} finally {
pauseLock.unlock();
}
}
}
然后结合上面的优先级线程池的实现,创建具有暂停功能的优先级线程池:
PausableThreadPoolExecutor pausableThreadPoolExecutor = new PausableThreadPoolExecutor(1, 1, 0L, TimeUnit.SECONDS, new PriorityBlockingQueue());
for (int i = 1; i 100; i++) {
final int priority = i;
pausableThreadPoolExecutor.execute(new PriorityRunnable(priority) {
@Override
public void doSth() {
runOnUiThread(new Runnable() {
@Override
public void run() {
textView.setText(priority + "");
}
});
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
代码中直接调用即可:
if (isPause) {
pausableThreadPoolExecutor.resume();
isPause = false;
} else {
pausableThreadPoolExecutor.pause();
isPause = true;
}
优化线程池ThreadPoolExecutor
虽说线程池极大改善了系统的性能,不过创建线程池也是需要资源的,所以线程池内线程数量的大小也会影响系统的性能,大了反而浪费资源,小了反而影响系统的吞吐量,所以创建线程池需要把握一个度才能合理的发挥它的优点,通常来说要考虑的因素有CPU的数量、内存的大小、并发请求的数量等因素,按需调整。
- 通常核心线程数可以设为CPU数量+1,而最大线程数可以设为CPU的数量*2+1。获取CPU数量的方法为:
Runtime.getRuntime().availableProcessors();
shutdown()和shutdownNow()的区别
关于线程池的停止,ExecutorService为我们提供了两个方法:shutdown和shutdownNow,这两个方法各有不同,可以根据实际需求方便的运用,如下:
- shutdown()方法在终止前允许执行以前提交的任务。
- shutdownNow()方法则是阻止正在任务队列中等待任务的启动并试图停止当前正在执行的任务。
关于AsyncTask的实现
大家都知道AsyncTask内部实现其实就是Thread+Handler。其中Handler是为了处理线程之间的通信,而这个Thread到底是指什么呢?通过AsyncTask源码可以得知,其实这个Thread是线程池,AsyncTask内部实现了两个线程池,分别是:串行线程池和固定线程数量的线程池。而这个固定线程数量则是通过CPU的数量决定的。
在默认情况下,我们大都通过AsyncTask::execute()来执行任务的,而execute()内部则是调用executeOnExecutor(sDefaultExecutor, params)方法执行的,第一个参数就是指定处理该任务的线程池,而默认情况下AsyncTask是传入串行线程池(在这里不讲版本的变化),也就是任务只能单个的按顺序执行,而我们要是想让AsyncTask并行的处理任务,大家都知道调用AsyncTask::executeOnExecutor(sDefaultExecutor, params)方法传入这个参数即可:AsyncTask.THREAD_POOL_EXECUTOR。而这个参数的意义在于为任务指定了一个固定线程数量的线程池去处理,从而达到了并行处理的功能,我们可以在源码中看到AsyncTask.THREAD_POOL_EXECUTOR这个参数就是一个固定线程数量的线程池:
public static final Executor THREAD_POOL_EXECUTOR
= new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);