JUC(4)---java线程池原理及源码分析
线程池,既然是个池子里面肯定就装很多线程。
如果并发的请求数量非常多,但每个线程执行的时间很短,这样就会频繁的创建和销毁 线程,如此一来会大大降低系统的效率。可能出现服务器在为每个请求创建新线程和销毁线 程上花费的时间和消耗的系统资源要比处理实际的用户请求的时间和资源更多。因此Java中提供线程池对线程进行统一的管理使用。
线程池可以让多个任务重用线程。减少线程创建,消亡的开销,提高性能。当任务达到不需要等待线程创建便可立即执行。提高线程的可管理性。使用线程池进行统一的分配,调优和监控。
Executor框架体系:
Executor接口是线程池框架中最顶级的接口,定义了一个用于执行Runnable的execute方法
体系简要类图:
ExecutorService也是一个重要的接口,其中也定义了一系列重要的方法:
- submit(task):可用来提交Callable或Runnable任务,并返回代表此任务的Future 对象
- shutdown():在完成已提交的任务后封闭办事,不再接管新任务
- shutdownNow():停止所有正在履行的任务并封闭办事。
- isTerminated():测试是否所有任务都履行完毕了。
- isShutdown():测试是否该ExecutorService已被关闭。.
- invokeXXX(task,...):执行给定任务
从类图中可以看到一个很重要的实现类就是ThreadPoolExecutor,我们通过这个类看下线程池中比较重要的一些属性。
RUNNING:ctl高三位111,可以接受新任务,并且处理已经添加的任务。线程池的初始化状态就是running。
SHUTDOWN:ctl高三位000,不接受新任务,但是可以处理已经添加的任务。调用shutdown()方法,由running变成shutdown
STOP:ctl高三位001,不接受新任务,不处理已添加的任务,并且还会中断正在处理的任务。调用shutdownNow()方法,由running/shutdown变成stop
TIDYING:ctl高三位010,当所有的任务已终止,ctl记录的任务数量为0,线程池会变为tidying 状态。当线程池变为tidying 状态时,会执行钩子函数terminated()。terminated()在 ThreadPoolExecutor类中是空的,若用户想在线程池变为tidying 时,进行其他处理;可以通过重写terminated()。线程池为shutdown状态,并且阻塞队列是空的,并且执行的任务也是空就会由shutdown变成tidying;stop状态时,线程池中任务为空也会变成tidying
TERMINATED:ctl高三位011,线程池彻底凉凉了
当线程池处于tidying状态并且执行完了terminated()方法,就会由tidying变成terminated
构造方法:
corePoolSize:线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当 前线程数等于corePoolSize;如果当前线程数为corePoolSize,继续提交的任务被保存到 阻塞队列中,等待被执行;如果执行了线程池的prestartAllCoreThreads()方法,线程池会 提前创建并启动所有核心线程。
maximumPoolSize:线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize;
keepAliveTime:线程池维护线程所允许的空闲时间。当线程池中的线程数量大于corePoolSize的时候,如果这时没有新的任务提交,非核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime。核心现成如果allowCoreThreadTimeOut字段设置为true,没有任务执行是也会根据此时间来销毁核心线程。
unit:keepAliveTime的单位。
workQueue:用来保存等待被执行的任务的阻塞队列,且任务必须实现Runable接口,在JDK中提供了如下阻塞队列:
1、ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO排序任务;
2、LinkedBlockingQuene:基于链表结构的阻塞队列,按FIFO排序任务,
3、SynchronousQuene:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态。
4、priorityBlockingQuene:具有优先级的无界阻塞队列;
threadFactory :ThreadFactory,用来创建新线程。默认使用 Executors.defaultThreadFactory() 来创建线程。使用默认的ThreadFactory来创建线程时,会使新创建的线程具有相同的NORM_PRIORITY优先级并且是非守护线程,同时也设置了线程的名称。
Handler:线程池的拒绝策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:
1、AbortPolicy:直接抛出异常,默认策略;
2、CallerRunsPolicy:用调用者所在的线程来执行任务(如果调用的线程还存在,如果不存在则丢弃任务)
3、DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
4、DiscardPolicy:直接丢弃任务;
上面的4种策略都是ThreadPoolExecutor的内部类。 当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义策略。
通过execute或者submit向线程池提交任务。任务提交的时候先提交给核心线程(corePoolSize);如果核心线程满了,就将任务放到workQueue里面去排队等待;如果队列也满了(取决用的什么队列,以及设置的大小),就会将新进来的任务提交给非核心线程,非核心线程数量等于maximumPoolSize - corePoolSize,非核心线程使用之后会被回收。如果非核心线程也满了,那么就执行相应的拒绝策略RejectedExecutionHandler。
源码解析:
1.execute方法:
2. 可以看到关键方法是addWork方法,可以看到在addWork方法先会进行一系列判断,如果都通过了,才会进行任务的创建
3.Worker内部类,这类是继承了AbstractQueuedSynchronizer并且实现了Runable接口,其中还有两个重要属性一个是Thread, 一个是firstTask,初始化的时候会吧AQS中的state字段设置-1,后面允许中断会将这个值修改。
会通过线程工厂创建一个线程和当前的worker绑定,创建线程的runable接口对象就是work本身。worker重写的run方法实际调用了线程池的runWorker方法
4. 回到addWorker方法,创建完了worker,可以就可以获取到绑定的线程了,将worker添加到工作线程的集合中去。然后调用对象绑定线程的start方法,实际上会调用到worker的run方法,进而调用线程池的runWorker方法
5. runWorker 方法中会去worker中取任务,如果firstTask空,就去队列中取,因为之前在addwork的时候有些场景传入的firstWork是null。while循环会一直从队列中取任务来执行,等于说这个线程一直在被重复利用了,从队列取不到任务了,也就是getTask返回null了,结束while循环,调用processWorkerExit方法移除任务,处理最终的一些状态转换,或者是队列中还有任务继续调用addwork方法,执行任务,不过这个时候,不需要传入任务
就是说通过线程调用worker的run方法,然后借助worker里面再来调用我们传入线程池中的任务的run方法。所以说其实是起了一些线程,然后调用run方法一直尝试去取任务,取到之后手动调用的run方法执行任务。