阿里架构师讲面试:最easy的线程池讲解

1,491 阅读15分钟

为什么引入线程池

背景

许多服务器应用常常需要处理大量而短小的请求(例如,Web 服务器,数据库服务器等等),通常它们收到的请求数量很大,一个简单的模型是,当服务器收到来自远程的请求时,为每一个请求开启一个线程,在请求完毕之后再对线程进行销毁。这样处理带来的问题是,创建和销毁线程所消耗的时间往往比任务本身所需消耗的资源要大得多。那么应该怎么办呢? 线程池为线程生命周期开销问题和资源不足问题提供了解决方案。我们可以通过线程池做到线程复用,不需要频繁的创建和销毁线程,让线程池中的线程一直存在于线程池中,然后线程从任务队列中取得任务来执行。而且这样做的另一个好处有,通过适当地调整线程池中的线程数目,也就是当请求的数目超过某个阈值时,就强制其它任何新到的请求一直等待,直到获得一个线程来处理为止,从而可以防止资源不足。

线程池是什么

线程池的基本思想是一种对象池,在程序启动时就开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建销毁线程对象所带来的性能开销,节省了系统的资源。

线程池好处<相比于每个任务创建一个线程>

  • 减少线程创建和销毁次数。(需要系统调用支持,到内核态执行)
  • 加快任务响应速度。(直接分配空闲线程)
  • 可以根据系统负载有效控制最大并发数。

线程池原理

组成结构

1、线程池管理器(ThreadPoolManager):用于创建并管理线程池。

2、工作线程(WorkThread): 线程池中线程。

3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行。

4、任务队列:用于存放没有处理的任务。提供一种缓冲机制。

线程池参数

corePoolSize

线程池核心线程数量,核心线程不会被回收,即使没有任务执行,也会保持空闲状态。如果线程池中的线程少于此数目,则在执行任务时创建(不管当前核心线程池有没有空闲线程)。

workQueue

当前线程数超过corePoolSize时,新进的任务会处在等待状态,并存在workQueue中。

maximumPoolSize

池允许最大的线程数,当线程数量达到corePoolSize,且workQueue队列塞满任务了之后,继续创建线程。

keepAliveTime

超过corePoolSize之后的“临时线程”的存活时间。

unit

keepAliveTime的单位。

allowCoreThreadTimeout

是否允许核心线程空闲退出,默认值为false。

RejectExecutionHandler

线程池执行拒绝策略,当workQueue已经塞满了任务,并且线程数量达到maximumPoolSize大小的情况下,线程池会调用handler拒绝策略来处理请求。系统默认的拒绝策略有以下几种:

  • AbortPolicy

直接抛出RejectedExecutionExeception异常来阻止系统正常运行,系统默认策略。

  • DisCardPolicy

直接抛弃当前任务,不做处理。

  • DiscardOldestPolicy

丢弃队列中最老的任务。

  • CallerRunsPolicy

将任务分配给当前执行execute方法线程来处理。

public static class CallerRunsPolicy implements RejectedExecutionHandler {
    public CallerRunsPolicy() { }
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            r.run();
        }
    }
}

注意在添加第五个任务,任务5 的时候,同样被线程池拒绝了,因此执行了CallerRunsPolicy的rejectedExecution方法,这个方法直接执行任务的run方法。因此可以看到任务5是在main线程中执行的。

从中也可以看出,因为第五个任务在主线程中运行,所以主线程就被阻塞了,以至于当第五个任务执行完,添加第六个任务时,前面两个任务已经执行完了,有了空闲线程,因此线程6又可以添加到线程池中执行了。

  • 自定义策略

我们还可以自定义拒绝策略,只需要实现RejectedExecutionHandler接口即可,友好的拒绝策略实现有如下:

  1. 将数据保存到数据库,待系统空闲时再进行处理
  2. 将数据用日志进行记录,后由人工处理
static class MyRejectedExecutionHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        new Thread(r,"新线程"+new Random().nextInt(10)).start();
    }
}
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 2, 30,
        TimeUnit.SECONDS,
        new LinkedBlockingDeque<Runnable>(2),
        new MyRejectedExecutionHandler());

执行过程

新任务进来时:

  1. 如果当前运行的线程少于corePoolSize<注意:即使有空闲线程也是直接创建>,则创建新线程(核心线程)来执行任务。
  2. 如果运行的线程等于或多于corePoolSize ,则将任务加入BlockingQueue。
  3. 如果BlockingQueue队列已满,则创建新的线程(非核心)来处理任务。
  4. 如果核心线程与非核心线程总数超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler拒绝策略。

Java util包下的线程池核心类ThreadPoolExecutor执行execute()方法的流程图

线程池生命周期

  • RUNNING:接收新的任务并处理队列中的任务。
  • SHUTDOWN:不接收新的任务,但是处理队列中的任务。
  • STOP:不接收新的任务,不处理队列中的任务,同时中断处理中的任务。
  • TIDYING:所有的任务处理完成,有效的线程数是0。
  • TERMINATED:terminated()方法执行完毕。

转换成TIDYING状态的线程池会运行terminated方法。

线程池使用

线程池创建的两种方式

ThreadPoolExecutor

public ThreadPoolExecutor(int corePoolSize,//核心线程数大小
                          int maximumPoolSize,//最大线程数
                          long keepAliveTime,//当线程数大于corePoolSize的空闲线程能保持的最大时间
                          TimeUnit unit,//非核心线程池线程保持时间单位
                          BlockingQueue<Runnable> workQueue,//保存任务的阻塞队列
                          ThreadFactory threadFactory,//创建线程的工厂
                          RejectedExecutionHandler handler//拒绝策略)

Executors(不推荐)

  • newFixedThreadPool(LinkedBlockingQueue)

线程池线程数量固定,即corePoolSize和maximumPoolSize数量一样。等待队列为无限队列。

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(
            nThreads,
            nThreads,
            0L,
            TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>());
}
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
            Executors.defaultThreadFactory(), defaultHandler);
}
  • newSingleThreadExecutor

单线程执行,即corePoolSize和maximumPoolSize数量指定为1。等待队列为无限队列

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

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
            Executors.defaultThreadFactory(), defaultHandler);
}
  • newCachedThreadPool

由于corePoolSize为0,所以任务会放入SynchronousQueue队列中,SynchronousQueue只能存放大小为0,所以会立刻新起线程,由于maxumumPoolSize为Integer.MAX_VALUE所以可以认为大小为2147483647,受内存大小限制。线程空闲时存活时间为60秒,这会导致任务每次进来都会创建线程来执行,在线程空闲时,存活时间到了又会释放线程资源。

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(
            0,
            Integer.MAX_VALUE,
            60L,
            TimeUnit.SECONDS,
            new SynchronousQueue<Runnable>());
}
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
            Executors.defaultThreadFactory(), defaultHandler);
}

  • newscheduledThreadPool
// ScheduledThreadPool
public ScheduledThreadPoolExecutor(int corePoolSize) {
    // 允许创建线程数为Integer.MAX_VALUE
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }

它用来处理延时任务或定时任务。

  • 它接收SchduledFutureTask类型的任务,有两种提交任务的方式:

  • scheduledAtFixedRate

  • scheduledWithFixedDelay

  • SchduledFutureTask接收的参数:

  • time:任务开始的时间

  • sequenceNumber:任务的序号

  • period:任务执行的时间间隔

  • 它采用DelayQueue存储等待的任务

  • DelayQueue内部封装了一个PriorityQueue,它会根据time的先后时间排序,若time相同则根据sequenceNumber排序;

  • DelayQueue也是一个无界队列;

  • 工作线程的执行过程:

  • 工作线程会从DelayQueue取已经到期的任务去执行;

  • 执行结束后重新设置任务的到期时间,再次放回DelayQueue

不推荐使用Executors创建线程池

  • newFixedThreadPool&newSingleThreadExecutor使用无限队列,大小为Integer.MAX_VALUE。
  • newCachedThreadPool&newscheduledThreadPool最大线程池大小设置为Integer.MAX_VALUE。

容易引起内存溢出。

创建线程demo

package executor;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Executor {

    /**
     * @param args
     */
    public static void main(String[] args) {
        //实际上不推荐用该方法创建线程
        ExecutorService executorService=Executors.newFixedThreadPool(10);
    }

}

向线程池添加任务,获取线程执行完毕后结果

executorService.execute(Runnable);

package executor;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Executor {

    /**
     * @param args
     * 
     */

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        ExecutorService executorService=Executors.newFixedThreadPool(2);//定义了线程池中最大存在的线程数目...

        //添加了第一个任务...这个任务会一直被执行...
        executorService.execute(new Runnable() {

            @Override
            public void run() {
                // TODO Auto-generated method stub
                while(true){

                    System.out.println("aa");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            }
        });

        //添加第二个任务,被执行三次停止...
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                // TODO Auto-generated method stub
                int i=0;
                while(true){
                    i++;
                    System.out.println("bb");
                    if(i==3){
                        break;
                    }
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }    
                }
            }
        });

        /*
         * @param
         * 第三个任务...只有当第二个任务被执行三次之后才能被执行...
         * 由于三次前,线程池已经满了,这个任务是轮不到被执行的..只能排队进行等待. 
         * 三次之后,第二个任务被终止,也就是线程池中出现了空闲的状态,所以这个任务将被放入到线程池中执行...
         * */
        executorService.execute(new Runnable() {

            @Override
            public void run() {
                // TODO Auto-generated method stub
                while(true){

                    System.out.println("cc");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            }
        });
    }

}

executorService.submit(Runnable);

这种方式与第一种的区别在于可以使用一个Future对象来判断当前的线程是否执行完毕。但是这种方法只能判断当前的线程是否执行完毕,无法返回数据信息。

Future future = executorService.submit(new Runnable() {
    public void run() {
        System.out.println("Asynchronous task");
    }
});
//如果任务结束执行则返回 null
System.out.println("future.get()=" + future.get());

executorService.submit(Callable);

这种调用方式与前一种有所不同,传递的参数为Callable对象,Callable与Runnbale很相似,但是Callable的call()方法可以返回数据信息。通过Future就能够获取到其中的信息。而Runnbale.run()方法是无法获取数据信息的。

Future future = executorService.submit(new Callable(){
    public Object call() throws Exception {
        System.out.println("Asynchronous Callable");
        return "Callable Result";
    }
});

System.out.println("future.get() = " + future.get());

//上述样例代码会输出如下结果: 
//Asynchronous Callable
//future.get() = Callable Result

executorService.invokeAny(callables);

invokeAny() 接收一个包含 Callable 对象的集合作为参数。调用该方法不会返回 Future 对象,而是返回集合中某一个 Callable 对象的结果,而且无法保证调用之后返回的结果是哪一个Callable,只知道它是这些 Callable 中一个执行结束的 Callable 对象。说实话这个方法我不知道它创建的目的到底是什么。这里执行后的结果是随机的。也就是输出是不固定的。

ExecutorService executorService = Executors.newSingleThreadExecutor();

Set<Callable<String>> callables = new HashSet<Callable<String>>();

callables.add(new Callable<String>() {
    public String call() throws Exception {
        return "Task 1";
    }
});
callables.add(new Callable<String>() {
    public String call() throws Exception {
        return "Task 2";
    }
});
callables.add(new Callable<String>() {
    public String call() throws Exception {
        return "Task 3";
    }
});

String result = executorService.invokeAny(callables);

System.out.println("result = " + result);

executorService.invokeAll(callables);

这个方法和上面不同的地方就在于它可以返回所有Callable的执行结果。获取到所有的执行结果,我们可以对其进行管理。相对而言,我觉得这个方法比上一个更实用吧。

ExecutorService executorService = Executors.newSingleThreadExecutor();

Set<Callable<String>> callables = new HashSet<Callable<String>>();

callables.add(new Callable<String>() {
    public String call() throws Exception {
        return "Task 1";
    }
});
callables.add(new Callable<String>() {
    public String call() throws Exception {
        return "Task 2";
    }
});
callables.add(new Callable<String>() {
    public String call() throws Exception {
        return "Task 3";
    }
});

List<Future<String>> futures = executorService.invokeAll(callables);

for(Future<String> future : futures){
    System.out.println("future.get = " + future.get());

线程池关闭

executorService.shutdown();

shutdown并不是直接关闭线程池,而是不再接受新的任务。如果线程池内有任务,那么把这些任务执行完毕后,关闭线程池。

executorService.shutdownNow();

这个方法表示不再接受新的任务,并把任务队列中的任务直接移出掉,如果有正在执行的,尝试进行停止。

package executor;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Executor {

    /**
     * @param args
     * 
     */

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        ExecutorService executorService=Executors.newFixedThreadPool(1);//定义了线程池中最大存在的线程数目...

        //添加了第一个任务...这个执行三次停止...
        executorService.execute(new Runnable() {

            @Override
            public void run() {
                // TODO Auto-generated method stub
                int j=0;
                while(true){
                    j++;
                    System.out.println("aa");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    if(j==3){
                        break;
                    }
                }
            }
        });

        //添加第二个任务,由于使用executorService.shutdown(),由于它的加入是在这个方法调用之前的,因此这个任务也会被执行...
        //如果我们使用了executorService.shutdownNow();方法,就算是他在之前加入的,由于调用了executorService.shutdownNow()方法
        //那么这个任务将直接被移出队列并且不会被执行...
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                // TODO Auto-generated method stub
                int i=0;
                while(true){
                    i++;
                    System.out.println("bb");
                    if(i==3){
                        break;
                    }
                }
            }
        });
        executorService.shutdown();//这里无论使用了那种方法,都会抛出一个异常...
        /*
         * @param
         * 第三个任务...只有当第二个任务被执行三次之后才能被执行...
         * 由于三次前,线程池已经满了,这个任务是轮不到被执行的..只能排队进行等待. 
         * 三次之后,第二个任务被终止,也就是线程池中出现了空闲的状态,所以这个任务将被放入到线程池中执行...
         * */
        executorService.execute(new Runnable() {

            @Override
            public void run() {
                // TODO Auto-generated method stub
                while(true){

                    System.out.println("cc");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            }
        });
    }

}

参考:

www.cnblogs.com/ldq2016/p/8…

线程池参数设置调优?

参数的设置跟系统的负载有直接的关系,下面为系统负载的相关指标:

  • tasks,每秒需要处理的的任务数(针对系统需求)
  • threadtasks,每个线程每钞可处理任务数(针对线程本身)
  • responsetime,系统允许任务最大的响应时间,比如每个任务的响应时间不得超过1秒。

核心线程池大小

通常我们是需要根据这批任务执行的性质来确定核心线程池大小:

  • IO 密集型任务:由于线程并不是一直在运行,所以可以尽可能的多配置线程,比如 CPU 个数 * 2。
  • CPU 密集型任务(大量复杂的运算)应当分配较少的线程,比如 CPU 个数相当的大小。

当然这些都是经验值,最好的方式还是根据实际情况测试得出最佳配置。

系统每秒有tasks个任务需要处理,则每个线程每秒可处理threadtasks个任务。则需要的线程数为:tasks/threadtasks。

假设系统每秒任务数为100 ~ 1000,每个线程每秒可处理10个任务,则需要100 / 10至1000 / 10,即10 ~ 100个线程。那么corePoolSize应该设置为大于10,具体数字最好根据8020原则,因为系统每秒任务数为100 ~ 1000,即80%情况下系统每秒任务数小于1000 * 20% = 200,则corePoolSize可设置为200 / 10 = 20。

最大线程池大小

当系统负载达到最大值时,核心线程数已无法按时处理完所有任务,这时就需要增加线程。每秒200个任务需要20个线程,那么当每秒达到1000个任务时,则需要(1000-queueCapacity)*(20/200),即60个线程,可将maxPoolSize设置为60。

workQueue

当前线程数超过corePoolSize时,新的任务会处在等待状态,并存在workQueue中。

线程池监控

线程池使用不当也会使服务器资源枯竭,导致异常情况的发生,比如固定线程池的阻塞队列任务数量过多、缓存线程池创建的线程过多导致内存溢出、系统假死等问题。因此,我们需要一种简单的监控方案来监控线程池的使用情况,比如完成任务数量、未完成任务数量、线程大小等信息。线程池提供了以下几个方法可以监控线程池的使用情况:

**方法****含义**
getActiveCount()线程池中正在执行任务的线程数量
getCompletedTaskCount()线程池已完成的任务数量,该值小于等于taskCount
getCorePoolSize()线程池的核心线程数量
getLargestPoolSize()线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过,也就是达到了maximumPoolSize
getMaximumPoolSize()线程池的最大线程数量
getPoolSize()线程池当前的线程数量
getTaskCount()线程池已经执行的和未执行的任务总数

在ThreadPoolExecutor类中提供了几个空方法,如beforeExecute方法, afterExecute方法和terminated方法,通过继承线程池并重写线程池的beforeExecute,afterExecute和terminated方法,可以在任务执行前,执行后和线程池关闭前干一些事情。如监控任务的平均执行时间,最大执行时间和最小执行时间等。这几个方法在线程池里是空方法。

此外,还可以关注cpu占用率,内存占用率等硬件信息来综合判断。

线程池隔离

如果我们很多业务都依赖于同一个线程池,当其中一个业务因为各种不可控的原因消耗了所有的线程,导致线程池全部占满。

这样其他的业务也就不能正常运转了,这对系统的打击是巨大的。

比如我们 Tomcat 接受请求的线程池,假设其中一些响应特别慢,线程资源得不到回收释放;线程池慢慢被占满,最坏的情况就是整个应用都不能提供服务。

所以我们需要将线程池进行隔离

通常的做法是按照业务进行划分:

比如下单的任务用一个线程池,获取数据的任务用另一个线程池。这样即使其中一个出现问题把线程池耗尽,那也不会影响其他的任务运行。

参考:juejin.cn/post/684490…

segmentfault.com/a/119000001…

segmentfault.com/a/119000001…

cloud.tencent.com/developer/a…

blog.csdn.net/qq_25806863…

www.jianshu.com/p/2a80237c3…

觉得有收获的话帮忙点个赞吧,让有用的知识分享给更多的人

## 欢迎关注掘金号:五点半社

## 关注微信公众号:五点半社(工薪族的财商启蒙)##