一篇线程池,肝了它!

何为线程池?

顾名思义,线程池就是管理线程的池子。可以利用线程的复用,降低资源消耗,提高响应速度。这里解释一下,一个线程从类加载到到被垃圾收回是十分消耗资源的,也是需要一定响应时间的,我们从线程池中拿一条线程复用,省去了创建到销毁的过程,降低资源消耗,响应时间也会短很多。Executor是Java并发的一个框架,它可分为两部分:任务,即业务的逻辑编写,需要代码实现Runnable接口或者Callable接口;执行任务把任务分派给多个线程的执行机制,即线程池的工作,需要实现Executor接口或者ExecutorService接口。

线程池任务

在说线程池之前,先了解一下线程池任务。通过实现Runnable接口或者Callable接口获得线程池任务。

class RunnableTest implements Runnable{
    @Override
    public void run() {

    }
}

class callableTest implements Callable<String>{
    @Override
    public String call() {
    }
}

线程池提交任务有两种形式,executesubmit,前者无返回值,后者有返回值。所以实现Runnable接口可以使用两种形式提交,实现Callable接口只能使用sumbit方式。返回值用get()方法获取。

ThreadPoolExecutor tpe = new ThreadPoolExecutor(5, 10,
                100, MILLISECONDS, new ArrayBlockingQueue<Runnable>(5));
Future<String> future = tpe.submit(new callableTest());
String result = future.get();

线程池

核心参数

线程池有7个构造参数,先了解一下,有利于明白线程池运行原理。

  • corePoolSize:核心线程数。
  • maximumPoolSize:线程池最大线程数,一般大于等于核心线程数,也可理解为最大线程数=核心线程数+非核心线程数。
  • keepAliveTime:非线程最大生存时间。
  • unit:线程空闲存活时间,我的理解这个值要大于keepAliveTime,这个值要专指核心线程。
  • workQueue:存放任务的阻塞队列。但工作的线程达到核心线程数,再过来的任务直接进入队列等待。
  • threadFactory:生成线程的工厂。
  • handler:线程池的拒绝策略。当队列满了,且工作线程达到maximumPoolSize的时候,拒绝任务的策略。
线程池工作原理

线程池工作原理.png

有任务来了,先逐个创建线程,直到线程数达到CorePoolSize时,再新来的线程回去workerQueue队列阻塞,注意,这里需要设置workerQueue最大阻塞数,不加限制很容易造成内存溢出。当workerQueue中任务已满的时候,线程池会新建线程,直到达到maximumPoolSize数,再新来的任务会直接进行拒绝策略。

关于keepAliveTime和unit时间再多说两句。假设任务逐步或者一口气被执行完,短时间内不会有任务了,我觉得线程数应该先达到核心线程数,再清零,而不是从maximumPoolSize直接清零,这样显得不是很丝滑。所以unit设置的应该比keepAliveTime大一点。

线程工厂

线程工厂需要实现ThreadFactory,从而给予线程定制,例如给线程不同的名字。

public class MyThreadFactory implements ThreadFactory {
    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r, "Thread-" + new Random().nextInt(100));
        return t;
    }
}
工作队列

线程池中的队列都是BlockingQueue(阻塞队列的实现类)。

阻塞队列.png

根据上图,可以看出BlockingQueue有4类增加、删除、检查的方法:

  1. 操作可能会抛出异常。
  2. 操作不抛异常,只返回操作是否成功。
  3. 无限期阻塞线程直至操作成功
  4. 有时间限制的阻塞。

BlockingQueue的实现类有:

  • ArrayBlockingQueue 数组型阻塞队列
  • LinkedBlockingQueue 链表型阻塞队列
  • DelayQueue 延时队列
  • SynchronousQueue 同步队列
  • PriorityBlockingQueue 优先阻塞队列
ArrayBlockingQueue

是一个数组实现的有界阻塞队列,符合FIFO,入队和出队使用同一个锁ReentrantLock,且互斥。

LinkedBlockingQueue

基于链表结构的阻塞队列,可以设置队列容积,默认最大容积为Integer.MAX_VALUE。入队和出队使用两把锁,效率较高。

DelayQueue

是一个任务有定时周期的延迟队列,每个任务都应该有一个延迟时间,到了延迟时间才会执行。put方法不阻塞。

SynchronousQueue

是一个不储存元素的阻塞队列,每次删除的时候需要等待插入操作,每次插入的时候需要等待删除操作,所以这个队列可以看作是一个通道,本身不储存元素,在多任务时,效率较快。

PriorityBlockingQueue

根据优先级排序的阻塞队列,数据结构采用二叉树。

线程池
  • newFixedThreadPool (固定数目线程的线程池)
  • newCachedThreadPool(可缓存线程的线程池)、
  • newSingleThreadExecutor(单线程的线程池)
  • newScheduledThreadPool(定时及周期执行的线程池)
newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads){
    return new ThreadPoolExecutor(
            nThreads,   // corePoolSize
            nThreads,   // maximumPoolSize == corePoolSize
            0L,         // 空闲时间限制是 0
            TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>() // 无界阻塞队列
        );
}

由源码可看出,此连接池maximumPoolSize = corePoolSize,且没有非空间时间,即没有非核心线程。队列使用LinkedBlockingQueue,且没有设置边界。使用时要注意队列中任务过多,产生内存溢出。FixedThreadPool 线程池适合执行数量较少且长期执行的任务。

newCachedThreadPool
public static ExecutorService newCachedThreadPool(){
    return new ThreadPoolExecutor(
        0,                  // corePoolSoze == 0
        Integer.MAX_VALUE,  // maximumPoolSize 非常大
        60L,                // 空闲判定是60 秒
        TimeUnit.SECONDS,
        // 神奇的无存储空间阻塞队列,每个 put 必须要等待一个 take
        new SynchronousQueue<Runnable>()  
    );
}

由源码可以看出这个线程池都是非核心线程,空闲时间为60秒,使用SynchronousQueue队列。newCachedThreadPool适用于并发量大,且没有执行周期短的任务。

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

由源码可看出,此连接池只有一个核心线程。当这唯一的线程执行的时候,新任务不断被放到队列中去,线程执行完了,就从队列中获取新的任务。该线程池适合执行串行化的任务。

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

有源码可看出,最大线程数为Integer.MAX_VALUE,核心线程数可输入,使用延迟队列。newScheduledThreadPool提交任务除了使用submitexecute外,还有三个特定的方法:

  1. schedule
  2. scheduleAtFixedRate
  3. scheduleWithFixedDelay
schedule

在设置任务的时候,可以设置延迟时间,只执行一次。

public class ScheduleThreadTest {
    public static void main(String[] args) {
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
        System.out.println("现在的时间是"+new Date());
        executorService.schedule(()->{
            System.out.println("此时的时间是"+new Date());
        },3, TimeUnit.SECONDS);// 设置时间的时候,可以设置时间类型
    }
}
// 输出内容为
现在的时间是Tue Sep 28 15:38:20 CST 2021
此时的时间是Tue Sep 28 15:38:23 CST 2021
scheduleAtFixedRate

周期性设置执行某一任务,这里周期性执行,后一次等待前一次开始后,时间间隔后开始,但是必须等前一次执行完。第二个参数是任务开始的一个延迟,第三个参数是周期间隔时间。

public class ScheduleThreadTest {
    public static void main(String[] args) {
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
        System.out.println("现在的时间是"+new Date());
        AtomicInteger atomicInteger = new AtomicInteger();
        executorService.scheduleAtFixedRate(()->{
            int index = atomicInteger.incrementAndGet();
            System.out.println("第"+index+"次"+",开始了,"+new Date());
            try {
                Thread.sleep(6000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("第"+index+"次"+",结束了,"+new Date());
        },3,5, TimeUnit.SECONDS);
    }
}
//运行结果
现在的时间是Tue Sep 28 15:57:42 CST 20211次,开始了,Tue Sep 28 15:57:45 CST 20211次,结束了,Tue Sep 28 15:57:51 CST 20212次,开始了,Tue Sep 28 15:57:51 CST 20212次,结束了,Tue Sep 28 15:57:57 CST 20213次,开始了,Tue Sep 28 15:57:57 CST 2021
scheduleWithFixedDelay

周期性设置执行某一任务,这里周期性执行,后一次必须等待前一次执行完,开始计算时间间隔,之后再执行。第二个参数是任务开始的一个延迟,第三个参数是周期间隔时间。

public class ScheduleThreadTest {
    public static void main(String[] args) {
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
        System.out.println("现在的时间是"+new Date());
        AtomicInteger atomicInteger = new AtomicInteger();
        executorService.scheduleWithFixedDelay(()->{
            int index = atomicInteger.incrementAndGet();
            System.out.println("第"+index+"次"+",开始了,"+new Date());
            try {
                Thread.sleep(6000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("第"+index+"次"+",结束了,"+new Date());
        },3,5, TimeUnit.SECONDS);
    }
}
//运行结果
现在的时间是Tue Sep 28 16:14:42 CST 20211次,开始了,Tue Sep 28 16:14:45 CST 20211次,结束了,Tue Sep 28 16:14:51 CST 20212次,开始了,Tue Sep 28 16:14:56 CST 20212次,结束了,Tue Sep 28 16:15:02 CST 20213次,开始了,Tue Sep 28 16:15:07 CST 2021
拒绝策略
  • CallerRunsPolicy - 交给调用线程池的线程处理,处理时请注意调用速度不宜过快。

  • AbortPolicy - 丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。

  • DiscardPolicy - 直接丢弃。

  • DiscardOldestPolicy - 当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入。

线程池状态切换

ThreadPoolExecutor中有一个变量ctl,记录着线程池中线程数量和线程池状态。

线程切换.png

  • RUNNING:一旦线程池被创建,就处于running状态,且池中的任务数为0。此时可以接受新任务,或者对已添加的任务进行处理。
  • SHUTDOWN:此时不接受新任务,但是可以对已添加的任务进行处理。线程池调用shutdown()方法的时候,由RUNNING状态到SHUTDOWN状态。
  • STOP:此时不接受新任务,不处理已添加的任务,并且中断正在执行的任务。线程池调用shutdownNow()时,由RUNNING状态或者SHUTDOWN到STOP状态。
  • TIDYING:此时所有任务已终止,且ctl为0。SHUTDOWN状态下队列为空,执行的任务为空时变成TIDYING状态;STOP状态下执行的任务为空时变成TIDYING状态。
  • TERMINAED:线程池彻底终止。当在TIDYING状态下,会执行钩子函数terminated(),此函数为空函数,调用者可重写实现业务逻辑。

默认标题_动态分割线_2021-09-28-0.gif

SpringBoot+SpringSecurity+jwt整合及初体验

强大的CompletableFuture

辣条君写爬虫5【恋爱综艺《心动的信号》视频下载】

好久不见,Java设计模式

@Transactional回滚问题(try catch、嵌套)

默认标题_动态横版二维码_2021-09-28-0.gif

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值