线程池的基本思想是一种对象池,在程序启动时就开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。
- 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 运用线程池能有效的控制线程最大并发数,可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
- 对线程进行一些简单的管理,比如:延时执行、定时循环执行的策略等,运用线程池都能进行很好的实现
一个线程池包括以下四个基本组成部分:
讲到线程池,要重点介绍java.uitl.concurrent.ThreadPoolExecutor类,ThreadPoolExecutor线程池中最核心的一个类,ThreadPoolExecutor在JDK中线程池常用类UML类关系图如下:
我们可以通过ThreadPoolExecutor来创建一个线程池
new ThreadPoolExecutor(corePoolSize, maximumPoolSize,keepAliveTime,
milliseconds,runnableTaskQueue, threadFactory,handler);
我们可以通过execute()或submit()两个方法向线程池提交任务,不过它们有所不同
threadsPool.execute(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
}
});
try {
Object s = future.get();
} catch (InterruptedException e) {
// 处理中断异常
} catch (ExecutionException e) {
// 处理无法执行任务异常
} finally {
// 关闭线程池
executor.shutdown();
}
我们可以通过shutdown()或shutdownNow()方法来关闭线程池,不过它们也有所不同
新建线程 -> 达到核心数 -> 加入队列 -> 新建线程(非核心) -> 达到最大数 -> 触发拒绝策略
- CachedThreadPool():可缓存线程池。
- FixedThreadPool():定长线程池。
- ScheduledThreadPool():
- SingleThreadExecutor():单线程化的线程池。
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
public class ThreadPoolExecutorTest1 {
public static void main(String[] args) {
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 1000; i++) {
final int index = i;
try {
Thread.sleep(index * 1000);
} catch (Exception e) {
e.printStackTrace();
}
cachedThreadPool.execute(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName()+":"+index);
}
});
}
}
}
newFixedThreadPool创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待,指定线程池中的线程数量和最大线程数量一样,也就线程数量固定不变
示例代码如下
public class ThreadPoolExecutorTest {
public static void main(String[] args) {
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);// 每隔两秒打印3个数
for (int i = 0; i < 10; i++) {
final int index = i;
fixedThreadPool.execute(new Runnable() {
public void run() {
try {
System.out.println(Thread.currentThread().getName()+":"+index);
//三个线程并发
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
}
newscheduledThreadPool创建一个定长线程池,支持定时及周期性任务执行。延迟执行示例代码如下.表示延迟1秒后每3秒执行一次
public class ThreadPoolExecutorTest3 {
public static void main(String[] args) {
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName() + ": delay 1 seconds, and excute every 3 seconds");
}
}, 1, 3, TimeUnit.SECONDS);// 表示延迟1秒后每3秒执行一次
}
}
newSingleThreadExecutor创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
public class ThreadPoolExecutorTest4 {
public static void main(String[] args) {
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index = i;
singleThreadExecutor.execute(new Runnable() {
public void run() {
try {
System.out.println(Thread.currentThread().getName() + ":" + index);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
}
结果依次输出,相当于顺序执行各个任务。使用JDK自带的监控工具来监控我们创建的线程数量,运行一个不终止的线程,创建指定量的线程,来观察
参数的设置跟系统的负载有直接的关系,下面为系统负载的相关参数:
每个任务需要tasktime秒处理,则每个线程每钞可处理1/tasktime个任务。系统每秒有tasks个任务需要处理,则需要的线程数为:tasks/(1/tasktime),即tasks*tasktime个线程数。
假设系统每秒任务数为100 ~ 1000,每个任务耗时0.1秒,则需要100 * 0.1至1000 * 0.1,即10 ~ 100个线程。那么corePoolSize应该设置为大于10,具体数字最好根据8020原则,即80%情况下系统每秒任务数小于200,最多时为1000,则corePoolSize可设置为20。
当系统负载达到最大值时,核心线程数已无法按时处理完所有任务,这时就需要增加线程。每秒200个任务需要20个线程,那么当每秒达到1000个任务时,则需要(1000-queueCapacity)*(20/200),即60个线程,可将maxPoolSize设置为60。
任务队列的长度要根据核心线程数,以及系统对任务响应时间的要求有关。队列长度可以设置为(corePoolSize/tasktime)*responsetime: (20/0.1)*2=400,即队列长度可设置为400。
队列长度设置过大,会导致任务响应时间过长,切忌以下写法:
LinkedBlockingQueue queue = new LinkedBlockingQueue();
这实际上是将队列长度设置为Integer.MAX_VALUE,将会导致线程数量永远为corePoolSize,再也不会增加,当任务数量陡增时,任务响应时间也将随之陡增。
当负载降低时,可减少线程数量,当线程的空闲时间超过keepAliveTime,会自动释放线程资源。默认情况下线程池停止多余的线程并最少会保持corePoolSize个线程。
默认情况下核心线程不会退出,可通过将该参数设置为true,让核心线程也退出。