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

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

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

线程池

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 核心数的两倍是最佳的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值