并发编程之定时任务&定时线程池

1、定时线程池

1.1、定时线程池的由来

ScheduledThreadPoolExecutor定时线程池是继承自 ThreadPoolExecutor 类和实现了 ScheduledExecutorService类。
如下图所示:

在这里插入图片描述

1.2、ScheduledThreadPoolExecutor的api

1.2.1、schedule

schedule api是将任务延迟执行,如下面代码,run方法里面的代码将会在线程启动后5s后执行,代码如下:

private static Logger logger = Logger.getLogger(Log.class.getName());
public static void main(String[] args) {
    ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1);
    logger.info("开始的时候!");
    
    scheduledThreadPoolExecutor.schedule(new Runnable() {
        @Override
        public void run() {
            logger.info("我要延迟5s执行!");
        }
    },5000, TimeUnit.MILLISECONDS);
}

程序执行结果截图:
17s = => 22s

在这里插入图片描述

1.2.2、scheduleAtFixedRate

它有两个参数,
第一个参数是线程再启动后延迟多长执行
第二个参数是循环执行的周期是多长时间
下面这个就是在线程启动后延迟一秒,然后以两秒的周期循环执行

private static Logger logger = Logger.getLogger(Log.class.getName());
public static void main(String[] args) {
    ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1);
    logger.info("开始的时候!");
    
    scheduledThreadPoolExecutor.scheduleAtFixedRate(new Runnable() {
        @Override
        public void run() {
            logger.info("执行。。。。");
        }
    },1000,2000,TimeUnit.MILLISECONDS);
}

执行结果去下图所示:

在这里插入图片描述


问题:如果任务执行的时间大于间隔周期的时间怎么办?

任务会在阻塞队列里面排队,当上一个任务执行完成之后刚空出来就会去执行下一个任务,代码如下所示:

private static Logger logger = Logger.getLogger(Log.class.getName());
public static void main(String[] args) {
    ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1);
    logger.info("开始的时候!");

    scheduledThreadPoolExecutor.scheduleAtFixedRate(new Runnable() {
        @Override
        public void run() {
            logger.info("执行。。。。");
            long start = System.currentTimeMillis();
            while(true){
                long end = System.currentTimeMillis();
                if(end - start > 5000){
                    break;
                }
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    },1000,2000,TimeUnit.MILLISECONDS);
}

执行结果如下图所示:

在这里插入图片描述

注意:这里虽然是5秒执行一个任务,但是在阻塞队列中一定排了很多任务,即使我们将线程池扩大,也无济于事,因为我们一个任务只能由一个线程执行。

1.2.3、scheduleWithFixedDelay

由于在很多时候我们不知道一个任务到底能执行多长时间,所以就有了下面这个api,就是当我们上一个任务执行完成之后,再间隔一段时间去进行下一次循环,代码如下所示:

private static Logger logger = Logger.getLogger(Log.class.getName());
public static void main(String[] args) {
    ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1);
    logger.info("开始的时候!");

    scheduledThreadPoolExecutor.scheduleWithFixedDelay(new Runnable() {
        @Override
        public void run() {
            logger.info("执行。。。。");
            long start = System.currentTimeMillis();
            while(true){
                long end = System.currentTimeMillis();
                if(end - start > 5000){
                    break;
                }
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    },1000,2000,TimeUnit.MILLISECONDS);
}

执行截图如下所示:
任务基本7s执行一次

在这里插入图片描述

1.3、定时线程池应用场景

1、Redis的分布式锁
如果我们的一个Redis有很多的客户端,当一个客户端拿到一个锁之后,突然挂掉了,这个时候我们这个锁资源会有一个过期时间,正常情况下是可以满足的,但是如果说我们这块的业务逻辑比较复杂的话,这个过期时间还不够,那么就会存在别的线程获取到当前锁资源的情况,所以这个时候就可以用到我们的这个定时线程池了,我们这个定时线程池在隔一段时间检测一下这个锁,如果还存在就给他续上几秒,直至这个锁被释放,这样,就无论你这个任务执行多久,都会保证把这个任务执行完之后,锁才会释放。


2、zk注册中心与服务发现
当我们的项目是微服务架构的话,我们每个系统只用去访问zk,zk会根据我们上送的服务名去自动分配活着的相应服务,而这些活着的服务信息是怎么来的呢,就是他们会每隔一段时间将自己的配置信息等情况发送给zk,zk拿到之后做管理,后续再相应其它服务的分配。这种隔一段时间就上送自己信息的方式就可以用ScheduledThreadPoolExecutor定时线程池做。


3、定期刷新缓存等等。

2、定时线程池源码简析

下面我以scheduleAtFixedRate api举例,其它的都差不多
首先我们进入到这个方法

2.1、scheduleAtFixedRate

scheduleAtFixedRate的构造方法
1、对任务进行排序。
2、将任务存储起来。
3、走延时执行方法。

public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit) {
    if (command == null || unit == null)
        throw new NullPointerException();
    if (period <= 0)
        throw new IllegalArgumentException();
    // 通过内部的一个包装,实现任务的排序,排序的方式是堆
    ScheduledFutureTask<Void> sft =
        new ScheduledFutureTask<Void>(command,
                                      null,
                                      triggerTime(initialDelay, unit),
                                      unit.toNanos(period));
    // 里面什么都没干,留给拓展用
    RunnableScheduledFuture<Void> t = decorateTask(command, sft);
    // 将当前任务存储在 outerTask 成员变量中
    sft.outerTask = t;
    // 执行延时执行
    delayedExecute(t);
    return t;
}

2.2、delayedExecute

延时执行方法
1、如果线程池已挂掉,执行拒绝策略。
2、将任务放进队列中,再次判断线程池的状态,如果没问题,则找线程执行。

private void delayedExecute(RunnableScheduledFuture<?> task) {
	// 如果线程池已挂掉,执行拒绝策略
    if (isShutdown())
        reject(task);
    else {
    	// 否则,将任务直接放在队列中
        super.getQueue().add(task);
        // 再次判断线程池的状态是否有问题
        if (isShutdown() &&
            !canRunInCurrentRunState(task.isPeriodic()) &&
            remove(task))
            task.cancel(false);
        else
        	// 若没有问题,创建线程,来执行任务
            ensurePrestart();
    }
}

2.3、ensurePrestart

主要就是创建线程去执行任务

void ensurePrestart() {
	// 取出线程池中的线程数量
    int wc = workerCountOf(ctl.get());
    // 如果小于核心线程数,创建一个核心线程来执行任务
    if (wc < corePoolSize)
        addWorker(null, true);
    // 工作线程都等于0了还没有核心线程数大,那核心线程数的配置也只能是0了
    // 能走到这块说明核心线程数参数为0,但是任务还是要执行,所以启动一个非核心线程
    else if (wc == 0)
        addWorker(null, false);
    // 表示核心线程已经创建满,则等待核心线程腾出来再执行,因为前面已经加入到队列里了
}

2.4、ScheduledThreadPoolExecutor的run方法

在addWorker里面进行一系列的状态判断,最终启动线程,由于刚才在创建线程的时候传入的参数为this,所以需要执行ScheduledThreadPoolExecutor里面的run方法。

public void run() {
	// 取出周期值
    boolean periodic = isPeriodic();
    // 判断当前线程的状态是否正常
    if (!canRunInCurrentRunState(periodic))
        cancel(false);
    // 如果不是周期的走这个,只执行一次的那种
    else if (!periodic)
        ScheduledFutureTask.super.run();
    // 如果是周期的,走这个,循环执行的那种
    // ScheduledFutureTask.super.runAndReset() 执行具体任务并重置
    else if (ScheduledFutureTask.super.runAndReset()) {
    	// 下一次执行时间
        setNextRunTime();
        // 为下一次执行做准备
        reExecutePeriodic(outerTask);
    }
}

2.5、reExecutePeriodic

void reExecutePeriodic(RunnableScheduledFuture<?> task) {
        if (canRunInCurrentRunState(true)) {
        	// 添加到队列
            super.getQueue().add(task);
            // 判断线程状态是否正常
            if (!canRunInCurrentRunState(true) && remove(task))
                task.cancel(false);
            else
            	// 继续走2.3
                ensurePrestart();
        }
    }

10、辅助知识

10.1、当任务中抛出异常时各定时任务的处理

10.1.1、ScheduledThreadPoolExecutor的处理

private static Logger logger = Logger.getLogger(Log.class.getName());
public static void main(String[] args) {
    ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1);
    logger.info("开始的时候!");

    scheduledThreadPoolExecutor.scheduleWithFixedDelay(new Runnable() {
        @Override
        public void run() {
            logger.info("执行。。。。");
            throw new RuntimeException();
        }
    },1000,2000,TimeUnit.MILLISECONDS);
}

执行截图如下:
我们会发现线程就卡在这里了,这是因为在线程池底层给我们把异常捕获到,然后另外创建了一个线程,虽然说线程池还存在,线程也存在,但是任务却被丢弃了,所以显示就是卡在这里了。

在这里插入图片描述

10.1.2、Timer的处理

代码示例如下:

private static Logger logger = Logger.getLogger(Log.class.getName());
public static void main(String[] args) {
    ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1);
    logger.info("开始的时候!");

    Timer timer = new Timer();
    timer.scheduleAtFixedRate(new TimerTask() {
        @Override
        public void run() {
            logger.info("执行。。。。");
            throw new RuntimeException();
        }
    },1000,2000);
}

执行截图如下:

在这里插入图片描述

我们会发现直接报错了,程序都已经停止运行了,因为它直接把线程给了一个成员变量,所以抛出了一个异常导致问题挺严重的,所以很多规范也不提倡我们用Timer。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值