ReentrantLock原理分析

ReentrantLock是Java并發(fā)包中提供的一個可重入的互斥鎖揪惦,它擁有與synchronized相同的作用遍搞,但卻比synchronized有更好的性能,在許多高并發(fā)編程中都會用到它器腋。由于大部分同學(xué)都只停留在了API調(diào)用的層次溪猿,對ReentrantLock的原理一知半解,甚至一無所知纫塌,因此寫下了這篇文章诊县,讓同學(xué)們真正的把ReentrantLock給拿下!

本文將會從以下幾個方面去進(jìn)行分享:

  • 使用場景
  • 源碼實(shí)現(xiàn)
  • 設(shè)計思想

使用場景

public class ReentrantLockTest {
    private ReentrantLock lock = new ReentrantLock();

    public void method() {
        lock.lock();
        // do something
        lock.unlock();
    }
}

ReentrantLock的使用十分簡單措左,在同步代碼塊前調(diào)用lock()加鎖依痊,同步代碼塊之后調(diào)用unlock()釋放鎖就可以了。另外要注意怎披,lock()和unlock()必須成雙成對的出現(xiàn)胸嘁。如果同步代碼塊可能拋出異常,則必須把unlock()調(diào)用放在finally塊里钳枕。

源碼實(shí)現(xiàn)

打開lock()查看它的實(shí)現(xiàn)缴渊。

public void lock() {
    sync.lock();
}

它通過調(diào)用了sync的lock()方法來完成加鎖,我們?nèi)タ聪聅ync的定義鱼炒。

private final ReentrantLock.Sync sync;

Sync是一個內(nèi)部類衔沼,我們?nèi)タ聪滤膌ock()實(shí)現(xiàn)。

abstract void lock();

很明顯昔瞧,有子類繼承了Sync指蚁,這時候我們可以去看sync的初始化代碼,看看是使用了哪個子類對sync進(jìn)行了初始化自晰。

public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

有兩個子類用于sync的初始化凝化,F(xiàn)airSync和NonfairSync。這其實(shí)就是我們所熟知的公平鎖和非公平鎖酬荞。ReentrantLock默認(rèn)情況下使用了非公平鎖搓劫,當(dāng)然也可以在創(chuàng)建ReentrantLock的時候顯示指定瞧哟。

現(xiàn)在我們先去看下非公平鎖NonfairSync對lock()的實(shí)現(xiàn)。

final void lock() {
    // 這步快速嘗試獲取鎖的操作枪向,公平鎖里邊沒有
    if (this.compareAndSetState(0, 1)) {
        this.setExclusiveOwnerThread(Thread.currentThread());
    } else {
        this.acquire(1);
    }
}

查看代碼可知勤揩,非公平鎖一上來就先調(diào)用一把compareAndSetState(),嘗試獲取鎖秘蛔,這個對于已經(jīng)在鎖隊列里苦苦等待的其他線程陨亡,是非常不公平的。劃重點(diǎn)了深员,同學(xué)們负蠕,這里是公平鎖和非公平鎖的重要區(qū)別。

現(xiàn)在我們來看compareAndSetState()的實(shí)現(xiàn)倦畅。

protected final boolean compareAndSetState(int var1, int var2) {
    return unsafe.compareAndSwapInt(this, stateOffset, var1, var2);
}

原理是通過unsafe提供的CAS原子操作進(jìn)行state的值更新遮糖。另外發(fā)現(xiàn)compareAndSetState()是位于AbstractQueuedSynchronizer類中的,繼而發(fā)現(xiàn)滔迈,Sync繼承了AbstractQueuedSynchronizer止吁,我們需要更新的state也位于AbstractQueuedSynchronizer中被辑。

private volatile int state;

state記錄了鎖重入的次數(shù)燎悍,如果為0,那么表示當(dāng)前沒有線程持有此鎖盼理,此時使用一個CAS操作即可快速完成鎖的申請谈山,這便是快速嘗試。

當(dāng)快速嘗試失敗之后宏怔,將會調(diào)用acquire()方法奏路,acquire()也是來自于AbstractQueuedSynchronizer,我們看下代碼臊诊。

public final void acquire(int var1) {
    if (!this.tryAcquire(var1) && this.acquireQueued(this.addWaiter(Node.EXCLUSIVE), var1)) {
        selfInterrupt();
    }
}

先tryAcquire()一下鸽粉,萬一自己是個鎖二代(鎖重入)呢,那就爽歪歪了抓艳,獲取鎖成功触机,直接撤退走人!

來看下非公平鎖是怎么獲取鎖的玷或,打開tryAcquire()的源碼儡首。

protected final boolean tryAcquire(int var1) {
    return this.nonfairTryAcquire(var1);
}

繼續(xù)查看Sync中的nonfairTryAcquire()。

final boolean nonfairTryAcquire(int var1) {
    // 獲取當(dāng)前線程
    Thread var2 = Thread.currentThread();
    // 獲取鎖重人次數(shù)
    int var3 = this.getState();
    // lock還沒有被任何線程霸占偏友,趕緊快速嘗試一把加鎖
    if (var3 == 0) {
        if (this.compareAndSetState(0, var1)) {
            this.setExclusiveOwnerThread(var2);
            return true;
        }
    } 
    // lock已經(jīng)被線程霸占了蔬胯,檢查一下是不是自己人,如果是的話位他,那當(dāng)前線程就是鎖二代了氛濒,state加1
    else if (var2 == this.getExclusiveOwnerThread()) {
        int var4 = var3 + var1;
        // 鎖重入的次數(shù)不能超過Ingteger.MAX_VALUE产场,不然會爆炸
        if (var4 < 0) {
            throw new Error("Maximum lock count exceeded");
        }
        this.setState(var4);
        return true;
    }
    // 既沒有創(chuàng)業(yè)成功,也不是鎖二代舞竿,就只有失敗的命運(yùn)了
    return false;
}

看完nonfairTryAcquire()的操作涝动,我們知道非公平鎖的鎖重入是怎么玩的了。

如果線程沒有獲取到鎖炬灭,就只能去隊列里等待鎖了醋粟,也就是調(diào)用addWaiter()方法,我們來看下它的實(shí)現(xiàn)重归。

private Node addWaiter(Node mode) {
    // 構(gòu)造一個Node米愿,與當(dāng)前線程綁定,mode傳入的是Node.EXCLUSIVE鼻吮,代表獨(dú)占鎖
    Node var2 = new Node(Thread.currentThread(), mode);
    // 獲取隊列末尾節(jié)點(diǎn)
    Node var3 = this.tail;
    // 末尾節(jié)點(diǎn)非空育苟,CAS快速嘗試,把自己更新為末尾節(jié)點(diǎn)
    if (var3 != null) {
        var2.prev = var3;
        if (this.compareAndSetTail(var3, var2)) {
            var3.next = var2;
            return var2;
        }
    }
    // 末尾節(jié)點(diǎn)不存在椎木,或者更新末尾節(jié)點(diǎn)失敗了
    this.enq(var2);
    return var2;
}

當(dāng)末尾節(jié)點(diǎn)為null违柏,或者更新末尾節(jié)點(diǎn)失敗了,那就調(diào)用enq()進(jìn)行處理香椎。

private Node enq(Node var1) {
    // 注意這里的while(true)漱竖,不達(dá)目的不罷休
    while(true) {
        Node var2 = this.tail;
        // 末尾節(jié)點(diǎn)為空,意味著整個隊列都為空畜伐,頭節(jié)點(diǎn)自然不存在馍惹,那就來初始化一波頭尾節(jié)點(diǎn)
        if (var2 == null) {
            // 通過CAS更新頭節(jié)點(diǎn),從這行代碼我們也可以知道玛界,鎖隊列里的頭節(jié)點(diǎn)是空的万矾,沒有和任何線程綁定
            if (this.compareAndSetHead(new Node())) {
                // 此時頭節(jié)點(diǎn)和末尾節(jié)點(diǎn)是同一個
                this.tail = this.head;
            }
        } 
        // 末尾節(jié)點(diǎn)已經(jīng)存在,直接CAS把自己更新為末尾節(jié)點(diǎn)
        else {
            var1.prev = var2;
            if (this.compareAndSetTail(var2, var1)) {
                var2.next = var1;
                return var2;
            }
        }
    }
}

用上了while(true)慎框,保證了enq()返回后良狈,當(dāng)前線程一定是被加入到了鎖隊列的末尾。

當(dāng)前線程對應(yīng)的Node加入隊列末尾之后笨枯,接著調(diào)用了acquireQueued()薪丁,我們來看下這個方法干了什么事。

final boolean acquireQueued(Node var1, int var2) {
    // 標(biāo)識該方法返回時猎醇,當(dāng)前線程是否已獲得鎖窥突,默認(rèn)值true代表沒有搶到
    boolean var3 = true;

    try {
        // 標(biāo)識一下當(dāng)前線程在睡覺時候有沒有被叫醒過
        boolean var4 = false;
        // 自旋獲取鎖
        while(true) {
            // 獲取當(dāng)前節(jié)點(diǎn)的上一個節(jié)點(diǎn) 
            Node var5 = var1.predecessor();
            // 如果上一個節(jié)點(diǎn)是頭節(jié)點(diǎn)的話,就可以直接嘗試搶鎖
            if (var5 == this.head && this.tryAcquire(var2)) {
                //把自己設(shè)置為頭節(jié)點(diǎn)
                this.setHead(var1);
                // 解除上一任頭節(jié)點(diǎn)的依賴硫嘶,讓它早日被GC干掉
                var5.next = null;
                // 標(biāo)識我已經(jīng)搶鎖成功啦
                var3 = false;
                // 最終的返回值阻问,居然是當(dāng)前線程睡覺時候有沒有被叫醒過
                boolean var6 = var4;
                return var6;
            }
            // 當(dāng)前節(jié)點(diǎn)不在頭節(jié)點(diǎn)之后,或者在頭節(jié)點(diǎn)之后沦疾,但是搶鎖失敗了
            // 調(diào)用shouldParkAfterFailedAcquire()称近,為自己找到一個歸宿(讓上一個節(jié)點(diǎn)完事之后通知自己)第队,然后就可以調(diào)用parkAndCheckInterrupt()讓自己去休眠了
            if (shouldParkAfterFailedAcquire(var5, var1) && this.parkAndCheckInterrupt()) {                 
                // 睡覺時被意外喚醒,記錄一下刨秆,自己也是發(fā)生過中斷的男人了
                var4 = true;
            }
        }
        
    } finally {
        // 如果var3為true凳谦,則證明線程沒有拿到鎖,并且它已經(jīng)廢了衡未,所以方法退出前尸执,得調(diào)用cancelAcquire()給線程收尸
        if (var3) {
            this.cancelAcquire(var1);
        }
    }
}

線程被加入到隊列之后,就是瘋狂自旋的干上面這幾件事情:找人叫醒自己缓醋,睡覺如失,被叫醒,周而復(fù)始送粱,直到自己拿到了鎖褪贵,然后離開。

至于線程怎么找人叫醒自己的抗俄,我們來看shouldParkAfterFailedAcquire()的實(shí)現(xiàn)脆丁。

// var0是上一個節(jié)點(diǎn),var1是當(dāng)前節(jié)點(diǎn)
private static boolean shouldParkAfterFailedAcquire(Node var0, Node var1) {
    int var2 = var0.waitStatus;
    // 上一個節(jié)點(diǎn)滿足被叫醒的條件动雹,那也就意味著上一個節(jié)點(diǎn)早晚會搶鎖槽卫,用完鎖后自然會通知自己,這樣的話洽胶,自己就可以安心去睡覺了
    if (var2 == -1) {
        return true;
    } else {
        // 上一個節(jié)點(diǎn)放棄搶鎖啦晒夹,指望不上了,繼續(xù)往前尋找可靠的節(jié)點(diǎn)作為依靠
        if (var2 > 0) {
            do {
                var1.prev = var0 = var0.prev;
            } while(var0.waitStatus > 0);
            
            var0.next = var1;
        } else {    // waitStatus不大于0姊氓,CAS把它設(shè)置為-1(滿足被喚醒的條件),但是設(shè)置不一定會成功
            compareAndSetWaitStatus(var0, var2, -1);
        }
        // 這一波操作喷好,沒有找到喚醒自己的人翔横,睡不成啰
        return false;
    }
}

既然睡不成,那還是繼續(xù)去看看有沒有搶鎖資格吧梗搅,有就搶一把禾唁,就這樣周而復(fù)始的的循環(huán)下去。

當(dāng)某一時刻无切,線程找到了能叫醒自己的人荡短,這時候它就可以去睡覺了,去睡覺自然就是調(diào)用parkAndCheckInterrupt()方法哆键。

private final boolean parkAndCheckInterrupt() {
    // 劃重點(diǎn)了掘托,同學(xué)們,線程阻塞就是調(diào)用這個API來完成的籍嘹,底層的實(shí)現(xiàn)是用的unsafe.park()
    LockSupport.park(this);
    // 線程睡醒了闪盔,但是它要判斷一下睡覺期間有沒有發(fā)生過中斷
    return Thread.interrupted();
}

如果發(fā)生過中斷弯院,則parkAndCheckInterrupt()會返回true。這是我們再去看acquire()方法泪掀,它會執(zhí)行selfInterrupt()听绳。

static void selfInterrupt() {
    // 給線程標(biāo)記上中斷位,這可謂中斷會延遲處理异赫,但是從未缺席
    Thread.currentThread().interrupt();
}

同學(xué)們椅挣,到這里lock()就分析完了。現(xiàn)在我們接著來看看unlock()是怎么玩的塔拳。

public void unlock() {
    this.sync.release(1);
}

看樣子是調(diào)用了AQS的release()方法贴妻,我們接著看。

public final boolean release(int var1) {
    // 釋放鎖蝙斜,只有state變?yōu)?了才會返回true
    if (this.tryRelease(var1)) {
        Node var2 = this.head;
        // 頭節(jié)點(diǎn)不為空名惩,代表隊列不為空,waitStatus不為0孕荠,代表它有后繼節(jié)點(diǎn)娩鹉,因此可以去喚醒下家去搶鎖
        if (var2 != null && var2.waitStatus != 0) {
            this.unparkSuccessor(var2);
        }
        return true;
    } else {
        return false;
    }
}

調(diào)用tryRelease()方法釋放鎖,看下它的實(shí)現(xiàn)稚伍。

protected final boolean tryRelease(int var1) {
    // state減1后的結(jié)果
    int var2 = this.getState() - var1;
    // 如果線程不是當(dāng)前鎖的線程弯予,那就玩大啦,吃不了逗著走个曙,直接拋出異常
    if (Thread.currentThread() != this.getExclusiveOwnerThread()) {
        throw new IllegalMonitorStateException();
    } else {
        // 標(biāo)識鎖是不是已經(jīng)完全釋放了
        boolean var3 = false;
        // 沒有線程占用鎖了锈嫩,可以讓下一個線程來持鎖了
        if (var2 == 0) {
            // 鎖完全釋放了就返回true
            var3 = true;
            // 把鎖的持有者設(shè)置為null
            this.setExclusiveOwnerThread((Thread)null);
        }
        // 更新state值
        this.setState(var2);
        return var3;
    }
}

如果鎖完全釋放了,那么就得喚醒下家去搶鎖垦搬。具體是怎么尋找下家的呢呼寸,看一下unparkSuccessor()。

private void unparkSuccessor(Node var1) {
    int var2 = var1.waitStatus;
    if (var2 < 0) {
        // 將頭節(jié)點(diǎn)設(shè)置為初始狀態(tài)
        compareAndSetWaitStatus(var1, var2, 0);
    }

    Node var3 = var1.next;
    if (var3 == null || var3.waitStatus > 0) {
        var3 = null;
        // 從隊列的末尾節(jié)點(diǎn)往前找下家猴贰,最終是找到隊列里(頭節(jié)點(diǎn)除外)最前面的節(jié)點(diǎn)对雪,作為喚醒對象
        for(Node var4 = this.tail; var4 != null && var4 != var1; var4 = var4.prev) {
            if (var4.waitStatus <= 0) {
                var3 = var4;
            }
        }
    }
    // 喚醒這個節(jié)點(diǎn)
    if (var3 != null) {
        LockSupport.unpark(var3.thread);
    }
}

非公平鎖到這里就講完了,至于tryLock()方法米绕,相信同學(xué)們在看完為上面lock()的分享瑟捣,已經(jīng)可以自己獨(dú)立把它拿下了,現(xiàn)在我們來講一下公平鎖栅干。前面已經(jīng)提到了公平鎖和非公平鎖的一個區(qū)別迈套,就是lock()里的tryAcquire()實(shí)現(xiàn)有所不同。非公平鎖任何一個新加入的線程都可以參與搶鎖碱鳞,但是公平鎖就得老老實(shí)實(shí)排隊桑李,講究個先來后到,具體來看下吧。

protected final boolean tryAcquire(int var1) {
    Thread var2 = Thread.currentThread();
    int var3 = this.getState();
    if (var3 == 0) {
        // hasQueuedPredecessors()很關(guān)鍵芙扎,它是公平性的核心體現(xiàn)
        if (!this.hasQueuedPredecessors() && this.compareAndSetState(0, var1)) {
            this.setExclusiveOwnerThread(var2);
            return true;
        }
    } else if (var2 == this.getExclusiveOwnerThread()) {
        // 鎖重入
        int var4 = var3 + var1;
        if (var4 < 0) {
            throw new Error("Maximum lock count exceeded");
        }
        this.setState(var4);
        return true;
    }
    // 搶鎖失敗了
    return false;
}

hasQueuedPredecessors()的作用是當(dāng)滿足以下兩種條件中的一種時星岗,線程就能獲得搶鎖的資格: 1. 鎖同步隊列里只有一個節(jié)點(diǎn);2. 第二個節(jié)點(diǎn)屬于當(dāng)前線程戒洼。

設(shè)計思想

先看一下AQS內(nèi)部維護(hù)的鎖同步隊列俏橘。

aqs-queue.png

ReentrantLock通過使用AQS來實(shí)現(xiàn)加解鎖。AQS內(nèi)部維護(hù)了一個雙向鏈表的鎖同步隊列圈浇,并維護(hù)頭節(jié)點(diǎn)head寥掐,尾節(jié)點(diǎn)tail和信號量state。每個節(jié)點(diǎn)是一個Node對象磷蜀,對象中定義了prev召耘,next分別指向它的上下游,還有一個waitStatus對象用于表示線程狀態(tài)(等鎖或已放棄)褐隆。當(dāng)有新的線程需要搶鎖時污它,新建一個和線程映射的Node,加入到鎖同步隊列的末尾庶弃。當(dāng)然這里有個重點(diǎn)衫贬,在加入的時候會做判斷,如果當(dāng)前末尾節(jié)點(diǎn)處于放棄狀態(tài)歇攻,那么會繼續(xù)往前遍歷固惯,尋找一個可靠的節(jié)點(diǎn)作為上游。AQS內(nèi)部的state為0時缴守,資源未被占用葬毫,線程可進(jìn)行CAS操作更新state,如果更新成功則代表加鎖成功屡穗。如果state不為0贴捡,則意味著資源已經(jīng)被線程占用。如果占用者是自己鸡捐,那么可以進(jìn)行重入栈暇,如果占用者不是自己,那么就老老實(shí)實(shí)等著箍镜。

關(guān)于ReentrantLock的源碼講解和原理分析,到這里就全部結(jié)束啦煎源。后續(xù)還會更新更多關(guān)于Java并發(fā)包的其他干貨色迂,同學(xué)們一定要結(jié)合起來閱讀,相輔相成手销,形成一個完整的知識體系歇僧。最后,喜歡我文章的同學(xué)們,歡迎關(guān)注我的公眾號《小瑾守護(hù)線程》诈悍,不錯過任何有價值的干貨祸轮。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市侥钳,隨后出現(xiàn)的幾起案子适袜,更是在濱河造成了極大的恐慌,老刑警劉巖舷夺,帶你破解...
    沈念sama閱讀 218,386評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件苦酱,死亡現(xiàn)場離奇詭異,居然都是意外死亡给猾,警方通過查閱死者的電腦和手機(jī)疫萤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來敢伸,“玉大人扯饶,你說我怎么就攤上這事〕鼐保” “怎么了尾序?”我有些...
    開封第一講書人閱讀 164,704評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長饶辙。 經(jīng)常有香客問我蹲诀,道長,這世上最難降的妖魔是什么弃揽? 我笑而不...
    開封第一講書人閱讀 58,702評論 1 294
  • 正文 為了忘掉前任脯爪,我火速辦了婚禮,結(jié)果婚禮上矿微,老公的妹妹穿的比我還像新娘痕慢。我一直安慰自己,他們只是感情好涌矢,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,716評論 6 392
  • 文/花漫 我一把揭開白布掖举。 她就那樣靜靜地躺著,像睡著了一般娜庇。 火紅的嫁衣襯著肌膚如雪塔次。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,573評論 1 305
  • 那天名秀,我揣著相機(jī)與錄音励负,去河邊找鬼。 笑死匕得,一個胖子當(dāng)著我的面吹牛继榆,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,314評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼略吨,長吁一口氣:“原來是場噩夢啊……” “哼集币!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起翠忠,我...
    開封第一講書人閱讀 39,230評論 0 276
  • 序言:老撾萬榮一對情侶失蹤鞠苟,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后负间,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體偶妖,經(jīng)...
    沈念sama閱讀 45,680評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,873評論 3 336
  • 正文 我和宋清朗相戀三年政溃,在試婚紗的時候發(fā)現(xiàn)自己被綠了趾访。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,991評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡董虱,死狀恐怖扼鞋,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情愤诱,我是刑警寧澤云头,帶...
    沈念sama閱讀 35,706評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站淫半,受9級特大地震影響溃槐,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜科吭,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,329評論 3 330
  • 文/蒙蒙 一昏滴、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧对人,春花似錦谣殊、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至势告,卻和暖如春蛇捌,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背咱台。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評論 1 270
  • 我被黑心中介騙來泰國打工豁陆, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人吵护。 一個月前我還...
    沈念sama閱讀 48,158評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親馅而。 傳聞我的和親對象是個殘疾皇子祥诽,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,941評論 2 355

推薦閱讀更多精彩內(nèi)容