线程进阶-1 线程池

一.说一下线程池的执行原理

1.线程池的七大核心参数

(1)int corePoolSize:核心线程数。默认情况下核心线程会一直存活,当设置allowCoreThreadTimeout为true时,核心线程也会被超时回收。

(2)int maximumPoolSize:线程池能容纳的最大线程数。maximumPoolSize = 核心线程数 + 救急线程数。

(3)long keepAliveTime:线程闲置的超时时长。当线程闲置超时时,此线程会被回收,释放资源。超时回收默认情况下只针对救急线程。

(4)TimeUnit unit:keepAliveTime的时间单位,如TimeUnit.MILLISECONDS、TimeUnit.SECONDS等。

(5)BlockingQueue<Runnable> workQueue:任务队列。当核心线程数都被占用时,新提交的任务会被放到任务队列中等待。

(6)ThreadFactory threadFactory:线程工厂,为可选参数。

        a.ThreadFactory为一个接口,只提供一个newThread()方法,传入一个Runnable对象,返回一个执行该Runnable的Thread对象。用于定制线程对象的创建。

        b.若未指定ThreadFactory,则线程池会使用默认的ThreadFactory——只是创建Thread执行Runnable,不对线程作额外的处理。

(7)RejectedExecutionHandler handler:拒绝策略,为可选参数

        a.当核心线程、救急线程都被占用,且任务队列已满时,会对新提交的任务执行拒绝策略。

        b.若未指定拒绝策略,则线程池会默认使用AbortPolicy

2.线程池的拒绝策略

(1)AbortPolicy:丢弃新任务,并抛出异常。

(2)DiscardPolicy:丢弃新任务,但不会抛出异常。

(3)DiscardOldestPolicy:丢弃任务队列队头的任务,将新任务插入到任务队列队尾。

(4)CallerRunsPolicy:让调用者线程自己来执行任务。即如果当前线程A向线程池提交了一个任务,且该任务触发了线程池的CallerRunsPolicy拒绝策略,则线程池会将该任务退回给线程A,让线程A来执行该任务。

(5)自定义拒绝策略:实现RejectedExecutionHandler接口,并重写rejectedExecution()方法。

3.线程池的执行原理

对于新提交的任务:

(1)判断核心线程是否有空闲,若有空闲则交由核心线程执行。若核心线程都被占用,则继续判断

(2)判断等待队列是否有空位,若有空位则插入到队尾。若等待队列已满,则继续判断

(3)判断救急线程是否有空闲,若有空闲则交由救急线程执行。若救急线程都被占用,则执行拒绝策略

补充:

(1)核心线程和救急线程每执行完一个任务,都会再去等待队列中取待执行任务继续执行。若等待队列中没有任务,则默认情况下,核心线程进入空闲状态,救急线程开始闲置超时的计时。

(2)当核心线程都被占用且等待队列已满时,才会创建救急线程来执行新任务。被创建的救急线程,会直接忽视等待队列的任务,先去执行新提交的任务;等到新任务执行完,才会开始执行等待队列的任务。

二.线程池中常见的任务队列

1.ArrayBlockingQueue:基于数组结构的有界阻塞队列,任务执行顺序为FIFO

2.LinkedBlockingQueue:基于链表结构的有界阻塞队列,任务执行顺序为FIFO

3.DelayedWorkQueue:是一个优先级队列。在提交任务时可以指定当前任务需要延时多长时间后才执行,DelayedWorkQueue的任务执行顺序为当前队列中指定执行时间最早的任务。

4.SynchronousQueue:不存储元素的阻塞队列。每个线程的插入操作都需要等待另一个线程的移除操作,是线程和线程间的传递。

三.说一下ArrayBlockingQueue和LinkedBlockingQueue的区别

1.底层数据结构

(1)ArrayBlockingQueue底层是数组

(2)LinkedBlockingQueue底层是单向链表

2.容量

(1)ArrayBlockingQueue是强制有界,创建时需要传入一个int值作为容量

(2)LinkedBlockingQueue是默认无界,支持有界。默认容量为Integer.MAX_VALUE,创建时可以传入一个int值作为容量。

3.加锁方式

ArrayBlockingQueue和LinkedBlockingQueue都是使用ReentrantLock加锁。

(1)ArrayBlockingQueue的插入和删除操作都是只加一把锁,同时锁住入队和出队。

(2)LinkedBlockingQueue插入操作锁尾节点,不锁头节点;删除操作锁头节点,不锁尾节点。即在执行入队时可以出队,执行出队时可以入队,效率更高。

四.线程池的execute()和submit()方法的区别

线程池可以通过execute()或submit()方法来提交任务

1.执行对象不同

(1)execute(Runnable command):该方法用于执行Runnable接口的任务

(2)submit(Callable<T> task)、submit(Runnable task)、submit(Runnable task,T result):submit()的三个重载方法表明其既可以执行Callable接口任务也可以执行Runnable接口任务。传入的Runnable接口在内部会先被转化为Callable接口再作为任务执行,因此submit()方法最终的执行对象都是Callable接口。

2.返回值不同

(1)execute()方法执行对象是Runnable接口任务,Runnable的run()方法没有返回值,因此execute()方法也没有返回值。execute()主要用于执行不需要返回结果的任务。

(2)submit()方法最终执行对象都是Callable接口任务,其返回值为Future<T>,可以调用Future的get()方法获取任务执行后的返回值。

3.异常处理不同

(1)execute()提交的任务在执行中遇到异常时,会将异常传递给线程的UncaughtExceptionHandler处理,默认是直接将异常抛出。

(2)submit()提交的任务在执行中遇到异常时,仅仅是停止执行任务,但不会将异常立即抛出,相当于暂时吞掉异常;直到外部调用Future的get()方法尝试获取任务执行的返回值时,才会将异常抛出。即允许延迟处理异常。

五.线程池有哪几种状态?

1.RUNNING:线程池的初态,代表线程池正在处理和接收任务。

2.SHUTDOWN:调用shutdown()方法会进入此状态。表明线程池不再接收新任务,但会继续处理已经提交到等待队列的任务。

3.STOP:调用shutdownNow()方法会进入此状态。表明线程池不再接收新任务,也不会再处理等待队列中的任务,同时还会尝试中断正在执行的任务。

4.TIDYING:过渡态,当线程池的所有任务都已经终止(无论是正常执行完还是通过取消或异常终止),并且所有工作线程都关闭,就会进入此状态。该状态下,线程池会执行terminated()钩子方法。

5.TERMINATED:最终态。当terminated()方法执行完就会进入此状态,代表线程池完全终止。

六.线程池的种类有哪些?

Executors类提供了创建多种线程池的静态方法,常见的有4种:

1.newFixedThreadPool:固定线程数的线程池。

(1)核心线程数=最大线程数,无救急线程。

(2)任务队列为LinkedBlockingQueue,容量为默认的Integer.MAX_VALUE。

(3)适用于任务量已知,相对耗时的任务。

2.newSingleThreadPool:单线程化的线程池。

(1)核心线程数=最大线程数=1,作为唯一的工作线程;无救急线程。

(2)任务队列为LinkedBlockingQueue,容量为默认的Integer.MAX_VALUE。

(3)适用于按照顺序执行的任务。

3.newCachedThreadPool:可缓存线程

(1)核心线程数=0,最大线程数=救急线程数=Integer.MAX_VALUE,即全为救急线程在工作,因此可以灵活创建、回收线程。

(2)任务队列为SynchronousQueue

(3)适用于任务数比较密集,但每个任务执行时间较短的情况。

4.newScheduledThreadPoolExecutor:具有延迟和周期执行功能的线程池。

(1)核心线程数自定义,最大线程数=Integer.MAX_VALUE。

(2)任务队列为DelayedWorkQueue,使用schedule()提交任务,指定任务的延时执行时间。

七.为什么不建议使用Executors类提供的方法创建线程?

1.FixedThreadPool和SignalThreadPool都允许任务队列的长度为Integer.MAX_VALUE,可能会堆积大量的任务请求,导致OOM。

2.newCachedThreadPool和newScheduledThreadPool的最大线程数都为Integer.MAX_VALUE,可能会创建大量的线程,导致OOM。

八.如何控制某个方法允许并发访问线程的数量?

1.使用newFixedThreadPool,但有OOM的风险。

2.使用Semaphore类,每一个线程访问该方法都acquire()获取一个信号量,若信号量数大于0,则该线程获取到一个信号量,信号量数减1;若信号量数小于0,则该线程阻塞;若线程执行完该方法,则release()释放一个信号量,信号量数加1。

九.如何确定线程池的核心线程数?

假设cpu的核数为N

1.对于高并发、执行时间短的任务:需要减少线程数以减少线程上下文切换带来的消耗。因此核心线程数设置为N+1。

2.对于并发不高、任务执行时间长的任务:

(1)IO密集型任务:例如文件读写、DB读写、网络请求等,不需要占用过多cpu。核心线程数设置为2N+1。

(2)cpu密集型任务:例如计算型代码、Bitmap转换、Gson转换等,需大量占用cpu,需要减少线程数以减少线程上下文切换带来的消耗。因此核心线程数设置为N+1。

十.说一下线程池的使用场景

1.数据批量导出

(1)使用线程池+CountDownLatch批量将数据库中的数据导出,避免OOM。

(2)场景:例如使用分页查询去导出数据库中的数据,线程池的每个线程负责一页数据的导出,并发执行,每有一个线程执行完,CountDownLatch减一,直到所有线程执行完,CountDownLatch等于零,主线程才继续执行。

2.数据汇总

(1)如果一个请求要调用多个接口来汇总数据,且接口间没有依赖关系,则可以使用线程池+Future获取调用结果的方式来并发执行,提升性能。

3.异步线程

(1)为了避免下一级方法影响上一级方法响应时间,可以使用异步线程并发调用下一级方法(前提是不需要下一级方法的返回值),提高响应速率。

  • 10
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值