線程上下文類加載器(Context ClassLoader)

寫在前面:

(1)一句話解釋線程上下文類加載器:正常情況下,線程執行到某個類的時候,只能看到這個類對應加載器所加載的類。但是你可以爲當前線程設置一個類加載器,然後可視範圍就增加多一個類加載器加載的類 

(2)爲什麼需要線程上下文類加載器:

jdk內部類用引導類加載器加載,調SPI接口的方法依賴外部JAR包用應用類加載器加載,父加載器訪問不到子加載器的類。但是可以設置當前線程的上下文類加載器,把當前線程上下文類加載器加載的類一併納入可視範圍

 

其他說明:

1.線程上下文類加載器是從jdk1.2開始引入的,類Thread中的getContextClassLoader()與setContextClassLoader(ClassLoader c1),分別用來獲取和設置類加載器

如果沒有通過setContextClassLoader方法進行設置的話,線程將繼承其父線程的上下文加載器,java應用運行時的初始線程的上下文類加載器是系統類加載器(這裏是由Launcher類設置的)。在線程中運行的代碼可以通過該類加載器來加載類和資源


2.線程上下文類加載器的重要性:

SPI(Service Provider Interface,服務提供者接口,指的是JDK提供標準接口,具體實現由廠商決定。例如sql)

父ClassLoader可以使用當前線程Thread.current.currentThread().getContextClassLoader()所指定的classLoader加載的類。這就改變了父ClassLoader不能使用子ClassLoader加載的類的情況,即改變了雙親委託模型。

線程上下文類加載器就是當前線程的CurrentClassloader。
 

在雙親委託模型下,類加載器是由下至上的,即下層的類加載器會委託上層進行加載。但是對於SPI來說,有些接口是JAVA核心庫提供的,而JAVA核心庫是由啓動類加載器來加載的,而這些接口的實現卻來自於不同的jar包(廠商提供),JAVA的啓動類加載器是不會加載其他來源的jar包,這樣傳統的雙親委託模型就無法滿足SPI的要求。而通過給當前線程設置上下文類加載器,就可以設置的上下文類加載器來實現對於接口實現類的加載。

 

3.線程上下文類加載器使用的一般模式:(獲取-使用-還原)

僞代碼:

 
  1. ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
  2. try{
  3. Thread.currentThread().setContextClassLoader(targetTccl);
  4. excute();
  5. } finally {
  6. Thread.currentThread().setContextClassLoader(classLoader);
  7. }
 

4.當高層提供了統一的接口讓低層去實現,同時又要在高層加載(或者實例化)低層的類時,就必須要通過線程上下文類加載器來幫助高層的ClassLoader找到並加載該類

通過上面的瞭解,我們應該知道爲什麼要用這種樹狀結構了。它都是一層一層級別的控制,這麼做方便去管理,提高安全性,出了問題也能很快的定位到。就像公司的人員組織架構一樣,一切都是爲了管理好公司。

什麼是線程組

在Java中,線程組使用ThreadGroup表示,其中Thread存於線程組中,從字面意思也很好理解。在創建線程過程中,Thread不能獨立於線程組之外,之前我們學習創建線程時,沒有指定線程組,因爲在默認情況下,它會將當前的線程環境作爲線程組, 可以通過Thread.currentThread().getThreadGroup()獲取線程組,線程組可以方便管理我們的線程,一定程度上提高了安全性。

public class ThreadGroupTest {
    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getThreadGroup().getName());
        }).start();
        System.out.println(Thread.currentThread().getThreadGroup().getName());
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

輸出:

main
main
  • 1.
  • 2.

可以發現在main線程組下;

ThreadGroup是一個標準的「向下引用」的樹狀結構,這樣設計的原因是「防止"上級"線程被"下級"線程引用而無法有效地被GC回收」。

線程優先級

線程的優先級級別由操作系統決定,不同的操作系統級別是不一樣的,在Java中,提供了一個級別範圍1~10,方便我們去參考。Java默認的線程優先級爲5,線程的執行順序由調度程序來決定,線程的優先級會在線程被調用之前設定。

源碼描述:

/**
     * 最低級別
     */
    public final static int MIN_PRIORITY = 1;

   /**
     * 默認級別
     */
    public final static int NORM_PRIORITY = 5;

    /**
     * 最高級別
     */
    public final static int MAX_PRIORITY = 10;
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

獲取線程優先級別

public static void main(String[] args) {
    new Thread(() -> {
        System.out.println("default level: {}" + Thread.currentThread().getPriority());
    }).start();
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

輸出: default level: {}5

設置級別

public static void main(String[] args) {
    Thread t = new Thread(() -> {
        System.out.println("default level: {}" + Thread.currentThread().getPriority());
    });
    t.start();
    t.setPriority(10);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

輸出: default level: {}10

通常來講,高級別的優先級往往會更高几率的執行,注意這裏是概率性問題,下面我們測試一下:

public static void main(String[] args) {
    Thread t = new Thread(() -> {
        System.out.println("hello " +  Thread.currentThread().getPriority());
        System.out.println("default level: {}" + Thread.currentThread().getPriority());
    });
    t.setPriority(3);

    Thread t1 = new Thread(() -> {
        System.out.println("hello " +  Thread.currentThread().getPriority());
        System.out.println("default level: {}" + Thread.currentThread().getPriority());
    });
    t1.setPriority(7);

    Thread t2 = new Thread(() -> {
        System.out.println("hello " +  Thread.currentThread().getPriority());
        System.out.println("default level: {}" + Thread.currentThread().getPriority());
    });
    t2.setPriority(10);


    t.start();
    t1.start();
    t2.start();
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.

第一次輸出:

hello 7
default level: {}7
hello 10
default level: {}10
hello 3
default level: {}3
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

第二次:

hello 3
default level: {}3
hello 7
default level: {}7
hello 10
default level: {}10
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

第三次

hello 10
default level: {}10
hello 7
default level: {}7
hello 3
default level: {}3
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

...

發現,不斷的嘗試之後,高級別出現的概率會比較靠前一點, 所以想要藉助它來完成一些特定業務的同學注意了,不建議使用,不靠譜,之前也講到,底層還是由操作系統調度完成

Java提供一個「線程調度器」來監視和控制處於「RUNNABLE狀態」的線程。線程的調度策略採用「搶佔式」,優先級高的線程比優先級低的線程會有更大的機率優先執行。在優先級相同的情況下,按照“先到先得”的原則。每個Java程序都有一個默認的主線程,就是通過JVM啓動的第一個線程main線程。

除了主線程之外,還有一個線程是守護線程,它的優先級比較低。如果所有的非守護線程都結束了,這個守護線程也會自動結束。可以藉助它實現一些特定場景,比如手動關閉線程的場景,某些場景下不關閉,會造成資源浪費,手動關閉又很麻煩。這裏我們可以通過setDaemon(true)指定。

public static void main(String[] args) {
    Thread t = new Thread(() -> {
        System.out.println("hello " +  Thread.currentThread().getPriority());
        System.out.println("default level: {}" + Thread.currentThread().getPriority());
    });
    t.setDaemon(true); // 默認爲false
    t.setPriority(10);

    Thread t1 = new Thread(() -> {
        System.out.println("hello " +  Thread.currentThread().getPriority());
        System.out.println("default level: {}" + Thread.currentThread().getPriority());
    });
    t1.setPriority(7);

    t.start();
    t1.start();
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

輸出:

hello 7
default level: {}7
hello 10
default level: {}10
  • 1.
  • 2.
  • 3.
  • 4.

發現即使指定了高級別,執行的優先級仍然是最低的

線程組下的優先級

剛剛我們都是在main線程組下,舉一反三,線程組下的優先級又是怎麼樣的呢❓下面,測試一下:

public static void main(String[] args) {
        // 指定 name 爲 g1的線程組
        ThreadGroup group = new ThreadGroup("g1");
        group.setMaxPriority(4);

        Thread t = new Thread(group, () -> {
            System.out.println("hello " +  Thread.currentThread().getPriority());
            System.out.println("default level: {}" + Thread.currentThread().getPriority());
        }, "t0");
        t.setPriority(10);
        t.start();
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

輸出:

hello 4
default level: {}4
  • 1.
  • 2.

發現,在g1線程組下指定了最大優先級後,線程t0的優先級最大級別只能是4, 所以這也是使用線程組的好處。

我們可以通過如下方式複製線程組, ThreadGroup提供了enumerate方法:

public static void main(String[] args) throws InterruptedException {
        // 指定 name 爲 g1的線程組
        ThreadGroup group = new ThreadGroup("g1");
        group.setMaxPriority(4);

        Thread t = new Thread(group, () -> {
            System.out.println("hello " +  Thread.currentThread().getPriority());
            System.out.println("default level: {}" + Thread.currentThread().getPriority());
        }, "t0");
        t.setPriority(10);
        t.start();

        // 複製線程組
        System.out.println(group.activeCount()); // 1
        Thread[] list = new Thread[group.activeCount()];
        group.enumerate(list);

        Thread.sleep(3000);
        System.out.println(list[0].getName()); // 輸出 t0

    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.

統一異常捕獲

public static void main(String[] args) {
        // 指定 name 爲 g1的線程組
        ThreadGroup group = new ThreadGroup("g1") {
            // 統一異常捕獲
            public void uncaughtException(Thread t, Throwable e) {
                System.out.println(t.getName() + ": " + e.getMessage()); // t0: 我出錯了
            }
        };
        group.setMaxPriority(4);

        Thread t = new Thread(group, () -> {
            System.out.println("hello " +  Thread.currentThread().getPriority());
            System.out.println("default level: {}" + Thread.currentThread().getPriority());
            throw new RuntimeException("我出錯了");
        }, "t0");
        t.setPriority(10);
        t.start();
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

向下引用的樹狀數據結構

線程組的內部其實不單單可以放線程,其實也可以放其它線程組,我們看下源碼定義

 public static void main(String[] args) {
        // 指定 name 爲 g1的線程組
        ThreadGroup group = new ThreadGroup("g1") {
            // 統一異常捕獲
            public void uncaughtException(Thread t, Throwable e) {
                System.out.println(t.getName() + ": " + e.getMessage()); // t0: 我出錯了
            }
        };
        group.setMaxPriority(4);

        Thread t = new Thread(group, () -> {
            System.out.println("hello " +  Thread.currentThread().getPriority());
            System.out.println("default level: {}" + Thread.currentThread().getPriority());
            throw new RuntimeException("我出錯了");
        }, "t0");
        t.setPriority(10);
        t.start();
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

這裏大家可以大膽去猜測一下,爲什麼要採用這種數據結構❓其實你通過源碼發現,它的內部很多地方都調用了checkAccess方法,特別是在set操作,字面意思是檢查是否有權限,我看下這個方法。

public final void checkAccess() {
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkAccess(this);
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

它調用了一個SecurityManager, 它是Java的安全管理器,它允許應用程序在執行一個可能不安全或敏感的操作前確定該操作是什麼,以及是否是在允許執行該操作的安全上下文中執行它。應用程序可以允許或不允許該操作。總的來說就是保證安全性。

通過上面的瞭解,我們應該知道爲什麼要用這種樹狀結構了。它都是一層一層級別的控制,這麼做方便去管理,提高安全性,出了問題也能很快的定位到。就像公司的人員組織架構一樣,一切都是爲了管理好公司。

面試官:有了解過線程組和線程優先級嗎?-51CTO.COM

一篇文章讓你徹底搞懂定時線程池ScheduledThreadPoolExecutor(深度剖析)_未聞花名丶丶的博客-CSDN博客_定時線程池

前言


一、ScheduledThreadPoolExecutor

這是一個可以在指定一定延遲時間後或者定時進行任務調度執行的線程池
可以看到他也是繼承於ThreadPoolExecutor線程池並且實現了ScheduledExecutorService接口。

在這裏插入圖片描述


1、快速入門-常用方法使用案例

public class ScheduledThreadPoolExecutorExample {

    public static void main(String[] args) {
        ScheduledThreadPoolExecutor executor = (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(5);
        Task task = new Task("任務");
        System.out.println("Created : " + task.getName());
         executor.schedule(task, 2, TimeUnit.SECONDS);// 只執行一次
        // executor.scheduleWithFixedDelay(task, 0, 2, TimeUnit.SECONDS); //任務+延遲
//        executor.scheduleAtFixedRate(task, 0, 1, TimeUnit.SECONDS);//任延遲取最大值 穩定定時器

    }
}

class Task implements Runnable {
    private String name;

    public Task(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void run() {
        System.out.println("Executing : " + name + ", Current Seconds : " + new Date().getSeconds());
        try {
        	// 模擬處理業務需要5秒鐘
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

schedule:延遲多長時間之後只執行一次;
schedule輸出結果:

Created : 任務
Executing : 任務, Current Seconds : 43

scheduleWithFixedDelay:延遲指定時間後執行一次,之後按照:上一次任務執行時長 + 週期的時長 的時間去週期執行(這裏是執行5秒+延遲2秒=7秒一次)
scheduleWithFixedDelay輸出結果:

Created : 任務
Executing : 任務, Current Seconds : 57
Executing : 任務, Current Seconds : 4
Executing : 任務, Current Seconds : 11
Executing : 任務, Current Seconds : 18
Executing : 任務, Current Seconds : 25
Executing : 任務, Current Seconds : 32

scheduleAtFixedRate:延遲指定時間後執行一次,之後按照固定的時長週期執行(這裏是5秒執行一次);
scheduleAtFixedRate輸出結果:

Created : 任務
Executing : 任務, Current Seconds : 19
Executing : 任務, Current Seconds : 24
Executing : 任務, Current Seconds : 29

二、源碼類圖分析

2.1、ScheduledThreadPoolExecutor-構造方法

public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

這裏構造方法其實還是調用ThreadPoolExecutor的構造方法,不同點的是這裏的隊列是用的DelayedWorkQueue(延遲阻塞隊列)


2.2、DelayedWorkQueue-延遲阻塞隊列

DelayedWorkQueue是ScheduledThreadPoolExecutor的一個靜態內部類,實現了阻塞隊列。
可以看到他是由數組實現,默認數組容量是16,並且支持擴容,每次擴容是50%。

static class DelayedWorkQueue extends AbstractQueue<Runnable>
        implements BlockingQueue<Runnable> {

private static final int INITIAL_CAPACITY = 16;
// 數組
private RunnableScheduledFuture<?>[] queue = new RunnableScheduledFuture<?>[INITIAL_CAPACITY];

2.3、ScheduledFutureTask-具有返回結果值的任務

ScheduledFutureTask是ScheduledThreadPoolExecutor的內部類。
實現於FutureTask 繼承於RunnableScheduledFuture。
每個要執行的線程任務都會被轉換爲ScheduledFutureTask。

private class ScheduledFutureTask<V>
            extends FutureTask<V> implements RunnableScheduledFuture<V> {

		// 任務的序號
		private final long sequenceNumber;
		// 延遲時間
        private long time;
        // 重複任務標識,正數爲重複任務,爲0或者負數爲非重複任務
        private final long period;

// 構造方法
// 對上面3個核心屬性進行初始化賦值,並且調用了父類FutureTask的構造方法
ScheduledFutureTask(Runnable r, V result, long ns, long period) {
            super(r, result);
            this.time = ns;
            this.period = period;
            this.sequenceNumber = sequencer.getAndIncrement();
        }

2.4、父類FutureTask的構造方法、屬性

public FutureTask(Runnable runnable, V result) {
		// callable是一個帶有返回值的線程
        this.callable = Executors.callable(runnable, result);
        // state狀態-NEW爲初始狀態
        this.state = NEW;       // ensure visibility of callable
    }

FutureTask內部有一個變量state用來表示任務的黃天,一開始狀態爲NEW;
所有狀態如下:

private static final int NEW  		=0;//	初始狀態
private static final int COMPLETING = 1; //執行中狀態
private static final int NORMAL = 2 ; // 正常運行結束狀態
private static final int EXCEPTIONAL = 3;// 運行中異常
private static final int CANCELLED = 4;// 任務被取消
private static final int INTERRUPTING= 5;// 任務正在被中斷
private static final int INTERRUPTED = 6;//任務已經被中斷

任務狀態轉換

初始化》執行中》正常執行結束
初始化》執行中》執行異常
初始化》任務取消
初始化》任務被中斷中》被中斷

三、核心方法源碼分析

圍繞上面3個核心方法的源碼進行分析拆解。

  public ScheduledFuture<?> schedule(Runnable command,
                                       long delay, TimeUnit unit);
    
    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit);

    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit);

}

3.1、schedule方法-只執行一次任務

根據延遲時間和執行時間只執行一次任務。

public ScheduledFuture<?> schedule(Runnable command,
                                       long delay,
                                       TimeUnit unit) {
        //  1、參數校驗
        if (command == null || unit == null)
            throw new NullPointerException();
        // 2、任務轉換
        RunnableScheduledFuture<?> t = decorateTask(command,
            new ScheduledFutureTask<Void>(command, null,
                                          triggerTime(delay, unit)));
        // 3、核心!添加任務延遲隊列
        delayedExecute(t);
        return t;
    }

代碼2這裏只是把command類型轉換成了ScheduledFutureTask。
初始化時,period設置爲0,代表只執行一次,不會重複執行;

ScheduledFutureTask(Runnable r, V result, long ns) {
            super(r, result);
            this.time = ns;
            this.period = 0;
            this.sequenceNumber = sequencer.getAndIncrement();
        }

父類構造方法把runnable轉換成了callable;

public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // ensure visibility of callable
    }

代碼3執行delayedExecute方法,源碼如下:

private void delayedExecute(RunnableScheduledFuture<?> task) {
    // 1、如果線程池不是RUNNING狀態,則使用拒絕策略把提交任務拒絕掉
    if (isShutdown())
        reject(task);
    else {
        // 2、與ThreadPoolExecutor不同,這裏直接把任務加入延遲隊列
        super.getQueue().add(task);
        // 3、如果當前狀態無法執行任務,則取消
        if (isShutdown() &&
            !canRunInCurrentRunState(task.isPeriodic()) &&
            remove(task))
            task.cancel(false);
        else
        //4、和ThreadPoolExecutor不一樣,corePoolSize沒有達到會增加Worker;
        //增加Worker,確保提交的任務能夠被執行
            ensurePrestart();
    }

主要說下代碼3,再次檢查線程池狀態是否被關閉,關閉則從延遲隊列裏刪除剛纔的任務,如果隊列沒有,可能已經在執行了,則調用task.cancel進行取消,

如果代碼3的情況不滿足,則增加Woker,確保至少一個線程在處理任務,
ensurePrestart代碼如下

void ensurePrestart() {
		// 獲取活躍線程數
        int wc = workerCountOf(ctl.get());
        // 小於核心線程則增加工人線程
        if (wc < corePoolSize)
            addWorker(null, true);
         // 如果初始化爲0,則添加工人線程
        else if (wc == 0)
            addWorker(null, false);
    }

至此,schedule的方法主要做了幾件事情
1、初始化隊列、初始化線程池
2、任務先入隊,然後在添加工人Woker去啓動執行
3、線程池狀態關閉情況下,則取消任務


3.2、schedule的run方法-任務執行

public void run() {
    // 1、是否週期性,就是判斷period是否爲0。
    boolean periodic = isPeriodic();
    // 2、檢查任務是否可以被執行,不能被執行,則取消
    if (!canRunInCurrentRunState(periodic))
        cancel(false);
    // 3、如果非週期性任務直接調用run運行即可(調用schedule時)。
    else if (!periodic)
        ScheduledFutureTask.super.run();
    // 4、如果成功runAndRest,則設置下次運行時間並調用reExecutePeriodic。
    else if (ScheduledFutureTask.super.runAndReset()) {
	    // 設置time=time+period
        setNextRunTime();
        // 5、需要重新將任務(outerTask)放到工作隊列中。此方法源碼會在後文介紹ScheduledThreadPoolExecutor本身API時提及。
        reExecutePeriodic(outerTask);
    }

代碼1、判斷是一次性任務還是可重複任務,代碼如下

public boolean isPeriodic() {
            return period != 0;
        }

可以看到內部是通過period來判斷的,schedule方法初始化時設置的爲0,所以他是一次性的,這裏返回false。

代碼2、判斷當前任務是否應該被取消,canRunInCurrentRunState代碼如下。

boolean canRunInCurrentRunState(boolean periodic) {
        return isRunningOrShutdown(periodic ?
                                   continueExistingPeriodicTasksAfterShutdown :
                                   executeExistingDelayedTasksAfterShutdown);
    }
private volatile boolean executeExistingDelayedTasksAfterShutdown = true;

這裏傳遞的periodic的值爲false,所以isRunningOrShutdown的參數爲executeExistingDelayedTasksAfterShutdown。
executeExistingDelayedTasksAfterShutdown的參數默認爲true。isRunningOrShutdown代碼如下

final boolean isRunningOrShutdown(boolean shutdownOK) {
        int rs = runStateOf(ctl.get());
        return rs == RUNNING || (rs == SHUTDOWN && shutdownOK);
    }

表示有其他線程調用了SHUTDOWN命令關閉了線程池後,當前任務還是要執行,否則爲false,則當前任務要取消。

代碼3、由於periodic爲false,所以執行ScheduledFutureTask.super.run();這裏調用了父類的Future的run方法,代碼如下:

public void run() {
		// 1、如果狀態不是初始化,或者是初始化但是CAS設置當前任務持有者爲當前線程失敗,則直接返回
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return;
        try {
        	// 2、判斷了狀態是否被修改
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                	// 3、執行任務並且阻塞線程直到拿到返回值
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    // 4、執行異常則CAS更新state爲運行異常
                    setException(ex);
                }
                // 5、cas更新狀態爲運行結束
                if (ran)
                    set(result);
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

這裏說一下
代碼5,執行成功則執行set方法修改狀態,set代碼如下。

protected void set(V v) {
		// 當前狀態爲new,則cas更新爲執行中
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = v;
            // 狀態更新執行完成(這裏沒用CAS是因爲只可能有一個線程會進來),所以他比CAS性能更好
            UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
            finishCompletion();
        }
    }

代碼4、既然有執行成功,就有執行失敗的時候,通常當有4個線程多次提交到線程池,就會出現多個線程同時CAS從NEW更新到執行中,因爲任務共享同一個狀態值stat,執行失敗,則執行setException代碼如下

protected void setException(Throwable t) {
		// 當前狀態爲new,則cas更新爲執行中
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = t;
            // 設置當前任務狀態任務非正常結束
            UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
            finishCompletion();
        }
    }

至此、schedule的run方法主要做了幾件事情
1、檢查任務是不是週期性任務
2、檢查線程池狀態,有問題就取消
3、schedule明顯不是週期性任務,所以只執行一次,更新線程狀態結束


3.3、scheduleWithFixedDelay-延遲+定時

public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit) {
        // 1、參數校驗
        if (command == null || unit == null)
            throw new NullPointerException();
        // 2、延遲時間不可小於=0,否則拋異常
        if (delay <= 0)
            throw new IllegalArgumentException();
        // 3、任務類型轉換,這裏和Schedule不同的是period=-delay
        ScheduledFutureTask<Void> sft = new ScheduledFutureTask<Void>(command,
                                          null,
                                          triggerTime(initialDelay, unit),
                                          unit.toNanos(-delay));
        RunnableScheduledFuture<Void> t = decorateTask(command, sft);
        sft.outerTask = t;
        // 4、添加任務到隊列
        delayedExecute(t);
        return t;
    }

參數說明:

command:提交的任務
initialDelay:表示延遲多少時間後開始執行command
delay:表示執行任務後,需要延遲多少時間
unit:是initialDelay和delay的時間單位。

任務會一直重複執行,直到任務運行中拋出了異常,被取消了,或者關閉了線程池。

這個方法和schedule方法不同的地方就在於Schedule傳的值是0,這裏值爲-period,period<0說明是可重複執行的任務。
delayedExecute方法和Schedule調用的是同一個,這裏不再複述。

線程池會從隊列裏獲取任務,然後調用ScheduledFutureTask的run方法,由於這裏period<0,所以isPeriodic返回true,所以執行runAndReset代碼如下

public void run() {
            boolean periodic = isPeriodic();
            if (!canRunInCurrentRunState(periodic))
                cancel(false);
            else if (!periodic)
                ScheduledFutureTask.super.run();
            // 進入這個if調用runAndReset
            else if (ScheduledFutureTask.super.runAndReset()) {
                setNextRunTime();
                reExecutePeriodic(outerTask);
            }
        }

3.4、runAndReset方法-重複執行任務

protected boolean runAndReset() {
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return false;
        boolean ran = false;
        int s = state;
        try {
            Callable<V> c = callable;
            if (c != null && s == NEW) {
                try {
                    c.call(); // don't set result
                    ran = true;
                } catch (Throwable ex) {
                    setException(ex);
                }
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
        // 主要這裏和3.2的方法不一樣,他是void,但是這裏不會更新狀態,只會判斷狀態返回結果
        return ran && s == NEW;
    }

該方法和FutureTask的run方法類似,只是任務正常執行完畢後,不會修改任務的狀態,這樣做是爲了可以重複執行。
這裏返回了狀態判斷結果,正常執行完畢會返回true,否則返回false。
如果返回true,則執行setNextRunTime方法設置該任務下一次執行時間。


3.6、setNextRunTime-設置該任務下一次執行時間

private void setNextRunTime() {
            long p = period;
            // fixed-rate任務
            if (p > 0)
                time += p;
            else // fixed-delay任務
                time = triggerTime(-p);
        }

這裏p<0,所以是fixed-delay任務。然後設置time爲當前時間加上-p的時間,也就是延遲-p 時間後再次執行


3.7、triggerTime(long delay)-獲取觸發時間

如果delay < Long.Max_VALUE/2,則下次執行時間爲當前時間+delay。
否則爲了避免隊列中出現由於溢出導致的排序紊亂,需要調用overflowFree來修正一下delay(如果有必要的話)。

long triggerTime(long delay) {
    /*
     * 如果delay < Long.Max_VALUE/2,則下次執行時間爲當前時間+delay。
     * 否則爲了避免隊列中出現由於溢出導致的排序紊亂,需要調用overflowFree來修正一下delay(如果有必要的話)。
     */
    return now() + ((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
}

3.8、overflowFree(long delay)-隊列溢出處理

/**
 * 主要就是有這麼一種情況:
 * 某個任務的delay爲負數,說明當前可以執行(其實早該執行了)。
 * 工作隊列中維護任務順序是基於compareTo的,在compareTo中比較兩個任務的順序會用time相減,負數則說明優先級高。
 *
 * 那麼就有可能出現一個delay爲正數,減去另一個爲負數的delay,結果上溢爲負數,則會導致compareTo產生錯誤的結果。
 *
 * 爲了特殊處理這種情況,首先判斷一下隊首的delay是不是負數,如果是正數不用管了,怎麼減都不會溢出。
 * 否則可以拿當前delay減去隊首的delay來比較看,如果不出現上溢,則整個隊列都ok,排序不會亂。
 * 不然就把當前delay值給調整爲Long.MAX_VALUE + 隊首delay。
 */
private long overflowFree(long delay) {
        Delayed head = (Delayed) super.getQueue().peek();
        if (head != null) {
            long headDelay = head.getDelay(NANOSECONDS);
            if (headDelay < 0 && (delay - headDelay < 0))
                delay = Long.MAX_VALUE + headDelay;
        }
        return delay;
    }

在設置好下一次執行時間後,調用reExecutePeriodic方法重複執行,代碼如下


3.9、reExecutePeriodic-重新執行

void reExecutePeriodic(RunnableScheduledFuture<?> task) {
		// 1、校驗線程池運行狀態,正常則添加任務到隊列
        if (canRunInCurrentRunState(true)) {
            super.getQueue().add(task);
            // 2、如果狀態已關閉或停止,則從隊列裏移除任務,如果移除成功,則取消任務
            if (!canRunInCurrentRunState(true) && remove(task))
                task.cancel(false);
            else
            	// 3、添加工人線程
                ensurePrestart();
        }
    }

每次重新執行,如果線程池狀態正常,則往隊列裏添加任務,然後添加工人線程去處理。

至此、scheduleWithFixedDelay總結下
1、當添加一個任務到延遲隊列後,等待initialDelay時間,任務就會過期,過期的任務就會被從隊列彙總移除,並執行。
2、執行完畢後,會重新設置任務的延遲時間,然後在把任務放入延遲隊列,循環往復。
3、如果一個任務在執行中異常了,那麼這個任務就結束了,但是不影響其他線程任務的執行。

執行規則是,延遲時間+任務執行時間,爲下一次循環執行的時間。


4.0、scheduleAtFixedRate-任務延遲取最大值-穩定定時器

以固定頻率調用指定任務的線程池。
scheduleAtFixedRate和scheduleAtFixedDelay類似,這裏主要討論不同點,相同點可以回頭在看下上面。

public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit) {
        if (command == null || unit == null)
            throw new NullPointerException();
        if (period <= 0)
            throw new IllegalArgumentException();
        // 任務轉換,這裏和fixed-delay不同,這裏傳的是正數period
        ScheduledFutureTask<Void> sft =
            new ScheduledFutureTask<Void>(command,
                                          null,
                                          triggerTime(initialDelay, unit),
                                          unit.toNanos(period));
        RunnableScheduledFuture<Void> t = decorateTask(command, sft);
        sft.outerTask = t;
        delayedExecute(t);
        return t;
    }

由於period是正數,所以在執行setNextRunTime的時候,他是直接time+= p。而不是time = triggerTime(-p);
runAndReset執行完後,執行setNextRunTime這裏走的就是p>0邏輯;

private void setNextRunTime() {
            long p = period;
            // fixed-rate任務
            if (p > 0)
                time += p;
            else // fixed-delay任務
                time = triggerTime(-p);
        }

scheduleAtFixedRate總結:
相對於fixed-delay任務來說,fixed-rate是以最大的延遲時間來重複執行。
比如任務處理要3秒,延遲5秒,那就是每5秒一次執行;
比如任務執行要5秒,延遲3秒,到達延遲時間後,則不會併發執行新的任務,而是等到第5秒後,下次要執行的任務纔會延遲執行。


總結

常用的延遲任務是rate和delay2種,任務類型使用period區分。
提交任務,先提交到延遲隊列,然後時間到了,從隊列移除,核心線程獲取任務並且執行。

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章