話說多線程

1.10 JAVA面試題解惑系列(十)——話說多線程
發表時間: 2008-08-08 關鍵字: java 面試題 多線程 thread 線程池 synchronized 死鎖
作者:臧圩人(zangweiren)
網址:http://zangweiren.javaeye.com
>>>轉載請註明出處!<<<
線程或者說多線程,是我們處理多任務的強大工具。線程和進程是不同的,每個進程都是一個獨立運行的程
序,擁有自己的變量,且不同進程間的變量不能共享;而線程是運行在進程內部的,每個正在運行的進程至少
有一個線程,而且不同的線程之間可以在進程範圍內共享數據。也就是說進程有自己獨立的存儲空間,而線程
是和它所屬的進程內的其他線程共享一個存儲空間。線程的使用可以使我們能夠並行地處理一些事情。線程通
過並行的處理給用戶帶來更好的使用體驗,比如你使用的郵件系統(outlook、Thunderbird、foxmail等),
你當然不希望它們在收取新郵件的時候,導致你連已經收下來的郵件都無法閱讀,而只能等待收取郵件操作執
行完畢。這正是線程的意義所在。
實現線程的方式
實現線程的方式有兩種:
1. 繼承java.lang.Thread,並重寫它的run()方法,將線程的執行主體放入其中。
2. 實現java.lang.Runnable接口,實現它的run()方法,並將線程的執行主體放入其中。
這是繼承Thread類實現線程的示例:
public class ThreadTest extends Thread {
public void run() {
// 在這裏編寫線程執行的主體
// do something
}
}
這是實現Runnable接口實現多線程的示例:
public class RunnableTest implements Runnable {
public void run() {
// 在這裏編寫線程執行的主體
// do something
}
}
這兩種實現方式的區別並不大。繼承Thread類的方式實現起來較爲簡單,但是繼承它的類就不能再繼承別的類
了,因此也就不能繼承別的類的有用的方法了。而使用是想Runnable接口的方式就不存在這個問題了,而且這
種實現方式將線程主體和線程對象本身分離開來,邏輯上也較爲清晰,所以推薦大家更多地採用這種方式。
如何啓動線程
我們通過以上兩種方式實現了一個線程之後,線程的實例並沒有被創建,因此它們也並沒有被運行。我們要啓
動一個線程,必須調用方法來啓動它,這個方法就是Thread類的start()方法,而不是run()方法(既不是我們繼
承Thread類重寫的run()方法,也不是實現Runnable接口的run()方法)。run()方法中包含的是線程的主體,也
就是這個線程被啓動後將要運行的代碼,它跟線程的啓動沒有任何關係。上面兩種實現線程的方式在啓動時會
有所不同。
繼承Thread類的啓動方式:
public class ThreadStartTest {
public static void main(String[] args) {
// 創建一個線程實例
ThreadTest tt = new ThreadTest();
// 啓動線程
tt.start();
}
}
實現Runnable接口的啓動方式:
public class RunnableStartTest {
public static void main(String[] args) {
// 創建一個線程實例
Thread t = new Thread(new RunnableTest());
// 啓動線程
t.start();
}
}
實際上這兩種啓動線程的方式原理是一樣的。首先都是調用本地方法啓動一個線程,其次是在這個線程裏執行
目標對象的run()方法。那麼這個目標對象是什麼呢?爲了弄明白這個問題,我們來看看Thread類的run()方法的
實現:
public void run() {
if (target != null) {
target.run();
}
}
當我們採用實現Runnable接口的方式來實現線程的情況下,在調用new Thread(Runnable target)構造器時,
將實現Runnable接口的類的實例設置成了線程要執行的主體所屬的目標對象target,當線程啓動時,這個實例
的run()方法就被執行了。當我們採用繼承Thread的方式實現線程時,線程的這個run()方法被重寫了,所以當線
程啓動時,執行的是這個對象自身的run()方法。總結起來就一句話,線程類有一個Runnable類型的target屬
性,它是線程啓動後要執行的run()方法所屬的主體,如果我們採用的是繼承Thread類的方式,那麼這個target
就是線程對象自身,如果我們採用的是實現Runnable接口的方式,那麼這個target就是實現了Runnable接口
的類的實例。
線程的狀態
在Java 1.4及以下的版本中,每個線程都具有新建、可運行、阻塞、死亡四種狀態,但是在Java 5.0及以上版本
中,線程的狀態被擴充爲新建、可運行、阻塞、等待、定時等待、死亡六種。線程的狀態完全包含了一個線程
從新建到運行,最後到結束的整個生命週期。線程狀態的具體信息如下:
1. NEW(新建狀態、初始化狀態):線程對象已經被創建,但是還沒有被啓動時的狀態。這段時間就是
在我們調用new命令之後,調用start()方法之前。
2. RUNNABLE(可運行狀態、就緒狀態):在我們調用了線程的start()方法之後線程所處的狀態。處於
RUNNABLE狀態的線程在JAVA虛擬機(JVM)上是運行着的,但是它可能還正在等待操作系統分配給
它相應的運行資源以得以運行。
3. BLOCKED(阻塞狀態、被中斷運行):線程正在等待其它的線程釋放同步鎖,以進入一個同步塊或者
同步方法繼續運行;或者它已經進入了某個同步塊或同步方法,在運行的過程中它調用了某個對象繼承
自java.lang.Object的wait()方法,正在等待重新返回這個同步塊或同步方法。
4. WAITING(等待狀態):當前線程調用了java.lang.Object.wait()、java.lang.Thread.join()或者
java.util.concurrent.locks.LockSupport.park()三個中的任意一個方法,正在等待另外一個線程執行某
個操作。比如一個線程調用了某個對象的wait()方法,正在等待其它線程調用這個對象的notify()或者
notifyAll()(這兩個方法同樣是繼承自Object類)方法來喚醒它;或者一個線程調用了另一個線程的
join()(這個方法屬於Thread類)方法,正在等待這個方法運行結束。
5. TIMED_WAITING(定時等待狀態):當前線程調用了java.lang.Object.wait(long timeout)、
java.lang.Thread.join(long millis)、java.util.concurrent.locks.LockSupport.packNanos(long
nanos)、java.util.concurrent.locks.LockSupport.packUntil(long deadline)四個方法中的任意一個,
進入等待狀態,但是與WAITING狀態不同的是,它有一個最大等待時間,即使等待的條件仍然沒有滿
足,只要到了這個時間它就會自動醒來。
6. TERMINATED(死亡狀態、終止狀態):線程完成執行後的狀態。線程執行完run()方法中的全部代
碼,從該方法中退出,進入TERMINATED狀態。還有一種情況是run()在運行過程中拋出了一個異常,
而這個異常沒有被程序捕獲,導致這個線程異常終止進入TERMINATED狀態。
在Java5.0及以上版本中,線程的全部六種狀態都以枚舉類型的形式定義在java.lang.Thread類中了,代碼如
下:
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
sleep()和wait()的區別
sleep()方法和wait()方法都成產生讓當前運行的線程停止運行的效果,這是它們的共同點。下面我們來詳細說說
它們的不同之處。
sleep()方法是本地方法,屬於Thread類,它有兩種定義:
public static native void sleep(long millis) throws InterruptedException;
public static void sleep(long millis, int nanos) throws InterruptedException {
//other code
}
其中的參數millis代表毫秒數(千分之一秒),nanos代表納秒數(十億分之一秒)。這兩個方法都可以讓調用
它的線程沉睡(停止運行)指定的時間,到了這個時間,線程就會自動醒來,變爲可運行狀態
(RUNNABLE),但這並不表示它馬上就會被運行,因爲線程調度機制恢復線程的運行也需要時間。調用
sleep()方法並不會讓線程釋放它所持有的同步鎖;而且在這期間它也不會阻礙其它線程的運行。上面的連個方
法都聲明拋出一個InterruptedException類型的異常,這是因爲線程在sleep()期間,有可能被持有它的引用的
其它線程調用它的interrupt()方法而中斷。中斷一個線程會導致一個InterruptedException異常的產生,如果
你的程序不捕獲這個異常,線程就會異常終止,進入TERMINATED狀態,如果你的程序捕獲了這個異常,那麼
程序就會繼續執行catch語句塊(可能還有finally語句塊)以及以後的代碼。

爲了更好地理解interrupt()效果,我們來看一下下面這個例子:
public class InterruptTest {
public static void main(String[] args) {
Thread t = new Thread() {
public void run() {
try {
System.out.println("我被執行了-在sleep()方法前");
// 停止運行10分鐘
Thread.sleep(1000 * 60 * 10);
System.out.println("我被執行了-在sleep()方法後");
} catch (InterruptedException e) {
System.out.println("我被執行了-在catch語句塊中");
}
System.out.println("我被執行了-在try{}語句塊後");
}
};
// 啓動線程
t.start();
// 在sleep()結束前中斷它
t.interrupt();
}
}

運行結果:
1. 我被執行了-在sleep()方法前
2. 我被執行了-在catch語句塊中
3. 我被執行了-在try{}語句塊後
wait()方法也是本地方法,屬於Object類,有三個定義:
public final void wait() throws InterruptedException {
//do something
}
public final native void wait(long timeout) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException {
//do something
}
wari()和wait(long timeout,int nanos)方法都是基於wait(long timeout)方法實現的。同樣地,timeout代表毫
秒數,nanos代表納秒數。當調用了某個對象的wait()方法時,當前運行的線程就會轉入等待狀態
(WAITING),等待別的線程再次調用這個對象的notify()或者notifyAll()方法(這兩個方法也是本地方法)喚
醒它,或者到了指定的最大等待時間,線程自動醒來。如果線程擁有某個或某些對象的同步鎖,那麼在調用了
wait()後,這個線程就會釋放它持有的所有同步資源,而不限於這個被調用了wait()方法的對象。wait()方法同
樣會被Thread類的interrupt()方法中斷,併產生一個InterruptedException異常,效果同sleep()方法被中斷一
樣。
實現同步的方式
同步是多線程中的重要概念。同步的使用可以保證在多線程運行的環境中,程序不會產生設計之外的錯誤結
果。同步的實現方式有兩種,同步方法和同步塊,這兩種方式都要用到synchronized關鍵字。
給一個方法增加synchronized修飾符之後就可以使它成爲同步方法,這個方法可以是靜態方法和非靜態方法,
但是不能是抽象類的抽象方法,也不能是接口中的接口方法。
下面代碼是一個同步方法的示例:
public synchronized void aMethod() {
// do something
}

public static synchronized void anotherMethod() {
// do something
}
線程在執行同步方法時是具有排它性的。當任意一個線程進入到一個對象的任意一個同步方法時,這個對象的
所有同步方法都被鎖定了,在此期間,其他任何線程都不能訪問這個對象的任意一個同步方法,直到這個線程
執行完它所調用的同步方法並從中退出,從而導致它釋放了該對象的同步鎖之後。在一個對象被某個線程鎖定
之後,其他線程是可以訪問這個對象的所有非同步方法的。
同步塊的形式雖然與同步方法不同,但是原理和效果是一致的。同步塊是通過鎖定一個指定的對象,來對同步
塊中包含的代碼進行同步;而同步方法是對這個方法塊裏的代碼進行同步,而這種情況下鎖定的對象就是同步
方法所屬的主體對象自身。如果這個方法是靜態同步方法呢?那麼線程鎖定的就不是這個類的對象了,也不是
這個類自身,而是這個類對應的java.lang.Class類型的對象
。同步方法和同步塊之間的相互制約只限於同一個對
象之間,所以靜態同步方法只受它所屬類的其它靜態同步方法的制約,而跟這個類的實例(對象)沒有關係。
下面這段代碼演示了同步塊的實現方式:
public void test() {
// 同步鎖
String lock = "LOCK";
// 同步塊
synchronized (lock) {
// do something
}
int i = 0;
// ...
}
對於作爲同步鎖的對象並沒有什麼特別要求,任意一個對象都可以。如果一個對象既有同步方法,又有同步
塊,那麼當其中任意一個同步方法或者同步塊被某個線程執行時,這個對象就被鎖定了,其他線程無法在此時
訪問這個對象的同步方法,也不能執行同步塊。
synchronized和Lock
Lock是一個接口,它位於Java 5.0新增的java.utils.concurrent包的子包locks中。concurrent包及其子包中的
類都是用來處理多線程編程的。實現Lock接口的類具有與synchronized關鍵字同樣的功能,但是它更加強大一
些。java.utils.concurrent.locks.ReentrantLock是較常用的實現了Lock接口的類。下面是ReentrantLock類的
一個應用實例:
private Lock lock = new ReentrantLock();
public void testLock() {
// 鎖定對象
lock.lock();
try {
// do something
} finally {
// 釋放對對象的鎖定
lock.unlock();
}
}
lock()方法用於鎖定對象,unlock()方法用於釋放對對象的鎖定,他們都是在Lock接口中定義的方法。位於這兩
個方法之間的代碼在被執行時,效果等同於被放在synchronized同步塊中。一般用法是將需要在lock()和
unlock()方法之間執行的代碼放在try{}塊中,並且在finally{}塊中調用unlock()方法,這樣就可以保證即使在執
行代碼拋出異常的情況下,對象的鎖也總是會被釋放,否則的話就會爲死鎖的產生增加可能

使用synchronized關鍵字實現的同步,會把一個對象的所有同步方法和同步塊看做一個整體,只要有一個被某
個線程調用了,其他的就無法被別的線程執行,即使這些方法或同步塊與被調用的代碼之間沒有任何邏輯關
系,這顯然降低了程序的運行效率。而使用Lock就能夠很好地解決這個問題。我們可以把一個對象中按照邏輯
關係把需要同步的方法或代碼進行分組,爲每個組創建一個Lock類型的對象,對實現同步。那麼,當一個同步
塊被執行時,這個線程只會鎖定與當前運行代碼相關的其他代碼最小集合,而並不影響其他線程對其餘同步代
碼的調用執行。
關於死鎖
死鎖就是一個進程中的每個線程都在等待這個進程中的其他線程釋放所佔用的資源,從而導致所有線程都無法
繼續執行的情況。死鎖是多線程編程中一個隱藏的陷阱,它經常發生在多個線程共用資源的時候。在實際開發
中,死鎖一般隱藏的較深,不容易被發現,一旦死鎖現象發生,就必然會導致程序的癱瘓。因此必須避免它的
發生。
程序中必須同時滿足以下四個條件纔會引發死鎖:
1. 互斥(Mutual exclusion):線程所使用的資源中至少有一個是不能共享的,它在同一時刻只能由一
個線程使用。
2. 持有與等待(Hold and wait):至少有一個線程已經持有了資源,並且正在等待獲取其他的線程所持
有的資源。
3. 非搶佔式(No pre-emption):如果一個線程已經持有了某個資源,那麼在這個線程釋放這個資源之
前,別的線程不能把它搶奪過去使用。
4. 循環等待(Circular wait):假設有N個線程在運行,第一個線程持有了一個資源,並且正在等待獲取
第二個線程持有的資源,而第二個線程正在等待獲取第三個線程持有的資源,依此類推……第N個線程
正在等待獲取第一個線程持有的資源,由此形成一個循環等待。
線程池
線程池就像數據庫連接池一樣,是一個對象池。所有的對象池都有一個共同的目的,那就是爲了提高對象的使
用率,從而達到提高程序效率的目的。比如對於Servlet,它被設計爲多線程的(如果它是單線程的,你就可以
想象,當1000個人同時請求一個網頁時,在第一個人獲得請求結果之前,其它999個人都在鬱悶地等待),如
果爲每個用戶的每一次請求都創建一個新的線程對象來運行的話,系統就會在創建線程和銷燬線程上耗費很大
的開銷,大大降低系統的效率。因此,Servlet多線程機制背後有一個線程池在支持,線程池在初始化初期就創
建了一定數量的線程對象,通過提高對這些對象的利用率,避免高頻率地創建對象,從而達到提高程序的效率
的目的。
下面實現一個最簡單的線程池,從中理解它的實現原理。爲此我們定義了四個類,它們的用途及具體實現如
下:
[list=1]
• Task(任務):這是個代表任務的抽象類,其中定義了一個deal()方法,繼承Task抽象類的子類需要實
現這個方法,並把這個任務需要完成的具體工作在deal()方法編碼實現。線程池中的線程之所以被創
建,就是爲了執行各種各樣數量繁多的任務的,爲了方便線程對任務的處理,我們需要用Task抽象類來
保證任務的具體工作統一放在deal()方法裏來完成,這樣也使代碼更加規範。
Task的定義如下:
public abstract class Task {
public enum State {
/* 新建 */NEW, /* 執行中 */RUNNING, /* 已完成 */FINISHED
}
// 任務狀態
private State state = State.NEW;
public void setState(State state) {
this.state = state;
}
public State getState() {
return state;
}
public abstract void deal();
}
• TaskQueue(任務隊列):在同一時刻,可能有很多任務需要執行,而程序在同一時刻只能執行一定
數量的任務,當需要執行的任務數超過了程序所能承受的任務數時怎麼辦呢?這就有了先執行哪些任
務,後執行哪些任務的規則。TaskQueue類就定義了這些規則中的一種,它採用的是FIFO(先進先
出,英文名是First In First Out)的方式,也就是按照任務到達的先後順序執行。
TaskQueue類的定義如下:
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
public class TaskQueue {
private List<Task> queue = new LinkedList<Task>();
// 添加一項任務
public synchronized void addTask(Task task) {
if (task != null) {
queue.add(task);
}
}
// 完成任務後將它從任務隊列中刪除
public synchronized void finishTask(Task task) {
if (task != null) {
task.setState(Task.State.FINISHED);
queue.remove(task);
}
}
// 取得一項待執行任務
public synchronized Task getTask() {
Iterator<Task> it = queue.iterator();
Task task;
while (it.hasNext()) {
task = it.next();
// 尋找一個新建的任務
if (Task.State.NEW.equals(task.getState())) {
// 把任務狀態置爲運行中
task.setState(Task.State.RUNNING);
return task;
}
}
return null;
}
}
addTask(Task task)方法用於當一個新的任務到達時,將它添加到任務隊列中。這裏使用了LinkedList
類來保存任務到達的先後順序。finishTask(Task task)方法用於任務被執行完畢時,將它從任務隊列中
清除出去。getTask()方法用於取得當前要執行的任務。
• TaskThread(執行任務的線程):它繼承自Thread類,專門用於執行任務隊列中的待執行任務。
public class TaskThread extends Thread {
// 該線程所屬的線程池
private ThreadPoolService service;
public TaskThread(ThreadPoolService tps) {
service = tps;
}
public void run() {
// 在線程池運行的狀態下執行任務隊列中的任務
while (service.isRunning()) {
TaskQueue queue = service.getTaskQueue();
Task task = queue.getTask();
if (task != null) {
task.deal();
}
queue.finishTask(task);
}
}
}
• ThreadPoolService(線程池服務類):這是線程池最核心的一個類。它在被創建了時候就創建了幾
個線程對象,但是這些線程並沒有啓動運行,但調用了start()方法啓動線程池服務時,它們才真正運
行。stop()方法可以停止線程池服務,同時停止池中所有線程的運行。而runTask(Task task)方法是將
一個新的待執行任務交與線程池來運行。
ThreadPoolService類的定義如下:
import java.util.ArrayList;
import java.util.List;
public class ThreadPoolService {
// 線程數
public static final int THREAD_COUNT = 5;
// 線程池狀態
private Status status = Status.NEW;
private TaskQueue queue = new TaskQueue();
public enum Status {
/* 新建 */NEW, /* 提供服務中 */RUNNING, /* 停止服務 */TERMINATED,
}
private List<Thread> threads = new ArrayList<Thread>();
public ThreadPoolService() {
for (int i = 0; i < THREAD_COUNT; i++) {
Thread t = new TaskThread(this);
threads.add(t);
}
}
// 啓動服務
public void start() {
this.status = Status.RUNNING;
for (int i = 0; i < THREAD_COUNT; i++) {
threads.get(i).start();
}
}
// 停止服務
public void stop() {
this.status = Status.TERMINATED;
}
// 是否正在運行
public boolean isRunning() {
return status == Status.RUNNING;
}
// 執行任務
public void runTask(Task task) {
queue.addTask(task);
}
protected TaskQueue getTaskQueue() {
return queue;
}
}
[/list]
完成了上面四個類,我們就實現了一個簡單的線程池。現在我們就可以使用它了,下面的代碼做了一個
簡單的示例:
public class SimpleTaskTest extends Task {
@Override
public void deal() {
// do something
}
public static void main(String[] args) throws InterruptedException {
ThreadPoolService service = new ThreadPoolService();
service.start();
// 執行十次任務
for (int i = 0; i < 10; i++) {
service.runTask(new SimpleTaskTest());
}
// 睡眠1秒鐘,等待所有任務執行完畢
Thread.sleep(1000);
service.stop();
}
}
當然,我們實現的是最簡單的,這裏只是爲了演示線程池的實現原理。在實際應用中,根據情況的不
同,可以做很多優化。比如:
◦ 調整任務隊列的規則,給任務設置優先級,級別高的任務優先執行。
◦ 動態維護線程池,當待執行任務數量較多時,增加線程的數量,加快任務的執行速度;當任務
較少時,回收一部分長期閒置的線程,減少對系統資源的消耗。
事實上Java5.0及以上版本已經爲我們提供了線程池功能,無需再重新實現。這些類位於
java.util.concurrent包中。
Executors類提供了一組創建線程池對象的方法,常用的有一下幾個:
public static ExecutorService newCachedThreadPool() {
// other code
}
public static ExecutorService newFixedThreadPool(int nThreads) {
// other code
}
public static ExecutorService newSingleThreadExecutor() {
// other code
}
newCachedThreadPool()方法創建一個動態的線程池,其中線程的數量會根據實際需要來創建和回
收,適合於執行大量短期任務的情況;newFixedThreadPool(int nThreads)方法創建一個包含固定數
量線程對象的線程池,nThreads代表要創建的線程數,如果某個線程在運行的過程中因爲異常而終止
了,那麼一個新的線程會被創建和啓動來代替它;而newSingleThreadExecutor()方法則只在線程池中
創建一個線程,來執行所有的任務。
這三個方法都返回了一個ExecutorService類型的對象。實際上,ExecutorService是一個接口,它的
submit()方法負責接收任務並交與線程池中的線程去運行。submit()方法能夠接受Callable和Runnable
兩種類型的對象。它們的用法和區別如下:
1. Runnable接口:繼承Runnable接口的類要實現它的run()方法,並將執行任務的代碼放入其
中,run()方法沒有返回值。適合於只做某種操作,不關心運行結果的情況。
2. Callable接口:繼承Callable接口的類要實現它的call()方法,並將執行任務的代碼放入其中,
call()將任務的執行結果作爲返回值。適合於執行某種操作後,需要知道執行結果的情況。
無論是接收Runnable型參數,還是接收Callable型參數的submit()方法,都會返回一個Future(也是一
個接口)類型的對象。該對象中包含了任務的執行情況以及結果。調用Future的boolean isDone()方法
可以獲知任務是否執行完畢;調用Object get()方法可以獲得任務執行後的返回結果,如果此時任務還
沒有執行完,get()方法會保持等待,直到相應的任務執行完畢後,纔會將結果返回。
我們用下面的一個例子來演示Java5.0中線程池的使用:
import java.util.concurrent.*;
public class ExecutorTest {
public static void main(String[] args) throws InterruptedException,
ExecutionException {
ExecutorService es = Executors.newSingleThreadExecutor();
Future fr = es.submit(new RunnableTest());// 提交任務
Future fc = es.submit(new CallableTest());// 提交任務
// 取得返回值並輸出
System.out.println((String) fc.get());
// 檢查任務是否執行完畢
if (fr.isDone()) {
System.out.println("執行完畢-RunnableTest.run()");
} else {
System.out.println("未執行完-RunnableTest.run()");
}
// 檢查任務是否執行完畢
if (fc.isDone()) {
System.out.println("執行完畢-CallableTest.run()");
} else {
System.out.println("未執行完-CallableTest.run()");
}
// 停止線程池服務
es.shutdown();
}
}
class RunnableTest implements Runnable {
public void run() {
System.out.println("已經執行-RunnableTest.run()");
}
}
class CallableTest implements Callable {
public Object call() {
System.out.println("已經執行-CallableTest.call()");
return "返回值-CallableTest.call()";
}
}
運行結果:
1. 已經執行-RunnableTest.run()
2. 已經執行-CallableTest.call()
3. 返回值-CallableTest.call()
4. 執行完畢-RunnableTest.run()
5. 執行完畢-CallableTest.run()
使用完線程池之後,需要調用它的shutdown()方法停止服務,否則其中的所有線程都會保持運行,程
序不會退出。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章