1.为什么需要线程池?
在实际使用中,线程是很占用系统资源的,对线程管理不善,容易导致系统问题。因此,在很多并发框架中,都会使用线程池来管理线程,其好处如下:
(1)使用线程池可以重复利用已有的线程继续执行任务,避免线程在创建和销毁时造成额外的消耗,从而可以提高系统响应速度。
(2)通过线程池,可以对线程进行合理的管理,根据系统的承受能力调整可以运行的线程数量。
2.线程池的分类:
ThreadPoolExecutor
(1)newCachedThreadPool :
创建一个可根据业务需要,来创建新线程的线程池,可以重用之前构造的线程,并在需要时使用提供的ThreadFactory创建新线程。
特点:
A.线程池中线程的数量没有固定,可以达到最大值(Integer.MAX_VALUE)
B.线程池中的线程可用缓存重复利用和回收(回收默认时间是1分钟)
C.当线程池中,没有可用线程,会重新创建一个新线程来执行任务。
代码:
实现一个TaskThread,任务线程类
public class TaskThread implements Runnable {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " is running");
}
}
在测试类中:
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0;i < 30;i++){
executorService.execute(new TaskThread());
}
executorService.shutdown();
(2)newFixedThreadPool:创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行池中线程。
大多数线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交任务,则在有可用线程之前,提交任务将会在队列中等待。
如果在线程池运行过程中,由于某些原因而导致任何线程终止,那么就会创建一个新线程代替终止线程执行后续任务。
特点:
A.线程池中的线程处于一定的数量,可以很好地控制线程的并发量。
B.线程可以重复使用,在显示关闭之前,线程将会一直存在。
C.超出一定量的线程被提交时,需要在队列中等待。
// newFixedThreadPool: 线程池需要指定最多能有容纳多少个线程
ExecutorService executorService = Executors.newFixedThreadPool(4);
for (int i = 20;i > 0;i--){
executorService.execute(new TaskThread());
}
executorService.shutdown();
(3)newSingleThreadExecutor :
创建一个使用单个工作线程的Executor,以无界队列的方式来运行该线程。
如果在线程池运行期间由于某些原因终止了此单个线程,然而还有后续任务需要执行,就会创建一个新线程来执行后面的任务。
可保证无需重新配置,此方法所返回的执行程序就可以使用其他的线程。
特点:线程池中最多执行1个线程,其后提交的任务将会排在阻塞队列中等待执行。
ExecutorService executorService = Executors.newSingleThreadExecutor();
// while (!executorService.isTerminated()){
for (int j = 0;j < 50;j++){
executorService.execute(new TaskThread());
}
executorService.shutdown();
ScheduledThreadPoolExecutor :可定时或延迟执行线程活动
(1)newScheduledThreadPool :
创建一个线程池,它可安排在给定延迟后运行命令或定期地执行。
特点:线程池中具有指定数量的线程,空线程也会被保留
(2)newSingleThreadScheduledExecutor
创建一个只有单线程的线程池,它可安排在给定延迟后执行命令或者定期地执行。
特点:线程池最多执行1个线程,之后提交的线程任务将会排在阻塞队列中等待执行。
代码:
在测试类中,为能看到延迟执行和定期执行的效果,可以多启动几个线程,创建多个匿名内部类来实现Runnable接口
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
System.out.println(System.currentTimeMillis());
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("1------延迟一秒执行,每三秒执行一次");
System.out.println(System.currentTimeMillis());
}
},1,2, TimeUnit.SECONDS);
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("2------延迟一秒执行,每两秒执行一次");
System.out.println(System.currentTimeMillis());
}
},1,2, TimeUnit.SECONDS);
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("3-------延迟一秒执行,每两秒执行一次");
System.out.println(System.currentTimeMillis());
}
},1,2, TimeUnit.SECONDS);
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("4--------延迟一秒执行,每两秒执行一次");
System.out.println(System.currentTimeMillis());
}
},1,2, TimeUnit.SECONDS);
// shutdown方法关闭线程池,可以避免资源的浪费
scheduledExecutorService.shutdown();
}
ForkJoinPool:
newWorkStealingPool:
创建一个带并行级别的线程池,并行级别决定了同一时刻最多有多少个线程在执行,如果不传入并行级别参数,就会默认并行级别为当前系统的CPU个数。
3.线程池的创建:
// ThreadPoolExecutor类的构造方法如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) { ..... }
构造器的参数说明:
corePoolSize :核心线程池 能够创建线程的数量
maximumPoolSize:线程池能创建线程的最大个数
keepAliveTime:(空闲线程的存活时间),线程池的工作线程空闲后,保持存活的时间。所以,如果 任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。
unit:为keepAliveTime指定时间单位,可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和纳秒 (NANOSECONDS,千分之一微秒)。
workQueue:用于保存任务的阻塞队列(任务队列),相当于生产者-消费者模式中的传输通道。
threadFactory:创建线程的工厂类
handler:饱和策略/拒绝策略,队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常(RejectedExecutionException)。
注:当提交一个任务到线程池时,如果当前 poolSize < corePoolSize 时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程,这样可以减少任务被线程池处理时所需的等待时间。
4.execute( )的执行逻辑:
(1)如果当前运行的线程数量少于corePoolSize,就会创建新的线程来执行新的任务。
(2)如果当前运行的线程数量等于或大于corePoolSize,就会将提交的任务加入到阻塞队列workQueue中。
(3)如果当前阻塞队列已满的话,就会创建新的线程来执行任务。
(4)如果线程数量超过了maximumPoolSize,就会交由饱和策略RejectedExecutionHandler来处理。
饱和策略/拒绝策略:
(1)ThreadPoolExecutor.AbortPolicy :丢弃任务并抛出RejectedExecutionException异常,是默认的饱和策略。
(2)ThreadPoolExecutor.DiscardPolicy:丢弃任务但不会抛出异常。
(3)ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务,在重复此过程
(4)ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。
5.线程池的工作原理(执行需要提交的任务过程):
线程池的执行过程:
(1)先判断线程池中核心线程池所有的线程是否都在执行任务,如果不是所有线程都在执行,则创建一个线程执行刚提交的任务,否则,进行第(2)步
(2)判断当前阻塞队列是否已满,如果未满,则将提交的任务放置在阻塞队列中,如果阻塞队列没满,则进入第(3)步
(3)判断线程池中所有的线程是否都在执行任务,如果没有,则创建一个新的线程来执行任务,否则,就交由饱和策略进行处理。
6.线程池的生命周期
(1)各个状态的说明:
RUNNING:能接受新提交的任务,还能处理阻塞队列中的任务。
SHUTDOWN:关闭状态,不再接受新提交的任务,但可以继续处理阻塞队列中已经保存的任务。
STOP:不能接受新任务,也不处理队列中的任务,会中断正在处理任务线程。
TIDYING:如果所有的任务都已经终止了,workerCount(有效线程数)为0,线程池进入TIDYING状态后,会调用terminate( )方法进入TERMINATED状态。
(从SHUTDOWN状态到TIDYING阻塞队列为空,线程池中工作的线程数量为0 )
TERMINATED: 默认terminated( )方法中什么也不会做
(2)线程池的关闭
关闭线程池,可以通过shutdown和shutdownNow两个方法来实现
步骤:遍历线程池中的所有线程,然后依次中断线程
a.shutdownNow :将线程池的状态设置为STOP,然后,中断所有正在执行和未执行任务的线程,返回等待执行任务的列表。(太过暴力,不推荐)
b.shutdown:只是将线程池中的线程设置SHUTDOWN状态,让正在执行的线程继续执行直至完毕,中断所有还未执行任务的线程。
7.线程池中常用的几种阻塞队列(任务队列):
ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。
LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于 ArrayBlockingQueue。静态工厂方法固定线程池和单例线程池使用了这个队列。
DelayQueue:使用该队列向线程池提交的任务不会被真正地保存,而总是将新任务提交给线程执行,如果没有空闲的线程,则尝试创建新的线程。
PriorityBlockingQueue:一个具有优先级的阻塞队列。该队列可以控制任务的执行先后顺序,其他队列都是按照先进先出算法处理任务的,而PriorityBlockingQueue则根据任务自身的优先级顺序先后执行。
SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法
面试题补充 : 如果提交任务时,线程池队列已满会发生什么?
如果使用的是无界队列LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务.
如果使用的是有界队列比如ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,会根据maximumPoolSize的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue继续满,那么则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy.
8. 实现Callable接口和Runnable接口的区别:
注:submit方法是基方法Executor.execute(Runnable …)的延伸,通过创建并返回一个Future类对象,可用于取消执行或等待完成。
代码:
import java.util.concurrent.Callable;
public class Task implements Callable<String> {
private int i;
public Task(int i){
this.i = i;
}
@Override
public String call() throws Exception {
//允许将异常向上抛出或在内部用try-catch进行处理
Thread.sleep(500);
return Thread.currentThread().getName() + " is running" + i;
}
}
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class CallableDemo {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 20; i++) {
Future<String> submit = executorService.submit(new Task(9));
try {
String s = submit.get();
System.out.println(s);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
executorService.shutdown();
}
}
面试补充:
执行execute()方法和submit()方法的区别:
execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否。execute方法中的参数为线程对象,该对象实现Runnable接口
submit()方法用于提交需要返回值的任务。线程池会返回一个Future类型的对象,通过这个Future对象可以判断任务是否执行成功,并且可以通过Future的get()方法来获取返回值.
submit方法中的参数为线程对象,该线程对象实Callable接口
注意:Future的get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
客户端代码应该尽可能早地向线程池提交任务,并仅在需要相应任务的处理结果数据的那一刻才调用Future.get().