- 你有一個思想,我有一個思想,我們交換後,一個人就有兩個思想
- If you can NOT explain it simply, you do NOT understand it well enough
前言
建立執行緒有幾種方式?這個問題的答案應該是可以脫口而出的吧
- 繼承 Thread 類
- 實現 Runnable 介面
但這兩種方式建立的執行緒是屬於”三wu產品“:
- 沒有引數
- 沒有返回值
- 沒辦法丟擲異常
class MyThread implements Runnable{
@Override
public void run() {
log.info("my thread");
}
}
Runnable 介面是 JDK1.0 的核心產物
/**
* @since JDK1.0
*/
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
用著 “三wu產品” 總是有一些弊端,其中沒辦法拿到返回值是最讓人不能忍的,於是 Callable 就誕生了
Callable
又是 Doug Lea 大師,又是 Java 1.5 這個神奇的版本
/**
* @see Executor
* @since 1.5
* @author Doug Lea
* @param <V> the result type of method {@code call}
*/
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
Callable 是一個泛型介面,裡面只有一個 call()
方法,該方法可以返回泛型值 V ,使用起來就像這樣:
Callable<String> callable = () -> {
// Perform some computation
Thread.sleep(2000);
return "Return some result";
};
二者都是函式式介面,裡面都僅有一個方法,使用上又是如此相似,除了有無返回值,Runnable 與 Callable 就點差別嗎?
Runnable VS Callable
兩個介面都是用於多執行緒執行任務的,但他們還是有很明顯的差別的
執行機制
先從執行機制上來看,Runnable 你太清楚了,它既可以用在 Thread 類中,也可以用在 ExecutorService 類中配合執行緒池的使用;Bu~~~~t, Callable 只能在 ExecutorService 中使用,你翻遍 Thread 類,也找不到Callable 的身影
異常處理
Runnable 介面中的 run 方法簽名上沒有 throws ,自然也就沒辦法向上傳播受檢異常;而 Callable 的 call() 方法簽名卻有 throws,所以它可以處理受檢異常;
所以歸納起來看主要有這幾處不同點:
整體差別雖然不大,但是這點差別,卻具有重大意義
返回值和處理異常很好理解,另外,在實際工作中,我們通常要使用執行緒池來管理執行緒(原因已經在 為什麼要使用執行緒池? 中明確說明),所以我們就來看看 ExecutorService 中是如何使用二者的
ExecutorService
先來看一下 ExecutorService 類圖
我將上圖示記的方法單獨放在此處
void execute(Runnable command);
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
可以看到,使用ExecutorService 的 execute()
方法依舊得不到返回值,而 submit()
方法清一色的返回 Future
型別的返回值
細心的朋友可能已經發現, submit() 方法已經在 CountDownLatch 和 CyclicBarrier 傻傻的分不清楚? 文章中多次使用了,只不過我們沒有獲取其返回值罷了,那麼
- Future 到底是什麼呢?
- 怎麼通過它獲取返回值呢?
我們帶著這些疑問一點點來看
Future
Future 又是一個介面,裡面只有五個方法:
從方法名稱上相信你已經能看出這些方法的作用
// 取消任務
boolean cancel(boolean mayInterruptIfRunning);
// 獲取任務執行結果
V get() throws InterruptedException, ExecutionException;
// 獲取任務執行結果,帶有超時時間限制
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
// 判斷任務是否已經取消
boolean isCancelled();
// 判斷任務是否已經結束
boolean isDone();
鋪墊了這麼多,看到這你也許有些亂了,我們們趕緊看一個例子,演示一下幾個方法的作用
@Slf4j
public class FutureAndCallableExample {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executorService = Executors.newSingleThreadExecutor();
// 使用 Callable ,可以獲取返回值
Callable<String> callable = () -> {
log.info("進入 Callable 的 call 方法");
// 模擬子執行緒任務,在此睡眠 2s,
// 小細節:由於 call 方法會丟擲 Exception,這裡不用像使用 Runnable 的run 方法那樣 try/catch 了
Thread.sleep(5000);
return "Hello from Callable";
};
log.info("提交 Callable 到執行緒池");
Future<String> future = executorService.submit(callable);
log.info("主執行緒繼續執行");
log.info("主執行緒等待獲取 Future 結果");
// Future.get() blocks until the result is available
String result = future.get();
log.info("主執行緒獲取到 Future 結果: {}", result);
executorService.shutdown();
}
}
程式執行結果如下:
如果你執行上述示例程式碼,主執行緒呼叫 future.get() 方法會阻塞自己,直到子任務完成。我們也可以使用 Future 方法提供的 isDone
方法,它可以用來檢查 task 是否已經完成了,我們將上面程式做點小修改:
// 如果子執行緒沒有結束,則睡眠 1s 重新檢查
while(!future.isDone()) {
System.out.println("Task is still not done...");
Thread.sleep(1000);
}
來看執行結果:
如果子程式執行時間過長,或者其他原因,我們想 cancel 子程式的執行,則我們可以使用 Future 提供的 cancel 方法,繼續對程式做一些修改
while(!future.isDone()) {
System.out.println("子執行緒任務還沒有結束...");
Thread.sleep(1000);
double elapsedTimeInSec = (System.nanoTime() - startTime)/1000000000.0;
// 如果程式執行時間大於 1s,則取消子執行緒的執行
if(elapsedTimeInSec > 1) {
future.cancel(true);
}
}
來看執行結果:
為什麼呼叫 cancel 方法程式會出現 CancellationException 呢? 是因為呼叫 get() 方法時,明確說明了:
呼叫 get() 方法時,如果計算結果被取消了,則丟擲 CancellationException (具體原因,你會在下面的原始碼分析中看到)
有異常不處理是非常不專業的,所以我們需要進一步修改程式,以更友好的方式處理異常
// 通過 isCancelled 方法判斷程式是否被取消,如果被取消,則列印日誌,如果沒被取消,則正常呼叫 get() 方法
if (!future.isCancelled()){
log.info("子執行緒任務已完成");
String result = future.get();
log.info("主執行緒獲取到 Future 結果: {}", result);
}else {
log.warn("子執行緒任務被取消");
}
檢視程式執行結果:
相信到這裡你已經對 Future
的幾個方法有了基本的使用印象,但 Future
是介面,其實使用 ExecutorService.submit()
方法返回的一直都是 Future
的實現類 FutureTask
接下來我們就進入這個核心實現類一探究竟
FutureTask
同樣先來看類結構
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
很神奇的一個介面,FutureTask
實現了 RunnableFuture
介面,而 RunnableFuture
介面又分別實現了 Runnable
和 Future
介面,所以可以推斷出 FutureTask
具有這兩種介面的特性:
- 有
Runnable
特性,所以可以用在ExecutorService
中配合執行緒池使用 - 有
Future
特性,所以可以從中獲取到執行結果
FutureTask原始碼分析
如果你完整的看過 AQS 相關分析的文章,你也許會發現,閱讀 Java 併發工具類原始碼,我們無非就是要關注以下這三點:
- 狀態 (程式碼邏輯的主要控制)
- 佇列 (等待排隊佇列)
- CAS (安全的set 值)
腦海中牢記這三點,我們們開始看 FutureTask 原始碼,看一下它是如何圍繞這三點實現相應的邏輯的
文章開頭已經提到,實現 Runnable 介面形式建立的執行緒並不能獲取到返回值,而實現 Callable 的才可以,所以 FutureTask 想要獲取返回值,必定是和 Callable 有聯絡的,這個推斷一點都沒錯,從構造方法中就可以看出來:
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
即便在 FutureTask 構造方法中傳入的是 Runnable 形式的執行緒,該構造方法也會通過 Executors.callable
工廠方法將其轉換為 Callable 型別:
public FutureTask(Runnable runnable, V result) {
this.callable = Executors.callable(runnable, result);
this.state = NEW; // ensure visibility of callable
}
但是 FutureTask 實現的是 Runnable 介面,也就是隻能重寫 run() 方法,run() 方法又沒有返回值,那問題來了:
- FutureTask 是怎樣在 run() 方法中獲取返回值的?
- 它將返回值放到哪裡了?
- get() 方法又是怎樣拿到這個返回值的呢?
我們來看一下 run() 方法(關鍵程式碼都已標記註釋)
public void run() {
// 如果狀態不是 NEW,說明任務已經執行過或者已經被取消,直接返回
// 如果狀態是 NEW,則嘗試把執行執行緒儲存在 runnerOffset(runner欄位),如果賦值失敗,則直接返回
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
// 獲取建構函式傳入的 Callable 值
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
// 正常呼叫 Callable 的 call 方法就可以獲取到返回值
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
// 儲存 call 方法丟擲的異常
setException(ex);
}
if (ran)
// 儲存 call 方法的執行結果
set(result);
}
} finally {
runner = null;
int s = state;
// 如果任務被中斷,則執行中斷處理
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
run()
方法沒有返回值,至於 run()
方法是如何將 call()
方法的返回結果和異常都儲存起來的呢?其實非常簡單, 就是通過 set(result) 儲存正常程式執行結果,或通過 setException(ex) 儲存程式異常資訊
/** The result to return or exception to throw from get() */
private Object outcome; // non-volatile, protected by state reads/writes
// 儲存異常結果
protected void setException(Throwable t) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = t;
UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
finishCompletion();
}
}
// 儲存正常結果
protected void set(V v) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = v;
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
finishCompletion();
}
}
setException
和 set
方法非常相似,都是將異常或者結果儲存在 Object
型別的 outcome
變數中,outcome
是成員變數,就要考慮執行緒安全,所以他們要通過 CAS方式設定 outcome 變數的值,既然是在 CAS 成功後 更改 outcome 的值,這也就是 outcome 沒有被 volatile
修飾的原因所在。
儲存正常結果值(set方法)與儲存異常結果值(setException方法)兩個方法程式碼邏輯,唯一的不同就是 CAS 傳入的 state 不同。我們上面提到,state 多數用於控制程式碼邏輯,FutureTask 也是這樣,所以要搞清程式碼邏輯,我們需要先對 state 的狀態變化有所瞭解
/*
*
* Possible state transitions:
* NEW -> COMPLETING -> NORMAL //執行過程順利完成
* NEW -> COMPLETING -> EXCEPTIONAL //執行過程出現異常
* NEW -> CANCELLED // 執行過程中被取消
* NEW -> INTERRUPTING -> INTERRUPTED //執行過程中,執行緒被中斷
*/
private volatile int state;
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;
7種狀態,千萬別慌,整個狀態流轉其實只有四種線路
FutureTask 物件被建立出來,state 的狀態就是 NEW 狀態,從上面的建構函式中你應該已經發現了,四個最終狀態 NORMAL ,EXCEPTIONAL , CANCELLED , INTERRUPTED 也都很好理解,兩個中間狀態稍稍有點讓人困惑:
- COMPLETING: outcome 正在被set 值的時候
- INTERRUPTING:通過 cancel(true) 方法正在中斷執行緒的時候
總的來說,這兩個中間狀態都表示一種瞬時狀態,我們將幾種狀態圖形化展示一下:
我們知道了 run() 方法是如何儲存結果的,以及知道了將正常結果/異常結果儲存到了 outcome 變數裡,那就需要看一下 FutureTask 是如何通過 get() 方法獲取結果的:
public V get() throws InterruptedException, ExecutionException {
int s = state;
// 如果 state 還沒到 set outcome 結果的時候,則呼叫 awaitDone() 方法阻塞自己
if (s <= COMPLETING)
s = awaitDone(false, 0L);
// 返回結果
return report(s);
}
awaitDone 方法是 FutureTask 最核心的一個方法
// get 方法支援超時限制,如果沒有傳入超時時間,則接受的引數是 false 和 0L
// 有等待就會有佇列排隊或者可響應中斷,從方法簽名上看有 InterruptedException,說明該方法這是可以被中斷的
private int awaitDone(boolean timed, long nanos)
throws InterruptedException {
// 計算等待截止時間
final long deadline = timed ? System.nanoTime() + nanos : 0L;
WaitNode q = null;
boolean queued = false;
for (;;) {
// 如果當前執行緒被中斷,如果是,則在等待對立中刪除該節點,並丟擲 InterruptedException
if (Thread.interrupted()) {
removeWaiter(q);
throw new InterruptedException();
}
int s = state;
// 狀態大於 COMPLETING 說明已經達到某個最終狀態(正常結束/異常結束/取消)
// 把 thread 只為空,並返回結果
if (s > COMPLETING) {
if (q != null)
q.thread = null;
return s;
}
// 如果是COMPLETING 狀態(中間狀態),表示任務已結束,但 outcome 賦值還沒結束,這時主動讓出執行權,讓其他執行緒優先執行(只是發出這個訊號,至於是否別的執行緒執行一定會執行可是不一定的)
else if (s == COMPLETING) // cannot time out yet
Thread.yield();
// 等待節點為空
else if (q == null)
// 將當前執行緒構造節點
q = new WaitNode();
// 如果還沒有入佇列,則把當前節點加入waiters首節點並替換原來waiters
else if (!queued)
queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
q.next = waiters, q);
// 如果設定超時時間
else if (timed) {
nanos = deadline - System.nanoTime();
// 時間到,則不再等待結果
if (nanos <= 0L) {
removeWaiter(q);
return state;
}
// 阻塞等待特定時間
LockSupport.parkNanos(this, nanos);
}
else
// 掛起當前執行緒,知道被其他執行緒喚醒
LockSupport.park(this);
}
}
總的來說,進入這個方法,通常會經歷三輪迴圈
- 第一輪for迴圈,執行的邏輯是
q == null
, 這時候會新建一個節點 q, 第一輪迴圈結束。 - 第二輪for迴圈,執行的邏輯是
!queue
,這個時候會把第一輪迴圈中生成的節點的 next 指標指向waiters,然後CAS的把節點q 替換waiters, 也就是把新生成的節點新增到waiters 中的首節點。如果替換成功,queued=true。第二輪迴圈結束。 - 第三輪for迴圈,進行阻塞等待。要麼阻塞特定時間,要麼一直阻塞知道被其他執行緒喚醒。
對於第二輪迴圈,大家可能稍稍有點迷糊,我們前面說過,有阻塞,就會排隊,有排隊自然就有佇列,FutureTask 內部同樣維護了一個佇列
/** Treiber stack of waiting threads */
private volatile WaitNode waiters;
說是等待佇列,其實就是一個 Treiber 型別 stack,既然是 stack, 那就像手槍的彈夾一樣(腦補一下子彈放入彈夾的情形),後進先出,所以剛剛說的第二輪迴圈,會把新生成的節點新增到 waiters stack 的首節點
如果程式執行正常,通常呼叫 get() 方法,會將當前執行緒掛起,那誰來喚醒呢?自然是 run() 方法執行完會喚醒,設定返回結果(set方法)/異常的方法(setException方法) 兩個方法中都會呼叫 finishCompletion 方法,該方法就會喚醒等待佇列中的執行緒
private void finishCompletion() {
// assert state > COMPLETING;
for (WaitNode q; (q = waiters) != null;) {
if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
for (;;) {
Thread t = q.thread;
if (t != null) {
q.thread = null;
// 喚醒等待佇列中的執行緒
LockSupport.unpark(t);
}
WaitNode next = q.next;
if (next == null)
break;
q.next = null; // unlink to help gc
q = next;
}
break;
}
}
done();
callable = null; // to reduce footprint
}
將一個任務的狀態設定成終止態只有三種方法:
- set
- setException
- cancel
前兩種方法已經分析完,接下來我們就看一下 cancel
方法
檢視 Future cancel(),該方法註釋上明確說明三種 cancel 操作一定失敗的情形
- 任務已經執行完成了
- 任務已經被取消過了
- 任務因為某種原因不能被取消
其它情況下,cancel操作將返回true。值得注意的是,cancel操作返回 true 並不代表任務真的就是被取消, 這取決於發動cancel狀態時,任務所處的狀態
- 如果發起cancel時任務還沒有開始執行,則隨後任務就不會被執行;
-
如果發起cancel時任務已經在執行了,則這時就需要看
mayInterruptIfRunning
引數了:- 如果mayInterruptIfRunning 為true, 則當前在執行的任務會被中斷
- 如果mayInterruptIfRunning 為false, 則可以允許正在執行的任務繼續執行,直到它執行完
有了這些鋪墊,看一下 cancel 程式碼的邏輯就秒懂了
public boolean cancel(boolean mayInterruptIfRunning) {
if (!(state == NEW &&
UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
return false;
try { // in case call to interrupt throws exception
// 需要中斷任務執行執行緒
if (mayInterruptIfRunning) {
try {
Thread t = runner;
// 中斷執行緒
if (t != null)
t.interrupt();
} finally { // final state
// 修改為最終狀態 INTERRUPTED
UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
}
}
} finally {
// 喚醒等待中的執行緒
finishCompletion();
}
return true;
}
核心方法終於分析完了,到這我們們喝口茶休息一下吧
我是想說,使用 FutureTask 來演練燒水泡茶經典程式
如上圖:
- 洗水壺 1 分鐘
- 燒開水 15 分鐘
- 洗茶壺 1 分鐘
- 洗茶杯 1 分鐘
- 拿茶葉 2 分鐘
最終泡茶
讓我心算一下,如果序列總共需要 20 分鐘,但很顯然在燒開水期間,我們可以洗茶壺/洗茶杯/拿茶葉
這樣總共需要 16 分鐘,節約了 4分鐘時間,燒水泡茶尚且如此,在現在高併發的時代,4分鐘可以做的事太多了,學會使用 Future 優化程式是必然(其實優化程式就是尋找關鍵路徑,關鍵路徑找到了,非關鍵路徑的任務通常就可以和關鍵路徑的內容並行執行了)
@Slf4j
public class MakeTeaExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(2);
// 建立執行緒1的FutureTask
FutureTask<String> ft1 = new FutureTask<String>(new T1Task());
// 建立執行緒2的FutureTask
FutureTask<String> ft2 = new FutureTask<String>(new T2Task());
executorService.submit(ft1);
executorService.submit(ft2);
log.info(ft1.get() + ft2.get());
log.info("開始泡茶");
executorService.shutdown();
}
static class T1Task implements Callable<String> {
@Override
public String call() throws Exception {
log.info("T1:洗水壺...");
TimeUnit.SECONDS.sleep(1);
log.info("T1:燒開水...");
TimeUnit.SECONDS.sleep(15);
return "T1:開水已備好";
}
}
static class T2Task implements Callable<String> {
@Override
public String call() throws Exception {
log.info("T2:洗茶壺...");
TimeUnit.SECONDS.sleep(1);
log.info("T2:洗茶杯...");
TimeUnit.SECONDS.sleep(2);
log.info("T2:拿茶葉...");
TimeUnit.SECONDS.sleep(1);
return "T2:福鼎白茶拿到了";
}
}
}
上面的程式是主執行緒等待兩個 FutureTask 的執行結果,執行緒1 燒開水時間更長,執行緒1希望在水燒開的那一剎那就可以拿到茶葉直接泡茶,怎麼半呢?
那隻需要線上程 1 的FutureTask 中獲取 執行緒 2 FutureTask 的返回結果就可以了,我們稍稍修改一下程式:
@Slf4j
public class MakeTeaExample1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(2);
// 建立執行緒2的FutureTask
FutureTask<String> ft2 = new FutureTask<String>(new T2Task());
// 建立執行緒1的FutureTask
FutureTask<String> ft1 = new FutureTask<String>(new T1Task(ft2));
executorService.submit(ft1);
executorService.submit(ft2);
executorService.shutdown();
}
static class T1Task implements Callable<String> {
private FutureTask<String> ft2;
public T1Task(FutureTask<String> ft2) {
this.ft2 = ft2;
}
@Override
public String call() throws Exception {
log.info("T1:洗水壺...");
TimeUnit.SECONDS.sleep(1);
log.info("T1:燒開水...");
TimeUnit.SECONDS.sleep(15);
String t2Result = ft2.get();
log.info("T1 拿到T2的 {}, 開始泡茶", t2Result);
return "T1: 上茶!!!";
}
}
static class T2Task implements Callable<String> {
@Override
public String call() throws Exception {
log.info("T2:洗茶壺...");
TimeUnit.SECONDS.sleep(1);
log.info("T2:洗茶杯...");
TimeUnit.SECONDS.sleep(2);
log.info("T2:拿茶葉...");
TimeUnit.SECONDS.sleep(1);
return "福鼎白茶";
}
}
}
來看程式執行結果:
知道這個變化後我們再回頭看 ExecutorService 的三個 submit 方法:
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
<T> Future<T> submit(Callable<T> task);
第一種方法,逐層程式碼檢視到這裡:
你會發現,和我們改造燒水泡茶的程式思維是相似的,可以傳進去一個 result,result 相當於主執行緒和子執行緒之間的橋樑,通過它主子執行緒可以共享資料
第二個方法引數是 Runnable 型別引數,即便呼叫 get() 方法也是返回 null,所以僅是可以用來斷言任務已經結束了,類似 Thread.join()
第三個方法引數是 Callable 型別引數,通過get() 方法可以明確獲取 call() 方法的返回值
到這裡,關於 Future 的整塊講解就結束了,還是需要簡單消化一下的
總結
如果熟悉 Javascript 的朋友,Future 的特性和 Javascript 的 Promise 是類似的,私下開玩笑通常將其比喻成男朋友的承諾
迴歸到Java,我們從 JDK 的演變歷史,談及 Callable 的誕生,它彌補了 Runnable 沒有返回值的空缺,通過簡單的 demo 瞭解 Callable 與 Future 的使用。 FutureTask 又是 Future介面的核心實現類,通過閱讀原始碼瞭解了整個實現邏輯,最後結合FutureTask 和執行緒池演示燒水泡茶程式,相信到這裡,你已經可以輕鬆獲取執行緒結果了
燒水泡茶是非常簡單的,如果更復雜業務邏輯,以這種方式使用 Future 必定會帶來很大的會亂(程式結束沒辦法主動通知,Future 的連結和整合都需要手動操作)為了解決這個短板,沒錯,又是那個男人 Doug Lea, CompletableFuture
工具類在 Java1.8 的版本出現了,搭配 Lambda 的使用,讓我們編寫非同步程式也像寫序列程式碼那樣簡單,縱享絲滑
接下來我們就瞭解一下 CompletableFuture
的使用
靈魂追問
- 你在日常開發工作中是怎樣將整塊任務做到分工與協作的呢?有什麼基本準則嗎?
- 如何批量的執行非同步任務呢?
參考
- Java 併發程式設計實戰
- Java 併發程式設計的藝術
- Java 併發程式設計之美
日拱一兵 | 原創