java特种兵读书笔记(5-6)——并发之线程池与调度池

来源:互联网 发布:java 接口文档编写 编辑:程序博客网 时间:2024/05/17 23:07

阻塞队列模型


阻塞队列的接口为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


它是消费者和生产者的平衡者,内在的实现可以是队列或者栈,默认通过栈来实现。


0 0
原创粉丝点击