赵云胡说-解密线程池
序
java并发编程中绕不过去的一环就是线程池了,笔者也是随着对技术的深入,从刚开始对其敬畏,到了现在对其钦佩.今天,笔者斗胆尝试去解开线程池它神秘的面纱
FutureTask
桥豆麻袋!不是说线程池嘛,怎么出现了一个不太认识的东西呢.或许你不太认识它,但是我觉得你可能已经在不知不觉中使用过它了.同时本篇是对线程池的一个原理的解析,对线程池总体架构的一个梳理,所以对于线程池,读者需要一定的使用基础(最起码线程池得使用过=_=!)
好了,说正事.当我们使用线程池时,会有一个submit方法呀,submit方法它是会有一个返回值的,这个返回是Future类型的.其实呢,它真正的类型是FutureTask类的.那么为什么future.get()方法,会让阻塞呢?(如果future中无值阻塞知道future有值,如果有值则不会阻塞)
所以FutureTask究竟做了什么神奇的事情呢?且细听我来说到
首先,我们思考下,如果让你来设计这个类,你觉得它应该怎样去设计呢?
它是有阻塞的,然后拿到值是会解开阻塞的.那么是否可以这么思考
while(target == null){
Thread.sleep(100);
}
return target;
我让主线程去无限循环,直到目标中的值不为空.那么这个值不为空该怎么去判定呢?是的,我们的线程池中的线程执行完以后,把最后的值给写入到这个target所指向的堆内地址.
如图所示,最下面的是时间轴,只有当辅线程执行完,主线程才能够继续执行
这样设计是可以实现功能的(大概吧),但是这里无限的自旋加上sleep的系统调用,无疑是非常损耗性能的,所以我们怎么能够忘记高端的AQS框架呢?
private ReentrantLock reentrantLock = new ReentrantLock();
private Condition condition = reentrantLock.newCondition();
public Object get(){
while(target == null){
condition.await();
}
return target;
}
这里我们设置一个条件锁然后在set方法中我们去唤醒它
public void set(Object target){
this.target = target
condition.signal();
}
这样就能够解决性能的问题了.其实FutureTask的思路也是这样的.在get时去阻塞,在set时去唤醒.
同时它也兼顾了多个线程同时去get(),让所有的线程去阻塞的思路,并且在唤醒时将他们全部给唤醒起来.或许是源码设计者觉得这里不需要如此复杂的并发处理,或者是为了更加符合该类,
它的底层其实是自己实现的一个链表,每个节点里面存储着线程的信息和指向下个节点.当使用set方法以后会将改链表的所有的线程都唤醒起来,这样他们就都可以get到存在值得目标值了
这里补充一下, **LockSupport.unpark(Thread thread);可以唤醒线程LockSupport.park(Thread thread)**会阻塞线程所以存了Thread信息就可以唤醒了
线程池之路
至此是否有疑问,线程池跟这个FutureTask有啥米联系呢,其实在线程池的submit方法会返回一个future,而这个future其实就是FutureTask,如果点击进入submit的源码,我们会发现
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
其实就是new了一个FutureTask而已,然后将这个FutureTask交给线程池的execute方法去执行,本节主要叙述的就是线程池里面的execute方法啦.
首先呢,线程池中有几个比较重要的概念.核心线程数,最大线程数,过期时间以及阻塞队列等,在execute方法中涉及的就是.核心线程数,最大线程数,过期时间了,这边大概的分几种情况去细说一下这个线程池的执行步骤吧
场景一: 线程池中线程<核心线程数
此时,线程池是会创建一个线程的,那么这个线程是怎么创建的,它的结构又是怎么样的,让其可以一直存在的呢?
其实,在线程池中创建线程就是创建一个Worker类,这个类是线程池中的一个内部类可以看下它的结构
然后看一下这个类的方法和属性
我们可以看到他是实现了Runnable和AQS接口的,实现了AQS则此worker本身就是一个锁,并且它实现了Runnable,其内部也是有一个Thread的,则说明它也是一个线程.当第一个任务被submit到线程池的时候,线程池便会创建一个worker,
在创建worker的时候会使用线程池的线程池工厂去创建一个线程,该线程执行的就是worker的run方法
这里的worker的run方法在这种情况下是比较简单的,就是去将提交的任务给执行完~~
在执行完任务以后,会阻塞在阻塞队列中(具体怎么阻塞,会在下个小节中交代)
当阻塞队列中有任务提交时,会唤醒所有阻塞的线程,成功拿到任务的worker执行拿到的任务,没有拿到任务的worker继续休眠
场景二: 线程池中线程=核心线程数
当核心线程数已经满了,那么继续提交任务会怎么样呢?此时因为核心线程已经满了,所以导致线程池是不会继续的创建新的worker了,此时被提交的任务会被存储到阻塞队列中.如果此时队列中没有任务,那么入队时会去唤醒阻塞队列中阻塞的所有worker,如果有值则直接入队
场景三: 阻塞队列已满,线程数<最大线程数
当阻塞队列已经满了以后,此时线程池便会去继续地添加worker,这些worker和核心的worker基本上功能是一样的,唯一的不同的就是它有过期时间,那么这个国企时间是怎么去实现的呢,其实也很简单,只是在去取阻塞队列阻塞的时候使用了poll方法,该方法就是取阻塞队列中数据,有就直接取出来,没有就阻塞,但是这个阻塞有时间限制,如果时间过了就会直接返回,之后这个worker会直接执行完,然后线程就会结束了
场景四: 阻塞队列已满,线程数=最大线程数
这个场景下会触发线程池的拒绝策略
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常
ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务
ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务
,如果没有设置线程池的拒绝策略,那么会有默认的拒绝策略是:AbortPolicy
队列的秘密
其实线程池中最后唤醒和阻塞的秘密都是在阻塞队列中的,阻塞队列BlockingQueue
BlockingQueue只是一个接口,它规定了阻塞队列api的一些规则.
在线程池中使用到了队列中的take(),poll(time,unit),put()等方法,这些方法随着不同的具体的阻塞队列的实现类是会有不同的实现的.
可能有的是通过AQS中的条件锁去实现的功能
也可以由Synochronized的wait和notify去实现功能
其中比较主要的关注点
take():就是取阻塞队列中的值,如果有则取出来,没有则会阻塞住线程.并且等待有入队操作唤醒,线程池的核心线程使用该方法去实现等待的
poll(time,unit):就是取阻塞队列中的值,如果有则返回,如果没有则阻塞,如果阻塞则会有一个等待时间,超过这个时间则会出现问题线程池的非核心线程使用该方法去实现等待的
put():就是往一个阻塞队列中加值,如果阻塞队列中本来没有值,则会在添加完后进行唤醒所有线程的操作.
结
线程池的使用是很便利的,了解其中原理也是能够让我们了解到设计的奥妙,以及在以后的开发中更好的使用的.