并发编程-深入理解线程池

201 阅读8分钟

# Pxoolcm的并发编程第三期-深入理解线程池

感谢大家支持,在前两期的并发编程之后,我更坚定了完成整个并发编程专栏的决心,一切动力都来自于自己的自律精神,同时也感谢于各位看我的文章,让我感觉自己的只是可以帮助更多人!接下来,让我们开始第三期的旅程-深入理解线程池。

为什么使用线程池

使用线程来执行程序

package bat.ke.qq.com.threadpool;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/***
 * 使用线程的方式去执行程序
 */
public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        Long start = System.currentTimeMillis();
        final Random random = new Random();
        final List<Integer> list = new ArrayList<Integer>();
        for (int i = 0; i < 100000; i++) {
            Thread thread = new Thread() {
                @Override
                public void run() {
                    list.add(random.nextInt());
                }
            };
            thread.start();
            thread.join();
        }
        System.out.println("时间:" + (System.currentTimeMillis() - start));
        System.out.println("大小:" + list.size());
    }
}

大家很简单的看到这段程序的作用是什么:创建十万个线程取随机数加入到list中,接下来是我运行程序的结果:

image.png 大家可以看到时间是8286ms,因为使用了join,所以必须该线程结束了才会进行下一个线程的执行,故没有出现并发造成的共享变量不可见问题。

使用线程池执行程序

package bat.ke.qq.com.threadpool;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/***
 * 线程池执行
 */
public class ThreadPoolTest {
    public static void main(String[] args) throws InterruptedException {
        Long start = System.currentTimeMillis();
        final Random random = new Random();
        final List<Integer> list = new ArrayList<Integer>();

        ExecutorService executorService = Executors.newSingleThreadExecutor();

        for (int i = 0; i < 100000; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    list.add(random.nextInt());
                }
            });
        }

        executorService.shutdown();
        executorService.awaitTermination(1, TimeUnit.DAYS);

        System.out.println("时间:" + (System.currentTimeMillis() - start));
        System.out.println("大小:" + list.size());
    }
}

在这里使用了ExecutorService类,是一个创建线程池的类,之后执行ExecutorService实例对象的excute方法来创建线程,再进行业务处理。接下来是我执行的代码结果:

image.png 可以看到,时间是31ms,对比单个创建线程,线程池快了不止十倍。这就是处理多线程问题时使用线程池的原因,而且,线程池对于维护线程的状态和安全各个方面又更大的优势。所以,大家显式创建线程时,IDEA也会进行警告:不要显式创建线程,请使用线程池。

线程池

大家肯定想知道为什么单个创建线程会那么消耗时间(8286ms),其实是因为单个创建线程时会大量消耗CPU和内存资源,在创建资源这里就花了很多时间,这种对程序的性能造成十分大的影响。所以Java的开发者开发出了线程池,那大家肯定也想知道为什么线程池可以这么高效,其实是因为线程池提供了管理线程的方法,比如可以让线程在队列中等待,这样就可以让前一个线程销毁了而下一个线程直接上,而不是先创建再上,这点可以节省很多时间。线程池底层还实现了多种线程池,也提供了多种保证线程安全的操作,比如CAS,AQS等,接下来我们都会带着大家揭开线程池的神秘面纱。

image.png

线程池基本组成

在介绍这些核心组成的时候,我们可以把线程池当作可以公司,这样可以更好的帮助大家理解。 核心线程数(corePoolSize):线程池中始终维护的线程数量。首先理解下始终维护的对象是什么意思?其实很简单,就是线程池中始终会保持这个对象进行任务的处理,如果有五个核心线程数,那么就一定会有五个线程来进行对任务的处理(如果任务不足五个那么其他线程会等待)。在公司中就是一些核心员工,这些员工是一直待命的,如果有任务他们肯定先上。

最大线程数(maximum Pool Size):线程池中最多的线程数量。在这里大家可能会有个误区,就是最大线程数量是不包含核心线程数的,其实这是错误的。最大线程数量是包含了核心线程数的,比如核心线程数等于最大线程数量,那么其他的线程数量就为0。这个概念也好理解,就是如果任务数量大于核心线程数了,那么就会调用除开核心线程数的线程来进行对任务的处理。在公司中可以理解为次要员工,只有当核心员工的人手腾不出来的时候才会调用这些次要员工来进行任务的处理。当然,这样的员工的效率肯定没有核心员工高的,因为核心员工才是公司一直维护管理的对象。

工作队列(workQueue):底层的数据结构是阻塞队列,用来保存那些暂时还在等待被执行的任务,因为最大线程数量的全部线程都在执行任务,所以现在暂时没有线程来执行任务了,这些任务就被放在阻塞队列中,因为队列是先进先出的,所以先到的任务先被执行。在公司中可以看作一个任务的清单,这个清单保存了那些任务暂时不能被执行的,但是后期可以被任意一个线程执行。

线程工厂(threadFactory):这就很简单了,就是创建线程的工厂,用来创建各种线程,相当于我们自己手动创建线程,只不过线程池帮我们创建了。放到公司这就是人才市场,可以招募工人。

拒绝策略(handler):这也很好理解,就是连任务队列都满了的时候,还来加进来任务,这时候肯定处理不了了,那么这时候我们就要想怎么拒绝这种任务。

线程池生命周期

线程池从诞生到死亡,中间会经历RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED五个生命周期状态。

RUNNING:线程池处于运行状态,可以接受新提交的任务也可以处理新提交的任务。该状态是线程池的初始状态,在线程池刚被创建的时候,线程池就进入了RUNNING状态。

SHUTDOWN:线程池处于关闭状态,但是不是真正意义上的关闭,会处理已经添加的任务,但不接受新添加的任务。在RUNNING状态时调用shutdown会进入SHUTDOWN状态。

STOP:线程处于停止状态,这才是真正意义上的关闭,不处理已经添加的任务,也不会处理新添加的任务,可以简单的理解为突然“宕机”,什么也不处理。RUNNING状态调用shutdownNow后线程池进入STOP状态。

TIDING:所有任务已经终止(这里不能说完成,因为有些任务是没有正常完成的,但是也是结束的任务),并且任务数量为0时,线程池会进入TIDYING。当线程池处于SHUTDOWN并且阻塞队列中任务数量为0,这时候状态会进入TIDYING。当线程池处于STOP状态,线程池中也没有正在执行的任务,状态会由STOP进入TIDYING。

TERMINATED:线程终止状态。处于TIDYING状态的线程执行terminated进入TERMINATED。

// runState is stored in the high‐order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;

image.png

线程池工作流程

线程池家族

image.png

execute方法

从刚开始提供的代码可以看到,其实线程池开始执行的方法就是execute方法,接下来我们会详细剖析。

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();
if (workerCountOf(c) < corePoolSize) {
    if (addWorker(command, true))
        return;
    c = ctl.get();
}
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);
}
else if (!addWorker(command, false))
    reject(command);

这就是execute的源码,可以看到上面有很长的文本注释,上面分为三步讲。

第一步

判断当前线程数是否小于核心线程数,如果是,那么就通过addWorker方法传入任务并且创建一个新线程。如果可以完成新线程创建execute方法结束,那么就提交任务。

第二步

如果第一步没有成功提交任务,那么就将任务状态保存为运行态(RUNNING),再将任务加入到工作队列中成功后。进行一次重检查(recheck)。如果这次检查任务不是RUNNING态(可能是执行到这里线程池变执行shutdown了,进入了SHUTDOWN,那么这样肯定是错误的)。这时候要拒绝任务(reject)。之后再判断这次重检查的线程数量是否为0,如果是,创建一个线程,但是不执行任务。

第三步

如果通过任务传参创建新线程失败了,那么就证明阻塞队列也满了,就是线程池是饱和状态或者是shutdown,所以进行拒绝(reject)。从上面新增任务的execute方法也可以看出,拒绝策略不仅仅是在饱和状态下使用,在线程池进入到关闭阶段同样需要使用到;上面的几行代码还不能完全清楚这个新增任务的过程,还需要addWorker方法!