线程池知识点及面试总结

本文详细阐述了线程池的必要性,介绍了ThreadPoolExecutor的各类别(如newCachedThreadPool、newFixedThreadPool和newSingleThreadExecutor等),讲解了线程池创建、execute方法逻辑和常见阻塞队列。重点探讨了线程池的生命周期管理和不同饱和策略。
摘要由CSDN通过智能技术生成

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().

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值