多线程之线程池
线程池的优势和劣势
优势:减少线程的创建和销毁,控制和调整线程的并发数
劣势:需要根据不同的电脑设备配置不同核心线程数,根据业务设置不同的阻塞队列的形式等;一句话:因地制宜,学习起来相对于new thread比较麻烦。
关于疑问
1 thread的创建和销毁占用较多的资源,到底是哪些资源呢?
2 怎么使用线程池就能减少资源消耗?
3 线程池是怎么让线程不被销毁的?
thread 的创建是依托操作系统的,java线程的线程栈是在堆外的,不受虚拟机控制完全受操作系统控制;同时创建和销毁需要内存的申请和回收、列入调度、在cpu进行切换时需要在内存中来回读取信息,这样看thread的使用确实是很复杂且比较重。
使用线程池保证了thread不是频繁的new,有效的控制了thread的数量,在池内有可用线程时不去创建,在没有任务执行时进行相对应的阻塞等一系列手段来减少资源的消耗。
API内的线程池
多线程之Thread 里面提到了thread的执行和工作是不分离的,而线程池的存在就行是把繁杂的工作进行有序的梳理,流程,控制(执行调度和工作单元分开)
ThreadPoolExecutor 通过名称可以知道“线程池执行者”,这个对象就是线程的执行调度者,在它内部有两个函数
execute(Runnable)
submit(Runnable/Callable)
而Runnable Callable 是线程的工作单元,这样DougLea 通过把线程的执行和工作分开,让线程更加工序化(prestartedXX,afterExecute...这一点在Android AsyncTask 的实现中更能说明这一项)
ThreadPoolExecutor是concurrent包下提供的默认实现,文章只分析ThreadPoolExecutor至于Executors里面提供的实现感兴趣的可以自己学习。
打开ThreadPoolExecutor ctrl+o 先笼统的过一遍整体函数,有助于理解的就那么几个函数而已:
1 构造函数
2 addWorker
3 beforeExecute
4 ctlOf
5 execute
6 submit
剩下都是一些get set、shutdown及线程策略(RejectedExecuteHandler的子类)
备注:也许我个人理解的太简单,但是我自己的学习路线确实是这么做的
构造函数
ThreadPoolExecutor(int corePoolSize,int maxPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,RejectExecuteHandler rejectHandler)
构造函数是重载的,既然是重载的就允许使用者自我定制,且程序内的某些参数会存在默认实现。整个线程池内对于线程的管理创建销毁等都是有以上这些参数共同决定的。
Execute 函数
execute函数内执行了3个if 判断
execute(Runnable command){
int c =ctl.get();
if (workerCountOf(c)<corePoolSize){
if (addWorker(command,true)) return;
c =ctl.get();
}
}
每一次只要有工作单元的对象过来都要调用workerCountOf 和corePoolSize做比较,当小于corePoolSize的时候执行的逻辑都是把“工作单元”的对象添加到works中(HashSet workers = new HashSet()) 并执行对应“工作单元”
addWorker(Runnable task,boolean core){
Work w = new Work(task);
Thread t = w.thread;
xxxx
works.add(w);
if(workedAdd)t.start();
}
到这里已经大概清晰了, 调用方创建Runnable “工作单元”,交给Executor 这个“调度者”(ThreadPoolExecutor) 通过execute进行执行(调度者里面有很多控制 工作单元的方法)
execute(Runnable command){
int 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)
addWorker(null,false);
}
}
上面的逻辑addWorker(null,falise)是关键,里面只是把 “工作单元”加入到了构造函数的 队列里面 。
截止到目前为止已经解决了文章开头的前两个问题,还剩下最后一个问题"池的线程怎么不销毁的"
在addWorker()函数内,Worker 对象绑定了一个Thread 在调用start 函数时直接触发的是Worker---->run()
run(){
runWorker(Worker);
}
runWorker(Worker){
while(task != null || (task == getTask()) != null){xxx}
}
关键点就在于getTask这里
Runnable getTask(){
Runnable r = timed ?workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :workQueue.take();
if (r !=null)
return r;
}
workQueue 是一个阻塞队列,当队内没有任务时就会阻塞,这里看一个BlockingQueue的实现
public E take()throws InterruptedException {
final ReentrantLock lock =this.lock;
lock.lockInterruptibly();
try {
while (count ==0)
notEmpty.await();
return dequeue();
}
}
notEmpty.await();这一行就是真正实现阻塞的原因。
到这里文章开头的三个疑问都已经解答了。