JAVA多线程之基础(三)

   上一篇文章说到了wait(),notify(),notifyAll()方法为什么在Object类中以及他们之间的异同还有什么是线程的安全,本文章就会讲解线程池相关的知识,为什么要使用线程池,使用它有什么好处,还有线程池的类型等一些知识。

1.什么是线程池,我们为什么要使用线程池? 使用线程池能给我们的系统带来什么好处?

首先我们先来说一下什么是线程池,在我们刚使用Java的时候是没有线程池的,而是先有线程,但是随着我们的业务不断地变得复杂,线程数变得越来越多,人们就需要一个类专门的去管理线程,于是才会有了线程池。在我们没有线程池的时候,每发布一个任务的时候,都会去创建一个线程,如果在任务非常少的时候这样操作是没问题的。但是随着我们系统中的任务越来越多,频繁的去创建线程会造成很大的系统开销和资源浪费,因为线程的每一次创建都会有创建和销毁的过程,这些都需要时间,如果任务比较简单,可能还会造成线程的创建比执行任务本身消耗的资源还要大,而且过多的线程会占用过多的内存资源,还会带来过多的上下文切换。而这个时候我们的线程池就出场了 ,首先给大家看一段代码。

大家看到这段代码,首先无论你提交多少个任务,它都是这固定的5个线程去执行,不会无限制的去创建线程,我在代码中定义了100个任务,那么线程池就会将这100个任务分配给这五个线程,这五个线程反复的去领取这一百个任务,直到所有线程执行完毕,这就是线程池的思想,那么我们使用线程池能给我们的系统带来什么好处了?

1.不用在去频繁的去创建任务线程并且线程池里面的线程是复用的,提高了程序的响应速度,充分利用了系统的CPU资源

2.可以灵活的去控制线程数量,统一管理资源,避免资源的使用不当

2.线程池中每个参数的含义,线程池中的拒绝策略,常见的线程池类型

下面我们就来简单的说一下线程池中参数的含义:

1.corePoolSize:核心线程数,也就是线程池中常驻线程

2.maxPoolSize:最大线程数,也就是当我们的任务队列达到了上限时,那么就会启动我们的设置的最大线程数

3.keepAliveTime:空闲线程的存活时间,当我们没有任务执行时,并且我们线程数大于核心线程数时,那么他就会根据存活时间去销毁那些大于核心线程数的线程

4.ThreadFactory:线程工厂,主要是用来创建线程

5.workQueue:用来存放任务的队列

6.Handler:处理被拒绝的任务,也就是线程的拒绝策略

线程池的核心参数就是这些,下面我们来画一张图,看一下流程

大家看到上面一个流程图,可以总结出来一些线程池的特点:

1.线程池在开始会保持比较少的线程数,只有在达到上限时才会去增加线程数

2.线程池只有在工作队列容量填满时才会创建多于corePoolSize的线程数,如果使用的是无界队列,由于队列不会满,所以不会创建多于核心线程数的线程

3.通过设置corePoolSize和maxPoolSize的值,可以设定固定的线程池数量

4.如果设置maxPoolSize的值为Integer.MAX_VALUE,可以允许线程池创建任意多的线程

接着我们来说一下线程池的四种拒绝策略:

首先我们要知道线程池会在那几种情况下实行拒绝策略

1.当我们调用shutdown()方法时,如果任务还在执行中,而这个时候我们在向线程池提交任务时,就会被拒绝

2.线程池已经没有能力在去处理新任务的时候

那么我们着重的说一下第二种情况:也就是由于工作任务饱和而导致的拒绝,举一个例子,比如我们使用ArrayBlockingQueue作为任务队列,我们设置核心线程数为5个,最大线程数为10,而此时我们有五十个任务需要执行,那么我们会先创建5个线程去执行任务,队列容量满了之后我们会继续创建新的线程数也就是最大线程数,但是我们五十个线程可能会有二十个线程在工作队列里面等待被执行,但是我们不能在去增加更多的线程去执行任务了,所以这个时候我们就会执行拒绝策略。在Java中ThreadPoolExecutor类中为我们提供了四种拒绝策略,都实现了RejectedExecutionHandler接口。

第一种 AbortPolicy:如果我们选用这个策略的话,他会直接抛出一个RejectedExecutionException的RunTimeException异常,让你感知到任务被拒绝了,于是你根据业务逻辑选择重新提交或者放弃提交。

第二种 DiscardPolicy:选用该拒绝策略时,当工作队列任务饱和,再有新任务提交之后会直接丢弃掉,也不会给你通知,会有一定的风险,可能会给我们造成数据丢失的情况。

第三种 DiscardOldestPolicy:如果线程池没有被关闭并且没有能力在去执行新的任务时,会丢弃掉工作队列的头节点,通常是存活时间最长的任务,这种拒绝策略与第二种不同的是不会丢弃最新的任务,而是丢弃工作队列中存活时间最长的,这样就会腾出新的空间给新任务执行,但是相对而言,也有一定的风险。

第四种 CallerRunsPolicy:相对其他三种拒绝策略而言,第四种就比较完善,如果线程池没有被关闭并且没有能力在去执行新任务时,则把这个提交的新任务交于提交任务的线程去执行,也就是谁提交任务谁执行,这样做新提交的任务不会被丢弃,而且谁提交任务谁执行,而执行任务又比较耗时,在这段期间,提交任务的线程被占用,也就不会再去提交新的任务,减缓了任务提交的速度,线程池中的线程也可以充分利用这段时间去执行掉一部分任务,腾出一定的空间,相当于给了线程池一定的缓冲期。

线程池

下面我们来简单介绍一下线程池有那几种常见类型

1.FixedThreadPool 该线程主要特点是核心线程数和最大线程数是一样的,我们就可以把它看做一个固定的线程池,就算我们任务多于我们线程数也不会再去创建新的线程去执行任务,而是把超出线程外的任务放到任务队列里面去等候。

2.CachedThreadPool 该线程可以称作为缓存线程池,主要特点是线程数可以无限制的增加,实际可以达Integer.MAX_VALUE,实际上不可能达到的,而他的工作队列是采用synchronsQueue队列,队列容量为0,实际不存储任何任务,只负责对任务进行中转和传递,所以效率比较高。

3.ScheduledThreadPool该线城称作为定时任务线程池,比如可以没隔十秒执行一次任务,而实现这种功能的方法主要有三种:

第一种schedule比较简单,表示延迟指定时间执行一次任务,我们在代码中设置为十秒,那么也就是十秒后执行一次任务。

第二种scheduleAtFixedRate表示固定的频率去执行任务,他的第二个参数initiaDelay表示第一次延时时间,第三个period表示周期,那么也就是第一次延时后每次延时多长时间执行一次任务。

第三种scheduleWithFixedDelay他与第二种相类似,也是周期执行任务,但是不同的在于对周期的定义,scheduleWithFixedDelay是以任务结束的时间为下一次循环的时间起点开始计时执行任务。

4.SingleThreadExecutor该线程是唯一线程池,意思就是只会使用唯一的线程去执行任务,意思和我们的FixedThreadPool差不多,只不过这里只有一个,而FixedThreadPool可以自己定义线程数,但是该线程在执行任务的时候发生异常那么线程池会重新创建一个线程去执行后续的任务,由于该线程只会有一个线程去执行任务,非常适合有序执行的场景,因为他是单线程的,不会像前面几种都是多线程并行执行。 

5.SingleThreadScheduleExecutor该线程实际上和SingleThreadExecutor非常的相似,他的内部也是只有一个线程去执行任务。

6.ForkJoinPool该线程池是jdk1.7加入进去的,我们见名思意Fork拆分,Join合并,比如我们现在有一个任务名为Task,那么这个Task可以产生三个子任务去分别执行,互不影响并且是独立的,最后将结果进行汇总。

我们说完了常见的线程池类型,接下来可以阐述一下常见线程池里面常用的阻塞队列:

大家看到这些对应图,发现五种线程池对应三种阻塞队列,我们先来说一下第一种阻塞队列:

1.LinkedBlockingQueue该阻塞队列的容量为Integer.MAX_VALUE,可以认为是无界队列(FixThreadPool,SingleThreadExecutor)

2.SynchronousQueue该阻塞队列并不是用来存储任务,而是起到一个转发的作用,当有新的任务到来就直接被转发到线程或者创建新线程去执行,并不需要去保存他们(CacheThreadPool)

3.DelayedWorkQueue该阻塞队列内部采用的是 堆 的数据结构,内部存入元素的顺序是按照延迟的时间长短去进行排序,他和LinkedBlockingQueue一样也是一个无界队列(ScheduledThreadPool,DelayedWorkQueue)

4.至于ForkJoinPool采用的线程池是他的内部类WorkQueue,具体可自行查看。

在最上面时,我们讲到为什么要创建线程池以及他给我们带来的好处,还有创建线程池的几种方式,但是在实际使用中并不推荐使用上面那几种方式去创建线程池,而是建议我们手动创建线程池,那么我们为什么推荐手动创建线程池并且根据我们自己的实际需要去创建线程池,而我们应该创建多少合适的线程数量?下面我们来讲解一下

1.为什么建议手动创建线程池?如何根据自己的实际需要去创建线程池?

(1)我们可以观看上面几种线程池源码,会发现FixedThreadPool内部实际上调用了ThreadPoolExector构造函数并通过构造函数传参,创建了一个核心线程数和最大线程数,他们的数量也就是我们传入的参数,但是这里得重点在于他采用的阻塞队列,LinkedBlockingQueue无界队列,如果线程在处理任务的速度比较慢时,而我们的请求越来越多,会导致大量的任务堆积并且占用大量内存,最终导致OOM也就是OutOfMemoryError,这几乎会导致我们整个系统的瘫痪,SingleThreadExecutor同理。

(2)CacheThradPool该线程采用的阻塞队列为SynchronousQueue,该阻塞队列本身并不存储任何任务,而是对其进行转发,但是由于CacheThreadPool并不限制线程的数量,当任务数量特别多的时候,就可能会导致创建非常多的线程,最终超过了操作系统的上限而无法创建新线程,或者导致内存不足。

(3)ScheduledThreadPool&&SingleThreadScheduledExecutor我们查看其源码发现他内部调用的是ScheduledThreadPoolExecutor,该构造函数是ThreadPoolExecutor的子类,这两个采用的阻塞队列为DelayedWorkQueue,而它也是一个无界队列,如果存放过多的任务也会造成OOM。

大家可以看到这几种自动创建线程池都存在风险,所以相比较而言,我们自己手动创建会更加好,因为我们可以根据自己的需要去创建,避免资源耗尽的风险。

那么我们如何根据自身的需求去创建自己的线程池了?

那么毋庸置疑核心线程数,阻塞队列,线程工厂,拒绝策略这几个核心参数是绝对需要的。

1.说到核心线程数,那么就要合适的线程数量,在Java并发编程实战中推荐的计算方法:

线程数=CPU核心数*(1+平均等待时间/平均工作时间)通过这个公司我们可以得出一个合适的线程数量,过少的线程会使得程序整体性能降低,过多的线程也会消耗内存等其他资源,所以合适线程数还需要经过一系列的压测,监控等手段去确定,合理并充分利用资源。

2.阻塞队列,对于这个我们可以选择我们上面讲解的几种阻塞队列,但是除了上面几种阻塞队列之外,还有一种阻塞队列叫ArrayBlockingQueue,该阻塞队列是利用数组实现的,在新建时要求传入容量值,并且后期不能扩容,这样一来该阻塞队列的容量是有限的,不会像其他几种阻塞队列会有OOM的风险,可是他也有缺点,因为他的容量是有限的,如果队列的任务放满了,且线程数也达到了最大,线程池就会根据规则拒绝新任务,可能会造成数据丢失的情况。但是相比造成OOM的风险,数据丢失的情况还是会好一些,如果我们使用ArrayBlockingQueue设置了最大线程数量,就可以有效的防止资源耗尽的情况

3.线程工厂,对于该参数我们可以采用默认的defaultThreadFactory,也可以传入自定义的线程工厂,因为我们可能有多个线程池,而为了更好的区分业务,我们可以利用不同的线程池去命名以区分不同的业务。

4.拒绝策略,该参数在上面我们也有所讲解,此处就不在过多讲解,除了上面那几种拒绝策略之外,我们还可以实现RejectedExecution接口来实现自己的拒绝策略。

总结:我们定时自己的线程池是和业务相关的,在创建自己的线程池时,我们必须要掌握每个参数的含义,创建一个非常适合自己的线程池,这样既不会导致内存不足,同事又可以用合适的线程来保障任务执行的效率。

那么我们使用线程池的时候没有任务执行时,里面一直有核心线程数存在,这也是对资源的一种浪费,我们可以使用完线程池之后把它关闭,外面大概常用的就是shutdown和shutdownNow,我们来讲讲他们之间的区别

shutdown:线程池关闭,他可以安全的关闭一个线程池,但是当我们调用shutdown线程池并不是立马就关闭的,因为有可能我们线程池还有很多任务正在被执行,他会等线程池执行完正在执行的任务和任务队列中的任务之后才彻底关闭,但是调用shutdown方法之后,线程池会根据拒绝策略拒绝后续提交的新任务。

shutdownNow:他也是线程池的关闭方法,在执行shutdownNow方法之后,他会给所有线程池之中的线程发送一个interrupt中断信号,尝试中断这些任务的执行,然后会将任务队列中正在等待执行的任务封装成一个List并返回给我们,让我们进行一些补救的操作,但是由于Java不推荐强制停止线程,所以即便我们调用shutdownNow()方法,如果被中断的线程对中断信号不理不睬,那么依然有可能会导致任务不会被停止。

 

说到这里线程池的有关知识已经差不多了,还有其他的需要小伙伴自行去查看,如有错误,欢迎指正。

 

 

 

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值