JAVA线程池的创建与使用


  • 为什么要用线程池?

我们都知道,每一次创建一个线程,JVM后面的工作包括:为线程建立虚拟机栈、本地方法栈、程序计数器的内存空间(下图可看出),所以线程过多容易导致内存空间溢出。同时,当频繁的创建和销毁线程容易浪费系统的计算能力在资源的回收和申请中。

另外:创建过多的线程,会导致cpu在线程中的切换时间比处理时间还多,大大降低了系统的吞吐量。因此我们使用线程池如下好处:

  1. 有效控制线程的数量,防止线程数量过多。
  2. 提高线程的利用程度,避免频繁的创建及销毁线程。
  3. 有更灵活的线程使用方式及拒绝措施。

再给大家看看阿里开发规约里面是怎么说的

  • 线程的快速示例

我知道大多数人都希望先看看线程池怎么创建,然后再深入了解。下面给大家一个demo

 1         //存放任务的阻塞队列
 2         BlockingDeque queue = new LinkedBlockingDeque<>(10);
 3         //BasicThreadFactory是自己实现ThreadFactory接口而来
 4         BasicThreadFactory factory = new BasicThreadFactory();
 5         ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(3, 10, 60,
 6                 TimeUnit.SECONDS, queue, factory,
 7                 (Runnable r, ThreadPoolExecutor executor)->{
 8                         System.out.println(executor.getQueue().size()+"消息队列已满");
 9                         System.out.println("拒绝服务");
10 
11                 });
ThreadPoolDemo
  • 线程池相关概念

  1. 核心线程:若线程池中的线程标记为核心线程,即使核心线程没有运行任务,它也不会被销毁,会一直存在于线程池中,直至线程池被shutdown。
  2. 非核心线程:当线程池中没有空闲的核心线程时,线程池会创建一个非核心线程,并且非核心线程的一定时间内处于空闲状态的时候,非核心线程会被销毁。
  3. 阻塞队列:阻塞队列是当线程池中的没有能用于处理任务的线程时,会把该任务放入阻塞队列,待有能用于处理的线程时,把任务从队列取出处理,阻塞队列的长度可以设置。
  4. 拒绝服务处理:当线程池中的没有线程能提供处理,并且阻塞队列的空间已满,此时会触发拒绝服务异常,开发人员可以根据自己的需求定制不同的处理策略。
  • 创建线程池的7个参数

一般我们推荐使用ThreadPoolExecutor()自定义创建线程池,因为这比较灵活切可控。

  1. int corePoolSize  核心线程数,即确定有多少个核心线程。
  2. int maximumPoolSize  最大线程数,即限定线程池中的最大线程数量。
  3. long keepAliveTime  非核心线程的存活时间,配合下面的TimeUnit参数确定时间。
  4. TimeUnit unit  一个时间类型的枚举类。有从纳秒到天的时间量度,配合上面的keepAliveTime确定非核心线程的存活时间。
  5. BlockingQueue workQueue   装载Runnable的阻塞队列,具体类型可以自己确定。
  6. ThreadFactory threadFactory  线程工厂,这是一个函数式接口,里面只定义了一个newThread(Runnable task)方法,需要自己实现工厂的方法,在这里我们可以对线程进行自定义的初始化,例如给线程设定名字,这样方便后期的调试。
  7. RejectedExecutionHandler handler   拒绝服务处理,这也是一个函数式接口,我们需要实现rejectedExecution(Runnable r, ThreadPoolExecutor executor)这个方法,这里可以根据需求自定义你希望在处理逻辑。当然Java里面也有已经定义好的四种策略静态类。可以通过ThreadPoolExecutor调用
  • Executors中实现的线程池类型

下面介绍的线程池类型,是Jdk帮我们制定好的策略。但是,有的线程池类型中,要么存在线程数量无限制、要么存在阻塞队列长度无限制,但是这些应该在开发中避免,因为一旦并发过高,会导致大量的对象积压,导致JVM内存溢出。

写在前面:jdk提供了默认的工厂方法和默认的默认的拒绝处理策略。

默认拒绝策略是:不执行并抛出异常

默认的工厂方法是:对线程进行安全检查并命名。

 1     static class DefaultThreadFactory implements ThreadFactory {
 2         private static final AtomicInteger poolNumber = new AtomicInteger(1);
 3         private final ThreadGroup group;
 4         private final AtomicInteger threadNumber = new AtomicInteger(1);
 5         private final String namePrefix;
 6 
 7         DefaultThreadFactory() {
 8             SecurityManager s = System.getSecurityManager();
 9             group = (s != null) ? s.getThreadGroup() :
10                                   Thread.currentThread().getThreadGroup();
11             namePrefix = "pool-" +
12                           poolNumber.getAndIncrement() +
13                          "-thread-";
14         }
15 
16         public Thread newThread(Runnable r) {
17             Thread t = new Thread(group, r,
18                                   namePrefix + threadNumber.getAndIncrement(),
19                                   0);
20             if (t.isDaemon())
21                 t.setDaemon(false);
22             if (t.getPriority() != Thread.NORM_PRIORITY)
23                 t.setPriority(Thread.NORM_PRIORITY);
24             return t;
25         }
26     }
defaultFactory
  1. FixedThreadPool       固定核心线程的线程池。

特点:它的核心线程数量就是最大线程数,所以线程池内的线程永远不会消亡,它采用了无参数的链表阻塞队列,最大的任务数可达232-1个。因此存在任务积压导致内存溢出的风险

   2.  CachedThreadPool   缓存线程池

特点:没有核心线程,线程池不能满足任务运行时会创建新的线程,线程数量没有上限。默认的消亡时间为60秒。值得注意的是:它的阻塞队列是SynchronousQueue,这是一个没有存储性质的阻塞队列,它的取值操作和放入操作必须是互斥的。根据源码文档的解释,可以理解为每当有任务放入时会立即有线程将它取出执行。

  3.  ScheduledThreadPool  固定调度线程池

特点:有固定的核心线程,线程的数量没有限制,默认存活时间为60秒。同时支持定时及周期性任务执行

  4. SingleThreadExecutor  单核心线程池

特点:只有一个核心线程,所以能保证任务的串行化执行。

  5. WorkStealingPool  并行执行线程池

特点:在jdk8中实现 线程池。它内部的线程池实现是ForkJoinPool,这是一个可以同时利用多个线程来执行任务的线程池。无参默认使用CPU数量的线程数执行任务,由于这个线程池比较复杂,下次专门写一篇博文用于更新。

  • 线程池的调用流程

需要注意的是:线程池设计的流程是先利用核心线程处理、核心线程不能处理即把它放入阻塞队列,最好才创建线程来执行任务,直到新建线程也失败才调用拒绝服务处理。

试着理解一下这样设计的好处。可以看到,创建线程永远不是最先想到的办法,线程池尽量避免创建线程。因为创建线程需要调用全局锁来确定线程的正确创建,同时也因为线程创建和销毁也需要消耗资源,所以这种方式在最大努力的避免这种情况的发生。

  • 线程池的关闭

虽然在实际的开发中,线程池一般是随着项目的部署一起存活的,不会经常关闭,但是还是需要了解如何关闭,怎么关闭比较安全。

线程池可通过调用线程池的shutdownshutdownNow方法来关闭线程池.
它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止.
但是它们存在一定的区别

  • shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表
  • shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程.

只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true.
当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true.
至于应该调用哪一种方法,应该由提交到线程池的任务的特性决定,通常调用shutdown方法来关闭线程池,若任务不一定要执行完,则可以调用shutdownNow方法.

线程关闭的方法转载于作者:全网搜索关注JavaEdge
链接:https://www.nowcoder.com/discuss/152050?type=0&order=0&pos=6&page=0