java 线程池那点事儿

1.为什么要用线程池

线程池提供了一种任务的提交与任务的执行解偶的策略,并且有以下几种优势

提高资源利用率
通过复用自己的线程来降低创建线程和销毁线程的开销。
如果不用线程池而采用为每个任务都创建一个新线程的话会很浪费系统资源。因为创建线程和销毁线程都是耗系统资源的行为。除此之外还会由于线程过多而导致JVM出现OutOfMemory

提高响应速度
当新来一个任务时,如果有空闲线程存在可立即执行任务,中间节省了创建线程的过程

统一管理线程
如果不用线程池来管理,而是无限创建线程的话不仅消耗系统资源,而且还会导致系统不稳定。使用线程池可以进行统一分配,调优以及监控。

2.常见线程池以及参数

2.1 创建线程池

通过Executors的工厂方法来创建线程池,比如创建一个固定线程数的线程池

ExecutorService executorService = Executors.newFixedThreadPool(5);

通过ThreadPoolExecutor的构造函数来创建线程池

ThreadPoolExecutor pool = new ThreadPoolExecutor(int corePoolSize,
                                                 int maxmumPoolSize,
                                                 long keepAliveTime,
                                                 TimeUnit unit,
                                                 BolockingQueue<Runnable> workQueue,
                                                 ThreadFactory factory,
                                                 RejectedExecusionHandler handler);

官方比较推荐使用后者,因为可以灵活选择参数配置,以及自定义配置

无论是采用那种方式创建一个线程池,它内部都是通过 ThreadPoolExecutor的构造函数来实现的。所以要想全面掌握线程池的各种特性以及性能的话需要对 ThreadPoolExecutor类深入理解,包括各种参数的意义,参数之间是如何配合工作的等等。

2.2 线程池参数

1) corePoolSize 核心线程数。默认情况下,当提交一个任务时线程池会新建一个线程池(及时有空闲线程存在),直到线程数量等于基本大小(也可以预初始化线程)
2) workQueue 一个存放任务的阻塞队列,当线程池里的基本线程都在忙着的时候,提交一个新任务的话会暂时存放到阻塞队列,等待执行,常用的阻塞队列有一下几种

  • ArrayBlockingQueue 基于数组实现的FIFO阻塞队列(有限长度)
  • LinkedBlockingQueue 基于链表实现的FIFO阻塞队列(无限长度)
  • SynchronousQueue 本身不存储任务,当有任务来的时候会一直阻塞,直到线程去执行
  • PriorityBlockingQueue 具有优先级的优先队列

3) maximumPoolSize 最大线程数。当所有的核心线程都在运行并且阻塞队列也满了的话会创建额外的几个线程来执行,这时候线程池里的所有线程数量就是最大线程数量。如果线程池采用的是像LinkedBlockingQueue这种无界队列的话,该参数不会起到作用
4) KeepAliveTimeTimeUnit 如果某个线程的空闲时间大于KeepAliveTime的时候会被标记为可回收, 并且当前线程池里的数量大于核心线程数量的时候会被终止。所以该参数跟maximumPoolSize一样,使用无界队列的时候不起作用,TimeUnit是时间单位

5) RejectedExecusionHandler 饱和策略。如果我们的任务队列满了,并且线程池里的线程数量已经达到了最大线程数,而且这些线程都不再空闲状态。这时候新提交任务的话,无法去执行,所以需要一种饱和策略来去处理这些任务。Java提供了一下几种策略

* AbortPolicy (默认策略)   直接抛异常,调用者需要根据自己的需求去捕获处理异常
* DiscardPolicy   直接丢弃,不处理直接丢弃该任务
* DiscardOldestPolicy 丢弃队列里最近的一个任务,并执行当前任务。如果是优先队列的话会丢弃优先级最高的任务,所以不推荐与优先队列一起组合使用
* CallerRunsPolicy 由调用者当前线程来执行该任务。

##### 2.3 常见线程池

Java类库提供了灵活的创建线程池方法,可以通过调用Executors中的静态工厂方法来创建一个线程池。

1) newFixedThreadPool 将创建一个固定线程数量的线程池,每当提交一个任务时就创建一个线程。直到达到线程池的最大数量。corePoolSize和maximumPoolSize值相同,并且采用了LinkedBlockingQueue,所以最大线程池数,饱和策略,存活时间等等参数都将不被用到。

   public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

2) newSingleThreadExecutor 一个但线程的Executor,它跟上面的newFixedThreadPool类似,区别在于只有一个工作线程。通过该线程池可以确保有序的执行队列中的任务,因为只有一个工作线程,所以出队的顺序就是执行的顺序。

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

3) newCachedThreadPool 此线程池的线程数量没有显式的限制默认为Integer.MAX_VALUE,可以为每个任务创建一个线程来处理,但是它没这么做,它是具有缓存线程的功能,是一种可伸缩的。比如,当处理新任务是首先看有没有空闲可用的线程来处理,如果没有的话才会新创建。并且当创建的线程空闲一段时间之后会回收(默认一分钟)

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

4) newScheduledThreadPool
创建一个固定线程数的线程池,并且以延迟或定时的方式来执行。

   public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }

3.执行流程

线程池执行任务是由Executor接口的execute()方法来执行的,下面看下执行任务流程的一段核心代码( java8)
    public void execute(Runnable command) {

        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

线程池的执行流程由以下几个步骤来完成(每种线程池的实现略有不同,但核心思想相似)

1)首先,如果当前工作的线程数量小于核心线程数,则新创建一个线程来执行。如果核心线程池数满了,

2)判断线程池的任务队列是否已满,如果没满的话把该任务放到任务队列里,等待核心线程空闲之后去执行,如果满了的话进入下一阶段

3)判断判断线程池里工作的线程数量是否达到了最大线程数,如果没达到则创建一个新的线程来去执行。否则,采用饱和策略来处理

如果进去看内部实现的话会发现,线程池会把每个工作线程包装成Worker,而把需要执行的任务包装成Task来运行。

4.健康检查

在项目中使用线程池的时候有必要对线程池进行监控,这样可以根据线程池的使用状况快速定位问题。可以通过线程池提供的参数进行监控,在监控线程池的时候可以使用以下属性。

  • taskCount 线程池需要执行的任务数量
  • completedTaskCount 已经成功运行的任务数
  • largestPoolSize 曾经出现的最多线程数
  • getPoolSize 获取线程数
  • getActiveCount 活动线程数

示例代码

public class Task  implements Runnable{
    private int i;

    public Task(int i) {
        this.i = i;
    }

    @Override
    public void run() {
        System.out.println("task num ="+i);
    }
}
public class Test {
    public static void main(String[] args) {
        ThreadPoolExecutor pool = new ThreadPoolExecutor(10,20,60,TimeUnit.SECONDS,new ArrayBlockingQueue<>(5));
        for (int i=0;i<5;i++){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("完成任务数= "+pool.getCompletedTaskCount()+"  任务数= "+pool.getTaskCount()+" " +
                    ""+"  活动线程数="+pool.getActiveCount() +"" +"  线程数="+pool.getPoolSize() +"  "+"  最大线程数="+pool.getLargestPoolSize() );

            Task task = new Task(i);
            pool.execute(task);
        }
        pool.shutdown();

    }
}

打印结果如下:

完成任务数= 0  任务数= 0   活动线程数=0  线程数=0    最大线程数=0
task num =0
完成任务数= 1  任务数= 1   活动线程数=0  线程数=1    最大线程数=1
task num =1
完成任务数= 2  任务数= 2   活动线程数=0  线程数=2    最大线程数=2
task num =2
完成任务数= 3  任务数= 3   活动线程数=0  线程数=3    最大线程数=3
task num =3
完成任务数= 4  任务数= 4   活动线程数=0  线程数=4    最大线程数=4
task num =4

猜你喜欢

转载自www.cnblogs.com/wuliaojava/p/11733119.html