摘要:鎖與很好的隔離使用者與實(shí)現(xiàn)者所需要關(guān)注的領(lǐng)域。那么這個(gè)就是包裝線(xiàn)程并且放入到隊(duì)列的過(guò)程實(shí)現(xiàn)的方法。也證實(shí)了就是獲取鎖的線(xiàn)程的節(jié)點(diǎn)。如果發(fā)生異常取消請(qǐng)求,也就是將當(dāng)前節(jié)點(diǎn)重隊(duì)列中移除。
前言
自從JDK1.5后,jdk新增一個(gè)并發(fā)工具包java.util.concurrent,提供了一系列的并發(fā)工具類(lèi)。而今天我們需要學(xué)習(xí)的是java.util.concurrent.lock也就是它下面的lock包,其中有一個(gè)最為常見(jiàn)類(lèi)ReentrantLock,
我們知道ReentrantLock的功能是實(shí)現(xiàn)代碼段的并發(fā)訪(fǎng)問(wèn)控制,也就是通常意義上所說(shuō)的鎖。之前我們也學(xué)習(xí)過(guò)一種鎖的實(shí)現(xiàn),也就是synchronized關(guān)鍵詞,synchronized是在字節(jié)碼層面,通過(guò)對(duì)象的監(jiān)視器鎖實(shí)現(xiàn)的。那么ReentrantLock又是怎么實(shí)現(xiàn)的呢?
如果不看源碼,可能會(huì)以為它的實(shí)現(xiàn)是通過(guò)類(lèi)似于synchronized,通過(guò)對(duì)象的監(jiān)視器鎖實(shí)現(xiàn)的。但事實(shí)上它僅僅是一個(gè)工具類(lèi)!沒(méi)有使用更“高級(jí)”的機(jī)器指令,不是關(guān)鍵字,也不依靠JDK編譯時(shí)的特殊處理,僅僅作為一個(gè)普普通通的類(lèi)就完成了代碼塊的并發(fā)訪(fǎng)問(wèn)控制,這就更讓人疑問(wèn)它怎么實(shí)現(xiàn)的代碼塊的并發(fā)訪(fǎng)問(wèn)控制的了。
我們查看源碼發(fā)現(xiàn),它是通過(guò)繼承抽象類(lèi)實(shí)現(xiàn)的AbstractQueuedSynchronizer,為了方便描述,接下來(lái)我將用AQS代替AbstractQueuedSynchronizer。
關(guān)于AQSAQS,它是用來(lái)構(gòu)建鎖或者其他同步組建的基礎(chǔ)框架,我們見(jiàn)過(guò)許多同步工具類(lèi)都是基于它構(gòu)建的。包括ReentrantLock、CountDownLatch等。在深入了解AQS了解之前,我們需要知道鎖跟AQS的區(qū)別。鎖,它是面向使用者的,它定義了使用者與鎖交互的接口,隱藏了實(shí)現(xiàn)的細(xì)節(jié);而AQS面像的是鎖的實(shí)現(xiàn)者,它簡(jiǎn)化了鎖的實(shí)現(xiàn)。鎖與AQS很好的隔離使用者與實(shí)現(xiàn)者所需要關(guān)注的領(lǐng)域。那么我們今天就作為一個(gè)鎖的實(shí)現(xiàn)者,一步一步分析鎖的實(shí)現(xiàn)。
AQS又稱(chēng)同步器,它的內(nèi)部有一個(gè)int成員變量state表示同步狀態(tài),還有一個(gè)內(nèi)置的FIFO隊(duì)列來(lái)實(shí)現(xiàn)資源獲取線(xiàn)程的排隊(duì)工作。通過(guò)它們我們就能實(shí)現(xiàn)鎖。
在實(shí)現(xiàn)鎖之前,我們需要考慮做為鎖的使用者,鎖會(huì)有哪幾種?
通常來(lái)說(shuō),鎖分為兩種,一種是獨(dú)占鎖(排它鎖,互斥鎖),另一種就是共享鎖了。根據(jù)這兩類(lèi),其實(shí)AQS也給我們提供了兩套API。而我們作為鎖的實(shí)現(xiàn)者,通常都是要么全部實(shí)現(xiàn)它的獨(dú)占api,要么實(shí)現(xiàn)它的共享api,而不會(huì)出現(xiàn)一起實(shí)現(xiàn)的。即使juc內(nèi)置的ReentrantReadWriteLock也是通過(guò)兩個(gè)子類(lèi)分別來(lái)實(shí)現(xiàn)的。
鎖的實(shí)現(xiàn) 獨(dú)占鎖獨(dú)占鎖又名互斥鎖,同一時(shí)間,只有一個(gè)線(xiàn)程能獲取到鎖,其余的線(xiàn)程都會(huì)被阻塞等待。其中我們常用的ReentrantLock就是一種獨(dú)占鎖,我們一起來(lái)ReentrantLock 分析ReentrantLock的同時(shí)看一看AQS的實(shí)現(xiàn),再推理出AQS獨(dú)特的設(shè)計(jì)思路和實(shí)現(xiàn)方式。最后,再看其共享控制功能的實(shí)現(xiàn)。
首先我們來(lái)看看獲取鎖的過(guò)程
加鎖我們查看ReentrantLock的源碼。來(lái)分析它的lock方法
public void lock() { sync.lock(); }
與我們之前分析的一樣,鎖的具體實(shí)現(xiàn)由內(nèi)部的代理類(lèi)完成,lock只是暴露給鎖的使用者的一套api。使用過(guò)ReentrantLock的同學(xué)應(yīng)該知道,ReentrantLock又分為公平鎖和非公平鎖,所以,ReentrantLock內(nèi)部只有兩個(gè)sync的實(shí)現(xiàn)。
/** * Sync object for non-fair locks */ static final class NonfairSync extends Sync{..} /** * Sync object for fair locks */ static final class FairSync extends Sync{..}
公平鎖 :每個(gè)線(xiàn)程獲取鎖的順序是按照調(diào)用lock方法的先后順序來(lái)的。
非公平鎖:每個(gè)線(xiàn)程獲取鎖的順序是不會(huì)按照調(diào)用lock方法的先后順序來(lái)的。完全看運(yùn)氣。
所以我們完全可以猜測(cè)到,這個(gè)公平與不公平的區(qū)別就體現(xiàn)在鎖的獲取過(guò)程。我們以公平鎖為例,來(lái)分析獲取鎖過(guò)程,最后對(duì)比非公平鎖的過(guò)程,尋找差異。
lock查看FairSync的lock方法
final void lock() { acquire(1); }
這里它調(diào)用到了父類(lèi)AQS的acquire方法,所以我們繼續(xù)查看acquire方法的代碼
acquire/** * Acquires in exclusive mode, ignoring interrupts. Implemented * by invoking at least once {@link #tryAcquire}, * returning on success. Otherwise the thread is queued, possibly * repeatedly blocking and unblocking, invoking {@link * #tryAcquire} until success. This method can be used * to implement method {@link Lock#lock}. * * @param arg the acquire argument. This value is conveyed to * {@link #tryAcquire} but is otherwise uninterpreted and * can represent anything you like. */ public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
查看方法方法的注釋我們可以知道這個(gè)方法的作用,這里我簡(jiǎn)單的翻譯一下.
Acquires方法是一個(gè)獨(dú)占鎖模式的方法,它是不會(huì)響應(yīng)中斷的。它至少執(zhí)行一次tryAcquire去獲取鎖,如果返回true,則代表獲取鎖成功,否則它將會(huì)被加入等待隊(duì)列阻塞,直到重新嘗試獲取鎖成功。所以我們需要看看嘗試獲取鎖的方法tryAcquire的實(shí)現(xiàn)
tryAcruireprotected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }
拋出一個(gè)異常,沒(méi)有實(shí)現(xiàn)。所以我們需要查看它的子類(lèi),在我們這里就是FairSync的實(shí)現(xiàn)。
這里也會(huì)大家會(huì)有疑惑,沒(méi)有實(shí)現(xiàn)為什么不寫(xiě)成抽象方法呢,前面我們提到過(guò),我們不會(huì)同時(shí)在一個(gè)類(lèi)中實(shí)現(xiàn)獨(dú)占鎖跟共享鎖的api,那么tryAcruire是屬于獨(dú)占鎖,那么如果我想一個(gè)共享鎖也要重新獨(dú)占鎖的方法嗎?所以大師的設(shè)計(jì)是絕對(duì)沒(méi)有問(wèn)題的。
protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread();//獲取當(dāng)前線(xiàn)程 int c = getState(); //獲取父類(lèi)AQS中的標(biāo)志位 if (c == 0) { if (!hasQueuedPredecessors() && //如果隊(duì)列中沒(méi)有其他線(xiàn)程 說(shuō)明沒(méi)有線(xiàn)程正在占有鎖! compareAndSetState(0, acquires)) { //修改一下?tīng)顟B(tài)位,注意:這里的acquires是在lock的時(shí)候傳遞來(lái)的,從上面的圖中可以知道,這個(gè)值是寫(xiě)死的1 setExclusiveOwnerThread(current); //如果通過(guò)CAS操作將狀態(tài)為更新成功則代表當(dāng)前線(xiàn)程獲取鎖,因此,將當(dāng)前線(xiàn)程設(shè)置到AQS的一個(gè)變量中,說(shuō)明這個(gè)線(xiàn)程拿走了鎖。 return true; } } else if (current == getExclusiveOwnerThread()) { //如果不為0 意味著,鎖已經(jīng)被拿走了,但是,因?yàn)镽eentrantLock是重入鎖, //是可以重復(fù)lock,unlock的,只要成對(duì)出現(xiàn)行。一次。這里還要再判斷一次 獲取鎖的線(xiàn)程是不是當(dāng)前請(qǐng)求鎖的線(xiàn)程。 int nextc = c + acquires;//如果是的,累加在state字段上就可以了。 if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
目前為止,如果獲取鎖成功,則返回true,獲取鎖的過(guò)程結(jié)束,如果獲取失敗,則返回false
按照之前的邏輯,如果線(xiàn)程獲取鎖失敗,則會(huì)被放入到隊(duì)列中,但是在放入之前,需要給線(xiàn)程包裝一下。
那么這個(gè)addWaiter就是包裝線(xiàn)程并且放入到隊(duì)列的過(guò)程實(shí)現(xiàn)的方法。
addWaiter/** * Creates and enqueues node for current thread and given mode. * * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared * @return the new node */ private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node; }
注釋: 把當(dāng)前線(xiàn)程作為一個(gè)節(jié)點(diǎn)添加到隊(duì)列中,并且為這個(gè)節(jié)點(diǎn)設(shè)置模式
模式: 也就是獨(dú)占模式/共享模式,在這里模式是形參,所以我們看看起調(diào)方acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) Node.EXCLUSIVE 就代表這是獨(dú)占鎖模式。
創(chuàng)建好節(jié)點(diǎn)后,將節(jié)點(diǎn)加入到隊(duì)列尾部,此處,在隊(duì)列不為空的時(shí)候,先嘗試通過(guò)cas方式修改尾節(jié)點(diǎn)為最新的節(jié)點(diǎn),如果修改失敗,意味著有并發(fā),這個(gè)時(shí)候才會(huì)進(jìn)入enq中死循環(huán),“自旋”方式修改。
將線(xiàn)程的節(jié)點(diǎn)接入到隊(duì)里中后,當(dāng)然還需要做一件事:將當(dāng)前線(xiàn)程掛起!這個(gè)事,由acquireQueued來(lái)做。
在解釋acquireQueued之前,我們需要先看下AQS中隊(duì)列的內(nèi)存結(jié)構(gòu),我們知道,隊(duì)列由Node類(lèi)型的節(jié)點(diǎn)組成,其中至少有兩個(gè)變量,一個(gè)封裝線(xiàn)程,一個(gè)封裝節(jié)點(diǎn)類(lèi)型。
而實(shí)際上,它的內(nèi)存結(jié)構(gòu)是這樣的(第一次節(jié)點(diǎn)插入時(shí),第一個(gè)節(jié)點(diǎn)是一個(gè)空節(jié)點(diǎn),代表有一個(gè)線(xiàn)程已經(jīng)獲取鎖,事實(shí)上,隊(duì)列的第一個(gè)節(jié)點(diǎn)就是代表持有鎖的節(jié)點(diǎn)):
黃色節(jié)點(diǎn)為隊(duì)列默認(rèn)的頭節(jié)點(diǎn),每次有線(xiàn)程競(jìng)爭(zhēng)失敗,進(jìn)入隊(duì)列后其實(shí)都是插入到隊(duì)列的尾節(jié)點(diǎn)(tail后面)后面。這個(gè)從enq方法可以看出來(lái),上文中有提到enq方法為將節(jié)點(diǎn)插入隊(duì)列的方法:
enqprivate Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize // 一個(gè)空的節(jié)點(diǎn),通常代表獲取鎖的線(xiàn)程 if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }acquireQueued
接著我們來(lái)看看當(dāng)節(jié)點(diǎn)被放入到隊(duì)列中,如何將線(xiàn)程掛起,也就是看看acquireQueued方法的實(shí)現(xiàn)。
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { // 獲取當(dāng)前節(jié)點(diǎn)前驅(qū)結(jié)點(diǎn) final Node p = node.predecessor(); // 如果前驅(qū)節(jié)點(diǎn)是head,那么它就是等待隊(duì)列中的第一個(gè)線(xiàn)程 // 因?yàn)槲覀冎纇ead就是獲取線(xiàn)程的節(jié)點(diǎn),那么它就有機(jī)會(huì)再次獲取鎖 if (p == head && tryAcquire(arg)) { //成功后,將上圖中的黃色節(jié)點(diǎn)移除,Node1變成頭節(jié)點(diǎn)。 也證實(shí)了head就是獲取鎖的線(xiàn)程的節(jié)點(diǎn)。 setHead(node); p.next = null; // help GC failed = false; return interrupted; } // 1、檢查前一個(gè)節(jié)點(diǎn)的狀態(tài),判斷是否要掛起 // 2、如果需要掛起,則通過(guò)JUC下的LockSopport類(lèi)的靜態(tài)方法park掛起當(dāng)前線(xiàn)程,直到被喚醒。 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { // 如果發(fā)生異常 if (failed) // 取消請(qǐng)求,也就是將當(dāng)前節(jié)點(diǎn)重隊(duì)列中移除。 cancelAcquire(node); } }
這里我還需要解釋的是:
1、Node節(jié)點(diǎn)除了存儲(chǔ)當(dāng)前線(xiàn)程之外,節(jié)點(diǎn)類(lèi)型,前驅(qū)后驅(qū)指針之后,還存儲(chǔ)一個(gè)叫waitStatus的變量,該變量用于描述節(jié)點(diǎn)的狀態(tài)。共有四種狀態(tài)。
/** waitStatus value to indicate thread has cancelled */ static final int CANCELLED = 1; /** waitStatus value to indicate successor"s thread needs unparking */ static final int SIGNAL = -1; /** waitStatus value to indicate thread is waiting on condition */ static final int CONDITION = -2; /** * waitStatus value to indicate the next acquireShared should * unconditionally propagate */ static final int PROPAGATE = -3;
分別表示:
1 = 取消狀態(tài),該節(jié)點(diǎn)將會(huì)被隊(duì)列移除。
-1 = 等待狀態(tài),后驅(qū)節(jié)點(diǎn)處于等待狀態(tài)。
-2 = 等待被通知,該節(jié)點(diǎn)將會(huì)阻塞至被該鎖的condition的await方法喚醒。
-3 = 共享傳播狀態(tài),代表該節(jié)點(diǎn)的狀態(tài)會(huì)向后傳播。
到此為止,一個(gè)線(xiàn)程對(duì)于鎖的一次競(jìng)爭(zhēng)才告于段落,結(jié)果有兩種,要么成功獲取到鎖(不用進(jìn)入到AQS隊(duì)列中),要么,獲取失敗,被掛起,等待下次喚醒后繼續(xù)循環(huán)嘗試獲取鎖,值得注意的是,AQS的隊(duì)列為FIFO隊(duì)列,所以,每次被CPU假喚醒,且當(dāng)前線(xiàn)程不是出在頭節(jié)點(diǎn)的位置,也是會(huì)被掛起的。AQS通過(guò)這樣的方式,實(shí)現(xiàn)了競(jìng)爭(zhēng)的排隊(duì)策略。
釋放鎖看完了加鎖,再看釋放鎖。我們先不看代碼也可以猜測(cè)到釋放鎖需要的步驟。
隊(duì)列的頭節(jié)點(diǎn)是當(dāng)前獲取鎖的線(xiàn)程,所以我們需要移除頭節(jié)點(diǎn)
釋放鎖,喚醒頭節(jié)點(diǎn)后驅(qū)節(jié)點(diǎn)來(lái)競(jìng)爭(zhēng)鎖
接下來(lái)我們查看源碼來(lái)驗(yàn)證我們的猜想是否在正確。
unlockpublic void unlock() { sync.release(1); }
unlock方法調(diào)用AQS的release方法,因?yàn)槲覀兊腶cquire的時(shí)候傳入的是1,也就是同步狀態(tài)量+1,那么對(duì)應(yīng)的解鎖就要-1。
releasepublic final boolean release(int arg) { // 嘗試釋放鎖 if (tryRelease(arg)) { // 釋放鎖成功,獲取當(dāng)前隊(duì)列的頭節(jié)點(diǎn) Node h = head; if (h != null && h.waitStatus != 0) // 喚醒當(dāng)前節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn) unparkSuccessor(h); return true; } return false; }tryRelease
同樣的它是交給子類(lèi)實(shí)現(xiàn)的
protected final boolean tryRelease(int releases) { int c = getState() - releases; // 當(dāng)前線(xiàn)程不是獲取鎖的線(xiàn)程 拋出異常 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; // 因?yàn)槭侵厝氲年P(guān)系,不是每次釋放鎖c都等于0,直到最后一次釋放鎖時(shí),才通知AQS不需要再記錄哪個(gè)線(xiàn)程正在獲取鎖。 if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; }unparkSuccessor
釋放鎖成功之后,就喚醒頭節(jié)點(diǎn)后驅(qū)節(jié)點(diǎn)來(lái)競(jìng)爭(zhēng)鎖
private void unparkSuccessor(Node node) { int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) LockSupport.unpark(s.thread); }
值得注意的是,尋找的順序是從隊(duì)列尾部開(kāi)始往前去找的最前面的一個(gè)waitStatus小于0的節(jié)點(diǎn)。因?yàn)榇笥? 就是1狀態(tài)的節(jié)點(diǎn)是取消狀態(tài)。
公平鎖與非公平鎖到此我們鎖獲取跟鎖的釋放已經(jīng)分析的差不多。那么公平鎖跟非公平鎖的區(qū)別在于加鎖的過(guò)程。對(duì)比代碼
static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; final void lock() { acquire(1); } }
static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; /** * Performs lock. Try immediate barge, backing up to normal * acquire on failure. */ final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } }
從代碼中也可以看出來(lái),非公平在公平鎖的加鎖的邏輯之前先直接cas修改一次state變量(嘗試獲取鎖),成功就返回,不成功再排隊(duì),從而達(dá)到不排隊(duì)直接搶占的目的。
最后歡迎大家關(guān)注一下我的個(gè)人公眾號(hào)。一起交流一起學(xué)習(xí),有問(wèn)必答。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/77100.html
摘要:今天給大家總結(jié)一下,面試中出鏡率很高的幾個(gè)多線(xiàn)程面試題,希望對(duì)大家學(xué)習(xí)和面試都能有所幫助。指令重排在單線(xiàn)程環(huán)境下不會(huì)出先問(wèn)題,但是在多線(xiàn)程環(huán)境下會(huì)導(dǎo)致一個(gè)線(xiàn)程獲得還沒(méi)有初始化的實(shí)例。使用可以禁止的指令重排,保證在多線(xiàn)程環(huán)境下也能正常運(yùn)行。 下面最近發(fā)的一些并發(fā)編程的文章匯總,通過(guò)閱讀這些文章大家再看大廠面試中的并發(fā)編程問(wèn)題就沒(méi)有那么頭疼了。今天給大家總結(jié)一下,面試中出鏡率很高的幾個(gè)多線(xiàn)...
摘要:的主要功能和關(guān)鍵字一致,均是用于多線(xiàn)程的同步。而僅支持通過(guò)查詢(xún)當(dāng)前線(xiàn)程是否持有鎖。由于和使用的是同一把可重入鎖,所以線(xiàn)程可以進(jìn)入方法,并再次獲得鎖,而不會(huì)被阻塞住。公平與非公平公平與非公平指的是線(xiàn)程獲取鎖的方式。 1.簡(jiǎn)介 可重入鎖ReentrantLock自 JDK 1.5 被引入,功能上與synchronized關(guān)鍵字類(lèi)似。所謂的可重入是指,線(xiàn)程可對(duì)同一把鎖進(jìn)行重復(fù)加鎖,而不會(huì)被阻...
摘要:開(kāi)始獲取鎖終于輪到出場(chǎng)了,的調(diào)用過(guò)程和完全一樣,同樣拿不到鎖,然后加入到等待隊(duì)列隊(duì)尾然后,在阻塞前需要把前驅(qū)結(jié)點(diǎn)的狀態(tài)置為,以確保將來(lái)可以被喚醒至此,的執(zhí)行也暫告一段落了安心得在等待隊(duì)列中睡覺(jué)。 showImg(https://segmentfault.com/img/remote/1460000016012467); 本文首發(fā)于一世流云的專(zhuān)欄:https://segmentfault...
摘要:撤銷(xiāo)鎖偏向鎖使用了一種等到競(jìng)爭(zhēng)出現(xiàn)才釋放鎖的機(jī)制,所以當(dāng)其他線(xiàn)程嘗試競(jìng)爭(zhēng)偏向鎖時(shí),持有偏向鎖的線(xiàn)程才會(huì)釋放鎖。輕量級(jí)鎖線(xiàn)程在執(zhí)行同步代碼塊之前,會(huì)先在當(dāng)前線(xiàn)程的棧楨中創(chuàng)建用于存儲(chǔ)鎖記錄的空間,并將對(duì)象頭中的復(fù)制到鎖記錄中,官方稱(chēng)為。 前言 作者前面也寫(xiě)了幾篇關(guān)于Java并發(fā)編程,以及線(xiàn)程和volatil的基礎(chǔ)知識(shí),有興趣可以閱讀作者的原文博客,今天關(guān)于Java中的兩種鎖進(jìn)行詳解,希望對(duì)...
閱讀 2669·2021-11-23 09:51
閱讀 3255·2021-11-22 14:44
閱讀 4592·2021-11-22 09:34
閱讀 5138·2021-10-08 10:14
閱讀 2463·2021-09-22 15:47
閱讀 3521·2021-09-22 15:40
閱讀 1520·2019-08-30 15:44
閱讀 1630·2019-08-28 18:23