java线程池详解

我们要了解线程池,首先要知道线程池产生的原因,即线程池存在的意义,然后再剖析其工作原理,以及如何合理使用线程池,这样我们才会对线程池有个较为深刻的认识。

一、线程池存在的意义

我们常见的创建线程的方法,一种是继承Thread类,一种是实现Runnable的接口,Thread类其实也是实现了Runnable接口,不管用哪种方式,我们所创建的这两种线程在运行结束后都会被虚拟机销毁,如果任务数量多的话,频繁的创建和销毁线程会浪费大量时间,降低任务效率,创建过多的线程也会使内存开销吃紧。那么有没有一种方法能很好的解决这种问题呢?答案就是线程池。线程池可以让任务线程运行完成任务后不用立即销毁,而是让线程重复使用,节省了大量时间。

线程池的优点

1.服务器的线程数量是有限的,使用线程池使得每个工作线程都可以重复利用,有效的减少了因过多的创建和销毁线程所带来的时间和空间(内存)压力,省时省资源。

2.可以根据系统的承受能力,调整线程池中工作线程的数量,提高服务器工作效率,降低因为消耗过多内存导致服务器崩溃的风险。

二、线程池工作原理

事实上,线程池运用的思想就是“以空间换时间”,牺牲一定的内存,来换取任务效率,就现在服务器发展的速度来看,牺牲这点空间所换来的效率,性价比是非常之大的,线程池合理的利用wait和notify两个方法处理线程状态,从而达到有效减少切换上下文的频率。在讲原理之前我们需要了解几个关键名词:

1.poolSize:线程池中当前存活线程的数量,当该值为0的时候,意味着没有任何线程,线程池会终止;同一时刻,poolSize不会超过maximumPoolSize。

2.allowCoreThreadTimeOut:该属性用来控制是否允许核心线程超时退出。
如果线程池的大小已经达到了corePoolSize,不管有没有任务需要执行,线程池都会保证这些核心线程处于存活状态。但是如果将allowCoreThreadTimeOut属性设置为true,则核心线程在空闲时间达到keepAliveTime时也会退出,该属性只是用来控制核心线程的状态的。

3.keepAliveTime:如果一个线程处在空闲状态的时间超过了该属性值,就会因为超时而退出。
举个例子,如果线程池的核心大小corePoolSize=5,而当前大小poolSize =8,那么超出核心大小的线程(3个),会按照keepAliveTime的值判断是否会超时退出。如果线程池的核心大小corePoolSize=5,而当前大小poolSize =5,那么线程池中所有线程都是核心线程,这个时候线程是否会退出,取决于allowCoreThreadTimeOut。

4.corePoolSize:线程池基础大小(或者说线程池核心线程数量)

5.maximumPoolSize:线程池中允许的最大线程数,线程池中的当前线程数目不会超过该值。如果队列中任务已满,并且当前线程个数小于maximumPoolSize,那么会创建新的线程来执行任务。这里值得一提的是largestPoolSize,该变量记录了线程池在整个生命周期中曾经出现的最大线程个数。为什么说是曾经呢?因为线程池创建之后,可以调用setMaximumPoolSize()改变运行的最大线程的数目。

6.workQueue:(阻塞)缓存队列,用来存放等待被执行的任务。有ArrayBlockingQueue和PriorityBlockingQueue,一般使用LinkedBlockingQueue和Synchronous。线程池的排队策略与BlockingQueue有关。
在当前时刻工作线程数达到corePoolSize时,新进入的任务才会被塞入workQueue进行排队等待。当workQueue达到上限,且线程池工作线程达到maximumPoolSize时,新进入线程的任务会被线程池拒绝处理,具体拒绝策略需要参照handler的拒绝策略

7.unit:存活时间的单位(和keepAliveTime配合使用)

8.handler:饱和策略(RejectedExecutionHandler),线程数量大于最大线程数就会采用拒绝处理策略,四种策略为

策略方案注释
ThreadPoolExecutor.AbortPolicy丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy由调用线程处理该任务

线程池任务执行描述

1.线程池启动初期:线程池在启动初期,线程并不会立即启动(poolSize=0),而是要等到有任务提交时才会启动,除非调用了prestartCoreThread(预启动一个空闲任务线程待命)或者prestartAllCoreThreads(预启动全部空闲任务线程待命,此时poolSize=corePoolSize)事先启动核心线程。

2.任务数量小于corePoolSize的情况:我们假设未进行线程预启动,那么每提交一个任务给线程池,线程池都会为其创建一个任务线程,直至所创建的线程数量达到corePoolSize(这些线程都是非空闲状态的线程,如果有空闲状态的线程,新提交的任务会直接分配给空闲任务线程去处理)。

3.任务数量大于corePoolSize且小于maximumPoolSize的情况:当线程池中工作线程数量达到corePoolSize时,线程池会把此时进入的任务提交给workQueue进行“排队等待”处理,如果此时恰好线程池内某个(或者某些)核心线程处于空闲状态(已处理完之前的任务),那么线程池会把任务从阻塞队列中取出,交给这个(些)空闲的线程去处理。如果此时workQueue已经满了,且工作核心线程数已经到poolSize=corePoolSize的状态,那么线程池就会继续创建新的线程来处理任务,但是工作线程总数不会超过maximumPoolSize。

4.任务数量超出maximumPoolSize的情况:当线程池接受的任务数量超出maximumPoolSize时,超出的任务会被线程池拒绝处理,我们称线程池已饱和,具体饱和策略需要参照handler。

注意:

  • 当线程池中工作线程的数量超过corePoolSize时,超出的工作线程空闲时间达到keepAliveTime时,会关闭空闲线程。
  • 当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程(核心线程)空闲时间达到keepAliveTime也将关闭

线程池执行代码:
1.以下是执行ThreadPoolExecutor的execute()方法

       public void execute(Runnable command) {
          if (command == null)
              throw new NullPointerException();
       //如果线程数大于等于基本线程数或者线程创建失败,将任务加入队列
          if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
          //线程池处于运行状态并且加入队列成功
              if (runState == RUNNING && workQueue.offer(command)) {
                  if (runState != RUNNING || poolSize == 0)
                      ensureQueuedTaskHandled(command);
              }
         //线程池不处于运行状态或者加入队列失败,则创建线程(创建的是非核心线程)
              else if (!addIfUnderMaximumPoolSize(command))
           //创建线程失败,则采取阻塞处理的方式
                 reject(command); // is shutdown or saturated
         }
     }

2.创建线程的方法:addIfUnderCorePoolSize(command)

    private boolean addIfUnderCorePoolSize(Runnable firstTask) {
          Thread t = null;
          final ReentrantLock mainLock = this.mainLock;
          mainLock.lock();
          try {
              if (poolSize < corePoolSize && runState == RUNNING)
                  t = addThread(firstTask);
          } finally {
              mainLock.unlock();
          }
           if (t == null)
              return false;
           t.start();
           return true;
     }
     
     private Thread addThread(Runnable firstTask) {
          Worker w = new Worker(firstTask);
          Thread t = threadFactory.newThread(w);
          if (t != null) {
              w.thread = t;
              workers.add(w);
              int nt = ++poolSize;
              if (nt > largestPoolSize)
                  largestPoolSize = nt;
          }
          return t;
      }

三、线程池的优缺点及适用条件

1.适用条件
使用线程池一般需要有两个特点:

  • 单个任务处理时间短
  • 需要处理的任务数量大

2.优点

  • 重用存在的线程,减少对象创建、消亡的开销,提升性能。
  • 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
    提供定时执行、定期执行、单线程、并发数控制等功能。

3.缺点

  • 每次通过new Thread()创建对象性能不佳。
  • 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。
  • 缺乏更多功能,如定时执行、定期执行、线程中断。

四、常用线程池类型及适用场景

Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。

1.newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
适用场景:执行很多短期异步的小程序或者负载较轻的服务器。

2.newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待,表示同一时刻只能有这么大的并发数。
适用场景:执行长期的任务,性能好很多

3.NewScheduledThreadPool:创建一个定时线程池,支持定时及周期性任务执行。
适用场景:在给定延迟后运行命令或者定期地执行任务的场景。

4.newSingleThreadExecutor:创建一个使用单个工作线程的线程池,以无界队列方式来运行该线程。(注意,如果因为在关闭前的执行期间出现失败而终止了此单个线程,那么如果需要,一个新线程将代替它执行后续的任务)。可保证顺序(FIFO, LIFO, 优先级)地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。与其他等效的 newFixedThreadPool(1)不同,可保证无需重新配置此方法所返回的执行程序即可使用其他的线程。
适用场景:按序且逐个执行任务的场景

五、合理的配置线程池的大小

从以下几个角度分析任务的特性:
1.任务的性质:CPU密集型任务、IO密集型任务、混合型任务。

2.任务的优先级:高、中、低。

3.任务的执行时间:长、中、短。

4.任务的依赖性:是否依赖其他系统资源,如数据库连接、网络状况、内存、磁盘大小及读写速度等。

通常情况下(假设cpu总数为N),

  • CPU密集型任务应配置尽可能小的线程,如配置CPU个数+1的线程数,即N+1;
  • IO密集型任务应配置尽可能多的线程,因为IO操作不占用CPU,不要让CPU闲下来,应加大线程数量, 如配置两倍CPU个数+1,即2N+1;
  • 混合型的任务,如果可以拆分,拆分成IO密集型和CPU密集型分别处理,前提是两者运行的时间是差不多的,如果处理时间相差很大,则没必要拆分了。

这是本人对线程池的知识点总结,希望大家指正、补充,谢谢!
------------------------------------------------------------终结线---------------------------------------------------------------------

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值