线程池的基本使用

要用到线程池,就要先介绍一个类:ThreadPoolExecutor.

通过查看源码发现继承及实现关系为


在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:核心池的大小
  • maximumPoolSize:线程池大小(最大能容纳线程数)
  • keepAliveTime:允许线程空闲时间。当线程池中线程数大于核心池大小时,当有线程空闲时间超过keepAliveTime就会终止。如果调用了allowCoreThreadTimeOut(boolean)方法,则核心池中的空闲线程也会被终止。
  • unit:keepAliveTime的时间单位,格式TimeUnit.XXXX(XXXX用HOURS、MILLISECONDS等替代)
  • workQueue:阻塞队列,一般可选三种类型
    • ArrayBlockingQueue数组类型,指定大小
    • LinkedBlockingQueue链表类型,不用指定大小
    • SynchronousQueue:CLICK
    • PriorityBlockingQueue
  • threadFactory:线程工厂,主要用来创建线程
  • handler:指定拒绝处理任务时的策略

使用

实例化一个ThreadPoolExecutor类

ThreadPoolExecutor executor=new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>,5));

核心池大小为5,线程池大小为10,线程允许空闲200ms,阻塞队列大小5.

然后,创建20个线程并每次打印线程池中的状态

for(int a=0;a<20;a++) {
    executor.execute(new ThreadPoolT(a));
    System.out.println("线程池中线程数目"+executor.getPoolSize()+"正在等待执行的任务数目"+executor.getQueue().size()+"已经执行完的任务数目:"+executor.getCompletedTaskCount());
}

其中线程的run方法为

    @Override
public void run() {
    // TODO Auto-generated method stub
    System.out.print("线程"+id+"正在执行...");
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    System.out.println("线程"+id+"结束..");
}

关闭线程池

executor.shutdown();

总代码

package com.test;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolT implements Runnable{

    private int id;


    public ThreadPoolT(int id) {
        super();
        this.id = id;
    }

    public static void main(String[] args) {

        ThreadPoolExecutor executor=new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(5));
        for(int a=0;a<20;a++) {
            executor.execute(new ThreadPoolT(a));
            System.out.println("线程池中线程数目"+executor.getPoolSize()+"正在等待执行的任务数目"+executor.getQueue().size()+"已经执行完的任务数目:"+executor.getCompletedTaskCount());
        }
        System.out.println(executor.getLargestPoolSize());
        System.out.println(executor.getPoolSize());
        executor.shutdown();
    }

    @Override
    public void run() {
        // TODO Auto-generated method stub
        System.out.print("线程"+id+"正在执行...");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println("线程"+id+"结束..");
    }

}

运行

可见

  1. 线程池会先创建核心池大小量的线程数,并加入需运行的线程
  2. 然后再往线程池中加线程时,就会往阻塞队列里添加,等待执行
  3. 当阻塞队列满了,再往线程池里添加线程,这个时候线程池就会再创建新线程来弥补不足
  4. 当线程池满了(达到了maximumPoolSize ),再往线程池里添加线程时,线程池就会拒绝任务,而拒绝策略和之前参数的设置有关,这里是抛出RejectedExecutionException异常,并停止添加更多线程执行。
  5. 等待线程运行完

如果把前面的队列里的参数5去掉,并在线程池结束前加下列代码:

Thread.sleep(10000);
System.out.println(executor.getPoolSize()+"AAAAAAAAAAAAAAA");
executor.shutdown();

则运行结果为:

可见线程全部运行结束后核心池仍有5个线程,这便是线程池使用的优点,不用频繁开线程关线程,而是提供一些线程代理运行需要运行的线程,提高了程序效率。


另外,其实一般使用线程池不用我们每次指定那么多参数。一般使用Executors类中提供的静态方法来创建线程池:

//corePoolSize和maximumPoolSize值相等,它使用LinkedBlockingQueue
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(),
                                  threadFactory);
}


//corePoolSize和maximumPoolSize都设置为1,使用LinkedBlockingQueue
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}


//corePoolSize设置为0,maximumPoolSize设置为Integer.MAX_VALUE,使用SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

合理设置线程池大小

(1)高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换
(2)并发不高、任务执行时间长的业务要区分开看:
  a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以适当加大线程池中的线程数目,让CPU处理更多的业务 (2*cpu核心)
  b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换