一文读懂Java线程池


1 线程池前置知识

1.1 Callable

  以往当我们创建一个线程时,需要实现Runnable接口来创建一个任务,并交由Thread对象去执行。在JDK1.5之后,还提供了另外一种方式:实现Callable接口来创建任务。Callable与Runnable类似,都可以用来创建一个可被Thread对象执行的任务。它们的不同之处在于,Runnable的run()方法并没有返回值,而Callable的call()方法有返回值,可以在任务执行完之后返回结果。

1.2 Future

  Future用来存放任务执行的将来才会产生的结果。其用法如下:

public class TestThreadPool {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<String> task = new Callable() {
            @Override
            public String call() throws Exception {
                // do something
                return "testCallable";
            }
        };
        ExecutorService executor = Executors.newCachedThreadPool();
        Future future = executor.submit(task);
        System.out.println(future.get());
    }
}

利用Callable创建一个任务,调用submit(task)方法提交任务之后,立即返回。而任务的执行结果将保存在Future中,通过Future.get()方法可以获取到任务的执行结果。需要注意的是,get()方法会阻塞等待任务返回执行结果,只有当获取到执行结果之后,该方法才会继续往下执行。

1.3 FutureTask

在这里插入图片描述
  从上图可以看出,FutureTask相当于Runnable和Future的组合,这意味着FutureTask既是一个可以被Thread执行的任务,又可以保存任务执行的结果。

2 ThreadPoolExecutor

2.1 ThreadPoolExecutor继承体系

在这里插入图片描述
  如上图所示,线程池ThreadPoolExecutor的顶层是一个Executor接口,其中只生命了一个方法execute,这表明线程池其实就是一个可以用来执行任务的一个执行器。而ExecutorService接口中则声明了一些与线程池的生命周期相关的一些方法。ThreadPoolExecutor是ExecutorService的具体实现,使用这个类可以创建我们需要的线程池。

2.2 ThreadPoolExecutor参数

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    // do something
}

  上面是ThreadPoolExecutor的构造方法,创建一个ThreadPoolExecutor一共需要7个参数:

  • corePoolSize:核心线程数。
  • maximumPoolSize:最大线程数。
  • keepAliveTime:工作线程空闲时的存活时间,当工作线程的空闲时间超过这个时间后,空闲工作线程将会被回收归还给操作系统,当然,核心线程会继续存活不参与回收。
  • unit:工作线程空闲时的存活时间单位,可指定为秒、分钟、小时等。
  • workQueue:工作队列,表示提交的任务将加入到哪个阻塞队列中。
  • threadFactory:线程工厂,用于自定义线程的创建,可以设定线程名称等。
  • handler:拒绝策略,当线程池中所有线程都处于工作中且等待队列已满的情况下,会根据拒绝策略来处理任务。

2.3 ThreadPoolExecutor任务提交流程

在这里插入图片描述
  向线程池提交一个任务的流程如下:

  1. 先判断线程池是否运行,如果线程池没有运行,会根据拒绝策略拒绝任务。
  2. 如果线程池正在运行,判断当前线程数是否小于核心线程数,如果是,则创建新的工作线程并执行任务。
  3. 如果当前线程数大于等于核心线程数,判断等待队列是否已满,如果未满,则将任务加入到等待队列中,等待工作线程执行。
  4. 如果队列已满,判断当前线程数是否小于最大线程数,如果是,则创建新的工作线程并执行任务。
  5. 如果当前线程数等于最大线程数,则根据拒绝策略拒绝任务。

2.4 线程工厂

  Executors提供了默认的线程工厂DefaultThreadFactory。但是在实际开发过程中,创建线程时,必须要为线程定义好有意义的线程名称,这样有助于出现异常时进行回溯。比如使用jstack导出线程栈的信息时,可以根据线程名称很方便地定位到抛出异常的代码。在创建线程池的时候,可以通过实现ThreadFactory接口来自定义创建线程的规则,在其中定义线程名称。

2.5 拒绝策略

  JDK默认提供了4种拒绝策略:

  • AbortPolicy:不执行任务且抛出异常。
  • DiscardPolicy:不执行任务,并且不会抛出异常。
  • DiscardOldestPolicy:将队列中最先提交的任务丢弃。
  • CallerRunsPolicy:由提交任务的线程来执行任务。

  在实际开发过程中,其实很少用到默认提供的拒绝策略。我们一般会通过实现RejectedExecutionHandler接口来自定义拒绝策略,比如将任务保存到数据库、放入消息队列进行持久化等,以便后续可以继续处理这些任务。

2.6 Executors提供的线程池

2.6.1 newCachedThreadPool

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

  该方法返回一个可以根据实际情况调整线程池中线程的数量的线程池。如果线程池中的线程数量过大,它可以有效的回收多余的线程,如果线程数不足,那么它可以创建新的线程。该线程池使用的工作队列是SynchronousQueue。

2.6.2 newFixedThreadPool

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

  该方法返回一个固定线程数量的线程池,该线程池中的线程数量始终不变,即不会再创建新的线程,也不会销毁已经创建好的线程,自始至终都是那几个固定的线程在工作,所以该线程池可以控制线程的最大并发数。该线程池使用的工作队列是LinkedBlockingQueue。

2.6.3 newSingleThreadExecutor

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

  该方法返回一个只有一个线程的线程池,即每次只能执行一个线程任务,多余的任务会保存到一个任务队列中,等待这一个线程空闲,当这个线程空闲了再按 FIFO 方式顺序执行任务队列中的任务,即保证了多个线程的执行顺序。该线程池使用的工作队列是LinkedBlockingQueue。

2.6.4 Executors总结

  开发过程中,不建议使用Executors创建线程池,FixedThreadPool和SingleThreadExecutor允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,导致OOM。CachedThreadPool允许创建的最大线程数为Integer.MAX_VALUE,可能会创建大量的线程,导致OOM。

2.7 ThreadPoolExecutor源码解析

2.7.1 Worker

private final class Worker
    extends AbstractQueuedSynchronizer
    implements Runnable
{
    /**
     * This class will never be serialized, but we provide a
     * serialVersionUID to suppress a javac warning.
     */
    private static final long serialVersionUID = 6138294804551838833L;

    /** Thread this worker is running in.  Null if factory fails. */
    final Thread thread;
    /** Initial task to run.  Possibly null. */
    Runnable firstTask;
    /** Per-thread task counter */
    volatile long completedTasks;

    /**
     * Creates with given first task and thread from ThreadFactory.
     * @param firstTask the first task (null if none)
     */
    Worker(Runnable firstTask) {
        setState(-1); // inhibit interrupts until runWorker
        this.firstTask = firstTask;
        // 使用了线程工厂创建了一个线程。传入的参数为当前worker
        this.thread = getThreadFactory().newThread(this);
    }
    ......
}

  Worker实际上是对工作线程的封装,其内部的一些操作依赖于AQS。Worker的关键点在于,创建一个Worker时,使用线程工厂创建一个线程,传入的参数是Worker本身,因为Worker本身也实现了Runnable接口。所以必须要有新任务提交时,才有可能创建新的线程。

2.7.2 execute

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    /*
     * Proceed in 3 steps:
     *
     * 1. If fewer than corePoolSize threads are running, try to
     * start a new thread with the given command as its first
     * task.  The call to addWorker atomically checks runState and
     * workerCount, and so prevents false alarms that would add
     * threads when it shouldn't, by returning false.
     *
     * 2. If a task can be successfully queued, then we still need
     * to double-check whether we should have added a thread
     * (because existing ones died since last checking) or that
     * the pool shut down since entry into this method. So we
     * recheck state and if necessary roll back the enqueuing if
     * stopped, or start a new thread if there are none.
     *
     * 3. If we cannot queue task, then we try to add a new
     * thread.  If it fails, we know we are shut down or saturated
     * and so reject the task.
     */
    int c = ctl.get();
    // 1. worker数量比核心线程数小,直接创建worker执行任务
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // 2. worker数量超过核心线程数,任务直接进入队列
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // 3. 如果线程池不是运行状态,或者任务进入队列失败,则尝试创建worker执行任务
    else if (!addWorker(command, false))
        reject(command);
}

  execute方法主要有三个步骤:

  1. worker数量比核心线程数小,直接创建worker执行任务。
  2. worker数量超过核心线程数,任务直接进入队列。
  3. 如果线程池不是运行状态,或者任务进入队列失败,则尝试创建worker执行任务。
    可参考2.3章节进行分析。

2.7.3 addWorker

private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // 先判断是否需要新增worker
        // Check if queue empty only if necessary.
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;

        for (;;) {
            int wc = workerCountOf(c);
            // worker数量超过容量,直接返回false
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            // 使用CAS的方式增加worker数量
            if (compareAndIncrementWorkerCount(c))
                break retry;
            c = ctl.get();  // Re-read ctl
            // 线程池状态发生变化,对外层循环进行自旋
            if (runStateOf(c) != rs)
                continue retry;
            // 其他情况,直接内层循环进行自旋即可
            // else CAS failed due to workerCount change; retry inner loop
        }
    }

    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
            final ReentrantLock mainLock = this.mainLock;
            // worker的添加必须是串行的,因此需要加锁
            mainLock.lock();
            try {
                // Recheck while holding lock.
                // Back out on ThreadFactory failure or if
                // shut down before lock acquired.
                int rs = runStateOf(ctl.get());

                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    workers.add(w);
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            // worker创建成功,启动worker线程
            if (workerAdded) {
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        // worker线程启动失败,说明线程池状态发生了变化(关闭操作被执行),需要进行shutdown相关操作
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

3 ForkJoinPool

在这里插入图片描述
  如上图所示,ForkJoin提供一种将大任务拆分成小任务,并将小任务的结果汇总成大任务结果的一种机制。使用案例:

public class TestForkJoin {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 构造数据
        int length = 100000000;
        long[] arr = new long[length];
        for (int i = 0; i < length; i++) {
            arr[i] = ThreadLocalRandom.current().nextInt(Integer.MAX_VALUE);
        }

        long start = System.currentTimeMillis();

        ForkJoinPool forkJoinPool = ForkJoinPool.commonPool();
        // 提交任务
        ForkJoinTask<Long> forkJoinTask = forkJoinPool.submit(new SumTask(arr, 0, arr.length));
        // 获取结果
        Long sum = forkJoinTask.get();

        forkJoinPool.shutdown();

        System.out.println("sum: " + sum);
        System.out.println("fork join elapse: " + (System.currentTimeMillis() - start));
    }
    
    private static class SumTask extends RecursiveTask<Long> {
        private long[] arr;
        private int from;
        private int to;

        public SumTask(long[] arr, int from, int to) {
            this.arr = arr;
            this.from = from;
            this.to = to;
        }

        @Override
        protected Long compute() {
            // 小于1000的时候直接相加,可灵活调整
            if (to - from <= 1000) {
                long sum = 0;
                for (int i = from; i < to; i++) {
                    // 模拟耗时
                    sum += (arr[i]/3*3/3*3/3*3/3*3/3*3);
                }
                return sum;
            }

            // 分成两段任务
            int middle = (from + to) / 2;
            SumTask left = new SumTask(arr, from, middle);
            SumTask right = new SumTask(arr, middle, to);

            // 提交左边的任务
            left.fork();
            // 右边的任务直接利用当前线程计算,节约开销
            Long rightResult = right.compute();
            // 等待左边计算完毕
            Long leftResult = left.join();
            // 返回结果
            return leftResult + rightResult;
        }
    }
}

总结

  JDK提供了两种类型的线程池:ThreadPoolExecutor和ForkJoinPool。ForkJoinPool 不是为了替代 ExecutorService,而是它的补充,在某些应用场景下性能比 ExecutorService 更好,ForkJoinPool 主要是运用“分而治之”的思想。此外,ThreadPoolExecutor内部维护了一个工作线程容器,其中的workers从同一个工作队列中获取任务并执行;而ForkJoin中的每个工作线程会维护单独的等待队列。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值