茫茫人海千千万万,感谢这一秒你看到这里。希望我的面试题系列能对你的有所帮助!共勉!

愿你在未来的日子,保持热爱,奔赴山海!

每日三道面试题,成就更好自我

今天我们还是继续聊聊多线程中的线程池吧!

1. 你知道线程池吗?为什么需要使用线程池?

在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。

而在Java中, JVM 中每创建和销毁线程就需要资源和时间的损耗了,线程中也是存在上下文切换,这需要一定的开销,并且线程的创建并不是越多越好,而如果创建的线程数太多,上下文切换的频率就变高,可能使得多线程带来的好处抵不过线程切换带来的开销,就有点得不偿失了。

那我们需要如何管控好线程呢?

所以我们可以创建一个容器把线程数缓存在容器了,以便给他人使用,并且无需再自行创建和销毁线程。

总结:

线程池就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁。

使用线程池的好处:

  • 降低了资源的消耗,重用存在的线程,减少线程的创建和销毁的资源损耗。
  • 提高了响应速度,无需等待创建和销毁的时间,一旦任务到达的时候,即可通过线程池的线程执行。
  • 提高了线程的管控性,线程是稀缺的资源,如果无限创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

不错呀!线程池也有涉及,看来平时都有学习呀,那咱们继续

2. 你知道有多少种创建线程池的方式

JDK1.5以后提供一个Executors工具类 ,里面提供一些静态工厂方法,生成一些常用的线程池。

  • newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

    那我们来看看底层的方法和实现过程:

    底层:

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

    我们实现的步骤:

    public class ThreadPoolDemo {
    
        public static void main(String[] args) {
            threadPoolTest();
        }
    
        private static void threadPoolTest() {
            // 1. 使用工厂类获取线程池对象
            ExecutorService executorService = Executors.newCachedThreadPool();
    
            // 2. 提交任务
            for (int i = 1; i < 8; i++) {
                executorService.submit(new MyRunnable(i));
            }
        }
    }
    
    // 我们的任务类
    class MyRunnable implements Runnable {
    
        private int id;
        public MyRunnable(int id) {
            this.id = id;
        }
        @Override
        public void run() {
            // 打印是哪个线程的名称。
            System.out.println(Thread.currentThread().getName() + "执行了任务" + id);
        }
    }
    
    

    可以得到的结果:

    可以发现,线程池的开启是一直运行的状态,而如果你想结束的话,可以使用一个shutdown方法即 executorService.shutdown(); 每次任务都会创建多一个线程出来了。

    我们可以看下newCacheThreadPool的运行流程如下:

    1. 提交任务进线程池。
    2. 因为corePoolSize为0的关系,不创建核心线程,线程池最大为Integer.MAX_VALUE。
    3. 尝试将任务添加到SynchronousQueue队列。
    4. 如果SynchronousQueue入列成功,等待被当前运行的线程空闲后拉取执行。如果当前没有空闲线程,那么就创建一个非核心线程,然后从SynchronousQueue拉取任务并在当前线程执行。
    5. 如果SynchronousQueue已有任务在等待,入列操作将会阻塞。

    当需要执行很多短时间的任务时,newCacheThreadPool的线程复用率比较高, 会显著的提高性能。而且线程60s后会回收,意味着即使没有任务进来,newCacheThreadPool并不会占用很多资源。

  • newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

    还是一样看下底层和代码实现过程吧:

    底层:

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

    代码实现过程:

    public static void main(String[] args) {
        // threadPoolTest();
        threadPoolTest2();
    }
    
    private static void threadPoolTest2() {
        // 1. 使用工厂类获取线程池对象
        ExecutorService executorService = Executors.newFixedThreadPool(3);
    
        // 2. 提交任务
        for (int i = 1; i < 8; i++) {
            executorService.submit(new MyRunnable(i));
        }
    }
    

    得到的结果:

    pool-1-thread-2执行了任务2
    pool-1-thread-1执行了任务1
    pool-1-thread-3执行了任务3
    pool-1-thread-3执行了任务6
    pool-1-thread-1执行了任务5
    pool-1-thread-2执行了任务4
    pool-1-thread-3执行了任务7
    

    我们可以发现,创建线程数量就是我们指定3,核心线程数量和总线程数量相等,都是传入的参数nThreads,所以只能创建核心线程,不能创建非核心线程。因为LinkedBlockingQueue的默认大小是Integer.MAX_VALUE,故如果核心线程空闲,则交给核心线程处理;如果核心线程不空闲,则入列等待,直到核心线程空闲。

    与newCacheThreadPool的区别:

    • 因为 corePoolSize == maximumPoolSize ,所以FixedThreadPool只会创建核心线程。 而CachedThreadPool因为corePoolSize=0,所以只会创建非核心线程。
    • 在 getTask() 方法,如果队列里没有任务可取,线程会一直阻塞在 LinkedBlockingQueue.take() ,线程不会被回收。 CachedThreadPool会在60s后收回。
    • 由于线程不会被回收,会一直卡在阻塞,所以没有任务的情况下, FixedThreadPool占用资源更多
    • 都几乎不会触发拒绝策略,但是原理不同。FixedThreadPool是因为阻塞队列可以很大(最大为Integer最大值),故几乎不会触发拒绝策略;CachedThreadPool是因为线程池很大(最大为Integer最大值),几乎不会导致线程数量大于最大线程数,故几乎不会触发拒绝策略。

  • newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行

    还是一样看下底层和代码实现过程吧:

    底层:

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

    代码实现过程:

    public static void main(String[] args) {
        // threadPoolTest();
        // threadPoolTest2();
        threadPoolTest3();
    }
    
    private static void threadPoolTest3() {
        // 1. 使用工厂类获取线程池对象
        ExecutorService executorService = Executors.newSingleThreadExecutor();
    
        // 2. 提交任务
        for (int i = 1; i < 8; i++) {
            executorService.submit(new MyRunnable(i));
        }
    }
    

    得到的结果:

    pool-1-thread-1执行了任务1
    pool-1-thread-1执行了任务2
    pool-1-thread-1执行了任务3
    pool-1-thread-1执行了任务4
    pool-1-thread-1执行了任务5
    pool-1-thread-1执行了任务6
    pool-1-thread-1执行了任务7
    

    可以发现,只创建了一个线程,有且仅有一个核心线程( corePoolSize == maximumPoolSize=1),使用了LinkedBlockingQueue(容量很大),所以,不会创建非核心线程。所有任务按照先来先执行的顺序执行。如果这个唯一的线程不空闲,那么新来的任务会存储在任务队列里等待执行。

  • newScheduledThreadpool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

    还是一样看下底层和代码实现过程吧:

    底层:

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
    
    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }
    

    代码实现:

    public static void main(String[] args) {
        //        threadPoolTest();
        //        threadPoolTest2();
        //        threadPoolTest3();
        threadPoolTest4();
    }
    
    private static void threadPoolTest4() {
        // 1. 使用工厂类获取线程池对象
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
    
        // 2. 每个任务延迟两秒执行
        for (int i = 1; i < 8; i++) {
            scheduledExecutorService.schedule(new MyRunnable(i), 2, TimeUnit.SECONDS);
        }
    
        System.out.println("看是不是我先执行了!");
    }
    

    可以看到的结果:

    看是不是我先执行了!
    pool-1-thread-1执行了任务1
    pool-1-thread-1执行了任务4
    pool-1-thread-2执行了任务2
    pool-1-thread-3执行了任务3
    pool-1-thread-2执行了任务6
    pool-1-thread-1执行了任务5
    pool-1-thread-3执行了任务7
    

    我们可以发现,线程池只创建我们指定的线程数,并且返回的是一个继承了ExecutorService的ScheduledExecutorService的接口。它给我们提供一些延迟的方法:

    public ScheduledFuture<?> schedule(Runnable command,
                                           long delay, TimeUnit unit);
    延迟时间单位是unit,时间数是delay,任务是Runnable类型的command。
        
    public <V> ScheduledFuture<V> schedule(Callable<V> callable,
                                            long delay, TimeUnit unit);
    而这个方法是上面方法的重载,不一样的是任务是Callable类型的
    

秒啊,讲的这么详细。那按照惯例问你最后一道:

3. 线程池的五种状态你有了解吗?

线程池它有以下五种状态:

具体有:

  • RUNNING:这是最正常的状态,能正常接受新的任务,正常处理等待队列中的任务。

  • SHUTDOWN:不接受新的任务提交,但是会继续处理正在执行的业务并且也会处理阻塞队列中的任务。

  • STOP:不接受新的任务提交,不再处理等待队列中的任务,并且中断正在执行任务的线程。

  • TIDYING:所有的任务都执行完毕或销毁了,当前活动线程数为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()进入终止状态。

  • TERMINATED:线程池彻底终止,即terminated()方法结束后,线程池的状态就会变成这个。

小伙子不错嘛!今天就到这里,期待你明天的到来,希望能让我继续保持惊喜!

参考资料:线程池原理

注: 如果文章有任何错误和建议,请各位大佬尽情留言!如果这篇文章对你也有所帮助,希望可爱亲切的您给个三连关注下,非常感谢啦!