ThreadPoolExcutor 线程池

昨天在查看项目时,偶尔看到代码里通过静态代码块实现的单例模式的线程池,如下:

public static final ThreadPoolExecutor threadPool;

static {
     int nCpu = Runtime.getRuntime().availableProcessors();
     int maxPoolSize = (2 * nCpu) + 1;
     threadPool = new ThreadPoolExecutor(3, maxPoolSize, 60, TimeUnit.SECONDS,     new LinkedBlockingQueue<Runnable>(),
                new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable r) {
                        return new Thread(r, "xxxThreadPool-" + r.hashCode());
                    }
                });
}
注意:这里通过静态代码块实现,其实是饿汉单例模式的变种,并不是懒汉模式。

看到上面的参数配置,回想起去年解决的一个OOM的问题。
先不说造成问题的具体原因,说下线程池的各个参数配置。

基本参数

  • corePoolSize:核心线程数

  • maxPoolSize:最大线程数

  • keepAliveTime:线程空闲时间

  • unit:时间单位

  • workQueue:任务队列

  • threadFactory:线程工厂

    阿里 JAVA开发手册
    【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。

  • handler:拒绝策略

执行顺序

在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务。除非通过调用prestartAllCoreThreads()或者prestartCoreThread()方法,来预创建线程。
当线程空闲时间达到keepAliveTime,该线程会退出,直到线程数量等于corePoolSize。如果allowCoreThreadTimeout设置为true,则所有线程均会退出直到线程数量为0。

  • 当线程数小于核心线程数时,创建线程。
  • 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
  • 当线程数大于等于核心线程数,且任务队列已满
    • 若线程数小于最大线程数,创建线程
    • 若线程数等于最大线程数,根据拒绝策略执行,四种拒绝策略如下:
      • ThreadPoolExecutor.AbortPolicy();//默认策略,抛出运行时异常 RejectedExecutionException
      • ThreadPoolExecutor.CallerRunsPolicy();//由主线程执行该任务 队列满了丢任务不异常
      • ThreadPoolExecutor.DiscardPolicy();//直接丢弃任务
      • ThreadPoolExecutor.DiscardOldestPolicy();//将最早进入队列的任务删,之后再尝试加入队列

设定线程数量

如果是CPU密集型应用,则线程池大小设置为cpu核数或者cpu核数±1
如果是IO密集型应用,则线程池大小设置为((线程等待时间+线程cpu时间)/线程cpu时间*cpu数目)

设置后可以通过压测验证数量是否合适。

OOM问题

阿里 JAVA开发手册
【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这
样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下: 1) FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。 2) CachedThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

一开始解决的问题是由于同事在创建线程池时使用了局部变量,每个请求进入这个方法时,都会创建一个线程池。大量并发请求进入后,新建了大量的线程,导致系统虚拟内存被耗尽,JVM抛出 java.lang.OutOfMemoryError: Unable to create new native thread 错误。

解决了这个问题后,重新部署后,JVM仍然会出现crash的现象。通过MAT分析hs_err[pid].log文件,发现仍然是由于oom导致的。只是异常信息变成了

java.lang.OutOfMemoryError: Java heap space

通过日志文件分析,发现是由于使用了无界队列,而第三方接口响应时长很大,导致核心线程数消化不了这些请求,任务被不停加进队列中,而队列又是无界的,导致了OOM。

另外,看日志文件,可以看到多次的OOM异常信息,直到某一个时刻,JVM才崩溃退出。说明当某一线程OOM的时候,会把该线程占用的内存释放,而不影响调用它的线程!然后会再次有任务加入,再次OOM,释放资源,直到JVM崩溃推出……

解决方案即设定队列会有界队列,同时选择合适的拒绝策略。

注意:
OOM并一定会导致jvm crash。
jvm crash和jvm heap dump掉其实不是一个概念,jvmcrash是某些代码,在某种特殊条件下触发了jvm底层的bug,导致jvm进程直接kill掉了;jvmheapdump 是由于jvm发生了OutOfMemoryError(就是我们的黑话oom错误),从而导致jvm自动退出。可以这么理解,前者是底层的异常导致进程退出,后者是应用代码的异常导致了jvm出于保护机制导致了进程退出。

线程池内异常捕获

提交方式

向线程池提交任务,通常有两种方式

  • execute,提交无返回值的任务
  • submit,提交有返回值的任务

事实上,submit最终也是调用的execute方法的,在调用execute前,submit方法会将我们的Runnable包装为一个RunnableFuture对象,这个对象实际上是FutureTask实例,然后将这个FutureTask交给execute方法执行。

/**
 * @throws RejectedExecutionException {@inheritDoc}
 * @throws NullPointerException       {@inheritDoc}
 */
public Future<?> submit(Runnable task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<Void> ftask = newTaskFor(task, null);
    execute(ftask);
    return ftask;
}

在FutureTask的构造方法中,Runnable被包装成了一个Callable类型的对象。

    /**
     * Creates a {@code FutureTask} that will, upon running, execute the
     * given {@code Runnable}, and arrange that {@code get} will return the
     * given result on successful completion.
     *
     * @param runnable the runnable task
     * @param result the result to return on successful completion. If
     * you don't need a particular result, consider using
     * constructions of the form:
     * {@code Future<?> f = new FutureTask<Void>(runnable, null)}
     * @throws NullPointerException if the runnable is null
     */
    public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // ensure visibility of callable
    }

submit的异常捕获方式

在FutureTask的run方法中,调用了Callable对象的call方法,即调用了Runnable对象的run方法。同时,如果代码(Runnable)抛出异常,异常将被捕获并保存下来。

    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);
        }
    }
    
    protected void setException(Throwable t) {
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = t;
            UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
            finishCompletion();
        }
    }

在调用Future对象的get方法时,会将保存的异常重新抛出,然后针对异常做一些处理。

   public V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException {
        if (unit == null)
            throw new NullPointerException();
        int s = state;
        if (s <= COMPLETING &&
            (s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
            throw new TimeoutException();
        return report(s);
    }
    /**
     * Returns result or throws exception for completed task.
     * @param s completed state value
     */
    @SuppressWarnings("unchecked")
    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);
    }

execute的异常捕获方式

如果我们不关心这个任务的结果,可以直接使用ExecutorService中的execute方法(实际是继承Executor接口)来直接去执行任务。这样异常就不会被保存下来,不用get方法就可以捕获到异常。
在使用execute提交任务的时,任务最终会被一个Worker对象执行。这个Worker内部封装了一个Thread对象,这个Thread就是线程池的工作者线程。工作者线程会调用runWorker方法来执行我们提交的任务:

    /**
     * Main worker run loop.  Repeatedly gets tasks from queue and
     * executes them, while coping with a number of issues:
     *
     * 1. We may start out with an initial task, in which case we
     * don't need to get the first one. Otherwise, as long as pool is
     * running, we get tasks from getTask. If it returns null then the
     * worker exits due to changed pool state or configuration
     * parameters.  Other exits result from exception throws in
     * external code, in which case completedAbruptly holds, which
     * usually leads processWorkerExit to replace this thread.
     *
     * 2. Before running any task, the lock is acquired to prevent
     * other pool interrupts while the task is executing, and then we
     * ensure that unless pool is stopping, this thread does not have
     * its interrupt set.
     *
     * 3. Each task run is preceded by a call to beforeExecute, which
     * might throw an exception, in which case we cause thread to die
     * (breaking loop with completedAbruptly true) without processing
     * the task.
     *
     * 4. Assuming beforeExecute completes normally, we run the task,
     * gathering any of its thrown exceptions to send to afterExecute.
     * We separately handle RuntimeException, Error (both of which the
     * specs guarantee that we trap) and arbitrary Throwables.
     * Because we cannot rethrow Throwables within Runnable.run, we
     * wrap them within Errors on the way out (to the thread's
     * UncaughtExceptionHandler).  Any thrown exception also
     * conservatively causes thread to die.
     *
     * 5. After task.run completes, we call afterExecute, which may
     * also throw an exception, which will also cause thread to
     * die. According to JLS Sec 14.20, this exception is the one that
     * will be in effect even if task.run throws.
     *
     * The net effect of the exception mechanics is that afterExecute
     * and the thread's UncaughtExceptionHandler have as accurate
     * information as we can provide about any problems encountered by
     * user code.
     *
     * @param w the worker
     */
    final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            while (task != null || (task = getTask()) != null) {
                w.lock();
                // If pool is stopping, ensure thread is interrupted;
                // if not, ensure thread is not interrupted.  This
                // requires a recheck in second case to deal with
                // shutdownNow race while clearing interrupt
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    //可重写此方法或terminated方法输出日志等
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        //从任务队列中取出任务执行
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

如果任务代码(task.run())抛出异常,会被最内层的try–catch块捕获,然后重新抛出。注意到最里面的finally块,在重新抛出异常之前,会先执行afterExecute方法,这个方法的默认实现为空,即什么也不做。
在此可以重写ThreadPoolExecutor.afterExecute方法,处理传递到afterExecute方法中的异常。

如果未重写afterExecute方法,即异常未捕获,则会调用Thread#dispatchUncaughtException方法

    /**
     * Dispatch an uncaught exception to the handler. This method is
     * intended to be called only by the JVM.
     */
    private void dispatchUncaughtException(Throwable e) {
        getUncaughtExceptionHandler().uncaughtException(this, e);
    }
    public void uncaughtException(Thread t, Throwable e) {
        if (parent != null) {
            parent.uncaughtException(t, e);
        } else {
            Thread.UncaughtExceptionHandler ueh =
                Thread.getDefaultUncaughtExceptionHandler();
            if (ueh != null) {
                ueh.uncaughtException(t, e);
            } else if (!(e instanceof ThreadDeath)) {
                System.err.print("Exception in thread \""
                                 + t.getName() + "\" ");
                e.printStackTrace(System.err);
            }
        }
    }

此处可以在ThreadPoolExecutor线程工厂ThreadFactory中提供自定义的UncaughtExceptionHandler,在uncaughtException方法中处理异常。

除以上三种方法外,还可以在业务代码中直接try/catch,捕获任务代码可能抛出的所有异常,包括未检测异常。

线程池的关闭

  • shutdown()
    shutdown并不直接关闭线程池,而是不再接受新的任务。如果线程池内有任务,那么待这些任务执行完毕后再关闭线程池。
  • shutdownNow()
    shutdownNow表示不再接受新的任务,并把任务队列中的任务直接移出掉,如果有正在执行的,尝试进行停止。

题外话

一个线程对应一个 JVM Stack。JVM Stack 中包含一组 Stack Frame。线程每调用一个方法就对应着 JVM Stack 中 Stack Frame 的入栈,方法执行完毕或者异常终止对应着出栈(销毁)。

当 JVM 调用一个 Java 方法时,它从对应类的类型信息中得到此方法的局部变量区和操作数栈的大小,并据此分配栈帧内存,然后压入 JVM 栈中。
在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法。

主要关注的stack栈内存,就是虚拟机栈中局部变量表部分。局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。并且在Java编译为Class文件时,就已经确定了该方法所需要分配的局部变量表的最大容量。
局部变量表内容越多,栈帧越大,栈深度越小。

通过-Xss可以设置栈的大小。如果线程需要一个比固定大小大的Stack,会发生StackOverflowError;如果系统没有足够的内存为新线程创建Stack,发生OutOfMemoryError。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值