深入理解java中的线程池
文章目录
- 深入理解java中的线程池
- 1、线程池ThreadPoolExecutor的实现原理?
- 2、Executor框架
- 3、java提供了ExecutorService的三种实现:
- 3.1、ThreadPoolExecutor:标准线程池
- 3.2、ScheduledThreadPoolExecutor:支持延迟任务的线程池
- 3.3、ForkJoinPool:(分支/合并框架)java7引入
- 4、常见的线程池: 线程池的接口类是Executors,有一些静态方法
- 5、如何合理地配置线程池
- 6、使用线程池时遇到过的问题:
- 7、ExecutorService使用: 类似于一个线程池
- 8、线程池的启动策略/增长策略?(蚂蚁金服)***
- 9、sumbit()内部实现?
- 10、线程池的状态 5种
- 11、线程池的监控:
- 12、JAVA多线程之线程间的通信方式?**
先看几道多线程相关的问题
1、三个线程 a、b、c并发运行,b,c线程需要a的数据怎么实现?
难点:
1、是让 ThreadB 和 ThreadC 等待 ThreadA 先执行完
2、 ThreadA 执行完之后给ThreadB和ThreadC发送消息
思路:我们必须让ThreadB和ThreadC去等待ThreadA完成任务后发出的消息
并使用Semaphore类来控制线程的等待acquire和释放release()方法释放permit
摘要:
java中的线程池时运用场景最多的并发框架,几乎所有需要一部或并发执行任务的程序都可以使用线程池。
使用线程池带来的好处:
1、降低资源消耗;2、提高响应速度;3、提高线程的可管理性(进行统一分配、调优和监控)
下面对线程池中知识点逐一剖析
1、线程池ThreadPoolExecutor的实现原理?
通过减少频繁创建和销毁线程来降低性能损耗。每个线程都需要一个内存栈,用于存储诸如局部变量、操作栈等信息,可以通过-Xss参数来调整每个线程栈大小(1024k)
线程池一般配合队列一起工作,使用线程池限制并发处理任务的数量,然后设置队列的大小,当任务超过队列大小时,创建新线程来处理任务,如果当前线程超出maximumPoolSize,任务将被拒绝,通过一定的拒绝策略来处理,这样可以保护系统免受大流量而导致崩溃。
特点:线程池一般有核心池大小和线程池最大大小设置,当线程池中的线程空闲一段时间后会被回收,而核心线程池中的线程不会被回收。
2、Executor框架
主要由3大部分组成
任务 | 执行任务需要实现的接口:runnable接口和callable接口 |
---|---|
任务的执行 | 核心接口Executor,以及继承自Executor的executorService 接口,两大实现类ThreadPoolExecutor和scheduledThreadpoolExecutor |
异步计算的结果 | 包括接口Future和实现类Futuretask |
3、java提供了ExecutorService的三种实现:
3.1、ThreadPoolExecutor:标准线程池
序号 | 参数 | 含义 |
---|---|---|
1 | corePoolSize | 核心线程池大小,线程池维护的线程最小大小,即没有任务处理情况下,线程池可以有多个空闲线程(少于corePoolSize的一直创建线程,即使有线程空闲;空闲线程不释放,任务数多于corePoolSize的任务放入阻塞队列) |
2 | maximumPoolSize | 线程池最大大小 |
3 | keepAliveTime | 线程池中线程的最大空闲时间,存活时间超过该时间的线程会被回收。 |
4 | workqueue | 线程池使用的任务缓冲队列(包括:有界阻塞队列 ArrayBlockingQueue, //有界阻塞队列需要设置合理的队列大小;有界/无界阻塞链表队列 LinkedBlockingQueue;优先级阻塞队列 PriorityBlockingQueue;无缓冲区阻塞队列 SynchronousQueue) |
5 | ThreadFactory | 创建线程的工厂,我们可以设置线程的名字,是否是后台线程;作用:统一创建线程时的参数,如是否守护线程 |
6 | rejectedExecutionHandler | (京东面试题,蚂蚁金服)当缓冲队列满后的拒绝策略,包括:1、Abort(直接抛出 rejectedExecutionException);2、Discard(按照LIFO丢弃)新任务被抛弃;3、DiscardOldest(按照LRU丢弃)旧任务被抛弃;4、CallerRuns(主线程执行)在调用者的线程中运行新的任务,既不抛弃任务也不抛出异常;5、自定义策略(用于记录日志或持久化存储不能处理的任务) |
3.2、ScheduledThreadPoolExecutor:支持延迟任务的线程池
使用executors来创建,两种子类(此线程池使用较少)
1、scheduledthreadpoolExecutor
2、singleThreadscheduledExecutor
3.3、ForkJoinPool:(分支/合并框架)java7引入
类似于ThreadPoolExecutor,就是在必要的情况下,将一个大任务,进行拆分(fork)成若干个小任务(拆到不可再拆时),再将一个小任务运算的结果进行join汇总。
可以使用work-stealing模式,其会为线程池中的每个线程创建一个队列,用work-stealing(任务窃取)算法使得线程可以从其他线程任务里窃取任务来执行。即如果自己的任务处理完成了,则可以去忙绿的工作线程那里窃取任务执行。
fork/join框架与线程池的区别
1、采取的是使用work-stealing模式
2、一般的线程池中,如果一个线程正在执行的任务由于某些原因无法继续运行,那么该线程会处于等待状态。而在fork/join框架中,如果某个子问题由于等待另一个子问题的完成而无法继续运行。那么处理该子问题的线程会主动寻找其他尚未运行的子问题来执行,这种方式减少了线程的等待时间,提高了性能。
3、任务的类型:
1、没有任何返回值,不需要join,使用recursiveAction
比如写数据到磁盘,然后就退出(一个 RecursiveAction可以把自己的工作分割成更小的几块,这样它们可以由独立的线程或者 CPU 执行)
2、任务有返回值:使用recursiveTask
子任务的执行结果合并到一个集体结果
4、常见的线程池: 线程池的接口类是Executors,有一些静态方法
线程池名称 | 简介 | 详解 | 特点 |
---|---|---|---|
1、newFixedThreadpool | 请求固定的线程 | 等价于 return new ThreadPoolExecutor(nThreads,nThreads,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue())说明:指定线程数,使用LinkedBlockingQueue 链表阻塞队列 | 特点:当线程池没有可执行任务时,线程空闲不释放***由于使用的是无界队列,队列原则上不会限制队列大小,以至于线程池中的任务不会超过corePoolSize |
2、newSingleThreadpool | 单个线程 | ThreadPoolExecutor(1,1,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue())说明:初始化只有一个线程,内部使用 LinkedBlockingQueue 阻塞队列 | 特点:保证所提交任务的顺序执行,如果该线程异常结束,会重新创建一个新的线程继续执行任务 |
3、newCachedThreadpool | 可缓存的线程池 | 线程数量不固定 最大线程数量 Integer.MAX_VALUE,其使用SynchronousQueue队列,一个没有数据缓冲的阻塞队列。对其执行put操作后,必须等待take操作消费该数据,反之亦反,等价于 new ThreadPoolExecutor(0,Integer.MAX_VALUE,60L,TimeUnit.SECONDS,new SynchronousQueue()); | 特点:1、在没有任务执行时,线程的空闲时间超过keepAliveTime 60s,会自动释放线程资源;2、当提交新任务时,复用未超过60s的空闲线程,若没有空闲线程,则创建新线程;3、SynchronousQueue是没有容量的阻塞队列/每一个put操作必须要等待一个线程的take操作;缺点:使用时注意控制并发的任务数,防止因创建大量的线程导致而降低性能 |
4、newScheduledThreadPool() | 支持延迟执行的线程池 | 使用delayedWorkQueue实现任务延迟。比timer更强大,因为timer对应的是单个后台程序,而ScheduledThreadPool可以在构造函数中指定多个线程scheduleExecutorservice ses = Executors.newScheduledThreadPool(10);//使用定时执行风格的方法 ;pool.schedule(t4, 10, TimeUnit.MILLISECONDS); //t4 在 10 秒后执行 | delayQueue详解:封装了priorityQueue,这会对队列中的scheduledFutureTask进行排序,time小的排前面先被执行。time相等比较sequenceNumber;获取任务:获取lock,获取周期任务,释放lock |
线程池中,线程对象的两种实现方式: 创建Runnable接口子类对象 重写run方法 创建callable接口子类对象 重写call方法
线程池执行后的返回值:
返回Executorservice接口,接口的对象可以调用submit方法来执行线程池中的线程
5、如何合理地配置线程池
根据任务类型是IO密集型还是cpu密集型、任务的优先级,任务的执行时间,任务是否依赖其他系统资源,来设置合理的线程池大小、队列大小、拒绝策略,并进行压测和不断调优来决定适合自己场景的参数。
1、cpu密集型 经常进行上下文切换,因此配置尽可能小的线程
2、IO密集型 因为经常进行IO操作,可以分配多一点线程
3、对于优先级不同的任务 使用优先级队列PriorityBlockingQueue,让优先级高的任务先执行
4、执行时间不同的任务 可以使用不同规模的线程池/使用优先级队列,耗时短的先执行
5、依赖数据库连接池的任务 线程数应该设置大点
6、建议使用有界队列 增加系统的稳定性
6、使用线程池时遇到过的问题:
1、maximumPoolSize设置过大导致瞬间线程数非常多,若是进行线程的上下文切换,会消耗相当多的资源;
2、在使用Executors.newFixedThreadPool时,因没有设置队列大小,默认为integer.MAX_VALUE,如果有大量任务被缓存到LinkedBlockingQueue中等待线程执行,则会出现GC慢等问题,造成系统响应慢甚至OOM,因此,在使用线程池时务必设置池大小,队列大小并设置相应的拒绝策略。
3、在java的多线程中,一旦线程关闭,就会成为死线程。关闭后死线程就没有办法再启动了。再次启动就会出现异常信息:Exception in thread “main” java.lang.IllegalThreadStateException。
那么如何解决这个问题呢?
可以使用Executors.newSingleThreadExecutor()来再次启动一个线程。
4、我们后台任务线程池的队列和线程池全满了,不断抛弃任务的异常,经过排查,发现是数据库出现了问题,导致sql执行的非常慢
5、线上问题定位
线上问题定位就只能看日志、系统状态和dump线程。
1、在linux命令行下使用top命令查看每个进程的情况
关注command是java的性能数据 使用top的交互命令数字1查看每个CPU的性能数据
H:查看每个线程的性能信息
会出现3中情况:
1、cpu利用率100%,说明这个线程可能死循环 可能是GC造成,可以使用jstat命令查看GC情况
2、一直在top10,说明线程可能有性能问题
3、cpu利用率高的几个线程在不停变化,说明并不是由某一个线程导致CPU偏高
7、ExecutorService使用: 类似于一个线程池
1、作用:
一个线程将一个任务委派给一个 ExecutorService 去异步执行。
一旦该线程将任务委派给 ExecutorService,该线程将继续它自己的执行,独立于该任务的执行
2、 几种不同的方式来将任务委托给 ExecutorService。
方法 | 特点 |
---|---|
1、execute(Runnable) | 没有办法得知被执行的结果 |
2、submit(Runnable) | 返回一个 Future 对象;这个Future对象可以用来检查Runnable是否已经执行完毕 //但new Runnable()无法返回数据信息 |
3、submit(Callable) | 返回一个Future对象;new Callable()可以返回数据信息;可判断当前的线程是否执行完毕 |
4、invokeAny() | |
5、invokeAll() |
一个任务可能会由于一个异常而结束,因此它可能没有 "成功"。
8、线程池的启动策略/增长策略?(蚂蚁金服)***
1、线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
2、当调用execute() 方法添加一个任务时,线程池会做如下判断:
1、如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
2、如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
3、如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建线程运行这个任务
4、如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常,告诉调用者“我不能再接受任务了
3、当一个线程完成任务时,它会从队列中取下一个任务来执行。
4、当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉;
所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
9、sumbit()内部实现?
1.将提交的Callable任务会被封装成了FutureTask对象
2.FutureTask类也实现了Runnable接口,通过Executor.execute()提交到线程池,执行run方法;最终返回FutureTask对象
比较:两个方法都可以向线程池提交任务
- execute()方法的返回类型是void,它定义在Executor接口中
- submit()方法可返回持有计算结果的Future对象;定义在ExecutorService接口中
3、FutureTask详解
作用 | 代表异步计算的结果;Futuretask实现了Future接口和Runnable接口,因此,futuretask可以交给executor执行,也可以由调用线程直接执行。 |
---|---|
什么时候使用? | 当一个线程需要等待另一个线程把某个任务执行完后他才能继续执行,此时可以使用futureTask |
实现原理 | 基于AQS(AQS是一个同步框架,他提供通用机制来原子性管理同步状态、阻塞和唤醒线程,以及维护被阻塞线程的队列)基于AQS实现的同步器包括:reentrantLock,semaphore,reentrantReadWriteLock,CountDownLatch和futureTask |
同步器的两种类型的操作 | 1、acquire操作。这个操作阻塞调用线程,直到AQS的状态允许这个线程继续执行;Futuretask的acquire操作为get()/get(long timeout,TimeUnit unit);2、release操作。改变AQS的状态,改变后的状态可允许一个或多个阻塞线程被解除阻塞,Futuretask的release操作包括run()和cancel()方法 |
10、线程池的状态 5种
状态 | 特点 |
---|---|
1、运行RUNNING | 会接收新任务、处理阻塞队列中的任务; |
2、关闭SHUTDOWN | 不会接收新任务,会处理阻塞队列中的任务; |
3、停止STOP | 不接收,不处理任务,会中断正在运行的任务; |
4、休息TIDYING | 对线程进行整理优化; |
5、终止TERMINATED | 停止工作; |
shutdown()和shutdownNow()区别
原理:遍历线程池中的工作线程,然后逐一调用线程的interrupt方法来中断线程
区别:
shutdown():不立即终止线程池,要等所有任务队列中的任务都执行完后才终止,不会接受新的任务 //状态设置为STOP
shutdownNow():立即终止线程池,中断正在执行的任务,清空任务缓存队列,返回尚未执行的任务 //状态设置为SHUTDOWN
11、线程池的监控:
可以通过线程池提供的参数进行监控,有如下属性:
1、taskCount:线程池需要执行的任务数量
2、copmpletedTaskCount:线程池在运行过程中已完成的任务数量,小于等于taskCount
3、largestPoolSize:曾经创建过的最大线程数量
4、getPoolSize:线程池的线程数量,如果线程池不销毁,这个值只增不减
5、getActiveCount:获取活动的线程数。
如何使用:
通过继承线程池来实现自定义线程池,重写线程池的beforeExecute、afterExecute和terminated方法
也可以在任务执行前后执行一些代码来进行监控
12、JAVA多线程之线程间的通信方式?**
1、同步
这里讲的同步是指多个线程通过synchronized关键字这种方式来实现线程间的通信(本质上就是“共享内存”式的通信);多个线程需要访问同一个共享变量,谁拿到了锁(获得了访问权限),谁就可以执行
例如:线程B需要等待线程A执行完了methodA()方法之后,它才能执行methodB()方法。这样,线程A和线程B就实现了 通信
2、while轮询的方式
线程A不断地改变条件,线程ThreadB不停地通过while语句检测这个条件(list.size()==5)是否成立 ,从而实现了线程间的通信。但是这种方式会浪费CPU资源
轮询的条件的可见性问题:线程都是先把变量读取到本地线程栈空间,然后再去再去修改的本地变量。因此,如果线程B每次都在取本地的条件变量,那么尽管另外一个线程已经改变了轮询的条件,它也察觉不到,这样可能会造成死循环
3、wait/notify机制**
4、管道通信
就是使用java.io.PipedInputStream 和 java.io.PipedOutputStream进行通信
分布式系统中说的两种通信机制:共享内存机制和消息通信机制
①中的synchronized关键字和②中的while轮询 “属于” 共享内存机制,由于是轮询的条件使用了volatile关键字修饰时,这就表示它们通过判断这个“共享的条件变量“是否改变了,来实现进程间的交流
管道通信,更像消息传递机制,也就是说:通过管道,将一个线程中的消息发送给另一个
5、Exchanger(线程间交换数据)
提供了在线程间交换数据的一种手段,它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,他会一直等待第二个线程也执行此方法,当两个线程都到达同步点时,这两个线程就交换数据。
- 应用场景:
1、用于遗传算法:选两个人作为交配对象,需要交换两人的数据,并使用交叉规则得出2个交配结果
2、用于校对工作:我们需要将纸质银行流水通过人功能的方式录入成电子银行流水,为避免错误,采用AB岗录入,对两个excel数据进行校对,看是否录入一致;可以使用exchange(V x,long timeout,TimeUnit unit) //设置最大等待时长