咱们今天也来说说定时器Timer

Timer是什么?

Timer  n. [电子] 定时器;计时器;计时员

从翻译来看,我们可以知道Timer的本意是,定时定点。

而JDK中Timer类也的确是这个本意。那么接下来,我们通过JDK中的源码来学习下Timer这个类。

  1. private final TaskQueue queue = new TaskQueue();
  2. private final TimerThread thread = new TimerThread(queue);

Timer中有这样两个变量。这两个变量是Timer类中,最重要的三个变量中的两个。一个是Queue,它的作用是作为一个队列,来存放添加到Timer类中的任务,但是他不是一个简单的队列,后续我会通过代码来讲(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )述他的原理,这里先提前说明下,这个Queue设计的非常巧妙。另外一个是TimerThread,他的作用是Timer的主线程,无论是循环,还是执行都与这个线程密不可分,后续我们也会说到他。

Timer三巨头

接下来是一个final 引用ThreadReaper。

  1. private final Object threadReaper = new Object() {
  2. protected void finalize() throws Throwable {
  3. synchronized(queue) {
  4. thread.newTasksMayBeScheduled = false;
  5. queue.notify(); // In case queue is empty.
  6. }
  7. }
  8. };

Reaper翻译为n. 收割者;收割机;收获者;死神,死

这里这个对象可以理解为线程收割者。这个引用在Timer中,没有再次使用,只是纯定义,目的就是在Timer回收之前,优先执行这个引用复写的finalize方法。方法的内容是置变量“是否能添加新任务”设定为false,同时唤醒timerthread线程,他们的作用,我后续会说。说真的,这种写法我觉得并不好,而且诸如effective java,等书也并不推荐这种写法。

  1. private final static AtomicInteger nextSerialNumber = new AtomicInteger(0);
  2. private static int serialNumber() {
  3. return nextSerialNumber.getAndIncrement();
  4. }

接下来 serialNumber()的方法是,生成一个依次增长的变量。比如第一次调用时,返回0,接着返回1,2,3....。这种方法我觉得要比弄一个i++来用,更安全也更优雅,有兴趣的同学查下API,看看他的使用方法。

接下来是4个构造函数:

  1. public Timer() {
  2. this("Timer-" + serialNumber());
  3. }
  4. public Timer(boolean isDaemon) {
  5. this("Timer-" + serialNumber(), isDaemon);
  6. }
  7. public Timer(String name) {
  8. thread.setName(name);
  9. thread.start();
  10. }
  11.  
  12. public Timer(String name, boolean isDaemon) {
  13. thread.setName(name);
  14. thread.setDaemon(isDaemon);
  15. thread.start();
  16. }

这4个构造函数没什么主要讲的,也就是如果被主动设定线程名字后,主线程timerThread是直接启动的,另外就是是否要设置isDeamon 属性,他的作用是用来设置是否为守护线程的。对于服务器这种大型程序来说,作用不大,一般是脚本程序的话,有必要设定这个值。

接下来是6个很重要的公有方法:

(1)delay毫秒后,执行task任务

  1. public void schedule(TimerTask task, long delay) {
  2. if (delay < 0)
  3. throw new IllegalArgumentException("Negative delay.");
  4. sched(task, System.currentTimeMillis()+delay, 0);
  5. }

(2)在time时间点,执行task任务

  1. public void schedule(TimerTask task, Date time) {
  2. sched(task, time.getTime(), 0);
  3. }

(3)delay毫秒时间点执行,并且以周期是period毫秒来执行

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

(4)firstTime时间点第一次执行该任务,并且每次以period为周期执行

  1. public void schedule(TimerTask task, Date firstTime, long period) {
  2. if (period <= 0)
  3. throw new IllegalArgumentException("Non-positive period.");
  4. sched(task, firstTime.getTime(), -period);
  5. }

(5)delay毫秒后执行任务,然后周期是period

  1. public void scheduleAtFixedRate(TimerTask task, long delay, long period) {
  2. if (delay < 0)
  3. throw new IllegalArgumentException("Negative delay.");
  4. if (period <= 0)
  5. throw new IllegalArgumentException("Non-positive period.");
  6. sched(task, System.currentTimeMillis()+delay, period);
  7. }

(6)firstTime时间点第一次执行该任务,并且每次以period为周期

  1. public void scheduleAtFixedRate(TimerTask task, Date firstTime,
  2. long period) {
  3. if (period <= 0)
  4. throw new IllegalArgumentException("Non-positive period.");
  5. sched(task, firstTime.getTime(), period);
  6. }

前两个方法是不反复执行的,没什么讲的,中间两个方法是反复执行,但是名字没有加atFixedRate(以固定频率)的,最后两个加了atFixedRate。3、4和5、的区别是在处理period时,前者传入了相反数(也就是负数)后者传入了正数。(看源代码的时候,我才突然在记忆的深刻想起,java中相反数直接加负号就可以了。然后想起了这种很萌的形式o-=-o;)

  1. private void sched(TimerTask task, long time, long period) {
  2. if (time < 0)
  3. throw new IllegalArgumentException("Illegal execution time.");
  4.  
  5. // Constrain value of period sufficiently to prevent numeric
  6. // overflow while still being effectively infinitely large.
  7. if (Math.abs(period) > (Long.MAX_VALUE >> 1))
  8. period >>= 1;
  9.  
  10. synchronized(queue) {
  11. if (!thread.newTasksMayBeScheduled)
  12. throw new IllegalStateException("Timer already cancelled.");
  13.  
  14. synchronized(task.lock) {
  15. if (task.state != TimerTask.VIRGIN)
  16. throw new IllegalStateException(
  17. "Task already scheduled or cancelled");
  18. task.nextExecutionTime = time;
  19. task.period = period;
  20. task.state = TimerTask.SCHEDULED;
  21. }
  22.  
  23. queue.add(task);
  24. if (queue.getMin() == task)
  25. queue.notify();
  26. }
  27. }

这个方法的主要作用是将任务添加到任务队列中。并且设置

在方法开始的地方,判断周期是否小于long的最大值,如(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )果超过的话,那么就对周期除以2,防止后续使用周期的地方,出现运算溢出。

紧接着锁定任务队列,并且开始判断当前主线程是否还计划执行新任务。注意这个变量是在被回收,以及下文任务被取消掉的时候被改变的。接着锁定这个新任务,将下次执行的时间和周期以及状态赋值到这个上。状态标识为“计划中-TimerTask.SCHEDULED”,接下来将任务添加到任务队列中。同时获取任务队列中的最近时间点的任务,如果发现这个任务就是新添加的任务的话,那么就唤醒当前队列上等待wait的线程。

这里需要先说明一下,队列的添加,和获取最近时间点的方法,非常巧妙,会在后续的方法中详细讲述。

接着是取消方法:

  1. public void cancel() {
  2. synchronized(queue) {
  3. thread.newTasksMayBeScheduled = false;
  4. queue.clear();
  5. queue.notify(); // In case queue was already empty.
  6. }
  7. }

这个方法的作用的是,取消当前的定时器,他的核心内容是前文中回收timer调用的析构的内容是一样的。这几个变量的使用在上一个方法:sched()已经被使用到。也就是设定主线程不允许增加新任务。同时清除队列的所有任务。接着唤醒队列上所有等待的线程。

  1. public int purge() {
  2. int result = 0;
  3.  
  4. synchronized(queue) {
  5. for (int i = queue.size(); i > 0; i--) {
  6. if (queue.get(i).state == TimerTask.CANCELLED) {
  7. queue.quickRemove(i);
  8. result++;
  9. }
  10. }
  11.  
  12. if (result != 0)
  13. queue.heapify();
  14. }
  15.  
  16. return result;
  17. }

purge vt. 净化;清洗;通便

方法名字的意思是清理,清除。

方法的处理逻辑是:锁定任务队列,判断队列中的子任务状态,如果发现任务状态被取消了,那么就在队列中快速移除掉该任务,同时记录移除子任务的个数。如果发现有子任务被移除,最后会把队列再重新堆化。同时返回删除的子任务个数。这个方法(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )是Timer中的最后一个方法。回顾前文中的所有方法,我们发现Timer中并没有定义删除子任务的方法。而唯一可以删除的形式,就是设定子任务状态,然后调用purge()方法进行一次洗牌。这种做法和JVM GC中标记回收有点异曲同工之处。倘若将回收的方法,公开出来,则Timer内部需要提供很健壮的任务管理机制,防止在高并发的情况下,队列维持的堆不会出现数据错误,或性能问题(想一下如果有大量的移除操作,那么每个移除操作都需要同步队列,然后重新堆化)。

讲完了Timer类之后,我们来讲讲TimerThread。

这个类是定时器的主执行线程,所有的的子任务执行都是由这个线程来操刀的,形象一点就是,他才是幕后的“大boss”

这个类继承自Thread

在类的内部定义了两个全局变量

  1. boolean newTasksMayBeScheduled = true;
  2. private TaskQueue queue;

定义的作用,不再赘述,后续方法也会用到。

接下来是TimerThread的构造方法:

  1. TimerThread(TaskQueue queue) {
  2. this.queue = queue;
  3. }

由于TimerThread继承自Thread,因此TimerThread中也肯定有实现run方法:

  1. public void run() {
  2. try {
  3. mainLoop();
  4. } finally {
  5. // Someone killed this Thread, behave as if Timer cancelled
  6. synchronized(queue) {
  7. newTasksMayBeScheduled = false;
  8. queue.clear(); // Eliminate obsolete references
  9. }
  10. }
  11. }

在run方法中,会调用另外一个mainLoop()的主循环方法。

并且在调用后(更准确的说应该是捕捉到异常后),会置允许新增子任务变量为false.同时清空子任务队列。注意run()方法被调用的时机,是在Timer被创建时就启动的。

接下来是主循环方法,这个方法是Timer方法中非常核心的一个方法。同时由于方法比较长,我直接在方法中添加注释,来解释方法。

  1. /**
  2. * The main timer loop. (See class comment.)
  3. */
  4. private void mainLoop() {
  5. while (true) {//不断循环获取下一个任务
  6. try {
  7. TimerTask task;
  8. boolean taskFired;
  9. synchronized(queue) {//锁定队列
  10. // Wait for queue to become non-empty
  11. while (queue.isEmpty() && newTasksMayBeScheduled)//如果队列为空,并且还允许添加子任务的话
  12. queue.wait();//当前线程(timerThread)进入等待,等待队列中添加对象,或timer被取消时,唤醒
  13. if (queue.isEmpty())//唤醒之后,如果队列为空,那么就退出主循环了,一般这时候timer都是被取消了
  14. break; // Queue is empty and will forever remain; die
  15.  
  16. // Queue nonempty; look at first evt and do the right thing
  17. long currentTime, executionTime;//可以运行到这里,说明队列中包含子任务,需要开始考虑执行了
  18. task = queue.getMin();//获取队列中,执行时间最靠前的子任务
  19. synchronized(task.lock) {
  20. if (task.state == TimerTask.CANCELLED) {
  21. queue.removeMin();//如果发现最靠前的子任务已经被取消了,那么从队列中移除掉他,并且进入到下次循环中。
  22. continue; // No action required, poll queue again
  23. }
  24. currentTime = System.currentTimeMillis();//获取当前执行时间
  25. executionTime = task.nextExecutionTime;//获取子任务的下一次执行时间(其实就是本次要执行的时间点,因为还没有执行)
  26. if (taskFired = (executionTime<=currentTime)) {//如果子任务的下次执行时间点,小于当前时间
  27. if (task.period == 0) { // Non-repeating, remove
  28. //如果当前任务没有循环周期的话
  29. queue.removeMin();//队列中移除最前子任务(其实就是当前任务)
  30. task.state = TimerTask.EXECUTED;//将任务状态设定为已执行
  31. } else { // Repeating task, reschedule
  32. //如果当前任务,是需要循环执行的
  33. queue.rescheduleMin(//队列重新设定最前任务,并且当前子任务的执行时间发生变化,变化规则如下:如果周期是负值(添加子任务采用的无fixed后缀的方法),那么下次执行时间是当前时间点+周期时间。换句话说就是等待时间为(所有)任务执行时间+ 等待周期。而如果周期为正值(添加子任务采用的有fixed后缀的方法), 代表的是固定频率。则下次执行时间是,上次预计的执行时间+周期时间(注意这个时间点可能还是小于当前时间,仍然会被快速执行到)
  34. task.period<0 ? currentTime - task.period
  35. : executionTime + task.period);
  36. }
  37. }
  38. }
  39. //跳出子任务同步代码块
  40. if (!taskFired) // Task hasn't yet fired; wait
  41. //如果最前子任务还没到被执行的时间点,那么主线程就等待中间的时间差。注意在前边的方法中有写过,添加子任务等方法是会重新唤醒主线程的
  42. queue.wait(executionTime - currentTime);
  43. }
  44. if (taskFired) // Task fired; run it, holding no locks
  45. task.run();//如果子任务的时间已经到了,那么就会执行这个子任务的run()方法。这里特别要注意两点:1直接运行run()方法的,说明是主线程全权负责执行,所以出现一个子任务挂了,整个定时器可能搁浅。2这里的标识为使用的值,还是旧值,也就是说如果出现主线程等待,那么他必须要再循环一次,才可以执行子任务。这是由于在等待期间,可能有更新的子任务添加进来,任务队列发生了变化,所以需要重新计算
  46. } catch(InterruptedException e) {
  47. }
  48. }
  49. }

接下来要介绍的类是TaskQueue

这个类的作用非常简单,就是维护一个很好的最小堆。什么是最小堆呢?你可以理解为就是父节点都小于子节点的这样一棵树。而根节点就是下次运行时间最小的任务。下面我们来看看代码,来看看这个Queue内部的设计。

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

这个是维护堆的一个数据结构,长度为128的一个数组。(话说,为什么定义这么大的,比hashMap之类的大多了)

  1. /**
  2. * The number of tasks in the priority queue. (The tasks are stored in
  3. * queue[1] up to queue[size]).
  4. */
  5. private int size = 0;
  6.  
  7. /**
  8. * Returns the number of tasks currently on the queue.
  9. */
  10. int size() {
  11. return size;
  12. }

接下来是长度size,因为queue变量只是一个堆,具体有多少个可用元素,还是需要其他变量来表示的。

接下来是add方法,我们在Timer类中的sched()方法曾经见过这个方法被调用。

方法的内部逻辑是:

1>如果queue已经被塞满了(之所以加1,是因为数组(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )的第一个元素是从未被使用的,这样是为了方便使用索引计算出堆中的位置),那么queue进行一次扩容。

2>然后把新任务放到堆的最后一个元素的位置。(注意size的作用是堆中元素的个数,而不是堆的容积)

3>然后进行一次堆的上推,也就是把新增任务的位置,按照堆的设计,依次上推到属于他的位置。

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

接下来是返回最小堆的根元素,timerTask会调用这个方法,准备执行优先级最高的任务。

  1. TimerTask getMin() {
  2. return queue[1];
  3. }

取出任意的堆中元素,清理定时器废弃任务的时候(purge()),会调用这个方法。

  1. TimerTask get(int i) {
  2. return queue[i];
  3. }

移除最小元素,这个方法会在以下两种情况被调用:
1、在执行周期为0(也就是不会再次执行)的子任务时,在取出该子任务后会调用该方法;
2、在主循环取出最近子任务时,发现该任务当前的装备已经被置为取消了,也会调用该方法,然后再次进行循环取出下一个子任务。

  1. void removeMin() {
  2. queue[1] = queue[size];
  3. queue[size--] = null; // Drop extra reference to prevent memory leak
  4. fixDown(1);
  5. }

方法实现主要逻辑是,移除掉min任务,然后把下边的子任务依次往最小堆的根部推。但是采用的方法却非常巧妙:将最后一个元素赋值到根元素的位置上,然后将最后一个元素的位置设置为null,接着将根元素依次向下推送到合适的位置,以保证最小堆的结构仍然正常。

接下来是快速移除方法,将i位置的元素,设置为堆元素的最后一个值,然后将最后的位置设置为null。需要注意的地方如下:

1>这里有涉及到assert关键字,不明白的话,看我的另外一篇博客,点击这里。(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )

2>同时不知道大家发现没有,在方法的内部没有进行同步保护。可能存在线程不安全的地方,调用这个方法的另外一个方法,是在前文中的purge()方法。在调用之前,已经锁住queue变量,所以线程不安全的担心是多余的。

3>快速移除后,最小堆的结构已经发生变化,在purge()调用后,又重新对queue继续堆化。以保证queue的使用不会再出现问题。最后才解除queue锁定。所以无论怎样,该方法都不会对定时器的使用造成空引用或触发错误。当然前提是包中的其他jdk源码不出现错误的使用。

同时我们也应该反思自己日常工作中的代码,很多时候,可以从整理逻辑上保持代码的安全和简洁,而不是将控制的粒度放到非常小,导致代码的性能和逻辑的可读性非常差。

  1. void quickRemove(int i) {
  2. assert i <= size;
  3.  
  4. queue[i] = queue[size];
  5. queue[size--] = null; // Drop extra ref to prevent memory leak
  6. }

这个方法的功能是重行规划queue中根元素的位置,用于执行需要重复运行的子任务时。

  1. void rescheduleMin(long newTime) {
  2. queue[1].nextExecutionTime = newTime;
  3. fixDown(1);
  4. }

判断queue中,是否还包含有子任务,size的含义前文中提到过。

  1. boolean isEmpty() {
  2. return size==0;
  3. }

清除当前的queue,并且置size为0;

这个方法在两个地方会被调用

1>主循环时捕捉到了异常,注意这个特性,也就是说子任务的run方法中,要自己做好异常的保护,否则一旦出现异常,那么Timer即可会退出。所以这时候是不需要线程保护的。

2>当任务被取消的时候,cancel()会调用该方法。cancel()想要clear掉整个堆,需要首先(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )抢到锁。而cancel后,重新唤起queue上等待的线程,但是注意主循环上的等待线程,此时都不会直接获取堆中的元素。所以不会出现空引用异常:

有两处wait(),第一处无限等待,被唤醒后会判断queue是否为空,然后才继续执行。第二处等待若干秒后,时间没有到即被唤醒的话,当次循环并不会执行queue,需要至少在等待一个循环。这个在主循环的最后部分有讲到。

  1. void clear() {
  2. // Null out task references to prevent memory leak
  3. for (int i=1; i<=size; i++)
  4. queue[i] = null;
  5.  
  6. size = 0;
  7. }

下边是维持堆化时,非常重要的两个方法:

fixup是将元素从底部往根的位置向上推送

  1. private void fixUp(int k) {
  2. while (k > 1) {
  3. int j = k >> 1;
  4. if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime)
  5. break;
  6. TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp;
  7. k = j;
  8. }
  9. }

fixdown是将根位置的元素,向底部推送

  1. private void fixDown(int k) {
  2. int j;
  3. while ((j = k << 1) <= size && j > 0) {
  4. if (j < size &&
  5. queue[j].nextExecutionTime > queue[j+1].nextExecutionTime)
  6. j++; // j indexes smallest kid
  7. if (queue[k].nextExecutionTime <= queue[j].nextExecutionTime)
  8. break;
  9. TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp;
  10. k = j;
  11. }
  12. }

这两个方法没什么好讲的,只是需要强调一下,在需要大量整形的乘2或者除以2的运算,都可以通过<<1、>>1的形式来表达。

堆化的方法,i从size的一半的位置,向前取出每个元素,然后依次向下推送元素。因为1/2位置的元素是最小堆叶子节点的父节点(即倒数第二层),依次向前遍历时,每一层的元素都会进行一个fixdown的操作,所以整体来说,耗费的时间非常短暂。

  1. void heapify() {
  2. for (int i = size/2; i >= 1; i--)
  3. fixDown(i);
  4. }

最后一个类是TimerTask

这个类是一个继承自接口Runnable的抽象类,需要实现类自己去补充run方法。

接下来直接看代码

首先是内部保证同步逻辑的一个锁变量。

final Object lock = new Object();

接着是状态变量,初始状态为virgin。只有这个状态的任务才可以添加到queue中,sched(),子任务添加后,会改变子任务的状态,所以子任务不会被反复多次添加到queue中。

int state = VIRGIN;

接下来是4个状态变量

  1. static final int VIRGIN = 0;//初始化
  2. static final int SCHEDULED = 1;//任务被添加到queue中即会设置该状态
  3. static final int EXECUTED = 2;//被执行过,只有不反复循环的子任务会被设置该状态
  4. static final int CANCELLED = 3;//被取消

下次被执行的时间(维持最小堆的判断标准)

long nextExecutionTime;

周期,初始是0毫秒,即不被反复执行。

long period = 0;

构造方法(抽象类的)

  1. protected TimerTask() {
  2. }

抽象run方法

public abstract void run();

取消任务时调用的方法,这个方法jdk源码没有调用,是供外部调用的

  1. public boolean cancel() {
  2. synchronized(lock) {
  3. boolean result = (state == SCHEDULED);
  4. state = CANCELLED;
  5. return result;
  6. }
  7. }

下一次计划执行时间:当前计划执行时间加周期时间。注意这个方法的返回值,可能是一个过去时间。

这个方法jdk源码也没有调用,是供外部调用的。

  1. public long scheduledExecutionTime() {
  2. synchronized(lock) {
  3. return (period < 0 ? nextExecutionTime + period
  4. : nextExecutionTime - period);
  5. }
  6. }

最后的最后,来谈谈Timer类的定位:

(1)前Timer时代。

Timer是jdk1.3的时候,添加进源码的。这个时候大概是2000年左右。具体java被推出,才仅仅过去5年,所以1.3的主要改进,表现在新增的大量类库上。而在此之前,想拥有一个如Timer般的定时功能,是非常麻烦的,基本都要手动去实现。

(2)后Timer时代

查看了Timer的源代码之后,我们发现Timer在使用中存在这么问题:

1、定时任务是顺序执行的,也就是说后续的任务,一定要等到前边的任务执行完毕后,才会执行,否则将会一直等待。(其实这一点说不上来好还是坏,因为有时候我们可能会希望尽管是定时任务,但是执行时是有顺序完成和开始的,是要保证先后顺序的)

2、对系统时间非常敏感,通过代码我们知道,在每次子任务被取出后(执行run前),都会计算一遍执行时间,同时在判定子任务的执行时间是否已经到来时,都是直接获取到系统时间。倘若系统时间发生了修改,而使用的计划时间仍然是使用上次修改前的时间段时,就会出现一些意想不到的结果。如计划是5秒后执行,主线程wait 5秒钟后,被唤醒,在这5秒钟内,系统时间向后推迟了1天,那么主任务,仍然会执行该子任务(其他的也都会依次迅速执行,因为时间已经过了)。而倘若向前调整一天,那么主线程判断的时间仍然是,调整时间前的时间点,所以需要再等待一天。因此会出(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )现很多人以为Timer在调整时间后,被挂起,但是查看线程状态,发现还存在的奇怪场景。

3、子任务之间存在依赖。其实子任务之间的依赖关系并不强,无非就是前边的子任务执行完后,后边的子任务才可以开始执行。但是倘若在执行某个子任务时,捕捉到了异常,那么线程会立刻结束执行,后续的子任务都不会执行了,这个问题有时会对我们造成很大的困扰。

为了解决以上种种在jdk1.5中提供了ScheduledExecutorService接口以供开发者使用。

这个接口的实现,主要是通过线程池的形式,解决了上述遇到的问题(线程池也是jdk 1.5时才推出的),很多人因此认为Timer已经过时了,我觉得完全没有必要这样认为,通过自己对比Timer的原理和ScheduledExecutorService的改进之后。我们发现很多地方Timer仍然是有自己存在的必要的,只是占用场景不如ScheduledExecutorService多罢了。关于ScheduledExecutorService的学习,此处不再罗列,有兴趣的同学可以自己学习。

Timer的故事----Jdk源码解读的更多相关文章

  1. HashTable的故事----Jdk源码解读

    HashTable的故事 很早之前,在讲HashMap的时候,我们就说过hash是散列,把...弄碎的意思.hashtable中的hash也是这个意思,而table呢,是指数据表格,也就是说hasht ...

  2. HashSet的故事----Jdk源码解读

    Hash,我们在说HashMap的时候,已经知道Hash是散列,Map是映射了. 那么Set又是什么呢 ? 先来看看Set的翻译是什么 n. [数] 集合:一套:布景:[机] 装置 这里Set所取的含 ...

  3. JDK源码解读之toUnsignedString

    我们知道,所有整数都是通过二进制编码的形式存储在内存中的.比如32位的整数,最高位是符号位,0代表正数,1代表负数. 那么怎么才能够将整数的二进制编码形式打印出来呢?Integer类提供了一个公有静态 ...

  4. Java是如何实现自己的SPI机制的? JDK源码(一)

    注:该源码分析对应JDK版本为1.8 1 引言 这是[源码笔记]的JDK源码解读的第一篇文章,本篇我们来探究Java的SPI机制的相关源码. 2 什么是SPI机制 那么,什么是SPI机制呢? SPI是 ...

  5. JDK容器类Map源码解读

    java.util.Map接口是JDK1.2开始提供的一个基于键值对的散列表接口,其设计的初衷是为了替换JDK1.0中的java.util.Dictionary抽象类.Dictionary是JDK最初 ...

  6. JDK容器类List,Set,Queue源码解读

    List,Set,Queue都是继承Collection接口的单列集合接口.List常用的实现主要有ArrayList,LinkedList,List中的数据是有序可重复的.Set常用的实现主要是Ha ...

  7. 如何阅读JDK源码

    JDK源码阅读笔记: https://github.com/kangjianwei/LearningJDK 如何阅读源码,是每个程序员需要面临的一项挑战. 为什么需要阅读源码?从实用性的角度来看,主要 ...

  8. AFNetworking 3.0 源码解读(九)之 AFNetworkActivityIndicatorManager

    让我们的APP像艺术品一样优雅,开发工程师更像是一名匠人,不仅需要精湛的技艺,而且要有一颗匠心. 前言 AFNetworkActivityIndicatorManager 是对状态栏中网络激活那个小控 ...

  9. 【原】Spark中Job的提交源码解读

    版权声明:本文为原创文章,未经允许不得转载. Spark程序程序job的运行是通过actions算子触发的,每一个action算子其实是一个runJob方法的运行,详见文章 SparkContex源码 ...

随机推荐

  1. @寒冬winter 大神的css作业问题

    块级元素   ①总是在新行上开始: ②高度,行高以及外边距和内边距都可控制: ③宽度缺省是它的容器的100%,除非设定一个宽度. ④它可以容纳内联元素和其他块元素 行内级元素   ①和其他元素都在一行 ...

  2. 【BZOJ】3930: [CQOI2015]选数

    题意 从区间\([L, R]\)选\(N\)个数(可以重复),问这\(N\)个数的最大公约数是\(K\)的方案数.(\(1 \le N, K \le 10^9, 1 \le L \le R \le 1 ...

  3. Sublime Text 2配置文件详解

    Sublime Text 2是那种让人会一眼就爱上的编辑器,不仅GUI让人眼前一亮,功能更是没的说,拓展性目前来说也完全够用了,网上介绍软件的文章和推荐插件的文章也不少,而且很不错,大家可以去找找自己 ...

  4. *HDU3367 最小生成树

    Pseudoforest Time Limit: 10000/5000 MS (Java/Others)    Memory Limit: 65536/65536 K (Java/Others)Tot ...

  5. 如何在Eclipse中查看JDK的源代码

    设置: 1.点 "Window"-> "Preferences" -> "Java" -> "Installed ...

  6. mysql performance_schema 和information_schema.tables了解

    这个是关于mysql的系统表,性能表,核心表操作的一些介绍,深入算不上 我们一般很少去动 mysql  information_schema 信息相关  performance_schema 性能相关 ...

  7. Ajax调用Conrtoller返回数据

    前端ajax function doRefund(){ $.ajax({ type: "POST", catch: false, url: "@Url.Action(&q ...

  8. ThinkPHP 3.2.3 加减乘法验证码类

    ThinkPHP 3.2.3 自带的验证码类位于 /ThinkPHP/Library/Think/Verify.class.php,字体文件位于 /ThinkPHP/Library/Think/Ver ...

  9. MyBatis学习总结(五)——实现关联表查询(转载)

    本文转载自:http://www.cnblogs.com/jpf-java/p/6013516.html 一.一对一关联 1.1.提出需求 根据班级id查询班级信息(带老师的信息) 1.2.创建表和数 ...

  10. JavaScript对象属性(二)

    对象object  例子一: var car = { "wheels":4, "engines":1, "seats":5}; 例子二: v ...