并发编程02
Java线程池是如何实现线程复用的呢
在线程池中,线程会从 workQueue 中读取任务来执行,最小的执行单位就是 Worker,Worker 实现了 Runnable 接口,重写了 run 方法,这个 run 方法是让每个线程去执行一个循环,在这个循环代码中,去判断是否有任务待执行,若有则直接去执行这个任务,因此线程数不会增加。
以春运形象来解释参数:
corePoolSize:表示常驻核心线程数,如果大于0,即使本地任务执行完也不会被销毁。
日常固定的列车数辆(不管是不是春运,都要有固定这些车次运行)。
maximumPoolSize:表示线程池能够容纳可同时执行的最大线程数。
春运客流量大,临时加车,加车后,总列车次数不能超过这个最大值,否则就会出现调度不开等问题。
keepAliveTime:表示线程池中线程空闲的时间,当空闲时间达到该值时,线程会被销毁,只剩下 corePoolSize
个线程位置。
春运压力过后,临时的加车(如果空闲时间超过
keepAliveTime
)就会被撤掉,只保留日常固定的列车车次数量用于日常运营。
workQueue:当请求的线程数大于 maximumPoolSize
时,线程进入该阻塞队列。
春运压力异常大,即便加车后(达到
maximumPoolSize
)也不能满足要求,所有乘坐请求都会进入该阻塞队列中排队。
unit:keepAliveTime
的时间单位,最终都会转换成【纳秒】,因为CPU的执行速度杠杠滴。
keepAliveTime
的单位,春运以【天】为计算单位。
threadFactory:顾名思义,线程工厂,用来生产一组相同任务的线程,同时也可以通过它增加前缀名,虚拟机栈分析时更清晰。
比如(北京——上海)就属于该段列车所有前缀,表明列车运输职责。
handler:执行拒绝策略,当 workQueue 达到上限,同时也达到 maximumPoolSize 就要通过这个来处理,比如拒绝,丢弃等,这是一种限流的保护措施。
当workQueue排队也达到队列最大上线,maximumPoolSize 就要提示无票等拒绝策略了,因为我们不能加车了,当前所有车次已经满负载。
1、首先会判断线程池的状态,也就是是否在运行,若线程为非运行状态,则会拒绝。
2、接下来会判断线程数是否小于核心线程数,若小于核心线程数,会新建工作线程并执行任务,随着任务的增多,线程数会慢慢增加至核心线程数,
3、如果此时还有任务提交,就会判断阻塞队列 workQueue 是否已满,若没满,则会将任务放入到阻塞队列中,等待工作线程获得并执行,
4、如果任务提交非常多,使得阻塞队达到上限,会去判断线程数是否小于最大线程数 maximumPoolSize,若小于最大线程数,线程池会添加工作线程并执行任务,
5、如果仍然有大量任务提交,使得线程数等于最大线程数,如果此时还有任务提交,就会被拒绝。
线程池的任务提交从 submit 方法来说,submit 方法是 AbstractExecutorService 抽象类定义的,主要做了两件事情:
- 把 Runnable 和 Callable 都转化成 FutureTask
- 使用 execute 方法执行 FutureTask
execute 方法是 ThreadPoolExecutor 中的方法,execute 方法中的的核心方法为 addWorker。
上述方法的核心主要就是addWorker方法,「work」类实现了「Runnable」接口,然后run方法里面调用了「runWorker」方法,这个runwork方法中会优先取worker绑定的任务,如果创建这个worker的时候没有给worker绑定任务,worker就会从队列里面获取任务来执行,执行完之后worker并不会销毁,而是通过while循环不停的执行getTask方法从阻塞队列中获取任务调用task.run()来执行任务,这样的话就达到了线程复用的目的。
while (task != null || (task = getTask()) != null) 这个循环条件只要getTask 返回获取的值不为空这个循环就不会终止, 这样线程也就会一直在运行。「那么任务执行完怎么保证核心线程不销毁?非核心线程销毁?」答案就在这个**getTask()**方法里面
只要timed为false这个workQueue.take()就会一直阻塞,也就保证了线程不会被销毁。timed的值又是通过allowCoreThreadTimeOut和正在运行的线程数量是否大于coreSize控制的。
只要getTask方法返回null 我们的线程就会被回收runWorker()方法会调用processWorkerExit()
这个方法的源码也就解释了为什么我们在创建线程池的时候设置了allowCoreThreadTimeOut =true的话,核心线程也会进行销毁。
通过这个方法我也们可以回答上面那个问题线程池是不区分核心线程和非核心线程的。
可以看到,使用线程池不但能完成手动创建线程可以做到的工作,同时也填补了手动线程不能做到的空白。归纳起来说,线程池的作用包括:
- 利用线程池管理并服用线程,控制最大并发数(手动创建线程很难得到保证)
- 实现任务线程队列缓存策略和拒绝机制
- 实现某些与实践相关的功能,如定时执行,周期执行等(比如列车指定时间运行)
- 隔离线程环境,比如,交易服务和搜索服务在同一台服务器上,分别开启两个线程池,交易线程的资源消耗明显要大。因此,通过配置独立的线程池,将较慢的交易服务与搜索服务个离开,避免个服务线程互相影响
禁止使用Executors创建线程池
相信很多人都看到过这个问题(阿里巴巴Java开发手册说明禁止使用 Executors 创建线程池):
Executors 大大的简化了我们创建各种类型线程池的方式,为什么还不让使用呢?
其实,只要你打开看看它的静态方法参数就会明白了
传入的workQueue 是一个边界为 Integer.MAX_VALUE 队列,我们也可以变相的称之为无界队列了,因为边界太大了,这么大的等待队列也是非常消耗内存的。
另外该 ThreadPoolExecutor方法使用的是默认拒绝策略(直接拒绝),但并不是所有业务场景都适合使用这个策略,当很重要的请求过来直接选择拒绝显然是不合适的。
总的来说,使用 Executors 创建的线程池太过于理想化,并不能满足很多现实中的业务场景,所以要求我们通过 ThreadPoolExecutor来创建,并传入合适的参数。
线程的生老病死
Java 中线程共有六种状态,分别是:
1.NEW(初始化状态) 2.RUNNABLE(可运行 / 运行状态)
3.BLOCKED(阻塞状态) 4.WAITING(无时限等待)
5.TIMED_WAITING(有时限等待) 6.TERMINATED(终止状态)
1、RUNNABLE 与 BLOCKED 的状态转换
只有一种场景会触发这种转换,就是线程等待 synchronized 的隐式锁。
2、RUNNABLE 与 WAITING 的状态转换
总体来说,有三种场景会触发这种转换。
第一种场景,获得 synchronized 隐式锁的线程,调用无参数的 Object.wait() 方法。
第二种场景,调用无参数的 Thread.join() 方法。
第三种场景,调用 LockSupport.park() 方法。其中的 LockSupport 对象,也许你有点陌生,其实 Java 并发包中的锁,都是基于它实现的。
3、RUNNABLE 与 TIMED_WAITING 的状态转换
有五种场景会触发这种转换:
1.调用带超时参数的 Thread.sleep(long millis) 方法;
2.获得 synchronized 隐式锁的线程,调用带超时参数的 Object.wait(long timeout) 方法;
3.调用带超时参数的 Thread.join(long millis) 方法;
4.调用带超时参数的 LockSupport.parkNanos(Object blocker, long deadline) 方法;
5.调用带超时参数的 LockSupport.parkUntil(long deadline) 方法。
这里你会发现 TIMED_WAITING 和 WAITING 状态的区别,仅仅是触发条件多了超时参数。
4、从 NEW 到 RUNNABLE 状态
Java 刚创建出来的 Thread 对象就是 NEW 状态,而创建 Thread 对象主要有两种方法。
一种是继承 Thread 对象,重写 run() 方法。
public class MyThread extends Thread {
@Override
public void run() {
// 线程需要执行的代码
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
// 创建线程对象
MyThread myThread = new MyThread();
}
}
另一种是实现 Runnable 接口,重写 run() 方法,并将该实现类作为创建 Thread 对象的参数。示例代码如下:
public class Runner implements Runnable {
@Override
public void run() {
// 线程需要执行的代码
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
// 创建线程对象
Thread thread = new Thread(new Runner());
}
}
NEW 状态的线程,不会被操作系统调度,因此不会执行。Java 线程要执行,就必须转换到 RUNNABLE 状态。从 NEW 状态转换到 RUNNABLE 状态很简单,只要调用线程对象的 start() 方法就可以了,示例代码如下:
public class Runner implements Runnable {
@Override
public void run() {
// 线程需要执行的代码
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
// 创建线程对象
Thread thread = new Thread(new Runner());
// 从 NEW 状态转换到 RUNNABLE 状态
thread.start();
}
}
5、从 RUNNABLE 到 TERMINATED 状态
**线程执行完 run() 方法后,会自动转换到 TERMINATED 状态,**当然如果执行 run() 方法的时候异常抛出,也会导致线程终止。有时候我们需要强制中断 run() 方法的执行,例如 run() 方法访问一个很慢的网络,我们等不下去了,想终止怎么办呢?Java 的 Thread 类里面倒是有个 stop() 方法,不过已经标记为 @Deprecated,所以不建议使用了。正确的姿势其实是调用 interrupt() 方法。
那 stop() 和 interrupt() 方法的主要区别是什么呢?
stop() 方法会真的杀死线程,如果线程持有 ReentrantLock 锁,被 stop() 的线程并不会自动调用 ReentrantLock 的 unlock() 去释放锁,那其他线程就再也没机会获得 ReentrantLock 锁,这实在是太危险了。
而 interrupt() 方法仅仅是通知线程,线程有机会执行一些后续操作,同时也可以无视这个通知。
stop() 方法会真的杀死线程,如果线程持有 ReentrantLock 锁,被 stop() 的线程并不会自动调用 ReentrantLock 的 unlock() 去释放锁,那其他线程就再也没机会获得 ReentrantLock 锁,这实在是太危险了。
而 interrupt() 方法仅仅是通知线程,线程有机会执行一些后续操作,同时也可以无视这个通知。