并发编程-线程池

416 阅读7分钟

不讲概念,来记录下常见线程池的配置以及区别。

先来回顾下常见的配置参数:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory)

使用

newFixedThreadPool

该方法返回一个固定大小的线程池,该线程从中的线程数量固定不变,当有任务提交时如果有空闲线程,立即执行,否则加入等待队列,直到有空闲线程时再执行。

public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(),
                                  threadFactory);
}

可以看到corePoolSize=maximumPoolSize,并且等待队列大小为Integer.MAX_VALUE,也就是说这是一个固定大小的线程池,可以无限接收任务直到服务器挂了。

示例:

ExecutorService pool = Executors.newFixedThreadPool(3, new ThreadFactory() {
 AtomicLong count = new AtomicLong(1);

 @Override
 public Thread newThread(Runnable r) {
  Thread thread = new Thread(r);
  thread.setName("nameThreadFactory-" + count.getAndIncrement());
  return thread;
 }
});

int i = 0;
for (; i < 10; i++) {
 pool.execute(() -> {
  System.out.println(Thread.currentThread().getName() + " go");
 });
}
//必须结束,否则线程无法关闭
pool.shutdown();

console output:

nameThreadFactory-1 go
nameThreadFactory-3 go
nameThreadFactory-2 go
nameThreadFactory-1 go
nameThreadFactory-3 go
nameThreadFactory-2 go
nameThreadFactory-3 go
nameThreadFactory-1 go
nameThreadFactory-3 go
nameThreadFactory-2 go

可以看出来来回回只有三个线程在切换执行。

newCachedThreadPool

该方法返回一个可根据实际情况调整数量的线程池,数量不固定,但优先会复用线程,如果当前线程数量不够,再创建新的线程。

public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(0Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>(),
                                  threadFactory);
}

初始大小为 0,最大大小无上限,也就代表了可以一直创建新的线程,这里的等待队列SynchronousQueue比较特殊,是一个直接提交的队列,不会保存即总是将任务交给线程执行,也就是说newCachedThreadPool是不带缓冲队列的。

示例:

ExecutorService pool = Executors.newCachedThreadPool(new ThreadFactory() {
 AtomicLong count = new AtomicLong(1);

 @Override
 public Thread newThread(Runnable r) {
  Thread thread = new Thread(r);
  thread.setName("nameThreadFactory-" + count.getAndIncrement());
  return thread;
 }
});

int i = 0;
for (; i < 10; i++) {
 pool.execute(() -> {
  System.out.println(Thread.currentThread().getName() + " go");
 });
}
//必须结束,否则线程无法关闭
pool.shutdown();

console output:

nameThreadFactory-1 go
nameThreadFactory-2 go
nameThreadFactory-3 go
nameThreadFactory-4 go
nameThreadFactory-5 go
nameThreadFactory-6 go
nameThreadFactory-7 go
nameThreadFactory-8 go
nameThreadFactory-4 go
nameThreadFactory-8 go

可以看出有重复的线程在执行任务,这就是复用的表现。

newScheduledThreadPool

定时调度的线程池在中间件研发中经常会被使用到,其中主要包含三个方法:

//将在delay单位的延迟后开始执行一次任务
public ScheduledFuture<?> schedule(Runnable command,long delay, TimeUnit unit);
//以固定的速率执行任务,initialDelay代表第一次执行需要延迟的时间,period代表多久重复一次
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit);
//以固定的延迟周期执行任务,即每个任务执行相差delay时间
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit);
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3new ThreadFactory() {
 AtomicLong count = new AtomicLong(1);

 @Override
 public Thread newThread(Runnable r) {
  Thread thread = new Thread(r);
  thread.setName("nameThreadFactory-" + count.getAndIncrement());
  return thread;
 }
});

Runnable task = new Runnable() {
 @Override
 public void run() {
  System.out.println(Thread.currentThread().getName() + " time:" + System.currentTimeMillis() + " go");
 }
};

Runnable scheduleAtFixedRateTask = new Runnable() {
 @Override
 public void run() {
  System.out.println("scheduleAtFixedRateTask" + " begin time:" + System.currentTimeMillis() + " go");
  try {
   TimeUnit.SECONDS.sleep(3);
  } catch (InterruptedException e) {
   e.printStackTrace();
  }
  System.out.println("scheduleAtFixedRateTask" + " end   time:" + System.currentTimeMillis() + " go");
 }
};

// 1秒过后执行
scheduledThreadPool.schedule(task, 1, TimeUnit.SECONDS);

// 1秒以后开始启动,每过两秒执行一次,如果上一个工作超过了2秒,那么下一个并不会开始,而是等到上一个结束后立马开始。
scheduledThreadPool.scheduleAtFixedRate(scheduleAtFixedRateTask, 12, TimeUnit.SECONDS);

// 1秒以后开始启动,每个任务之间间隔2秒,无论上一个任务执行了多久,下一个任务总是会在上一个任务结束后2秒开始。
scheduledThreadPool.scheduleWithFixedDelay(task, 12, TimeUnit.SECONDS);

// 结束后,两个定时执行的都无法工作
// scheduledThreadPool.shutdown();
}

以上程序的输出就不再展示了,简单说下现象:

scheduledThreadPool.schedule(task, 1, TimeUnit.SECONDS);会在一秒后执行一次结束。

scheduledThreadPool.scheduleAtFixedRate(scheduleAtFixedRateTask, 1, 2, TimeUnit.SECONDS);就比较有意思了,这里设定的是程序开始一秒后开始执行第一次计划任务,每隔 2 秒重复一次。如果任务执行时间小于 2 秒,那自然没什么问题。但是这里设置的任务时长为 3 秒,会发现上一个任务没结束,即使到了 2 秒的间隔,下一个也不会开始,而是痴情的等待上一个任务的结束,只要它一结束,下一个会立刻开始。

scheduleAtFixedRateTask begin time1555324763580 go
scheduleAtFixedRateTask end   time1555324766581 go
scheduleAtFixedRateTask begin time1555324766581 go

输出就是如此这般。

scheduledThreadPool.scheduleWithFixedDelay(task, 1, 2, TimeUnit.SECONDS);是比较好理解的,无论上一个任务执行多久,只要它一结束后再过 2 秒,下一个任务即开始,没什么特殊的地方。

拾遗

由于对线程池的实现比较熟悉,所以不再进行详细的记录,简单的记录下要注意的地方:

  1. 饱和策略

当线程池负载高,无法处理新投递的任务时有几种拒绝策略:

  1. 抛出异常
  2. 谁调用谁执行
  3. 丢弃队列中最老的任务,也就是即将执行的任务
  4. 直接丢弃当前任务

2)线程数设置多大

一般 IO 密集型 CPU*2,计算密集型 CPU+1

3) 丢失的异常堆栈

如果执行的任务报错了,并且刚好没有捕获任何异常,那么不好意思,堆栈丢了,要么自己捕获Runable,要么扩展线程池,可以看看Guava中对线程池的扩展,或者唯品会的vipTools

如果有人问线程池是咋回事,你就这么说

曾经有一个创业公司,它就是线程池。开张的时候拉了 4 个兄弟,就是 coreSize=4,后来公司发展的太快,需求越来越多,兄弟们忙不过来了,怎么办只能任务排期了,但排期总有个上限吧,不能压着 10000 个需求不做吧?这个个数呢就是队列的长度。这个时候公司扩招了,技术部最多招 30 个兄弟。好了,兄弟们来了,10000 个需求也搞完了。这个时候呢,9012 年了经济危机了资本家不乐意了,公司不养闲人啊,那个老王,你看你们技术部那些新来的(maxSize-coreSize)有哪些一个月没干活了(aliveTime)开了吧。呵呵。。

纪念下那些被开的兄弟。(刘强东说了不能奋斗的人都不是我的兄弟)