序 、最近几天心情不是太美丽 。
如果有人说在面试生涯中没有被面试过线程相关的技术点 ,我是不信的 。比如最基础的线程状态 、创建线程的几种方式 ;或者在问一下线程开发遇到的问题 ;还有一个知识点就是线程池了 ,优化多线程 。
先回顾一下 ,线程池是 Java 1.5 之后才出现的饿 API 。
1. 最顶层的接口 Executor ,里面一个方法 execute ,传入一个 Runnable 参数 ;
2. 继承 Execute 的子类 ExecutorService 也属于顶层接口 ,里面多了一些操作方法 ;
3. 实现接口的 ExecutorService 的抽象类 AbstractExecutorService ;
4. 继承抽象类 AbstractExecutorService 的 ThreadPoolExecutor ,我们平常使用的线程池也就是 ThreadPoolExecutor 。
常用的线程池
Java通过Executors提供四种线程池,分别为:
1、newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
2、newFixedThreadPool
创建一个指定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
3、newScheduledThreadPool
创建一个可定期或者延时执行任务的定长线程池,支持定时及周期性任务执行。
4、newCachedThreadPoo
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
实例代码
//缓存线程池
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
//指定线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
//单例线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
//可调度线程池
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
线程池参数
/**
* todo 参数一:corePoolSize 核心线程数, 线程池长期维持的线程数,即使线程处于Idle状态,也不会回收。
* todo 参数二:maximumPoolSize, 线程池规定大小
* todo 参数三四:时间单位
* 正在执行的任务 Runnable20 > corePoolSize --- > 参数三四才会起作用;
* Runnable 执行完毕后 ,闲置 60 s ,如果过了闲置60s ,会回收掉 Runnable 任务 ;
* 如果在闲置时间 60s ,复用此线程 Runnable
* todo 参数五:workQueue 队列
* 会把超出的线程加入队列中
* todo 参数六:ThreadFactory threadFactory, // 新线程的产生方式
*
*/
1. 核心线程数为 1 ,线程池大小为 1 ,时间为 60s 的线程池 。
···
参数一:corePoolSize 核心线程数 -----1
参数二:maximumPoolSize, 线程池规定大小 ------ 1
参数三四:时间单位 -----60s
正在执行的任务 Runnable数量20 > corePoolSize --- > 参数三四才会起作用;
Runnable 执行完毕后 ,闲置 60 s ,如果过了闲置60s ,会回收掉 Runnable 任务 ; 如果在闲置时间 60s ,复用此线程 Runnable
参数五:workQueue 队列 ,会把超出的线程加入队列中
···
ExecutorService executorService = new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, new LinkedBlockingDeque());
for (int i = 0; i < 20; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(100);
Log.i("TTTTTTTTTTTTT", "当前线程名字是 :" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
打印结果
PS:正在执行的任务 Runnable 数量为 20 > corePoolSize (核心线程数),参数三参数四才会起作用 ,Runnable 执行完毕后 ,闲置 60 s ,如果过了闲置60s ,会回收掉 Runnable 任务 ;如果在闲置时间 60s 内,则复用此线程 Runnable 。可能有点懵 ,简单来说就是如果核心线程数会复用 ,如果把核心线程数调到 2的话 ,就会看到两个线程在工作 。
2. 核心线程数为 5 ,线程池大小为 2 ,时间为 60s 的线程池 。
ExecutorService executorService = new ThreadPoolExecutor(5, 2, 60, TimeUnit.SECONDS, new LinkedBlockingDeque());
for (int i = 0; i < 20; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(100);
Log.i("TTTTTTTTTTTTT", "当前线程名字是 :" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
PS:直接崩溃 ,参数异常 。核心线程数大于线程池的最大容量 ,还搞个毛线 。
3.核心线程数为 0 ,线程池大小设置为最大 ,时间为 60s 的线程池 。 缓存线程池
ExecutorService executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS, new LinkedBlockingDeque());
for (int i = 0; i < 20; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(100);
Log.i("TTTTTTTTTTTTT", "当前线程名字是 :" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
打印结果
PS:可以看到这个是线程池是一直在复用同一个线程 。核心线程数为 0 的情况下 ,不考虑并发 ,只会有一个线程再跑 ,然后一直循环复用 ,所以我们就看到只有一个线程在不停的复用 。
可以回顾下 OKHTTP 源码分析(1)
在 OKHTTP 的 任务调度器中 Dispatcher 中 enqueue 方法 。
任务调度器 :Dispatcher
synchronized void enqueue(AsyncCall call) {
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
线程池
executorService().execute(call);
} else {
readyAsyncCalls.add(call);
}
}
我们进入看下 executorService 方法 。
典型的缓存线程池策略
public synchronized ExecutorService executorService() {
if (executorService == null) {
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
new SynchronousQueue(), Util.threadFactory("OkHttp Dispatcher", false));
}
return executorService;
}
PS:OKHTTP 里面使用的线程池是缓存线程池策略 。后面的线程工厂 。进入 Util.threadFactory 里面是如下代码 。
Util 类的静态方法 threadFactory 方法 。
public static ThreadFactory threadFactory(final String name, final boolean daemon) {
return new ThreadFactory() {
@Override public Thread newThread(Runnable runnable) {
Thread result = new Thread(runnable, name);
result.setDaemon(daemon);
return result;
}
};
}
设置线程名字 和设置是否为守护线程 。
示例代码
ExecutorService executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS, new SynchronousQueue(), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setDaemon(false);
thread.setName("My - OkHttp Dispatcher");
return thread;
}
});
for (int i = 0; i < 20; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
Log.i("TTTTTTTTTTTTT", "并发 = 当前线程名字是 :" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
打印结果
PS:OKHTTP 使用的是缓存线城市策略 。
往期回顾
OKHTTP源码分析(1)调用流程梳理