AQS 原理剖析
AQS即AbstractQueuedSynchronizer類稱作隊(duì)列同步器,是構(gòu)建其他同步器的一個(gè)重要的基礎(chǔ)框架,同步器自身是沒有實(shí)現(xiàn)任何同步接口。它是通過控制一個(gè)int類型的state變量來表示同步狀態(tài),使用一個(gè)內(nèi)置的FIFO(先進(jìn)先出)隊(duì)列來構(gòu)建工作隊(duì)列操作。
同步器定義有兩種資源共享方式:Exclusive(獨(dú)占式)和Share(共享式)的獲取同步狀態(tài)。
獨(dú)占式:一個(gè)時(shí)間點(diǎn)只能執(zhí)行一個(gè)線程。共享式:一個(gè)時(shí)間點(diǎn)可多個(gè)線程同時(shí)執(zhí)行。
使用方式
同步器的設(shè)計(jì)采用模板模式,要實(shí)現(xiàn)一個(gè)同步組件得先繼承AbstractQueuedSynchronizer類,通過調(diào)用同步器提供的方法和重寫同步器的方法來實(shí)現(xiàn)。
調(diào)用同步器中的方法就是調(diào)用前面提到的通過state變量值的操作來表示同步操作,state是被volatile修飾來保證線程可見性。
| 方法名 | 描述 |
|---|---|
| getState() | 獲取當(dāng)前線程同步狀態(tài)值。 |
| setState(int newState) | 設(shè)置當(dāng)前同步狀態(tài)值。 |
| compareAndSetState(int expect, int update) | 通過CAS設(shè)置state的值。 |
為了避免被重寫,以上方法都被final修飾了。
實(shí)現(xiàn)同步組件,需要自己根據(jù)自己定制化的需求進(jìn)行處理,所以需要自己重寫同步器提供的方法,要重寫的方法主要是獨(dú)占式獲取與釋放同步狀態(tài)、共享式獲取與釋放同步狀態(tài)。
tryAcquire(int arg) 獨(dú)占式獲取同步狀態(tài),返回值為boolean類型,獲取成返回true,獲取失敗返回false。
tryRelease(int arg) 獨(dú)占式釋放同步狀態(tài),返回值為boolean類型,釋放成返回true,釋放失敗返回false。
tryAcquireShared(int arg) 共享式獲取同步狀態(tài),返回值為int類型,獲取成功返回大于 0 的值。
tryReleaseShared(int arg) 共享式釋放同步狀態(tài),返回值為boolean類型,釋放成返回true,釋放失敗返回false。
isHeldExclusively() 獨(dú)占模式下是否被當(dāng)前前程獨(dú)占,返回值為boolean類型,已被當(dāng)前線程所獨(dú)占返回true,反之為false。
同步器隊(duì)列
一個(gè)同步器里面擁有一個(gè)同步隊(duì)列和多個(gè)等待隊(duì)列。
同步隊(duì)列
在AbstractQueuedSynchronizer類中,有一個(gè)內(nèi)部類Node,通過該類構(gòu)造一個(gè)內(nèi)部的同步隊(duì)列,這是一個(gè)FIFO 雙向隊(duì)列。當(dāng)前運(yùn)行線程回去同步狀態(tài)時(shí),如果獲取失敗,則將當(dāng)前線程信息創(chuàng)建一個(gè)Node追加到同步隊(duì)列尾部,然后阻塞當(dāng)前線程,直到隊(duì)列的上一個(gè)節(jié)點(diǎn)的同步狀態(tài)釋放,再喚醒當(dāng)前線程嘗試重新獲取同步狀態(tài)。這個(gè)重新獲取同步狀態(tài)操作的節(jié)點(diǎn),一定要是同步隊(duì)列中第一節(jié)點(diǎn)。
Node 源碼如下:
static final class Node {
// 共享模式下等待的標(biāo)記
static final Node SHARED = new Node();
// 獨(dú)占模式下等待的標(biāo)記
static final Node EXCLUSIVE = null;
/*
* 等待狀態(tài)常量值,以下四個(gè)常量都是
*/
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
// 等待狀態(tài)
volatile int waitStatus;
// 當(dāng)前節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)
volatile Node prev;
// 當(dāng)前節(jié)點(diǎn)的后繼節(jié)點(diǎn)
volatile Node next;
// 獲取同步狀態(tài)的線程(引用)
volatile Thread thread;
// 等待隊(duì)列中的后繼節(jié)點(diǎn)
Node nextWaiter;
// 是否共享模式
final boolean isShared() {
return nextWaiter == SHARED;
}
// 獲取前驅(qū)節(jié)點(diǎn)
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
通過以上代碼,可以看到節(jié)點(diǎn)中保存了節(jié)點(diǎn)模式、等待狀態(tài)、線程引用、前驅(qū)和后繼節(jié)點(diǎn),構(gòu)造節(jié)點(diǎn)。
同步隊(duì)列中被阻塞的線程的等待狀態(tài)包含有四個(gè)常量值:CANCELLED、SIGNAL、CONDITION、PROPAGATE ,它們對(duì)應(yīng)的被阻塞原因如下:
-
CANCELLED同步隊(duì)列中當(dāng)前節(jié)點(diǎn)的線程等待超時(shí)或被中斷,需要從同步隊(duì)列中取消等待。 -
SIGNAL當(dāng)前節(jié)點(diǎn)釋放同步狀態(tài)或被取消后,通知后繼節(jié)點(diǎn)的線程運(yùn)行。 -
CONDITION當(dāng)前節(jié)點(diǎn)在 Condition 上等待,當(dāng)其他線程對(duì) Condition 調(diào)用了 signal() 方法后,該節(jié)點(diǎn)將添加到同步隊(duì)列中。 -
PROPAGATE該狀態(tài)存在共享模式的首節(jié)點(diǎn)中,當(dāng)前節(jié)點(diǎn)喚醒后將傳播喚醒其他節(jié)點(diǎn)。
同步器中持有同步隊(duì)列的首節(jié)點(diǎn)和尾節(jié)點(diǎn)的引用,在AbstractQueuedSynchronizer中分別對(duì)應(yīng)head和tail字段。
所以同步隊(duì)列的基本結(jié)構(gòu)如圖:
等待隊(duì)列
AbstractQueuedSynchronizer類中包含一個(gè)內(nèi)部類ConditionObject,該類實(shí)現(xiàn)了Condition的接口。一個(gè)Condition對(duì)象包含一個(gè)等待隊(duì)列,同時(shí)Condition對(duì)象可以實(shí)現(xiàn)等待/通知功能。
Condition持有等待隊(duì)列中的首節(jié)點(diǎn)(firstWaiter)和尾節(jié)點(diǎn)(lastWaiter),如下圖代碼所示:
如果當(dāng)前線程調(diào)用Condition.await()時(shí),會(huì)將當(dāng)前線程信息構(gòu)建一個(gè) Node 節(jié)點(diǎn),因?yàn)?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(40, 202, 113);">Condition持有等待隊(duì)列中的首尾節(jié)點(diǎn),所以將當(dāng)前等待隊(duì)列中的尾節(jié)點(diǎn)的nextWaiter指向當(dāng)前線程構(gòu)建的節(jié)點(diǎn),同時(shí)更新lastWaiter的引用節(jié)點(diǎn)。
上述過程中的節(jié)點(diǎn)、隊(duì)列的操作,是獲取到鎖的線程來調(diào)用Condition.await()的,所以整個(gè)執(zhí)行過程在沒有基于 CAS 的情況下,也是線程安全的。
通過以上的描述,可以知道一個(gè)同步器中同步隊(duì)列、等待隊(duì)列構(gòu)成的示意圖:
當(dāng)調(diào)用Condition.await()時(shí),同步隊(duì)列中的首節(jié)點(diǎn),也就是當(dāng)前線程所創(chuàng)建的節(jié)點(diǎn),會(huì)加入到等待隊(duì)列中的尾部,釋放當(dāng)前線程的同步狀態(tài)并且喚醒同步隊(duì)列的后繼節(jié)點(diǎn),當(dāng)前線程也就進(jìn)入等待狀態(tài),這個(gè)先后順序不能顛倒。這個(gè)過程相當(dāng)于同步隊(duì)列的首節(jié)點(diǎn)的線程構(gòu)造新的節(jié)點(diǎn)加入到等待隊(duì)列的尾部。
當(dāng)調(diào)用Condition.signal()方法時(shí),會(huì)先將等待隊(duì)列中首節(jié)點(diǎn)轉(zhuǎn)移到同步隊(duì)列尾部,然后喚醒該同步隊(duì)列中的線程,該線程從Condition.await()中自旋退出,接著在在同步器的acquireQueued()中自旋獲取同步狀態(tài)。
當(dāng)調(diào)用Condition.wait()方法,同步隊(duì)列首節(jié)點(diǎn)轉(zhuǎn)移到等待隊(duì)列方法:
public final void await() throws InterruptedException {
// 如果線程已中斷,則拋出中斷異常
if (Thread.interrupted())
throw new InterruptedException();
// 添加節(jié)點(diǎn)到等待隊(duì)列中
Node node = addConditionWaiter();
// 修改 state 來達(dá)到釋放同步狀態(tài),避免死鎖
int savedState = fullyRelease(node);
int interruptMode = 0;
// 判斷當(dāng)前節(jié)點(diǎn)是否在同步隊(duì)列中
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 繼續(xù)獲取同步狀態(tài)競(jìng)爭(zhēng)
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // 清除已取消的節(jié)點(diǎn)
unlinkCancelledWaiters();
if (interruptMode != 0) // 被中斷時(shí)的處理
reportInterruptAfterWait(interruptMode);
}
上面addc方法是向等待隊(duì)列中添加一個(gè)新的節(jié)點(diǎn)。
private Node addConditionWaiter() {
// 獲取等待隊(duì)列中尾節(jié)點(diǎn)
Node t = lastWaiter;
// 如果最后一個(gè)節(jié)點(diǎn)已取消,則清除取消節(jié)點(diǎn)
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
// 利用當(dāng)前線程信息創(chuàng)建等待隊(duì)列的節(jié)點(diǎn)
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null) // 如果最后尾節(jié)點(diǎn)為空,當(dāng)前節(jié)點(diǎn)則為等待隊(duì)列的首節(jié)點(diǎn)
firstWaiter = node;
else // 否則將當(dāng)前尾節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)指向當(dāng)前線程信息所構(gòu)造的節(jié)點(diǎn)
t.nextWaiter = node;
lastWaiter = node; // 更新 Condition 的尾節(jié)點(diǎn)引用
return node;
}
當(dāng)調(diào)用Condition.signal()方法,等待隊(duì)列首節(jié)點(diǎn)轉(zhuǎn)移到同步隊(duì)列方法:
public final void signal() {
// 是否被當(dāng)前線程所獨(dú)占
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 獲取等待隊(duì)列中首節(jié)點(diǎn)
Node first = firstWaiter;
if (first != null)
// 轉(zhuǎn)移到同步隊(duì)列,然后喚醒該節(jié)點(diǎn)
doSignal(first);
}
轉(zhuǎn)移同步隊(duì)列首節(jié)點(diǎn)到同步隊(duì)列,并喚醒該節(jié)點(diǎn)方法doSignal()
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
// 去除首節(jié)點(diǎn)
first.nextWaiter = null;
} while (!transferForSignal(first) && // 從等待隊(duì)列中轉(zhuǎn)移到同步隊(duì)列
(first = firstWaiter) != null);
}
轉(zhuǎn)移等待隊(duì)列到同步隊(duì)列方法transferForSignal(Node node)
final boolean transferForSignal(Node node) {
// 驗(yàn)證節(jié)點(diǎn)是否被取消
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 轉(zhuǎn)移節(jié)點(diǎn)至同步隊(duì)列
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
等待隊(duì)列中的頭結(jié)點(diǎn)線程安全移動(dòng)到同步隊(duì)列方法enq(final Node node)
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 同步隊(duì)列中如果為空,則初始化同步器
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 否則新節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)為當(dāng)前同步隊(duì)列的尾節(jié)點(diǎn)
node.prev = t;
// 設(shè)置當(dāng)前新節(jié)點(diǎn)為同步隊(duì)列的尾節(jié)點(diǎn),并更新先前同步隊(duì)列的尾節(jié)點(diǎn)的后繼節(jié)點(diǎn)指向當(dāng)前新節(jié)點(diǎn)
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
獨(dú)占式同步狀態(tài)
獨(dú)占式同步狀態(tài)獲取和釋放是線程安全的操作,一個(gè)時(shí)間點(diǎn)確保只有一個(gè)線程獲取到同步狀態(tài)。
獨(dú)占式同步狀態(tài)獲取
acquire(int arg)方法是獲取獨(dú)占式同步狀態(tài)的方法,當(dāng)線程獲取同步失敗時(shí),會(huì)加入到同步隊(duì)列中。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
上述代碼中,當(dāng)執(zhí)行tryAcquire(int arg)方法獲取同步狀態(tài)失敗時(shí),接著通過addWaiter(Node.EXCLUSIVE)構(gòu)造當(dāng)前線程信息的節(jié)點(diǎn),隨后將新構(gòu)造的節(jié)點(diǎn)通過acquireQueued(final Node node, int arg)方法加入到同步隊(duì)列中,節(jié)點(diǎn)在同步隊(duì)列中自旋等待獲取同步狀態(tài)。
tryAcquire(int arg)是自定義同步器實(shí)現(xiàn)的,實(shí)現(xiàn)該方法需要保證線程安全獲取同步狀態(tài),前面講到AQS提供的compareAndSetState(int expect, int update)方法通過CAS設(shè)置state值來保證線程安全。
上面獲取獨(dú)占式同步狀態(tài)時(shí),主要分析acquireQueued(final Node node, int arg)方法,節(jié)點(diǎn)加入隊(duì)列并自旋等待。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
// 是否中斷標(biāo)識(shí)
boolean interrupted = false;
for (;;) {
// 獲取當(dāng)前節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)
final Node p = node.predecessor();
// 如果前驅(qū)節(jié)點(diǎn)是首節(jié)點(diǎn),并且當(dāng)前節(jié)點(diǎn)獲取到同步狀態(tài)
if (p == head && tryAcquire(arg)) {
// 將當(dāng)前節(jié)點(diǎn)設(shè)置為首節(jié)點(diǎn)
setHead(node);
// 將原首節(jié)點(diǎn)(即當(dāng)前節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn))引用清空,利于 GC 回收
p.next = null;
// 成功獲取到同步狀態(tài)標(biāo)志
failed = false;
return interrupted;
}
// 判斷前驅(qū)節(jié)點(diǎn)是否超時(shí)或取消,以及當(dāng)前線程是否被中斷
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 如果被中斷,則節(jié)點(diǎn)出隊(duì)
if (failed)
cancelAcquire(node);
}
}
在首節(jié)點(diǎn)釋放同步狀態(tài)后,同時(shí)喚醒后繼節(jié)點(diǎn)。后繼節(jié)點(diǎn)通過自旋的方式(這里利用死循環(huán)方式)也會(huì)檢查自己的前驅(qū)節(jié)點(diǎn)是否為首節(jié)點(diǎn),如果是前驅(qū)節(jié)點(diǎn)則會(huì)嘗試獲取同步狀態(tài)。獲取成功則返回,否則判斷是否被中斷或者繼續(xù)自旋上述獲取同步狀態(tài)操作。
獨(dú)占式同步狀態(tài)釋放
release(int arg)方法是釋放同步狀態(tài),當(dāng)釋放同步狀態(tài)后會(huì)喚醒后繼節(jié)點(diǎn)。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
tryRelease(int arg)方法同樣也是自定義同步器實(shí)現(xiàn)。當(dāng)首節(jié)點(diǎn)不為空且處于等待狀態(tài)時(shí),那么調(diào)用unparkSuccessor(Node node)方法喚醒后繼節(jié)點(diǎn)。
private void unparkSuccessor(Node node) {
// CAS 設(shè)置等待狀態(tài)為初始狀態(tài)
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
// 如果當(dāng)前釋放同步狀態(tài)的節(jié)點(diǎn)不存在后繼節(jié)點(diǎn)或后繼節(jié)點(diǎn)超時(shí)/被中斷
if (s == null || s.waitStatus > 0) {
s = null;
// 從尾節(jié)點(diǎn)中開始尋找等待狀態(tài)的節(jié)點(diǎn)作為新首節(jié)點(diǎn),這里已排除當(dāng)前節(jié)點(diǎn)(t != node)
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
釋放同步狀態(tài)的整個(gè)過程就是:釋放同步狀態(tài),喚醒后繼節(jié)點(diǎn)。這個(gè)后繼節(jié)點(diǎn)必須滿足,非空、非當(dāng)前節(jié)點(diǎn)、等待狀態(tài)小于或等于 0 ,即SIGNAL、CONDITION、PROPAGATE和初始化狀態(tài)。
獨(dú)占式資源共享方式除了上面的同步狀態(tài)獲取,還有獨(dú)占式超時(shí)獲取使用的方法是doAcquireNanos(int arg, long nanosTimeout)、獨(dú)占式可中斷獲取使用的方法是acquireInterruptibly(int arg)。
共享式同步狀態(tài)
共享式同步狀態(tài)同一時(shí)間點(diǎn)可以有多個(gè)線程獲取到同步狀態(tài)。
共享式同步狀態(tài)獲取
acquireShared(int arg)方法是共享式同步狀態(tài)獲取的方法。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
// 獲取同步狀態(tài)失敗后調(diào)用的方法
doAcquireShared(arg);
}
tryAcquireShared(int arg)方法是自定義同步器實(shí)現(xiàn)的,返回大于或等于 0 時(shí),表示獲取成功。如果小于 0 時(shí),獲取同步狀態(tài)失敗后會(huì)調(diào)用doAcquireShared(int arg)方法進(jìn)行再次嘗試獲取。
private void doAcquireShared(int arg) {、
// 構(gòu)造一個(gè)當(dāng)前線程信息的新節(jié)點(diǎn)
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
// 自旋式獲取同步狀態(tài)
for (;;) {
final Node p = node.predecessor();
// 判斷新節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)是否為首節(jié)點(diǎn)
if (p == head) {
// 再次嘗試獲取同步狀態(tài)
int r = tryAcquireShared(arg);
// 獲取成功后退出自旋
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
上面代碼中,當(dāng)獲取同步狀態(tài)失敗后,則創(chuàng)建一個(gè)共享模式類型的節(jié)點(diǎn),然后自旋式獲取同步狀態(tài),如果前驅(qū)節(jié)點(diǎn)為首節(jié)點(diǎn)時(shí)則嘗試再次獲取同步狀態(tài),獲取同步狀態(tài)成功后退出當(dāng)前自旋。
共享式釋放同步狀態(tài)
releaseShared(int arg)方法來釋放共享式同步狀態(tài)。
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
// 同步狀態(tài)釋放成功后,喚醒后面等待狀態(tài)的節(jié)點(diǎn)
doReleaseShared();
return true;
}
return false;
}
上面tryReleaseShared(int arg)釋放同步狀態(tài)方法必須保證線程安全,因?yàn)樗鄠€(gè)線程獲取到同步狀態(tài)時(shí)會(huì)引發(fā)并發(fā)操作,可以通過循環(huán)操作和 CAS 來確保安前行。
doReleaseShared()方法喚醒后續(xù)等待狀態(tài)的節(jié)點(diǎn)。
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
// 驗(yàn)證后繼節(jié)點(diǎn)的線程處于等待狀態(tài)
if (ws == Node.SIGNAL) {
// 再次檢查后繼節(jié)點(diǎn)的線程是否處于等待狀態(tài)
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
// 喚醒后繼節(jié)點(diǎn),這時(shí)每喚醒一次就更新一次首節(jié)點(diǎn)
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
共享同步狀態(tài)釋放后,自旋式依次喚醒隊(duì)列中節(jié)點(diǎn)。
總結(jié)
從 AQS 中可以借鑒它利用循環(huán)和 CAS 來確保并發(fā)的安全性的思路,同時(shí)它采用模板設(shè)計(jì)模式定義一個(gè)處理邏輯,將具體的特定處理邏輯交由子類自定義實(shí)現(xiàn)。在 ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch 以及 Tomncat 的 LimitLatch 都有用其作為同步器。
推薦閱讀
《Java 多線程中使用 JDK 自帶工具類實(shí)現(xiàn)計(jì)數(shù)器》
《Java 線程通信之 wait/notify 機(jī)制》
《你必須會(huì)的 JDK 動(dòng)態(tài)代理和 CGLIB 動(dòng)態(tài)代理》
關(guān)注【ytao】,更多原創(chuàng)好文
