При использовании Java для планирования задач по времени мы часто используем для этого класс Timer. Timer прост в использовании, а его исходный код очень понятен.В этом разделе мы тщательно проанализируем класс Timer и посмотрим, как автор исходного кода JDK реализует стабильный и надежный простой планировщик.
Использование таймера
Задачи планирования таймера включают однократное планирование и циклическое планирование.Циклическое планирование делится на планирование с фиксированной скоростью (fixRate) и планирование с фиксированной задержкой (fixDelay). Фиксированная ставка — это то же самое, что вы работаете сверхурочно сегодня допоздна, но вы должны быть в компании вовремя на следующий день, Если вы случайно проработаете сверхурочно до 9 утра следующего дня, у вас даже не будет времени на отдых. Фиксированная задержка означает, что вы должны поспать 8 часов, прежде чем прийти на работу.Если вы работаете сверхурочно до 6 утра, вы можете прийти на работу после обеда. Фиксированная скорость подчеркивает пунктуальность, а фиксированная задержка подчеркивает интервал.
Timer timer = new Timer();
TimerTask task = new TimerTask() {
public void run() {
System.out.println("wtf");
}
};
// 延迟 1s 打印 wtf 一次
timer.schedule(task, 1000)
// 延迟 1s 固定时延每隔 1s 周期打印一次 wtf
timer.schedule(task, 1000, 1000);
// 延迟 1s 固定速率每隔 1s 周期打印一次 wtf
timer.scheduleAtFixRate(task, 1000, 1000)
Если у вас есть задача, которую нужно планировать вовремя каждый день, вам следует использовать планирование с фиксированной скоростью и убедиться, что время выполнения каждой задачи не слишком велико, не превышает точку следующего дня. Если у вас есть задача, которая должна выполняться каждые несколько минут, используйте планирование с фиксированной задержкой, на самом деле не важно, сколько времени требуется для выполнения вашей отдельной задачи.
внутренняя структура
Класс Timer содержит очередь задач и поток асинхронного опроса. Очередь задач содержит все задачи для выполнения.Все задачи будут выполняться в этом асинхронном потоке.Помните, что код выполнения задачи не может выдать исключение, иначе поток Timer зависнет и все задачи не будут выполнены. Нелегко выполнять одну задачу слишком долго, иначе это повлияет на точность планирования задач с точки зрения времени. Например, если вы выполняете задачу слишком долго, другие задачи, ожидающие планирования, будут находиться в состоянии голодания и не могут быть запланированы. Все задачи выполняются в этом единственном потоке TimerThread.
class Timer {
TaskQueue queue = new TaskQueue();
TimerThread thread = new TimerThread(queue);
}
class TaskQueue {
TimerTask[] queue = new TimerTask[128];
int size;
}
Любой поток может добавлять задачи в TaskQueue с помощью метода Timer.schedule, но TaskQueue не является потокобезопасной структурой данных. Каждый раз, когда TaskQueue изменяется, ее необходимо блокировать.
synchronized(queue) {
...
}
статус задачи
TimerTask имеет 4 состояния, состояние по умолчанию — VIRGIN, только что созданный экземпляр не был запланирован. SCHEDULED указывает, что задача помещена в очередь задач и ожидает выполнения. EXECUTED указывает, что задача выполнена. CANCELED указывает, что задача была отменена, и она была искусственно отменена до того, как ее можно было выполнить.
abstract class TimerTask {
int state = VIRGIN;
static final int VIRGIN = 0;
static final int SCHEDULED = 1;
static final int EXECUTED = 2;
static final int CANCELLED = 3;
long nextExecutionTime; // 下次执行时间
long period = 0; // 间隔
}
У повторяющейся задачи нет состояния ВЫПОЛНЕНО, потому что она перепланируется каждый раз, когда завершает выполнение. Состояние ВЫПОЛНЯЕТСЯ существует только в разовых задачах, и это состояние на самом деле не означает, что задача была выполнена, это означает, что она была удалена из очереди задач и вот-вот будет выполнена.
Поле интервала задачи период является специальным, при использовании фиксированной ставки период имеет положительное значение, при использовании фиксированного интервала период является отрицательным значением, а при разовой задаче период равен нулю. Ниже приведена следующая настройка времени планирования повторяющейся задачи.
currentTime = System.currentTimeMillis();
executionTime = task.nextExecutionTime;
// 固定时延基于 currentTime 顺延
// 固定速率基于 executionTime(设定时间) 顺延
// next_exec_time = exec_time + period = first_delay + n * period
queue.rescheduleMin(
task.period<0 ? currentTime - task.period
: executionTime + task.period);
Для фиксированной скорости, если время выполнения задачи превышает интервал, она может продолжать занимать очередь задач, поскольку ее время планирования всегда будет меньше, чем currentTime, и она будет занимать первое место в куче. Это. После запуска переназначьте задачу, ее время все равно не успевает. Продолжайте, и вы увидите, что время планирования задачи сильно отстает от текущего времени, а другие задачи могут просто зависнуть. Вот почему важно обращать особое внимание на то, чтобы повторяющиеся задачи с фиксированной скоростью не выполнялись слишком долго.
блокировка задачи
Задача Timer поддерживает операции отмены, и поток, отменяющий задачу, и поток, выполняющий задачу, скорее всего, не являются одним и тем же потоком. Возможно, что задача выполняется, и в результате другой поток говорит отменить задачу. Как Timer справляется с этим в это время? Блокировка видна в классе TimerTask. Когда атрибуты задачи необходимо изменить, они будут заблокированы.
abstract class TimerTask {
final Object lock = new Object();
}
// 取消任务
public boolean cancel() {
synchronized(lock) {
boolean result = (state == SCHEDULED);
state = CANCELLED;
return result;
}
}
// 调度任务
private void sched(TimerTask task, long time, long period) {
synchronized(task.lock) {
if (task.state != TimerTask.VIRGIN)
throw new IllegalStateException(
"Task already scheduled or cancelled");
task.nextExecutionTime = time;
task.period = period;
task.state = TimerTask.SCHEDULED;
}
}
// 运行任务
private void mainLoop() {
while(true) {
synchronized(task.lock) {
if (task.state == TimerTask.CANCELLED) {
queue.removeMin();
continue;
}
...
if(task.period == 0) {
task.state = TimerTask.EXECUTED;
}
...
}
task.run();
}
}
Перед запуском задачи она проверит, не была ли задача отменена, и если она будет отменена, то будет удалена из очереди. Как только задача начинает выполняться run(), ее нельзя отменить для одной задачи, и повторяющаяся задача не будет продолжать планироваться в следующий раз. Если задача не имеет шансов на выполнение (время установлено слишком большое), то даже если задача будет отменена, она продолжит лежать в очереди задач. Представьте, если вы запланируете длинную серию задач, а затем все отмените, это может стать точкой утечки памяти. Таким образом, Timer также предоставляет отдельный метод purge() для одновременной очистки всех отмененных задач.
public int purge() {
int result = 0;
// 灭掉 CANCELLED 状态的任务
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;
}
очередь задач пуста
В очереди задач нет задач, и поток планирования должен спать в соответствии с определенной стратегией. Ему нужно спать до тех пор, пока он не проснется сразу после выполнения первой задачи, поэтому крайний срок сна — это время, когда первая задача будет выполнена. В то же время во время сна могут быть добавлены новые задачи, и время их планирования может быть раньше, поэтому, когда приходит новая задача, ей нужно разбудить спящий поток.
private void mainLoop() {
while(true) {
...
task = queue.getMin();
currentTime = System.currentTimeMillis();
executionTime = task.nextExecutionTime;
if(executionTime > currentTime) {
// 开始睡大觉
queue.wait(executionTime - currentTime);
}
...
}
}
// 新任务进来了
private void sched(TimerTask task, long time, long period) {
...
queue.add(task);
if (queue.getMin() == task)
queue.notify(); // 唤醒轮训线程
}
Метод wait() в коде вызывает Object.wait() для перехода в спящий режим. Когда приходит новая задача и обнаруживается, что время выполнения этой новой задачи самое раннее, вызовите метод notify(), чтобы разбудить поток опроса.
Таймер истекает
Таймер предоставляет метод отменить () для опорожнения очереди, остановить планировщик, не позволяет войти никаких новых задач. Это будет newtasksmaybescheduled поле настроен на ложный таймер представления будет прекращен.
class TimerThread {
...
boolean newTasksMayBeScheduled; // 终止的标志
...
}
public void cancel() {
synchronized(queue) {
thread.newTasksMayBeScheduled = false;
queue.clear();
queue.notify();
}
}
Если таймер истекает, при появлении новой задачи будет выдано исключение.
private void sched(TimerTask task, long time, long period) {
synchronized(queue) {
if (!thread.newTasksMayBeScheduled)
throw new IllegalStateException("Timer already cancelled.");
...
}
}
Мы также заметили, что метод Timer.cancel() пробуждает поток опроса, чтобы его можно было немедленно остановить. Однако, если задача в данный момент выполняется, то функция cancel() должна дождаться завершения задачи, прежде чем ее можно будет остановить.
private void mainLoop() {
while(true) {
// 正常清空下,队列空了,轮训线程会休眠
// 但是如果 newTasksMayBeScheduled 为 false
// 那么循环会退出,轮训线程会终止
while (queue.isEmpty() && newTasksMayBeScheduled)
queue.wait();
if (queue.isEmpty())
break;
...
}
}
вывоз мусора
Существует также особый сценарий, требующий особого внимания, то есть, когда поток опроса приостанавливается из-за отсутствия задач в очереди, объект Timer удаляется сборщиком мусора, поскольку на него больше нет ссылок. В это время вам нужно активно разбудить обучающий поток вращения и дать ему выйти.
class Timer {
...
private final Object threadReaper = new Object() {
@SuppressWarnings("deprecation")
protected void finalize() throws Throwable {
synchronized(queue) {
thread.newTasksMayBeScheduled = false;
queue.notify();
}
}
};
...
}
Когда Timer перезапускается, объект, на который указывает внутреннее поле threadPeaper, также перезапускается. Таким образом, метод finalize будет вызываться для пробуждения и завершения потока опроса Timer. Без этого объекта threadPeaper это может привести к тому, что в JVM останутся потоки-зомби.
Читайте больше качественных статей, сканируйте приведенный выше QR-код на WeChat и подписывайтесь на общедоступный аккаунт Code Cave.