Executors与线程池:如何创建正确的线程池

在很多公司的面试中,线程池的题目往往都是比较多且较难的,并且线程相关的对象和其他业务API是不相同的,区别在于一个直接操作操作系统使用的是操作系统相关的API,一个单纯只占用内存。

从Java核心技术专栏线程中的相关知识我们可以知道,线程产生与销毁都会消耗一定的性能,因此要避免频繁的创建与销毁。

因此解决这个问题的关键就是创建线程池。

概述

线程池的需求很普遍,从一般使用的池化角度来看:当需要资源的时候就用acquire()方法去申请资源,用完之后就调用release()释放资源。但是在线程池中是完全不一样的,Java没有提供申请线程和释放线程的方法。

线程池模型:生产-消费

为什么线程池的模型和普通的池化资源不同呢,如果采用了一般模型的线程池设计,如下:

class ThreadPool{
    //获取空闲线程
    Thread acquire() {
    }
    //释放空闲线程
    void release(Thread t) {
    }
}
    ThreadPool pool;
    Thread t1 = pool.acquire();
    //传入Runnable对象
    t1.execute(()->{
        //业务代码
    });

过程:假设获取到一个空闲线程T1,然后使用t1完成业务。

  1. 创建线程t1
  2. 调用t1.execute()
  3. 传入Runnable执行基本逻辑,就像是通过构造函数Thread(Runnable target)创建线程

但是并没有execute(Runnable target)这个方法。所以线程池并不是这么设计的。

线程池最终采用的设计方式为生产-消费

线程池的使用方式生产者,线程池本身是消费者
如下示例代码:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

//简化的线程池,仅仅用来说明工作原理
public class MyThreadPool {
    //利用阻塞队列来实现生产者-消费者模型
    BlockingQueue<Runnable> workQueue;
    //保存内部工作线程
    List<WorkerThread> threads = new ArrayList<>();
    //构造方法
    MyThreadPool(int poolSize, BlockingQueue<Runnable> workQueue) {
        this.workQueue = workQueue;
        //创建工作线程
        for (int idx = 0; idx < poolSize; idx++) {
            WorkerThread workerThread = new WorkerThread();
            workerThread.start();
            threads.add(workerThread);
        }
    }
    //工作线程负责消费任务,并且执行任务
    class WorkerThread extends Thread {
        @Override
        public void run() {
            //循环取任务并且执行
            while (true) {
                Runnable task = null;
                try {
                    task = workQueue.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                task.run();
            }
        }
    }

    //提交任务
    void execute(Runnable command) throws InterruptedException {
        workQueue.put(command);
    }
}

/*使用实例*/
//创建有界阻塞队列
class User {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(2);
        //创建线程池
        MyThreadPool pool = new MyThreadPool(10, workQueue);
        //提交任务
        pool.execute(() ->{
            System.out.println("hello");
        });

    }
}
//output : hello

接下来看一下程序,在MyThreadPool内部,我们维护了一个阻塞队列workQueue和一组工作线程,工作线程的个数由构造函数的poolSize决定。用户通过execute()方法提交Runnable任务,execute()方法内部仅仅是添加任务到任务队列。而MyThreadPool会消费任务队列执行任务,相关的代码就是while循环。

多线程编程无法throws异常
多线程编程之所以无法throws抛出异常,用最简单的话来说,多线程中一个线程中止,不会影响其他线程继续运行,所以Exception就会逃逸,在Java线程被设计出来的时候,有这样一个理念,线程自己的异常由线程自己解决,而线程的实现是在run方法内的,如果想捕获到单线程的异常,就需要在run方法内部catch,而不是在大类里面throws。

但是,也并不是完全没有方法在不catch的情况下获得到线程内部的异常,java5有一个方法Thread.UncaughtExceptionHandler.uncaughtException(),可以帮助我们获取到线程内部的异常,但是很麻烦,简化之后,我们就参照正常的线程设计,去设计我们的架构就好。

如何使用Java线程池
Java线程池中,最核心的就是ThreadPoolExecutor,此工具构造函数十分复杂,如下代码所示共有七个参数:

ThreadPoolExecutor(		
int	corePoolSize,		
int	maximumPoolSize,		
long	keepAliveTime,		
TimeUnit	unit,		
BlockingQueue<Runnable>	workQueue,		
ThreadFactory	threadFactory,		
RejectedExecutionHandler	handler
)

介绍一下里面的参数:

  • corePoolSize :表示线程池保有的最小线程数。一般给不太重要的业务使用。
  • maximumPoolSize :表示线程池创建的最大线程数。给最繁忙的业务用,但是当业务清净下来,也会降低线程使用,但不会低于corePoolSize。
  • keepAliveTime & unit :用来定义业务的空闲与繁忙,当超过keepAliveTime & unit(ms)都没有响应,就会定义为空闲,降低线程的占用。
  • workQueue :工作队列,和上面示例代码的工作队列同义。必须是BlockingQueue
  • threadFactory :通过这个参数你可以自定义如何创建线程,例如你可以给线程指定一个有意义的名字
  • handler::通过这个参数你可以自定义任务的拒绝策略。如果线程池中所有的线程都在忙碌,并且工作队列也满了(前提是工作队列是有界队列),那么此时提交任务,线程池就会拒绝接收。至于拒绝的策略, 你可以通过handler这个参数来指定。以下是四种拒绝方法:
    1、CallerRunsPolicy:提交任务的线程自己去执行该任务。
    2、AbortPolicy:默认的拒绝策略,会throws RejectedExecutionException。
    3、DiscardPolicy:直接丢弃任务,没有任何异常抛出。
    4、DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入 到工作队列。

使用线程池需要注意的:
因为上面提到的这些参数确实使用起来较复杂,于是并发包提供了一个静态工厂类Executors快速创建线程池,,但是不建议这么做。

原因是:Executors很多方法都是无界的LinkedBlockingQueue,无边界队列想当然会造成oom(Out of Memory),而oom会耽误所有事情,所以建议使用有节队列。

当采用了有界队列,当并发量超载的时候,就会触发RejectedExecutionException拒绝策略,此异常不会被强制catch,容易中断项目,因此要慎用包括锁的降级同理,因此最好自定义降级策略和拒绝策略。

当使用线程池进行异常处理的时候,建议根据不同的异常,写出不同的策略。

你可能感兴趣的:(Java,Java并发编程实战)