溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

如何使用JDK中的Timer

發(fā)布時間:2021-10-18 14:02:26 來源:億速云 閱讀:117 作者:iii 欄目:編程語言

這篇文章主要講解了“如何使用JDK中的Timer”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“如何使用JDK中的Timer”吧!

定時器Timer的使用

java.util.Timer是JDK提供的非常使用的工具類,用于計劃在特定時間后執(zhí)行的任務,可以只執(zhí)行一次或定期重復執(zhí)行。在JDK內部很多組件都是使用的java.util.Timer實現(xiàn)定時任務或延遲任務。

Timer可以創(chuàng)建多個對象的實例,每個對象都有且只有一個后臺線程來執(zhí)行任務。

如何使用JDK中的Timer

Timer類是線程安全的,多個線程可以共享一個計時器,而無需使用任何的同步。

構造方法

首先我們可以看下Timer類的構造方法的API文檔

如何使用JDK中的Timer 

  1. Timer(): 創(chuàng)建一個新的計時器。

  2. Timer(boolean isDaemon): 創(chuàng)建一個新的定時器,其關聯(lián)的工作線程可以指定為守護線程。

  3. Timer(String name): 創(chuàng)建一個新的定時器,其關聯(lián)的工作線程具有指定的名稱。

  4. Timer(String name, boolean isDaemon): 創(chuàng)建一個新的定時器,其相關線程具有指定的名稱,可以指定為守護線程。

Note: 守護線程是低優(yōu)先級線程,在后臺執(zhí)行次要任務,比如垃圾回收。當有非守護線程在運行時,Java應用不會退出。如果所有的非守護線程都退出了,那么所有的守護線程也會隨之退出。

實例方法

接下來我們看下Timer類的實例方法的API文檔

如何使用JDK中的Timer

  1. cancel(): 終止此計時器,并丟棄所有當前執(zhí)行的任務。

  2. purge(): 從該計時器的任務隊列中刪除所有取消的任務。

  3. schedule(TimerTask task, Date time): 在指定的時間執(zhí)行指定的任務。

  4. schedule(TimerTask task, Date firstTime, long period): 從指定 的時間開始 ,對指定的任務按照固定的延遲時間重復執(zhí)行 。

  5. schedule(TimerTask task, long delay): 在指定的延遲之后執(zhí)行指定的任務。

  6. schedule(TimerTask task, long delay, long period): 在指定的延遲之后開始 ,對指定的任務按照固定的延遲時間重復執(zhí)行 。

  7. scheduleAtFixedRate(TimerTask task, Date firstTime, long period): 從指定的時間開始 ,對指定的任務按照固定速率重復執(zhí)行 。

  8. scheduleAtFixedRate(TimerTask task, long delay, long period): 在指定的延遲之后開始 ,對指定的任務按照固定速率重復執(zhí)行。

schedulescheduleAtFixedRate都是重復執(zhí)行任務,區(qū)別在于schedule是在任務成功執(zhí)行后,再按照固定周期再重新執(zhí)行任務,比如第一次任務從0s開始執(zhí)行,執(zhí)行5s,周期是10s,那么下一次執(zhí)行時間是15s而不是10s。而scheduleAtFixedRate是從任務開始執(zhí)行時,按照固定的時間再重新執(zhí)行任務,比如第一次任務從0s開始執(zhí)行,執(zhí)行5s,周期是10s,那么下一次執(zhí)行時間是10s而不是15s。

使用方式

1. 執(zhí)行時間晚于當前時間

接下來我們將分別使用schedule(TimerTask task, Date time)schedule(TimerTask task, long delay)用來在10秒后執(zhí)行任務,并展示是否將Timer的工作線程設置成守護線程對Timer執(zhí)行的影響。

首先我們創(chuàng)建類Task, 接下來我們的所有操作都會在這個類中執(zhí)行, 在類中使用schedule(TimerTask task, Date time),代碼如下

import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
import static java.text.MessageFormat.format;

public class Task {

    private static final long SECOND = 1000;

    public static void main(String[] args) {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println(format("程序結束時間為: {0}", currentTimeMillis()));
        }));

        long startTimestamp = currentTimeMillis();
        System.out.println(format("程序執(zhí)行時間為: {0}", startTimestamp));
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                long exceptedTimestamp = startTimestamp + 10 * SECOND;
                long executingTimestamp = currentTimeMillis();
                long offset = executingTimestamp - exceptedTimestamp;
                System.out.println(format("任務運行在線程[{0}]上, 期望執(zhí)行時間為[{1}], 實際執(zhí)行時間為[{2}], 實際偏差[{3}]",
                        currentThread().getName(), exceptedTimestamp, executingTimestamp, offset));
            }
        }, new Date(startTimestamp + 10 * SECOND));
    }
}

在程序的最開始,我們注冊程序結束時執(zhí)行的函數(shù),它用來打印程序的結束時間,我們稍后將會用它來展示工作線程設置為守護線程與非守護線程的差異。接下來是程序的主體部分,我們記錄了程序的執(zhí)行時間,定時任務執(zhí)行時所在的線程、定時任務的期望執(zhí)行時間與實際執(zhí)行時間。

程序運行后的實際執(zhí)行效果

程序執(zhí)行時間為: 1,614,575,921,461
任務運行在線程[Timer-0]上, 期望執(zhí)行時間為[1,614,575,931,461], 實際執(zhí)行時間為[1,614,575,931,464], 實際偏差[3]

程序在定時任務執(zhí)行結束后并沒有退出,我們注冊的生命周期函數(shù)也沒有執(zhí)行,我們將在稍后解釋這個現(xiàn)象。

接下來我們在類中使用schedule(TimerTask task, long delay), 來達到相同的在10秒鐘之后執(zhí)行的效果

import java.util.Timer;
import java.util.TimerTask;

import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
import static java.text.MessageFormat.format;

public class Task {

    private static final long SECOND = 1000;

    public static void main(String[] args) {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println(format("程序結束時間為: {0}", currentTimeMillis()));
        }));

        Timer timer = new Timer();
        long startTimestamp = currentTimeMillis();
        System.out.println(format("程序執(zhí)行時間為: {0}", startTimestamp));
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                long exceptedTimestamp = startTimestamp + 10 * SECOND;
                long executingTimestamp = currentTimeMillis();
                long offset = executingTimestamp - exceptedTimestamp;
                System.out.println(format("任務運行在線程[{0}]上, 期望執(zhí)行時間為[{1}], 實際執(zhí)行時間為[{2}], 實際偏差[{3}]",
                        currentThread().getName(), exceptedTimestamp, executingTimestamp, offset));
            }
        }, 10 * SECOND);
    }
}

程序運行后的實際執(zhí)行效果

程序執(zhí)行時間為: 1,614,576,593,325
任務運行在線程[Timer-0]上, 期望執(zhí)行時間為[1,614,576,603,325], 實際執(zhí)行時間為[1,614,576,603,343], 實際偏差[18]

回到我們剛剛的問題上,為什么我們的程序在執(zhí)行完定時任務后沒有正常退出?我們可以從Java API中對Thread類的描述中找到相關的內容:

如何使用JDK中的Timer

從這段描述中,我們可以看到,只有在兩種情況下,Java虛擬機才會退出執(zhí)行

  1. 手動調用Runtime.exit()方法,并且安全管理器允許進行退出操作

  2. 所有的非守護線程都結束了,要么是執(zhí)行完run()方法,要么是在run()方法中拋出向上傳播的異常

所有的Timer在創(chuàng)建后都會創(chuàng)建關聯(lián)的工作線程,這個關聯(lián)的工作線程默認是非守護線程的,所以很明顯我們滿足第二個條件,所以程序會繼續(xù)執(zhí)行而不會退出。

那么如果我們將Timer的工作線程設置成守護線程會發(fā)生什么呢?

import java.util.Timer;
import java.util.TimerTask;

import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
import static java.text.MessageFormat.format;

public class Task {

    private static final long SECOND = 1000;

    public static void main(String[] args) {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println(format("程序結束時間為: {0}", currentTimeMillis()));
        }));

        Timer timer = new Timer(true);
        long startTimestamp = currentTimeMillis();
        System.out.println(format("程序執(zhí)行時間為: {0}", startTimestamp));
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                long exceptedTimestamp = startTimestamp + 10 * SECOND;
                long executingTimestamp = currentTimeMillis();
                long offset = executingTimestamp - exceptedTimestamp;
                System.out.println(format("任務運行在線程[{0}]上, 期望執(zhí)行時間為[{1}], 實際執(zhí)行時間為[{2}], 實際偏差[{3}]",
                        currentThread().getName(), exceptedTimestamp, executingTimestamp, offset));
            }
        }, 10 * SECOND);
    }
}

程序運行后的實際執(zhí)行結果

程序執(zhí)行時間為: 1,614,578,037,976
程序結束時間為: 1,614,578,037,996

可以看到我們的延遲任務還沒有開始執(zhí)行,程序就已經(jīng)結束了,因為在我們的主線程退出后,所有的非守護線程都結束了,所以Java虛擬機會正常退出,而不會等待Timer中所有的任務執(zhí)行完成后再退出。

2. 執(zhí)行時間早于當前時間

如果我們是通過計算Date來指定執(zhí)行時間的話,那么不可避免會出現(xiàn)一個問題——計算后的時間是早于當前時間的,這很常見,尤其是Java虛擬機會在不恰當?shù)臅r候執(zhí)行垃圾回收,并導致STW(Stop the world)。

接下來,我們將調整之前調用schedule(TimerTask task, Date time)的代碼,讓它在過去的時間執(zhí)行

import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
import static java.text.MessageFormat.format;

public class Task {

    private static final long SECOND = 1000;

    public static void main(String[] args) {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println(format("程序結束時間為: {0}", currentTimeMillis()));
        }));

        Timer timer = new Timer();
        long startTimestamp = currentTimeMillis();
        System.out.println(format("程序執(zhí)行時間為: {0}", startTimestamp));
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                long exceptedTimestamp = startTimestamp - 10 * SECOND;
                long executingTimestamp = currentTimeMillis();
                long offset = executingTimestamp - exceptedTimestamp;
                System.out.println(format("任務運行在線程[{0}]上, 期望執(zhí)行時間為[{1}], 實際執(zhí)行時間為[{2}], 實際偏差[{3}]",
                        currentThread().getName(), exceptedTimestamp, executingTimestamp, offset));
            }
        }, new Date(startTimestamp - 10 * SECOND));
    }
}

程序運行后的執(zhí)行結果

程序執(zhí)行時間為: 1,614,590,000,184
任務運行在線程[Timer-0]上, 期望執(zhí)行時間為[1,614,589,990,184], 實際執(zhí)行時間為[1,614,590,000,203], 實際偏差[10,019]

可以看到,當我們指定運行時間為過去時間時,Timer的工作線程會立執(zhí)行該任務。

但是如果我們不是通過計算時間,而是期望延遲負數(shù)時間再執(zhí)行,會發(fā)生什么呢?我們將調整之前調用schedule(TimerTask task, long delay)的代碼, 讓他以負數(shù)延遲時間執(zhí)行

import java.util.Timer;
import java.util.TimerTask;

import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
import static java.text.MessageFormat.format;

public class Task {

    private static final long SECOND = 1000;

    public static void main(String[] args) {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println(format("程序結束時間為: {0}", currentTimeMillis()));
        }));

        Timer timer = new Timer();
        long startTimestamp = currentTimeMillis();
        System.out.println(format("程序執(zhí)行時間為: {0}", startTimestamp));
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                long exceptedTimestamp = startTimestamp - 10 * SECOND;
                long executingTimestamp = currentTimeMillis();
                long offset = executingTimestamp - exceptedTimestamp;
                System.out.println(format("任務運行在線程[{0}]上, 期望執(zhí)行時間為[{1}], 實際執(zhí)行時間為[{2}], 實際偏差[{3}]",
                        currentThread().getName(), exceptedTimestamp, executingTimestamp, offset));
            }
        }, -10 * SECOND);
    }
}

程序運行后的執(zhí)行結果

程序執(zhí)行時間為: 1,614,590,267,556
Exception in thread "main" java.lang.IllegalArgumentException: Negative delay.
	at java.base/java.util.Timer.schedule(Timer.java:193)
	at cn.mgdream.schedule.Task.main(Task.java:22)

如果我們傳入負數(shù)的延遲時間,那么Timer會拋出異常,告訴我們不能傳入負數(shù)的延遲時間,這似乎是合理的——我們傳入過去的時間是因為這是我們計算出來的,而不是我們主觀傳入的。在我們使用schedule(TimerTask task, long delay)需要注意這一點。

3. 向Timer中添加多個任務

接下來我們將分別向Timer中添加兩個延遲任務,為了更容易地控制兩個任務的調度順序和時間,我們讓第一個任務延遲5秒,第二個任務延遲10秒,同時讓第一個任務阻塞10秒后再結束,通過這種方式來模擬出長任務。

import java.util.Timer;
import java.util.TimerTask;

import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
import static java.text.MessageFormat.format;

public class Task {

    private static final long SECOND = 1000;

    public static void main(String[] args) {
        Timer timer = new Timer();
        long startTimestamp = currentTimeMillis();
        System.out.println(format("程序執(zhí)行時間為: {0}", startTimestamp));
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                try {
                    long exceptedTimestamp = startTimestamp + 5 * SECOND;
                    long executingTimestamp = currentTimeMillis();
                    long offset = executingTimestamp - exceptedTimestamp;
                    System.out.println(format("任務[0]運行在線程[{0}]上, 期望執(zhí)行時間為[{1}], 實際執(zhí)行時間為[{2}], 實際偏差[{3}]",
                            currentThread().getName(), exceptedTimestamp, executingTimestamp, offset));
                    Thread.sleep(10 * SECOND);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, 5 * SECOND);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                long exceptedTimestamp = startTimestamp + 10 * SECOND;
                long executingTimestamp = currentTimeMillis();
                long offset = executingTimestamp - exceptedTimestamp;
                System.out.println(format("任務[1]運行在線程[{0}]上, 期望執(zhí)行時間為[{1}], 實際執(zhí)行時間為[{2}], 實際偏差[{3}]",
                        currentThread().getName(), exceptedTimestamp, executingTimestamp, offset));
            }
        }, 10 * SECOND);
    }
}

程序運行后的執(zhí)行結果

程序執(zhí)行時間為: 1,614,597,388,284
任務[0]運行在線程[Timer-0]上, 期望執(zhí)行時間為[1,614,597,393,284], 實際執(zhí)行時間為[1,614,597,393,308], 實際偏差[24]
任務[1]運行在線程[Timer-0]上, 期望執(zhí)行時間為[1,614,597,398,284], 實際執(zhí)行時間為[1,614,597,403,312], 實際偏差[5,028]

可以看到,兩個任務在同個線程順序執(zhí)行,而第一個任務因為阻塞了10秒鐘,所以是在程序開始運行后的第15秒結束,而第二個任務期望在第10秒結束,但是因為第一個任務還沒有結束,所以第二個任務在第15秒開始執(zhí)行,與與其執(zhí)行時間偏差5秒鐘。在使用Timer時盡可能不要執(zhí)行長任務或使用阻塞方法,否則會影響后續(xù)任務執(zhí)行時間的準確性。

4. 周期性執(zhí)行任務

接下來我們將會分別使用schedulescheduleAtFixedRate實現(xiàn)周期性執(zhí)行任務。為了節(jié)省篇幅,我們將只演示如何使用schedule(TimerTask task, long delay, long period)scheduleAtFixedRate(TimerTask task, long delay, long period)來實現(xiàn)周期性執(zhí)行任務,并介紹它們的差異。而其他的兩個方法schedule(TimerTask task, Date firstTime, long period)scheduleAtFixedRate(TimerTask task, Date firstTime, long period)具有相同的效果和差異,就不再贅述。

首先我們修改Task類,調用schedule(TimerTask task, long delay, long period)來實現(xiàn)第一次執(zhí)行完延遲任務后,周期性地執(zhí)行任務

import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicLong;

import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
import static java.text.MessageFormat.format;

public class Task {

    private static final long SECOND = 1000;

    public static void main(String[] args) {
        AtomicLong counter = new AtomicLong(0);
        Timer timer = new Timer();
        long startTimestamp = currentTimeMillis();
        System.out.println(format("程序執(zhí)行時間為: {0}", startTimestamp));
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                long count = counter.getAndIncrement();
                long exceptedTimestamp = startTimestamp + 10 * SECOND + count * SECOND;
                long executingTimestamp = currentTimeMillis();
                long offset = executingTimestamp - exceptedTimestamp;
                System.out.println(format("任務運行在線程[{0}]上, 期望執(zhí)行時間為[{1}], 實際執(zhí)行時間為[{2}], 實際偏差[{3}]",
                        currentThread().getName(), exceptedTimestamp, executingTimestamp, offset));
            }
        }, 10 * SECOND, SECOND);
    }
}

修改后的代碼和使用schedule(TimerTask task, long delay)時的代碼基本相同,我們額外添加計數(shù)器來記錄任務的執(zhí)行次數(shù),方法調用添加了第三個參數(shù)period,表示任務每次執(zhí)行時到下一次開始執(zhí)行的時間間隔,我們這里設置成1秒鐘。

程序運行后的執(zhí)行結果

程序執(zhí)行時間為: 1,614,609,111,434
任務運行在線程[Timer-0]上, 期望執(zhí)行時間為[1,614,609,121,434], 實際執(zhí)行時間為[1,614,609,121,456], 實際偏差[22]
任務運行在線程[Timer-0]上, 期望執(zhí)行時間為[1,614,609,122,434], 實際執(zhí)行時間為[1,614,609,122,456], 實際偏差[22]
任務運行在線程[Timer-0]上, 期望執(zhí)行時間為[1,614,609,123,434], 實際執(zhí)行時間為[1,614,609,123,457], 實際偏差[23]
任務運行在線程[Timer-0]上, 期望執(zhí)行時間為[1,614,609,124,434], 實際執(zhí)行時間為[1,614,609,124,462], 實際偏差[28]
任務運行在線程[Timer-0]上, 期望執(zhí)行時間為[1,614,609,125,434], 實際執(zhí)行時間為[1,614,609,125,467], 實際偏差[33]
任務運行在線程[Timer-0]上, 期望執(zhí)行時間為[1,614,609,126,434], 實際執(zhí)行時間為[1,614,609,126,470], 實際偏差[36]
任務運行在線程[Timer-0]上, 期望執(zhí)行時間為[1,614,609,127,434], 實際執(zhí)行時間為[1,614,609,127,473], 實際偏差[39]
任務運行在線程[Timer-0]上, 期望執(zhí)行時間為[1,614,609,128,434], 實際執(zhí)行時間為[1,614,609,128,473], 實際偏差[39]
任務運行在線程[Timer-0]上, 期望執(zhí)行時間為[1,614,609,129,434], 實際執(zhí)行時間為[1,614,609,129,474], 實際偏差[40]

可以看到,每次任務執(zhí)行都會有一定時間的偏差,而這個偏差隨著執(zhí)行次數(shù)的增加而不斷積累。這個時間偏差取決于Timer中需要執(zhí)行的任務的個數(shù),隨著Timer中需要執(zhí)行的任務的個數(shù)增加呈非遞減趨勢。因為這個程序現(xiàn)在只有一個任務在重復執(zhí)行,因此每次執(zhí)行的偏差不是很大,如果同時維護成百上千個任務,那么這個時間偏差會變得很明顯。

接下來我們修改Task類,調用scheduleAtFixedRate(TimerTask task, long delay, long period)來實現(xiàn)周期性執(zhí)行任務

import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicLong;

import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
import static java.text.MessageFormat.format;

public class Task {

    private static final long SECOND = 1000;

    public static void main(String[] args) {
        AtomicLong counter = new AtomicLong(0);
        Timer timer = new Timer();
        long startTimestamp = currentTimeMillis();
        System.out.println(format("程序執(zhí)行時間為: {0}", startTimestamp));
        timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                long count = counter.getAndIncrement();
                long exceptedTimestamp = startTimestamp + 10 * SECOND + count * SECOND;
                long executingTimestamp = currentTimeMillis();
                long offset = executingTimestamp - exceptedTimestamp;
                System.out.println(format("任務運行在線程[{0}]上, 期望執(zhí)行時間為[{1}], 實際執(zhí)行時間為[{2}], 實際偏差[{3}]",
                        currentThread().getName(), exceptedTimestamp, executingTimestamp, offset));
            }
        }, 10 * SECOND, SECOND);
    }
}

方法scheduleAtFixedRate(TimerTask task, long delay, long period)schedule(TimerTask task, long delay)的效果基本相同,它們都可以達到周期性執(zhí)行任務的效果,但是scheduleAtFixedRate方法會修正任務的下一次期望執(zhí)行時間,按照每一次的期望執(zhí)行時間加上period參數(shù)來計算出下一次期望執(zhí)行時間,因此scheduleAtFixedRate是以固定速率重復執(zhí)行的,而schedule則只保證兩次執(zhí)行的時間間隔相同。

程序運行后的執(zhí)行結果

程序執(zhí)行時間為: 1,614,610,372,927
任務運行在線程[Timer-0]上, 期望執(zhí)行時間為[1,614,610,383,927], 實際執(zhí)行時間為[1,614,610,383,950], 實際偏差[23]
任務運行在線程[Timer-0]上, 期望執(zhí)行時間為[1,614,610,384,927], 實際執(zhí)行時間為[1,614,610,384,951], 實際偏差[24]
任務運行在線程[Timer-0]上, 期望執(zhí)行時間為[1,614,610,385,927], 實際執(zhí)行時間為[1,614,610,385,951], 實際偏差[24]
任務運行在線程[Timer-0]上, 期望執(zhí)行時間為[1,614,610,386,927], 實際執(zhí)行時間為[1,614,610,386,947], 實際偏差[20]
任務運行在線程[Timer-0]上, 期望執(zhí)行時間為[1,614,610,387,927], 實際執(zhí)行時間為[1,614,610,387,949], 實際偏差[22]
任務運行在線程[Timer-0]上, 期望執(zhí)行時間為[1,614,610,388,927], 實際執(zhí)行時間為[1,614,610,388,946], 實際偏差[19]
任務運行在線程[Timer-0]上, 期望執(zhí)行時間為[1,614,610,389,927], 實際執(zhí)行時間為[1,614,610,389,946], 實際偏差[19]
任務運行在線程[Timer-0]上, 期望執(zhí)行時間為[1,614,610,390,927], 實際執(zhí)行時間為[1,614,610,390,947], 實際偏差[20]
任務運行在線程[Timer-0]上, 期望執(zhí)行時間為[1,614,610,391,927], 實際執(zhí)行時間為[1,614,610,391,950], 實際偏差[23]
任務運行在線程[Timer-0]上, 期望執(zhí)行時間為[1,614,610,392,927], 實際執(zhí)行時間為[1,614,610,392,946], 實際偏差[19]
5. 停止任務

盡管我們很少會主動停止任務,但是這里還是要介紹下任務停止的方式。

停止任務的方式分為兩種:停止單個任務和停止整個Timer。

首先我們介紹如何停止單個任務,為了停止單個任務,我們需要調用TimerTaskcancal()方法,并調用Timerpurge()方法來移除所有已經(jīng)被停止了的任務(回顧我們之前提到的,過多停止的任務不清空會影響我們的執(zhí)行時間)

import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicLong;

import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
import static java.text.MessageFormat.format;

public class Task {

    private static final long SECOND = 1000;

    public static void main(String[] args) {
        AtomicLong counter = new AtomicLong(0);
        Timer timer = new Timer();
        long startTimestamp = currentTimeMillis();
        System.out.println(format("程序執(zhí)行時間為: {0}", startTimestamp));
        TimerTask[] timerTasks = new TimerTask[4096];
        for (int i = 0; i < timerTasks.length; i++) {
            final int serialNumber = i;
            timerTasks[i] = new TimerTask() {
                @Override
                public void run() {
                    long count = counter.getAndIncrement();
                    long exceptedTimestamp = startTimestamp + 10 * SECOND + count * SECOND;
                    long executingTimestamp = currentTimeMillis();
                    long offset = executingTimestamp - exceptedTimestamp;
                    System.out.println(format("任務[{0}]運行在線程[{1}]上, 期望執(zhí)行時間為[{2}], 實際執(zhí)行時間為[{3}], 實際偏差[{4}]",
                            serialNumber, currentThread().getName(), exceptedTimestamp, executingTimestamp, offset));
                }
            };
        }
        for (TimerTask timerTask : timerTasks) {
            timer.schedule(timerTask, 10 * SECOND, SECOND);
        }
        for (int i = 1; i < timerTasks.length; i++) {
            timerTasks[i].cancel();
        }
        timer.purge();
    }
}

首先我們創(chuàng)建了4096個任務,并讓Timer來調度它們,接下來我們把除了第0個任務外的其他4095個任務停止掉,并從Timer中移除所有已經(jīng)停止的任務。

程序運行后的執(zhí)行結果

程序執(zhí)行時間為: 1,614,611,843,830
任務[0]運行在線程[Timer-0]上, 期望執(zhí)行時間為[1,614,611,853,830], 實際執(zhí)行時間為[1,614,611,853,869], 實際偏差[39]
任務[0]運行在線程[Timer-0]上, 期望執(zhí)行時間為[1,614,611,854,830], 實際執(zhí)行時間為[1,614,611,854,872], 實際偏差[42]
任務[0]運行在線程[Timer-0]上, 期望執(zhí)行時間為[1,614,611,855,830], 實際執(zhí)行時間為[1,614,611,855,875], 實際偏差[45]
任務[0]運行在線程[Timer-0]上, 期望執(zhí)行時間為[1,614,611,856,830], 實際執(zhí)行時間為[1,614,611,856,876], 實際偏差[46]
任務[0]運行在線程[Timer-0]上, 期望執(zhí)行時間為[1,614,611,857,830], 實際執(zhí)行時間為[1,614,611,857,882], 實際偏差[52]
任務[0]運行在線程[Timer-0]上, 期望執(zhí)行時間為[1,614,611,858,830], 實際執(zhí)行時間為[1,614,611,858,883], 實際偏差[53]
任務[0]運行在線程[Timer-0]上, 期望執(zhí)行時間為[1,614,611,859,830], 實際執(zhí)行時間為[1,614,611,859,887], 實際偏差[57]
任務[0]運行在線程[Timer-0]上, 期望執(zhí)行時間為[1,614,611,860,830], 實際執(zhí)行時間為[1,614,611,860,890], 實際偏差[60]
任務[0]運行在線程[Timer-0]上, 期望執(zhí)行時間為[1,614,611,861,830], 實際執(zhí)行時間為[1,614,611,861,891], 實際偏差[61]
任務[0]運行在線程[Timer-0]上, 期望執(zhí)行時間為[1,614,611,862,830], 實際執(zhí)行時間為[1,614,611,862,892], 實際偏差[62]

我們可以看到,只有第0個任務再繼續(xù)執(zhí)行,而其他4095個任務都沒有執(zhí)行。

接下來我們介紹如何使用Timercancel()來停止整個Timer的所有任務,其實很簡單,只需要執(zhí)行timer.cancel()就可以。

import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicLong;

import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
import static java.text.MessageFormat.format;

public class Task {

    private static final long SECOND = 1000;

    public static void main(String[] args) {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println(format("程序結束時間為: {0}", currentTimeMillis()));
        }));
        
        AtomicLong counter = new AtomicLong(0);
        Timer timer = new Timer();
        long startTimestamp = currentTimeMillis();
        System.out.println(format("程序執(zhí)行時間為: {0}", startTimestamp));
        TimerTask[] timerTasks = new TimerTask[4096];
        for (int i = 0; i < timerTasks.length; i++) {
            final int serialNumber = i;
            timerTasks[i] = new TimerTask() {
                @Override
                public void run() {
                    long count = counter.getAndIncrement();
                    long exceptedTimestamp = startTimestamp + 10 * SECOND + count * SECOND;
                    long executingTimestamp = currentTimeMillis();
                    long offset = executingTimestamp - exceptedTimestamp;
                    System.out.println(format("任務[{0}]運行在線程[{1}]上, 期望執(zhí)行時間為[{2}], 實際執(zhí)行時間為[{3}], 實際偏差[{4}]",
                            serialNumber, currentThread().getName(), exceptedTimestamp, executingTimestamp, offset));
                }
            };
        }
        timer.cancel();
    }
}

在將所有的任務添加到Timer后,我們執(zhí)行Timer對象的cancel()方法,為了更方便地表現(xiàn)出Timer的工作線程也終止了,我們注冊了生命周期方法,來幫我們在程序結束后打印結束時間。

程序運行后的執(zhí)行結果

程序執(zhí)行時間為: 1,614,612,436,037
程序結束時間為: 1,614,612,436,061

可以看到,在執(zhí)行Timer對象的cancel()方法后,Timer的工作線程也隨之結束,程序正常退出。

源碼解析

TimerTask


TimerTask類是一個抽象類,實現(xiàn)了Runnable接口

public abstract class TimerTask implements Runnable
TimerTask對象的成員

首先來看TimerTask類的成員部分

final Object lock = new Object();

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;

對象lock是對外用來控制TimerTask對象修改的鎖對象,它控制了鎖的粒度——只會影響類屬性的變更,而不會影響整個類的方法調用。接下來是state屬性表示TimerTask對象的狀態(tài)。nextExecutionTime屬性表示TimerTask對象的下一次執(zhí)行時間,當TimerTask對象被添加到任務隊列后,將會使用這個屬性來按照從小到大的順序排序。period屬性表示TimerTask對象的執(zhí)行周期,period屬性的值有三種情況

  1. 如果是0,那么表示任務不會重復執(zhí)行

  2. 如果是正數(shù),那么就表示任務按照相同的執(zhí)行間隔來重復執(zhí)行

  3. 如果是負數(shù),那么就表示任務按照相同的執(zhí)行速率來重復執(zhí)行

TimerTask對象的構造方法

Timer對象的構造方法很簡單,就是protected限定的默認構造方法,不再贅述

protected TimerTask() {
}
TimerTask對象的成員方法

接下來我們看下TimerTask對象的成員方法

public abstract void run();

public boolean cancel() {
    synchronized(lock) {
        boolean result = (state == SCHEDULED);
        state = CANCELLED;
        return result;
    }
}

public long scheduledExecutionTime() {
    synchronized(lock) {
        return (period < 0 ? nextExecutionTime + period
                           : nextExecutionTime - period);
    }
}

首先是run()方法實現(xiàn)自Runnable()接口,為抽象方法,所有的任務都需要實現(xiàn)此方法。接下來是cancel()方法,這個方法會將任務的狀態(tài)標記為CANCELLED,如果在結束前任務處于被調度狀態(tài),那么就返回true,否則返回false。至于scheduledExecutionTime()只是用來計算重復執(zhí)行的下一次執(zhí)行時間,在Timer中并沒有被使用過,不再贅述。

TimerQueue


TimerQueueTimer維護任務調度順序的最小優(yōu)先隊列,使用的是最小二叉堆實現(xiàn),如上文所述,排序用的Key是TimerTasknextExecutionTime屬性。

在介紹TimerQueue之前,我們先補充下數(shù)據(jù)結構的基礎知識

二叉堆(Binary heap)

二叉堆是一顆除了最底層的元素外,所有層都被填滿,最底層的元素從左向右填充的完全二叉樹(complete binary tree)。完全二叉樹可以用數(shù)組表示,假設元素從1開始編號,下標為i的元素,它的左孩子的下標為2*i,它的右孩子的下標為2*i+1。

二叉堆的任意非葉節(jié)點滿足堆序性:假設我們定義的是最小優(yōu)先隊列,那么我們使用的是小根堆,任意節(jié)點的元素值都小于它的左孩子和右孩子(如果有的話)的元素值。

二叉堆的定義滿足遞歸定義法,即二叉堆的任意子樹都是二叉堆,單個節(jié)點本身就是二叉堆。

根據(jù)堆序性和遞歸定義法,二叉堆的根節(jié)點一定是整個二叉堆中元素值最小的節(jié)點。

與堆結構有關的操作,除了add, getMinremoveMin之外,還有fixUp、fixDownheapify三個關鍵操作,而add、getMinremoveMin也是通過這三個操作來完成的,下面來簡單介紹下這三個操作

  1. fixUp: 當我們向二叉堆中添加元素時,我們可以簡單地將它添加到二叉樹的末尾,此時從這個節(jié)點到根的完整路徑上不滿足堆序性。之后將它不斷向上浮,直到遇到比它小的元素,此時整個二叉樹的所有節(jié)點都滿足堆序性。當我們減少了二叉堆中元素的值的時候也可以通過這個方法來維護二叉堆。

  2. fixDown: 當我們從二叉堆中刪除元素時,我們可以簡單地將二叉樹末尾的元素移動到根,此時不一定滿足堆序性,之后將它不斷下沉,直到遇到比它大的元素,此時整個二叉樹的所有節(jié)點都滿足堆序性。當我們增加了二叉堆中元素的值的時候也可以通過這個方法來維護二叉堆。

  3. heapify: 當我們拿到無序的數(shù)組的時候,也可以假設我們拿到了一棵不滿足堆序性的二叉樹,此時我們將所有的非葉節(jié)點向下沉,直到整個二叉樹的所有節(jié)點都滿足堆序性,此時我們得到了完整的二叉堆。這個操作是原地操作,不需要額外的空間復雜度,而時間復雜度是O(N)。

關于二叉堆的詳細內容將會在后續(xù)的文章中展開詳解,這里只做簡單的介紹,了解這些我們就可以開始看TimerQueue的源碼。

TimerQueue的完整代碼

我們直接來看TaskQueue的完整代碼

class TaskQueue {

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

    private int size = 0;

    int size() {
        return size;
    }

    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);
    }

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

    TimerTask get(int i) {
        return queue[i];
    }

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

    void quickRemove(int i) {
        assert i <= size;

        queue[i] = queue[size];
        queue[size--] = null;  // Drop extra ref to prevent memory leak
    }

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

    boolean isEmpty() {
        return size==0;
    }

    void clear() {
        // Null out task references to prevent memory leak
        for (int i=1; i<=size; i++)
            queue[i] = null;

        size = 0;
    }

    private void fixUp(int k) {
        while (k > 1) {
            int j = k >> 1;
            if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime)
                break;
            TimerTask tmp = queue[j];  queue[j] = queue[k]; queue[k] = tmp;
            k = j;
        }
    }

    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;
        }
    }

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

按照我們之前介紹的二叉堆的相關知識,我們可以看到TimerQueue維護了TimerTask的數(shù)組queue,初始大小size為0。

add操作首先判斷了數(shù)組是否滿了,如果數(shù)組已經(jīng)滿了,那么先執(zhí)行擴容操作,再進行添加操作。如上所述,add操作先將元素放到二叉樹末尾的元素(queue[++size]),之后對這個元素進行上浮來維護堆序性。

getMin直接返回二叉樹的樹根(queue[1]),get方法直接返回數(shù)組的第i個元素。removeMin方法會將二叉樹末尾的元素(queue[size])移動到樹根(queue[1]),并將原本二叉樹末尾的元素設置成null,來讓垃圾回收器回收這個TimerTask,之后執(zhí)行fixDown來維護堆序性,quickRemove也是相同的過程,只不過它在移動元素后沒有執(zhí)行下沉操作,當連續(xù)執(zhí)行多次quickRemove后統(tǒng)一執(zhí)行heapify來維護堆序性。

rescheduleMin會將樹根元素的元素值設置成newTime,并將它下沉到合適的位置。

fixUp、fixDownheapify操作就如上文所述,用來維護二叉堆的讀序性。不過這里面實現(xiàn)的fixUpfixDown并不優(yōu)雅,基于交換臨位元素的實現(xiàn)需要使用T(3log(N))的時間,而實際上有T(log(N))的實現(xiàn)方法。后續(xù)的文章中會詳細介紹優(yōu)先隊列與二叉堆的實現(xiàn)方式。

TimerThread


我們直接來看TimerThread的代碼

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
            }
        }
    }

    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) {
            }
        }
    }
}

首先是控制變量newTasksMayBeScheduled,表示當前工作線程是否應該繼續(xù)執(zhí)行任務,當它為false的時候它將不會再從任務隊列中取任務執(zhí)行,表示當前工作線程已結束。接下來的queue變量是通過構造方法傳進來的任務隊列,工作線程的任務隊列與Timer共享,實現(xiàn)生產(chǎn)消費者模型。

進入到run()方法,run()方法會調用mainLoop()方法來執(zhí)行主循環(huán),而finally代碼塊會在主循環(huán)結束后清空任務隊列實現(xiàn)優(yōu)雅退出。

mainLoop()方法中執(zhí)行了死循環(huán)來拉取執(zhí)行任務,在死循環(huán)中首先獲取queue的鎖來實現(xiàn)線程同步,接下來判斷任務隊列是否為且工作線程是否停止,如果任務隊列為空且工作線程未停止,那么就使用queue.wait()來等待Timer添加任務后喚醒該線程,Object#wait()方法會釋放當前線程所持有的該對象的鎖,關于wait/notisfy的內容可以去看Java API相關介紹。如果queue退出等待后依舊為空,則表示newTasksMayBeScheduledfalse,工作線程已停止,退出主循環(huán),否則會從任務隊列中取出需要最近執(zhí)行的任務(并不會刪除任務)。

取到需要最近執(zhí)行的任務后,獲取該任務的鎖,并判斷該任務是否已經(jīng)停止,如果該任務已經(jīng)停止,那么就把它從任務隊列中移除,并什么都不做繼續(xù)執(zhí)行主循環(huán)。接下來判斷當前時間是否小于等于任務的下一次執(zhí)行時間,如果滿足條件則將taskFired設置成true,判斷當前任務是否需要重復執(zhí)行。如果不需要重復執(zhí)行就將它從任務隊列中移除,并將任務狀態(tài)設置成EXECUTED,如果需要重復執(zhí)行就根據(jù)period設置它的下一次執(zhí)行時間并重新調整任務隊列。

完成這些操作后,如果taskFiredfalse,就讓queue對象進入有限等待狀態(tài),很容易得到我們需要的最大等待時間為executionTime - currentTime。如果taskFiredtrue,那么就釋放鎖并執(zhí)行被取出的任務。

Timer


Timer對象的成員

首先來看Timer的成員部分

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

private final Object threadReaper = new Object() {
    @SuppressWarnings("deprecation")
    protected void finalize() throws Throwable {
        synchronized(queue) {
            thread.newTasksMayBeScheduled = false;
            queue.notify(); // In case queue is empty.
        }
    }
};

private static final AtomicInteger nextSerialNumber = new AtomicInteger(0);

其中queue對象是如前面所說,為了任務調度的最小優(yōu)先隊列。接下來是TimerThread,它是Timer的工作線程,在Timer創(chuàng)建時就已經(jīng)被分配,并與Timer共享任務隊列。

threadReaper是一個只復寫了finalize方法的對象,它的作用是當Timer對象沒有存活的引用后,終止任務線程,并等待任務隊列中的所有任務執(zhí)行結束后退出工作線程,實現(xiàn)優(yōu)雅退出。

nextSerialNumber用來記錄工作線程的序列號,全局唯一,避免生成的線程名稱沖突。

Timer對象的構造方法

接下來我們看下Timer的所有構造方法

public Timer() {
    this("Timer-" + serialNumber());
}

public Timer(boolean isDaemon) {
    this("Timer-" + serialNumber(), isDaemon);
}

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

public Timer(String name, boolean isDaemon) {
    thread.setName(name);
    thread.setDaemon(isDaemon);
    thread.start();
}

可以看到,所有的構造構造方法所做的事都相同:設置工作線程屬性,并啟動工作線程。

成員函數(shù)

接下來我們可以看下Timer的成員函數(shù),我們首先不考慮cancel()purge()方法,直接看schedule系列方法

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 task, Date time) {
    sched(task, time.getTime(), 0);
}

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);
}

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

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);
}

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

可以看到,所有的schedule方法除了做參數(shù)教研外,都將延遲時間和計劃執(zhí)行時間轉化為時間戳委托給sched方法來執(zhí)行。schedulescheduleAtFixedRate傳遞的參數(shù)都相同,不過在傳遞period參數(shù)時使用符號來區(qū)分周期執(zhí)行的方式。

接下來我們可以看下這位神秘嘉賓——sched方法到底做了哪些事

private void sched(TimerTask task, long time, long period) {
    if (time < 0)
        throw new IllegalArgumentException("Illegal execution time.");

    // Constrain value of period sufficiently to prevent numeric
    // overflow while still being effectively infinitely large.
    if (Math.abs(period) > (Long.MAX_VALUE >> 1))
        period >>= 1;

    synchronized(queue) {
        if (!thread.newTasksMayBeScheduled)
            throw new IllegalStateException("Timer already cancelled.");

        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;
        }

        queue.add(task);
        if (queue.getMin() == task)
            queue.notify();
    }
}

sched方法首先做了一些參數(shù)校驗,保證期待執(zhí)行時間不小于0,且執(zhí)行周期不至于太大。接下來獲取任務隊列queue對象的monitor(監(jiān)視器鎖),如果Timer的工作線程已經(jīng)被停止了,那么就會拋出IllegalStateException來禁止繼續(xù)添加任務,newTasksMayBeScheduled這個變量將會在稍后介紹。之后sched方法會嘗試獲取task.lock對象的鎖,判斷task的狀態(tài)避免重復添加,并設置task的下一次執(zhí)行時間、task的執(zhí)行周期和狀態(tài)。之后將task添加到任務隊列中,如果當前任務就是執(zhí)行時間最近的任務,那么就會喚起等待queue對象的線程(其實就是thread工作線程)繼續(xù)執(zhí)行。

感謝各位的閱讀,以上就是“如何使用JDK中的Timer”的內容了,經(jīng)過本文的學習后,相信大家對如何使用JDK中的Timer這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內容。

AI