java學(xué)習(xí)筆記:并發(fā)編程實(shí)戰(zhàn)

一场仲、線程安全性

在線程安全性中法竞,最核心的概念是正確性,而正確性的含義是:某個(gè)類的行為與其規(guī)范完全一致参歹。這里的規(guī)范可以粗略理解為在各種限定條件下仰楚,類對(duì)象的結(jié)果與預(yù)期一致。在單線程中犬庇,正確性可以近似的定義為“所見即所知(we know it when we see it)”僧界。在大概明確了“安全性”的概念后,我們可以認(rèn)為線程安全性就是:當(dāng)多個(gè)線程訪問某個(gè)類時(shí)臭挽,這個(gè)類始終都能表現(xiàn)出正確的行為捂襟,那么這個(gè)類就可以認(rèn)為是線程安全的。

當(dāng)多個(gè)線程訪問某個(gè)類時(shí)埋哟,不管運(yùn)行環(huán)境采用何種調(diào)度方式或者這些線程將如何交替執(zhí)行笆豁,并且在主調(diào)代碼中不需要任何額外的同步或協(xié)同郎汪,這個(gè)類都能表現(xiàn)出正確的行為,那么就稱這個(gè)類是線程安全的闯狱。

也可以將線程安全類認(rèn)為是一個(gè)在并發(fā)環(huán)境和單線程環(huán)境中都不會(huì)被破壞的類煞赢。如果某個(gè)類在單線程環(huán)境下都不是線程安全類,那么它肯定不是線程安全類哄孤。下面是一個(gè)線程安全類的示例:

public class StatelessFactorizer implements Servlet{

public void service(ServletRequest req, ServletResponse resp){

BigInteger i = extractFromRequest(req);

BigInteger[] factors = factor(i);

encodeIntoResponse(resp, factors);

}

}

這個(gè)StatelessFactorizer是無狀態(tài)的:它既不包含任何域照筑,也不包含任何對(duì)其他類中域的引用。方法中的局部變量只能由正在執(zhí)行的線程訪問瘦陈。如果同時(shí)有多個(gè)線程在訪問StatelessFactorizer凝危,那么這些線程之間將不會(huì)互相影響,因?yàn)榫€程之間并沒有共享狀態(tài)晨逝,就好像在訪問不同的實(shí)例蛾默。

由于線程訪問無狀態(tài)對(duì)象的行為并不會(huì)影響其他線程中操作的正確性,因此無狀態(tài)對(duì)象是線程安全的捉貌,且無狀態(tài)對(duì)象一定是線程安全的支鸡。

二、原子性

什么是原子性呢趁窃?其實(shí)原子性就是一個(gè)不可再分割的性質(zhì)牧挣,不能再分成更細(xì)的粒度。

如果我們?cè)趧倓偟氖纠性黾右粋€(gè)狀態(tài)(既一個(gè)計(jì)數(shù)器)醒陆,用來統(tǒng)計(jì)已處理請(qǐng)求數(shù)量瀑构,每處理一個(gè)請(qǐng)求就將這個(gè)值加1,程序如下所示:

public class StatelessFactorizer implements Servlet{

private long count = 0;

public long getCount(){return count;}

public void service(ServletRequest req, ServletResponse resp){

BigInteger i = extractFromRequest(req);

BigInteger[] factors = factor(i);

++count;

encodeIntoResponse(resp, factors);

}

}

在上面的程序示例中刨摩,咋一看沒問題寺晌,++count看起來像是一個(gè)操作,但是這個(gè)自增操作并非原子性的码邻。因?yàn)閷?shí)際上折剃,它包含了三個(gè)操作:“讀取-修改-寫入”的操作序列。每個(gè)操作都依賴于前面之前的狀態(tài)像屋。如果此時(shí)有兩個(gè)線程A怕犁、B,如果A線程已經(jīng)進(jìn)行到了修改操作己莺,此時(shí)如果B線程進(jìn)行了讀取奏甫,那么最終A、B線程寫入的值是一樣的凌受,這樣就與預(yù)期結(jié)果偏差了1.

雖然在這里看起來阵子,結(jié)果偏離了一些可以接受,但是如果這個(gè)計(jì)數(shù)器的值被用來生成數(shù)值序列或唯一的對(duì)象標(biāo)識(shí)符胜蛉,那么在多次調(diào)用中返回相同的值將導(dǎo)致嚴(yán)重的數(shù)據(jù)完整性問題挠进。

在并發(fā)編程中色乾,像這種由于不恰當(dāng)?shù)膱?zhí)行時(shí)序而出現(xiàn)不正確的結(jié)果是一種非常重要的情況,這種情況叫做“競態(tài)條件(Race Condition)”领突。

競態(tài)條件

當(dāng)某個(gè)計(jì)算的正確性取決于多個(gè)線程的交替執(zhí)行時(shí)序的時(shí)候暖璧,那么就會(huì)發(fā)生競態(tài)條件。常見的競態(tài)條件類型是“先檢查后執(zhí)行”操作君旦,既通過一個(gè)可能失效的觀測結(jié)果來決定下一步的動(dòng)作澎办。

舉個(gè)栗子:你和朋友約好一起去網(wǎng)吧開黑,你當(dāng)了網(wǎng)吧的時(shí)候金砍,發(fā)現(xiàn)你朋友不在局蚀,此時(shí)你可能選擇呆在網(wǎng)吧里等他,也可能去他家找他恕稠,如果你去找他琅绅,那么當(dāng)你出了網(wǎng)吧以后,你在網(wǎng)吧的觀測結(jié)果(朋友不在)就可能失效了谱俭,因?yàn)樗赡茉谀闳ニ艺宜穆飞弦呀?jīng)到了網(wǎng)吧奉件,而你卻去找他了宵蛀。

這個(gè)栗子中昆著,正確的結(jié)果是(你們?cè)诰W(wǎng)吧會(huì)面),但是這個(gè)結(jié)果取決于事件發(fā)生的時(shí)序(既誰先到網(wǎng)吧并且等待對(duì)方的時(shí)長)术陶。這種觀察結(jié)果的失效就是大多數(shù)競態(tài)條件的本質(zhì)——基于一種可能失效的觀測結(jié)果來做出判斷或者執(zhí)行某個(gè)計(jì)算凑懂。

再舉個(gè)栗子,假設(shè)有兩個(gè)線程A梧宫、B接谨,A、B線程都用來判斷某個(gè)文件夾是否存在塘匣,不存在就創(chuàng)建它脓豪,假如當(dāng)A線程發(fā)現(xiàn)文件夾不存在時(shí),正打算創(chuàng)建文件夾忌卤,但是此時(shí)B線程已經(jīng)完成了文件夾的創(chuàng)建扫夜,那么此時(shí)A線程觀測的結(jié)果就已經(jīng)失效了,但是A線程依舊根據(jù)這個(gè)已失效的觀測結(jié)果在進(jìn)行下一步動(dòng)作驰徊,這就可能會(huì)導(dǎo)致各種問題笤闯。

使用“先檢查后執(zhí)行”的一種常見的情況就是延遲初始化。就比如在單例模式中有一種寫法如下:

public class LazyInitRace {

private static LazyInitRace instance = null;

public LazyInitRace getInstance(){

if(instance == null){

instance = new LazyInitRace();

}

return instance;

}

}

這就是典型的延遲初始化棍厂,在單線程中這樣寫沒毛病颗味,但是在多線程環(huán)境中,如果有A牺弹、B線程同時(shí)執(zhí)行g(shù)etInstance()方法浦马,那么結(jié)果可能符合預(yù)期时呀,也可能會(huì)得到兩個(gè)不一樣的對(duì)象。因?yàn)樵贏線程發(fā)現(xiàn)instace為null時(shí)晶默,B線程可能也同時(shí)發(fā)現(xiàn)instace為null退唠。

與大多數(shù)并發(fā)錯(cuò)誤一樣,競態(tài)條件并不總是會(huì)產(chǎn)生錯(cuò)誤荤胁,還需要某種不恰當(dāng)?shù)膱?zhí)行時(shí)序瞧预,但是如果發(fā)生問題,那么可能導(dǎo)致很嚴(yán)重的問題仅政。

在上面的示例中都包含了一組需要以原子方式執(zhí)行(或者說不可分割)的操作垢油。要避免競態(tài)條件問題,就必須在某個(gè)線程修改變量時(shí)圆丹,通過某種方式防止其他線程使用這個(gè)變量滩愁,從而確保其他線程只能在修改操作完成之前或之后讀取和修改狀態(tài),而不是在修改過程中辫封。

在上面統(tǒng)計(jì)已處理請(qǐng)求數(shù)量的示例中硝枉,我們可以使用AtomicLong對(duì)象來替換long,因?yàn)锳tmoicLong類是線程安全類倦微,所以可以保證示例也是示例安全的妻味,但是在添加一個(gè)狀態(tài)變量時(shí),是否還可以通過使用線程安全的對(duì)象來管理而類的狀態(tài)以維護(hù)其線程安全性呢欣福?如下所示:

public class UnsafeCachingFactorizer implements Servlet {

private final AtomicReference<BigInteger> lastNumber = new AtomicReference<>();

private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<>();

public void service(ServletRequest req, ServletResponse resp) {

BigInteger i = extractFromRequest(req);

if (i.equals(lastNumber.get())) {

encodeIntoResponse(resp, lastFactors.get());

} else {

BigInteger[] factors = factor(i);

lastNumber.set(i);

lastFactors.set(factors);

encodeIntoResponse(resp, factors);

}

}

}

在上述例子中责球,雖然兩個(gè)變量都是線程安全的,但是在service方法中依然存在競態(tài)條件拓劝,因?yàn)樵谏鲜隼又谐猓惖牟蛔冃詶l件已經(jīng)被破壞了,只有確保了這個(gè)不變性條件不被破壞郑临,才是正確的栖博。當(dāng)不變性條件中涉及到了多個(gè)變量時(shí),各個(gè)變量之間并不是彼此獨(dú)立的厢洞,而是某個(gè)變量的值會(huì)對(duì)其他變量的值產(chǎn)生約束仇让。因此,當(dāng)更新某一個(gè)變量時(shí)犀变,需要在同一個(gè)原子操作中對(duì)其他變量同時(shí)進(jìn)行更新妹孙。

在上述例子中,雖然set方法是原子操作获枝,但是在set方法無法同時(shí)更新lastNumber和lastFactors蠢正。如果當(dāng)一個(gè)線程執(zhí)行了lastNumber.set()方法還沒執(zhí)行下一個(gè)set方法時(shí),如果此時(shí)有一個(gè)線程訪問service方法省店,那么得到的結(jié)果就與我們所預(yù)期的不一致了嚣崭。

所以笨触,要保持狀態(tài)的一致性,就需要在單個(gè)原子操作中更新所有相關(guān)的狀態(tài)變量雹舀。

三芦劣、加鎖機(jī)制

3.1內(nèi)置鎖

在Java中提供了一種內(nèi)置的鎖機(jī)制來支持原子性:同步代碼塊(Synchronized Block)。同步代碼塊包含兩部分:一個(gè)是作為鎖的對(duì)象引用说榆,一個(gè)作為由這個(gè)鎖保護(hù)的代碼塊虚吟。以關(guān)鍵字synchronized來修飾的方法是一種橫跨整個(gè)方法體的同步代碼塊,其中該同步代碼塊的鎖就是方法調(diào)用所在的對(duì)象(this).靜態(tài)的synchronized方法以Class對(duì)象為作為鎖签财。

每個(gè)Java對(duì)象都可以用做一個(gè)實(shí)現(xiàn)同步的鎖串慰,這些鎖被稱為內(nèi)置鎖(Intrinsic Lock)或是監(jiān)視器鎖(Monitor Lock)。線程在進(jìn)入同步代碼塊之前會(huì)自動(dòng)獲得鎖唱蒸,并且在退出同步代碼塊時(shí)自動(dòng)釋放鎖邦鲫。

Java的內(nèi)置鎖相當(dāng)于一種互斥鎖,最多只有一個(gè)線程能持有這種鎖神汹。當(dāng)線程A嘗試獲取線程B持有的鎖時(shí)庆捺,線程A必須等待或阻塞,知道線程B釋放了該鎖屁魏。如果線程B不釋放鎖滔以,則線程A也將永遠(yuǎn)等下去。任何一個(gè)執(zhí)行同步代碼塊的線程蚁堤,都不可能看到有其他線程正在執(zhí)行由同一個(gè)鎖保護(hù)的同步代碼塊醉者。

下面時(shí)應(yīng)用了內(nèi)置鎖的示例:

public class SynchronizedFactorizer implements Servlet {

private BigInteger lastNumber;

private BigInteger[] lastFactors;

public synchronized void service(ServletRequest req, ServletResponse resp) {

BigInteger i = extractFromRequest(req);

if (i.equals(lastNumber)) {

encodeIntoResponse(resp, lastFactors.get());

} else {

BigInteger[] factors = factor(i);

lastNumber = i;

lastFactors = factors;

encodeIntoResponse(resp, factors);

}

}

}

雖然使用synchrnoized關(guān)鍵字保證了結(jié)果的正確性,但是在同一時(shí)刻只有一個(gè)線程可以執(zhí)行service方法披诗,這就導(dǎo)致了服務(wù)的響應(yīng)性非常低,并發(fā)性非常的糟糕立磁,變成了一個(gè)性能問題呈队,而不是線程安全問題。

3.2 重入

當(dāng)某個(gè)線程請(qǐng)求一個(gè)由其他線程持有的鎖是唱歧,發(fā)出請(qǐng)求的線程就會(huì)被阻塞宪摧,但是,由于內(nèi)置鎖是可重入的颅崩,即如果某個(gè)線程試圖獲得一個(gè)已經(jīng)由它自己持有的鎖時(shí)几于,那么這個(gè)請(qǐng)求就會(huì)成功。"重入"意味著獲取鎖的操作粒度是“線程”而不是“調(diào)用”沿后。重入的一種實(shí)現(xiàn)方法就是沿彭,為每一個(gè)鎖關(guān)聯(lián)一個(gè)計(jì)數(shù)值和一個(gè)所有者線程。當(dāng)計(jì)數(shù)值為0時(shí)尖滚,就認(rèn)為這個(gè)鎖是沒有被任何線程持有喉刘。當(dāng)線程請(qǐng)求一個(gè)未被持有的鎖時(shí)瞧柔,JVM記下鎖的持有者,并且將獲取計(jì)數(shù)值置為1睦裳。如果同一個(gè)線程再次獲取這個(gè)鎖造锅,計(jì)數(shù)值將遞增抱慌,而當(dāng)線程退出同步代碼塊時(shí)脚乡,計(jì)數(shù)值會(huì)相應(yīng)的遞減。當(dāng)計(jì)數(shù)值為0時(shí)莱坎,這個(gè)鎖將被釋放蛛蒙。

下面是一個(gè)重入的例子:

public class Widget{

public synchronized void doSomething(){

System.out.println(toString() + ": calling doSomething");

}

}

public class LoggingWidget extends Widget{

public synchronzied void doSomething(){

System.out.println(toString() + ": calling doSomething");

super.doSomething();

}

}

在上述例子中肺素,LoggingWidget繼承了Widget并改寫了父類,并且都是用了synchronized關(guān)鍵字修飾doSomething方法宇驾,如果子類對(duì)象在調(diào)用doSomething方法時(shí)倍靡。如果沒有可重入鎖,那么這段代碼就會(huì)產(chǎn)生死鎖课舍。因?yàn)槊總€(gè)doSomething方法在執(zhí)行前都會(huì)獲得Widget上的鎖塌西,如果內(nèi)置鎖是不可重入的,那么在調(diào)用super.doSomething時(shí)就無法獲得Widget上的鎖筝尾,因?yàn)檫@個(gè)鎖已經(jīng)被持有了捡需,從而線程將永遠(yuǎn)停頓下去,等待一個(gè)永遠(yuǎn)也無法獲得的鎖筹淫。注意:在這里synchronized關(guān)鍵字修飾的是方法體站辉,也就是說它鎖住的是對(duì)象本身(this),所以當(dāng)?shù)谝淮芜M(jìn)入doSomething方法時(shí),鎖住的是LoggingWidget對(duì)象损姜,而在調(diào)用super.doSomething時(shí)饰剥,并沒有新建一個(gè)父類對(duì)象,鎖的對(duì)象還是this.

四摧阅、用鎖來保護(hù)狀態(tài)

對(duì)于可能被多個(gè)線程同時(shí)訪問的可變狀態(tài)變量汰蓉,在訪問它時(shí)都需要持有同一個(gè)鎖,在這種情況下棒卷,我們稱狀態(tài)變量是由這個(gè)鎖保護(hù)的顾孽。對(duì)象的內(nèi)置鎖與其狀態(tài)之間沒有內(nèi)在的關(guān)聯(lián),對(duì)象的域并不一定要通過內(nèi)置鎖類保護(hù)比规。當(dāng)獲取與對(duì)象關(guān)聯(lián)的鎖時(shí)若厚,并不能阻止其他線程訪問該對(duì)象,某個(gè)線程在獲得對(duì)象的鎖之后蜒什,只能阻止其他線程獲得同一個(gè)鎖测秸,每個(gè)對(duì)象都有一個(gè)內(nèi)置鎖。

每個(gè)共享的和可變的變量都應(yīng)該只由一個(gè)鎖來保護(hù)。一種常見的加鎖約定是乞封,將所有可變狀態(tài)都封裝在對(duì)象內(nèi)部做裙,并通過對(duì)象的內(nèi)置鎖對(duì)所有訪問可變狀態(tài)的代碼路勁進(jìn)行同步,使得在該對(duì)象上不會(huì)發(fā)生并發(fā)訪問肃晚。但是锚贱,如果在添加新的方法或代碼路徑時(shí)忘記了使用同步,那么這種加鎖協(xié)議會(huì)很容易被破壞关串。

我們應(yīng)該知道的是拧廊,并非所有數(shù)據(jù)都需要鎖的保護(hù),只有被多個(gè)線程同時(shí)訪問的可變數(shù)據(jù)才需要通過鎖來保護(hù)晋修。當(dāng)某個(gè)變量由鎖來保護(hù)時(shí)吧碾,意味著每次訪問這個(gè)變量時(shí)都需要首先獲得這個(gè)鎖,這樣就確保在同一時(shí)刻只有一個(gè)一個(gè)線程可以訪問這個(gè)變量墓卦。當(dāng)類的不變性條件涉及多個(gè)狀態(tài)變量時(shí)倦春,那么還有另外一個(gè)需求:在不變性條件中的每個(gè)變量都必須由同一個(gè)鎖來保護(hù)。

雖然同步可以避免競態(tài)條件問題落剪,但并不意味著可以在每個(gè)方法聲明時(shí)都是用關(guān)鍵字synchronized.如果將程序中存在過多的同步方法睁本,可能會(huì)導(dǎo)致活躍性問題或性能問題。

我們應(yīng)該盡量將不影響共享狀態(tài)且執(zhí)行時(shí)間較長的操作從同步代碼塊中分離出去忠怖,確保同步代碼塊中盡量只存在原子性的操作呢堰。

在使用鎖時(shí),應(yīng)該清楚代碼塊中實(shí)現(xiàn)的功能凡泣,以及在執(zhí)行該代碼塊時(shí)是否需要很長的時(shí)間枉疼,當(dāng)執(zhí)行時(shí)間較長的計(jì)算或者可能無法快速完成的操作時(shí)(例如,網(wǎng)絡(luò)I/O或控制臺(tái)I/O)鞋拟,一定不要持有鎖!!!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末骂维,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子严卖,更是在濱河造成了極大的恐慌席舍,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件哮笆,死亡現(xiàn)場離奇詭異,居然都是意外死亡汰扭,警方通過查閱死者的電腦和手機(jī)稠肘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來萝毛,“玉大人项阴,你說我怎么就攤上這事。” “怎么了环揽?”我有些...
    開封第一講書人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵略荡,是天一觀的道長。 經(jīng)常有香客問我歉胶,道長汛兜,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任通今,我火速辦了婚禮粥谬,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘辫塌。我一直安慰自己漏策,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開白布臼氨。 她就那樣靜靜地躺著掺喻,像睡著了一般。 火紅的嫁衣襯著肌膚如雪储矩。 梳的紋絲不亂的頭發(fā)上感耙,一...
    開封第一講書人閱讀 51,763評(píng)論 1 307
  • 那天,我揣著相機(jī)與錄音椰苟,去河邊找鬼抑月。 笑死,一個(gè)胖子當(dāng)著我的面吹牛舆蝴,可吹牛的內(nèi)容都是我干的谦絮。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼洁仗,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼层皱!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起赠潦,我...
    開封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤叫胖,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后她奥,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體瓮增,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年哩俭,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了绷跑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡凡资,死狀恐怖砸捏,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤垦藏,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布梆暖,位于F島的核電站,受9級(jí)特大地震影響掂骏,放射性物質(zhì)發(fā)生泄漏轰驳。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一芭挽、第九天 我趴在偏房一處隱蔽的房頂上張望滑废。 院中可真熱鬧,春花似錦袜爪、人聲如沸蠕趁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽俺陋。三九已至,卻和暖如春昙篙,著一層夾襖步出監(jiān)牢的瞬間腊状,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來泰國打工苔可, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留缴挖,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓焚辅,卻偏偏與公主長得像映屋,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子同蜻,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355

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