ホームページ > Java > &#&チュートリアル > java-concurrency-Timer と TimerTask

java-concurrency-Timer と TimerTask

黄舟
リリース: 2017-01-19 11:43:31
オリジナル
1262 人が閲覧しました

Timerはスケジューラ、TimerTaskはrunメソッドを実装したクラスに過ぎず、特定のTimerTaskは自分で実装する必要がある

[code]Timer timer = new Timer();
timer.schedule(new TimerTask() {
        public void run() {
            System.out.println("abc");
        }
}, 200000 , 1000);
ログイン後にコピー

public voidスケジュール(TimerTaskタスク、長い遅延)

このメソッドはタスクをスケジュールし、遅延 (ms) 後にスケジュールを開始します。これは 1 回だけスケジュールされます。

public void schedule(TimerTask task, Date time)

指定された時点時刻に一度スケジュールします。

public voidスケジュール(TimerTaskタスク、長い遅延、長い期間)

このメソッドは、タスクをスケジュールし、遅延(ms)後にスケジューリングを開始します。各スケジューリング後、少なくとも期間(ms)待ってからスケジューリングを開始します。

public voidSchedule(TimerTask task, Date firstTime, long period)

前のメソッドと同様、唯一の違いは、渡される 2 番目のパラメーターが最初のスケジュールの時刻であることです。

public voidスケジュールAtFixedRate(TimerTaskタスク,長い遅延,長い期間)

タスクは遅延(ms)後にスケジュールされ、その後周期(ms)ごとに再度スケジュールされます。メソッド:scheduleと同じように見えますが、実際、それ以外の場合は、ソース コードを後で見ると、スケジュールが次の実行時刻を計算するときに、現在時刻 (タスクの実行前に取得される) + タイム スライスが使用されるのに対し、scheduleAtFixedRate メソッドは実行される現在の時刻を使用することがわかります (つまり、実行されるべき時間に表示されます) + タイム スライスを計算します。前者は実際の操作時間であり、後者は理論上の時点です。たとえば、スケジュールのタイム スライスは 5 秒です。 5、10、15、および 20 タイム スライスでスケジュールされますが、CPU 要求によりスケジュールされず、8 秒目までに初めてスケジュールされなかった場合、次回はスケジュールによって計算されます。メソッドは 10 秒目ではなく 13 秒目である必要があるため、20 秒後には 1 回以上スケジュールされる可能性があります。 8 秒がスケジュールされている場合、計算は 10 秒である必要があるため、現在時刻から 2 秒離れています。その場合、再スケジュール キューの並べ替えで最初にスケジュールされるため、スケジュールが欠落する状況を最小限に抑えるようにしてください。

[code]public Timer() {
    this("Timer-" + serialNumber());
}
ログイン後にコピー

作成されたスレッドはメインスレッドではありません。メインスレッドの終了後、タイマーの終了を完了するためにキャンセルを使用しなくても、タイマーは自動的に終了します。

[code]public Timer(boolean isDaemon) {
    this("Timer-" + serialNumber(), isDaemon);
}
ログイン後にコピー

バックグラウンド スレッドかどうかを入力します。バックグラウンド スレッドは、プロセスが終了したときのみ自動的にログアウトします。

[code]public Timer(String name, boolean isDaemon) {
      thread.setName(name);
      thread.setDaemon(isDaemon);
      thread.start();
  }
ログイン後にコピー

ここにスレッドがあります。このスレッドは明らかにスレッドであり、Timer クラスにパッケージ化されています。Timer は外部スレッドから独立してスレッドを内部的にラップします。デフォルトのタイプ。デフォルトでは参照できず、Timer 自体によって使用されます。

private TaskQueue queue = new TaskQueue();
ログイン後にコピー

threadReaper はオブジェクト型で、ガベージ コレクション中に対応する情報をリサイクルし、GC バックアップを実行するために使用されます。ただし、何らかの理由でタイマー スレッドが終了した場合に使用されます。キャンセルされていない場合は、内部のキュー内の情報をクリアする必要がありますが、通常はこのメソッドを考慮しないため、Java が何のためにこのメソッドを作成するのかを知る必要があるだけです。

スケジューリングメソッド

public void schedule(TimerTaskタスク、長い遅延)

[code]public void schedule(TimerTask task, long delay) {
       if (delay < 0)
           throw new IllegalArgumentException("Negative delay.");
       sched(task, System.currentTimeMillis()+delay, 0);
   }
ログイン後にコピー

public void schedule(TimerTaskタスク、長い遅延、長い期間)

[code]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);
    }
ログイン後にコピー

また、メソッドschedを呼び出してスケジューリングを完了します。これは、上記の方法とスケジューリング時の違いは、受信期間が追加され、最初の受信期間が 0 であるため、このパラメーターは回数ではなくタイム スライスとして決定されることです。ここでは負の数が期間に追加されることに注意してください。つまり、1000 を渡し始め、sched を呼び出すと -1000 になります。実際、ソース コードを読むと、これが外国人にとっての数値の理解であることがわかります。

public voidScheduleAtFixedRate(TimerTasktask, long late, long period)

[code]public void scheduleAtFixedRate(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);
   }
ログイン後にコピー

唯一の違いは、この操作を実行すると、同期が発生するため、タイマーレベルで、期間が反転されないことです。これはスレッドセーフです。最後に、タスク関連のパラメーターが割り当てられ、主に nextExecutionTime (次回の実行時間)、period (タイム スライス)、state (状態) が含まれ、それをキューに入れて通知操作を実行します。なぜ通知操作を行う必要があるのですか?

[code]class TaskQueue {

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

    private int size = 0;
ログイン後にコピー

TaskQueue的结构很简单,为一个数组,加一个size,有点像ArrayList,是不是长度就128呢,当然不是,ArrayList可以扩容,它可以,只是会造成内存拷贝而已,所以一个Timer来讲,只要内部的task个数不超过128是不会造成扩容的;内部提供了add(TimerTask)、size()、getMin()、get(int)、removeMin()、quickRemove(int)、rescheduleMin(long newTime)、isEmpty()、clear()、fixUp()、fixDown()、heapify();

 add(TimerTaskt)为增加一个任务

  size()任务队列的长度

  getMin()获取当前排序后最近需要执行的一个任务,下标为1,队列头部0是不做任何操作的。

  get(inti)获取指定下标的数据,当然包括下标0.

  removeMin()为删除当前最近执行的任务,也就是第一个元素,通常只调度一次的任务,在执行完后,调用此方法,就可以将TimerTask从队列中移除。

  quickRmove(inti)删除指定的元素,一般来说是不会调用这个方法的,这个方法只有在Timer发生purge的时候,并且当对应的TimerTask调用了cancel方法的时候,才会被调用这个方法,也就是取消某个TimerTask,然后就会从队列中移除(注意如果任务在执行中是,还是仍然在执行中的,虽然在队列中被移除了),还有就是这个cancel方法并不是Timer的cancel方法而是TimerTask,一个是调度器的,一个是单个任务的,最后注意,这个quickRmove完成后,是将队列最后一个元素补充到这个位置,所以此时会造成顺序不一致的问题,后面会有方法进行回补。

  rescheduleMin(long newTime)是重新设置当前执行的任务的下一次执行时间,并在队列中将其从新排序到合适的位置,而调用的是后面说的fixDown方法。

  对于fixUp和fixDown方法来讲,前者是当新增一个task的时候,首先将元素放在队列的尾部,然后向前找是否有比自己还要晚执行的任务,如果有,就将两个任务的顺序进行交换一下。而fixDown正好相反,执行完第一个任务后,需要加上一个时间片得到下一次执行时间,从而需要将其顺序与后面的任务进行对比下。

[code]private void fixDown(int k) {
       int j;
       while ((j = k << 1) <= size && j > 0) {
           if (j < size &&
               queue[j].nextExecutionTime > queue[j+1].nextExecutionTime)
               j++; // j indexes smallest kid
           if (queue[k].nextExecutionTime <= queue[j].nextExecutionTime)
               break;
           TimerTask tmp = queue[j];  queue[j] = queue[k]; queue[k] = tmp;
           k = j;
       }
   }
ログイン後にコピー

 这种方式并非排序,而是找到一个合适的位置来交换,因为并不是通过队列逐个找的,而是每次移动一个二进制为,例如传入1的时候,接下来就是2、4、8、16这些位置,找到合适的位置放下即可,顺序未必是完全有序的,它只需要看到距离调度部分的越近的是有序性越强的时候就可以了,这样即可以保证一定的顺序性,达到较好的性能。

[code]public void cancel() {
        synchronized(queue) {
            thread.newTasksMayBeScheduled = false;
            queue.clear();
            queue.notify();  // In case queue was already empty.
        }
    }
ログイン後にコピー

貌似仅仅将队列清空掉,然后设置了newTasksMayBeScheduled状态为false,最后让队列也调用了下notify操作,但是没有任何地方让线程结束掉,那么就要回到我们开始说的Timer中包含的thread为:TimerThread类了,在看这个类之前,再看下Timer中最后一个purge()类,当你对很多Task做了cancel操作后,此时通过调用purge方法实现对这些cancel掉的类空间的回收,上面已经提到,此时会造成顺序混乱,所以需要调用队里的heapify方法来完成顺序的重排,源码如下:

[code]public int purge() {
         int result = 0;

         synchronized(queue) {
             for (int i = queue.size(); i > 0; i--) {
                 if (queue.get(i).state == TimerTask.CANCELLED) {
                     queue.quickRemove(i);
                     result++;
                 }
             }

             if (result != 0)
                 queue.heapify();
         }
         return result;
     }
ログイン後にコピー

那么调度呢,是如何调度的呢,那些notify,和清空队列是如何做到的呢?我们就要看看TimerThread类了,内部有一个属性是:newTasksMayBeScheduled,也就是我们开始所提及的那个参数在cancel的时候会被设置为false。

也就是我们所调用的queue了,这下联通了吧,不过这里是queue是通过构造方法传入的,传入后赋值用以操作,很明显是Timer传递给这个线程的,我们知道它是一个线程,所以执行的中心自然是run方法了,所以看下run方法的body部分是:

[code]public void run() {
        try {
            mainLoop();
        } finally {
            synchronized(queue) {
                newTasksMayBeScheduled = false;
                queue.clear();  // Eliminate obsolete references
            }
        }
    }
ログイン後にコピー
[code]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&#39;t yet fired; wait
                        queue.wait(executionTime - currentTime);
                }
                if (taskFired)  // Task fired; run it, holding no locks
                    task.run();
            } catch(InterruptedException e) {
            }
        }
    }
ログイン後にコピー

以发现这个timer是一个死循环程序,除非遇到不能捕获的异常或break才会跳出,首先注意这段代码:

hile (queue.isEmpty() &&newTasksMayBeScheduled) 

                        queue.wait();
ログイン後にコピー


 循环体为循环过程中,条件为queue为空且newTasksMayBeScheduled状态为true,可以看到这个状态其关键作用,也就是跳出循环的条件就是要么队列不为空,要么是newTasksMayBeScheduled状态设置为false才会跳出,而wait就是在等待其他地方对queue发生notify操作,从上面的代码中可以发现,当发生add、cancel以及在threadReaper调用finalize方法的时候会被调用,第三个我们基本可以不考虑其实发生add的时候也就是当队列还是空的时候,发生add使得队列不为空就跳出循环,而cancel是设置了状态,否则不会进入这个循环,那么看下面的代码

if (queue.isEmpty()) 

    break;
ログイン後にコピー

当跳出上面的循环后,如果是设置了newTasksMayBeScheduled状态为false跳出,也就是调用了cancel,那么queue就是空的,此时就直接跳出外部的死循环,所以cancel就是这样实现的,如果下面的任务还在跑还没运行到这里来,cancel是不起作用的。

  接下来是获取一个当前系统时间和上次预计的执行时间,如果预计执行的时间小于当前系统时间,那么就需要执行,此时判定时间片是否为0,如果为0,则调用removeMin方法将其移除,否则将task通过rescheduleMin设置最新时间并排序:

[code]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);
     }
ログイン後にコピー


这里可以看到,period为负数的时候,就会被认为是按照按照当前系统时间+一个时间片来计算下一次时间,就是前面说的schedule和scheduleAtFixedRate的区别了,其实内部是通过正负数来判定的,也许java是不想增加参数,而又想增加程序的可读性,才这样做,其实通过正负判定是有些诡异的,也就是你如果在schedule方法传入负数达到的功能和scheduleAtFixedRate的功能是一样的,相反在scheduleAtFixedRate方法中传入负数功能和schedule方法是一样的。

  同时你可以看到period为0,就是只执行一次,所以时间片正负0都用上了,呵呵,然后再看看mainLoop接下来的部分:

[code]if (!taskFired)// Taskhasn&#39;t yet fired; wait
    queue.wait(executionTime- currentTime);
ログイン後にコピー

这里是如果任务执行时间还未到,就等待一段时间,当然这个等待很可能会被其他的线程操作add和cancel的时候被唤醒,因为内部有notify方法,所以这个时间并不是完全准确,在这里大多数情况下是考虑Timer内部的task信息是稳定的,cancel方法唤醒的话是另一回事。

[code] if (taskFired) // Task fired; run it, holding no locks
    task.run();
ログイン後にコピー

如果线程需要执行,那么调用它的run方法,而并非启动一个新的线程或从线程池中获取一个线程来执行,所以TimerTask的run方法并不是多线程的run方法,虽然实现了Runnable,但是仅仅是为了表示它是可执行的,并不代表它必须通过线程的方式来执行的。

 Timer和TimerTask的简单组合是多线程的嘛?不是,一个Timer内部包装了“一个Thread”和“一个Task”队列,这个队列按照一定的方式将任务排队处理,包含的线程在Timer的构造方法调用时被启动,这个Thread的run方法无限循环这个Task队列,若队列为空且没发生cancel操作,此时会一直等待,如果等待完成后,队列还是为空,则认为发生了cancel从而跳出死循环,结束任务;循环中如果发现任务需要执行的时间小于系统时间,则需要执行,那么根据任务的时间片从新计算下次执行时间,若时间片为0代表只执行一次,则直接移除队列即可。

  但是是否能实现多线程呢?可以,任何东西是否是多线程完全看个人意愿,多个Timer自然就是多线程的,每个Timer都有自己的线程处理逻辑,当然Timer从这里来看并不是很适合很多任务在短时间内的快速调度,至少不是很适合同一个timer上挂很多任务,在多线程的领域中我们更多是使用多线程中的:但是是否能实现多线程呢?可以,任何东西是否是多线程完全看个人意愿,多个Timer自然就是多线程的,每个Timer都有自己的线程处理逻辑,当然Timer从这里来看并不是很适合很多任务在短时间内的快速调度,至少不是很适合同一个timer上挂很多任务,在多线程的领域中我们更多是使用多线程中的:

Executors.newScheduledThreadPool
ログイン後にコピー

   来完成对调度队列中的线程池的处理,内部通过new ScheduledThreadPoolExecutor来创建线程池的Executor的创建,当然也可以调用

[code]Executors.unconfigurableScheduledExecutorService
ログイン後にコピー


方法来创建一个DelegatedScheduledExecutorService其实这个类就是包装了下下scheduleExecutor,也就是这只是一个壳,英文理解就是被委派的意思,被托管的意思。

以上就是java-并发-Timer和TimerTask的内容,更多相关内容请关注PHP中文网(www.php.cn)!


関連ラベル:
ソース:php.cn
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート