一文搞懂 CountDownLatch 用法和源碼!

2020-12-22 51CTO

 

CountDownLatch 是多線程控制的一種工具,它被稱為 門閥、 計數器或者 閉鎖。這個工具經常用來用來協調多個線程之間的同步,或者說起到線程之間的通信(而不是用作互斥的作用)。下面我們就來一起認識一下 CountDownLatch

認識 CountDownLatch

CountDownLatch 能夠使一個線程在等待另外一些線程完成各自工作之後,再繼續執行。它相當於是一個計數器,這個計數器的初始值就是線程的數量,每當一個任務完成後,計數器的值就會減一,當計數器的值為 0 時,表示所有的線程都已經任務了,然後在 CountDownLatch 上等待的線程就可以恢復執行接下來的任務。

CountDownLatch 的使用

CountDownLatch 提供了一個構造方法,你必須指定其初始值,還指定了 countDown 方法,這個方法的作用主要用來減小計數器的值,當計數器變為 0 時,在 CountDownLatch 上 await 的線程就會被喚醒,繼續執行其他任務。當然也可以延遲喚醒,給 CountDownLatch 加一個延遲時間就可以實現。

其主要方法如下

CountDownLatch 主要有下面這幾個應用場景

CountDownLatch 應用場景

典型的應用場景就是當一個服務啟動時,同時會加載很多組件和服務,這時候主線程會等待組件和服務的加載。當所有的組件和服務都加載完畢後,主線程和其他線程在一起完成某個任務。

CountDownLatch 還可以實現學生一起比賽跑步的程序,CountDownLatch 初始化為學生數量的線程,鳴槍後,每個學生就是一條線程,來完成各自的任務,當第一個學生跑完全程後,CountDownLatch 就會減一,直到所有的學生完成後,CountDownLatch 會變為 0 ,接下來再一起宣布跑步成績。

順著這個場景,你自己就可以延伸、拓展出來很多其他任務場景。

CountDownLatch 用法

下面我們通過一個簡單的計數器來演示一下 CountDownLatch 的用法

  1. public class TCountDownLatch { 
  2.  
  3.     public static void main(String[] args) { 
  4.         CountDownLatch latch = new CountDownLatch(5); 
  5.         Increment increment = new Increment(latch); 
  6.         Decrement decrement = new Decrement(latch); 
  7.  
  8.         new Thread(increment).start(); 
  9.         new Thread(decrement).start(); 
  10.  
  11.         try { 
  12.             Thread.sleep(6000); 
  13.         } catch (InterruptedException e) { 
  14.             e.printStackTrace(); 
  15.         } 
  16.     } 
  17.  
  18. class Decrement implements Runnable { 
  19.  
  20.     CountDownLatch countDownLatch; 
  21.  
  22.     public Decrement(CountDownLatch countDownLatch){ 
  23.         this.countDownLatch = countDownLatch; 
  24.     } 
  25.  
  26.     @Override 
  27.     public void run() { 
  28.         try { 
  29.  
  30.             for(long i = countDownLatch.getCount();i > 0;i 
  31.                 Thread.sleep(1000); 
  32.                 System.out.println("countdown"); 
  33.                 this.countDownLatch.countDown(); 
  34.             } 
  35.  
  36.         } catch (InterruptedException e) { 
  37.             e.printStackTrace(); 
  38.         } 
  39.     } 
  40.  
  41.  
  42. class Increment implements Runnable { 
  43.  
  44.     CountDownLatch countDownLatch; 
  45.  
  46.     public Increment(CountDownLatch countDownLatch){ 
  47.         this.countDownLatch = countDownLatch; 
  48.     } 
  49.  
  50.     @Override 
  51.     public void run() { 
  52.         try { 
  53.             System.out.println("await"); 
  54.             countDownLatch.await(); 
  55.         } catch (InterruptedException e) { 
  56.             e.printStackTrace(); 
  57.         } 
  58.         System.out.println("Waiter Released"); 
  59.     } 

在 main 方法中我們初始化了一個計數器為 5 的 CountDownLatch,在 Decrement 方法中我們使用 countDown 執行減一操作,然後睡眠一段時間,同時在 Increment 類中進行等待,直到 Decrement 中的線程完成計數減一的操作後,喚醒 Increment 類中的 run 方法,使其繼續執行。

下面我們再來通過學生賽跑這個例子來演示一下 CountDownLatch 的具體用法

  1. public class StudentRunRace { 
  2.  
  3.     CountDownLatch stopLatch = new CountDownLatch(1); 
  4.     CountDownLatch runLatch = new CountDownLatch(10); 
  5.  
  6.     public void waitSignal() throws Exception{ 
  7.         System.out.println("選手" + Thread.currentThread().getName() + "正在等待裁判發布口令"); 
  8.         stopLatch.await(); 
  9.         System.out.println("選手" + Thread.currentThread().getName() + "已接受裁判口令"); 
  10.         Thread.sleep((long) (Math.random() * 10000)); 
  11.         System.out.println("選手" + Thread.currentThread().getName() + "到達終點"); 
  12.         runLatch.countDown(); 
  13.     } 
  14.  
  15.     public void waitStop() throws Exception{ 
  16.         Thread.sleep((long) (Math.random() * 10000)); 
  17.         System.out.println("裁判"+Thread.currentThread().getName()+"即將發布口令"); 
  18.         stopLatch.countDown(); 
  19.         System.out.println("裁判"+Thread.currentThread().getName()+"已發送口令,正在等待所有選手到達終點"); 
  20.         runLatch.await(); 
  21.         System.out.println("所有選手都到達終點"); 
  22.         System.out.println("裁判"+Thread.currentThread().getName()+"匯總成績排名"); 
  23.     } 
  24.  
  25.     public static void main(String[] args) { 
  26.         ExecutorService service = Executors.newCachedThreadPool(); 
  27.         StudentRunRace studentRunRace = new StudentRunRace(); 
  28.         for (int i = 0; i < 10; i++) { 
  29.             Runnable runnable = () -> { 
  30.                 try { 
  31.                     studentRunRace.waitSignal(); 
  32.                 } catch (Exception e) { 
  33.                     e.printStackTrace(); 
  34.                 } 
  35.             }; 
  36.             service.execute(runnable); 
  37.         } 
  38.         try { 
  39.             studentRunRace.waitStop(); 
  40.         } catch (Exception e) { 
  41.             e.printStackTrace(); 
  42.         } 
  43.         service.shutdown(); 
  44.     } 

下面我們就來一起分析一下 CountDownLatch 的源碼

CountDownLatch 源碼分析

CountDownLatch 使用起來比較簡單,但是卻非常有用,現在你可以在你的工具箱中加上 CountDownLatch 這個工具類了。下面我們就來深入認識一下 CountDownLatch。

CountDownLatch 的底層是由 AbstractQueuedSynchronizer 支持,而 AQS 的數據結構的核心就是兩個隊列,一個是 同步隊列(sync queue),一個是條件隊列(condition queue)。

Sync 內部類

CountDownLatch 在其內部是一個 Sync ,它繼承了 AQS 抽象類。

  1. private static final class Sync extends AbstractQueuedSynchronizer {...} 

CountDownLatch 其實其內部只有一個 sync 屬性,並且是 final 的

  1. private final Sync sync; 

CountDownLatch 只有一個帶參數的構造方法

  1. public CountDownLatch(int count) { 
  2.   if (count < 0) throw new IllegalArgumentException("count < 0"); 
  3.   this.sync = new Sync(count); 

也就是說,初始化的時候必須指定計數器的數量,如果數量為負會直接拋出異常。

然後把 count 初始化為 Sync 內部的 count,也就是

  1. Sync(int count) { 
  2.   setState(count); 

注意這裡有一個 setState(count),這是什麼意思呢?見聞知意這只是一個設置狀態的操作,但是實際上不單單是,還有一層意思是 state 的值代表著待達到條件的線程數。這個我們在聊 countDown 方法的時候再討論。

getCount() 方法的返回值是 getState() 方法,它是 AbstractQueuedSynchronizer 中的方法,這個方法會返回當前線程計數,具有 volatile 讀取的內存語義。

  1. //  
  2.  
  3. int getCount() { 
  4.   return getState(); 
  5.  
  6. //  
  7.  
  8. protected final int getState() { 
  9.   return state; 

tryAcquireShared() 方法用於獲取·共享狀態下對象的狀態,判斷對象是否為 0 ,如果為 0 返回 1 ,表示能夠嘗試獲取,如果不為 0,那麼返回 -1,表示無法獲取。

  1. protected int tryAcquireShared(int acquires) { 
  2.   return (getState() == 0) ? 1 : -1; 
  3.  
  4. //  

這個 共享狀態 屬於 AQS 中的概念,在 AQS 中分為兩種模式,一種是 獨佔模式,一種是 共享模式。

  • tryAcquire 獨佔模式,嘗試獲取資源,成功則返回 true,失敗則返回 false。
  • tryAcquireShared 共享方式,嘗試獲取資源。負數表示失敗;0 表示成功,但沒有剩餘可用資源;正數表示成功,且有剩餘資源。

tryReleaseShared() 方法用於共享模式下的釋放

  1. protected boolean tryReleaseShared(int releases) { 
  2.   // 減小數量,變為 0 的時候進行通知。 
  3.   for (;;) { 
  4.     int c = getState(); 
  5.     if (c == 0) 
  6.       return false
  7.     int nextc = c-1; 
  8.     if (compareAndSetState(c, nextc)) 
  9.       return nextc == 0; 
  10.   } 

這個方法是一個無限循環,獲取線程狀態,如果線程狀態是 0 則表示沒有被線程佔有,沒有佔有的話那麼直接返回 false ,表示已經釋放;然後下一個狀態進行 - 1 ,使用 compareAndSetState CAS 方法進行和內存值的比較,如果內存值也是 1 的話,就會更新內存值為 0 ,判斷 nextc 是否為 0 ,如果 CAS 比較不成功的話,會再次進行循環判斷。

await 方法

await() 方法是 CountDownLatch 一個非常重要的方法,基本上可以說只有 countDown 和 await 方法才是 CountDownLatch 的精髓所在,這個方法將會使當前線程在 CountDownLatch 計數減至零之前一直等待,除非線程被中斷。

CountDownLatch 中的 await 方法有兩種,一種是不帶任何參數的 await(),一種是可以等待一段時間的await(long timeout, TimeUnit unit)。下面我們先來看一下 await() 方法。

  1. public void await() throws InterruptedException { 
  2.   sync.acquireSharedInterruptibly(1); 

await 方法內部會調用 acquireSharedInterruptibly 方法,這個 acquireSharedInterruptibly 是 AQS 中的方法,以共享模式進行中斷。

  1. public final void acquireSharedInterruptibly(int arg) 
  2.   throws InterruptedException { 
  3.   if (Thread.interrupted()) 
  4.     throw new InterruptedException(); 
  5.   if (tryAcquireShared(arg) < 0) 
  6.     doAcquireSharedInterruptibly(arg); 

可以看到,acquireSharedInterruptibly 方法的內部會首先判斷線程是否中斷,如果線程中斷,則直接拋出線程中斷異常。如果沒有中斷,那麼會以共享的方式獲取。如果能夠在共享的方式下不能獲取鎖,那麼就會以共享的方式斷開連結。

  1. private void doAcquireSharedInterruptibly(int arg) 
  2.   throws InterruptedException { 
  3.   final Node node = addWaiter(Node.SHARED); 
  4.   boolean failed = true
  5.   try { 
  6.     for (;;) { 
  7.       final Node p = node.predecessor(); 
  8.       if (p == head) { 
  9.         int r = tryAcquireShared(arg); 
  10.         if (r >= 0) { 
  11.           setHeadAndPropagate(node, r); 
  12.           p.next = null; // help GC 
  13.           failed = false
  14.           return
  15.         } 
  16.       } 
  17.       if (shouldParkAfterFailedAcquire(p, node) && 
  18.           parkAndCheckInterrupt()) 
  19.         throw new InterruptedException(); 
  20.     } 
  21.   } finally { 
  22.     if (failed) 
  23.       cancelAcquire(node); 
  24.   } 

這個方法有些長,我們分開來看

  • 首先,會先構造一個共享模式的 Node 入隊
  • 然後使用無限循環判斷新構造 node 的前驅節點,如果 node 節點的前驅節點是頭節點,那麼就會判斷線程的狀態,這裡調用了一個 setHeadAndPropagate ,其源碼如下

  1. private void setHeadAndPropagate(Node node, int propagate) { 
  2.   Node h = head;  
  3.   setHead(node); 
  4.   if (propagate > 0 || h == null || h.waitStatus < 0 || 
  5.       (h = head) == null || h.waitStatus < 0) { 
  6.     Node s = node.next
  7.     if (s == null || s.isShared()) 
  8.       doReleaseShared(); 
  9.   } 

首先會設置頭節點,然後進行一系列的判斷,獲取節點的獲取節點的後繼,以共享模式進行釋放,就會調用 doReleaseShared 方法,我們再來看一下 doReleaseShared 方法

  1. private void doReleaseShared() { 
  2.  
  3.   for (;;) { 
  4.     Node h = head; 
  5.     if (h != null && h != tail) { 
  6.       int ws = h.waitStatus; 
  7.       if (ws == Node.SIGNAL) { 
  8.         if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) 
  9.           continue;            // loop to recheck cases 
  10.         unparkSuccessor(h); 
  11.       } 
  12.       else if (ws == 0 && 
  13.                !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) 
  14.         continue;                // loop on failed CAS 
  15.     } 
  16.     if (h == head)                   // loop if head changed 
  17.       break; 
  18.   } 

這個方法會以無限循環的方式首先判斷頭節點是否等於尾節點,如果頭節點等於尾節點的話,就會直接退出。如果頭節點不等於尾節點,會判斷狀態是否為 SIGNAL,不是的話就繼續循環 compareAndSetWaitStatus,然後斷開後繼節點。如果狀態不是 SIGNAL,也會調用 compareAndSetWaitStatus 設置狀態為 PROPAGATE,狀態為 0 並且不成功,就會繼續循環。

也就是說 setHeadAndPropagate 就是設置頭節點並且釋放後繼節點的一系列過程。

  • 我們來看下面的 if 判斷,也就是 shouldParkAfterFailedAcquire(p, node) 這裡

  1. if (shouldParkAfterFailedAcquire(p, node) && 
  2.     parkAndCheckInterrupt()) 
  3.   throw new InterruptedException(); 

如果上面 Node p = node.predecessor() 獲取前驅節點不是頭節點,就會進行 park 斷開操作,判斷此時是否能夠斷開,判斷的標準如下

  1. private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { 
  2.   int ws = pred.waitStatus; 
  3.   if (ws == Node.SIGNAL) 
  4.     return true
  5.   if (ws > 0) { 
  6.     do { 
  7.       node.prev = pred = pred.prev; 
  8.     } while (pred.waitStatus > 0); 
  9.     pred.next = node; 
  10.   } else { 
  11.     compareAndSetWaitStatus(pred, ws, Node.SIGNAL); 
  12.   } 
  13.   return false

這個方法會判斷 Node p 的前驅節點的結點狀態(waitStatus),節點狀態一共有五種,分別是

  1. CANCELLED(1):表示當前結點已取消調度。當超時或被中斷(響應中斷的情況下),會觸發變更為此狀態,進入該狀態後的結點將不會再變化。
  2. SIGNAL(-1):表示後繼結點在等待當前結點喚醒。後繼結點入隊時,會將前繼結點的狀態更新為 SIGNAL。
  3. CONDITION(-2):表示結點等待在 Condition 上,當其他線程調用了 Condition 的 signal() 方法後,CONDITION狀態的結點將從等待隊列轉移到同步隊列中,等待獲取同步鎖。
  4. PROPAGATE(-3):共享模式下,前繼結點不僅會喚醒其後繼結點,同時也可能會喚醒後繼的後繼結點。
  5. 0:新結點入隊時的默認狀態。

如果前驅節點是 SIGNAL 就會返回 true 表示可以斷開,如果前驅節點的狀態大於 0 (此時為什麼不用 ws == Node.CANCELLED ) 呢?因為 ws 大於 0 的條件只有 CANCELLED 狀態了。然後就是一系列的查找遍歷操作直到前驅節點的 waitStatus > 0。如果 ws <= 0 ,而且還不是 SIGNAL 狀態的話,就會使用 CAS 替換前驅節點的 ws 為 SIGNAL 狀態。

如果檢查判斷是中斷狀態的話,就會返回 false。

  1. private final boolean parkAndCheckInterrupt() { 
  2.   LockSupport.park(this); 
  3.   return Thread.interrupted(); 

這個方法使用 LockSupport.park 斷開連接,然後返回線程是否中斷的標誌。

  • cancelAcquire() 用於取消等待隊列,如果等待過程中沒有成功獲取資源(如timeout,或者可中斷的情況下被中斷了),那麼取消結點在隊列中的等待。

  1. private void cancelAcquire(Node node) { 
  2.   if (node == null
  3.     return
  4.  
  5.   node.thread = null
  6.  
  7.   Node pred = node.prev; 
  8.   while (pred.waitStatus > 0) 
  9.     node.prev = pred = pred.prev; 
  10.  
  11.   Node predNext = pred.next
  12.  
  13.   node.waitStatus = Node.CANCELLED; 
  14.  
  15.   if (node == tail && compareAndSetTail(node, pred)) { 
  16.     compareAndSetNext(pred, predNext, null); 
  17.   } else { 
  18.     int ws; 
  19.     if (pred != head && 
  20.         ((ws = pred.waitStatus) == Node.SIGNAL || 
  21.          (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && 
  22.         pred.thread != null) { 
  23.       Node next = node.next
  24.       if (next != null && next.waitStatus <= 0) 
  25.         compareAndSetNext(pred, predNext, next); 
  26.     } else { 
  27.       unparkSuccessor(node); 
  28.     } 
  29.     node.next = node; // help GC 
  30.   } 

所以,對 CountDownLatch 的 await 調用大致會有如下的調用過程。

一個和 await 重載的方法是 await(long timeout, TimeUnit unit),這個方法和 await 最主要的區別就是這個方法能夠可以等待計數器一段時間再執行後續操作。

countDown 方法

countDown 是和 await 同等重要的方法,countDown 用於減少計數器的數量,如果計數減為 0 的話,就會釋放所有的線程。

  1. public void countDown() { 
  2.   sync.releaseShared(1); 

這個方法會調用 releaseShared 方法,此方法用於共享模式下的釋放操作,首先會判斷是否能夠進行釋放,判斷的方法就是 CountDownLatch 內部類 Sync 的 tryReleaseShared 方法

  1. public final boolean releaseShared(int arg) { 
  2.   if (tryReleaseShared(arg)) { 
  3.     doReleaseShared(); 
  4.     return true
  5.   } 
  6.   return false
  7.  
  8. //  
  9.  
  10. protected boolean tryReleaseShared(int releases) { 
  11.   for (;;) { 
  12.     int c = getState(); 
  13.     if (c == 0) 
  14.       return false
  15.     int nextc = c-1; 
  16.     if (compareAndSetState(c, nextc)) 
  17.       return nextc == 0; 
  18.   } 

tryReleaseShared 會進行 for 循環判斷線程狀態值,使用 CAS 不斷嘗試進行替換。

如果能夠釋放,就會調用 doReleaseShared 方法

  1. private void doReleaseShared() { 
  2.   for (;;) { 
  3.     Node h = head; 
  4.     if (h != null && h != tail) { 
  5.       int ws = h.waitStatus; 
  6.       if (ws == Node.SIGNAL) { 
  7.         if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) 
  8.           continue;            // loop to recheck cases 
  9.         unparkSuccessor(h); 
  10.       } 
  11.       else if (ws == 0 && 
  12.                !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) 
  13.         continue;                // loop on failed CAS 
  14.     } 
  15.     if (h == head)                   // loop if head changed 
  16.       break; 
  17.   } 

可以看到,doReleaseShared 其實也是一個無限循環不斷使用 CAS 嘗試替換的操作。

總結

本文是 CountDownLatch 的基本使用和源碼分析,CountDownLatch 就是一個基於 AQS 的計數器,它內部的方法都是圍繞 AQS 框架來談的,除此之外還有其他比如 ReentrantLock、Semaphore 等都是 AQS 的實現,所以要研究並發的話,離不開對 AQS 的探討。CountDownLatch 的源碼看起來很少,比較簡單,但是其內部比如 await 方法的調用鏈路卻很長,也值得花費時間深入研究。

本文轉載自微信公眾號「 Java建設者」,可以通過以下二維碼關注。轉載本文請聯繫 Java建設者公眾號。

【編輯推薦】

【責任編輯:

武曉燕

TEL:(010)68476606】

點讚 0

相關焦點

  • Java並發工具三劍客之CountDownLatch源碼解析
    CountDownLatch是Java並發包下的一個工具類,latch是門閂的意思,顧名思義,CountDownLatch就是有一個門閂擋住了裡面的人(線程)出來,當count減到0的時候,門閂就打開了,人(線程)就可以出來了。
  • Java並發包CountDownLatch、CyclicBarrier、Semaphore原理解析
    組隊準備,房間已滿不可加入線程Thread-10組隊準備,房間已滿不可加入線程Thread-11組隊準備,房間已滿不可加入線程Thread-12組隊準備,房間已滿不可加入線程Thread-13組隊準備,房間已滿不可加入本案例中有兩個線程都調用了latch.await
  • Java項目實踐,CountDownLatch實現多線程閉鎖
    e.printStackTrace();}long end = System.currentTimeMillis();System.out.println("耗時:" + (end - start));}}class MyThread implements Runnable {private CountDownLatch latch
  • Latch-up
    晶片中的latch up和ESD是一個很大的方向,有些大公司會有專門的設計團隊。本期講latch up為什麼要提及ESD呢?原因很簡單,這個latch up跟ESD相關。       相信很多IC designer對latch up都不陌生,它是指晶片內部觸發寄生npn和pnp引起電源地之間出現低阻通路進而導致電源地之間出現大電流的一種現象。
  • 鎖存器Latch和觸發器Flip-flop有何區別
    打開APP 鎖存器Latch和觸發器Flip-flop有何區別 發表於 2018-04-18 14:10:10
  • 「原創」Java並發編程系列22|倒計時器CountDownLatch
    今天就介紹一種JDk提供的解決方案來優雅的解決這一問題,那就是倒計時器CountDownLatch。本文將分以下兩部分介紹:CountDownLatch的使用CountDownLatch源碼分析1.總結CountDownLatch的使用步驟:(比如線程A需要等待線程B和線程C執行後再執行)創建CountDownLatch對象,設置要等待的線程數N(這裡是2);等待線程A調用await()
  • Java編發編程中CountDownLatch到底是個啥?
    很抽象,小問題,下方的兩節或許能讓你理解CountDownLatch的用法和內部的實現。1.CountDownLatch的使用假設現在,我們要起一個3塊錢的集資項目,並且限定每個人一次只能捐1塊錢當募集到3塊錢的時候立馬就把這筆錢捐給我自己,如果湊齊之後你還想捐,那麼我會跟你說,項目已經完成了,你這一塊錢我不受理,自己去買雪糕吃吧
  • Android-Automotive之源碼編譯過中APK是如何打包和籤名的
    說到安卓應用,一般的開發者用的比較多的是AndroidStudio集成開發工具,根據Google相關的文檔說明就可以一步一步做到應用的打包,籤名和發布
  • 【伴奏】賭神開場曲The Final Countdown(附譜)
    《the final countdown(最後倒計時,最後倒數)》是1986年2月14日發行,Europe樂團創作並演唱的一首瑞典歌曲。Europe樂團1981年成立於瑞典.起初樂團名字叫「Force」(力量).由核心成員Joey Tempest(主唱)、John Norum(吉他)和John Leven(貝斯)組成.在一次瑞典全國業餘搖滾樂團大賽中獲得冠軍並得到一紙唱片合約.之後將樂團名更改為Europe並在瑞典發行了兩張專輯「Europe」和「Wings of Tomorrow」.
  • countdown軟體測試死亡時間是真的嗎 感興趣的朋友們一起來看看吧
    countdown這款軟體是一款有電影衍生出來的產品,很多朋友都很好奇這個countdown app測試死亡時間準不準,那麼今天小編就為大家帶來詳細的介紹,感興趣的朋友們一   原標題:countdown軟體測試死亡時間是真的嗎 感興趣的朋友們一起來看看吧
  • countdown軟體測試死亡時間是真的嗎 app測試時間準嗎
    countdown死亡倒計時真的假的?countdown這款軟體是一款有電影衍生出來的產品,很多朋友都很好奇這個countdown app測試死亡時間準不準,那麼今天小編就為大家帶來詳細的介紹,感興趣的朋友們一起來看看吧!  countdown軟體測試死亡時間是真的嗎 app測試時間準嗎
  • 雲豹一對一直播app源碼搭建教程
    擁有了雲豹一對一直播app源碼,要搭建完畢才能進行下一步的上架/運營工作。那麼,該如何進行一對一直播app源碼搭建工作呢?一對一直播app源碼申請三方在獲取雲豹一對一直播app源碼後,或者在一對一直播源碼的開發過程中,我們就要開始著手準備申請各種三方帳號了。
  • GitHub 下架 Youtube-dl 遭粉絲瘋狂上傳源碼報復,開源者的權益誰來維護?
    這一刪除,粉絲們不幹了。憤怒的程式設計師粉絲們瘋狂複製並帶有Youtube-dl的源碼,甚至有人向Github官方的 DMCA 通知存儲庫裡。 目前,在GitHub搜索「youtube-dl」,可以找到4115個相關結果。
  • 直播交友APP源碼開發,直播交友系統源碼搭建,直播軟體源碼開發
    直播市場競爭十分的激烈,直播行業正朝著多元化方向發展,對於很多中小型運營商來講,沒有強大的技術支持,是很難擠進這塊大市場的,於是直播系統源碼成為打造一個優質平臺的基石。那麼開發一套直播系統源碼以及搭建該怎麼做?
  • 材質、細節、尺寸、重量,一文搞懂!
    今天,小編帶你一文搞懂~行李箱買多大的呢?這個問題其實決定權不完全在我們這裡。儘管我們有太多行李要裝,但是每個航空公司都有自己的免費行李託運規定,比如可以帶幾件,以及每個行李的尺寸和重量。所以,建議先閱讀英國/澳洲和國內常見航空公司對行李箱的限制,然後再選購行李箱。
  • 一對一直播源碼開發,這幾個盈利機制促進平臺可持續性發展
    一對一直播源碼一、一對一直播源碼支持的系統盈利功能1.付費觀看內容功能一對一直播系統內主播上傳的視頻和相冊可以設置公開或私密兩種模式,公開視頻為所有人免費查看,私密視頻和相冊封面有毛玻璃效果,一對一直播源碼可以設置個人相冊和視頻收費規則,需要用戶付費觀看或者開通vip後免費查看。4.
  • 材質、細節、尺寸、重量,一文搞懂!
    今天,易醬帶你一文搞懂~(本文出自知乎帳號【易醬】的回答,歡迎關註解鎖更多留學乾貨)行李箱買多大的呢?這個問題其實決定權不完全在我們這裡。儘管我們有太多行李要裝,但是每個航空公司都有自己的免費行李託運規定,比如可以帶幾件,以及每個行李的尺寸和重量。
  • 深入源碼剖析componentWillXXX為什麼UNSAFE
    模式遷移讓我們再看第二個原因:React從Legacy模式遷移到Concurrent模式後,這些鉤子的表現會和之前不一致。我們先了解下什麼是模式?不同模式有什麼區別?從Legacy到Concurrent從React15升級為React16後,源碼改動如此之大,說React被重構可能更貼切些。
  • 手機直播系統源碼搭建功能
    如今各大直播平臺為了爭奪全國已經接近4億的用戶群體可謂是使盡了渾身解數,直播系統源碼開發更是經歷了前所未有的挑戰。想把直播系統源碼搭建開發出來,絕對不是很容易的,因為直播系統源碼開發中運用到的技術難點非常多。我們在開發直播系統源碼時需要注意哪些功能呢?
  • Java中的並發工具類CountDownLatch
    可以循環使用的屏障,讓一組線程到達一個屏障時被阻塞,等到最後一個線程到達屏障時才會開門。用這個CyclicBarrier實現上面的需求,有幾處地方需要修改,最主要的是CountDownLatch換成了CyclicBarrier,CyclicBarrier的構造方法裡的參數表示要攔截的數量,這裡是21包括主線程。再就是把原來的線程池換成了不限制數量的,如果最大是10個線程,這個程序永遠不會執行完畢。