线程池详解

线程池的详解



一、线程池是什么?

线程池:简单来说就是管理线程的工具池

二、线程池的优点

  • 降低资源的消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应的速度,当任务到达时,可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性,统一管理线程,避免系统创建大量同类线程而导致消耗完内存。

那如果我想要自己创建线程呢,如果我们自己创建线程它会有2个缺点

  • 风险不可控

为什么不可控?
系统资源有限,每个人针对不同业务都可以手动创建线程,并且创建线程没有统一标准,比如创建的线程有没有名字等。当系统运行起来,所有线程都在抢占资源,毫无规则,那自然就不好管控。

  • 频繁创建销毁开销大

我们先要知道线程创建的过程
1.JVM为一个线程栈分配内存,该栈为每个线程方法调用保存一个栈帧
2.每一栈帧由一个局部变量数组、返回值、操作数堆栈和常量池组成
3.每个线程获得一个程序计数器,用于记录当前虚拟机正在执行的线程指令地址
4.系统创建一个与Java线程对应的本机线程将与线程相关的描述符添加到JVM内部数据结构中
5.线程共享堆和方法区域

创建一个线程是需要大概1M左右的空间,那么如果我们频繁销毁创建那消耗可想而知


三、线程池的参数

  • corePoolSize :核心线程
  • maximumPoolSize : 最大线程数
  • BlockingQueue: 存放等待运行的任务队列
  • keepAliveTime :保存存活的时间
  • TimeUnit:时间单位
  • ThreadFactory :线程工厂(可以用来给线程命名)
  • RejectExecutionHandler:拒绝策略
AbortPolicy:默认的策略,直接抛出RejectedExecutionException
DiscardPolicy:不处理,直接丢弃
DiscardOldestPolicy:将等待队列队首的任务丢弃,并执行当前任务
CallerRunsPolicy:由调用线程处理该任务

四、线程池的原理在这里插入图片描述

  • 当线程池中存活的线程数小于核心线程数时,对于新提交的任务,我们会创建一个新的线程去处理任务。当线程池的存活的线程数小于等核心线程数的时候,它会一直存活,即使空闲的时间超出,也不会被销毁,而是被一直阻塞着,等待下一个任务的到来
  • 如果线程池里面的存活线程数已经等于核心线程数,任务队列未满的情况,我们会将它放入任务队列当中
  • 如果线程池里面的存活线程数已经等于核心线程数,任务队列已经满了的情况,我们会先判断是否超过最大线程数,如果没有我们就会创建新的线程来执行任务
  • 最后如果我们的最大线程数也满了,任务队列也满了,那就会采用拒绝策略来解决

线程池的大小设置

  • CPU密集型任务(N+1):这种任务消耗的主要是cpu的资源,可以将线程数设置为N(cpu的核心数)+1,多出来的一个线程是为了防止线程阻塞(如IO操作,释放锁)而带来的影响,一个线程被阻塞,释放了cup的资源,而在这种情况下多出来一个线程就可以充分利用系统的资源
  • I/O密集型任务(2N):系统大部分时间都在处理IO操作,此时线程可能会被阻塞,释放cup的资源,这时就可以将CPU交给其他线程使用。所以在IO密集型任务的应用中,可以多配置一些线程,一般可以设置为2N

五、线程池的类型

- FixedThreadPool(固定线程数线程池)

public static ExecutorService newFixedThreadPool(int nThreads) {
	return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}

使用无界队列 LinkedBlockingQueue(队列容量为 Integer.MAX_VALUE),运行中的线程池不会拒绝任务,即不会调用RejectedExecutionHandler.rejectedExecution()方法。

maxThreadPoolSize 是无效参数,故将它的值设置为与 coreThreadPoolSize 一致。

keepAliveTime 也是无效参数,设置为0L,因为此线程池里所有线程都是核心线程,核心线程不会被回收(除非设置了executor.allowCoreThreadTimeOut(true))。

适用场景:适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。需要注意的是,FixedThreadPool 不会拒绝任务,在任务比较多的时候会导致 OOM(内存用完了)。

- SingleThreadExecutor(只有一个线程的线程池)

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

使用无界队列 LinkedBlockingQueue。线程池只有一个运行的线程,新来的任务放入工作队列,线程处理完任务就循环从队列里获取任务执行。保证顺序的执行各个任务。

适用场景:适用于串行执行任务的场景,一个任务一个任务地执行。在任务比较多的时候也是会导致 OOM。

- CachedThreadPool(根据需要创建新线程的线程池)

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

如果主线程提交任务的速度高于线程处理任务的速度时,CachedThreadPool 会不断创建新的线程。极端情况下,这样会导致耗尽 cpu 和内存资源。

使用没有容量的SynchronousQueue作为线程池工作队列,当线程池有空闲线程时,SynchronousQueue.offer(Runnable task)提交的任务会被空闲线程处理,否则会创建新的线程处理任务。

适用场景:用于并发执行大量短期的小任务。CachedThreadPool允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。

- ScheduledThreadPoolExecutor(定时线程池)

在给定的延迟后运行任务,或者定期执行任务。

使用的任务队列 DelayQueue 封装了一个 PriorityQueue,PriorityQueue 会对队列中的任务进行排序,时间早的任务先被执行(即ScheduledFutureTask 的 time 变量小的先执行),如果time相同则先提交的任务会被先执行(ScheduledFutureTask 的 squenceNumber 变量小的先执行)。

六、怎么判断线程池的任务是不是执行完了?

1、使用线程池的原生函数isTerminated();executor提供一个原生函数isTerminated()来判断线程池中的任务是否全部完成。如果全部完成返回true,否则返回false。

2、使用重入锁,维持一个公共计数。所有的普通任务维持一个计数器,当任务完成时计数器加一(这里要加锁),当计数器的值等于任务数时,这时所有的任务已经执行完毕了。

3、使用CountDownLatch。它的原理跟第二种方法类似,给CountDownLatch一个计数值,任务执行完毕后,调用countDown()执行计数值减一。最后执行的任务在调用方法的开始调用await()方法,这样整个任务会阻塞,直到这个计数值为零,才会继续执行。这种方式的缺点就是需要提前知道任务的数量。

4、submit向线程池提交任务,使用Future判断任务执行状态。使用submit向线程池提交任务与execute提交不同,submit会有Future类型的返回值。通过future.isDone()方法可以知道任务是否执行完成。

  • 54
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值