一、线程池简介

1.概念

线程池就是首先创建一些线程,它们的集合称为线程池。使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。

2.线程池的工作机制

在线程池的编程模式下,任务是提交给整个线程池,而不是直接提交给某个线程,线程池在拿到任务后,就在内部寻找是否有空闲的线程,如果有,则将任务交给某个空闲的线程。

一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。

3.使用线程池的原因

多线程运行时间,系统不断的启动和关闭新线程,成本非常高,会过渡消耗系统资源,以及过渡切换线程的危险,从而可能导致系统资源的崩溃。这时,线程池就是最好的选择了。

二、四种常用的线程池

线程池的返回值:ExecutorService
ExecutorService是Java提供的用于管理线程池的类。该类的两个作用:控制线程数量和重用线程

具体的四种常用线程池:

1.Executors.newCacheThreadPool():可缓存线程池

先查看池中有没有以前建立的线程,如果有,就直接使用。如果没有,就建一个新的线程加入池中,缓存型池子通常用于执行一些生存期很短的异步型任务

public class NewCachedThreadPool {
    private static ExecutorService executorService = Executors.newCachedThreadPool();

    private static AtomicInteger atomicInteger = new AtomicInteger();

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            Thread.sleep(1000L);
            executorService.execute(NewCachedThreadPool::print);
        }
    }

    public static void print() {
        System.out.println(Thread.currentThread().getName());
        System.out.println(atomicInteger.incrementAndGet());
    }

}

java completefuture 默认线程池 ForkJoinPool_线程池


线程池为无限大,当执行当前任务时上一个任务已经完成,会复用执行上一个任务的线程,而不用每次新建线程

2.Executors.newFixedThreadPool(int n):固定大小线程池

创建一个可重用固定个数的线程池,以共享的无界队列方式来运行这些线程。

public class NewFixedThreadPoolTest {

    public static void main(String[] args) {
        // 创建一个可重用固定个数的线程池
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 10; i++) {
            fixedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        // 打印正在执行的缓存线程信息
                        System.out.println(Thread.currentThread().getName()
                                + "正在被执行");
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}

java completefuture 默认线程池 ForkJoinPool_后端_02


因为线程池大小为3,每个任务输出打印结果后sleep 2秒,所以每两秒打印3个结果。定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()

3.Executors.newScheduledThreadPool(int n):周期性执行的线程池

创建一个定长线程池,支持定时及周期性任务执行

public class NewScheduledThreadPoolTest {
    public static void main(String[] args) {
        //创建一个定长线程池,支持定时及周期性任务执行——延迟执行
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
        //延迟1秒执行
                 /*scheduledThreadPool.schedule(new Runnable() {
                     public void run() {
                        System.out.println("延迟1秒执行");
                     }
                 }, 1, TimeUnit.SECONDS);*/


        //延迟1秒后每3秒执行一次
        scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println("延迟1秒后每3秒执行一次");
            }
        }, 1, 3, TimeUnit.SECONDS);

    }
}

java completefuture 默认线程池 ForkJoinPool_后端_03

4.Executors.newSingleThreadExecutor():单线程池

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

java completefuture 默认线程池 ForkJoinPool_线程池_04


三、多线程方法:

shutdown():不再接受新的任务,之前提交的任务等执行结束再关闭线程池。
shutdownNow():不再接受新的任务,试图停止池中的任务再关闭线程池,返回所有未处理的线程list列表。

execute:执行任务。没有返回值。
submit:执行任务但是有返回值,返回一个future。

fixedThreadPool.shutdown();
fixedThreadPool.submit(()->{

});
fixedThreadPool.execute(()->{

});

关于Future,它是一个接口,里面可以定义泛型
计算1+2+3+…+666666

public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException {
        ExecutorService executorService = Executors.newFixedThreadPool(6);
        Future<Long> submit1 = executorService.submit(() -> {
            Long sum = 0L;
            for (int j = 0; j < 333333; j++) {
                sum += j;
            }
            return sum;
        });
        Future<Long> submit2 = executorService.submit(() -> {
            Long sum = 0L;
            for (int j = 333333; j < 666667; j++) {
                sum += j;
            }
            return sum;
        });

//        while (!submit1.isDone() || !submit2.isDone()){}
        executorService.shutdown();
        System.out.println(submit1.isDone()+""+submit2.isDone());
        System.out.println(submit1.get()+submit2.get());
        System.out.println(submit1.get(1, TimeUnit.MILLISECONDS)+submit2.get(1,TimeUnit.MILLISECONDS));

    }

siDone:当前线程是否执行完

get不带参数:登待执行完成,然后获取执行结果

get带参数:登待指定的时间,时间一到没有返回则报超时异常

四、关于使用线程池

java completefuture 默认线程池 ForkJoinPool_开发语言_05


顶级接口是Executor,而Executors相当于一个工厂类,用于创建线程池。他没有继承或实现任何类和接口。

而用Executors本质上也是用上面ThreadPoolExecutor去创建线程池。阿里规约检查不推荐用Executors这个方式创建线程池,原因是不便于理解

比如:newFixedThreadPool创建指定线程数大小的线程池

java completefuture 默认线程池 ForkJoinPool_java_06

1.线程池的七个参数:

  • corePoolSize:线程池的核心大小,也可以理解为最小的线程池大小。
  • maximumPoolSize:最大线程池大小。
  • keepAliveTime:空余线程存活时间,指的是超过corePoolSize的空余线程达到多长时间才进行销毁。
  • unit:销毁时间单位。
  • workQueue:存储等待执行线程的工作队列。
  • threadFactory:创建线程的工厂,一般用默认即可。
  • handler:拒绝策略,当工作队列、线程池全已满时如何拒绝新任务,默认抛出异常。

线程池工作流程

1、如果线程池中的线程小于corePoolSize时就会创建新线程直接执行任务。

2、如果线程池中的线程大于corePoolSize时就会暂时把任务存储到工作队列workQueue中等待执行。

3、如果工作队列workQueue也满时:当线程数小于最大线程池数maximumPoolSize时就会创建新线程来处理,而线程数大于等于最大线程池数maximumPoolSize时就会执行拒绝策略。

public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 4,
                2L, TimeUnit.SECONDS,
                new SynchronousQueue<Runnable>());
        System.out.println("初始化完成后corePoolSize="+threadPoolExecutor.getPoolSize());
        threadPoolExecutor.submit(()->{
            System.out.println("**************************"+Thread.currentThread().getName()+"执行任务1");
        });

        System.out.println("执行第一个任务后corePoolSize="+threadPoolExecutor.getPoolSize());
        threadPoolExecutor.submit(()->{
            System.out.println("**************************"+Thread.currentThread().getName()+"执行任务2");
        });
        System.out.println("执行第二个任务后corePoolSize="+threadPoolExecutor.getPoolSize());
        threadPoolExecutor.submit(()->{
            System.out.println("**************************"+Thread.currentThread().getName()+"执行任务3");
        });

        System.out.println("执行第三个任务后corePoolSize="+threadPoolExecutor.getPoolSize());
        threadPoolExecutor.submit(()->{
            System.out.println("**************************"+Thread.currentThread().getName()+"执行任务4");
        });

        System.out.println("执行第四个任务后corePoolSize="+threadPoolExecutor.getPoolSize());
        Thread.sleep(4*1000);
        System.out.println("超过线程最大存活时间后corePoolSize="+threadPoolExecutor.getPoolSize());

    }

执行结果:

java completefuture 默认线程池 ForkJoinPool_开发语言_07

一开始初始化线程池不会创建线程,只有开始执行任务的时候才会创建线程,当corePoolSize小于预先设定的corePoolSize会一直创建新的线程处理任务,不管线程池有没有空闲线程。当线程池中线程数等于预先设置的线程数,但是不大于maximumPoolSize,还可以创建新的线程,但是,创建完成之后,线程池中空闲的线程空闲的时间大于预先设置的值后就会销毁到corePoolSize的线程数量。

还有一个,可以设置核心线程也超时销毁,设置allowCoreThreadTimeOut=true,corePoolSize的线程超时也会被销毁。

如果线程池中线程数等于maximumPoolSize之后还要新增任务,就要看handler指定的处理策略了

通俗解释, 如果把线程池比作一个单位的话,corePoolSize 就表示正式工,线程就可以表示一个员工。当我们向单位委派一项工作时,如果单位发现正式工还没招满,单位就会招个正式工来完成这项工作。随着我们向这个单位委派的工作增多,即使正式工全部满了,工作还是干不完,那么单位只能按照我们新委派的工作按先后顺序将它们找个地方搁置起来,这个地方就是 workQueue ,等正式工完成了手上的工作,就到这里来取新的任务。如果不巧,年末了,各个部门都向这个单位委派任务,导致 workQueue 已经没有空位置放新的任务,于是单位决定招点临时工吧(临时工:又是我!)。临时工也不是想招多少就找多少,上级部门通过这个单位的 maximumPoolSize 确定了你这个单位的人数的最大值,换句话说最多招maximumPoolSize – corePoolSize 个临时工。当然,在线程池中,谁是正式工,谁是临时工是没有区别,完全同工同酬。

如果workQueue(这个队列是可以设置大小的)队列放满了,maximumPoolSize也达到了指定值, 就要靠handler来处理了,handler默认是抛出异常。

java completefuture 默认线程池 ForkJoinPool_后端_08


查看LinkedBlockingQueue就会发现,这里面其实又一个默认值,初始化了队列的大小的。

workQueue

它决定了缓存任务的排队策略。对于不同的应用场景我们可能会采取不同的排队策略,这就需要不同类型的队列。这个队列需要一个实现了BlockingQueue接口的任务等待队列。

在ThreadPoolExecutor线程池的API文档中,一共推荐了三种等待队列,它们是:SynchronousQueueLinkedBlockingQueueArrayBlockingQueue简单点说,这个队列的类型影响的就是放入队列中任务的顺序。

SynchronousQueue

SynchronousQueue是无界的,是一种无缓冲的等待队列,但是由于该Queue本身的特性,在某次添加元素后必须等待其他线程取走后才能继续添加;可以认为SynchronousQueue是一个缓存值为1的阻塞队列,但是 isEmpty()方法永远返回是true,remainingCapacity() 方法永远返回是0,remove()和removeAll() 方法永远返回是false,iterator()方法永远返回空,peek()方法永远返回null。

在线程池中:SynchronousQueue没有容量,是无缓冲等待队列,是一个不存储元素的阻塞队列,会直接将任务交给消费者,必须等队列中的添加元素被消费后才能继续添加新的元素。

public static void main(String[] args) throws InterruptedException {
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1,
            10L, TimeUnit.SECONDS,
            new SynchronousQueue<>());
    System.out.println("执行第一个任务");
    threadPoolExecutor.submit(()->{
        try{
            Thread.sleep(1*1000);
        }catch(Exception ex) {

        }
    });
    System.out.println("执行第二个任务");
    threadPoolExecutor.submit(()->{
        try{
            Thread.sleep(1*1000);
        }catch(Exception ex) {

        }
    });
    System.out.println(threadPoolExecutor.toString());
}

java completefuture 默认线程池 ForkJoinPool_java_09

LinkedBlockingQueue

LinkedBlockingQueue是一个无界缓存等待队列。当前执行的线程数量达到corePoolSize的数量时,剩余的元素会在阻塞队列里等待。(所以在使用此阻塞队列时maximumPoolSizes就相当于无效了),每个线程完全独立于其他线程。生产者和消费者使用独立的锁来控制数据的同步,即在高并发的情况下可以并行操作队列中的数据。

注:这个队列需要注意的是,虽然通常称其为一个无界队列,但是可以人为指定队列大小,而且由于其用于记录队列大小的参数是int类型字段,所以通常意义上的无界其实就是队列长度为 Integer.MAX_VALUE,且在不指定队列大小的情况下也会默认队列大小为 Integer.MAX_VALUE

ArrayBlockingQueue

ArrayBlockingQueue是一个有界缓存等待队列,可以指定缓存队列的大小,当正在执行的线程数等于corePoolSize时,多余的元素缓存在ArrayBlockingQueue队列中等待有空闲的线程时继续执行,当ArrayBlockingQueue已满时,加入ArrayBlockingQueue失败,会开启新的线程去执行,当线程数已经达到最大的maximumPoolSizes时,再有新的元素尝试加入ArrayBlockingQueue时会执行拒绝策略。

Handler处理策略

java completefuture 默认线程池 ForkJoinPool_后端_10