线程池

423 阅读9分钟

1. 为什么要用线程池(以往的方式创建线程会有什么问题)

一个最最典型的场景,在Android的RecycerView(就是类似微信消息页面那样的列表)中,我们在每个RecycerView启动一个子线程来加载图片,传统的线程开启方式,包括Runnable、Thread等等,很简单的我们就可以创建一个线程。

但是这些线程的声明散落在各个RecycerView的Adapter项目中,我们难以集中对其进行管理,如果图片本身较大,可能会导致一些其他的引用资源无法被及时地释放。如果我们手指快速地滑动RecycerView,不断地有线程被创建,最终结果就是造成OOM(内存溢出)。

面对这个问题,我们参考操作系统中的做法,操作系统实质上就是一个软件的中间层,该层向上层的用户APP提供系统调用,向下层的硬件进行统一管理。线程同样是一种资源,我们也要想一个办法对其进行集中的管理,于是乎,一个新的结构:线程池,就被提出了。

2. 几个问题

以往的线程创建方式主要由两种:

  • 继承Thread
  • 实现Runnable接口

如果我们的线程池仍然基于Thread类来实现,那么我们在构建具体任务的时候,同样需要在外部构建多个的Thread,难以实现统一的管理。所以,我们将要做的事情缩小到最小的粒度:Runnable

我们原先是将Runnable交给一个Thread,但是在线程池模式下,我们需要将Runnable显式地交给谁?这个问题的答案,显然不再是Thread了,应该是线程池

实际上在操作系统中,我们申请资源、归还资源也都是和操作系统进行操作,应用程序通过系统调用来执行底层的操作,我们的App释放内存直接跟物理内存打交道,这显然是不可能的,缺乏管理势必会造成整个系统的不稳定。

同样地,我们在计算机中的很多地方,都使用着这种池化的思想,例如在数据库的连接池、OkHttp的连接池等等。

线程池具有如下的优点:

  1. 减少了创建线程的时间。
  2. 降低资源消耗(重复利用线程池中的线程,不需要每次都去新建)。
  3. 便于线程管理。
    • CorePoolSize:线程池核心线程的数量
    • MaximumPoolSize:最大的线程数,满了则不创建了,交给拒绝策略。
    • KeepAliveTime:线程没有任务时最多保持多长时间后会终止(滞留时间)

3. 相关结构

3.1 ExecutorService

真正的线程池接口,常见的一个子类:ThreadPoolExecutor

3.2 Executors

这是一个线程池的执行者集合,内部有很多的静态方法,这些静态方法会帮助我们创建不同的线程池。

例如:

1. newCachedThreadPool()

用于创建一个可根据需要创建新线程的线程池。

2. newFixedThreadPool(n)

用于构建一个可重用固定线程数的线程池。

3. newSingleThreadExecutor()

用于创建一个只有一个线程的线程池。

4. newScheduledThreadPool(n)

创建一个线程池,它可安排在给定延时后运行命令或者定期地执行

注意,我们使用以上的静态方法构建的任一线程池对象都可以用ExecutorService来进行接收。因为这些静态方法的实现返回的结果都是该接口的实现类。

4 Callable和Future

(这是基础知识,相当于回顾复习了一遍,看过的可以直接到第五点了。) 我们先看看Runnable,对于传统的Runnable接口,我们无法返回值,因为他重写方法是被定死的。

@Override
public void run(){}

如果,我们的方法想要返回值似乎就难以实现了。所以,Callable正是解决了这个问题。

4.1.1 Callable

要用到Callable,我们需要实现Callable接口,重写Call方法:

class MyCallable implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {
        Thread.sleep(5000);

        return 12580;
    }

    public static void main(String[] args) throws Exception {
        MyCallable myCallable = new MyCallable();
        int x = myCallable.call();
        System.out.println(x);//这里会因为Call方法被阻塞住,不会先打印75,先打印X再打印75
        System.out.println(75);
    }

    /**
     * output
     * 12580
     * 75
     */
}

值得注意的是,单独调用Callable,会导致方法执行的阻塞问题。

4.1.2 Future

Future如果写过Flutter网络请求的同学,对Future一定不陌生,Future本意是未来的,即还未发生的事务,例如没做作业的你,我们需要在上面补完,完成后交给老师。于是我们先拿一本空白的本子交给老师,并期望老师不要先改到你的。并且在上课时,疯狂补作业,下课后,去补交作业。

于是,交给老师的白纸就是FutureData,即未来的数据,自然而然是空白的。补交的数据即RealData,这才是经过操作后的真实数据。其实这种设计模型是一种用来处理异步请求的设计模型,C端(Client,即服务端)发送给S端(Server,即服务器)时,由于种种原因,S端无法快速地响应,会先返回一个FutureData,这里面并没有我们想要的数据,但是当我们真的需要使用数据时,S端再将真正的数据发送给C端。

再举一个多子任务的例子,主任务A的完成,依赖于子线程1、子线程2、子线程3的完成,只要有一个线程失败,那么就宣告这个任务A失败。要求失败时,能够尽快得到失败的结果。

如果我们使用常用的Runnable实际上是比较难做的,因为我们要求有返回值。所以我们用Callable是个更好的选择。所以我们采用一个Callable + Future的组合来实现这个要求,虽然我们能在主线程及时收到子线程出错时返回的null或者是标记,但是我们还是难以做到出错时,立即停止其他两个线程的运行。这里就涉及到了一种设计模式:Observer。即观察者模式,该模式中,事件的发生者通过回调方法来传达事件,让事件的接受者及时接收到事件的变动。事件的发生者需要记录下所有在观察的对象名单,然后通过notifyListener来通知所有的事件接收者。而接收者只需要注册、反注册事件即可。接收者通过传来的回调函数回调方法,这样才能实现立即通知其他线程停止执行。

第三方类库会有相关的实现,例如:Guava的ListeningExecutorSerivce这也是一个线程池,搭配ListenableFuture实现。而Java本身也提供了相关的实现:CompletableFuture。这二者都通过回调方法处理出错时的结果。

FutureTask,FutureTask和Runnable有点像(其实它就是一个Runnable的实现类),如果我们不把它装入线程、线程池,它是不会执行的:

    Callable<Integer> callable = new Callable<Integer>() {
        @Override
        public Integer call() throws Exception {
            Thread.sleep(1500);
            return 12580;
        }
    };


    FutureTask<Integer> futureData = new FutureTask(callable);
    new Thread(futureData).start();//执行Future

    System.out.println("1");
    System.out.println(futureData.get());//注意,此处会被阻塞住,不会往下执行了,1.5s后打印12580和2
    System.out.println("2");

我们需要实现一个Future接口,我们需要实现五个相关的方法。

方法名作用
get该方法用于任务结束后的结果返回
get(long timeout,TImeUnit unit)做多等待时间就会返回结果,有时限的任务
cancel可以用来停止一个任务,如果停止成功返回True
isDone是否完成
isCancel判断方法是否取消

5. 线程池的使用

两个方法来使用线程池。executesubmit

其中,execute用于构建一个使用Runnable的任务的执行,而后者使用于Callable的任务的执行。

例子:

//1.构建
ExecutorService service = new Executors.newFixedThreadPool(10);//构建具有五个线程的线程池

//2.使用
service.execute(new Runnable());
service.execute(new Runnable());

FutureTask futureTask = new FutureTask(new Runnable());//使用FutureTask接收具有返回值的调用

//3.关闭
service.shutdown();//关闭连接池

其实,我们使用的这些newFixedThreadPool都是为我们预估设计好参数的,他们本身是ThreadPoolExecutor的实现类,所以,我们可以利用强制转换得到ThreadPoolExecutor,来为线程池设置相关的参数 值:

ExecutorService executorService = Executors.newFixedThreadPool(5);
executorService.setMaximumPoolSize(500);//报错啦
((ThreadPoolExecutor)executorService).setMaximumPoolSize(500);
System.out.println(((ThreadPoolExecutor) executorService).getMaximumPoolSize());//500

6. ThreadPoolExecutor

我们首先有一个ExecutorService,线程池都是实现自这个接口,我们常用的线程池有ThreadPoolExecutor,参数有:

	    public ThreadPoolExecutor(
              int corePoolSize,//线程池的大小
              int maximumPoolSize,//最大线程数
              long keepAliveTime,//非核心线程(> CorePoolSize时的空闲线程)的滞留时间
              TimeUnit unit,//时间单位
              BlockingQueue<Runnable> workQueue,//未处理线程的阻塞队列
              ThreadFactory threadFactory,//线程工厂
              RejectedExecutionHandler handler,//拒绝策略
            ) {}

6.1 CorePoolSize和maximumPoolSize

CorePoolSize该属性和maximumPoolSize很容易被误解成:Size的MAX_SIZE的意思,其实不是,完整的流程如下:

  1. 首先,当池子中线程的数目 < CorePoolSize时,就新建线程,处理请求。
  2. 当池子大小等于CorePoolSize时,就把消息放入workQueue,当池子里有线程空闲时,就去workQueue中取任务并执行。
  3. workQueue放不下新的任务时,再新建线程入池,处理请求,如果池子大小撑到了maximumPoolSize就使用RejectedExecutionHandler来做拒绝处理。
  4. 当池子的线程数远大于CorePoolSize时,多余的线程会等待KeepAliveTime长的时间,如果无请求可处理就自行销毁。

所以,实际上CorePoolSize只是基本/核心线程的数量,当不足这个数量,且当前线程不够用时,我们会创建新的线程,直到MaximumPoolSize数量,当这个数量也被撑满了,那么就开始拒绝处理了。另外,KeepAliveTime非核心线程的滞留时间,如果长时间空闲,他们会被杀死。

形象地说,核心线程就是正式工,没有活的时候只要一定数目的人就行了,多了浪费;非核心线程就是临时工,平时不需要,活的时候就需要大量的临时工来工作,工作完了,没事了,即解雇。但是临时工也不可能无限多,否则把办公室撑满了。活实在多的不行了,就不能再接了,采用拒绝策略进行处理。

注意,在Executors.newCachedThreadPool()中,默认的Core和Max的数目是:0和Integer.MAX_VALUE。

6.2 ThreadFactory

工厂模式的提出,本身就是为了统一某一个类的构建过程,将类的实现和具体的类生成脱离。我们通过一个工厂类来做这件事,就是线程工厂,比如我们可以为线程设置统一的名称:

class MyThreadFactory implements ThreadFactory{

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(
                r,"线程的名称" + String.valueOf(Math.random())
        );
        return t;
    }
 }

然后在调用处:

ExecutorService executorService = Executors.newFixedThreadPool(5,new MyThreadFactory());

传入该工厂,那么我们构建的线程就具有自定义的名称了。

##6.3 RejectedExecutionHandler 拒绝策略

从上文我们得知,线程池的线程数量达到MaximumPoolSize以后,我们需要采用一定的策略来进行拒绝新任务的加入。

有以下几种策略:

  • 默认策略:AbortPolicy 这是线程池的默认策略,如果线程池满了,则丢弃任务,并抛出异常:RejectedExecutionException
  1. DiscardPolicy 静默版本的AbortPolicy,即直接Discard(丢弃)掉,不抛出异常。

  2. DiscardOldestPolicy 丢弃掉**"最老"**的任务,即最早进入的任务会被丢弃掉,也不抛出异常,这是符合队列特性的(尾进,头出)

  3. CallerRunsPolicy 如果阻塞队列满了,那么主线程“亲力亲为”,自己干这件事,实际上直接执行力Runnable的run方法。

  4. 自定义 我们可以自定义一个实现类,实现RejectedExecutionHandler接口,并且实现rejectedExecution方法。具体的逻辑就在rejectedExecution方法里去定义就OK了。

     public class MyRejectPolicy implements RejectedExecutionHandler{
         public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {   
             System.out.println("我满了......");
         }
     }
    
参考来源

1.JAVA Future类详解

2.ThreadPoolExecutor运转机制及BlockingQueue详解