线程池的细致剖析及简单的面试问题


线程池是啥?面试问题解析

  • 1.首先什么是线程池?
  • 2.问:为什么很多 Java 规范都建议不要显式的创建 Thread,而使用线程池?
  • 3.应用场景
  • 线程池的两种创建方式
  • 3.Executors类下 常见的四种线程池 及区别
    • 1)fixThreadPool 正规线程
    • 2)caCheThreadPool 缓存线程池
    • 3)singleThreadPoll 单线程线程池
    • 4)ScheduledThreadPoll 调度线程池
    • 上个例子
  • 5.问:为什么不建议在代码中直接使用Executors创建线程池,而是推荐通过 ThreadPoolExecutor 方式创建?
  • Java中的ThreadPoolExecutor类
    • 上个例子


1.首先什么是线程池?

线程池:在我的理解来看,就是一个保存了一定数量线程的空间,系统提前申请好空间和资源,在线程池销毁之前一直等待着被调用。
  在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在java的并发开发中,如果说并发的线程数量很多,并且每一个线程都是执行很短的任务就结束的话,这样频繁的创建和销毁线程就会大大的降低系统的效率。
  如此需求,就有了以下的解决办法——线程池:执行完一个任务并不销毁,而是进入休眠状态来等待事件的发生。

2.问:为什么很多 Java 规范都建议不要显式的创建 Thread,而使用线程池?

答:因为使用线程池的好处是减少在创建和销毁线程上所消耗的时间和系统资源,解决资源不足的问题,如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者过渡切换问题。至于显示创建Thread的两种方式,参见博客。

3.应用场景

  • 需要大量的线程来完成任务,且完成任务的时间较短。例如Web服务器完成网页请求,因为单个任务小且任务量非常的巨大,非常适合线程池技术。可以想一下火爆网站的瞬间访问量。诚然,服务器的处理速度是非常重要的,但是线程池的技术也是重中之重。但是时间较长的任务请求线程池的优点就不明显了。因为这种操作花费的时间和线程的创建销毁时间相比多了很多。

  • 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。

  • 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,并出现"OutOfMemory"的错误。

线程池的两种创建方式

  • 可以直接 使用ThreadPoolExecutor类 直接创建一个线程池,这种线程池需要设置具体的参数。
  • 也可以 使用Executors类创建,Executors类是java.util.concurrent提供的一个创建线程池的工厂类,使用该类可以方便的创建线程池,此类提供的几种方法,支持创建四种类型的线程池,分别是:newCachedThreadPool、newFixedThreadPool、newScheduledThreadPool、newSingleThreadExecutor。

3.Executors类下 常见的四种线程池 及区别

1)fixThreadPool 正规线程

这是一个有指定的线程数的线程池,有核心的线程,里面有固定的线程数量,响应的速度快。正规的并发线程,多用于服务器。固定的线程数由系统资源设置。

//指定线程数
 public static ExecutorService newFixedThreadPool(int threads)
    {
    return newFixedThreadPool(threads,threads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
    }

核心线程是没有超时机制的,队列大小没有限制,除非线程池关闭了核心线程才会被回收。

2)caCheThreadPool 缓存线程池

只有非核心线程,最大线程数很大(Int.Max(values)),可以理解为无界,它会为每一个任务添加一个新的线程,这边有一个超时机制,当空闲的线程超过60s内没有用到的话,就会被回收。缺点就是没有考虑到系统的实际内存大小。
 
这种类型的线程池特点是:

  • 工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
  • 如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
  • 在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。
//无参数
 public static ExecutorService newCachedThreadPool()
    {
    return newFixedThreadPool(threads,Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
    }

3)singleThreadPoll 单线程线程池

只有一个核心线程,将任务通过一个缓存队列持续放到线程中去,完成一个则下一个进入。没有并发性,处理速度慢。会发生阻塞和拥塞。

4)ScheduledThreadPoll 调度线程池

这个线程池就厉害了,是唯一一个有延迟执行和周期重复执行的线程池。它的核心线程池固定,非核心线程的数量没有限制,但是闲置时会立即会被回收。

上个例子

/**
     * 创建固定大小的线程池
     */
    public static void createFixedThreadPool() {
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
        final CountDownLatch countDownLatch = new CountDownLatch(5);
        for (int i = 0; i < 5; i++) {
            final int currentIndex = i;
            fixedThreadPool.execute(() -> {
                System.out.println(Thread.currentThread().getName() + ", currentIndex is : " + currentIndex);
                countDownLatch.countDown();
            });
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("全部线程执行完毕");
    }
    
打印结果:

pool-1-thread-4, currentIndex is : 3
pool-1-thread-5, currentIndex is : 4
pool-1-thread-2, currentIndex is : 1
pool-1-thread-1, currentIndex is : 0
pool-1-thread-3, currentIndex is : 2
全部线程执行完毕



5.问:为什么不建议在代码中直接使用Executors创建线程池,而是推荐通过 ThreadPoolExecutor 方式创建?

答:其实不直接使用工具类的目的只有一个,那就是可以明确的让我们知道线程池的运行规则,避免使用工具类的包装而不够直观内部机制而导致潜在的问题。譬如使用 Executors 的 FixedThreadPool 和 SingleThreadPool 创建线程池的原理都允许请求的队列长度为 Integer 的最大值,这样的话可能会堆积大量的请求导致OOM(程序申请内存过大,虚拟机无法满足我们,然后自杀了。);所以推荐直接通过明确的构造参数创建线程池,这样就相当与时刻提醒自己的线程池特性是什么。

Java中的ThreadPoolExecutor类

java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类,因此如果要透彻地了解Java中的线程池,必须先了解这个类。下面我们来看一下ThreadPoolExecutor类的具体实现源码。

在ThreadPoolExecutor类中提供了四个构造方法:

public class ThreadPoolExecutor extends AbstractExecutorService {
    .....
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
        BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
    ...
}

从上面的代码可以得知,ThreadPoolExecutor继承了AbstractExecutorService类,并提供了四个构造器,事实上,通过观察每个构造器的源码具体实现,发现前面三个构造器都是调用的第四个构造器进行的初始化工作。

下面解释下一下构造器中各个参数的含义:

  • corePoolSize:核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
  • maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;
  • keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
  • unit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:
TimeUnit.DAYS;               //天
TimeUnit.HOURS;             //小时
TimeUnit.MINUTES;           //分钟
TimeUnit.SECONDS;           //秒
TimeUnit.MILLISECONDS;      //毫秒
TimeUnit.MICROSECONDS;      //微妙
TimeUnit.NANOSECONDS;       //纳秒
  • workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:
ArrayBlockingQueue;
LinkedBlockingQueue;
SynchronousQueue;
  • ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和Synchronous。线程池的排队策略与BlockingQueue有关。

  • threadFactory:线程工厂,主要用来创建线程;

  • handler:表示当拒绝处理任务时的策略,有以下四种取值:

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。 
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务 

上个例子

/**
     * 使用ThreadPoolExecutor创建线程池
     */
    public void createThreadPoolExecutor() {

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 10L, TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(1000),
                new ThreadPoolExecutor.AbortPolicy());

        final CountDownLatch countDownLatch = new CountDownLatch(8);
        for (int i = 0; i < 8; i++) {
            final int currentIndex = i;
            System.out.println("提交第" + i + "个线程");
            threadPoolExecutor.execute(() -> {
                System.out.println(Thread.currentThread().getName() + ", currentIndex is : " + currentIndex);
                countDownLatch.countDown();
            });
        }
        System.out.println("全部提交完毕");
        try {
            System.out.println("准备等待线程池任务执行完毕");
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("全部线程执行完毕");
    }

提交第0个线程
提交第1个线程
提交第2个线程
提交第3个线程
提交第4个线程
pool-1-thread-2, currentIndex is : 1
提交第5个线程
pool-1-thread-4, currentIndex is : 3
pool-1-thread-3, currentIndex is : 2
pool-1-thread-1, currentIndex is : 0
pool-1-thread-3, currentIndex is : 5
pool-1-thread-5, currentIndex is : 4
提交第6个线程
提交第7个线程
pool-1-thread-2, currentIndex is : 6
pool-1-thread-2, currentIndex is : 7
全部提交完毕
准备等待线程池任务执行完毕
全部线程执行完毕

还有个问题也遇见了好几次,是线程中的用户线程和守护线程的区别。参见博客

你可能感兴趣的:(java面试题)