摘要:現(xiàn)在終止一個(gè)線程,基本上只能靠曲線救國(guó)式的中斷來(lái)實(shí)現(xiàn)。中斷機(jī)制的核心在于中斷狀態(tài)和異常中斷狀態(tài)設(shè)置一個(gè)中斷狀態(tài)清除一個(gè)中斷狀態(tài)方法同時(shí)會(huì)返回線程原來(lái)的中斷的狀態(tài)。中斷異常中斷異常一般是線程被中斷后,在一些類型的方法如中拋出。
前言
系列文章目錄
線程中斷是一個(gè)很重要的概念,通常,取消一個(gè)任務(wù)的執(zhí)行,最好的,同時(shí)也是最合理的方法,就是通過(guò)中斷。
本篇我們主要還是通過(guò)源碼分析來(lái)看看中斷的概念。
本文的源碼基于JDK1.8
Interrupt status & InterruptedExceptionjava線程的中斷機(jī)制為我們提供了一個(gè)契機(jī),使被中斷的線程能夠有機(jī)會(huì)從當(dāng)前的任務(wù)中跳脫出來(lái)。而中斷機(jī)制的最核心的兩個(gè)概念就是interrupt status 和 InterruptedException。
java中對(duì)于中斷的大部分操作無(wú)外乎以下兩點(diǎn):
設(shè)置或者清除中斷標(biāo)志位
拋出InterruptedException
interrupt status在java中,每一個(gè)線程都有一個(gè)中斷標(biāo)志位,表征了當(dāng)前線程是否處于被中斷狀態(tài),我們可以把這個(gè)標(biāo)識(shí)位理解成一個(gè)boolean類型的變量,當(dāng)我們中斷一個(gè)線程時(shí),將該標(biāo)識(shí)位設(shè)為true,當(dāng)我們清除中斷狀態(tài)時(shí),將其設(shè)置為false, 其偽代碼如下:
(注意,本文的偽代碼部分是我個(gè)人所寫,并不權(quán)威,只是幫助我自己理解寫的)
// 注意,這是偽代碼?。?! // 注意,這是偽代碼?。?! // 注意,這是偽代碼?。?! public class Thread implements Runnable { private boolean interruptFlag; // 中斷標(biāo)志位 public boolean getInterruptFlag() { return this.interruptFlag; } public void setInterruptFlag(boolean flag) { this.interruptFlag = flag; } }
然而,在Thread線程類里面,并沒有類似中斷標(biāo)志位的屬性,但是提供了獲取中斷標(biāo)志位的接口:
/** * Tests if some Thread has been interrupted. The interrupted state * is reset or not based on the value of ClearInterrupted that is * passed. */ private native boolean isInterrupted(boolean ClearInterrupted);
這是一個(gè)native方法,同時(shí)也是一個(gè)private方法,該方法除了能夠返回當(dāng)前線程的中斷狀態(tài),還能根據(jù)ClearInterrupted參數(shù)來(lái)決定要不要重置中斷標(biāo)志位(reset操作相當(dāng)于上面的interruptFlag = false)。
Thread類提供了兩個(gè)public方法來(lái)使用該native方法:
public boolean isInterrupted() { return isInterrupted(false); } public static boolean interrupted() { return currentThread().isInterrupted(true); }
其中isInterrupted調(diào)用了isInterrupted(false), ClearInterrupted參數(shù)為false, 說(shuō)明它僅僅返回線程實(shí)例的中斷狀態(tài),但是不會(huì)對(duì)現(xiàn)有的中斷狀態(tài)做任何改變,偽代碼可以是:
// 注意,這是偽代碼?。?! // 注意,這是偽代碼?。。?// 注意,這是偽代碼!??! public boolean isInterrupted() { return interruptFlag; //直接返回Thread實(shí)例的中斷狀態(tài) }
而interrupted是一個(gè)靜態(tài)方法,所以它可以由Thread類直接調(diào)用,自然就是作用于當(dāng)前正在執(zhí)行的線程,所以函數(shù)內(nèi)部使用了currentThread()方法,與isInterrupted()方法不同的是,它的ClearInterrupted參數(shù)為true,在返回線程中斷狀態(tài)的同時(shí),重置了中斷標(biāo)識(shí)位,偽代碼可以是:
// 注意,這是偽代碼!??! // 注意,這是偽代碼?。?! // 注意,這是偽代碼!?。?public static boolean interrupted() { Thread current = Thread.currentThread(); // 獲取當(dāng)前正在執(zhí)行的線程 boolean interruptFlag = current.getInterruptFlag(); // 獲取線程的中斷狀態(tài) current.setInterruptFlag(false); // 清除線程的中斷狀態(tài) return interruptFlag; //返回線程的中斷狀態(tài) }
可見,isInterrupted() 和 interrupted() 方法只涉及到中斷狀態(tài)的查詢,最多是多加一步重置中斷狀態(tài),并不牽涉到InterruptedException。
不過(guò)值得一提的是,在我們能使用到的public方法中,interrupted()是我們清除中斷的唯一方法。
InterruptedException我們直接來(lái)看的源碼:
/** * Thrown when a thread is waiting, sleeping, or otherwise occupied, * and the thread is interrupted, either before or during the activity. * Occasionally a method may wish to test whether the current * thread has been interrupted, and if so, to immediately throw * this exception. The following code can be used to achieve * this effect: ** if (Thread.interrupted()) // Clears interrupted status! * throw new InterruptedException(); ** * @author Frank Yellin * @see java.lang.Object#wait() * @see java.lang.Object#wait(long) * @see java.lang.Object#wait(long, int) * @see java.lang.Thread#sleep(long) * @see java.lang.Thread#interrupt() * @see java.lang.Thread#interrupted() * @since JDK1.0 */ public class InterruptedException extends Exception { private static final long serialVersionUID = 6700697376100628473L; /** * Constructs anInterruptedException
with no detail message. */ public InterruptedException() { super(); } /** * Constructs anInterruptedException
with the * specified detail message. * * @param s the detail message. */ public InterruptedException(String s) { super(s); } }
上面的注釋是說(shuō),在線程處于“waiting, sleeping”甚至是正在運(yùn)行的過(guò)程中,如果被中斷了,就可以拋出該異常,我們先來(lái)回顧一下我們前面遇到過(guò)的拋出InterruptedException異常的例子:
(1) wait(long timeout)方法中的InterruptedException
/* * * @param timeout the maximum time to wait in milliseconds. * @throws IllegalArgumentException if the value of timeout is * negative. * @throws IllegalMonitorStateException if the current thread is not * the owner of the object"s monitor. * @throws InterruptedException if any thread interrupted the * current thread before or while the current thread * was waiting for a notification. The interrupted * status of the current thread is cleared when * this exception is thrown. * @see java.lang.Object#notify() * @see java.lang.Object#notifyAll() */ public final native void wait(long timeout) throws InterruptedException;
該方法的注釋中提到,如果在有別的線程在當(dāng)前線程進(jìn)入waiting狀態(tài)之前或者已經(jīng)進(jìn)入waiting狀態(tài)之后中斷了當(dāng)前線程,該方法就會(huì)拋出InterruptedException,同時(shí),異常拋出后,當(dāng)前線程的中斷狀態(tài)也會(huì)被清除。
(2) sleep(long millis)方法中的InterruptedException
/* @param millis * the length of time to sleep in milliseconds * * @throws IllegalArgumentException * if the value of {@code millis} is negative * * @throws InterruptedException * if any thread has interrupted the current thread. The * interrupted status of the current thread is * cleared when this exception is thrown. */ public static native void sleep(long millis) throws InterruptedException;
與上面的wait方法一致,如果當(dāng)前線程被中斷了,sleep方法會(huì)拋出InterruptedException,并且清除中斷狀態(tài)。
如果有其他方法直接或間接的調(diào)用了這兩個(gè)方法,那他們自然也會(huì)在線程被中斷的時(shí)候拋出InterruptedException,并且清除中斷狀態(tài)。例如:
wait()
wait(long timeout, int nanos)
sleep(long millis, int nanos)
join()
join(long millis)
join(long millis, int nanos)
這里值得注意的是,雖然這些方法會(huì)拋出InterruptedException,但是并不會(huì)終止當(dāng)前線程的執(zhí)行,當(dāng)前線程可以選擇忽略這個(gè)異常。
也就是說(shuō),無(wú)論是設(shè)置interrupt status 還是拋出InterruptedException,它們都是給當(dāng)前線程的建議,當(dāng)前線程可以選擇采納或者不采納,它們并不會(huì)影響當(dāng)前線程的執(zhí)行。
至于在收到這些中斷的建議后,當(dāng)前線程要怎么處理,也完全取決于當(dāng)前線程。
interrupt上面我們說(shuō)了怎么檢查(以及清除)一個(gè)線程的中斷狀態(tài),提到當(dāng)一個(gè)線程被中斷后,有一些方法會(huì)拋出InterruptedException。
下面我們就來(lái)看看怎么中斷一個(gè)線程。
要中斷一個(gè)線程,只需調(diào)用該線程的interrupt方法,其源碼如下:
/** * Interrupts this thread. * *Unless the current thread is interrupting itself, which is * always permitted, the {@link #checkAccess() checkAccess} method * of this thread is invoked, which may cause a {@link * SecurityException} to be thrown. * *
If this thread is blocked in an invocation of the {@link * Object#wait() wait()}, {@link Object#wait(long) wait(long)}, or {@link * Object#wait(long, int) wait(long, int)} methods of the {@link Object} * class, or of the {@link #join()}, {@link #join(long)}, {@link * #join(long, int)}, {@link #sleep(long)}, or {@link #sleep(long, int)}, * methods of this class, then its interrupt status will be cleared and it * will receive an {@link InterruptedException}. * *
If this thread is blocked in an I/O operation upon an {@link * java.nio.channels.InterruptibleChannel InterruptibleChannel} * then the channel will be closed, the thread"s interrupt * status will be set, and the thread will receive a {@link * java.nio.channels.ClosedByInterruptException}. * *
If this thread is blocked in a {@link java.nio.channels.Selector} * then the thread"s interrupt status will be set and it will return * immediately from the selection operation, possibly with a non-zero * value, just as if the selector"s {@link * java.nio.channels.Selector#wakeup wakeup} method were invoked. * *
If none of the previous conditions hold then this thread"s interrupt * status will be set.
* *Interrupting a thread that is not alive need not have any effect. * * @throws SecurityException * if the current thread cannot modify this thread * * @revised 6.0 * @spec JSR-51 */ public void interrupt() { if (this != Thread.currentThread()) checkAccess(); synchronized (blockerLock) { Interruptible b = blocker; if (b != null) { interrupt0(); // Just to set the interrupt flag b.interrupt(this); return; } } interrupt0(); }
上面的注釋很長(zhǎng),我們一段一段來(lái)看:
/** * Interrupts this thread. * *Unless the current thread is interrupting itself, which is * always permitted, the {@link #checkAccess() checkAccess} method * of this thread is invoked, which may cause a {@link * SecurityException} to be thrown. ... */
上面這段首先說(shuō)明了這個(gè)函數(shù)的目的是中斷這個(gè)線程,這個(gè)this thread,當(dāng)然指的就是該方法所屬的線程對(duì)象所代表的線程。
接著說(shuō)明了,一個(gè)線程總是被允許中斷自己,但是我們?nèi)绻胍谝粋€(gè)線程中中斷另一個(gè)線程的執(zhí)行,就需要先通過(guò)checkAccess()檢查權(quán)限。這有可能拋出SecurityException異常, 這段話用代碼體現(xiàn)為:
if (this != Thread.currentThread()) checkAccess();
我們接著往下看:
/* ... *If this thread is blocked in an invocation of the {@link * Object#wait() wait()}, {@link Object#wait(long) wait(long)}, or {@link * Object#wait(long, int) wait(long, int)} methods of the {@link Object} * class, or of the {@link #join()}, {@link #join(long)}, {@link * #join(long, int)}, {@link #sleep(long)}, or {@link #sleep(long, int)}, * methods of this class, then its interrupt status will be cleared and it * will receive an {@link InterruptedException}. ... */
上面這段是說(shuō),如果線程因?yàn)橐韵路椒ǖ恼{(diào)用而處于阻塞中,那么(調(diào)用了interrupt方法之后),線程的中斷標(biāo)志會(huì)被清除,并且收到一個(gè)InterruptedException:
Object的方法
wait()
wait(long)
wait(long, int)
Thread的方法
join()
join(long)
join(long, int)
sleep(long)
sleep(long, int)
關(guān)于這一點(diǎn),我們上面在分析InterruptedException的時(shí)候已經(jīng)分析過(guò)了。
這里插一句,由于上面這些方法在拋出InterruptedException異常后,會(huì)同時(shí)清除中斷標(biāo)識(shí)位,因此當(dāng)我們此時(shí)不想或者無(wú)法傳遞InterruptedException異常,也不對(duì)該異常做任何處理時(shí),我們最好通過(guò)再次調(diào)用interrupt來(lái)恢復(fù)中斷的狀態(tài),以供上層調(diào)用者處理,這一點(diǎn),我們?cè)谥鹦蟹治鯝QS源碼(二): 鎖的釋放的最后就說(shuō)明過(guò)這種用法。
接下來(lái)的兩段注釋是關(guān)于NIO的,我們暫時(shí)不看,直接看最后兩段:
/* ... *If none of the previous conditions hold then this thread"s interrupt * status will be set.
* *Interrupting a thread that is not alive need not have any effect. */
這段話是說(shuō):
如果線程沒有因?yàn)樯厦娴暮瘮?shù)調(diào)用而進(jìn)入阻塞狀態(tài)的話,那么中斷這個(gè)線程僅僅會(huì)設(shè)置它的中斷標(biāo)志位(而不會(huì)拋出InterruptedException)
中斷一個(gè)已經(jīng)終止的線程不會(huì)有任何影響。
注釋看完了之后我們?cè)賮?lái)看代碼部分,其實(shí)代碼部分很簡(jiǎn)單,中間那段同步代碼塊是和NIO有關(guān)的,我們可以暫時(shí)不管,整個(gè)方法的核心調(diào)用就是interrupt0()方法,而它是一個(gè)native方法:
private native void interrupt0();
這個(gè)方法所做的事情很簡(jiǎn)單:
Just to set the interrupt flag
所以,至此我們明白了,所謂“中斷一個(gè)線程”,其實(shí)并不是讓一個(gè)線程停止運(yùn)行,僅僅是將線程的中斷標(biāo)志設(shè)為true, 或者在某些特定情況下拋出一個(gè)InterruptedException,它并不會(huì)直接將一個(gè)線程停掉,在被中斷的線程的角度看來(lái),僅僅是自己的中斷標(biāo)志位被設(shè)為true了,或者自己所執(zhí)行的代碼中拋出了一個(gè)InterruptedException異常,僅此而已。
終止一個(gè)線程既然上面我們提到了,中斷一個(gè)線程并不會(huì)使得該線程停止執(zhí)行,那么我們?cè)撛鯓咏K止一個(gè)線程的執(zhí)行呢。早期的java中提供了stop()方法來(lái)停止一個(gè)線程,但是這個(gè)方法是不安全的,所以已經(jīng)被廢棄了。現(xiàn)在終止一個(gè)線程,基本上只能靠“曲線救國(guó)”式的中斷來(lái)實(shí)現(xiàn)。
終止處于阻塞狀態(tài)的線程前面我們說(shuō)過(guò),當(dāng)一個(gè)線程因?yàn)檎{(diào)用wait,sleep,join方法而進(jìn)入阻塞狀態(tài)后,若在這時(shí)中斷這個(gè)線程,則這些方法將會(huì)拋出InterruptedException異常,我們可以利用這個(gè)異常,使線程跳出阻塞狀態(tài),從而終止線程。
@Override public void run() { while(true) { try { // do some task // blocked by calling wait/sleep/join } catch (InterruptedException ie) { // 如果該線程被中斷,則會(huì)拋出InterruptedException異常 // 我們通過(guò)捕獲這個(gè)異常,使得線程從block狀態(tài)退出 break; // 這里使用break, 可以使我們?cè)诰€程中斷后退出死循環(huán),從而終止線程。 } } }終止處于運(yùn)行狀態(tài)的線程
與中斷一個(gè)處于阻塞狀態(tài)所不同的是,中斷一個(gè)處于運(yùn)行狀態(tài)的線程只會(huì)將該線程的中斷標(biāo)志位設(shè)為true, 而并不會(huì)拋出InterruptedException異常,為了能在運(yùn)行過(guò)程中感知到線程已經(jīng)被中斷了,我們只能通過(guò)不斷地檢查中斷標(biāo)志位來(lái)實(shí)現(xiàn):
@Override public void run() { while (!isInterrupted()) { // do some task... } }
這里,我們每次循環(huán)都會(huì)先檢查中斷標(biāo)志位,只要當(dāng)前線程被中斷了,isInterrupted()方法就會(huì)返回true,從而終止循環(huán)。
終止一個(gè)Alive的線程上面我們分別介紹了怎樣終止一個(gè)處于阻塞狀態(tài)或運(yùn)行狀態(tài)的線程,如果我們將這兩種方法結(jié)合起來(lái),那么就可以同時(shí)應(yīng)對(duì)這兩種狀況,從而能夠終止任意一個(gè)存活的線程:
@Override public void run() { try { // 1. isInterrupted() 用于終止一個(gè)正在運(yùn)行的線程。 while (!isInterrupted()) { // 執(zhí)行任務(wù)... } } catch (InterruptedException ie) { // 2. InterruptedException異常用于終止一個(gè)處于阻塞狀態(tài)的線程 } }
不過(guò)使用這兩者的組合一定要注意,wait,sleep,join等方法拋出InterruptedException有一個(gè)副作用: 清除當(dāng)前的中斷標(biāo)志位,所以不要在異常拋出后不做任何處理,而寄望于用isInterrupted()方法來(lái)判斷,因?yàn)橹袠?biāo)志位已經(jīng)被重置了,所以下面這種寫法是不對(duì)的:
@Override public void run() { //isInterrupted() 用于終止一個(gè)正在運(yùn)行的線程。 while (!isInterrupted()) { try { // 執(zhí)行任務(wù)... } } catch (InterruptedException ie) { // 在這里不做任何處理,僅僅依靠isInterrupted檢測(cè)異常 } } }
這個(gè)方法中,在catch塊中我們檢測(cè)到異常后沒有使用break方法跳出循環(huán),而此時(shí)中斷狀態(tài)已經(jīng)被重置,當(dāng)我們?cè)偃フ{(diào)用isInterrupted,依舊會(huì)返回false, 故線程仍然會(huì)在while循環(huán)中執(zhí)行,無(wú)法被中斷。
總結(jié)Java沒有提供一種安全直接的方法來(lái)停止某個(gè)線程,但是提供了中斷機(jī)制。對(duì)于被中斷的線程,中斷只是一個(gè)建議,至于收到這個(gè)建議后線程要采取什么措施,完全由線程自己決定。
中斷機(jī)制的核心在于中斷狀態(tài)和InterruptedException異常
中斷狀態(tài):
設(shè)置一個(gè)中斷狀態(tài): Thread#interrupt
清除一個(gè)中斷狀態(tài): Thread.interrupted
Thread.interrupted方法同時(shí)會(huì)返回線程原來(lái)的中斷的狀態(tài)。
如果僅僅想查看線程當(dāng)前的中斷狀態(tài)而不清除原來(lái)的狀態(tài),則應(yīng)該使用Thread#isInterrupted。
某些阻塞方法在拋出InterruptedException異常后,會(huì)同時(shí)清除中斷狀態(tài)。若不能對(duì)該異常做出處理也無(wú)法向上層拋出,則應(yīng)該通過(guò)再次調(diào)用interrupt方法恢復(fù)中斷狀態(tài),以供上層處理,通常情況下我們都不應(yīng)該屏蔽中斷請(qǐng)求。
中斷異常:
中斷異常一般是線程被中斷后,在一些block類型的方法(如wait,sleep,join)中拋出。
我們可以使用Thread#interrupt中斷一個(gè)線程,被中斷的線程所受的影響為以下兩種之一:
若被中斷前,該線程處于非阻塞狀態(tài),那么該線程的中斷狀態(tài)被設(shè)為true, 除此之外,不會(huì)發(fā)生任何事。
若被中斷前,該線程處于阻塞狀態(tài)(調(diào)用了wait,sleep,join等方法),那么該線程將會(huì)立即從阻塞狀態(tài)中退出,并拋出一個(gè)InterruptedException異常,同時(shí),該線程的中斷狀態(tài)被設(shè)為false, 除此之外,不會(huì)發(fā)生任何事。
無(wú)論是中斷狀態(tài)的改變還是InterruptedException被拋出,這些都是當(dāng)前線程可以感知到的"建議",如果當(dāng)前線程選擇忽略這些建議(例如簡(jiǎn)單地catch住異常繼續(xù)執(zhí)行),那么中斷機(jī)制對(duì)于當(dāng)前線程就沒有任何影響,就好像什么也沒有發(fā)生一樣。
所以,中斷一個(gè)線程,只是傳遞了請(qǐng)求中斷的消息,并不會(huì)直接阻止一個(gè)線程的運(yùn)行。
(完)
查看更多系列文章:系列文章目錄
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/76808.html
摘要:我們知道,這個(gè)函數(shù)將返回當(dāng)前正在執(zhí)行的線程的中斷狀態(tài),并清除它。注意,中斷對(duì)線程來(lái)說(shuō)只是一個(gè)建議,一個(gè)線程被中斷只是其中斷狀態(tài)被設(shè)為線程可以選擇忽略這個(gè)中斷,中斷一個(gè)線程并不會(huì)影響線程的執(zhí)行。 前言 系列文章目錄 上一篇文章 我們逐行分析了獨(dú)占鎖的獲取操作, 本篇文章我們來(lái)看看獨(dú)占鎖的釋放。如果前面的鎖的獲取流程你已經(jīng)趟過(guò)一遍了, 那鎖的釋放部分就很簡(jiǎn)單了, 這篇文章我們直接開始看...
摘要:前言中的線程是使用類實(shí)現(xiàn)的,在初學(xué)的時(shí)候就學(xué)過(guò)了,也在實(shí)踐中用過(guò),不過(guò)一直沒從源碼的角度去看過(guò)它的實(shí)現(xiàn),今天從源碼的角度出發(fā),再次學(xué)習(xí),愿此后對(duì)的實(shí)踐更加得心應(yīng)手。如果一個(gè)線程已經(jīng)啟動(dòng)并且尚未死亡,則該線程處于活動(dòng)狀態(tài)。 showImg(https://segmentfault.com/img/remote/1460000017963014?w=1080&h=720); 前言 Java...
摘要:表示一個(gè)異步任務(wù)的結(jié)果,就是向線程池提交一個(gè)任務(wù)后,它會(huì)返回對(duì)應(yīng)的對(duì)象。它們分別提供兩個(gè)重要的功能阻塞當(dāng)前線程等待一段時(shí)間直到完成或者異常終止取消任務(wù)。此時(shí),線程從中返回,然后檢查當(dāng)前的狀態(tài)已經(jīng)被改變,隨后退出循環(huán)。 0 引言 前段時(shí)間需要把一個(gè)C++的項(xiàng)目port到Java中,因此時(shí)隔三年后重新熟悉了下Java。由于需要一個(gè)通用的線程池,自然而然就想到了Executors。 用了...
摘要:為了避免一篇文章的篇幅過(guò)長(zhǎng),于是一些比較大的主題就都分成幾篇來(lái)講了,這篇文章是筆者所有文章的目錄,將會(huì)持續(xù)更新,以給大家一個(gè)查看系列文章的入口。 前言 大家好,筆者是今年才開始寫博客的,寫作的初衷主要是想記錄和分享自己的學(xué)習(xí)經(jīng)歷。因?yàn)閷懽鞯臅r(shí)候發(fā)現(xiàn),為了弄懂一個(gè)知識(shí),不得不先去了解另外一些知識(shí),這樣以來(lái),為了說(shuō)明一個(gè)問(wèn)題,就要把一系列知識(shí)都了解一遍,寫出來(lái)的文章就特別長(zhǎng)。 為了避免一篇...
摘要:為了避免一篇文章的篇幅過(guò)長(zhǎng),于是一些比較大的主題就都分成幾篇來(lái)講了,這篇文章是筆者所有文章的目錄,將會(huì)持續(xù)更新,以給大家一個(gè)查看系列文章的入口。 前言 大家好,筆者是今年才開始寫博客的,寫作的初衷主要是想記錄和分享自己的學(xué)習(xí)經(jīng)歷。因?yàn)閷懽鞯臅r(shí)候發(fā)現(xiàn),為了弄懂一個(gè)知識(shí),不得不先去了解另外一些知識(shí),這樣以來(lái),為了說(shuō)明一個(gè)問(wèn)題,就要把一系列知識(shí)都了解一遍,寫出來(lái)的文章就特別長(zhǎng)。 為了避免一篇...
閱讀 2659·2021-11-11 16:55
閱讀 697·2021-09-04 16:40
閱讀 3093·2019-08-30 15:54
閱讀 2635·2019-08-30 15:54
閱讀 2424·2019-08-30 15:46
閱讀 418·2019-08-30 15:43
閱讀 3241·2019-08-30 11:11
閱讀 2995·2019-08-28 18:17