在上一篇文章中,我们介绍了创建线程的三种方法,但实际开发中如果需要频繁创建线程则不会使用前文说的那三种方法,而是选择使用线程池创建线程。使用线程池可以有效减少在手动创建线程过程中产生的开销,方便线程进行统一管理,提高系统资源利用率。
在阿里巴巴Java开发手册中也强制规定了要使用线程资源必须通过线程池创建,不允许在应用中自行显式创建线程。
下面介绍几种常见的线程池:
-
newFixedThreadPool(固定大小的线程池)
-
newCachedThreadPool(可缓存线程的线程池)
-
newSingleThreadExecutor(单线程的线程池)
-
newScheduledThreadPool(定时及周期执行线程池)
第一种是固定大小的线程池,下面给出该线程池的测试demo。设置固定的线程数为3,创建4个线程,第四个线程会在线程池中有空闲线程时调用该线程。
public class TestThreadPool {
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(3);
for (int i = 0; i <= 3; ++i) {
es.execute(() -> {
System.out.println("固定大小的线程池在" + new SimpleDateFormat("HH:mm:ss").format(new Date()) +"创建了" + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
es.shutdown();
}
}
接着是可缓存线程的线程池,这里可以开启任意多个线程(前提是不超过内存限制)。该线程池可以灵活回收空闲线程,如果线程池中没有足够线程就会创建一个新线程。
public class TestThreadPool {
public static void main(String[] args) {
ExecutorService es = Executors.newCachedThreadPool();
for (int i = 0; i < 5; ++i) {
es.execute(() -> {
System.out.println("可缓存线程的线程池在" + new SimpleDateFormat("HH:mm:ss").format(new Date()) +"创建了" + Thread.currentThread().getName());
});
}
es.shutdown();
}
}
第三种是单线程的线程池,线程池中仅有一个线程,严格按照先来先服务的原则进行代码执行。
public class TestThreadPool {
public static void main(String[] args) {
ExecutorService es = Executors.newSingleThreadExecutor();
for (int i = 0; i < 3; ++i) {
es.execute(() -> {
System.out.println("单线程的线程池在" + new SimpleDateFormat("HH:mm:ss").format(new Date()) +"创建了" + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
es.shutdown();
}
}
第四种是定时及周期执行线程池,用于设置定时及周期性任务。如下的代码会每隔1秒钟进行一次报时,3秒后关闭线程池。
public class TestThreadPool {
public static void main(String[] args) {
ScheduledExecutorService es = Executors.newScheduledThreadPool(1);
es.scheduleAtFixedRate(() -> System.out.println("定时周期执行线程池为您报时:" + new SimpleDateFormat("HH:mm:ss").format(new Date())), 0, 1, TimeUnit.SECONDS);
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
es.shutdown();
}
}
但是,比较不幸的是,阿里巴巴Java开发手册中明确指出上面四种线程池不允许使用,必须使用它们底层所使用的类ThreadPoolExcutor来创建线程池。
下面是ThreadPoolExecutor方法的具体内容,当然这是一个重载方法。我们选择参数最多的方法,一个参数一个参数的介绍。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
- corePoolSize
首先是corePoolSize,这是线程池核心线程数,除非将allowCoreThreadTimeOut
设置为true,否则默认核心线程将一直在线程池中存活。
- maximumPoolSize
maximumPoolSize是线程池中的最大线程数,这里的最大线程数是指核心线程数与最大非核心线程数之和。
- keepAliveTime
keepAliveTime用来表示非核心线程的存活时长。
- unit
unit是非核心线程存活的时间单位,如秒、毫秒、小时等。
- workQueue
用于在执行任务之前使用的队列。常见的队列有三种:SynchronousQueue
、LinkedBlockingDeque
和ArrayBlockingQueue
。
- threadFactory
线程工厂。
- handler
使用特定策略处理无法处理的任务。常见的拒绝策略包括AbortPolicy
、CallerRunsPolicy
、DiscardOldestPolicy
和DiscardPolicy
。
下面详细介绍一下ThreadPoolExecutor的执行过程:
1.线程数低于corePoolSize时总是创建新线程来执行任务。
2.当线程数高于corePoolSize而低于maximumPoolSize时,如果是SynchronousQueue
这样的队列,则检查有无空闲线程,有的话则分配空闲线程执行任务,否则创建新的线程。如果是LinkedBlockingDeque
这样的队列,则将任务放入队列,这种情况下设置的maximumPoolSize
是没有意义的。如果是ArrayBlockingQueue
这种队列则会将任务入队,当队列满了就创建非核心线程来处理任务。
3.当队列满了且线程池达到了最大线程数,则任务会被拒绝策略处理。当采用AbortPolicy
策略时会自动丢弃任务并抛出RejectedExecutionException异常,当采用DiscardPolicy
策略时丢弃任务但不抛出异常,当采用DiscardOldestPolicy
策略时会丢弃队列最前面的任务并尝试重新提交任务,当采用CallerRunsPolicy
策略时,任务不会被抛弃,但不是由线程池中的线程执行,而是由调用线程执行。
现在我们基本把ThreadPoolExecutor的原理讲完了,话题回到前面的四种Executors创建的线程池。为什么阿里巴巴Java开发手册不允许使用它们呢?我们来看一下这四种线程池底层的实现源码。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
我们首先看一下newFixedThreadPool
和newSingleThreadExecutor
的源码,它们使用的队列是LinkedBlockingQueue
,这种队列前面没讲到的点是它默认的队列容量是Integer.MAX_VALUE
,这就导致当我们需要使用线程池完成耗时任务但一直源源不断有新的任务进来时可能导致OOM(内存溢出)。接着我们看newCachedThreadPool
和newScheduledThreadPool
这两种线程池,ScheduledThreadPoolExecutor是ThreadPoolExecutor的子类,所以根据上面的代码可以看出这两种线程池都会能创建最多Integer.MAX_VALUE
个线程,这必然也会有OOM的风险。
那么正确创建线程池的方式是怎样的呢?下面我们用一个demo演示一下。
ExecutorService es = new ThreadPoolExecutor(10, 50,
60, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(10));
try {
for (int i = 0; i < 100; ++i) {
es.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "成功执行任务");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
} finally {
es.shutdown();
}
上面的代码我们指定了核心线程数和最大线程数,并且将任务队列设置为固定大小的ArrayBlockingQueue,当然实际参数应该根据具体的需求进行调整。我故意设置了超出线程池处理能力的任务数。执行结果如下,抛出了RejectedExecutionException异常,不过异常总好过错误。
以上就是本文的全部内容,喜欢的朋友欢迎关注我的公众号:SKY技术修炼指南