线程池
引言
上一篇介绍了并发容器,它在线程池中发挥着重要作用,本篇将介绍java中的线程池。
Executor,ExecutorService,AbstractExecutorService
Executor是一个接口,只定义一个方法excute,参数是Runnable。这个接口体现了定义与运行的分开,已经告诉了你任务,具体要怎么运行由你自己定义。
public interface Executor {
/**
* Executes the given command at some time in the future. The command
* may execute in a new thread, in a pooled thread, or in the calling
* thread, at the discretion of the {@code Executor} implementation.
*
* @param command the runnable task
* @throws RejectedExecutionException if this task cannot be
* accepted for execution
* @throws NullPointerException if command is null
*/
void execute(Runnable command);
}
ExecutorService也是一个接口,它实现了Executor,完整了整个任务执行器的生命周期。
以下是它定义的一些方法
void shutdown();//结束
List<Runnable> shutdownNow();//马上结束
boolean isShutdown();//是否结束了
boolean isTerminated();//是不是整体都执行完了
boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException;//等着结束,等多长时间,时间到了还不结束的话他就返回false
AbstractExecutorService对ExecutorService的一些接口进行了实现,是一个抽象类,是为各种线程池做准备的。但其没有实现excute方法,对其各种不同的实现,分化出了各种不同的线程池。
两种类型线程池ThreadPoolExecutor和FixedThreadPool
ThreadPoolExecutor和ForkJoinPool都是继承于AbstractExecutorService,是两种不同的线程池。可以理解为ThreadPoolExecutor中每个线程取一个任务,就各自运行了。ForkJoinPool可以将同一个任务分块,然后各自运行完后再把结果合起来。所以两种线程池能干的事是不一样的。
手动定义线程池
线程池的方法有很多种构造方法,这里介绍一个最常见的。
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 4,
60, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(4),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
如上new 出ThreadPool后,创建任务,调用前面的提到的excute即可。
这个线程池有七个参数,这里介绍下这七个参数。
-
第一个参数corePoolSoze核心线程数,最开始的时候是有这个线程池里面是有一定的核心线程数的;
-
第二个叫maximumPoolSize最大线程数,线程数不够了,能扩展到最大线程是多少;
-
第三个keepAliveTime生存时间,意思是这个线程有很长时间没干活了请你把它归还给操作系统;
-
第四个TimeUnit.SECONDS生存时间的单位到底是毫秒纳秒还是秒自己去定义;
-
第五个是任务队列,就是我们上篇讲的BlockingQueue,各种各样的BlockingQueue你都可以往里面扔,我们这用的是ArrayBlockingQueue,参数最多可以装四个任务;
-
第六个是线程工厂defaultThreadFactory,他返回的是一个enw DefaultThreadFactory,它要去你去实现ThreadFactory的接口,这个接口只有一个方法叫newThread,所以就是产生线程的,可以通过这种方式产生自定义的线程,默认产生的是defaultThreadFactory,而defaultThreadFactory产生线程的时候有几个特点:new出来的时候指定了group制定了线程名字,然后指定的这个线程绝对不是守护线程,设定好你线程的优先级。
DefaultThreadFactory() { SecurityManager s = System.getSecurityManager(); group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); namePrefix = "pool-" + poolNumber.getAndIncrement() + "-thread-"; } public Thread newThread(Runnable r) { Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0); if (t.isDaemon()) // 若是守护线程,设置为false t.setDaemon(false); if (t.getPriority() != Thread.NORM_PRIORITY) t.setPriority(Thread.NORM_PRIORITY); return t; }
-
第七个叫拒绝策略,指的是线程池忙,而且任务队列满这种情况下我们就要执行各种各样的拒绝策略,jdk默认提供了四种拒绝策略,也是可以自定义的。
1:Abort:抛异常
2:Discard:扔掉,不抛异常
3:DiscardOldest:扔掉排队时间最久的
4:CallerRuns:调用者处理服务
根据实际的业务,常常会自定义拒绝策略。
JDK默认线程池
JDK给我们提供了一些默认的线程池的实现,这里总结下常见的默认线程池。
SingleThreadPool
这个线程池只有一个线程,保证任务是顺序执行的。用线程池的好处是:1,线程池是有任务队列的2.生命周期管理线程池是能帮你提供的。
可以运行下面方法,查看线程池的特点。
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());
});
}
}
CachedPool
这个线程池用的任务队列是synchronousQueue,所以来一个任务就要马上执行,不然提交任务的线程就阻塞住了。
可以运行下面方法,查看线程池的特点。
public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newCachedThreadPool();
System.out.println(service);
for (int i = 0; i < 2; 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(80);
System.out.println(service);
}
FixedThreadPool
固定核心线程和最大线程数。可以submit任务后,多个任务并行运算。
使用时要考虑固定的线程数是否够用。
ScheduledPool
new ScheduledThreadPool的时候返回的是ScheduledThreadPoolExecutor,然后在ScheduledThreadPoolExecutor里面他调用了super,他的super又是ThreadPoolExecutor,它本质上还是ThreadPoolExecutor。
它有一些方法可以对任务定时执行,如scheduleAtFixedRate可以设置间隔多长时间在一个固定的频率上来执行一次这个任务,如下。
public static void main(String[] args) {
ScheduledExecutorService service = Executors.newScheduledThreadPool(4);
service.scheduleAtFixedRate(()->{
try {
TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}, 0, 500, TimeUnit.MILLISECONDS);
}
ForkJoinPool
其和上面说的几个线程池不同之处在于,每个线程都会维护一个任务队列,当一个Thread空下来时,会去别的Thread那拿任务来执行。这样子,如果有一个线程执行一个很重的任务,其他线程空着可以给它分担。封装成的workStealingPoll,实际就是提供了一个更方便的创建接口。
说在最后
本篇介绍了线程池相关的知识。有些知识介绍的比较简略,后续单独介绍ForkJoinPool和解读线程池源码。