线程池

线程池主要是为了避免频繁创建线程和销毁线程。当单个任务处理时间比较长或者是需要处理的任务量很大,为了避免系统效率降低,可以创建线程池,复用线程。

创建线程池

1
2
3
4
5
6
7
//五个参数的构造函数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)
  • corePoolSize,核心线程数。

    线程池新建线程的时候,如果当前线程总数小于corePoolSize,则新建的是核心线程,如果超过corePoolSize,则新建的是非核心线程。

    即使核心线程处于闲置状态,核心线程也不会被销毁

  • maximumPoolSize,最大线程数。线程池中能创建的最大的线程数。

  • keepAliveTime.当非核心线程数闲置时长超过keepAliveTime,就会将其销毁。

  • workQueue。

    该线程池中的任务队列:维护着等待执行的Runnable对象。

    当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务

    可以使用无界队列或者是有界队列。

    使用无界队列的话,这样队列永远不会满,也就是说线程池内线程数只能是核心线程数。

  • handler:饱和策略,当任务数量超过了最大线程数,应该怎么做

ThreadPoolExecutor的策略

当一个任务被添加进线程池时:

  1. 线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务
  2. 线程数量达到了corePoolSize,则将任务移入队列等待
  3. 队列已满,新建线程(非核心线程)执行任务
  4. 队列已满,总线程数又达到了maximumPoolSize,根据设定的策略进行处理

title

饱和策略 (RejectedExecutionHandler):当等待队列已满,线程数也达到最大线程数时,线程池会根据饱和策略来执行后续操作,默认的策略是抛弃要加入的任务。

线程池种类

  • newFixedThreadPool,固定线程数的线程池。

    1
    2
    3
    4
    5
    public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
    0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<Runnable>());
    }
  • newCachedThreadPool。

    1
    2
    3
    4
    5
    public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
    60L, TimeUnit.SECONDS,
    new SynchronousQueue<Runnable>());
    }
  • 它是一个可以无限扩大的线程池;
  • 它比较适合处理执行时间比较小的任务;
  • corePoolSize为0,maximumPoolSize为无限大,意味着线程数量可以无限大;
  • keepAliveTime为60S,意味着线程空闲时间超过60S就会被杀死;
  • 采用SynchronousQueue装等待的任务,这个阻塞队列没有存储空间,这意味着只要有请求到来,就必须要找到一条工作线程处理他,如果当前没有空闲的线程,那么就会再创建一条新的线程。
  • newScheduledThreadPool().

    1
    2
    3
    4
    5
    6
    7
    public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE,
    DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
    new DelayedWorkQueue());
    }

    //DelayedWorkQueue 这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务
  • newSingleThreadPool().

    1
    2
    3
    4
    5
    6
    public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
    (new ThreadPoolExecutor(1, 1,
    0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<Runnable>()));
    }

为什么不建议使用Executors的方法创建ThreadPoolExecutor对象?

例如,FixedThreadPool和SingleThreadPool允许请求的队列长度是Integer.Max_value,很容易出现排队线程过多的情况。

关闭线程池

关闭线程池,可以通过shutdownshutdownNow这两个方法。它们的原理都是遍历线程池中所有的线程,然后依次中断线程。shutdownshutdownNow还是有不一样的地方:

  1. shutdownNow首先将线程池的状态设置为STOP,然后尝试停止所有的正在执行和未执行任务的线程,并返回等待执行任务的列表;
  2. shutdown只是将线程池的状态设置为SHUTDOWN状态,然后中断所有没有正在执行任务的线程

配置线程池参数

CPU密集型任务配置尽可能少的线程数量,如配置Ncpu+1个线程的线程池。IO密集型任务则由于需要等待IO操作,线程并不是一直在执行任务,则配置尽可能多的线程,如2xNcpu

线程池状态

线程池有五种状态,running,shutdown,stop,tidying,terminated。

running:运行状态。

shutdown:不再接收新提交的任务,但是可以处理阻塞队列中的任务。调用shutdown方法会达到这种状态。

stop:不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。调用shutdownnow方法会达到这个状态。

tidying:如果所有的任务都已终止了,workerCount (有效线程数) 为0,线程池进入该状态后会调用 terminated() 方法进入TERMINATED 状态。

Future模式

  • 通过它们可以在任务执行完毕之后得到任务执行结果。
  • Future模式是一个异步调用。
  • Future其实相当于一个占位符,当我们提交线程任务之后,可以不必等待任务完成,使用Future占位,当执行完成后,自然可以通过future.get()获取执行结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class FutureDemo1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
long l = System.currentTimeMillis();
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<Integer> future = executorService.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println("执行耗时操作...");
timeConsumingOperation();
return 100;
}
}); //<1>
// 其他耗时操作..<3>
System.out.println("计算结果:" + future.get());//<2>
System.out.println("主线程运算耗时:" + (System.currentTimeMillis() - l)+ "ms");
}

static void timeConsumingOperation() {
try {
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
}
}

我们观察一下控制台的输出,发现依旧耗费 3s 来完成这次耗时操作,并没有比同步调用方式快。但是提交任务(非阻塞)和获取结果(阻塞)之间我们可以进行一些额外的操作,而这将形成一个并行执行的效果

我们会发现如果 future 提交给线程池执行之后立刻 get(),其实执行效率并不会变高,因为执行结果没有完成其实会将线程阻塞,反而由于线程的开销会比同步调用更慢。这种将来式的 future 适用多个耗时操作并发执行的场景。

future并不是通过通知的方式来通知线程任务完成的

Future模式

execute and submit

向线程池中提交任务主要有submit以及execute两种方式。submit和execute最大的区别就是submit有返回值,而execute没有返回值。但是实际上submit的底层使用的还是execute。

submit

是将要执行的任务包装为FutureTask来提交,使用者可以通过FutureTask来拿到任务的执行状态和执行最终的结果,最终调用的都是execute方法,所以,submit提交任务有返回值。

execute