为什么要使用线程池
合理利用线程池能够带来三个好处。(复制过来的,懒)
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
线程池的使用
先定义线程池:
方法一:
private ExecutorService initExecutor(int size){
return Executors.newFixedThreadPool(size);//就是调用Executors工具类构造
}
方法二:
//自己构造
private ThreadPoolExecutor initExecutor2(int corePoreSize, int maxPoolSize, int queue, final String threadName){
return new ThreadPoolExecutor(corePoreSize,
maxPoolSize, 1,TimeUnit.MINUTES,
new ArrayBlockingQueue<Runnable>(queue),
new ThreadFactory(){
AtomicInteger i=new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
return new Thread(threadName+i.incrementAndGet());
}
}, new ThreadPoolExecutor.CallerRunsPolicy()
);
}
然后再向线程池中提交任务:
ExecutorService.submit(..);
或
ThreadPoolExecutor.execute(..);
submit 方法和execute方法提交任务有什么区别呢?请参考 java-源码解读-线程池提交之execute和submit有何不同
通过这两种方法定义的线程池哪种更好呢,先来了解线程的实现原理,再回答这个问题。
线程池的实现原理
不管以何种方法创建线程池,最终的结果都是调用ThreadPoolExecutor的构造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
//...
}
创建一个线程池需要输入几个参数:
corePoolSize:当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
workQueue(任务队列):用于保存等待执行的任务的阻塞队列。
maximumPoolSize(线程池最大大小):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。
ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字,Debug和定位问题时非常又帮助。
RejectedExecutionHandler(拒绝策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。以下是JDK1.5提供的四种策略。
AbortPolicy:直接抛出异常。
CallerRunsPolicy:只用调用者所在线程来运行任务。
DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
DiscardPolicy:不处理,丢弃掉。
keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
TimeUnit(线程活动保持时间的单位)。
上面的参数,在下面会提到。
当向线程池提交任务之后,到底发生了什么?
不管是用execute方法提交任务,还是用submit提交任务,最后都是调用ThreadPoolExecutor的execute方法。
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
/**
*工作线程数小于corePoolSize,线程池会创建一个线程来执行任务
* addWorker就是创建一个新的工作线程
*/
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
/**
*如果工作线程数大于corePoolSize
*就把任务加入到workQueue中
*/
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
//recheck。有可能把任务加入到队列时,线程池已不是RUNNING状态
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
/**
*如果不能加入workQueue
*只有执行拒绝策略了
*/
else if (!addWorker(command, false))
reject(command);
}
到目前为止说完了任务担交的过程。
提交的任务是什么时候执行的呢?
任务的执行在Wordker线程中执行, Worker 是ThreadPoolExecutor的内部类。
Worker实现了Runnable接口,也是一个线程。
直接看Worker的run方法执行了什么逻辑.
public void run() {
runWorker(this);
}
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock();
boolean completedAbruptly = true;
try {
/**
*worker取任务
*worker首先执行task,即通过worker构造器传递给worker的task
* 如果task为空,就调用getTask()方法从队列中取任务
*/
while (task != null || (task = getTask()) != null) {
w.lock();
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
//任务执行前,回调方法
beforeExecute(wt, task);
Throwable thrown = null;
try {
/**
*此处就是任务执行的地方
*/
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
//任务执行后,回调方法
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
这就是一个任务的生命周期,虽然省略ThreadPoolExecutor的好多源码,但大体可以看出一个任务的生命周期。
怎样构造一个安全的线程池
回答上面的问答,怎样构造一个线程比较好呢?
要构造一个好的线程池,无非就是控投制几个参数,corePoreSize ,maxPoolSize,queueSize,queue,RejectedExecutionHandler,和ThreadFactory。
corePoreSize ,maxPoolSize主要根据业务类型和cpu数量来决定,这里不讨论。总之耗cpu的任务就少开几个线程,io多的任务就多开个线程。
queueSize:列队的长度,我个人认为这个参数很重要,ThreadPoolExecutor自带的几个线程池实现比 如:newFixedThreadPool,newSingleThreadExecutor默认使用的都是无界队列,这个时候maxPoolSize,RejectedExecutionHandler这两个参数将不起使用。因为队列是无界,永远不会拒绝任务。而且当任务非常多时有内存溢出的风险。
queue:生产者消费者队列。不同的队列可以实现不同的功能。比如:newFixedThreadPool,newSingleThreadExecutor都使用LinkedBlockingQueue实现,newCachedThreadPool使用SynchronousQueue实现。而DelayQueue是java ScheduledExecutorService 的实现基础。
RejectedExecutionHandler:主要根据业务决定。
根据以上几个参数的分析,我个人认为少用java自带的线程池,尽量构造符合业务需要的线程池。
ThreadFactory:线程的工厂。当代码有异常时,此时ThreadFactory就派上用场了,我们可以给线程起一个有意义的名字,这样就能在异常中识别哪个线程出现了问题。