实现线程的多种方式&锁的介绍&ThreadLocal&线程池 详细总结(下)

本文主要介绍线程池的基本使用

上述其他介绍在上一篇文章中:实现线程的多种方式&锁的介绍&ThreadLocal&线程池 详细总结(上)-CSDN博客

线程池

5.1、为什么使用线程池

线程池可以看做是管理了 N 个线程的池子,和连接池类似

5.2、认识线程池

5.2.1、线程池继承体系
Java 1.5 之后就提供了线程池 ThreadPoolExecutor ,它的继承体系如下:
        ThreadPoolExecutor :线程池
        Executor: 线程池顶层接口,提供了 execute 执行线程任务的方法
        Execuors: 线程池的工具类,通常使用它来创建线程池

 示例:

        public static void main(String[] args) {
            ExecutorService executorService = Executors.newFixedThreadPool(5);
            for (int i = 0 ; i < 200 ; i++){
                executorService.execute(new Runnable() {
                    @Override
                    public void run() {
                        //有5个线程在执行
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();

                        }
                        System.out.println(Thread.currentThread().getName()+":线程执 行了...");
                    }
                });
            }
        }
    }
运行结果:

5.3、线程池原理 

5.3.1、执行流程
我们以一个生活中的举例来理解:
1. 老陈要开软件公司,合伙几个核心的程序员做开发 : ( 线程核心数 )
2. 新的项目过来一个人接收一个项目去做,没有人手了,把新进来的项目放入项目排队池 ( 任务队列 )
3. 如果项目队列中的任务过多,需要招聘一些临时的程序员 ( 非核心线程 ) ,但是规定所有的开发总人数不能50( 最大线程数 )
4. 如果新的项目进来,核心程序员和临时程序员都没有人手了,并且项目队列也放满了,新来的项目该如何处理呢?
1 、拒绝 2 、丢弃老的项目做新的项目 3 、老陈自己做新的项目
线程提交优先级:核心 -> 队列 -> 非核心
线程执行优先级:核心 -> 非核心 -> 队列
1 、线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
2 、当调用 execute() 方法添加一个任务时,线程池会做如下判断:
        a) 如果正在运行的线程数量小于 corePoolSize ,那么马上创建线程运行这个任务;
        b) 如果正在运行的线程数量大于或等于 corePoolSize ,那么将这个任务放入队列;
        c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize ,那么还是要创建非核 心线程立刻运行这个任务;
        d) 如果队列满了,而且正在运行的线程数量等于 maximumPoolSize ,那么线程池会抛出异常
RejectExecutionException
3 、当一个线程完成任务时,它会从队列中取下一个任务来执行。
4 、当一个线程无事可做,超过一定的时间( keepAliveTime )时,线程池会判断,如果当前运行的线程数大于 corePoolSize ,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
5.3.2、线程池核心构造器
线程池源码 ThreadPoolExecutor 构造器:

 线程池7个参数的构造器非常重要:

1 CorePoolSize: 核心线程数,不会被销毁
2 MaximumPoolSize : 最大线程数 ( 核心 + 非核心 ) ,非核心线程数用完之后达到空闲时间会被销毁
3 KeepAliveTime: 非核心线程的最大空闲时间,到了这个空闲时间没被使用,非核心线程销毁
4 Unit: 空闲时间单位
5 WorkQueue: 是一个 BlockingQueue阻塞队列,超过核心线程数的任务会进入队列排队         
        SynchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执 行新来的任务;
        LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为 Integer.MAX_VALUE
        ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小
6 ThreadFactory :线程工厂,用于创建线程池中线程的工厂方法,通过它可以设置线程的命名规则、优先级和线程类型。使用 ThreadFactory 创建新线程。 推荐使用 Executors.defaultThreadFactory
7 Handler: 拒绝策略,任务超过 最大线程数 + 队列排队数 ,多出来的任务该如何处理取决于 Handler
        AbortPolicy丢弃任务并抛出 RejectedExecutionException 异常;
        DiscardPolicy丢弃任务,但是不抛出异常;
        DiscardOldestPolicy丢弃队列最前面的任务,然后重新尝试执行任务;
        CallerRunsPolicy由调用线程处理该任务
可以定义和使用其他种类的 RejectedExecutionHandler 类来定义拒绝策略。

5.4、常见四种线程池 

Jdk 官方提供了常见四个静态方法来创建常用的四种线程 . 可以通过 Excutors 创建
1. CachedThreadPool :可缓存
2. FixedThreadPool :固定长度
3. SingleThreadPool :单个
4. ScheduledThreadPool :可调度
5.4.1CachedThreadPool
可缓存线程池,可以无限制创建线程
根据源码可以看出:
        这种线程池内部没有核心线程,线程的数量是有限制的最大是Integer 最大值
        在创建任务时,若有空闲的线程时则复用空闲的线程( 缓存线程 ) ,若没有则新建线程
        没有工作的线程(闲置状态)在超过了60S 还不做事,就会销毁
        适用:执行很多短期异步的小程序或者负载较轻的服务器
实战:
运行结果:
5.4.2FixedThreadPool  

根据源码可以看出:
        该线程池的最大线程数等于核心线程数,所以在默认情况下,该线程池的线程不会因为闲置状态超 时而被销毁
        如果当前线程数小于核心线程数,并且也有闲置线程的时候提交了任务,这时也不会去复用之前的 闲置线程,会创建新的线程去执行任务(必须达到最大核心数才会复用线程)。如果当前执行任务 数大于了核心线程数,大于的部分就会进入队列等待。等着有闲置的线程来执行这个任务
        适用:执行长期的任务,性能好很多  
实战:
    public class fixedThreadPool {
        public static void main(String[] args) {
            ExecutorService executorService = Executors.newFixedThreadPool(5);
            for (int i = 0; i < 150; i++) {
                executorService.execute(new Runnable() {
                    @Override
                    public void run() {
                        //有5个线程在执行
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + ":线程执 行...");
                    }
                });
            }
        }
    }
运行效果:
5.4.3SingleThreadPool

根据源码可以看出:

        有且仅有一个工作线程执行任务
        所有任务按照指定顺序执行,即遵循队列的入队出队规则。
        适用:一个任务一个任务执行的场景。 如同队列
实战:

 运行结果:

 5.4.4ScheduledThreadPool

根据源码可以看出:
        1. DEFAULT_KEEPALIVE_MILLIS就是默认 10L ,这里就是 10 秒。这个线程池有点像是
        CachedThreadPool和 FixedThreadPool 结合了一下
        2. 不仅设置了核心线程数,最大线程数也是Integer.MAX_VALUE
        3. 这个线程池是上述4 个中唯一一个有延迟执行和周期执行任务的线程池
        4. 适用:周期性执行任务的场景(定期的同步数据)
实战:
    public static void main(String[] args) {
        //带缓存的线程,线程复用,没有核心线程,线程的最大值是 Integer.MAX_VALUE
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5);
        //延迟 n 时间后,执行一次,延迟任务
        executorService.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("延迟任务执行.....");
            }
        }, 10, TimeUnit.SECONDS);
        //定时任务,固定 N 时间执行一次 ,按照上一次任务的开始执行时间计算下一次任务开始时间
        executorService.scheduleAtFixedRate(() -> {
            System.out.println("定时任务 scheduleAtFixedRate 执行time:" + System.currentTimeMillis());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, 1, 1, TimeUnit.SECONDS);
        //定时任务,固定 N 时间执行一次 ,按照上一次任务的结束时间计算下一次任务开始时间
        executorService.scheduleWithFixedDelay(() -> {
            System.out.println("定时任务 scheduleWithFixedDelay 执行time:" + System.currentTimeMillis());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, 1, 1, TimeUnit.SECONDS);
    }
运行结果:
总结:除了 new ScheduledThreadPool 的内部实现特殊一点之外,其它线程池内部都是基于
ThreadPoolExecutor 类( Executor 的子类)实现的。
5.4.5、自定义ThreadPoolExecutor
    public static void main(String[] args) {
        //核心 4 个 ,最大 10 个 ,30s的空闲销毁非核心6个线程, 队列最大排队 10 个
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(4, 10, 30L, TimeUnit.SECONDS, //超过核心线程数量的线程 30秒之后会退出
                new ArrayBlockingQueue<Runnable>(10), //队列排队10个
                new ThreadPoolExecutor.DiscardPolicy()); //任务满了就丢弃
        for (int i = 0 ; i < 210 ; i++){
            int finalI = i;
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    //始终只有一个线程在执行
                    System.out.println(Thread.currentThread().getName()+":线程执 行..."+ finalI);
                }
            });
        }
    }

分析: 上面示例中,是创建了 210 个线程,但是从结果来看,却只有 10 个线程,就是因为有下面的设置:
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(4, 10,
30L, TimeUnit.SECONDS, //超过核心线程数量的线程 30秒之后会退出
new ArrayBlockingQueue<Runnable>(10), //队列排队10个
new ThreadPoolExecutor.DiscardPolicy()); //任务满了就丢弃
这里设置了最大线程是 10 个,如果多了就会排队 10 个,再多的线程就会直接丢弃

5.5、在ThreadPoolExecutor类中几个重要的方法

Execute :方法实际上是 Executor 中声明的方法,在 ThreadPoolExecutor 进行了具体的实现,这个方法是 ThreadPoolExecutor 的核心方法,通过这个方法可以向线程池提交一个任务,交由线程池去执行。
Submit :方法是在 ExecutorService 中声明的方法,在 AbstractExecutorService 就已经有了具体的实现,在 ThreadPoolExecutor 中并没有对其进行重写,这个方法也是用来向线程池提交任务的,实际上它 还是调用的 execute() 方法,只不过它利用了 Future 来获取任务执行结果。
Shutdown :不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务。
shutdownNow :立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务。
isTerminated :调用 ExecutorService.shutdown 方法的时候,线程池不再接收任何新任务,但此时线程池并不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出。在调用 shutdown 方法 后我们可以在一个死循环里面用 isTerminated 方法判断是否线程池中的所有线程已经执行完毕,如果子 线程都结束了,我们就可以做关闭流等后续操作了。

5.6、如何设置最大线程数

5.6.1CPU密集型
定义:
CPU 密集型也是指计算密集型,大部分时间用来做计算逻辑判断等 CPU 动作的程序称为 CPU 密集型任务。该类型的任务需要进行大量的计算,主要消耗 CPU 资源。 这种计算密集型任务虽然也可以用多任务 完成,但是任务越多,花在任务切换的时间就越多, CPU 执行任务的效率就越低,所以,要最高效地利 CPU ,计算密集型任务同时进行的数量应当等于 CPU 的核心数。
特点:
1. CPU 使用率较高(也就是经常计算一些复杂的运算,逻辑处理等情况)非常多的情况下使用
2. 针对单台机器,最大线程数一般只需要设置为 CPU 核心数的线程个数就可以了
3. 这一类型多出现在开发中的一些业务复杂计算和逻辑处理过程中。
示例:
    public class Demo02 {
        public static void main(String[] args) {
            //自定义线程池! 工作中只会使用 ThreadPoolExecutor
            /**
             * 最大线程该如何定义(线程池的最大的大小如何设置!)
             * 1、CPU 密集型,几核,就是几,可以保持CPU的效率最高!
             */
            //获取电脑CPU核数
            System.out.println(Runtime.getRuntime().availableProcessors()); //8核
            ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                    2, //核心线程池大小
                    Runtime.getRuntime().availableProcessors(), //最大核心线程池大小(CPU密集型,根据CPU核数设置)
                    3, //超时了没有人调用就会释放
                    TimeUnit.SECONDS, //超时单位
                    new LinkedBlockingDeque<>(3), //阻塞队列
                    Executors.defaultThreadFactory(), //线程工厂,创建线程的,一般不用动
                    new ThreadPoolExecutor.AbortPolicy()); //银行满了,还有人进来,不处理这个人的,抛出异常
            try {
                //最大承载数,Deque + Max (队列线程数+最大线程数)
                //超出 抛出 RejectedExecutionException 异常
                for (int i = 1; i <= 9; i++) {
                    //使用了线程池之后,使用线程池来创建线程
                    threadPool.execute(() -> {
                        System.out.println(Thread.currentThread().getName() + " ok");
                    });
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                //线程池用完,程序结束,关闭线程池
                threadPool.shutdown(); //(为确保关闭,将关闭方法放入到finally中)
            }
        }
    }
5.6.2IO密集型
定义:
1 IO 密集型任务指任务需要执行大量的 IO 操作,涉及到网络、磁盘 IO 操作,对 CPU 消耗较少,其消耗的主要资源为 IO
2 、我们所接触到的 IO ,大致可以分成两种:磁盘 IO 和网络 IO
        磁盘 IO ,大多都是一些针对磁盘的读写操作,最常见的就是文件的读写,假如你的数据库、
Redis 也是在本地的话,那么这个也属于磁盘 IO
        网络 IO ,这个应该是大家更加熟悉的,我们会遇到各种网络请求,比如 http 请求、远程数据库读 写、远程 Redis 读写等等。
特点:
        IO 操作的特点就是需要等待,我们请求一些数据,由对方将数据写入缓冲区,在这段时间中,需 要读取数据的线程根本无事可做,因此可以把 CPU 时间片让出去,直到缓冲区写满
        既然这样,IO 密集型任务其实就有很大的优化空间了(毕竟存在等待)
        CPU 使用率较低,程序中会存在大量的 I/O 操作占用时间,导致线程空余时间很多,所以通常就需要开 CPU 核心数两倍的线程。当线程进行 I/O 操作 CPU 空闲时,线程等待时间所占比例越高,就 需要越多线程,启用其他线程继续使用 CPU ,以此提高 CPU 的使用率;线程 CPU 时间所占比例越 高,需要越少的线程,这一类型在开发中主要出现在一些计算业务频繁的逻辑中
示例:
    public class Demo02 {
        public static void main(String[] args) {
            //自定义线程池! 工作中只会使用 ThreadPoolExecutor
            /**
             * 最大线程该如何定义(线程池的最大的大小如何设置!)
             * 2、IO 密集型 >判断你程序中十分耗IO的线程
             * 程序 15个大型任务 io十分占用资源! (最大线程数设置为30)
             * 设置最大线程数为十分耗io资源线程个数的2倍
             */
            //获取电脑CPU核数
            System.out.println(Runtime.getRuntime().availableProcessors()); //8核
            ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                    2, //核心线程池大小
                    16, //若一个IO密集型程序有15个大型任务且其io十分占用资源!(最大线程数设置为 2*CPU 数目)
                    3, //超时了没有人调用就会释放
                    TimeUnit.SECONDS, //超时单位
                    new LinkedBlockingDeque<>(3), //阻塞队列
                    Executors.defaultThreadFactory(), //线程工厂,创建线程的,一般不用动
            new ThreadPoolExecutor.DiscardOldestPolicy()); //队列满了,尝试和最早的竞争,也不会抛出异常
            try {
                //最大承载数,Deque + Max (队列线程数+最大线程数)
                //超出 抛出 RejectedExecutionException 异常
                for (int i = 1; i <= 9; i++) {
                    //使用了线程池之后,使用线程池来创建线程
                    threadPool.execute(()->{
                        System.out.println(Thread.currentThread().getName()+" ok");
                    });
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                //线程池用完,程序结束,关闭线程池
                threadPool.shutdown(); //(为确保关闭,将关闭方法放入到finally中)
            }
        }
    }
         
5.6.3、分析
1 :高并发、任务执行时间短的业务,线程池线程数可以设置为 CPU 核数 +1 ,减少线程上下文的切换
2 并发不高、任务执行时间长的业务这就需要区分开看了:
   a )假如是业务时间长集中在 IO 操作上,也就是 IO 密集型的任务,因为 IO 操作并不占用 CPU ,所以不要让所有的 CPU 闲下来,可以适当加大线程池中的线程数目,让 CPU 处理更多的业务
   b )假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,线程池中的线程数设置得少一些,减少线程上下文的切换(其实从一二可以看出无论并发高不高,对于业务中是否是 cpu 密集还是 I/O 密集的判断都是需要的当前前提是你需要优化性能的前提下)
3 并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些 业务里面某些数据是否能做缓存是第一步,我们的项目使用的时 redis 作为缓存(这类非关系型数据库还 是挺好的)。增加服务器是第二步(一般政府项目的首先,因为不用对项目技术做大改动,求一个稳, 但前提是资金充足),至于线程池的设置,设置参考 2 。最后,业务执行时间长的问题,也可能需要分 析一下,看看能不能使用中间件(任务时间过长的可以考虑拆分逻辑放入队列等操作)对任务进行拆分 和解耦。
5.6.4、总结
1. 一个计算为主的程序( CPU 密集型程序),多线程跑的时候,可以充分利用起所有的 CPU 核心
数,比如说 8 个核心的 CPU , 8 个线程的时候,可以同时跑 8 个线程的运算任务,此时是最大效
率。但是如果线程远远超出 CPU 核心数量,反而会使得任务效率下降,因为频繁的切换线程也是
要消耗时间的。因此对于 CPU 密集型的任务来说,线程数等于 CPU 数是最好的了。
2. 果是一个磁盘或网络为主的程序( IO 密集型程序),一个线程处在 IO 等待的时候,另一个线程还可以在 CPU 里面跑,有时候 CPU 闲着没事干,所有的线程都在等着 IO ,这时候他们就是同时的 了,而单线程的话此时还是在一个一个等待的。我们都知道 IO 的速度比起 CPU 来是很慢的。此时线程数等于 CPU 核心数的两倍是最佳的。

猜你喜欢

转载自blog.csdn.net/Libigtong/article/details/134397949
今日推荐