什么是池?
例子—–>买火车票、医院挂号
数据库连接池
常量池
线程池
基本概念
线程池,就是一个线程的池子,里面有若干线程,它们的目的就是执行提交给线程池的任务,执行完一个任务后不会退出,而是继续等待或执行新任务。—>JDK1.5
优点
它可以重用线程,避免线程创建的开销
在任务过多时,通过排队避免创建过多线程,减少系统资源消耗和竞争,确保任务有序完成
ThreadPoolExecutor
一种生产者消费者模式的实现
构造方法
线程池大小
线程池的大小主要与四个参数有关:
corePoolSize:核心线程个数
maximumPoolSize:最大线程个数
keepAliveTime和unit:空闲线程存活时间
一般情况下,有新任务到来的时候,如果当前线程个数小于corePoolSize,就会创建一个新线程来执行该任务,需要说明的是,即使其他线程现在也是空闲的,也会创建新线程。
如果线程个数大于等于corePoolSize,那就不会立即创建新线程了,它会先尝试排队,需要强调的是,它是”尝试”排队,而不是”阻塞等待”入队,如果队列满了或其他原因不能立即入队,它就不会排队,而是检查线程个数是否达到了maximumPoolSize,如果没有,就会继续创建线程,直到线程数达到maximumPoolSize。
keepAliveTime的目的是为了释放多余的线程资源,它表示,当线程池中的线程个数大于corePoolSize时,额外空闲线程的存活时间,也就是说,一个非核心线程,在空闲等待新任务时,会有一个最长等待时间,即keepAliveTime,如果到了时间还是没有新任务,就会被终止。如果该值为0,表示所有线程都不会超时终止。
一些其他API
//返回当前线程个数
public int getPoolSize()
//返回线程池曾经达到过的最大线程个数
public int getLargestPoolSize()
//返回线程池自创建以来所有已完成的任务数
public long getCompletedTaskCount()
//返回所有任务数,包括所有已完成的加上所有排队待执行的
public long getTaskCount()
阻塞队列
BlockingQueue:
LinkedBlockingQueue:基于链表的阻塞队列,可以指定最大长度,但默认是无界的。
ArrayBlockingQueue:基于数组的有界阻塞队列
PriorityBlockingQueue:基于堆的无界阻塞优先级队列
SynchronousQueue:没有实际存储空间的同步阻塞队列
如果用的是无界队列,需要强调的是,线程个数最多只能达到corePoolSize,到达corePoolSize后,新的任务总会排队,参数maximumPoolSize也就没有意义了。
对于SynchronousQueue,我们知道,它没有实际存储元素的空间,当尝试排队时,只有正好有空闲线程在等待接受任务时,才会入队成功,否则,总是会创建新线程,直到达到maximumPoolSize。
任务拒绝策略
如果队列有界,且maximumPoolSize有限,则当队列排满,线程个数也达到了maximumPoolSize,这时,新任务来了,如何处理呢?此时,会触发线程池的任务拒绝策略。
默认情况下,提交任务的方法如execute/submit/invokeAll等会抛出异常,类型为RejectedExecutionException。
不过,拒绝策略是可以自定义的,ThreadPoolExecutor实现了四种处理方式:
ThreadPoolExecutor.AbortPolicy:这就是默认的方式,抛出异常
ThreadPoolExecutor.DiscardPolicy:静默处理,忽略新任务,不抛异常,也不执行
ThreadPoolExecutor.DiscardOldestPolicy:将等待时间最长的任务扔掉,然后自己排队
ThreadPoolExecutor.CallerRunsPolicy:在任务提交者线程中执行任务,而不是交给线程池中的线程执行.
它们都是ThreadPoolExecutor的public静态内部类,都实现了RejectedExecutionHandler接口,这个接口的定义为:
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
需要强调下,拒绝策略只有在队列有界,且maximumPoolSize有限的情况下才会触发。
线程工厂
线程池还可以接受一个参数,ThreadFactory,它是一个接口,定义为:
public interface ThreadFactory {
Thread newThread(Runnable r);
}
这个接口根据Runnable创建一个Thread,ThreadPoolExecutor的默认实现是Executors类中的静态内部类DefaultThreadFactory,主要就是创建一个线程,给线程设置一个名称,设置daemon属性为false,设置线程优先级为标准默认优先级,线程名称的格式为: pool-<线程池编号>-thread-<线程编号>。
高能
不过,ThreadPoolExecutor有如下方法,可以改变这个默认行为。
//预先创建所有的核心线程
public int prestartAllCoreThreads()
//创建一个核心线程,如果所有核心线程都已创建,返回false
public boolean prestartCoreThread()
//如果参数为true,则keepAliveTime参数也适用于核心线程
public void allowCoreThreadTimeOut(boolean value)
工厂类Executors
public static ExecutorService newSingleThreadExecutor()
public static ExecutorService newFixedThreadPool(int nThreads)
public static ExecutorService newCachedThreadPool()
newSingleThreadExecutor:
只使用一个线程,使用无界队列LinkedBlockingQueue,线程创建后不会超时终止,该线程顺序执行所有任务。该线程池适用于需要确保所有任务被顺序执行的场合。
newFixedThreadPool:
使用固定数目的n个线程,使用无界队列LinkedBlockingQueue,线程创建后不会超时终止。和newSingleThreadExecutor一样,由于是无界队列,如果排队任务过多,可能会消耗非常大的内存。
newCachedThreadPool:
当新任务到来时,如果正好有空闲线程在等待任务,则其中一个空闲线程接受该任务,否则就总是创建一个新线程,创建的总线程个数不受限制,对任一空闲线程,如果60秒内没有新任务,就终止。
newScheduledThreadPool:
适用于多个后台线程执行周期任务,并且限制后台线程的数量。
实际中,应该使用newFixedThreadPool还是newCachedThreadPool呢?
在系统负载很高的情况下,newFixedThreadPool可以通过队列对新任务排队,保证有足够的资源处理实际的任务,而newCachedThreadPool会为每个任务创建一个线程,导致创建过多的线程竞争CPU和内存资源,使得任何实际任务都难以完成,这时,newFixedThreadPool更为适用。
不过,如果系统负载不太高,单个任务的执行时间也比较短,newCachedThreadPool的效率可能更高,因为任务可以不经排队,直接交给某一个空闲线程。
在系统负载可能极高的情况下,两者都不是好的选择,newFixedThreadPool的问题是队列过长,而newCachedThreadPool的问题是线程过多,这时,应根据具体情况自定义ThreadPoolExecutor,传递合适的参数。
线程池的死锁
例子:
比如任务A,在它的执行过程中,它给同样的任务执行服务提交了一个任务B,但需要等待任务B结束。
如果任务A是提交给了一个单线程线程池,就会出现死锁,A在等待B的结果,而B在队列中等待被调度。
public class ThreadPoolDeadLockDemo {
private static final int THREAD_NUM = 5;
static ExecutorService executor = Executors.newFixedThreadPool(THREAD_NUM);
static class TaskA implements Runnable {
@Override
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
Future<?> future = executor.submit(new TaskB());
try {
future.get();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(“finished task A”);
}
}
static class TaskB implements Runnable {
@Override
public void run() {
System.out.println(“finished task B”);
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 5; i++) {
executor.execute(new TaskA());
}
Thread.sleep(2000);
executor.shutdown();
}
}
办法:
1>替换newFixedThreadPool为newCachedThreadPool,让创建线程不再受限
2>另一个解决方法,是使用SynchronousQueue,它可以避免死锁,怎么做到的呢?对于普通队列,入队只是把任务放到了队列中,而对于SynchronousQueue来说,入队成功就意味着已有线程接受处理,如果入队失败,可以创建更多线程直到maximumPoolSize,如果达到了maximumPoolSize,会触发拒绝机制,不管怎么样,都不会死锁。我们将创建executor的代码替换为:
static ExecutorService executor = new ThreadPoolExecutor(
THREAD_NUM, THREAD_NUM, 0, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
只是更改队列类型,运行同样的程序,程序不会死锁,不过TaskA的submit调用会抛出异常RejectedExecutionException,因为入队会失败,而线程个数也达到了最大值。