java concurrent 探祕
編寫多執行緒的程式一直都是一件比較麻煩的事情,要考慮很多事情,處理不好還會出很多意想不到的麻煩。加上現在很多開發者接觸到的專案都是打著企業級旗號的B/S專案,大多數人都很少涉及多執行緒,這又為本文的主角增加了一份神祕感。
講到Java多執行緒,大多數人腦海中跳出來的是Thread、Runnable、synchronized……這些是最基本的東西,雖然已經足夠強大,但想要用好還真不容易。從JDK 1.5開始,增加了java.util.concurrent包,它的引入大大簡化了多執行緒程式的開發(要感謝一下大牛Doug Lee)。
java.util.concurrent包分成了三個部分,分別是java.util.concurrent、java.util.concurrent.atomic和java.util.concurrent.lock。內容涵蓋了併發集合類、執行緒池機制、同步互斥機制、執行緒安全的變數更新工具類、鎖等等常用工具。
為了便於理解,本文使用一個例子來做說明,交代一下它的場景:
假設要對一套10個節點組成的環境進行檢查,這個環境有兩個入口點,通過節點間的依賴關係可以遍歷到整個環境。依賴關係可以構成一張有向圖,可能存在環。為了提高檢查的效率,考慮使用多執行緒。
1、Executors
通過這個類能夠獲得多種執行緒池的例項,例如可以呼叫newSingleThreadExecutor()獲得單執行緒的ExecutorService,呼叫newFixedThreadPool()獲得固定大小執行緒池的ExecutorService。拿到ExecutorService可以做的事情就比較多了,最簡單的是用它來執行Runnable物件,也可以執行一些實現了Callable<T>的物件。用Thread的start()方法沒有返回值,如果該執行緒執行的方法有返回值那用ExecutorService就再好不過了,可以選擇submit()、invokeAll()或者invokeAny(),根據具體情況選擇合適的方法即可。
- package service;
- import java.util.ArrayList;
- import java.util.List;
- import java.util.concurrent.ExecutionException;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- import java.util.concurrent.Future;
- import java.util.concurrent.TimeUnit;
- /**
- * 執行緒池服務類
- *
- * @author DigitalSonic
- */
- public class ThreadPoolService {
- /**
- * 預設執行緒池大小
- */
- public static final int DEFAULT_POOL_SIZE = 5;
- /**
- * 預設一個任務的超時時間,單位為毫秒
- */
- public static final long DEFAULT_TASK_TIMEOUT = 1000;
- private int poolSize = DEFAULT_POOL_SIZE;
- private ExecutorService executorService;
- /**
- * 根據給定大小建立執行緒池
- */
- public ThreadPoolService(int poolSize) {
- setPoolSize(poolSize);
- }
- /**
- * 使用執行緒池中的執行緒來執行任務
- */
- public void execute(Runnable task) {
- executorService.execute(task);
- }
- /**
- * 線上程池中執行所有給定的任務並取回執行結果,使用預設超時時間
- *
- * @see #invokeAll(List, long)
- */
- public List<Node> invokeAll(List<ValidationTask> tasks) {
- return invokeAll(tasks, DEFAULT_TASK_TIMEOUT * tasks.size());
- }
- /**
- * 線上程池中執行所有給定的任務並取回執行結果
- *
- * @param timeout 以毫秒為單位的超時時間,小於0表示不設定超時
- * @see java.util.concurrent.ExecutorService#invokeAll(java.util.Collection)
- */
- public List<Node> invokeAll(List<ValidationTask> tasks, long timeout) {
- List<Node> nodes = new ArrayList<Node>(tasks.size());
- try {
- List<Future<Node>> futures = null;
- if (timeout < 0) {
- futures = executorService.invokeAll(tasks);
- } else {
- futures = executorService.invokeAll(tasks, timeout, TimeUnit.MILLISECONDS);
- }
- for (Future<Node> future : futures) {
- try {
- nodes.add(future.get());
- } catch (ExecutionException e) {
- e.printStackTrace();
- }
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- return nodes;
- }
- /**
- * 關閉當前ExecutorService
- *
- * @param timeout 以毫秒為單位的超時時間
- */
- public void destoryExecutorService(long timeout) {
- if (executorService != null && !executorService.isShutdown()) {
- try {
- executorService.awaitTermination(timeout, TimeUnit.MILLISECONDS);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- executorService.shutdown();
- }
- }
- /**
- * 關閉當前ExecutorService,隨後根據poolSize建立新的ExecutorService
- */
- public void createExecutorService() {
- destoryExecutorService(1000);
- executorService = Executors.newFixedThreadPool(poolSize);
- }
- /**
- * 調整執行緒池大小
- * @see #createExecutorService()
- */
- public void setPoolSize(int poolSize) {
- this.poolSize = poolSize;
- createExecutorService();
- }
- }
這裡要額外說明一下invokeAll()和invokeAny()方法。前者會執行給定的所有Callable<T>物件,等所有任務完成後返回一個包含了執行結果的List<Future<T>>,每個Future.isDone()都是true,可以用Future.get()拿到結果;後者只要完成了列表中的任意一個任務就立刻返回,返回值就是執行結果。
造成這個問題的主要原因是兩個版本中invokeAll和invokeAny的方法簽名不同,1.6裡是invokeAll(Collection<? extends Callable<T>> tasks),而1.5裡是invokeAll(Collection<Callable<T>>tasks)。網上也有人遇到類似的問題(invokeAll() is not willing to acept a Collection<Callable<T>>)。
和其他資源一樣,執行緒池在使用完畢後也需要釋放,用shutdown()方法可以關閉執行緒池,如果當時池裡還有沒有被執行的任務,它會等待任務執行完畢,在等待期間試圖進入執行緒池的任務將被拒絕。也可以用shutdownNow()來關閉執行緒池,它會立刻關閉執行緒池,沒有執行的任務作為返回值返回。
2、Lock
多執行緒程式設計中常常要鎖定某個物件,之前會用synchronized來實現,現在又多了另一種選擇,那就是java.util.concurrent.locks。通過Lock能夠實現更靈活的鎖定機制,它還提供了很多synchronized所沒有的功能,例如嘗試獲得鎖(tryLock())。
使用Lock時需要自己獲得鎖並在使用後手動釋放,這一點與synchronized有所不同,所以通常Lock的使用方式是這樣的:
- Lock l = ...;
- l.lock();
- try {
- // 執行操作
- } finally {
- l.unlock();
- }
java.util.concurrent.locks中提供了幾個Lock介面的實現類,比較常用的應該是ReentrantLock。以下範例中使用了ReentrantLock進行節點鎖定:
- package service;
- import java.util.concurrent.locks.Lock;
- import java.util.concurrent.locks.ReentrantLock;
- /**
- * 節點類
- *
- * @author DigitalSonic
- */
- public class Node {
- private String name;
- private String wsdl;
- private String result = "PASS";
- private String[] dependencies = new String[] {};
- private Lock lock = new ReentrantLock();
- /**
- * 預設構造方法
- */
- public Node() {
- }
- /**
- * 構造節點物件,設定名稱及WSDL
- */
- public Node(String name, String wsdl) {
- this.name = name;
- this.wsdl = wsdl;
- }
- /**
- * 返回包含節點名稱、WSDL以及驗證結果的字串
- */
- @Override
- public String toString() {
- String toString = "Node: " + name + " WSDL: " + wsdl + " Result: " + result;
- return toString;
- }
- // Getter & Setter
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- public String getWsdl() {
- return wsdl;
- }
- public void setWsdl(String wsdl) {
- this.wsdl = wsdl;
- }
- public String getResult() {
- return result;
- }
- public void setResult(String result) {
- this.result = result;
- }
- public String[] getDependencies() {
- return dependencies;
- }
- public void setDependencies(String[] dependencies) {
- this.dependencies = dependencies;
- }
- public Lock getLock() {
- return lock;
- }
- }
- package service;
- import java.util.concurrent.Callable;
- import java.util.concurrent.locks.Lock;
- import java.util.logging.Logger;
- import service.mock.MockNodeValidator;
- /**
- * 執行驗證的任務類
- *
- * @author DigitalSonic
- */
- public class ValidationTask implements Callable<Node> {
- private static Logger logger = Logger.getLogger("ValidationTask");
- private String wsdl;
- /**
- * 構造方法,傳入節點的WSDL
- */
- public ValidationTask(String wsdl) {
- this.wsdl = wsdl;
- }
- /**
- * 執行鍼對某個節點的驗證<br/>
- * 如果正有別的執行緒在執行同一節點的驗證則等待其結果,不重複執行驗證
- */
- @Override
- public Node call() throws Exception {
- Node node = ValidationService.NODE_MAP.get(wsdl);
- Lock lock = null;
- logger.info("開始驗證節點:" + wsdl);
- if (node != null) {
- lock = node.getLock();
- if (lock.tryLock()) {
- // 當前沒有其他執行緒驗證該節點
- logger.info("當前沒有其他執行緒驗證節點" + node.getName() + "[" + wsdl + "]");
- try {
- Node result = MockNodeValidator.validateNode(wsdl);
- mergeNode(result, node);
- } finally {
- lock.unlock();
- }
- } else {
- // 當前有別的執行緒正在驗證該節點,等待結果
- logger.info("當前有別的執行緒正在驗證節點" + node.getName() + "[" + wsdl + "],等待結果");
- lock.lock();
- lock.unlock();
- }
- } else {
- // 從未進行過驗證,這種情況應該只出現在系統啟動初期
- // 這時是在做初始化,不應該有衝突發生
- logger.info("首次驗證節點:" + wsdl);
- node = MockNodeValidator.validateNode(wsdl);
- ValidationService.NODE_MAP.put(wsdl, node);
- }
- logger.info("節點" + node.getName() + "[" + wsdl + "]驗證結束,驗證結果:" + node.getResult());
- return node;
- }
- /**
- * 將src的內容合併進dest節點中,不進行深度拷貝
- */
- private Node mergeNode(Node src, Node dest) {
- dest.setName(src.getName());
- dest.setWsdl(src.getWsdl());
- dest.setDependencies(src.getDependencies());
- dest.setResult(src.getResult());
- return dest;
- }
- }
請注意ValidationTask的call()方法,這裡會先檢查節點是否被鎖定,如果被鎖定則表示當前有另一個執行緒正在驗證該節點,那就不用重複進行驗證。第50行和第51行,那到鎖後立即釋放,這裡只是為了等待驗證結束。
講到Lock,就不能不講Conditon,前者代替了synchronized,而後者則代替了Object物件上的wait()、notify()和notifyAll()方法(Condition中提供了await()、signal()和signalAll()方法),當滿足執行條件前掛起執行緒。Condition是與Lock結合使用的,通過Lock.newCondition()方法能夠建立與Lock繫結的Condition例項。JDK的JavaDoc中有一個例子能夠很好地說明Condition的用途及用法:
- class BoundedBuffer {
- final Lock lock = new ReentrantLock();
- final Condition notFull = lock.newCondition();
- final Condition notEmpty = lock.newCondition();
- final Object[] items = new Object[100];
- int putptr, takeptr, count;
- public void put(Object x) throws InterruptedException {
- lock.lock();
- try {
- while (count == items.length)
- notFull.await();
- items[putptr] = x;
- if (++putptr == items.length) putptr = 0;
- ++count;
- notEmpty.signal();
- } finally {
- lock.unlock();
- }
- }
- public Object take() throws InterruptedException {
- lock.lock();
- try {
- while (count == 0)
- notEmpty.await();
- Object x = items[takeptr];
- if (++takeptr == items.length) takeptr = 0;
- --count;
- notFull.signal();
- return x;
- } finally {
- lock.unlock();
- }
- }
- }
說到這裡,讓我解釋一下之前的例子裡為什麼沒有選擇Condition來等待驗證結束。await()方法在呼叫時當前執行緒先要獲得對應的鎖,既然我都拿到鎖了,那也就是說驗證已經結束了。。。
3、併發集合類
集合類是大家程式設計時經常要使用的東西,ArrayList、HashMap什麼的,java.util包中的集合類有的是執行緒安全的,有的則不是,在編寫多執行緒的程式時使用執行緒安全的類能省去很多麻煩,但這些類的效能如何呢?java.util.concurrent包中提供了幾個併發結合類,例如ConcurrentHashMap、ConcurrentLinkedQueue和CopyOnWriteArrayList等等,根據不同的使用場景,開發者可以用它們替換java.util包中的相應集合類。
CopyOnWriteArrayList是ArrayList的一個變體,比較適合用在讀取比較頻繁、修改較少的情況下,因為每次修改都要複製整個底層陣列。ConcurrentHashMap中為Map介面增加了一些方法(例如putIfAbsenct()),同時做了些優化,總之灰常之好用,下面的程式碼中使用ConcurrentHashMap來作為全域性節點表,完全無需考慮併發問題。ValidationService中只是宣告(第17行),具體的使用是在上面的ValidationTask中。
- package service;
- import java.util.ArrayList;
- import java.util.List;
- import java.util.Map;
- import java.util.concurrent.ConcurrentHashMap;
- /**
- * 執行驗證的服務類
- *
- * @author DigitalSonic
- */
- public class ValidationService {
- /**
- * 全域性節點表
- */
- public static final Map<String, Node> NODE_MAP = new ConcurrentHashMap<String, Node>();
- private ThreadPoolService threadPoolService;
- public ValidationService(ThreadPoolService threadPoolService) {
- this.threadPoolService = threadPoolService;
- }
- /**
- * 給出一個入口節點的WSDL,通過廣度遍歷的方式驗證與其相關的各個節點
- *
- * @param wsdl 入口節點WSDL
- */
- public void validate(List<String> wsdl) {
- List<String> visitedNodes = new ArrayList<String>();
- List<String> nextRoundNodes = new ArrayList<String>();
- nextRoundNodes.addAll(wsdl);
- while (nextRoundNodes.size() > 0) {
- List<ValidationTask> tasks = getTasks(nextRoundNodes);
- List<Node> nodes = threadPoolService.invokeAll(tasks);
- visitedNodes.addAll(nextRoundNodes);
- nextRoundNodes.clear();
- getNextRoundNodes(nodes, visitedNodes, nextRoundNodes);
- }
- }
- private List<String> getNextRoundNodes(List<Node> nodes,
- List<String> visitedNodes, List<String> nextRoundNodes) {
- for (Node node : nodes) {
- for (String wsdl : node.getDependencies()) {
- if (!visitedNodes.contains(wsdl)) {
- nextRoundNodes.add(wsdl);
- }
- }
- }
- return nextRoundNodes;
- }
- private List<ValidationTask> getTasks(List<String> nodes) {
- List<ValidationTask> tasks = new ArrayList<ValidationTask>(nodes.size());
- for (String wsdl : nodes) {
- tasks.add(new ValidationTask(wsdl));
- }
- return tasks;
- }
- }
4、AtomicInteger
對變數的讀寫操作都是原子操作(除了long或者double的變數),但像數值型別的++ --操作不是原子操作,像i++中包含了獲得i的原始值、加1、寫回i、返回原始值,在進行類似i++這樣的操作時如果不進行同步問題就大了。好在java.util.concurrent.atomic為我們提供了很多工具類,可以以原子方式更新變數。
以AtomicInteger為例,提供了代替++ --的getAndIncrement()、incrementAndGet()、getAndDecrement()和decrementAndGet()方法,還有加減給定值的方法、當前值等於預期值時更新的compareAndSet()方法。
下面的例子中用AtomicInteger儲存全域性驗證次數(第69行做了自增的操作),因為validateNode()方法會同時被多個執行緒呼叫,所以直接用int不同步是不行的,但用AtomicInteger在這種場合下就很合適。
- package service.mock;
- import java.util.ArrayList;
- import java.util.HashMap;
- import java.util.List;
- import java.util.Map;
- import java.util.concurrent.atomic.AtomicInteger;
- import java.util.logging.Logger;
- import service.Node;
- /**
- * 模擬執行節點驗證的Mock類
- *
- * @author DigitalSonic
- */
- public class MockNodeValidator {
- public static final List<Node> ENTRIES = new ArrayList<Node>();
- private static final Map<String, Node> NODE_MAP = new HashMap<String, Node>();
- private static AtomicInteger count = new AtomicInteger(0);
- private static Logger logger = Logger.getLogger("MockNodeValidator");
- /*
- * 構造模擬資料
- */
- static {
- Node node0 = new Node("NODE0", "http://node0/check?wsdl"); //入口0
- Node node1 = new Node("NODE1", "http://node1/check?wsdl");
- Node node2 = new Node("NODE2", "http://node2/check?wsdl");
- Node node3 = new Node("NODE3", "http://node3/check?wsdl");
- Node node4 = new Node("NODE4", "http://node4/check?wsdl");
- Node node5 = new Node("NODE5", "http://node5/check?wsdl");
- Node node6 = new Node("NODE6", "http://node6/check?wsdl"); //入口1
- Node node7 = new Node("NODE7", "http://node7/check?wsdl");
- Node node8 = new Node("NODE8", "http://node8/check?wsdl");
- Node node9 = new Node("NODE9", "http://node9/check?wsdl");
- node0.setDependencies(new String[] { node1.getWsdl(), node2.getWsdl() });
- node1.setDependencies(new String[] { node3.getWsdl(), node4.getWsdl() });
- node2.setDependencies(new String[] { node5.getWsdl() });
- node6.setDependencies(new String[] { node7.getWsdl(), node8.getWsdl() });
- node7.setDependencies(new String[] { node5.getWsdl(), node9.getWsdl() });
- node8.setDependencies(new String[] { node3.getWsdl(), node4.getWsdl() });
- node2.setResult("FAILED");
- NODE_MAP.put(node0.getWsdl(), node0);
- NODE_MAP.put(node1.getWsdl(), node1);
- NODE_MAP.put(node2.getWsdl(), node2);
- NODE_MAP.put(node3.getWsdl(), node3);
- NODE_MAP.put(node4.getWsdl(), node4);
- NODE_MAP.put(node5.getWsdl(), node5);
- NODE_MAP.put(node6.getWsdl(), node6);
- NODE_MAP.put(node7.getWsdl(), node7);
- NODE_MAP.put(node8.getWsdl(), node8);
- NODE_MAP.put(node9.getWsdl(), node9);
- ENTRIES.add(node0);
- ENTRIES.add(node6);
- }
- /**
- * 模擬執行遠端驗證返回節點,每次呼叫等待500ms
- */
- public static Node validateNode(String wsdl) {
- Node node = cloneNode(NODE_MAP.get(wsdl));
- logger.info("驗證節點" + node.getName() + "[" + node.getWsdl() + "]");
- count.getAndIncrement();
- try {
- Thread.sleep(500);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- return node;
- }
- /**
- * 獲得計數器的值
- */
- public static int getCount() {
- return count.intValue();
- }
- /**
- * 克隆一個新的Node物件(未執行深度克隆)
- */
- public static Node cloneNode(Node originalNode) {
- Node newNode = new Node();
- newNode.setName(originalNode.getName());
- newNode.setWsdl(originalNode.getWsdl());
- newNode.setResult(originalNode.getResult());
- newNode.setDependencies(originalNode.getDependencies());
- return newNode;
- }
- }
上述程式碼還有另一個功能,就是構造測試用的節點資料,一共10個節點,有2個入口點,通過這兩個點能夠遍歷整個系統。每次呼叫會模擬遠端訪問,等待500ms。環境間節點依賴如下:
Node1 [Node3, Node4]
Node2 [Node5]
Node6 [Node7, Node8]
Node7 [Node5, Node9]
Node8 [Node3, Node4]
5、CountDownLatch
CountDownLatch是一個一次性的同步輔助工具,允許一個或多個執行緒一直等待,直到計數器值變為0。它有一個構造方法,設定計數器初始值,即在await()結束等待前需要呼叫多少次countDown()方法。CountDownLatch的計數器不能重置,所以說它是“一次性”的,如果需要重置計數器,可以使用CyclicBarrier。在執行環境檢查的主類中,使用了CountDownLatch來等待所有驗證結束,在各個併發驗證的執行緒完成任務結束前都會呼叫countDown(),因為有3個併發的驗證,所以將計數器設定為3。
最後將所有這些類整合起來,執行環境檢查的主類如下。它會建立執行緒池服務和驗證服務,先做一次驗證(相當於是對系統做次初始化),隨後併發3個驗證請求。系統執行完畢會顯示實際執行的節點驗證次數和執行時間。如果是順序執行,驗證次數應該是13*4=52,但實際的驗證次數會少於這個數字(我這裡最近一次執行了33次驗證),因為如果同時有兩個執行緒要驗證同一節點時只會做一次驗證。關於時間,如果是順序執行,52次驗證每次等待500ms,那麼驗證所耗費的時間應該是26000ms,使用了多執行緒後的實際耗時遠小於該數字(最近一次執行耗時4031ms)。
- package service.mock;
- import java.util.ArrayList;
- import java.util.List;
- import java.util.concurrent.CountDownLatch;
- import service.Node;
- import service.ThreadPoolService;
- import service.ValidationService;
- /**
- * 模擬執行這個環境的驗證
- *
- * @author DigitalSonic
- */
- public class ValidationStarter implements Runnable {
- private List<String> entries;
- private ValidationService validationService;
- private CountDownLatch signal;
- public ValidationStarter(List<String> entries, ValidationService validationService,
- CountDownLatch signal) {
- this.entries = entries;
- this.validationService = validationService;
- this.signal = signal;
- }
- /**
- * 執行緒池大小為10,初始化執行一次,隨後併發三個驗證
- */
- public static void main(String[] args) {
- ThreadPoolService threadPoolService = new ThreadPoolService(10);
- ValidationService validationService = new ValidationService(threadPoolService);
- List<String> entries = new ArrayList<String>();
- CountDownLatch signal = new CountDownLatch(3);
- long start;
- long stop;
- for (Node node : MockNodeValidator.ENTRIES) {
- entries.add(node.getWsdl());
- }
- start = System.currentTimeMillis();
- validationService.validate(entries);
- threadPoolService.execute(new ValidationStarter(entries, validationService, signal));
- threadPoolService.execute(new ValidationStarter(entries, validationService, signal));
- threadPoolService.execute(new ValidationStarter(entries, validationService, signal));
- try {
- signal.await();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- stop = System.currentTimeMillis();
- threadPoolService.destoryExecutorService(1000);
- System.out.println("實際執行驗證次數: " + MockNodeValidator.getCount());
- System.out.println("實際執行時間: " + (stop - start) + "ms");
- }
- @Override
- public void run() {
- validationService.validate(entries);
- signal.countDown();
- }
- }
=================================我是分割線==============================
本文沒有覆蓋java.util.concurrent中的所有內容,只是挑選一些比較常用的東西,想要獲得更多詳細資訊請閱讀JavaDoc。自打有了“輪子”理論,重複造大輪子的情況的確少了,但還是有人會做些小輪子,例如編寫多執行緒程式時用到的小工具(執行緒池、鎖等等),如果可以,請讓自己再“懶惰”一點吧~
轉自http://www.iteye.com/topic/363625
相關文章
- Java 隨機數探祕Java隨機
- 探祕Java9Java
- JAVA不定引數探祕(轉)Java
- 探索JAVA系列(一)探祕Java反射(Reflect)Java反射
- 【字元編碼】Java編碼格式探祕字元Java
- 探祕WKWebViewWebView
- WebSocket探祕Web
- Web列印探祕Web
- Cglib proxy探祕CGLib
- Java併發---concurrent包Java
- 探祕 vue-rx 2.0Vue
- 【NIO系列】——之TCP探祕TCP
- java.uitl.concurrent包研究JavaUI
- java.util.concurrent中文APIJavaAPI
- Vue原始碼探祕(七)(createElement)Vue原始碼
- MySQL InnoDB 儲存引擎探祕MySql儲存引擎
- JVM系列(三) - JVM物件探祕JVM物件
- PHP現代化框架探祕PHP框架
- 探祕 Mach-O 檔案Mac
- MySQL排序內部原理探祕MySql排序
- 淺談 Laravel 之探祕 SoftDeletesLaraveldelete
- Node8.0 之 Napi 探祕API
- Git探祕:深入探索(2/2)Git
- 探祕Facebook瑞典資料中心
- java.util.concurrent.RejectedExecutionExceptionJavaException
- JAVA之Concurrent包書目錄Java
- Vue原始碼探祕(九)(createComponent)Vue原始碼
- Amazon Corretto技術細節探祕
- Elastic 探祕之遺落的珍珠AST
- MySQL探祕(八):InnoDB的事務MySql
- RocketMQ 5.0 POP 消費模式探祕MQ模式
- Koa:核心探祕與入坑指北
- 探祕:谷歌又在玩哪些新創意谷歌
- 探祕WebSphere Process Server事務性WebServer
- 5.介紹java.util.concurrentJava
- 乾貨|Java Concurrent -- FutureTask 原始碼分析Java原始碼
- php 核心探祕之 PHP_FUNCTION 巨集PHPFunction
- 探祕PHP拒絕服務攻擊PHP