线程池最佳实践

1、拒绝策略可以用一个兜底的线程池来执行
自定义拒绝策略
实现该接口,重写该方法,在这个方法中用兜底的线程池去执行

public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, 
    ThreadPoolExecutor executor);
}

2、每个业务线一个线程池,每个线程池有自己的名字,排除问题起来简单
3、自定义参数来建立线程池,不要使用JDK自带的,类似于无界队列,会OOM
4、一般设置allowCoreThreadTimeOut 为true当线程池没有任务时,允许核心线程也关掉。
5、线程池new出来后,核心线程不会启动,除非调用prestartCoreThread 该方法启动核心线程

6 、keepAliveTime=0
在JDK1.8中,keepAliveTime=0表示非核心线程执行完立刻终止。
默认情况下,keepAliveTime小于0,初始化的时候才会报错;但如果allowsCoreThreadTimeOut,keepAliveTime必须大于0,不然初始化报错。
7 怎么进行异常处理

很多代码的写法,我们都习惯按照常见范式去编写,而没有去思考为什么。比如:

如果我们使用execute()提交任务,我们一般要在Runable任务的代码加上try-catch进行异常处理。

如果我们使用submit()提交任务,我们一般要在主线程中,对Future.get()进行try-catch进行异常处理。

—— 但是在上面,我提到过,submit()底层实现依赖execute(),两者应该统一呀,为什么有差异呢?下面再扒一扒submit()的源码,它的实现蛮有意思。

首先,ThreadPoolExecutor中没有submit的代码,而是在它的父类AbstractExecutorService中,有三个submit的重载方法,代码非常简单,关键代码就两行:

 public Future<?> submit(Runnable task) {
      if (task == null) throw new NullPointerException();
      RunnableFuture<Void> ftask = newTaskFor(task, null);
      execute(ftask);
      return ftask;
  }
  public <T> Future<T> submit(Runnable task, T result) {
      if (task == null) throw new NullPointerException();
      RunnableFuture<T> ftask = newTaskFor(task, result);
      execute(ftask);
      return ftask;
  }
  public <T> Future<T> submit(Callable<T> task) {
      if (task == null) throw new NullPointerException();
      RunnableFuture<T> ftask = newTaskFor(task);
      execute(ftask);
      return ftask;
  }

正是因为这三个重载方法,都调用了execute,所以我才说submit底层依赖execute。通过查看这里execute的实现,我们不难发现,它就是ThreadPoolExecutor中的实现,所以,造成submit和execute的差异化的代码,不在这。那么造成差异的一定在newTaskFor方法中。这个方法也就new了一个FutureTask而已,FutureTask实现RunnableFuture接口,RunnableFuture接口继承Runnable接口和Future接口。而Callable只是FutureTask的一个成员变量。

所以讲到这里,就有另一个Java基础知识点:Callable和Future的关系。我们一般用Callable编写任务代码,Future是异步返回对象,通过它的get方法,阻塞式地获取结果。FutureTask的核心代码就是实现了Future接口,也就是get方法的实现:

    public V get() throws InterruptedException, ExecutionException {
        int s = state;
        if (s <= COMPLETING)
            // 核心代码
            s = awaitDone(false, 0L);
        return report(s);
    }

    private int awaitDone(boolean timed, long nanos)
        throws InterruptedException {
        final long deadline = timed ? System.nanoTime() + nanos : 0L;
        WaitNode q = null;
        boolean queued = false;
        // 死循环
        for (;;) {
            if (Thread.interrupted()) {
                removeWaiter(q);
                throw new InterruptedException();
            }

            int s = state;
            // 只有任务的状态是’已完成‘,才会跳出死循环
            if (s > COMPLETING) {
                if (q != null)
                    q.thread = null;
                return s;
            }
            else if (s == COMPLETING) // cannot time out yet
                Thread.yield();
            else if (q == null)
                q = new WaitNode();
            else if (!queued)
                queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                     q.next = waiters, q);
            else if (timed) {
                nanos = deadline - System.nanoTime();
                if (nanos <= 0L) {
                    removeWaiter(q);
                    return state;
                }
                LockSupport.parkNanos(this, nanos);
            }
            else
                LockSupport.park(this);
        }
    }

get的核心实现是有个awaitDone方法,这是一个死循环,只有任务的状态是“已完成”,才会跳出死循环;否则会依赖UNSAFE包下的LockSupport.park原语进行阻塞,等待LockSupport.unpark信号量。而这个信号量只有当运行结束获得结果、或者出现异常的情况下,才会发出来。分别对应方法set和setException。这就是异步执行、阻塞获取的原理,扯得有点远了。

回到最初我们的疑问,为什么submit之后,通过get方法可以获取到异常?原因是FutureTask有一个Object类型的outcome成员变量,用来记录执行结果。这个结果可以是传入的泛型,也可以是Throwable异常:

public void run() {
      if (state != NEW ||
          !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                       null, Thread.currentThread()))
          return;
      try {
          Callable<V> c = callable;
          if (c != null && state == NEW) {
              V result;
              boolean ran;
              try {
                  result = c.call();
                  ran = true;
              } catch (Throwable ex) {
                  result = null;
                  ran = false;
                  setException(ex);
              }
              if (ran)
                  set(result);
          }
      } finally {
          // runner must be non-null until state is settled to
          // prevent concurrent calls to run()
          runner = null;
          // state must be re-read after nulling runner to prevent
          // leaked interrupts
          int s = state;
          if (s >= INTERRUPTING)
              handlePossibleCancellationInterrupt(s);
      }
  }

// get方法中依赖的,报告执行结果
  private V report(int s) throws ExecutionException {
      Object x = outcome;
      if (s == NORMAL)
          return (V)x;
      if (s >= CANCELLED)
          throw new CancellationException();
      throw new ExecutionException((Throwable)x);
  }

FutureTask的另一个巧妙的地方就是借用RunnableAdapter内部类,将submit的Runnable封装成Callable。所以就算你submit的是Runnable,一样可以用get获取到异常。

不论是用execute还是submit,都可以自己在业务代码上加try-catch进行异常处理。我一般喜欢使用这种方式,因为我喜欢对不同业务场景的异常进行差异化处理,至少打不一样的日志吧。

如果是execute,还可以自定义线程池,继承ThreadPoolExecutor并复写其afterExecute(Runnable r, Throwable t)方法。
或者实现Thread.UncaughtExceptionHandler接口,实现void uncaughtException(Thread t, Throwable e);方法,并将该handler传递给线程池的ThreadFactory。
但是注意,afterExecute和UncaughtExceptionHandler都不适用submit。因为通过上面的FutureTask.run()不难发现,它自己对Throwable进行了try-catch,封装到了outcome属性,所以底层方法execute的Worker是拿不到异常信息的。

附录:
重复一下
【强制】使用ThreadPoolExecutor的构造函数声明线程池,避免使用Executors类的 newFixedThreadPool和newCachedThreadPool。
【强制】 创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。即threadFactory参数要构造好。
【建议】建议不同类别的业务用不同的线程池。
【建议】CPU密集型任务(N+1):这种任务消耗的主要是CPU资源,可以将线程数设置为N(CPU核心数)+1,比CPU核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用CPU的空闲时间。
【建议】I/O密集型任务(2N):这种任务应用起来,系统会用大部分的时间来处理I/O交互,而线程在处理I/O的时间段内不会占用CPU来处理,这时就可以将CPU交出给其它线程使用。因此在I/O密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是2N。
【建议】workQueue不要使用无界队列,尽量使用有界队列。避免大量任务等待,造成OOM。
【建议】如果是资源紧张的应用,使用allowsCoreThreadTimeOut可以提高资源利用率。
【建议(经过测试,我觉得这个是强制)】虽然使用线程池有多种异常处理的方式,但在任务代码中,使用try-catch最通用,也能给不同任务的异常处理做精细化。

巨人的肩膀
线程池十问十答

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值