作为一个面试经验不算丰富的程序员,在这行业摸爬滚打几年。我立志要征服个大公司的面试官。原则就是小厂我随便虐,大厂随便虐我。所以在去面试之前,java基础还是要恶补一番的。java线程池作为一个面试官常常问的东西,我是必须得熟悉的。
四种线程池
Java通过Executors提供四种线程池,分别为:
1、newSingleThreadExecutor
Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去
public static void singleTheadPoolTest() {
ExecutorService pool = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int ii = i;
pool.execute(() -> out.println(Thread.currentThread().getName() + "=>" + ii));
}
}
-----output-------
线程名称:pool-1-thread-1,执行0
线程名称:pool-1-thread-1,执行1
线程名称:pool-1-thread-1,执行2
线程名称:pool-1-thread-1,执行3
线程名称:pool-1-thread-1,执行4
线程名称:pool-1-thread-1,执行5
线程名称:pool-1-thread-1,执行6
线程名称:pool-1-thread-1,执行7
线程名称:pool-1-thread-1,执行8
线程名称:pool-1-thread-1,执行9
2、newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
public static void fixTheadPoolTest() {
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
final int ii = i;
fixedThreadPool.execute(() -> {
out.println("线程名称:" + Thread.currentThread().getName() + ",执行" + ii);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
------output-------
线程名称:pool-1-thread-3,执行2
线程名称:pool-1-thread-1,执行0
线程名称:pool-1-thread-2,执行3
线程名称:pool-1-thread-3,执行4
线程名称:pool-1-thread-1,执行5
线程名称:pool-1-thread-2,执行6
线程名称:pool-1-thread-3,执行7
线程名称:pool-1-thread-1,执行8
线程名称:pool-1-thread-3,执行9
3、newScheduledThreadPool
创建一个可定期或者延时执行任务的定长线程池,支持定时及周期性任务执行。
public static void sceduleThreadPool() {
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
Runnable r1 = () -> out.println("线程名称:" + Thread.currentThread().getName() + ",执行:3秒后执行");
scheduledThreadPool.schedule(r1, 3, TimeUnit.SECONDS);
Runnable r2 = () -> out.println("线程名称:" + Thread.currentThread().getName() + ",执行:延迟2秒后每3秒执行一次");
scheduledThreadPool.scheduleAtFixedRate(r2, 2, 3, TimeUnit.SECONDS);
Runnable r3 = () -> out.println("线程名称:" + Thread.currentThread().getName() + ",执行:普通任务");
for (int i = 0; i < 5; i++) {
scheduledThreadPool.execute(r3);
}}----output------
线程名称:pool-1-thread-1,执行:普通任务
线程名称:pool-1-thread-5,执行:普通任务
线程名称:pool-1-thread-4,执行:普通任务
线程名称:pool-1-thread-3,执行:普通任务
线程名称:pool-1-thread-2,执行:普通任务
线程名称:pool-1-thread-1,执行:延迟2秒后每3秒执行一次
线程名称:pool-1-thread-5,执行:3秒后执行
线程名称:pool-1-thread-4,执行:延迟2秒后每3秒执行一次
线程名称:pool-1-thread-4,执行:延迟2秒后每3秒执行一次
线程名称:pool-1-thread-4,执行:延迟2秒后每3秒执行一次
线程名称:pool-1-thread-4,执行:延迟2秒后每3秒执行一次
4、newCachedThreadPoo
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程。当任务数增加时,此线程池又可以智能的添加新线程来处理任务
public static void cacheThreadPool() {
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 1; i <= 10; i++) {
final int ii = i;
try {
Thread.sleep(ii * 1);
} catch (InterruptedException e) {
e.printStackTrace();
}
cachedThreadPool.execute(()->System.out.println("线程名称:" + Thread.currentThread().getName() + ",执行" + ii));
}
}
-----output------
线程名称:pool-1-thread-1,执行1
线程名称:pool-1-thread-1,执行2
线程名称:pool-1-thread-1,执行3
线程名称:pool-1-thread-1,执行4
线程名称:pool-1-thread-1,执行5
线程名称:pool-1-thread-1,执行6
线程名称:pool-1-thread-1,执行7
线程名称:pool-1-thread-1,执行8
线程名称:pool-1-thread-1,执行9
线程名称:pool-1-thread-1,执行10
ThreadPoolExecutor
一般我经常用ThreadPoolExecutor线程池
从图中可以看出,上面说的四种类型的线程池,其实内部也是使用ThreadPoolExecutor实现的。因此聪明的我们就清楚了,搞懂这个鬼东西就可以咯。
首先ThreadPoolExecutor提供了多个构造方法:
public class ThreadPoolExecutor extends AbstractExecutorService {
.....
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
...
}
使用这个线程池我们需要了解一些参数的意义:
1. corePoolSize:指定了线程池中的线程数量。
在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除 非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出, 是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。 默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务, 当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。
2. maximumPoolSize:指定了线程池中的最大线程数量。它表示在线程池中最多能创建多少个线程
如果这队列满了,而且正在运行的线程数量小于maximumPoolSize,那么就会创建非核心线程立刻运行 任务
3. keepAliveTime:当前线程池数量超过corePoolSize时,多余的空闲线程的存活时间,即多久时间内会被销毁
默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用, 直到线程池中的线程数不大于corePoolSize。 即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止, 直到线程池中的线程数不超过corePoolSize。 但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时, keepAliveTime参数也会起作用,直到线程池中的线程数为0;
4. unit: keepAliveTime的单位
TimeUnit.DAYS; //天 TimeUnit.HOURS; //小时 TimeUnit.MINUTES; //分钟 TimeUnit.SECONDS; //秒 TimeUnit.MILLISECONDS; //毫秒 TimeUnit.MICROSECONDS; //微妙 TimeUnit.NANOSECONDS; //纳秒</pre>
5. workQueue:任务队列,被提交但尚未被执行的任务。
一般来说,这里的阻塞队列有以下几种选择: ArrayBlockingQueue:一个基于数组结构的有界阻塞队列。 LinkedBlockingQueue:一个基于链表的阻塞队列,吞吐量要高于ArrayBlockingQueue。 SynchronousQueue:一个不存储元素的阻塞队列。每次插入操作必须等到另外一个线程调用移除操作, 否则一直处于阻塞状态。吞吐量要高于LinkedBlockingQueue。 PriorityBlockingQueue:一个具有优先级的无线阻塞队列。 ArrayBlockingQueue和PriorityBlockingQueue使用较少, 一般使用LinkedBlockingQueue和Synchronous。线程池的排队策略与BlockingQueue有关。
6. threadFactory:线程工厂,用于创建线程,一般用默认的即可。
7. handler:拒绝策略,当任务太多来不及处理,如何拒绝任务。
线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也塞不下新任务了。 这时候我们就需要拒绝策略机制合理的处理这个问题。 ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。 ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程) ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务 以上内置拒绝策略均实现了 RejectedExecutionHandler接口,若以上策略仍无法满足实际需要, 完全可以自己扩展RejectedExecutionHandler接口。
Java线程池工作过程
1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面 有任务,线程池也不会马上执行它们。除 非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出, 是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。
2. 当调用execute()方法添加一个任务时,线程池会做如下判断:
a) 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务; b) 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列; c) 如果这时候队列满了,而且正在运行的线程数量小于maximumPoolSize,那么还是要 创建非核心线程立刻运行这个任务; d) 如果队列满了,而且正在运行的线程数量大于或等于maximumPoolSize,那么线程池 会抛出异常 RejectExecutionException。
3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
4. 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运 行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它 最终会收缩到corePoolSize的大小。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时, keepAliveTime参数也会起作用,直到线程池中的线程数为0;
如何向线程池提交任务
向线程池提交任务,提供了两种方法,分别是execute()和submit()方法。
execute()方法
execute方法用于提交不需要返回值的任务,所以也就意味着无法判断是否执行成功。
submit方法
submit方法可以用于提交需要有返回值的任务。线程池会返回一个future类型的对象, 通过这个future对象可以判读是否执行成功,并且还可以通过get()方法来获取返回值。
关闭线程池
ExecutorService的shutdown()和shutdownNow()方法都可以用来关闭线程池,那么他们有什么区别呢?
shutdown()
当我们调用shutdown()方法后,线程池会等待我们已经提交的任务执行完成。 但是此时线程池不再接受新的任务,如果我们再向线程池中提交任务,将会抛 RejectedExecutionException异常。 如果线程池的shutdown()方法已经调用过,重复调用没有额外效应。 注意,当我们调用shutdown()方法后,会立即从该方法中返回而不会阻塞等待线程池关闭再返回, 如果希望阻塞等待可以调用awaitTermination()方法。
shutdownNow()
首先shutdownNow()方法和shutdown()方法一样,当我们调用了shutdownNow()方法后, 调用线程立马从该方法返回,而不会阻塞等待。这也算shutdownNow()和shutdown()方法的一个相同点。 与shutdown()方法不同的是shutdownNow()方法调用后,线程池会通过调用worker线程的interrupt方法 尽最大努力(best-effort)去"终止"已经运行的任务。 而对于那些在堵塞队列中等待执行的任务,线程池并不会再去执行这些任务,而是直接返回这些等待执行的任务, 也就是该方法的返回值。 值得注意的是,当我们调用一个线程的interrupt()方法后(前提是caller线程有权限,否则抛异常), 该线程并不一定会立马退出: 1:如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),那么线程立即退出被阻塞状态, 并抛出一个InterruptedException异常。 2:如果线程处于正常的工作状态,该方法只会设置线程的一个状态位为true而已,线程会继续执行不受影响。 如果想停止线程运行可以在任务中检查当前线程的状态(Thread.isInterrupted())自己实现停止 逻辑。
如何合理的配置线程池的大小
一般需要根据任务的类型来配置线程池大小:
如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1 如果是IO密集型任务,参考值可以设置为2*NCPU
建议使用有界队列。因为有界队列能够增加系统的稳定性和预警的能力,我们可以想象一下,当我们使用无界队列的时候,当我们系统的后台的线程池的队列和线程池会越来越多,这样当达到一定的程度的时候,有可能会撑满内存,导致系统出现问题。当我们是有界队列的时候,当我们系统的后台的线程池的队列和线程池满了之后,会不断的抛出异常的任务,我们可以通过异常信息做一些事情。
当然,这只是一个参考值,具体的设置还需要根据实际情况进行调整,比如可以先将线程池大小设置为参考值,再观察任务运行情况和系统负载、资源利用率来进行适当调整。
欢迎订阅服务号