【学习笔记】多线程编程-线程池/任务/线程

时间:2022-02-22 17:32:44

写在前面的话:之前一直没了解过多线程编程相关的东西,对于线程之类相关的概念也是一知半解。这次借着准备面试的时机,计划将一些关键性的盲区(除多线程以外,还有垃圾回收、图片缓存、性能优化等等)都彻底扫一遍。接下来我会陆续整理成学习笔记,主要作为自己学习后的温习巩固之用。如果能帮助到同样迷茫的小白的话,深感荣幸;如果因为缺少实战导致理解有误或解释不清的地方,还请路过的大牛不吝赐教,共同进步^_^

知识点相关

作为这次学习的切入点,线程池关联到的概念有很多,比如线程到底是什么,常搭配使用的Runnable又是啥,为什么又要搭配起来使用,等等。

关于这些,我找到了一篇博文:多线程(Thread、线程创建、线程池),里面的解释说明非常全面了,感兴趣的可以整篇通读,一定受益匪浅。我在这里仅把几个重要且基础的点列出来,按自己的理解简要说明一下:

  • 线程Thread类:用于创建线程(start方法)和定义线程所执行的任务(run方法),这里的run方法与主线程中的main方法等效。
  • 任务Runnable接口:一般有两种方法定义任务,一种是继承Thread类重写run方法,另一种则是实现Runnable接口重写run方法。后者用的比较多,是常用搭配,目的就在于将线程创建和线程任务这两块解耦
  • 任务Callable接口:作用与Runnable类似,call方法也与run方法地位相同,区别在于Callable返回结果而Runnable不返回
  • 任务结果Future接口:对应的通过Future接口的get方法来获取Callable返回的结果,注意:使用get方法时如还未得到结果则会阻塞当前线程直至拿到结果
  • 任务FutureTask类:可以理解为是Runnable/Callable和Future的结合体,通过在Callable(Runnable也会先转为Callable)外面加包一层,使用get方法直接获取任务结果

除了以上这些涉及到的类,还有一些多线程相关的概念列举如下:

  • 进程:简单来说,一个应用对应一个进程,占用一定内存
  • 线程:一个进程包含多个线程,线程相当于是任务执行单元,互不干扰
  • 抢占式调度:按优先级高低分配向线程分配内存资源;多线程并不提高程序运行速度,而是在于提高运行效率,提高CPU的使用率
  • 主线程:运行main方法所在的线程即为主线程;通常将耗时操作放到主线程以外的线程,以防阻塞主线程

准备工作完毕,下面重点讲线程池↓

为什么要使用线程池

简单来说,线程池就像一个管家,通过调度和合理分配工作,让每个佣人(即线程)在繁忙时都能各司其职高效完成任务,同时又能在空闲时得到充分休息。更具体来说,主要有两点:一是合理利用现存的线程,减少线程创建和销毁所消耗的资源开销,加快响应;二是便于监督和管理每个线程的生命周期。

线程池ThreadPoolExecutor类

要搞清楚这个类,先上一张图。

【学习笔记】多线程编程-线程池/任务/线程

这是一张继承关系图。最初的Executor接口定义的是execute方法,比较简单。它的子类ExecutorService接口,你可以理解为是通用线程池类,里面增加了线程池常用的一些用于控制线程的方法,如submit和shutdown等,用于管理生命周期。继承的AbstractExecutorService抽象类,是在ExecutorService基础上的默认实现。最后,ThreadPoolExecutor继承自该抽象类,通过构造方法来配置线程池。

这里还要提一下Executors类,不在图中,是线程池工厂类,可以通过一系列静态方法创建线程池。查看源码可以发现,方法内部都是使用ThreadPoolExecutor的构造方法加上不同的配置参数来实现。

下面是ThreadPoolExecutor类的几个构造方法:

【学习笔记】多线程编程-线程池/任务/线程

重点讲第三个,其他的构造方法基本类似。其中的参数有(按顺序依次):核心线程数N,最大线程数M,超时时间T,时间单位U,缓冲队列B和拒绝处理Handler。这些参数各自代表的含义,将结合不同的缓冲队列类型分情况讲解:

  • SynchronousQueue类型:所有任务不进入缓冲队列(可以理解为缓冲队列的容量为0),直接进入可用线程执行,当无可用线程时则直接创建新线程执行任务。当线程数超过M时,调用Handler处理被拒绝的新任务。
  • LinkedBlockingQueue类型:有新任务时,若当前线程数少于N,则创建新线程;若大于等于N,则进入缓冲队列;当缓冲队列充满时,且线程数少于M,则继续创建新线程,直至达到M时调用Handler处理拒绝任务。一般情况下,LinkedBlockingQueue作为*队列不指定缓冲容量,可以无限扩展,此时相当于M失效;但也可以通过构造函数来指定容量。
  • ArrayBlockingQueue类型:规则与LinkedBlockingQueue类似,但一般情况下指定队列长度。

具体这三种队列类型的区别可以参考这篇文章:ThreadPoolExecutor的三种队列

还有超时时间T和时间单位U,主要作用在于令非核心线程闲置超过一定时长后自动销毁,从而释放资源。

拒绝任务Handler

当线程数和缓冲队列都达到饱和时,会调用相应的Handler处理拒绝的任务。以下是四种预定义的处理策略: 

  • 在默认的AbortPolicy 中,拒绝后直接抛出异常RejectedExecutionException。
  • 在CallerRunsPolicy 中,直接在 execute 方法的调用线程中运行被拒绝的任务。该策略提供简单的反馈控制机制,能够减缓新任务的提交速度。
  • 在DiscardPolicy 中,拒绝后直接放弃任务。
  • 在DiscardOldestPolicy 中,位于工作队列头部(即最旧)的任务将被放弃,然后重试执行程序(如果再次失败,则重复此过程)。

线程池的简单示例

以斐波那契数列为例,主要展示了不同任务类型的用法区别,注意此处使用submit而不是execute来提交任务,两者区别类似Callable和Runnable。

 1 package com.effective.java.concurrent.task;  
 2   
 3 import java.util.concurrent.Callable;  
 4 import java.util.concurrent.ExecutionException;  
 5 import java.util.concurrent.ExecutorService;  
 6 import java.util.concurrent.Executors;  
 7 import java.util.concurrent.Future;  
 8 import java.util.concurrent.FutureTask;  
 9    
10 public class RunnableFutureTask {  
11   
12     static ExecutorService mExecutor =  new ThreadPoolExecutor(2, 3, 5, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());  
13   
14   
15     public static void main(String[] args) {  
16         futureDemo();  
17     }  
18   
19 
20     static void futureDemo() {  
21         try {  
22             /** 
23              * 提交runnable则没有返回值, future没有数据 
24              */  
25             Future<?> result = mExecutor.submit(new Runnable() {  
26   
27                 @Override  
28                 public void run() {  
29                     fibc(20);  
30                 }  
31             });  
32   
33             System.out.println("future result from runnable : " + result.get());  
34   
35             /** 
36              * 提交Callable, 有返回值, future中能够获取返回值 
37              */  
38             Future<Integer> result2 = mExecutor.submit(new Callable<Integer>() {  
39                 @Override  
40                 public Integer call() throws Exception {  
41                     return fibc(20);  
42                 }  
43             });  
44   
45             System.out  
46                     .println("future result from callable : " + result2.get());  
47   
48             /** 
49              * FutureTask可以通过Thread包装来直接执行,也可以提交给ExecuteService来执行 
50              * ,并且还可以通过v get()返回执行结果,在线程体没有执行完成的时候,主线程一直阻塞等待,执行完则直接返回结果。 
51              */  
52             FutureTask<Integer> futureTask = new FutureTask<Integer>(  
53                     new Callable<Integer>() {  
54                         @Override  
55                         public Integer call() throws Exception {  
56                             return fibc(20);  
57                         }  
58                     });  
59 
60             // 提交futureTask  
61             mExecutor.submit(futureTask) ;  
62             System.out.println("future result from futureTask : "  
63                     + futureTask.get());  
64   
65         } catch (InterruptedException e) {  
66             e.printStackTrace();  
67         } catch (ExecutionException e) {  
68             e.printStackTrace();  
69         }  
70     }  
71   
72     /** 
73      * 效率底下的斐波那契数列, 耗时的操作 
74      *  
75      * @param num 
76      * @return 
77      */  
78     static int fibc(int num) {  
79         if (num == 0) {  
80             return 0;  
81         }  
82         if (num == 1) {  
83             return 1;  
84         }  
85         return fibc(num - 1) + fibc(num - 2);  
86     }   
87 }  

 

除文中提到的博文外,其余参考博文列举如下,感谢各位博主的无私分享^_^:

java并发编程--Executor框架

一心多用多线程-线程池ThreadPoolExecutor-看这篇就够了

一心多用多线程-细谈java线程池submit与execute的区别

Java多线程-线程池ThreadPoolExecutor构造方法和规则

深入理解Callable

Java中的Runnable、Callable、Future、FutureTask的区别与示例