1.为什么要用线程池
在java中,开启线程的方式一般分为两种:
a.继承Thread,实现其run方法
b.实现Runnabler接口,通过Thread来实现线程
但无论哪种方式,当线程执行完成后,生命周期就结束了。在Linux系统中,线程的创建是一种很耗资源和时间的工作,因此,实现线程的复用便可以极大的减小资源的消耗,因此,有了线程池的出现
2.初始化线程池的参数问题
public ThreadPoolExecutor(int corePoolSize, // 1
int maximumPoolSize, // 2
long keepAliveTime, // 3
TimeUnit unit, // 4
BlockingQueue<Runnable> workQueue, // 5
ThreadFactory threadFactory, // 6
RejectedExecutionHandler handler ) { //7
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
这是java提供的基本的线程池构造方法,在使用时,需要注意以上参数的意义。
3.参数间的关系
在上面七个参数中,我们重点要关注的是 参数1,2,5,7间的关系。
corePoolSize,核心线程池大小。当我们添加的任务小于该值时,每添加一个任务,但会开启一个线程;一旦任务量大于了corePoolSize,则新添加的任务就会进入workQueue中,这是一个阻塞队列,当队列填满时,如果再添加任务,此时,新添加的任务就会触发新的线程的初始化。此时持续添加任务,便会持续造成新的线程产生,但总共的线程不能超过maximumPoolSize。当总共开启的线程超过maximumPoolSize时,会便启动handler,对新任务进行拒绝。因此,workQueue在传入时,要设定一个大小,否则队列不满,则线程总数只会有corePoolSize个。
如果线程空闲时间超过了keepAliveTime后,线程就会自动销毁。注意,这里销毁的线程不包括核心线程。
4.如何实现线程复用
线程的生命周期在运行完run方法之后就结束了,因此,没办法将Thread拿过来重新用。想实现复用,只能让run方法无法结束,这时workQueue就起到了作用。
在线程池中,所用的队列为阻塞队列。当队列中无数据时,当前线程就会阻塞,直到有数据进入,线程才会运行。因此当线程运行完一个任务后,去队列中获取下一个,如果无法取到新任务,则会阻塞,进而完成一个线程中运行多个任务,即复用的功能。
5.代码验证
public class ThreadPoolTest {
public static void main(String[] args) {
int corePoolSize = 2;
int maximumPoolSize = 5;
int keepAliveTime = 10 * 1000;
int workQueueSize = 10;
int taskSize = 4; //输入不同的任务
ExecutorService pool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(workQueueSize));
for (int i = 1; i <= taskSize; i++) {
pool.execute(new MyThread(i));
}
pool.shutdown();
}
}
class MyThread extends Thread {
private int addNum;
MyThread(int addNum) {
this.addNum = addNum;
}
@Override
public void run() {
try {
sleep(1000);
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName() + "正在执行。。。" + addNum);
}
}
corePoolSize,maximumPoolSize,blockQueueSize的值不变,我们测试taskSize不同时,输出的结果。
a. taskSize ==2
pool-1-thread-2正在执行。。。2
pool-1-thread-1正在执行。。。1
结果:只创建了两个核心线程
b. taskSize=12, workQueue有数据,但不满或刚满
pool-1-thread-2正在执行。。。2
pool-1-thread-1正在执行。。。1
pool-1-thread-1正在执行。。。4
pool-1-thread-2正在执行。。。3
pool-1-thread-1正在执行。。。5
pool-1-thread-2正在执行。。。6
pool-1-thread-2正在执行。。。8
pool-1-thread-1正在执行。。。7
pool-1-thread-1正在执行。。。10
pool-1-thread-2正在执行。。。9
pool-1-thread-2正在执行。。。12
pool-1-thread-1正在执行。。。11
结果:只创建了两个核心线程,其他的任务均会进入队列中,当thread1和thread2运行完成后,进行复用执行其他任务。
c. taskSize =15, taskSize=(maximumPoolSize+workQueueSize)阻塞队列填满,且线程正好开启到最大值
pool-1-thread-5正在执行。。。15
pool-1-thread-3正在执行。。。13
pool-1-thread-4正在执行。。。14
pool-1-thread-2正在执行。。。2
pool-1-thread-1正在执行。。。1
pool-1-thread-2正在执行。。。6
pool-1-thread-1正在执行。。。7
pool-1-thread-3正在执行。。。4
pool-1-thread-4正在执行。。。5
pool-1-thread-5正在执行。。。3
pool-1-thread-2正在执行。。。8
pool-1-thread-1正在执行。。。9
pool-1-thread-3正在执行。。。10
pool-1-thread-5正在执行。。。12
pool-1-thread-4正在执行。。。11
结论:可以看到任务1,2以及最后添加的13,14,15先运行了。这是因为,3到12之间的任务,会填入workQueue中,当workQueue填满时,还有任务进入,就会创建新的线程,运行后续加入的任务,直到所有线程数达到maximumPoolSize。我们这种情况正好wrokQueue填满,而线程开启到最大值maximumPoolSize,任务刚刚与两个值一样。
d. taskSize = 18 taskSize>(maximumPoolSize+workQueueSize), 任务超出最大线程数与队列等待数之和
pool-1-thread-2正在执行。。。2
pool-1-thread-1正在执行。。。1
pool-1-thread-3正在执行。。。13
pool-1-thread-4正在执行。。。14
pool-1-thread-5正在执行。。。15
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task Thread[Thread-15,5,main] rejected from java.util.concurrent.ThreadPoolExecutor@3d4eac69[Running, pool size = 5, active threads = 5, queued tasks = 10, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
at com.game.thread.ThreadPoolTest.main(ThreadPoolTest.java:21)
pool-1-thread-1正在执行。。。4
pool-1-thread-2正在执行。。。3
pool-1-thread-4正在执行。。。6
pool-1-thread-5正在执行。。。7
pool-1-thread-3正在执行。。。5
pool-1-thread-1正在执行。。。8
pool-1-thread-2正在执行。。。9
pool-1-thread-4正在执行。。。10
pool-1-thread-5正在执行。。。11
pool-1-thread-3正在执行。。。12
结果:从上面可以看出,任务15之后的就看不到了且出现了异常,这说明超出的线程池的处理能力,如果我们传RejectedExecutionHandler handler,也就是拒绝策略,此时就会超到任务,
e. corePoolSize=0,maximumPoolSize=3,blockQueueSize=90,taskSize=10
这是一个特殊情况,就是如果我们把corePoolSize置为0,且所有的任务不超过等待对列的大小会如何?按上面理的理解,因为队列不满,所以除了核心线程外,不会创建新线程,但此时corePoolSize为0?难道任务就一直在队列里无法执行吗?
pool-1-thread-1正在执行。。。1
pool-1-thread-1正在执行。。。2
pool-1-thread-1正在执行。。。3
pool-1-thread-1正在执行。。。4
pool-1-thread-1正在执行。。。5
pool-1-thread-1正在执行。。。6
pool-1-thread-1正在执行。。。7
pool-1-thread-1正在执行。。。8
pool-1-thread-1正在执行。。。9
pool-1-thread-1正在执行。。。10
实际这种情况下,任务依然执行了,但线程只有一个。这个和我们设置 corePoolSize=1运行结果是一样的,原因呢?
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0) //****注意这里 1
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
注意代码中标有注释的 1处,当corePoolSize==0时,会走到此处,引发创建线程的操作,所以当corePoolSize=0时,也会运行任务。关于 addWorker()代码的说明,可能参照 手撕ThreadPoolExecutor线程池源码
, 在Android OkHttp框架中,核心线程池就是0,且使用到的了个无容量的队列(相当于系统提供的newCachedThreadPool),有兴趣的可以去看一下。
6.参考值
在使用中,corePoolSize可根据业务来定,另一参数maximumPoolSize则比较重要了,其具体值可根据任务类型来定:
a.CPU密集型
此类的任务,特点为需要大量的使用cpu进行大量的计算,此时的最大线程数,最大值不能超过CPU核心数+1,之所以加1,考虑到cpu计算时,如果有数据在虚拟内存上,需要将其挪到内存上,此过程较为耗时,cpu在等待过程中,可能出现空闲,为了保证其不会空闲,所以+1。
b.IO密集型
当任务中存在大量的网络读取或磁盘文件读取时,maximumPoolSize最大值不要超过 cpu核心数2。因为IO密集型,在等待网络数据或文件读取时,是不需要cpu的,采用DMS机制,此时cpu会空闲下来,因此有了2的操作。
c.cpu+io混合型
如果任务中涉及到cpu计算以及IO操作,如果cpu计算与io操作所用的时间相差不大,则考虑将其拆分成两个任务;如果相差较大,一般是IO操作比较耗时,则可以忽略cpu任务,将其当成IO操作的任务即可。