程序员必需要会的线程池,附10道面试题

为何要用线程池呢?

下面是一段建立线程并运行的代码:java

for (int i = 0; i < 100; i++) {
    new Thread(() -> {
        System.out.println("run thread->" + Thread.currentThread().getName());
        userService.updateUser(....);
    }).start();
}

咱们想使用这种方式去作异步,或者提升性能,而后将某些耗时操做放入一个新线程去运行。面试

这种思路是没问题的,可是这段代码是存在问题的,有哪些问题呢?下面咱们就来看看有哪些问题;数据库

  • 建立销毁线程资源消耗;咱们使用线程的目的本是出于效率考虑,能够为了建立这些线程却消耗了额外的时间,资源,对于线程的销毁一样须要系统资源。缓存

  • cpu资源有限,上述代码建立线程过多,形成有的任务不能即时完成,响应时间过长。网络

  • 线程没法管理,无节制地建立线程对于有限的资源来讲彷佛成了“得不偿失”的一种做用。并发

既然咱们上面使用手动建立线程会存在问题,那有解决方法吗?异步

答案:有的,使用线程池。ide

线程池介绍

线程池(Thread Pool):把一个或多个线程经过统一的方式进行调度和重复使用的技术,避免了由于线程过多而带来使用上的开销。性能

线程池有什么优势?

  • 下降资源消耗。经过重复利用已建立的线程下降线程建立和销毁形成的消耗。测试

  • 提升响应速度。当任务到达时,任务能够不须要等到线程建立就能当即执行。

  • 提升线程的可管理性。

线程池使用

在JDK中rt.jar包下JUC(java.util.concurrent)建立线程池有两种方式:ThreadPoolExecutor 和 Executors,其中 Executors又能够建立 6 种不一样的线程池类型。

ThreadPoolExecutor 的使用

线程池使用代码以下:

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolDemo {
    private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 10, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue(100));

    public static void main(String[] args) {
        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("田先生您好");
            }
        });
    }
}

以上程序执行结果以下:

田先生您好

核心参数说明

ThreadPoolExecutor的构造方法有如下四个:

img

能够看到最后那个构造方法有 7 个构造参数,其实前面的三个构造方法只是对最后那个方法进行包装,而且前面三个构造方法最终都是调用最后那个构造方法,因此咱们这里就来聊聊最后那个构造方法。

参数解释

corePoolSize

线程池中的核心线程数,默认状况下核心线程一直存活在线程池中,若是将 ThreadPoolExecutor 的 allowCoreThreadTimeOut 属性设为 true,若是线程池一直闲置并超过了 keepAliveTime 所指定的时间,核心线程就会被终止。

maximumPoolSize

最大线程数,当线程不够时可以建立的最大线程数。

keepAliveTime

线程池的闲置超时时间,默认状况下对非核心线程生效,若是闲置时间超过这个时间,非核心线程就会被回收。若是 ThreadPoolExecutor 的 allowCoreThreadTimeOut 设为 true 的时候,核心线程若是超过闲置时长也会被回收。

unit

配合 keepAliveTime 使用,用来标识 keepAliveTime 的时间单位。

workQueue

线程池中的任务队列,使用 execute() 或 submit() 方法提交的任务都会存储在此队列中。

threadFactory

为线程池提供建立新线程的线程工厂。

rejectedExecutionHandler

线程池任务队列超过最大值以后的拒绝策略,RejectedExecutionHandler 是一个接口,里面只有一个 rejectedExecution 方法,可在此方法内添加任务超出最大值的事件处理。ThreadPoolExecutor 也提供了 4 种默认的拒绝策略:

  • DiscardPolicy():丢弃掉该任务,不进行处理。

  • DiscardOldestPolicy():丢弃队列里最近的一个任务,并执行当前任务。

  • AbortPolicy():直接抛出 RejectedExecutionException 异常(默认)。

  • CallerRunsPolicy():既不抛弃任务也不抛出异常,直接使用主线程来执行此任务。

包含全部参数的使用案例:

public class ThreadPoolExecutorTest {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1,
                10L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(2),
                new MyThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
        threadPool.allowCoreThreadTimeOut(true);
        for (int i = 0; i < 10; i++) {
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}
class MyThreadFactory implements ThreadFactory {
    private AtomicInteger count = new AtomicInteger(0);
    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r);
        String threadName = "MyThread" + count.addAndGet(1);
        t.setName(threadName);
        return t;
    }
}

运行输出:

main
MyThread1
main
MyThread1
MyThread1
....

这里仅仅是为了演示全部参数自定义,并无其余用途。

execute() 和 submit()的使用

execute() 和 submit() 都是用来执行线程池的,区别在于 submit() 方法能够接收线程池执行的返回值。

下面分别来看两个方法的具体使用和区别:

// 建立线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 10, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue(100));
// execute 使用
threadPoolExecutor.execute(new Runnable() {
    @Override
    public void run() {
        System.out.println("老田您好");
    }
});
// submit 使用
Future<String> future = threadPoolExecutor.submit(new Callable<String>() {
    @Override
    public String call() throws Exception {
        System.out.println("田先生您好");
        return "返回值";
    }
});
System.out.println(future.get());

以上程序执行结果以下:

老田您好
田先生您好
返回值

Executors

Executors 执行器建立线程池不少基本上都是在 ThreadPoolExecutor 构造方法上进行简单的封装,特殊场景根据须要自行建立。能够把Executors理解成一个工厂类 。Executors能够建立 6 种不一样的线程池类型。

下面对这六个方法进行简要的说明:

newFixedThreadPool

建立一个数量固定的线程池,超出的任务会在队列中等待空闲的线程,可用于控制程序的最大并发数。

newCacheThreadPool

短期内处理大量工做的线程池,会根据任务数量产生对应的线程,并试图缓存线程以便重复使用,若是限制 60 秒没被使用,则会被移除缓存。若是现有线程没有可用的,则建立一个新线程并添加到池中,若是有被使用完可是还没销毁的线程,就复用该线程。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。所以,长时间保持空闲的线程池不会使用任何资源。

newScheduledThreadPool

建立一个数量固定的线程池,支持执行定时性或周期性任务。

newWorkStealingPool

Java 8 新增建立线程池的方法,建立时若是不设置任何参数,则以当前机器CPU 处理器数做为线程个数,此线程池会并行处理任务,不能保证执行顺序。

newSingleThreadExecutor

建立一个单线程的线程池。这个线程池只有一个线程在工做,也就是至关于单线程串行执行全部任务。若是这个惟一的线程由于异常结束,那么会有一个新的线程来替代它。此线程池保证全部任务的执行顺序按照任务的提交顺序执行。

newSingleThreadScheduledExecutor

此线程池就是单线程的 newScheduledThreadPool。

线程池如何关闭?

线程池关闭,可使用 shutdown() 或 shutdownNow() 方法,它们的区别是:

  • shutdown():不会当即终止线程池,而是要等全部任务队列中的任务都执行完后才会终止。执行完 shutdown 方法以后,线程池就不会再接受新任务了。

  • shutdownNow():执行该方法,线程池的状态马上变成 STOP 状态,并试图中止全部正在执行的线程,再也不处理还在池队列中等待的任务,执行此方法会返回未执行的任务。

下面用代码来模拟 shutdown() 以后,给线程池添加任务,代码以下:

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class ThreadPoolExecutorAllArgsTest {
   public static void main(String[] args) throws InterruptedException, ExecutionException {
       //建立线程池
       ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1,
               10L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(2),
               new MyThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
       threadPoolExecutor.allowCoreThreadTimeOut(true);
       //提交任务
       threadPoolExecutor.execute(() -> {
           for (int i = 0; i < 3; i++) {
               System.out.println("提交任务" + i);
               try {
                   Thread.sleep(3000);
               } catch (InterruptedException e) {
                   System.out.println(e.getMessage());
               }
           }
       });
       threadPoolExecutor.shutdown();
       //再次说起任务
       threadPoolExecutor.execute(() -> {
           System.out.println("我想再次说起任务");
       });
   }
}

以上程序执行结果以下:

提交任务0
提交任务1
提交任务2

能够看出,shutdown() 以后就不会再接受新的任务了,不过以前的任务会被执行完成。

面试题

面试题1:ThreadPoolExecutor 有哪些经常使用的方法?

ThreadPoolExecutor有以下经常使用方法:

  • submit()/execute():执行线程池

  • shutdown()/shutdownNow():终止线程池

  • isShutdown():判断线程是否终止

  • getActiveCount():正在运行的线程数

  • getCorePoolSize():获取核心线程数

  • getMaximumPoolSize():获取最大线程数

  • getQueue():获取线程池中的任务队列

  • allowCoreThreadTimeOut(boolean):设置空闲时是否回收核心线程

这些方法能够用来终止线程池、线程池监控等。

面试题2:说说submit(和 execute两个方法有什么区别?

submit() 和 execute() 都是用来执行线程池的,只不过使用 execute() 执行线程池不能有返回方法,而使用 submit() 可使用 Future 接收线程池执行的返回值。

说说线程池建立须要的那几个核心参数的含义

ThreadPoolExecutor 最多包含如下七个参数:

  • corePoolSize:线程池中的核心线程数

  • maximumPoolSize:线程池中最大线程数

  • keepAliveTime:闲置超时时间

  • unit:keepAliveTime 超时时间的单位(时/分/秒等)

  • workQueue:线程池中的任务队列

  • threadFactory:为线程池提供建立新线程的线程工厂

  • rejectedExecutionHandler:线程池任务队列超过最大值以后的拒绝策略

面试题3:shutdownNow() 和 shutdown() 两个方法有什么区别?

shutdownNow() 和 shutdown() 都是用来终止线程池的,它们的区别是,使用 shutdown() 程序不会报错,也不会当即终止线程,它会等待线程池中的缓存任务执行完以后再退出,执行了 shutdown() 以后就不能给线程池添加新任务了;shutdownNow() 会试图立马中止任务,若是线程池中还有缓存任务正在执行,则会抛出 java.lang.InterruptedException: sleep interrupted 异常。

面试题6:了解过线程池的工做原理吗?

img

当线程池中有任务须要执行时,线程池会判断若是线程数量没有超过核心数量就会新建线程池进行任务执行,若是线程池中的线程数量已经超过核心线程数,这时候任务就会被放入任务队列中排队等待执行;若是任务队列超过最大队列数,而且线程池没有达到最大线程数,就会新建线程来执行任务;若是超过了最大线程数,就会执行拒绝执行策略。

面试题5:线程池中核心线程数量大小怎么设置?

「CPU密集型任务」:好比像加解密,压缩、计算等一系列须要大量耗费 CPU 资源的任务,大部分场景下都是纯 CPU 计算。尽可能使用较小的线程池,通常为CPU核心数+1。由于CPU密集型任务使得CPU使用率很高,若开过多的线程数,会形成CPU过分切换。

「IO密集型任务」:好比像 MySQL 数据库、文件的读写、网络通讯等任务,这类任务不会特别消耗 CPU 资源,可是 IO 操做比较耗时,会占用比较多时间。可使用稍大的线程池,通常为2*CPU核心数。IO密集型任务CPU使用率并不高,所以可让CPU在等待IO的时候有其余线程去处理别的任务,充分利用CPU时间。

另外:线程的平均工做时间所占比例越高,就须要越少的线程;线程的平均等待时间所占比例越高,就须要越多的线程;

以上只是理论值,实际项目中建议在本地或者测试环境进行屡次调优,找到相对理想的值大小。

面试题7:线程池为何须要使用(阻塞)队列?

主要有三点:

  • 由于线程如果无限制的建立,可能会致使内存占用过多而产生OOM,而且会形成cpu过分切换。

  • 建立线程池的消耗较高。

面试题8:线程池为何要使用阻塞队列而不使用非阻塞队列?

阻塞队列能够保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。

当队列中有任务时才唤醒对应线程从队列中取出消息进行执行。

使得在线程不至于一直占用cpu资源。

(线程执行完任务后经过循环再次从任务队列中取出任务进行执行,代码片断以下

 while (task != null || (task = getTask()) != null) {})。

不用阻塞队列也是能够的,不过实现起来比较麻烦而已,有好用的为啥不用呢?

面试题9:了解线程池状态吗?

经过获取线程池状态,能够判断线程池是不是运行状态、能否添加新的任务以及优雅地关闭线程池等。

img

  • RUNNING:线程池的初始化状态,能够添加待执行的任务。

  • SHUTDOWN:线程池处于待关闭状态,不接收新任务仅处理已经接收的任务。

  • STOP:线程池当即关闭,不接收新的任务,放弃缓存队列中的任务而且中断正在处理的任务。

  • TIDYING:线程池自主整理状态,调用 terminated() 方法进行线程池整理。

  • TERMINATED:线程池终止状态。

面试题10:知道线程池中线程复用原理吗?

线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了以前经过 Thread 建立线程时的一个线程必须对应一个任务的限制。

在线程池中,同一个线程能够从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对 Thread 进行了封装,并非每次执行任务都会调用 Thread.start() 来建立新线程,而是让每一个线程去执行一个“循环任务”,在这个“循环任务”中不停的检查是否有任务须要被执行,若是有则直接执行,也就是调用任务中的 run 方法,将 run 方法当成一个普通的方法执行,经过这种方式将只使用固定的线程就将全部任务的 run 方法串联起来。

总结

本文经过没有使用线程池带来的弊端,Executors介绍,Executors的六种方法介绍、如何使用线程池,了解线程池原理,核心参数,以及10到线程池面试题。

「成功不是未来才有的,而是从决定去作的那一刻起,持续累积而成。」

相关文章
相关标签/搜索