1. 一个JDK Timer的例子。
  2. JDK Timer蕴含的次要对象。
  3. Timer对象剖析。
  4. TimerTask对象剖析。
  5. 任务调度:一次性定时工作。
  6. 任务调度:屡次执行的定时工作(固定工夫点或固定工夫距离)。
  7. JDK Timer是单线程的吗?
  8. Thread和Runable的区别。
  9. 优缺点。

一个JDK Timer的例子

我的项目中其实是常常须要定时工作的,JDK就提供了一个定时工作的实现Timer,然而因为JDK Timer不是很灵便(比方不能反对cron表达式的执行打算),所以我的项目中理论用的应该比拟少。

不过JDK Timer的应用非常简单。

第一步:创立Timer对象。
第二步:扩大TimerTask实现其run办法。
第三步:调用Timer对象的schedule办法创立执行打算。

@Slf4jpublic class TimerTaskDemo {    public void runTimerA(){        Timer timer1=new Timer("Timer");        long delay=10000L;        timer1.schedule(new TimerTask(){            @Override            public void run(){                log.info("This is timerTaskA:" +  Thread.currentThread().getId());                timer1.cancel();            }},delay);    }    public static void main(String[] args) {        TimerTaskDemo timerTaskDemo=new TimerTaskDemo();        log.info("There we come:" + Thread.currentThread().getId());        timerTaskDemo.runTimerA();    }

运行后果:主线程输入,10秒后定时工作执行。

21:27:17.313 [main] INFO com.example.demo.task.TimerTaskDemo - There we come:121:27:27.340 [Timer] INFO com.example.demo.task.TimerTaskDemo - This is timerTaskA:11

JDK Timer蕴含的次要对象

JDK提供了定时控制器Timer,次要包含:

  1. Timer:定时控制器。
  2. TimerTask:定时控制器被触发当前要执行的工作,是一个实现了Runable的抽象类,利用须要扩大实现TimerTask从而执行咱们的定时工作。
  3. Schedule:执行打算,理论是Timer的一个办法,依照肯定的规定绑定TimerTask到Timer。
  4. TaskQueue:工作队列,每一个Timer都蕴含一个工作队列保留工作,以便Timer一个个取出并执行工作。

Timer对象剖析

JDK 定时工作的次要对象就是这个定时器,负责定时工作的创立及调度执行。

蕴含两个重要属性:

private final TaskQueue queue = new TaskQueue();private final TimerThread thread = new TimerThread(queue);

一个是工作队列TaskQueue,另外一个是定时器线程TimerThread,两个对象都是Timer对象初始化的时候间接创立,定时器线程TimerThread持有工作队列。

Timer#TaskQueue

TaskQueue是Timer的外部类,顾名思义,是工作队列。

工作队列以数组保留,初始化长度128。

  private TimerTask[] queue = new TimerTask[128];

通过add办法将工作退出工作队列,如果队列已满则裁减队列容量(2倍),之后通过fixUp调整队列程序,确保队列尽可能依照执行工夫的先后顺序排列。

void add(TimerTask task) {        // Grow backing store if necessary        if (size + 1 == queue.length)            queue = Arrays.copyOf(queue, 2*queue.length);        queue[++size] = task;        fixUp(size);    }

TaskQueue的其余办法咱们前面在调用到的时候再做剖析。

Timer#TimerThread

TimerThread也是Timer的外部类。

TimerThread是一个线程类,扩大了Thread并笼罩了他的run办法。

class TimerThread extends Thread {  boolean newTasksMayBeScheduled = true;    private TaskQueue queue;    TimerThread(TaskQueue queue) {        this.queue = queue;    }    public void run() {        try {            mainLoop();        } finally {            // Someone killed this Thread, behave as if Timer cancelled            synchronized(queue) {                newTasksMayBeScheduled = false;                queue.clear();  // Eliminate obsolete references            }        }    }

对象实例的同时初始化了工作列表,并初始化newTasksMayBeScheduled为true。

run办法调用了一个叫mainLoop()的办法,办法名通知咱们应该是一个主循环。

咱们晓得定时工作是启动一个新线程执行工作,他可能一直定时执行的起因就是新线程启动之后不退出,期待工作打算的调度。这个mainLoop应该就是线程启动之后的期待办法,始终期待任务调度,非必要不退出。

mainLoop代码稍后剖析。

Timer对象创立

从后面的例子咱们晓得,JDK Timer定时工作首先要创立一个Timer对象,看一下Timer的实例化办法:

 public Timer(String name) {        thread.setName(name);        thread.start();    }

给TimerThread设置name,之后间接启动TimerThread。

Timer对象创立的时候就间接启动的线程,咱们下面说过的mainLoop办法就开始运行了。然而这个时候Timer的工作队列还是空的,所以mainLoop应该是只能空转。

咱们先简略看一眼mainLoop办法,验证一下:

private void mainLoop() {        while (true) {            try {                TimerTask task;                boolean taskFired;                synchronized(queue) {                    // Wait for queue to become non-empty                    while (queue.isEmpty() && newTasksMayBeScheduled)                        queue.wait();                    if (queue.isEmpty())                        break; // Queue is empty and will forever remain; die                       //省略代码...

队列是空的,并且newTasksMayBeScheduled=true,所以调用queue.wait()办法挂起队列,期待被再次唤醒。

如果队列始终为空并且newTasksMayBeScheduled始终为true,并且也不通过其余伎俩完结以后线程的话,他就会始终期待上来。

好了,是时候给工作队列喂点货色了。

TimerTask

顾名思义,TimerTask,就是工作、或者叫定时工作。

TimerTask是一个实现了Runable接口的虚构类,他并没有实现Runable的run办法,须要咱们利用去实现。

TimerTask才是咱们业务须要关注的次要指标,比方咱们须要每天晚上2点钟跑批进行账户余额的更新,那这个更新账户余额的业务办法就是要在业务对象(扩大TimerTask)的run办法中去调用。

除了须要实现run办法去调用咱们的业务逻辑之外,还有两个属性理解一下:一个lock,一个state。咱们晓得Timer是线程平安的,lock是用来在线程执行过程中更新工作状态的时候锁定工作的,state是工作状态,包含:

  1. VIRGIN:工作尚未被调度。
  2. SCHEDULED:被调度然而尚未执行。
  3. EXECUTED:曾经执行实现。
  4. CANCELLED:被勾销。

任务调度:一次性工作

一次性工作指的是工作执行一次之后就完结,咱们下面的例子就是一个一次性工作。

一次性工作通过Timer的schedule办法调度:

Params:task – task to be scheduled.delay – delay in milliseconds before task is to be executed.public void schedule(TimerTask task, long delay)

schedule办法接管两个参数:

  1. TimerTask:要执行的工作。
  2. delay:工作在delay毫秒之后触发

schedule办法执行如下动作:

  1. 为确保Timer定时工作的线程平安行,同步Timer的工作队列。
  2. 同步TimerTask的lock,并设置TimerTask的执行工夫为以后零碎工夫+delay,设置工作状态为SCHEDULED,设置工作的period=0。
  3. TimerTask退出工作队列。
  4. 从工作队列中获取待执行的工作(队首的工作),如果队首工作就是当前任务的话,调用工作队列quene的notify()办法唤醒队列。

咱们看到schedule只是将当前任务退出队列,退出队列之后工作什么工夫被执行就和schedule没有关系了,其实退出工作队列之后,schedule就完成使命了。

工作具体什么时候被执行就是咱们下面所说的那个mainLoop的事件了,咱们后面说过,在Timer被创立之后,工作队列是空的,mainLoop通过调用队列queue.wait处于无限期待命状态。

在此状态下利用通过schedule办法退出一个工作到工作队列中,并且以后队列如果只有刚被退出的这一个工作的话,就会调用notify唤醒队列。

咱们持续剖析mainLoop的残余代码,看一下工作队列被唤醒之后的逻辑。

private void mainLoop() {        while (true) {            try {                TimerTask task;                boolean taskFired;                synchronized(queue) {                    // Wait for queue to become non-empty                    while (queue.isEmpty() && newTasksMayBeScheduled)                        queue.wait();                    //从这儿开始...                                        if (queue.isEmpty())                        break; // Queue is empty and will forever remain; die                    // Queue nonempty; look at first evt and do the right thing                    long currentTime, executionTime;                    task = queue.getMin();                     synchronized(task.lock) {                        if (task.state == TimerTask.CANCELLED) {                            queue.removeMin();                            continue;  // No action required, poll queue again                        }                        currentTime = System.currentTimeMillis();                        executionTime = task.nextExecutionTime;                        if (taskFired = (executionTime<=currentTime)) {                            if (task.period == 0) { // Non-repeating, remove                   queue.removeMin();                                task.state = TimerTask.EXECUTED;                            } else { // Repeating task, reschedule                    queue.rescheduleMin(                                  task.period<0 ? currentTime   - task.period                                                : executionTime + task.period);                            }                        }                    }                    if (!taskFired) // Task hasn't yet fired; wait                        queue.wait(executionTime - currentTime);                }                if (taskFired)  // Task fired; run it, holding no locks                    task.run();            } catch(InterruptedException e) {            }        }    }

代码其实比较简单:

  1. 如果newTasksMayBeScheduled=false,其实就是接管到了当前任务执行器要完结执行的信号了,此时如果工作队列空了,就完结mainLoop,就相当于当前任务执行器线程要完结了。
  2. 否则,从工作队列中获取队首工作。
  3. 工作上锁。
  4. 查看当前任务,如果曾经被勾销的话,将工作移除队列,啥也不干了(当前任务不须要被执行)。
  5. 比拟工作执行工夫nextExecutionTime如果小于以后零碎工夫的话,执行以下步骤:
    5.1 设置taskFired=true,查看period=0则表明是一次性工作,将该工作移除队列,并设置工作状态为EXECUTED。
    5.2 否则,是周期性工作,执行queue.rescheduleMin对当前任务重排。
  6. 如果taskFired=false,则表明还没有到当前任务的执行工夫,则限时挂起当前任务队列。
  7. 否则taskFired=true则调用TimeTask的run办法执行工作。

所以咱们当初明确一次性工作之所以执行一次后就不会被再次调度的起因是,工作执行后就被移出了工作队列。周期性工作能被屡次执行的起因是每次执行后都会对该工作在工作队列中的地位进行重排!

另外,咱们还须要搞清楚一个问题:如何完结定时控制器?

这个问题其实咱们在剖析mainLoop的代码是曾经取得的一半答案:newTasksMayBeScheduled=false并且工作队列为空。

答案的另一半就是要晓得如何满足上述条件?须要从Timer控制器提供的办法中寻找,Timer提供了一个cancel办法,cancel办法在设置newTasksMayBeScheduled为false并清空工作队列之后,立即调用工作队列的notify唤醒队列、完结Timer控制器。

    public void cancel() {        synchronized(queue) {            thread.newTasksMayBeScheduled = false;            queue.clear();            queue.notify();  // In case queue was already empty.        }    }

任务调度:周期性工作

JDK Timer反对两种类型的周期性工作,一种是fixRate周期性工作,通过定时控制器Timer的scheduleAtFixedRate办法调度,另外一种是非fixRate的,通过带有period的一般的schedule办法调度。

两者有什么区别呢?

搞清楚两者区别之前,咱们须要首先理解一个概念,就是定时控制器Timer在调度工作的时候是无奈保障严格依照调度打算执行工作的(不思考工作执行时长对调度周期的影响,比方咱们假如工作被调度后霎时就能够执行实现),什么意思呢?

比方咱们通过调度办法schedule安顿在以后工夫10秒后执行一个period=10秒的定时工作,比方以后工夫正好是12:00正,那么咱们的冀望是从12点10秒执行一次工作,后续每隔10秒执行一次,现实的执行工夫就是 10秒 20秒 30秒 40秒 50秒...以此类推。

然而这个执行工夫其实Timer是没有方法保障的,因为线程挂起之后再次被唤醒是依赖于CPU的调度的,CPU在10秒执行了一次工作之后,下次工作不肯定能在20秒被唤醒,有可能是22秒或者23秒的时候才会被唤醒。

假如22秒的时候工作被唤醒,Timer在安顿执行下次工作打算的时候提供了两个选项:

  1. fixRate:如果是通过scheduleAtFixedRate办法进行调度的(此时调度器外部的period>0),下次工作安顿在30秒执行。
  2. 如果是一般的schedule办法调度的(此时调度器外部的period<0),下次工作安顿在以后零碎工夫+10秒,也就是被安顿在第32秒执行。

所以两者的区别就高深莫测了。

另外,既然Timer外部是通过period大于或小于0来管制周期性工作的执行策略的,那咱们是不是能够在调用调度办法schedule的时候通过period来管制执行策略呢?答案当然是不能够,否则了解起来就会乱套了:

   public void schedule(TimerTask task, long delay, long period) {        if (delay < 0)            throw new IllegalArgumentException("Negative delay.");        if (period <= 0)            throw new IllegalArgumentException("Non-positive period.");        sched(task, System.currentTimeMillis()+delay, -period);    }

Timer的几个变形调度办法schedule都不容许period小于0。

JDK Timer是单线程?

JDK Timer是单线程执行这种说法其实比拟含糊,须要加以解释,否则容易混同。

对于主线程来说,JDK Timer的调度以及工作执行是在新启动的线程中执行的,调度和工作执行线程是和主线程独立的线程。所以从这个角度来看的话,说JDK Timer是单线程貌似不太正当。

然而JDK Timer的任务调度是在TimerThread线程中进行的,在TimerTread的mainLoop办法中查看到工作队列中的当前任务应该被调度的时候,通过TimerTask的run办法执行工作。咱们晓得TimerTask尽管实现了Runable接口,然而在TimerThread线程中间接调用TimerTask的run办法执行工作、而不是将TimerTask再次封装在一个新的Thread中通过Thread的start办法执行工作,这样的话TimerTaks其实就是在TimerThread线程中执行,而并不会开启一个新的线程。

所以咱们的论断就是:JDK Timer通过TimerThread线程调度工作,同时也是通过TimerThread线程执行工作,调度工作和执行工作是在同一个线程中实现的。从这个角度来讲,咱们能够说JDK Timer是单线程的。

因而,如果一个TimerTask的执行工夫太长,超过了周期性工作的period的话,工作的下次执行工夫将会受到影响!

Thread和Runable的区别

以上探讨过程中其实波及到了一个Thread和Runable区别的问题,咱们明天也不是专门探讨这个问题的,但既然波及到了,就简略说一下。

这个问题尽管被大家宽泛探讨,也有可能是一个比拟广泛的Java基础知识的面试问题。然而,集体了解,这个问题基本就不应该成为一个问题,因为两者其实没有什么可比性。

Thread简直能够认为是咱们启动线程的惟一抉择,只通过Runable而不借助Thread的话,咱们是没有方法启动一个新线程的。

Runable其实只是一个简略的接口,定义了一个run办法。Thread实现了Runable接口,而且Thread有一个定义为Runable的属性target。因而能够说Thread和Runable是有分割的。

咱们启动一个线程的惟一办法还是通过Thread,咱们能够继承Thread并笼罩他的run办法,这个时候从利用的角度看,整个启动线程的过程就和Runable没有半毛钱的关系。

另外咱们还能够自定义一个业务类,实现Runable接口,而后new一个Thread对象并且把咱们自定义的业务类作为参数送给Thread对象的targe。这种形式下从利用的角度看,启动线程的过程才和Runable有了关系。

如果咱们只是自定义了一个类实现了Runable接口,然而不通过Thread绑定这个自定义的类,不是通过Thread的start办法调用自定义类的run办法、而是间接调用run办法的话(这个过程就和TimerThread通过调用TimerTask的run办法执行工作有点相似)。那么咱们尽管实现了Runable接口,也调用了run办法,然而整个过程都和“启动新线程执行工作”没有半毛钱的关系。这种状况下咱们尽管调用了Runable的run办法然而却并没有启动新线程,run办法仍然还是在原来的线程下运行!这种状况下的Runable接口就和其余一般接口没有任何区别了,他只是个定义了一个run办法的一般接口。

也看到过很多对于两者区别的探讨中提到了两种形式下对于变量是共享还是隔离拜访的说法,集体认为齐全是跑偏了。这类观点认为Thread形式实现的多线程是独占成员变量的、而通过Runable实现的多线程是共享成员变量的。看过了他们列举的例子,其实是因为Thread形式下是new了多个Thread对象所以成员变量当然隔离的,因为他们基本就别离属于不同的对象。而Runable形式下就只是new了一个Runable对象,而后new了多个Thread、启动多个线程执行的时候是把这个惟一的Runable对象传递给了Thread的target,不同线程持有的是雷同的Runable对象作为他们的target,同一个对象的成员变量当然是共享的。

JDK Timer的优缺点

长处只有一个,就是JDK自带,不须要引入内部包,应用比较简单。

毛病是应用简略,调度策略比拟繁多,不能反对cron表达式,貌似也不能反对“几点开始、执行10次”这样的需要,这类需要都须要应用层想方法管制。

如果工作执行时长大于period的话,会影响到调度工夫。

调度策略比较简单、不灵便,可能也就是导致JDK Timer不被广泛应用的起因。

上一篇 基于Mybatis的分页管制 - PageHelper分页管制底层原理