阻塞队列模型
阻塞队列的接口为BlockingQueue。
①LinkedBlockingQueue
基于链表结构的阻塞队列,可通过重载的构造方法设定最大值。默认是Integer.MAX_VALUE,相当于无界。
②ArrayBlockingQueue
功能类似LinkedBlockingQueue,区别在于它是基于数组来实现的。
③PriorityBlockingQueue
支持优先级调度,无界阻塞队列,排序基于compareTo或者Comparator。
④SynchronousQueue
生产者与消费者之间的传递者,通过它实现生产者和消费者之间的平衡(生产者放入队列的数据在没有消费者获取时会等待,直到有消费者消费数据,这样不会造成生产过快而导致队列中存放大量数据)。
put和take:是会等待的操作。
offer:如果发现没有take操作挂在那里的话,会直接返回false。
put:如果发现没有take挂在那里的话,会一直等待。
take:如果发现没有put操作挂在那里的话,会一直等待。
add:如果发现offer是false,即没有take挂在那里的话,会直接抛出异常。
⑤DelayQueue
支持延迟获取元素,在系统调度中使用较多,或者用于某些条件变量达到要求后需要做的事情。
ThreadPoolExecutor
这是java默认提供的线程池。线程池——可以控制线程创建、释放,并通过某种策略尝试复用线程去执行任务的一个管理框架,实现线程资源与任务的平衡。
可以这样理解,程序需要执行的任务交给线程池的动作类似生产者,线程池执行任务类似消费者,生产消费的模型。
ThreadPoolExecutor参数解析
①corePoolSize:线程池的主要线程数量。有任务提交时,当poolSize<corePoolSize,会直接创建新的线程放到workers中(workers是一个hashset),如果线程数量大于这个值,会尝试放入等待队列workQueue(一个BlockingQueue队列),如果写入等待队列失败,会进入addIfUnderMaximumPoolSize(Runnable)方法做maximumPoolSize的判定和处理。
②maximumPoolSize:进入addIfUnderMaximumPoolSize方法后,发现线程数量小于maximumPoolSize值,会创建线程放入workers中。这是CachedThreadPool的关键,固定线程数的线程池不会出现这种情况。
③workQueue:是一个BlockingQueue队列,BlockingQueue只是一个接口。调用newFixedXXX的时候默认采用LinkedBlockingQueue,调用newCachedXXX的时候,默认使用SynchronousQueue。
④keepAliveTime:线程池中的线程尝试从阻塞队列workQueue中获取数据时会用到,传入参数的单位是秒。
⑤threadFactory,ThreadFactory接口的实现类,默认为DefaultThreadFactory,也可以自己实现。
⑥handler,RejectedExecutionHandler,要实现rejectExecution方法。当addIfUnderMaximumPoolSize失败后,会调用它来处理,即当任务无法放入执行队列,也无法放入等待队列的时候,同时线程数已经大于maximumPoolSize,需要做一个丢弃处理。
RejectedExecutionHandler
丢弃处理的默认几种实现。
①AbortPolicy:该方法直接抛出一个异常。
②DiscardPolicy:该方法是个空方法,外部不会感知到异常,任务自然被丢弃。
③DiscardOldestPolicy:如果线程池没有发生shutdown操作,会尝试从阻塞队列workQueue中取出一个任务,然后通过线程池的execute方法将任务放进去。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();//该任务被无辜的丢掉了
e.execute(r);//如果依然得不到空闲可能会重新进入rejectedExecution导致死递归
}}
如果e.execute(r);的执行依然得不到空闲的位置,那么会不停的进入rejectedExecution方法,不停的尝试,理论上有可能导致StackOverFlow。
④CallerRunsPolicy:如果线程池还没有被shutdown,直接调用任务的run方法,不过并没有开启一个新的线程或者交给线程池调度,该过程直接使用生产者的当前线程来运行。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();//生产者的当前线程运行
}}
ThreadFactory创建线程、Worker、用户任务之间的关系
ThreadFactory(一般用DefaultThreadFactory)通过newThread方法创建target Thread,通过ThreadPoolExecutor的Worker包装,作为Worker的firstTask属性,即firstTask是用户提交的任务。
addIfUnderCorePoolSize
poolSize<corePoolSize,增加线程。
这里有个双重锁判定,在execute方法中,会判断poolSize和corePoolSize,然后在addIfUnderCorePoolSize中再加锁判断一次,这样如果第一次的判定失败,后面的加锁就不会发生了,这样可以减小开销,因为如果没有前一个步骤,每次判定都要用锁。在固定长度的线程池中,初始化时线程池中没有任何线程,线程会不断增加,到达一定程度才会进入这个方法进行加锁的判定,征用概率降低了,开销减小。
addIfUnderMaximumPoolSize
addIfUnderMaximumPoolSize方法会根据workQueue.offer方法的返回值来判定如何操作。如果是固定线程数的线程池,默认情况下队列无界,所以该方法会一直返回true。
如果使用了newCachedThreadPool,默认使用SynchronousQueue,提交任务的生产者如果发现没有任何消费者在等待消费(没有take操作挂在那里),会直接返回false(offer方法返回false)。瞬间并发时,线程大多处于忙碌状态,返回false的概率会非常高。如果返回false,就会执行addIfUnderMaximumPoolSize方法。
ThreadPoolExecutor.Worker
包装用户提交的任务
Runnable getTask() {
for (;;) {
try {
int state = runState;
if (state > SHUTDOWN)
return null;
Runnable r;
if (state == SHUTDOWN) // Help drain queue
r = workQueue.poll();
else if (poolSize > corePoolSize || allowCoreThreadTimeOut)
r = workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS);
else
r = workQueue.take();
if (r != null)
return r;
if (workerCanExit()) {
if (runState >= SHUTDOWN) // Wake up others
interruptIdleWorkers();
return null;
}
// Else retry
} catch (InterruptedException ie) {
// On interruption, re-check runState
}}}
①if (state > SHUTDOWN)
state大于SHUTDOWN的情况有两种,一是当线程池调用了shutdown方法后,线程池状态会被设置为shutdown,然后遍历线程池中所有线程调用一次interrupt,如果休眠中的线程被激活,激活后的线程和以及调用shutdown方法本身的线程都会去尝试调用tryTerminate方法,该方法判定如果线程池中所记录的线程数为0,则将线程池的状态修改为TERMINATED,该值为3,大于shutdown。二是当线程调用了shutdownNow方法,会将线程池的状态改为STOP,该状态是大于shutdown的。
②r = workQueue.poll();
返回阻塞队列的一个元素,如果是newCachedThreadPool,使用SynchronoursQueue,如果没有消费者线程资源空闲时等待,不会停留在阻塞队列中,此时线程尝试通过这个队列去获取元素肯定是无法获取到的,该方法不会阻塞,所以理论上该情况永远返回null。
③r = workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS);
该方法是介于poll和take之间的一个方法,当阻塞队列中没有可获取的对象时,会像take方法那样被挂起,时间达到keepAliveTime后会被自动唤醒,不会永远阻塞沉睡。
④r = workQueue.take();
这个方法内部会不断尝试,直到获取元素后才会返回(发生中断后抛出异常也会打破这种情况,比如shutdown肯定能激活它)。take方法不会因为队列中没有任务就返回空值,通过newFixedThreadPool方式创建的线程池的线程数量默认情况下只增不减,但最大值不会超过corePoolSize。如果是newCachedThreadPool,虽然有超时机制,但是外面是一个死循环,本身在瞬间冲击时线程数量快速膨胀,如果不回收就会有问题。
LinkedBlockingQueue
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
try {
while (count.get() == 0)
notEmpty.await();
} catch (InterruptedException ie) {
notEmpty.signal(); // propagate to a non-interrupted thread
throw ie;
}
x = extract();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
线程的等待发生在while循环中,它在等一个非空信号量,队列为空时将一直等待。当生产者向阻塞队列中放入任务时(add,offer或者put),如果发现当前阻塞队列为空,会调用notEmpty的signal方法来激活一个等待的线程。阻塞的线程获取任务后发现阻塞队列的数量在获取任务之前是大于1的,说明还有剩下的等待任务,会再一次调用notEmpty的signal来激活下一个等待的线程进一步处理。
poll方法源码类似,只是没有while循环去阻塞。
待时间参数的poll方法也会阻塞指定的时间,是take和无参poll的折衷处理方式。
总结一下
对于具有固定大小的线程池初始化是没有任何线程的,也没有不断循环的线程来扫描任务,提交任务的时候会产生线程,即提交任务没有空闲线程处理时会创建线程,但总的线程数不会超过corePoolSize。
默认情况下这些线程只增不减,可以通过改变参数来达到线程回收的目的。
对于固定大小的线程池来说意义不大,我们只希望有请求能够尽快处理,线程只要不做事情,它的开销几乎可以忽略不计。
SynchronousQueue
它是消费者和生产者的平衡者,内在的实现可以是队列或者栈,默认通过栈来实现。