JAVA并发编程——线程池原理、使用和参数设置详解

苏凌峰

1.线程池简介

2.线程池使用

3.线程池参数详解

4.如何合理设置线程池参数

1.线程池简介
我们先来介绍一下,什么是线程池?
在以往的博客中,我们介绍线程的时候,无论是Runnable,还是Calllable这些,最终都是使用new Thread().start(); 的方式进行创建,其实在真实的生产环境中,我们是禁止使用这种写法的,因为这样有几个严重的弊端:
a.每次new Thread() 新建对象,会造成开销,导致服务器运行性能变差。
b.在无限制new Thread()的情况下,我们一不小心就会创建过多的线程,相互之间竞争资源,可能导致上下文频繁切换,占用资源过多,死锁或者oom。
c. 缺乏更多功能,如定时执行、定期执行、线程中断。

于是,JAVA线程池就产生了!

JAVA线程池做的主要工作是控制运行的线程的数量,处理过程中将任务放入队列然后在线程创建后启动这些任务,如果线程数量超过了最大执行数量,超出数量的线程将排队等候,等待其它线程执行完毕,再从队列中取出任务来执行

它的主要特点:线程服用,控制最大并发数,管理线程。

第一:降低资源消耗,通过重复利用自己创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行
第三:提高线程的可管理性,线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以统一地进行分配,调优和监控。

多说无用,我们来看看如何使用JAVA线程池吧!

2.线程池使用
我们先介绍一下JAVA自带的创建线程池的工具类!

Executors

它提供了三个方法来创建线程池:

        ExecutorService threadPool = Executors.newFixedThreadPool(5);//一池固定数线程
        ExecutorService threadPoo2 = Executors.newCachedThreadPool();//一池多线程
        ExecutorService threadPoo3 = Executors.newSingleThreadExecutor();//一池一线程

顾名思义:
newFixedThreadPool()方法就是创建一个固定线程数的线程池,
newCachedThreadPool()方法会根据你计算机和任务的情况,进行自动增减线程
newSingleThreadExecutor()创建一个单线程的线程池,每次只能执行一个任务

同时创建完线程池之后,使用线程池自带的submit或者excute方法,然后用lambda表达式传入函数体后,就可以执行任务了!

        threadPool.submit();
        threadPool.execute();

这时,我们可以使用一个线程池尝试着使用一下

        ExecutorService threadPool = Executors.newFixedThreadPool(5);//一池固定数线程
        //模拟10000个用户来办理业务,每一个用户就是来自外部的请求线程
        try {
            for (int i = 1; i < 10000; i++) {
                threadPool.submit(() -> {
                    System.out.println(Thread.currentThread().getName() + "正在办理业务");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }

我们创建了一个线程池,固定数量为5,然后启动10000个线程,看看运行情况。
image.png

可以看出,线程编号始终没有超过我们设置的初始值5。

这时,我们可以换一个线程池使用一下,我们使用cache线程池:

 
        ExecutorService threadPoo1 = Executors.newCachedThreadPool();//一池多线程
        //模拟10000个用户来办理业务,每一个用户就是来自外部的请求线程
        try {
            for (int i = 1; i < 10000; i++) {
                threadPoo1.submit(() -> {
                    System.out.println(Thread.currentThread().getName() + "正在办理业务");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPoo1.shutdown();
        }

image.png
可以看出,线程启动地越多,该线程池就会启动越多的线程来适应需求。

让我们来看一下最后一个线程池:

        ExecutorService threadPoo1 = Executors.newSingleThreadExecutor();//一池一线程
        //模拟10000个用户来办理业务,每一个用户就是来自外部的请求线程
        try {
            for (int i = 1; i < 10000; i++) {
                threadPoo1.submit(() -> {
                    System.out.println(Thread.currentThread().getName() + "正在办理业务");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPoo1.shutdown();
        }

查看结果:
image.png
可以看出,永远只有一个线程在运行。

是不是非常神奇?!我们可以看一下创建线程池的方法到底怎么执行地,居然有这种效果!

首先看一下固定数线程的方法:

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, //常驻核心线程数
                                      nThreads,//最大线程数
                                      0L,//多余的空闲线程的存活时间 
                                      TimeUnit.MILLISECONDS,//多余线程存活单位
                                      new LinkedBlockingQueue<Runnable>()); //使用的阻塞队列
    }

cache线程池的方法:

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

单线程池的方法:

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

从我给的源码注释中可以看出,
固定数线程池最大和常驻线程数都是传入的参数
而cache线程池的最大线程数是Integer.MAX_VALUE
单线程池底层使用了SynchronousQueue : 不存储元素的阻塞队列,即单个元素的队列。
多线程池的底层使用了LinkedBlockingQueue : 由链表组成的有界(但是默认值为Integer.MAX_VALUE阻塞队列
阻塞队列可以看我之前写的这篇文章:JAVA并发编程——阻塞队列理论及其API介绍
所以就导致了为什么会有以上的运行结果!

所以在生产中,我们究竟如何使用呢??

答案是————一个都不使用,因为LinkedBlockingQueue它的最大长度是Integer.MAX_VALUE,那就相当于无界可能会让服务器内存爆仓产生OOM,所以接下来,我们学习一下自定义创建线程池的参数!

3.线程池参数详解
我们先点到ThreadPoolExecutor这个类里看一下,它究竟有几个参数:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), handler);
    }

我们可以看到,有七个参数
corePoolSize 线程池中常驻核心线程数
maximumPoolSize 线程池能够容纳的同时执行的最大线程数,此值必须大于等于1
keppAliveTime 多余的空闲线程的存活时间
unit 存活时间单位
workQueue 任务队列,被提交但是尚未执行的任务
threadFactory 表示生成线程池中工作线程的线程工厂,用于创建线程,一般默认即可
handler 拒绝策略,表示队列满了并且工作线程大于等于线程池的最大线程数,如何处理。

前面几个参数都好理解,重要的是这个方法可以自定义阻塞队列的类型,我们就可以避免使用LinkedBlockingQueue了

接下来,我们看一下handler拒绝策略:
当核心线程数满了,阻塞队列也满了的时候,再进来的线程,就会启动拒绝策略,一般常见的拒绝策略有这几种:

AbortPolicy(默认):直接抛出RejectExcutionException异常阻止线程运行。
CallerRunsPolicy:将多出来的任务退还给调用者,从而降低流量。

DiscardOldestPolicy:抛弃等待队列中等待最久的任务,然后把当前任务加入等待队列。

DiscardPolicy:直接丢弃任务

接下来,我们尝试一下自己新建线程池,并尝试着使用该线程池执行一下任务。

     ThreadPoolExecutor threadPoolExecutor
                = new ThreadPoolExecutor(2,//常驻核心线程数
                5,//最大核心线程数
                1L,//多余的空闲线程的存活时间
                TimeUnit.SECONDS,//多余空闲线程存活时间单位
                new ArrayBlockingQueue<Runnable>(10),//阻塞队列
                Executors.defaultThreadFactory(),//生成线程池中工作线程的线程工厂
                new ThreadPoolExecutor.AbortPolicy());//拒绝策略

        //模拟多个用户来办理业务,每一个用户就是来自外部的请求线程
        try {
            for (int i = 1; i < 10000; i++) {
                threadPoolExecutor.submit(() -> {
                    System.out.println(Thread.currentThread().getName() + "正在办理业务");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPoolExecutor.shutdown();
        }
    }

在使用当前拒绝策略下,很明显可以看出,执行线程超出线程池的数量了,会抛异常,我们来看一下运行结果:
image.png

我们可以换一个拒绝策略试一下:比如说DiscardPolicy(抛弃任务)

        ThreadPoolExecutor threadPoolExecutor
                = new ThreadPoolExecutor(2,//常驻核心线程数
                5,//最大核心线程数
                1L,//多余的空闲线程的存活时间
                TimeUnit.SECONDS,//多余空闲线程存活时间单位
                new ArrayBlockingQueue<Runnable>(10),//阻塞队列
                Executors.defaultThreadFactory(),//生成线程池中工作线程的线程工厂
                new ThreadPoolExecutor.DiscardPolicy());//拒绝策略

image.png
它就运行完成了,且没有抛出任何异常。

4.如何合理设置线程池参数
如何配置线程池的参数,其实我们最需要配置的参数只有一个:
corePoolSize 线程池中常驻核心线程数

当我们的线程池使用场景是在CPU密集型(进程绝大部份任务依靠cpu的计算能力完成),那么为了防止上下文的切换造成的开销,我们一般设置最大线程数等于CPU核数(核数获取方法:Runtime.getRuntime().availableProcessors();)

当我们的线程池使用场景是在IO密集型(绝大部分任务就是在读入,输出数据),那么cpu不是非常非常繁忙,大部分在等待的情况下,最大线程可以是CPU核数的两倍。

5.总结
这次我们介绍了线程池的原理以及参数详解,最重要的是,创建线程池必须要手动使用构造方法创建,不能使用默认的方法类进行创建!

最后,我们用两张图来概括一下线程池的执行流程:

image.png

image.png

好了,今天的分享就到这里!

阅读 489

你的迷惑在于想得太多而书读的太少。

4 声望
5 粉丝
0 条评论
你知道吗?

你的迷惑在于想得太多而书读的太少。

4 声望
5 粉丝
宣传栏