AQS介紹和原理分析(下)

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"概述","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文首先用導圖總結一下上一篇文檔寫的內容,然後通過Mutex,ReentrantLock說明如何使用AQS,同時關注公平和非公平這個條件,最後關注一下可中斷和條件這兩個特性。上一篇的總結:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/df/df91b5b055172a3bf3cb6121de43d2ec.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"Mutex&ReentrantLock&公平/非公平","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在jdk8的current包中並沒有找到Doug Lea的Mutex類,不過這個不影響,就當我們自己寫的也一樣。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"class Mutex implements Lock, java.io.Serializable {\n // 自定義同步器\n private static class Sync extends AbstractQueuedSynchronizer {\n // 判斷是否鎖定狀態\n protected boolean isHeldExclusively() {\n return getState() == 1;\n }\n\n // 嘗試獲取資源,立即返回。成功則返回true,否則false。\n public boolean tryAcquire(int acquires) {\n assert acquires == 1; // 這裏限定只能爲1個量\n if (compareAndSetState(0, 1)) {//state爲0才設置爲1,不可重入!\n setExclusiveOwnerThread(Thread.currentThread());//設置爲當前線程獨佔資源\n return true;\n }\n return false;\n }\n\n // 嘗試釋放資源,立即返回。成功則爲true,否則false。\n protected boolean tryRelease(int releases) {\n assert releases == 1; // 限定爲1個量\n if (getState() == 0)//既然來釋放,那肯定就是已佔有狀態了。只是爲了保險,多層判斷!\n throw new IllegalMonitorStateException();\n setExclusiveOwnerThread(null);\n setState(0);//釋放資源,放棄佔有狀態\n return true;\n }\n }\n\n // 真正同步類的實現都依賴繼承於AQS的自定義同步器!\n private final Sync sync = new Sync();\n\n //lockacquire。兩者語義一樣:獲取資源,即便等待,直到成功才返回。\n public void lock() {\n sync.acquire(1);\n }\n\n //tryLocktryAcquire。兩者語義一樣:嘗試獲取資源,要求立即返回。成功則爲true,失敗則爲false。\n public boolean tryLock() {\n return sync.tryAcquire(1);\n }\n\n //unlockrelease。兩者語義一樣:釋放資源。\n public void unlock() {\n sync.release(1);\n }\n\n //鎖是否佔有狀態\n public boolean isLocked() {\n return sync.isHeldExclusively();\n }\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"讀過AQS代碼前面的註釋,就會了解這種實現就是註釋裏面建議的AbstractQueuedSynchronizer使用方式。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過內部類Sync繼承AbstractQueuedSynchronizer,通過實現tryAcquire()/tryRelease()或者ryAcquireShared()/tryReleaseShared()方法實現獨佔/共享方式獲取鎖。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"定義私有變量sync,提供外部接口lock()/unlock()。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來再來看一下ReentrantLock(主要關注它關於公平和非公平的特性)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ReentrantLock把所有Lock接口的操作都委派到一個Sync類上,該類繼承了AbstractQueuedSynchronizer:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"static abstract class Sync extends AbstractQueuedSynchronizer\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Sync又有兩個子類:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"final static class NonfairSync extends Sync\nfinal static class FairSync extends Sync \n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"顯然是爲了支持公平鎖和非公平鎖而定義,默認情況下爲非公平鎖","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/c6/c6033f0bd8f625a74b3726438a4b8f2c.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"非公平鎖","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"static final class NonfairSync extends Sync {\n private static final long serialVersionUID = 7316153563782823691L;\n\n /**\n * Performs lock. Try immediate barge, backing up to normal\n * acquire on failure.\n */\n final void lock() {\n if (compareAndSetState(0, 1))\n setExclusiveOwnerThread(Thread.currentThread());\n else\n acquire(1);\n }\n\n protected final boolean tryAcquire(int acquires) {\n return nonfairTryAcquire(acquires);\n }\n }\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"公平鎖","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"static final class FairSync extends Sync {\n final void lock() {\n acquire(1);\n }\n protected final boolean tryAcquire(int acquires) {\n final Thread current = Thread.currentThread();\n int c = getState();\n if (c == 0) {\n if (!hasQueuedPredecessors() &&\n compareAndSetState(0, acquires)) {\n setExclusiveOwnerThread(current);\n return true;\n }\n }\n else if (current == getExclusiveOwnerThread()) {\n int nextc = c + acquires;\n if (nextc < 0)\n throw new Error(\"Maximum lock count exceeded\");\n setState(nextc);\n return true;\n }\n return false;\n }\n }\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"共同的Sync","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"abstract static class Sync extends AbstractQueuedSynchronizer {\n \n final boolean nonfairTryAcquire(int acquires) {\n final Thread current = Thread.currentThread();\n int c = getState();\n if (c == 0) {\n if (compareAndSetState(0, acquires)) {\n setExclusiveOwnerThread(current);\n return true;\n }\n }\n else if (current == getExclusiveOwnerThread()) {\n int nextc = c + acquires;\n if (nextc < 0) // overflow\n throw new Error(\"Maximum lock count exceeded\");\n setState(nextc);\n return true;\n }\n return false;\n }\n\n protected final boolean tryRelease(int releases) {\n int c = getState() - releases;\n if (Thread.currentThread() != getExclusiveOwnerThread())\n throw new IllegalMonitorStateException();\n boolean free = false;\n if (c == 0) {\n free = true;\n setExclusiveOwnerThread(null);\n }\n setState(c);\n return free;\n }\n protected final boolean isHeldExclusively() {\n return getExclusiveOwnerThread() == Thread.currentThread();\n }\n final boolean isLocked() {\n return getState() != 0;\n }\n }\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏只貼出了部分代碼。小總結:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏和Mutex的實現方式相似,都是通過內部的Sync類來實現。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"公平鎖和非公平鎖的區別只是在獲取鎖的時候(在入","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"AQS","attrs":{}},{"type":"text","text":"的","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"CLH","attrs":{}},{"type":"text","text":"隊列之前)代碼有點區別,其它的都是一樣的。一旦進入了隊列,所有線程都是按照隊列中先來後到的順序請求鎖。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"非公平鎖的代碼中總是優先嚐試當前是否有線程持有鎖,一旦沒有任何線程持有鎖,那麼非公平鎖就霸道的嘗試將鎖“佔爲己有”。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ps:小插曲。這個時候如果有人再問Lock和Synchronized的區別,可以這麼回答了:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"AbstractQueuedSynchronizer通過構造一個基於阻塞的CLH隊列容納所有的阻塞線程,而對該隊列的操作均通過Lock-Free(CAS)操作,但對已經獲得鎖的線程而言,ReentrantLock實現了偏向鎖的功能。synchronized的底層也是一個基於CAS操作的等待隊列,但JVM實現的更精細,把等待隊列分爲ContentionList和EntryList,目的是爲了降低線程的出列速度;當然也實現了偏向鎖,從數據結構來說二者設計沒有本質區別。但synchronized還實現了自旋鎖,並針對不同的系統和硬件體系進行了優化,而Lock則完全依靠系統阻塞掛起等待線程。當然Lock比synchronized更適合在應用層擴展,可以繼承AbstractQueuedSynchronizer定義各種實現,比如實現讀寫鎖(ReadWriteLock),公平或不公平鎖;同時,Lock對應的Condition也比wait/notify要方便的多、靈活的多。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是後續的問題,只能祝你好運了……(學海無涯啊)","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"可中斷","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前提是AQS的acquire是不響應中斷的(關於Intereput的知識點,自行百度一下即可,需要知道線程在運行狀態下是不響應中斷的)。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可中斷和超時的實現都是在AbstractQueuedSynchronizer,沒有交給子類來實現。如果在獲取一個通過網絡交互實現的鎖時,這個鎖資源突然進行了銷燬,那麼使用acquireInterruptibly的獲取方式就能夠讓該時刻嘗試獲取鎖的線程提前返回。而同步器的這個特性被實現Lock接口中的lockInterruptibly方法。根據Lock的語義,在被中斷時,lockInterruptibly將會拋出InterruptedException來告知使用者","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"public final void acquireInterruptibly(int arg)\n\n    throws InterruptedException {\n\n    if (Thread.interrupted())\n\n        throw new InterruptedException();\n\n    if (!tryAcquire(arg))\n\n        doAcquireInterruptibly(arg);\n\n}\n\nprivate void doAcquireInterruptibly(int arg)\n\n    throws InterruptedException {\n\n    final Node node = addWaiter(Node.EXCLUSIVE);\n\n    boolean failed = true;\n\n    try {\n\n        for (;;) {\n\n            final Node p = node.predecessor();\n\n            if (p == head && tryAcquire(arg)) {\n\n                setHead(node);\n\n                p.next = null; // help GC\n\n                failed = false;\n\n                return;\n\n            }\n\n            // 檢測中斷標誌位\n\n            if (shouldParkAfterFailedAcquire(p, node) &&\n\n            parkAndCheckInterrupt())\n\n                throw new InterruptedException();\n\n        }\n\n    } finally {\n\n        if (failed)\n\n            cancelAcquire(node);\n\n    }\n\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上述邏輯主要包括:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1. 檢測當前線程是否被中斷;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"判斷當前線程的中斷標誌位,如果已經被中斷了,那麼直接拋出異常並將中斷標誌位設置爲false。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2. 嘗試獲取狀態;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"調用tryAcquire獲取狀態,如果順利會獲取成功並返回。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3. 構造節點並加入sync隊列;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"獲取狀態失敗後,將當前線程引用構造爲節點並加入到sync隊列中。退出隊列的方式在沒有中斷的場景下和acquireQueued類似,當頭結點是自己的前驅節點並且能夠獲取到狀態時,即可以運行,當然要將本節點設置爲頭結點,表示正在運行。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4. 中斷檢測。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在每次被喚醒時,進行中斷檢測,如果發現當前線程被中斷,那麼拋出InterruptedException並退出循環。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"# 超時","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"針對超時控制這部分的實現,主要需要計算出睡眠的delta,也就是間隔值。間隔可以表示爲nanosTimeout = 原有nanosTimeout – now(當前時間)+ lastTime(睡眠之前記錄的時間)。如果nanosTimeout大於0,那麼還需要使當前線程睡眠,反之則返回false。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"private boolean doAcquireNanos(int arg, long nanosTimeout)\n\nthrows InterruptedException {\n\n    long lastTime = System.nanoTime();\n\n    final Node node = addWaiter(Node.EXCLUSIVE);\n\n    boolean failed = true;\n\n    try {\n\n        for (;;) {\n\n            final Node p = node.predecessor();\n\n            if (p == head && tryAcquire(arg)) {\n\n                setHead(node);\n\n                p.next = null; // help GC\n\n                failed = false;\n\n                return true;\n\n            }\n\n            if (nanosTimeout <= 0)                 return false;             if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold)\n\n            LockSupport.parkNanos(this, nanosTimeout);\n\n            long now = System.nanoTime();\n\n            //計算時間,當前時間減去睡眠之前的時間得到睡眠的時間,然後被\n\n            //原有超時時間減去,得到了還應該睡眠的時間\n\n            nanosTimeout -= now - lastTime;\n\n            lastTime = now;\n\n            if (Thread.interrupted())\n\n                throw new InterruptedException();\n\n        }\n\n    } finally {\n\n        if (failed)\n\n            cancelAcquire(node);\n\n    }\n\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1. 加入sync隊列;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"將當前線程構造成爲節點Node加入到sync隊列中。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2. 條件滿足直接返回;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"退出條件判斷,如果前驅節點是頭結點並且成功獲取到狀態,那麼設置自己爲頭結點並退出,返回true,也就是在指定的nanosTimeout之前獲取了鎖。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3. 獲取狀態失敗休眠一段時間;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過LockSupport.unpark來指定當前線程休眠一段時間。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4. 計算再次休眠的時間;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"喚醒後的線程,計算仍需要休眠的時間,該時間表示爲nanosTimeout = 原有nanosTimeout – now(當前時間)+ lastTime(睡眠之前記錄的時間)。其中now – lastTime表示這次睡眠所持續的時間。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"5. 休眠時間的判定。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"喚醒後的線程,計算仍需要休眠的時間,並無阻塞的嘗試再獲取狀態,如果失敗後查看其nanosTimeout是否大於0,如果小於0,那麼返回完全超時,沒有獲取到鎖。 如果nanosTimeout小於等於1000L納秒,則進入快速的自旋過程。那麼快速自旋會造成處理器資源緊張嗎?結果是不會,經過測算,開銷看起來很小,幾乎微乎其微。Doug Lea應該測算了在線程調度器上的切換造成的額外開銷,因此在短時1000納秒內就讓當前線程進入快速自旋狀態,如果這時再休眠相反會讓nanosTimeout的獲取時間變得更加不精確。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"![Qz7aVLTPSvEul5y](https://i.loli.net/2021/07/02/Qz7aVLTPSvEul5y.png)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"條件中斷","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"參考","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https://www.cnblogs.com/onlywujun/articles/3531568.html","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"http://ifeve.com/introduce-abstractqueuedsynchronizer/","attrs":{}}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章