主頁 > 移動端開發 > 深入淺出Java執行緒池:原始碼篇

深入淺出Java執行緒池:原始碼篇

2021-02-06 08:02:04 移動端開發

前言

在上一篇文章深入淺出Java執行緒池:理論篇中,已經介紹了什么是執行緒池以及基本的使用,(本來寫作的思路是使用篇,但經網友建議后,感覺改為理論篇會更加合適),本文則深入執行緒池的原始碼,主要是介紹ThreadPoolExecutor內部的原始碼是如何實作的,對ThreadPoolExecutor有一個更加清晰的認識,

ThreadPoolExecutor的原始碼相對而言比較好理解,沒有特別難以讀懂的地方,相信沒有閱讀原始碼習慣的讀者,跟著本文,也可以很輕松地讀懂ThreadPoolExecutor的核心原始碼邏輯,

本文原始碼jdk版本為8,該類版本為jdk1.5,也就是在1.5之后,ThreadPoolExecutor的原始碼沒有做修改,

執行緒池家族

Java中的執行緒池繼承結構如下圖:(類圖中只寫了部分方法且省略引數)

  • 頂層介面Executor表示一個執行器,他只有一個介面:execute() ,表示可以執行任務
  • ExecutorService在Executor的基礎上拓展了更多的執行方法,如submit() shutdown() 等等,表示一個任務執行服務,
  • AbstarctExecutorService是一個抽象類,他實作了ExecutorService的部分核心方法,如submit等
  • ThreadPoolExecutor是最核心的類,也就是執行緒池,他繼承了抽象類AbstarctExecutorService
  • 此外還有ScheduledExecutorService介面,他表示一個可以按照指定時間或周期執行的執行器服務,內部定義了如schedule() 等方法來執行任務
  • ScheduledThreadPoolExecutor實作了ScheduledExecutorService介面,同時繼承于ThreadPoolExecutor,內部的執行緒池相關邏輯使用自ThreadPoolExecutor,在此基礎上拓展了延遲、周期執行等功能特性

ScheduledThreadPoolExecutor相對來說用的是比較少,延時任務在我們Android中有更加熟悉的方案:Handler;而周期任務則用的非常少,現在android的后臺限制非常嚴格,基本上一退出應用,應用行程很容易被系統干掉,當然ScheduledThreadPoolExecutor也不是完全沒有用處,例如桌面小部件需要設定定時重繪,那么他就可以派上用場了,

因此,我們本文的原始碼,主要針對ThreadPoolExecutor,在閱讀原始碼之前,我們先來看一下ThreadPoolExecutor內部的結構以及關鍵角色,

內部結構

閱讀原始碼前,我們先把ThreadPoolExecutor整個原始碼結構講解一下,形成一個整體概念,再閱讀原始碼就不會迷失在原始碼中了,先來看一下ThreadPoolExecutor的內部結構:

yGOUkq.md.png

  • ThreadPoolExecutor內部有三個關鍵的角色:阻塞佇列、執行緒、以及RejectExecutionHandler(這里寫個中文名純粹因為不知道怎么翻譯這個名字),他們的作用在理論篇有詳細介紹,這里不再贅述,
  • 在ThreadPoolExecutor中,一個執行緒對應一個worker物件,工人,非常形象,每個worker內部有一個獨立的執行緒,他會不斷去阻塞佇列獲取任務來執行,也就是呼叫阻塞佇列的 poll 或者 take 方法,他們區別后面會講,如果佇列沒有任務了,那么就會阻塞在這里,
  • workQueue,就是阻塞佇列,當核心執行緒已滿之后,任務就會被放置在這里等待被工人worker領取執行
  • RejectExecutionHandler本身是一個介面,ThreadPoolExecutor內部有這樣的一個介面物件,當任務無法被執行會呼叫這個物件的方法,ThreadPoolExecutor提供了該介面的4種實作方案,我們可以直接拿來用,或者自己繼承介面,實作自定義邏輯,在構造執行緒池的時候可以傳入RejectExecutionHandler物件,
  • 整個ThreadPoolExecutor中最核心的方法就是execute,他會根據具體的情況來選擇不同的執行方案或者拒絕執行,

這樣,我們就清楚ThreadPoolExecutor的內部結構了,然后,我們開始 Read the fucking code 吧,

原始碼分析

內部關鍵屬性

ThreadPoolExecutor內部有很多的變數,他們包含的資訊非常重要,先來了解一下,

ThreadPoolExecutor的狀態和執行緒數整合在同一個int變數中,類似于view測量中MeasureSpec,他的高三位表示執行緒池的狀態,低29位表示執行緒池中執行緒的數量,如下:

// AtomicInteger物件可以利用CAS實作執行緒安全的修改,其中包含了執行緒池狀態和執行緒數量資訊
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// COUNT_BITS=29,(對于int長度為32來說)表示執行緒數量的位元組位數
private static final int COUNT_BITS = Integer.SIZE - 3;
// 狀態掩碼,高三位是1,低29位全是0,可以通過 ctl&COUNT_MASK 運算來獲取執行緒池狀態
private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;

執行緒池的狀態一共有5個:

  • 運行running:執行緒池創建之后即是運行狀態
  • 關閉shutdown:呼叫shutdown方法之后執行緒池處于shutdown狀態,該狀態會停止接收任何任務,阻塞佇列中的任務執行完成之后會自動終止執行緒池
  • 停止stop:呼叫shutdownNow方法之后執行緒池處于stop狀態,和shutdown的區別是這個狀態下的執行緒池不會去執行佇列中剩下的任務
  • 整理tidying:在執行緒池stop之后,進入tidying狀態,然后執行 terminated() 方法,再進入terminated狀態
  • 終止terminated:執行緒池中沒有任何執行緒在執行任務,執行緒池完全終止,

在原始碼中這幾個狀態分別對應:

// runState is stored in the high-order bits
private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

上面的位操作不夠直觀,轉化后如下:

private static final int RUNNING    = 111 00000 00000000 00000000 00000000;
private static final int SHUTDOWN   = 000 00000 00000000 00000000 00000000; 
private static final int STOP       = 001 00000 00000000 00000000 00000000;
private static final int TIDYING    = 010 00000 00000000 00000000 00000000;
private static final int TERMINATED = 011 00000 00000000 00000000 00000000;

可以看到除了running是負數,其他的狀態都是正數,且狀態越靠后,數值越大,因此我們可以通過判斷 ctl&COUNT_MASK > SHUTDOWN 來判斷狀態是否處于 stop、tidying、terminated之一,后續原始碼中會有很多的這樣的判斷,舉其中的一個方法:

// 這里來判斷執行緒池的狀態
if(runStateAtLeast(ctl,SHUTDOWN)) {
    ...
}
// 這里執行邏輯,直接判斷兩個數的大小
private static boolean runStateAtLeast(int c, int s) {
    return c >= s;
}

ps:這里怎么沒有使用掩碼COUNT_MASK ?因為狀態是處于高位,低位的數值不影響高位的大小判斷,當然如果要判斷相等,就還是需要使用掩碼COUNT_MASK的,

接下來是ThreadPoolExecutor內部的三個關鍵角色物件:

// 阻塞佇列
private final BlockingQueue<Runnable> workQueue;
// 存盤worker的hashSet,worker被創建之后會被存盤到這里
private final HashSet<Worker> workers = new HashSet<>();
// RejectedExecutionHandler默認的實作是AbortPolicy
private volatile RejectedExecutionHandler handler;
private static final RejectedExecutionHandler defaultHandler =
        new AbortPolicy();

內部使用的鎖物件:

// 這里是兩個鎖,ThreadPoolExecutor內部并沒有使用Synchronize關鍵字來保持同步
// 而是使用Lock;和Synchronize的區別就是他是應用層的鎖,而synchronize是jvm層的鎖
private final ReentrantLock mainLock = new ReentrantLock();
private final Condition termination = mainLock.newCondition();

最后是內部一些引數的配置,前面都介紹過,把原始碼貼出來再回顧一下:

// 執行緒池歷史達到的最大執行緒數
private int largestPoolSize;
// 執行緒池完成的任務數,
// 該數并不是實時更新的,在獲取執行緒池完成的任務數時,需要去統計每個worker完成的任務并累加起來
// 當一個worker被銷毀之后,他的任務數就會被累加到這個資料中
private long completedTaskCount;
// 執行緒工廠,用于創建執行緒
private volatile ThreadFactory threadFactory;
// 空閑執行緒存盤的時間
private volatile long keepAliveTime;
// 是否允許核心執行緒被回收
private volatile boolean allowCoreThreadTimeOut;
// 核心執行緒數限額
private volatile int corePoolSize;
// 執行緒總數限額
private volatile int maximumPoolSize;

不是吧sir?原始碼還沒看到魂呢,整出來這么無聊的變數?
咳咳,別急嘛,原始碼決議馬上來,這些變數會貫穿整個原始碼程序始終,先對他們有個印象,后續閱讀原始碼就會輕松暢通很多,

關鍵方法:execute()

這個方法的主要任務就是根據執行緒池的當前狀態,選擇任務的執行策略,該方法的核心邏輯思路是:

  1. 在執行緒數沒有達到核心執行緒數時,會創建一個核心執行緒來執行任務

    public void execute(Runnable command) {
        // 不能傳入空任務
        if (command == null)
            throw new NullPointerException();
    
        // 獲取ctl變數,就是上面我們講的將狀態和執行緒數合在一起的一個變數
        int c = ctl.get();
        // 判斷核心執行緒數是否超過限額,否則創建一個核心執行緒來執行任務
        if (workerCountOf(c) < corePoolSize) {
            // addWorker方法是創建一個worker,也就是創建一個執行緒,引數true表示這是一個核心執行緒
            // 如果添加成功則直接回傳
            // 否則意味著中間有其他的worker被添加了,導致超出核心執行緒數;或者執行緒池被關閉了等其他情況
            // 需要進入下一步繼續判斷
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        ...
    }
    
  2. 當執行緒數達到核心執行緒數時,新任務會被放入到等待佇列中等待被執行

  3. 當等待佇列已經滿了之后,如果執行緒數沒有到達總的執行緒數上限,那么會創建一個非核心執行緒來執行任務

  4. 當執行緒數已經到達總的執行緒數限制時,新的任務會被拒絕策略者處理,執行緒池無法執行該任務,

    public void execute(Runnable command) {
        ...
        // 如果執行緒池還在運行,則嘗試添加任務到佇列中
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            // 再次檢查如果執行緒池被關閉了,那么把任務移出佇列
            // 如果移除成功則拒絕本次任務
            // 這里主要是判斷在插入佇列的程序中,執行緒池有沒有被關閉了
            if (! isRunning(recheck) && remove(command))
                reject(command);
            // 否則再次檢查執行緒數是否為0,如果是,則創建一個沒有任務的非主執行緒worker
            // 這里對應核心執行緒為0的情況,指定任務為null,worker會去佇列拿任務來執行
            // 這里表示執行緒池至少有一個執行緒來執行佇列中的任務
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        // 如果上面添加到佇列中失敗,則嘗試創建一個非核心執行緒來執行任務
        // 如果創建失敗,則拒絕任務
        else if (!addWorker(command, false))
            reject(command);
    }
    

原始碼中還設計到兩個關鍵方法:addWorker創建一個新的worker,也就是創建一個執行緒;reject拒絕一個任務,后者比較簡單我們先看一下,

拒絕任務:reject()

// 拒絕任務,呼叫rejectedExecutionHandler來處理
final void reject(Runnable command) {
    handler.rejectedExecution(command, this);
}

默認的實作類有4個,我們依次來看一下:

  • AbortPolicy是默認實作,會拋出一個RejectedExecutionException例外:

    public static class AbortPolicy implements RejectedExecutionHandler {
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }
    }
    
  • DiscardPolicy最簡單,就是:什么都不做,直接拋棄任務,(這是非常渣男不負責任的行為,咱們不能學他,所以也不要用它 [此處狗頭] )

    public static class DiscardPolicy implements RejectedExecutionHandler {
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        }
    }
    
  • DiscardOldestPolicy會洗掉佇列頭的一個任務,然后再次執行自己(擠掉原位,自己上位,綠茶行為?)

    public static class DiscardOldestPolicy implements RejectedExecutionHandler {
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                e.getQueue().poll();
                e.execute(r);
            }
        }
    }
    
  • CallerRunsPolicy最猛,他干脆在自己的執行緒執行run方法,不依靠執行緒池了,自己動手豐衣足食,

    public static class CallerRunsPolicy implements RejectedExecutionHandler {
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                r.run();
            }
        }
    }
    

上面4個ThreadPoolExecutor已經幫我們實作了,他的靜態內部類,在創建ThreadPoolExecutor的時候我們可以直接拿來用,也可以自己繼承介面實作自己的邏輯,具體選擇哪個需要根據實際的業務需求來決定,

那么接下來看創建worker的方法,

創建worker:addWorker()

方法的目的很簡單:創建一個worker,前面我們講到,worker內部創建了一個執行緒,每一個worker則代表了一個執行緒,非常類似android中的looper,looper的loop()方法會不斷地去MessageQueue獲取message,而Worker的run()方法會不斷地去阻塞佇列獲取任務,這個我們后面講,

addWorker() 方法的邏輯整體上分為兩個部分:

  1. 檢查執行緒狀態執行緒數是否滿足條件:

    // 第一個引數是創建的執行緒首次要執行的任務,可以是null,則表示初始化一個執行緒
    // 第二引數表示是否是一個核心執行緒
    private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (int c = ctl.get();;) {
            // 還記不記得我們前面講到執行緒池的狀態控制?
            // runStateAtLeast(c, SHUTDOWN)表示狀態至少為shutdown,后面類同
            // 如果執行緒池處于stop及以上,不會再創建worker
            // 如果執行緒池狀態在shutdown時,如果佇列不為慷訓者任務!=null,則還會創建worker
            if (runStateAtLeast(c, SHUTDOWN)
                && (runStateAtLeast(c, STOP)
                    || firstTask != null
                    || workQueue.isEmpty()))
                // 其他情況回傳false,表示拒絕創建worker
                return false;
            
    		// 這里采用CAS輪詢,也就是回圈鎖的策略來讓執行緒總數+1
            for (;;) {
                // 檢查是否超出執行緒數限制
                // 這里根據core引數判斷是核心執行緒還是非核心執行緒
                if (workerCountOf(c)
                    >= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
                    return false;
                // 利用CAS讓ctl變數自增,表示worker+1
                // 如果CAS失敗,則表示發生了競爭,則再來一次
                if (compareAndIncrementWorkerCount(c))
                    // 成功則跳出最外層回圈
                    break retry;
                // 如果這個期間ctl被改變了,則獲取ctl,再嘗試一次
                c = ctl.get();  
                // 如果執行緒池被shutdown了,那么重復最外層的回圈,重新判斷狀態是否可以創建worker
                if (runStateAtLeast(c, SHUTDOWN))
                    // 繼續最外層回圈
                    continue retry;
            }
        }
        
        // 創建worker邏輯
        ...
    }
    

    不知道讀者對于原始碼中的retry: 有沒有疑惑,畢竟平時很少用到,他的作用是標記一個回圈,這樣我們在內層的回圈就可以跳轉到任意一個外層的回圈,這里的retry只是一個名字,改成 repeat: 甚至 a: 都是可以的,他的本質就是:一個回圈的標記

  2. 創建worker物件,并呼叫其內部執行緒的start()方法來啟動執行緒:

    private boolean addWorker(Runnable firstTask, boolean core) {
    	...
        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            // 創建一個新的worker
            // 創建的程序中內部會創建一個執行緒
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                // 獲得全域鎖并加鎖
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    // 獲取鎖之后,需要再次檢查狀態
                    int c = ctl.get();
    				// 只有運行狀態或者shutDown&&task==null才會被執行
                    if (isRunning(c) ||
                        (runStateLessThan(c, STOP) && firstTask == null)) {
                        // 如果這個執行緒不是剛創建的,則拋出例外
                        if (t.getState() != Thread.State.NEW)
                            throw new IllegalThreadStateException(); 
                        // 添加到workerSet中
                        workers.add(w);
                        workerAdded = true;
                        int s = workers.size();
                        // 跟蹤執行緒池到達的最多執行緒數量
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                    }
                } finally {
                    // 釋放鎖
                    mainLock.unlock();
                }
                // 如果添加成功,啟動執行緒
                if (workerAdded) {
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            // 如果執行緒沒有啟動,表示添加worker失敗,可能在添加的程序中執行緒池被關閉了
            if (! workerStarted)
                // 把worker從workerSet中移除
                addWorkerFailed(w);
        }
        return workerStarted;
    }
    

經過前面兩步,如果沒有出現例外,則創建worker成功,最后還涉及到一個方法: addWorkerFailed(w) ,他的內容比較簡答,順便提一下吧:

// 添加worker失敗
private void addWorkerFailed(Worker w) {
    final ReentrantLock mainLock = this.mainLock;
    // 加鎖
    mainLock.lock();
    try {
        if (w != null)
            workers.remove(w);
        // 這里會讓執行緒總數-1
        decrementWorkerCount();
        // 嘗試設定執行緒池的狀態為terminad
        // 因為添加失敗有可能是執行緒池在添加worker的程序中被shutdown
        // 那么這個時候如果沒有任務正在執行就需要設定狀態為terminad
        // 這個方法后面會詳細講
        tryTerminate();
    } finally {
        mainLock.unlock();
    }
}

那么到這里,execute()方法中的一些呼叫方法就分析完了,阻塞佇列相關的方法不屬于本文的范疇,就不展開了,那么還有一個問題:worker是如何作業的呢?worker內部有一個執行緒,當執行緒啟動時,初始化執行緒的runnable物件的run方法會被呼叫,那么這個runnable物件是什么?我直接來看worker,

打工人:Worker

首先我們看到他的構造方法:

Worker(Runnable firstTask) {
    setState(-1); // inhibit interrupts until runWorker
    this.firstTask = firstTask;
    this.thread = getThreadFactory().newThread(this);
}

原始碼很簡單,把傳進來的任務設定給內部變數firstTask,然后把自己傳給執行緒工廠去創建一個執行緒,所以執行緒啟動時,Worker本身的run方法會被呼叫,那么我們看到Worker的 run()方法,

public void run() {
    runWorker(this);
}

Worker是ThreadPoolExecutor的內部類,這里直接呼叫到了ThreadPoolExecutor的方法: runWorker()來開始執行,那么接下來,我們就看到這個方法,

啟動worker:runWorker()

這個方法是worker執行的方法,在執行緒被銷毀前他會一直執行,類似于Handler的looper,不斷去佇列獲取訊息來執行:

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    // 獲取worker初始化時設定的任務,可以為null,如果為null則表示僅僅創建執行緒
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); 
    // 這個引數的作用后面解釋,需要結合其他的原始碼
    boolean completedAbruptly = true;
    try {
        // 如果自身的task不為null,那么會執行自身的task
        // 否則呼叫getTask去佇列獲取一個task來執行
        // 這個getTask最侄訓去呼叫佇列的方法來獲取任務
        // 而佇列如果為空他的獲取方法會進行阻塞,這里也就阻塞了,后面深入講
        while (task != null || (task = getTask()) != null) {
            try{
            // 執行任務
            ...
            } finally {
                // 任務執行完成,把task設定為null
                task = null;
                // 任務總數+1
                w.completedTasks++;
                // 釋放鎖
                w.unlock();
            }
        }
    	// 這里設定為false,先記住他
        completedAbruptly = false;
    } finally {
    	// 如果worker退出,那么需要執行后續的善后作業
        processWorkerExit(w, completedAbruptly);
    }
}

可以看到這個方法的整體框架還是比較簡單的,核心就在于 while (task != null || (task = getTask()) != null) 這個回圈中,如果 getTask() 回傳null,則表示執行緒該結束了,這和Handler機制也是一樣的,

上面的原始碼省略了具體執行任務的邏輯,他的邏輯也是很簡單:判斷狀態+運行任務,我們來看一下:

final void runWorker(Worker w) {
    ...;
    try {
        while (task != null || (task = getTask()) != null) {
            w.lock();
            // 如果執行緒池已經設定為stop狀態,那么保證執行緒是interrupted標志
            // 如果執行緒池沒有在stop狀態,那么保證執行緒不是interrupted標志
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                // 回呼方法,這個方法是一個空實作
                beforeExecute(wt, task);
                try {
                    // 運行任務
                    task.run();
                    // 回呼方法,也是一個空實作
                    afterExecute(task, null);
                } catch (Throwable ex) {
                    afterExecute(task, ex);
                    throw ex;
                }
            }
            ...
        }
    	completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}

在獲取到一個任務后,就會去執行該任務的run方法,然后再回去繼續獲取新的任務,

我們會發現其中有很多的空實作方法,他是給子類去實作的,有點類似于Activity的生命周期,子類需要重寫這些方法,在具體的情況做一些作業,當然,一般的使用是不需要去重寫這些方法,接下來需要來看看 getTask() 是如何獲取任務的,

獲取任務:getTask()

這個方法的內容可以分為兩個部分:判斷當前執行緒池的狀態+阻塞地從佇列中獲取一個任務,

第一部分是判斷當前執行緒池的狀況,如果處于關閉狀態那么直接回傳null來讓worker結束,否則需要判斷當前執行緒是否超時或者超出最大限制的執行緒數:

private Runnable getTask() {
    boolean timedOut = false; 
    // 內部使用了CAS,這里需要有一個回圈來不斷嘗試
    for (;;) {
        int c = ctl.get();
        // 如果處于shutdown狀態而且佇列為空,或者處于stop狀態,回傳null
        // 這和前面我們討論到不同的執行緒池的狀態的不同行為一致
        if (runStateAtLeast(c, SHUTDOWN)
            && (runStateAtLeast(c, STOP) || workQueue.isEmpty())) {
            // 這里表示讓執行緒總數-1,記住他,后面會繼續聊到
            decrementWorkerCount();
            return null;
        }
        
        // 獲取目前的執行緒總數
        int wc = workerCountOf(c);
        // 判斷該執行緒在空閑情況是否可以被銷毀:允許核心執行緒為null或者當前執行緒超出核心執行緒數
        // 可以看到這里并沒有去區分具體的執行緒是核心還是非核心,只有執行緒數量處于核心范圍還是非核心范圍
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
        // 超出最大執行緒數或者已經超時;
        // 這里可能是用戶通過 setMaximumPoolSize 改動了資料才會導致這里超出最大執行緒數
        // 同時還必須保證當前執行緒數量大于1或者佇列已經沒有任務了
        // 這樣就確保了當有任務存在時,一定至少有一個執行緒在執行任務
        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            // 使用CAS嘗試讓當前執行緒總數-1,失敗則從來一次上面的邏輯
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }
        
        // 獲取任務邏輯
        ...
    }
}

第二部分是獲取一個任務并執行,獲取任務使用的是阻塞佇列的方法,如果佇列中沒有任務,則會被阻塞:

private Runnable getTask() {
    boolean timedOut = false; 
    // 內部使用了CAS,這里需要有一個回圈來不斷嘗試
    for (;;) {
        // 判斷執行緒池狀態邏輯
        ...
        try {
            // 獲取一個任務
            // poll方法等待具體時間之后如果沒有獲取到物件,會回傳null
            // take方法會一直等到獲取新物件,除非被interrupt
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            // r==null,說明超時了,重新回圈
            timedOut = true;
        } catch (InterruptedException retry) {
            // 被interrupt,說明可能執行緒池被關閉了,重新判斷情況
            timedOut = false;
        }
    }
}

這里需要重點關注的是阻塞佇列的 poll()take() 方法,他們都會去佇列中獲取一個任務;但是,poll() 方法會阻塞指定時間后回傳,而 take() 則是無限期阻塞,這里對應的就是有存活時間的執行緒和不會被銷毀的核心執行緒,

同時注意 timedOut = true 是在這一部分被賦值的,當賦值為true之后需要再執行一次回圈,在上面的判斷中就會被攔截下來并回傳false,這在第一部分邏輯介紹了,而如果執行緒在等待的時候被 interrupt 了,說明執行緒池被關閉了,此時也會重走一次上面判斷狀態的邏輯,

到這里關于執行的邏輯就講得差不多了,下面聊一聊執行緒池關閉以及worker結束的相關邏輯,

worker退出作業:processWorkerExit

前面已經介紹 runWorker() 了方法,這個方法的主要任務就是讓worker動起來,不斷去佇列獲取任務,而當獲取任務的時候回傳了null,則表示該worker可以結束了,最后會呼叫 processWorkerExit() 方法,如下:

final void runWorker(Worker w) {
    ...
    try {
       ...
    } finally {
    	// 如果worker退出,那么需要執行后續的善后作業
        processWorkerExit(w, completedAbruptly);
    }
}

processWorkerExit() 會完成worker退出的善后作業,具體的內容是:

  1. 把完成的任務數合并到總的任務數,移除worker,嘗試設定執行緒池的狀態為terminated:
private void processWorkerExit(Worker w, boolean completedAbruptly) {
    // 如果不是經過getTask方法回傳null正常退出的,那么需要讓執行緒總數-1
    // 這個引數前面一直讓你們注意一下不知道你們還記不記得
    // 如果是在正常情況下退出,那么在getTask() 方法中就會執行decrementWorkerCount()了
    // 而如果出現一些特殊的情況突然結束了,并不是通過在getTask回傳null結束
    // Abruptly就是突然的意思,那么completedAbruptly就為true,正常情況下在runWorker方法中會被設定為false
    // 那什么叫突然結束?用戶的任務拋出了例外,這個時候執行緒就突然結束了,沒有經過getTask方法
    // 這里就需要讓執行緒總數-1
    if (completedAbruptly) 
        decrementWorkerCount();

    // 獲取鎖,并累加完成的任務總數,從set中移除worker
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        completedTaskCount += w.completedTasks;
        workers.remove(w);
    } finally {
        mainLock.unlock();
    }

    // 嘗試設定執行緒池的狀態為terminated
    // 這個方法前面我們addWorker失敗的時候提到過,后面再展開
    tryTerminate();
	...
}
  1. 移除worker之后,如果執行緒池還沒有被stop,那么最后必須保證佇列任務至少有一個執行緒在執行佇列中的任務:
private void processWorkerExit(Worker w, boolean completedAbruptly) {
    ...
	int c = ctl.get();
    // stop及以上的狀態不需要執行剩下的任務
    if (runStateLessThan(c, STOP)) {
        // 如果執行緒是突然終止的,那肯定需要重新創建一個
        // 否則進行判斷是否要保留一個執行緒
        if (!completedAbruptly) {
            int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
            if (min == 0 && ! workQueue.isEmpty())
                min = 1;
            if (workerCountOf(c) >= min)
                return; 
        }
        // 如果此時執行緒數<=核心執行緒數,或者當核心執行緒可被銷毀時,執行緒數==0且佇列不為空
        // 那么需要創建一個執行緒來執行任務
        addWorker(null, false);
    }
}

代碼雖然看起來很多,但是具體的邏輯內容還是比較簡單的,前面一直提到一個方法 tryTerminate() 但一直沒有展開解釋,下面來介紹一下,

嘗試終止執行緒池:tryTerminate()

這個方法出現在任何可能讓執行緒池進入終止狀態的地方,如添加worker失敗時,那么這個時候可能執行緒池已經處于stop狀態,且已經沒有任何正在執行的worker了,那么此時可以進入terminated狀態;再如worker被銷毀的時候,可能這是最后一個被銷毀的worker,那么此時執行緒池需要進入terminated狀態,

根據這個方法的使用情況其實就已經差不多可以推斷出這個方法的內容:判斷當前執行緒池的狀態,如果符合條件則設定執行緒池的狀態為terminated ,如果此時不能轉換為terminated狀態,則什么也不做,直接回傳,

  1. 首先判斷當前執行緒池狀態是否符合轉化為terminated,如果處于運行狀態或者tidying以上狀態,則肯定不需要進行狀態轉換,因為running需要先進入stop狀態,而tidying其實已經是準備進入terminated狀態了,如果處于shutdown狀態且佇列不為空,那么需要執行完佇列中的任務,所以也不適合狀態轉換:
final void tryTerminate() {
    for (;;) {
        int c = ctl.get();
        // 如果處于運行狀態或者tidying以上狀態時,直接回傳,不需要修改狀態
        // 如果處于stop以下狀態且佇列不為空,那么需要等佇列中的任務執行完成,直接回傳
        if (isRunning(c) ||
            runStateAtLeast(c, TIDYING) ||
            (runStateLessThan(c, STOP) && ! workQueue.isEmpty()))
            return;
        // 到這里說明執行緒池肯定處于stop狀態
        // 執行緒的數量不等于0,嘗試中斷一個空閑的worker執行緒
        // 這里他只中斷workerSet中的其中一個,當其中的一個執行緒停止時,會再次呼叫tryTerminate
        // 然后又會再去中斷workerSet中的一個worker,不斷回圈下去直到剩下最后一個,workercount==0
        // 這就是 鏈式反應 ,
        if (workerCountOf(c) != 0) { 
            interruptIdleWorkers(ONLY_ONE);
            return;
        }
		
        // 設定狀態為terminated邏輯
        ...
    }
}
  1. 經過上面的判斷,能到第二部分邏輯,執行緒池肯定是具備進入terminated狀態的條件了,剩下的代碼就是把執行緒池的狀態設定為terminated:
final void tryTerminate() {
    for (;;) {
        // 上一部分邏輯
        ...
        // 首先獲取全域鎖
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            // 嘗試把執行緒池的狀態從stop修改為tidying
            // 如果修改失敗,說明狀態已經被修改了,那么外層回圈再跑一個
            if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
                try {
                    // 這個方法是一個空實作,需要子類繼承重寫
                    terminated();
                } finally {
                    // 最后再設定狀態為terminated
                    ctl.set(ctlOf(TERMINATED, 0));
                    // 喚醒所有等待終止鎖的執行緒
                    termination.signalAll();
                }
                return;
            }
        } finally {
            // 釋放鎖
            mainLock.unlock();
        }
        // CAS修改執行緒池的狀態失敗,重新進行判斷
    }
}

當執行緒池被標記為terminated狀態時,那么這個執行緒池就徹底地終止了,

好了到這里,恭喜你,關于ThreadPoolExecutor的原始碼決議理解得差不多了,接下來剩下幾個常用的api方法:submit()shutdown()/shutdownNow() 順便看一下吧,他們的邏輯也是都非常簡單,

關閉執行緒池:shutdown/shutdownNow

關閉執行緒池有兩個方法:

  • shutdown:設定執行緒池的狀態為shutdown,同時嘗試中斷所有空閑執行緒,但是會等待佇列中的任務執行結束再終止執行緒池,
  • shutdownNow:設定執行緒池的狀態為stop,同時嘗試中斷所有空閑執行緒,不會等待佇列中的任務完成,正在執行中的執行緒執行結束,執行緒池馬上進入terminated狀態,

我們各自來看一下:

// 關閉后佇列中的任務依舊會被執行,但是不會再添加新的任務
public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        // 設定狀態為shutdown
        advanceRunState(SHUTDOWN);
        // 嘗試中斷所有空閑的worker
        interruptIdleWorkers();
        // 回呼方法,這個方法是個空方法,ScheduledThreadPoolExecutor中重寫了該方法
        onShutdown(); 
    } finally {
        mainLock.unlock();
    }
    // 嘗試設定執行緒池狀態為terminated
    tryTerminate();
}

再看一下另一個方法shutdownNow:

// 關閉后佇列中剩余的任務不會被執行
// 會把剩下的任務回傳交給開發者去處理
public List<Runnable> shutdownNow() {
    List<Runnable> tasks;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        // 檢查是否可以關閉執行緒
        checkShutdownAccess();
        // 設定狀態為stop
        advanceRunState(STOP);
        // 嘗試中斷所有執行緒
        interruptWorkers();
        // 回傳佇列中剩下的任務
        tasks = drainQueue();
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
    return tasks;
}

最后再來看一下和 execute()不同的提交任務方法:submit,

提交任務:submit()

submit方法并不是ThreadPoolExecutor實作的,而是AbstractExecutorService,如下:

// runnable沒有回傳值,創建FutureTask的回傳引數傳入null
public Future<?> submit(Runnable task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<Void> ftask = newTaskFor(task, null);
    execute(ftask);
    return ftask;
}
// 有引數回傳值的runnable
// 最終也是構造一個callable來執行,把回傳值設定為result
public <T> Future<T> submit(Runnable task, T result) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<T> ftask = newTaskFor(task, result);
    execute(ftask);
    return ftask;
}
// callable本身就擁有回傳值
public <T> Future<T> submit(Callable<T> task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<T> ftask = newTaskFor(task);
    execute(ftask);
    return ftask;
}

他們的邏輯都幾乎一樣:呼叫newTaskFor方法來構造一個Future物件并回傳,我們看到newTaskFor方法:

// 創建一個FutureTask來回傳
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
    return new FutureTask<T>(runnable, value);
}
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
    return new FutureTask<T>(callable);
}

可以看到這個方法很簡單:構造一個FutureTask并回傳,FutureTask也是Future介面目前唯一的實作類,

更加具體關于Future的內容就不展開了,有興趣的讀者可以去了解一下,

最后

好了到這里,關于ThreadPoolExecutor的原始碼分析內容就講完了,最后讓我們再回顧一下吧:

yGOUkq.md.png

  • ThreadPoolExecutor的整個執行流程從execute方法開始,他會根據具體的情況,采用合適的執行方案
  • 執行緒被封裝在worker物件中,worker物件通過runWorker方法,會一直不斷地呼叫getTask方法來呼叫佇列的poll或take方法獲取任務
  • 當需要退出一個worker時,只要getTask方法回傳null即可退出
  • 當執行緒池關閉時,會根據不同的關閉方法,等待所有的執行緒執行完成,然后關閉執行緒池,

執行緒池整體的模型和handler是十分類似的:一個生產者-消費者模型,但和Handler不同的是,ThreadPoolExecutor不支持延時任務,這點在ScheduledThreadPoolExecutor得到了實作;Handler的執行緒安全采用synchronize關鍵字,而ThreadPoolExecutor采用的是Lock和一些利用CAS實作執行緒安全的整型變數;Handler無法拒絕任務,執行緒池可以;Handler拋出例外會直接程式崩潰,而執行緒池不會等等,

了解了執行緒池的內部原始碼,對于他更加了解后,那么可以根據具體的問題,做出更加合適的解決方案,ThreadPoolExecutor還有一些原始碼沒有講到,以及ScheduledThreadPoolExecutor、阻塞佇列的原始碼,有興趣讀者可以自行去深入了解一下,拓展關于執行緒池的一切,

全文到此,假期肝文不容易啊,如果文章對你有幫助,求一個大拇指yJsExU.png,贊一下再走唄,

參考文獻

  • 《Java并發編程的藝術》:并發編程必讀,作者對一些原理講的很透徹
  • 《Java核心技術卷》:這系列的書主要是講解框架的使用,不會深入原理,適合入門
  • javaGuide:javaGuide,對java知識總結得很不錯的一個博客
  • Java并發編程:執行緒池的使用:博客園上一位很優秀的博主,文章寫得通俗易懂且不失深度

全文到此,原創不易,覺得有幫助可以點贊收藏評論轉發,
筆者才疏學淺,有任何想法歡迎評論區交流指正,
如需轉載請評論區或私信交流,

另外歡迎光臨筆者的個人博客:傳送門

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/257041.html

標籤:其他

上一篇:android跨行程斷點除錯問題

下一篇:強軟弱虛參考 看這個你就懂啦!

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more