Java常见线程池简介、线程池创建、线程池原理、死锁产生原因及排查

什么是线程池?

线程池可以简单理解为是管理的线程的池子,创建线程的方式除了继承Tread、实现Runnable、实现Callable这三种方式外,还可以直接从线程池中获取,线程池的作用是:

  1. 降低资源消耗,通过重复利用已创建的线程,来降低线程创建和销毁造成的消耗。
  2. 提高响应速度,当任务到达时,任务不需要等待线程创建,直接从线程池中拿肯定会快一些。
  3. 提高线程的可管理性,限制系统中执行线程的数量,根据系统的环境情况,灵活设置线程数量,达到运行的最佳效果,少了浪费了系统资源,多了造成系统拥挤效率不高。

什么是阻塞队列

阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列。
当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒。

支持以上两种阻塞场景的队列我们称之为阻塞队列。

线程池的工作队列

  1. ArrayBlockingQueue
    由数组结构实现的有界阻塞队列,按FIFO排序量。

  2. LinkedBlockingQueue
    由链表结构实现的有界阻塞队列,默认长度为Integer.MAX_VALUE,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQuene。newFixedThreadPool线程池使用了这个队列。

  3. DelayQueue
    是一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。newScheduledThreadPool线程池使用了这个队列。

  4. SynchronousQueue
    一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene。newCachedThreadPool线程池使用了这个队列。

阻塞队列参考博文

线程池执行流程

在这里插入图片描述

  1. 线程池创建后,等待任务提交过来。
  2. 当调用线程池execute()方法添加一个任务是,线程池会做如下判断:
    1. 如果正在运行的线程数量小于核心线程数(corePoolSize),马上创建线程运行这个任务。
    2. 如果正在运行的线程数量大于或等于核心线程数(corePoolSize),将这个任务放入队列。
    3. 如果队列满了,且正在运行的线程数量小于最大线程数(maximumPoolSize),创建非核心线程执行此任务。
    4. 如果队列满了,且正在运行的线程数量大于或等于最大线程数(maximumPoolSize),线程池会采用饱和策略来执行。
  3. 当一个线程完成任务后,它会从队列中获取下一个任务来执行。
  4. 当一个线程无事可做超过一定时间(keepAliveTime)时,如果当前运行的线程数小于核心线程数(corePoolSize),那么这个线程会被停掉,在线程池的所有任务完成后,线程池最终会收缩到核心线程数(corePoolSize)的大小。

几种常见的线程池

newFixedThreadPool

定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

Executors.newFixedThreadPool()创建线程源码:

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

核心线程数和最大线程数大小一样,空闲时间为0,阻塞队列为无界队列LinkedBlockingQueue。

执行流程:

  1. 如果线程数少于核心线程,创建核心线程执行任务
  2. 如果线程数大于等于核心线程,把任务添加到LinkedBlockingQueue阻塞队列
  3. 如果线程执行完任务,去阻塞队列取任务,继续执行。

适用场景:
FixedThreadPool 适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。

newCachedThreadPool

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

    public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>(),
                                      threadFactory);
    }

核心线程为0,所以任务直接加到SynchronousQueue队列,最大线程数量为Integer.MAX_VALUE,空闲时间为0。

执行流程:

  1. 判断是否有空闲线程,如果有,就取出任务执行。
  2. 如果没有空闲线程,就新建一个线程执行。
  3. 执行完任务的线程,还可以存活60秒,如果在这期间,接到任务,可以继续活下去,否则被销毁。

适用场景:
用于并发执行大量短期的小任务。

newSingleThreadExecutor

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

    public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));
    }

核心线程数为1,最大线程数也为1,空闲时间为0,阻塞队列是LinkedBlockingQueue。

执行流程:

  1. 判断线程池是否有一条线程在,如果没有,新建线程执行任务
  2. 如果有,将任务加到阻塞队列
  3. 当前的唯一线程,从队列取任务,执行完一个,再继续取,一个人(一条线程)夜以继日地干活。

使用场景:
适用于串行执行任务的场景,一个任务一个任务地执行。

newScheduledThreadPool

定长线程池,支持定时、延时及周期性任务执行。

    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }

最大线程数为Integer.MAX_VALUE,阻塞队列是DelayedWorkQueue,空闲时间为0

线程池内方法:

//按某种速率周期执行
scheduleAtFixedRate() 
//在某个延迟后执行
scheduleWithFixedDelay()

使用场景:
周期性执行任务的场景,需要限制线程数量的场景

常见线程池参考博文

底层原理

以上线程池的创建都是通过Executors.newScheduledThreadPool(10)这样的方式创建的,实际上每个线程池的创建都是创建的ThreadPoolExecutor对象,源码:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

参数简介

  • corePoolSize
    线程池中的常驻核心线程数,创建线程池后,当有请求任务来之后,就会安排池中的核心线程去执行请求任务,当线程池中的线程数量达到corePoolSize之后,就会把到达的任务放到缓存队列当中,当队列满了就会创建非核心线程执行任务。
  • maximumPoolSize
    线程池能够容纳同时执行的最大线程数,必须大于等于1
  • keepAliveTime
    多余的空闲线程的存活时间,当线程池中的线程数量达到corePoolSize之后,就会把到达的任务放到缓存队列当中,当队列满了就会创建非核心线程执行任务,非核心线程执行完任务后,当空闲时间达到KeepAliveTime值时,多余的空闲线程会被销毁直到只剩下corePoolSize数量的线程池
  • unit
    keepAliveTime的单位
  • workQueue
    阻塞队列类别
  • threadFactory
    表示生产线程池中线程的线程工厂,用于创建线程,默认即可
  • handler
    拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数时怎么处理

饱和拒绝策略

  1. AbortPolicy
    线程池默认的拒绝策略,会丢弃任务,并抛出 RejectedExecutionException 异常。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。
  2. DiscardPolicy
    直接丢弃,不处理也不抛异常,如果允许任务丢失,这是最好的一种方案。
  3. DiscardOldestPolicy
    当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入。
  4. CallerRunsPolicy
    当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大

创建线程池

上述是通过Executors.newScheduledThreadPool(10)方式创建的线程池,这样做的缺点很明显,比如newFixedThreadPool是采用的LinkedBlockingQueue,LinkedBlockingQueue的默认长度是Integer.MAX_VALUE,即2^31-1,21亿多,如果放任队列一直堆积任务的话,可能会导致OOM异常,所以不采用这种方式创建线程池,使用ThreadPoolExecutor自己创建线程池,可以灵活的定制队列的长度。

    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
    		//核心线程数量
            2,
            //最大线程数量
            5,
            //空闲时间
            1L,
            //空间时间的单位
            TimeUnit.SECONDS,
            //阻塞队列列表
            new LinkedBlockingDeque<Runnable>(3),
            //使用Executors.newScheduledThreadPool()默认的线程工厂
            Executors.defaultThreadFactory(),
            //饱和拒绝策略,灵活配置
            new ThreadPoolExecutor.AbortPolicy());

什么是死锁

死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉,它们都将无法推进下,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性较低,否则会因为争取有限的资源而陷入死锁

手写一个死锁

class Thread1 implements Runnable {
    private String lockA;
    private String lockB;

    public Thread1(String lockA, String lockB){
        this.lockA = lockA;
        this.lockB = lockB;
    }

    @Override
    public void run() {
        synchronized (lockA) {
            System.out.println(Thread.currentThread().getName()+"自己持有锁:"+lockA+",尝试获取:"+lockB);
            synchronized (lockB) {
                System.out.println(Thread.currentThread().getName()+"获取到了"+lockB);
            }
        }
    }
}

@Slf4j
public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        String lockA = "lockA";
        String lockB = "lockB";

        new Thread(new Thread1(lockA, lockB),"AAAA").start();
        new Thread(new Thread1(lockB, lockA),"BBBB").start();
    }
}

排查死锁

在这里插入图片描述
在这里插入图片描述
除此之外,还可以用jvisualvm工具查看系统运行情况,当系统里出现死锁的情况,jvisualvm会提示,如图:
jvisualvm提示
点击“线程dump”后,出现的提示和我们自己用jstack指令一致
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值