概述
程序运行的本质是消耗系统资源,线程、数据库连接等都会耗费系统的资源。线程、数据库连接等的创建、销毁等都十分消耗系统资源,所以,如果使用池化技术(线程池、数据库连接池等),可以对系统资源进行控制和使用优化。
池化技术说白了就是事先准备(也可以不准备)准备一些资源,比如事先准备好一定数量的线程放进线程池,程序要用,就到线程池里面拿,用完了不是销毁,而是归还给线程池。
池化技术的好处:
- 降低资源消耗,因为线程用完了,不是立即销毁,而是归还给线程池,达到重复利用,使用不用频繁的创建和销毁线程,降低系统资源的消耗。
- 提高响应速度。
- 使得系统资源达到可控,方便管理,如果不使用线程池,而是来一个请求就创建一个线程,那么,如果并发太大,无限地创建线程,最终系统资源会被耗尽。如果使用线程池,可以设定核心线程,最大线程数等参数,如果线程都在忙,有新的请求过来,就不会在创建新线程,而是会等待或者直接拒绝请求等操作(具体看拒绝策略)。
操作
java创建线程池的三大方法:
//创建一个固定线程数的线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
//创建一个只有单一线程的线程池,线程池里面只有一个线程
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
//创建一个可扩展的线程池
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
测试FixedThreadPool:
/**
* 测试固定线程池
*/
public static void testFixThreadPool(){
//创建一个固定线程数为3的固定线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
try {
for (int i = 0;i<10;i++){
fixedThreadPool.execute(()->{
System.out.println(Thread.currentThread().getName() + " run ");
});
}
}finally {
//关闭线程池
fixedThreadPool.shutdown();
}
}
可以看到来来回回都是这三个线程在工作。
测试SingleThreadExecutor
/**
* 测试固定线程池
*/
public static void testSingleThreadPool(){
//创建一个单线程线程池
ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
Integer result = 0;
try {
for (int i = 0;i<10;i++){
final int tempI = i;
//如果使用execute方法,是没有返回值的,如果使用submit方法,是用返回值的
Future<Integer> future = singleThreadPool.submit((Callable<Integer>) ()->{
System.out.println(Thread.currentThread().getName() + " submit ");
return tempI;
});
System.out.println(future.get());
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} finally {
//关闭线程池,如果开了不关闭,程序会阻塞
singleThreadPool.shutdown();
}
}
可以看到来来回回都只有一个线程在工作,并且上面的代码还使用了线程池的另外一种执行方法,submit,是带返回值的执行方法。不带返回值用execute方法。
测试CachedThreadPool:
/**
* 测试固定线程池
*/
public static void testCacheThreadPool(){
//创建一个可伸缩线程池
ExecutorService cacheThreadPool = Executors.newCachedThreadPool();
try {
for (int i = 0;i<100;i++){
cacheThreadPool.execute(()->{
System.out.println(Thread.currentThread().getName() + " run ");
});
}
}finally {
//关闭线程池,如果开了不关闭,程序会阻塞
cacheThreadPool.shutdown();
}
}
可以看到最大的线程是32号线程,我截取的是最大的了,它的原理是这样的,当请求来时,线程池里面有空闲线程的话,就使用该线程来处理,没有的话就创建一个新的线程来处理,因为执行100次,但是有些线程已经执行完前面的任务回到线程池了,所以线程池实际上并没有创建100个线程来处理,而是有些任务复用了一些线程。如果想要 看到100个线程效果,可以使任务睡眠2秒。
探索三种方法的源码并讲解线程池七大参数
可以发现三种方法都是new ThreadPoolExecutor对象,只是参数不一样而已。
七大参数:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
//corePoolSize:核心线程数
//maximumPoolSize: 最大线程数
//keepAliveTime: 空闲线程存储时间
// unit 时间单位
//BlockingQueue 阻塞队列
//ThreadFactory 线程工厂
//RejectedExecutionHandler 拒绝策略
- corePoolSize:核心线程数,就是线程池创建时就会创建,并且生命周期跟线程池一样的线程数。这些线程在线程池被销毁前都不会被销毁。
- maximumPoolSize:最大线程数,该线程池能创建的最大的线程数。
- keepAliveTime:有一些非核心线程,就是maximumPoolSize减corePoolSize剩下线程,这些线程如果空闲超过这个时间,就会被销毁。
- unit :keepAliveTime的时间单位
- BlockingQueue :阻塞队列,当前队列的线程都在忙时,就会把请求加入到阻塞队列中,如果阻塞队列满了,但是线程数还没达到最大线程数,就创建新的线程来处理请求。如果队列满了,当前忙的线程等于最大线程数,就会使用拒绝策略对新请求就行处理,可能直接拒绝等。所以线程池可处理请求数=最大线程数+阻塞队列大小。
- ThreadFactory :创建线程的工厂,这个一般都用默认的,Executors.defaultThreadFactory()。
- RejectedExecutionHandler :拒绝策略,当线程全部在忙,并且阻塞队列也满了后,对新来的任务的拒绝策略。默认是抛出异常,直接拒绝。
阿里巴巴开发手册上明确表明不允许使用Executors来创建线程池,而是要通过ThreadPoolExecutor来创建线程池,使用ThreadPoolExecutor可以让我们更加明确知道线程池的运行规则,达到心中有数,避免资源耗尽。
可以看看newFixedThreadPool和方法的参数,虽然核心线程数和最大线程数可以固定,但是他设置的阻塞队列的大小是Integer.max,那就意味着在高并发下可能会有大量的请求堆积在阻塞队列里面,导致OOM。
而newCachedThreadPool方法的最大线程数是Integer.max,这意味着可能会有大量的线程被创建。这就达不到资源控制的效果。
public static void testThreadPoolExecutor(){
//核心线程为3,最大线程为5,非核心线程空闲20秒后销毁,阻塞队列最大为5,拒绝策略为立即拒绝并抛出异常
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3,
5,
20,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(5),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
for (int i = 0;i<5;i++){
threadPoolExecutor.execute(()->{
System.out.println(Thread.currentThread().getName() + " execute ");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
结果:
因为睡眠了2秒,所以第4和第5个请求会进入阻塞队列,等待已有的线程执行完再去执行队列里面的请求,因为队列没有满,所以不会创建新的线程来处理,来来回回都是那三个线程,所以第4和第5会在2秒后打印。
把循环次数设置到10。
先进入三个请求,由核心线程执行,然后又来了5个请求直接入队,最后来了2个请求,因为队列已满,就创建新的线程来执行,因为最大线程数为5,所以来来回回就这5个线程来执行任务。
把循环次数调到11.
像上面的情况进行到10个请求,当第11个请求来时,因为队列已满、并且没有空闲线程,线程数已达最大线程数,所以就直接使用设置的拒绝策略来处理线程,上面使用的是抛出异常,直接拒绝。
拒绝策略
有四种拒绝策略:
-
AbortPolicy:直接拒绝并抛出异常,像刚刚上面情况。
-
CallerRunsPolicy:把任务哪里来回哪里去,线程池是在哪个线程调用的,就回哪个线程那里。
因为是main线程调用的线程池,所以哪来回哪去。 -
DiscardPolicy:直接放弃任务,但是不抛出异常。
这里只有10个任务,第11个被自动放弃了,但是没有抛出异常。 -
DiscardOldestPolicy:舍弃最老(队首,因为队列时先进先出,所以队首的任务是最先入队、最老的任务)的任务,上面的拒绝策略都是舍弃对队尾的任务。
一些参数设置建议:
首先要看线程池执行的任务是IO密集型还是CPU密集型人任务,IO密集型通常都是一些涉及计算比较多的任务,而IO密集型通常是一些Io操作,比如操作数据卷,读写文件,操作缓存等。
如果是CPU密集型任务,可以设置核心线程数等于最大线程数等于机器的核心数,这样可以充分利用CPU,不用进行不必要的线程上下文切换。
如果是IO密集型任务,通常Cpu是比较空闲的,因为多数时间都消耗在IO上,所以可以设置线程数多一点,要结合任务的数量来设置,通常为2(具体看系统的跟业务的访问量、并发数等等)倍,这样的话,在执行IO时期内还有其他线程来处理其他请求,而不至于阻塞。