Java 并發(fā)編程基礎(chǔ)知識

Java 并發(fā)編程基礎(chǔ)知識

CPU 核心數(shù)和線程數(shù)的關(guān)系

目前的 CPU 有雙核,四核筹煮,八核遮精,一般情況下,它和線程數(shù)是1:1的對應(yīng)關(guān)系败潦,也就是四核 CPU 一般就能并行執(zhí)行 4 個線程本冲。但 Intel 引入超線程技術(shù)后,使核心數(shù)與線程數(shù)形成1:2的關(guān)系,也就是我們常說的 4核8線程

線程調(diào)度與線程調(diào)度模型

任意時刻劫扒,只有一個線程占用 CPU檬洞,處于運行狀態(tài)。而多線程并發(fā)執(zhí)行就是輪流獲取 CPU 執(zhí)行權(quán)沟饥。

  • 分時調(diào)用模型

輪流獲取 CPU 執(zhí)行權(quán)添怔,均分 CPU 執(zhí)行時間。

  • 搶占式調(diào)度模型

優(yōu)先級高的線程優(yōu)先獲取 CPU 執(zhí)行權(quán)贤旷,這也是 JVM 采用的線程調(diào)度模型广料。

進程與線程

進程是程序運行資源分配的最小單位。

這些資源就包括 CPU幼驶,內(nèi)存空間艾杏,磁盤 IO 等。同一進程中的多個線程共享該進程的所有資源县遣,而不同進程是彼此獨立的糜颠。舉個栗子汹族,在手機開啟一個 APP 實際上就是開啟了一個進程了萧求,而每一個 APP 程序會有很多線程在跑,例如刷新 UI 的線程等顶瞒,所以說進程是包含線程的夸政。

線程是 CPU 調(diào)度的最小單位,必須依賴于進程而存在榴徐。

線程是比進程更小的守问,能獨立運行的基本單位匀归,每一個線程都有一個程序計數(shù)器,虛擬機棧等耗帕,它可以與同一進程下的其它線程共享該進程的資源穆端。

并行與并發(fā)

  • 并行

指應(yīng)用能夠同時執(zhí)行不同的任務(wù)。例如多輛汽車可以同時在同一條公路上的不同車道上并行通行仿便。

  • 并發(fā)

指應(yīng)用能夠交替執(zhí)行不同的任務(wù)体啰,因為一般的計算機只有一個 CPU 也就是只有一顆心,如果一個 CPU 要運行多個進程嗽仪,那就需要使用到并發(fā)技術(shù)了荒勇,例如時間片輪轉(zhuǎn)進程調(diào)度算。比如單 CPU 核心下執(zhí)行多線程任務(wù)時并非同時執(zhí)行多個任務(wù)闻坚,而是以一個非常短的時間不斷地切換執(zhí)行不同的任務(wù)沽翔,這個時間是我們無法察覺的出來的。

兩者的區(qū)別:并行是同時執(zhí)行窿凤,并發(fā)是交替執(zhí)行仅偎。

高并發(fā)編程的意義

  • 充分利用 CPU 資源

線程是 CPU 調(diào)度的最小的單位,我們的程序是跑在 CPU 的一個核中的某一個線程中的雳殊,如果在程序中只有一個線程哨颂,那么對于雙核心4線程的CPU來說就要浪費了 3/4 的 CPU 性能了,所以在創(chuàng)建線程的時候需要合理的利用 CPU 資源相种,具體可以看看 AsyncTask 內(nèi)部的線程池是如何設(shè)計的威恼。

  • 加快響應(yīng)用戶的時間

如果多個任務(wù)時串行執(zhí)行的話,那么效果肯定不好寝并,在移動端開發(fā)中箫措,并發(fā)執(zhí)行多個任務(wù)是很常見的操作,最常見的就是多線程下載了衬潦。

  • 可以使代碼模塊化斤蔓,異步化,簡單化

在 Android 應(yīng)用程序開發(fā)中的镀岛,一般 UI 線程負責(zé)去更新界面相關(guān)的工作弦牡,而一些 IO,網(wǎng)絡(luò)等操作一般會放在異步的工作線程去執(zhí)行漂羊,這樣使得不同的線程各司其職驾锰,異步化。

線程之間的安全性問題

問題1:同一進程間的多個線程是可以共享該進程的資源的走越,當(dāng)多個線程訪問共享變量時椭豫,就會線程安全問題。

問題2:為了解決線程之間的同步問題,一般會引入鎖機制赏酥,對于線程之間搶奪鎖時也是有可能造成死鎖問題喳整。

問題3:在 JVM 內(nèi)存模型中,每一個線程都會分配一個虛擬機棧裸扶,這個虛擬機棧是需要占用內(nèi)存空間的框都,如果無限制的創(chuàng)建線程的話,會耗盡系統(tǒng)的內(nèi)存呵晨。

線程的開啟與關(guān)閉

線程的啟動

  • 派生 Thread 類
//開啟一個線程
Thread thread = new Thread() {
    @Override
    public void run() {
        super.run();
        System.out.println("thread started");
    }
};
thread.start();
  • 實現(xiàn) Runnable 接口瞬项,將其交給 Thread 類去執(zhí)行
Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("runnable run invoked");
    }
});
thread.start();
  • 實現(xiàn) Callable 接口

因為 Thread 構(gòu)造中只接收 Runnable 類型的接口,需要實現(xiàn)將 Callable 的實現(xiàn)類包裝為 FutureTask 之后交給 Thread 類去執(zhí)行何荚。

Callable<String> callable = new Callable<String>() {
    @Override
    public String call() throws Exception {
        Thread.sleep(1500);
        return "work done!";
    }
};

FutureTask<String> futureTask = new FutureTask(callable);
Thread thread = new Thread(futureTask);
thread.start();
try {
    //get() 是一個阻塞式的操作囱淋,一直等待 call 方法執(zhí)行完畢。
    String resule = futureTask.get();
} catch (ExecutionException e) {
    e.printStackTrace();
}

再來看 FutureTask 的應(yīng)用:我們觀察到 AsyncTask 內(nèi)部就使用到了 FutureTask 餐塘,因為在 doInBackground() 需要有一個返回值妥衣,而恰好 Callable 就可以實現(xiàn)子線程執(zhí)行完有返回值。使用 FutureTask 來封裝 WorkRunnable 對象戒傻,然后再交給對應(yīng)的線程池去執(zhí)行税手。具體的代碼如下:

FutureTask

總結(jié):對于第1,2兩種方式是在線程執(zhí)行完畢后需纳,無法得到執(zhí)行的結(jié)果芦倒,而第三種方式是可以獲取執(zhí)行結(jié)果的。

線程的終止

  • 線程自然終止

也就是 run 執(zhí)行完畢不翩,或者是內(nèi)部出現(xiàn)一個異常導(dǎo)致線程提前結(jié)束兵扬。

  • 暴力終止

    • suspend() 使線程掛起,并且不會釋放線程占有的資源(例如鎖)口蝠,resume() 使掛起的線程恢復(fù)器钟。

    • stop() 暴力停止,立刻釋放鎖妙蔗,導(dǎo)致共享資源可能不同步傲霸。

以上幾個方法已經(jīng)被 JDK 標記為廢棄狀態(tài)。

  • interrupt() 安全終止

第一種情況:如果線程處于正常運行狀態(tài)眉反,那么線程的中斷狀態(tài)會被置為 true 昙啄,并且線程還是會正常執(zhí)行,僅此而已寸五。

第二種情況:如果當(dāng)前線程如果是處于阻塞狀態(tài)梳凛,例如調(diào)用了 wait,join,sleep 等方法,那么則會拋出InterruptedException異常播歼。

總結(jié): interrupt() 并不能中斷線程伶跷,需要線程配合才能實現(xiàn)中斷操作掰读。

示例01

public class EndThread extends Thread {

    @Override
    public void run() {
        super.run();

        System.out.println("isInterrupted:"+isInterrupted());
        while (true) {//盡管在其他線程中調(diào)用了 interrupt() 方法秘狞,但是線程并不會終止
            //while (!isInterrupted()){
            System.out.println("I am Thread body");
        }
//        System.out.println("isInterrupted:"+isInterrupted());
    }

    public static void main(String[] args) throws InterruptedException {

        final EndThread endThread = new EndThread();

        endThread.start();

        Thread.sleep(10);
        //在其他線程中去調(diào)用線程的interrupt方法給線程打一個終止的標記
        endThread.interrupt();
    }
}

上面的代碼時在線程體中執(zhí)行一個 while(true)的死循環(huán)叭莫,然后在其他線程中調(diào)用 endThread.interrupt() 觀察當(dāng)前線程是否會執(zhí)行完畢。

經(jīng)測試:在調(diào)用線程的 interrupt()方法之后烁试,while(true)是不會結(jié)束循環(huán)的雇初,也就是線程還是一直在運行著。所以說 interrupt() 并不會應(yīng)用 run 方法的執(zhí)行

示例02

下面再來看看另一個關(guān)于 interrupt 方法的使用

在線程體內(nèi)部 sleep(2000) 并且 try catch 對應(yīng)的 InterruptedException 異常减响,如果在其他線程調(diào)用了 endThread.interrupt() 那么此處就會拋出 InterruptedException 異常靖诗,并且會isInterrupted() 會返回 false 。

public class EndThread2 extends Thread {
    @Override
    public void run() {
        super.run();

        System.out.println("isInterrupted:" + isInterrupted());
        try {
            //在其他線程調(diào)用 endThread.interrupt() 之后支示,會拋出 InterruptedException 異常并且線程的
            // isInterrupted 會被標記為 false刊橘。因此最后輸出的結(jié)果還是 false
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.out.println("catch InterruptedException");
        }
        System.out.println("isInterrupted:" + isInterrupted());
    }

    public static void main(String[] args) throws InterruptedException {

        final EndThread2 endThread = new EndThread2();

        endThread.start();

        Thread.sleep(300);
        //在其他線程中去調(diào)用線程的interrupt方法給線程打一個終止的標記
        endThread.interrupt();

    }
}

執(zhí)行結(jié)果:

isInterrupted:false
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at endthread.EndThread.run(EndThread.java:28)
catch InterruptedException
isInterrupted:false

示例03

示例01中的 while(true) 修改為 while(!isInterrupted()){},在外界調(diào)用了 endThread.interrupt()之后颂鸿,線程的 isInterrupted() 就會返回 true促绵,標記著你可以結(jié)束線程了。

public class EndThread extends Thread {

    @Override
    public void run() {
        super.run();

        System.out.println("isInterrupted:"+isInterrupted());
       while (!isInterrupted()){
            System.out.println("I am Thread body");
        }
       System.out.println("isInterrupted:"+isInterrupted());
    }

    public static void main(String[] args) throws InterruptedException {

        final EndThread endThread = new EndThread();

        endThread.start();

        Thread.sleep(10);
        //在其他線程中去調(diào)用線程的interrupt方法給線程打一個終止的標記
        endThread.interrupt();
     )
    }
}

示例04

還有一種方式是設(shè)置一個 boolean 類型的變量 mIsExit 標記嘴纺,當(dāng)線程體內(nèi)部判斷到 mIsExit 為 false 那么就跳出循環(huán)败晴。具體示例代碼如下:

public class EndThread extends Thread {
    //這個變量需要在其他線程中判斷,因此需要設(shè)置為線程可見的
    private volatile boolean mIsExit = true;
    @Override
    public void run() {
        super.run();
        while(mIsExit){
            System.out.println("I am Thread body");
        }
    }

    public static void main(String[] args) throws InterruptedException {

        final EndThread endThread = new EndThread();

        endThread.start();

        Thread.sleep(3);
        //設(shè)置標記為退出狀態(tài)
        endThread.mIsExit = false;
    }
}

線程其他 API

Thread#start() 與 Thread#run()

start() 方法調(diào)用之后會讓一個線程進入就緒等待隊列栽渴,當(dāng)獲取到 CPU 執(zhí)行權(quán)之后會執(zhí)行線程體 run()方法尖坤。

run() 方法只是 Thread 類中一個普通方法,如果手動去調(diào)用闲擦,跟調(diào)用普通方法沒有什么區(qū)別慢味。

Thread#run() 與 Runnable#run()

在 Java 中只有 Thread 才能表示一個線程,而 Runnable 只能表示一個任務(wù)墅冷,任務(wù)是需要交給線程去執(zhí)行的贮缕,當(dāng)出現(xiàn)如下代碼時,你看到可能會懵逼俺榆,執(zhí)行結(jié)果到底是什么感昼?

//接受一個 runnable 接口
Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("runnable run invoked");
    }
}) {
    @Override
    public void run() {
        super.run();
        System.out.println("thread started");
    }
};
thread.start();

以上方式的輸出結(jié)果是:
如果線程 run 方法內(nèi)部調(diào)用了 super.run() 那么輸出結(jié)果如下:

runnable run invoked
thread started

如果線程 run 方法內(nèi)部不調(diào)用 super.run() 那么輸出結(jié)果如下:

thread started

我們可以通過源碼來解答這個問題:在創(chuàng)建線程時,如果往構(gòu)造函數(shù)中傳入一個 Runnable 對象罐脊,那么它會給線程 target 屬性賦值定嗓,并且在線程體執(zhí)行時先判斷 target 是否為空,不為空萍桌,則先執(zhí)行 Runnablerun 方法宵溅,再執(zhí)行當(dāng)前線程體的子類中的 run 方法。

//Thread.java
private Runnable target;

public Thread(ThreadGroup group, Runnable target, String name,
              long stackSize) {
    init(group, target, name, stackSize);
}

@Override
public void run() {
    if (target != null) {
        //如果傳入的 Runnable 實例上炎,那么會調(diào)用調(diào)用 runnable 實例的 run 方法
        target.run();
    }
}

yield()

Java線程中的 Thread.yield() 方法恃逻,譯為線程讓步雏搂。顧名思義,就是說當(dāng)一個線程使用了這個方法之后寇损,它會放棄自己CPU執(zhí)行權(quán)凸郑,讓自己或者其它的線程運行,注意是讓自己或者其他線程運行矛市,并不是單純的讓給其他線程芙沥。yield()的作用是讓步。它能讓當(dāng)前線程由“運行狀態(tài)”進入到“就緒狀態(tài)”浊吏,從而讓其它具有相同優(yōu)先級的等待線程獲取執(zhí)行權(quán)而昨;但是,并不能保證在當(dāng)前線程調(diào)用yield()`之后找田,其它具有相同優(yōu)先級的線程就一定能獲得執(zhí)行權(quán)歌憨;也有可能是當(dāng)前線程又進入到“運行狀態(tài)”繼續(xù)運行!

public class YieldDemo {

    public static void main(String[] args) {
        Yieldtask yieldtask = new Yieldtask();

        Thread t1 = new Thread(yieldtask, "A");
        Thread t2 = new Thread(yieldtask, "B");
        Thread t3 = new Thread(yieldtask, "C");
        Thread t4 = new Thread(yieldtask, "D");

        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }

    public static class Yieldtask implements Runnable {


        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "-" + i);

                if (i == 5) {
                    Thread.yield();
                }
            }
        }
    }
}

從運行結(jié)果可以看到墩衙,調(diào)用 yield() 方法的線程之后务嫡,CPU 執(zhí)行權(quán)不一定會給其他線程搶到,有可能還是當(dāng)前線程搶到 CPU 執(zhí)行權(quán)底桂。

...
A-0
D-4
D-5
D-6//在這里植袍,還是執(zhí)行 D 這個線程
D-7
D-8
D-9
B-2
B-3
B-4
B-5
C-6
B-6
A-1
A-2
A-3
A-4
...

join()

join() 把指定的線程加入到當(dāng)前線程,可以將兩個交替執(zhí)行的線程合并為順序執(zhí)行的線程籽懦。比如在線程B中調(diào)用了線程A的Join()方法于个,直到線程A執(zhí)行完畢后,才會繼續(xù)執(zhí)行線程B暮顺。

這里舉一個栗子:午飯時間到了厅篓,老王(線程A)調(diào)用了微波爐熱飯的方法,預(yù)約了4分鐘捶码,當(dāng)微波爐跑了2分鐘羽氮,這時老王看到一個美女(線程B)過來,這時主動調(diào)用了線程B.join()方法惫恼,此時把微波爐讓給了美女档押,這時老王就等待美女熱飯,直到熱好美女的飯之后祈纯,才輪到老王去繼續(xù)熱飯令宿,這就是一個 join() 方法的作用。

下面畫了一個草圖:

join
public class JoinDemo implements Runnable {

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        JoinDemo joinDemo = new JoinDemo();
        Thread thread = new Thread(joinDemo);
        thread.start();
        //join() 會阻塞當(dāng)前線程腕窥,等待子線程執(zhí)行完畢
        //在這里主線程會等待子線程執(zhí)行完畢之后才能往下執(zhí)行粒没。
        thread.join();
        System.out.println(Thread.currentThread().getName() + "  " + " done!");
    }

    @Override
    public void run() {
        try {
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName()+" done!");        
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

執(zhí)行結(jié)果:
Thread-0 done!
main   done簇爆!

線程狀態(tài)

在 Java Thread 類中定義了一個 State 枚舉癞松,內(nèi)部定義以下6個線程狀態(tài)爽撒。

Thread 狀態(tài)

在線程的生命周期中,它要經(jīng)過新建(New)响蓉、就緒(Runnable)硕勿、運行(Running)、阻塞(TIME_WAITING,WAITING)和死亡(TERMINATED)五種狀態(tài)厕妖。

我這里繪制一個草圖首尼,描述了各個狀態(tài)之前的切換:

線程狀態(tài)切換圖解.png
  • 新建狀態(tài)(NEW)

新建一個線程對象挑庶,此時并沒有執(zhí)行 start() 方法言秸,這時的線程狀態(tài)就是處于新建狀態(tài)。

Thread thread = new Thread(){
    public void run() {...}
};
  • 就緒狀態(tài)(RUNNABLE)

start() 方法調(diào)用之后會讓一個線程進入就緒等待隊列迎捺,JVM 會為其創(chuàng)建對應(yīng)的虛擬機棧举畸,本地方法棧和程序計數(shù)器。處于這個狀態(tài)的線程還沒開始運行凳枝,需要等待 CPU 的執(zhí)行權(quán)抄沮。

  • 運行狀態(tài)(RUNNING)

處于就緒狀態(tài)的線程在搶到 CPU 執(zhí)行權(quán)之后,就處于運行狀態(tài)岖瑰,執(zhí)行該線程的 run 方法叛买。

  • 阻塞狀態(tài)(BLOCKED)

TIME_WAIT/WAIT:

運行的線程執(zhí)行 wait() /wait(long),join()/join(long)方法,那么這些線程放棄 CPU 執(zhí)行和線程持有的鎖蹋订,并且 JVM 會將這些線程放入到等待池中率挣。

BLOCKED:

運行時的線程在獲取同步鎖時,發(fā)現(xiàn)鎖被其他線程持有露戒,這時 JVM 會將當(dāng)前線程放入鎖池中椒功。

  • 結(jié)束(TERMINATED)

線程 run 方法執(zhí)行完畢,或者線程被異常終止智什。

總結(jié)

以上是關(guān)于 Java 多線程的一些基本知識點的總結(jié)动漾,有任何不對的地方,請多多指正荠锭。

記錄于2019年4月7日

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末旱眯,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子证九,更是在濱河造成了極大的恐慌删豺,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件甫贯,死亡現(xiàn)場離奇詭異吼鳞,居然都是意外死亡,警方通過查閱死者的電腦和手機叫搁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進店門赔桌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來供炎,“玉大人,你說我怎么就攤上這事疾党∫艚耄” “怎么了?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵雪位,是天一觀的道長竭钝。 經(jīng)常有香客問我,道長雹洗,這世上最難降的妖魔是什么香罐? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮时肿,結(jié)果婚禮上庇茫,老公的妹妹穿的比我還像新娘。我一直安慰自己螃成,他們只是感情好旦签,可當(dāng)我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著寸宏,像睡著了一般宁炫。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上氮凝,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天羔巢,我揣著相機與錄音,去河邊找鬼覆醇。 笑死朵纷,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的永脓。 我是一名探鬼主播袍辞,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼常摧!你這毒婦竟也來了搅吁?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤落午,失蹤者是張志新(化名)和其女友劉穎谎懦,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體溃斋,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡界拦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了梗劫。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片享甸。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡截碴,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蛉威,到底是詐尸還是另有隱情日丹,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布蚯嫌,位于F島的核電站哲虾,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏择示。R本人自食惡果不足惜束凑,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望对妄。 院中可真熱鬧湘今,春花似錦敢朱、人聲如沸剪菱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽孝常。三九已至,卻和暖如春蚓哩,著一層夾襖步出監(jiān)牢的瞬間构灸,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工岸梨, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留喜颁,地道東北人。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓曹阔,卻偏偏與公主長得像半开,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子赃份,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,512評論 2 359

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