- 相關(guān)推薦
java編程的總結(jié)與思考
編寫(xiě)優(yōu)質(zhì)的并發(fā)代碼是一件難度極高的事情。Java語(yǔ)言從第一版本開(kāi)始內(nèi)置了對(duì)多線程的支持,這一點(diǎn)在當(dāng)年是非常了不起的,但是當(dāng)我們對(duì)并發(fā)編程有了更深刻的認(rèn)識(shí)和更多的實(shí)踐后,實(shí)現(xiàn)并發(fā)編程就有了更多的方案和更好的選擇。本文是對(duì)并發(fā)編程的一點(diǎn)總結(jié)和思考,
為什么需要并發(fā)
并發(fā)其實(shí)是一種解耦合的策略,它幫助我們把做什么(目標(biāo))和什么時(shí)候做(時(shí)機(jī))分開(kāi)。這樣做可以明顯改進(jìn)應(yīng)用程序的吞吐量(獲得更多的CPU調(diào)度時(shí)間)和結(jié)構(gòu)(程序有多個(gè)部分在協(xié)同工作)。做過(guò)Java Web開(kāi)發(fā)的人都知道,Java Web中的Servlet程序在Servlet容器的支持下采用單實(shí)例多線程的工作模式,Servlet容器幫助你處理了并發(fā)請(qǐng)求的問(wèn)題。
誤解和正解
最常見(jiàn)的對(duì)并發(fā)編程的誤解有以下這些:
A. 并發(fā)總能改進(jìn)性能。(真相:并發(fā)在CPU有很多空閑時(shí)間時(shí)能明顯改進(jìn)程序的性能,但當(dāng)線程數(shù)量較多的時(shí)候,線程間頻繁的調(diào)度切換反而會(huì)讓系統(tǒng)的性能下降)
B. 編寫(xiě)并發(fā)程序無(wú)需修改原有的設(shè)計(jì)。(真相:目的與時(shí)機(jī)的解耦往往會(huì)對(duì)系統(tǒng)結(jié)構(gòu)產(chǎn)生巨大的影響)
C. 在使用Web或EJB容器時(shí)不用關(guān)注并發(fā)問(wèn)題。(真相:只有了解了容器在做什么,才能更好的使用容器)
下面的這些說(shuō)法才是對(duì)并發(fā)編程比較客觀的認(rèn)識(shí):
A. 編寫(xiě)并發(fā)程序會(huì)在代碼上增加額外的開(kāi)銷。
B. 正確的并發(fā)是非常復(fù)雜的,即使對(duì)于很簡(jiǎn)單的問(wèn)題。
C. 并發(fā)中的缺陷因?yàn)椴灰字噩F(xiàn)也不容易被發(fā)現(xiàn)。
D. 并發(fā)往往需要對(duì)設(shè)計(jì)策略從根本上進(jìn)行修改。
并發(fā)編程的原則和技巧
1. 單一職責(zé)原則:分離并發(fā)相關(guān)代碼和其他代碼(并發(fā)相關(guān)代碼有自己的開(kāi)發(fā)、修改和調(diào)優(yōu)生命周期)。
2. 限制數(shù)據(jù)作用域:兩個(gè)線程修改共享對(duì)象的同一字段時(shí)可能會(huì)相互干擾,導(dǎo)致不可預(yù)期的行為,解決方案之一是構(gòu)造臨界區(qū),但是必須限制臨界區(qū)的數(shù)量。
3. 使用數(shù)據(jù)副本:數(shù)據(jù)副本是避免共享數(shù)據(jù)的好方法,復(fù)制出來(lái)的對(duì)象只是以只讀的方式對(duì)待。Java 5的java.util.concurrent包中增加一個(gè)名為CopyOnWriteArrayList的類,它是List接口的子類型,所以你可以認(rèn)為它是ArrayList的線程安全的版本,它使用了寫(xiě)時(shí)復(fù)制的方式創(chuàng)建數(shù)據(jù)副本進(jìn)行操作來(lái)避免對(duì)共享數(shù)據(jù)并發(fā)訪問(wèn)而引發(fā)的問(wèn)題。
4. 線程應(yīng)盡可能獨(dú)立:讓線程存在于自己的世界中,不與其他線程共享數(shù)據(jù)。有過(guò)Java Web開(kāi)發(fā)經(jīng)驗(yàn)的人都知道,Servlet就是以單實(shí)例多線程的方式工作,和每個(gè)請(qǐng)求相關(guān)的數(shù)據(jù)都是通過(guò)Servlet子類的service方法(或者是doGet或doPost方法)的參數(shù)傳入的。只要Servlet中的代碼只使用局部變量,Servlet就不會(huì)導(dǎo)致同步問(wèn)題。Spring MVC的控制器也是這么做的,從請(qǐng)求中獲得的對(duì)象都是以方法的參數(shù)傳入而不是作為類的成員,很明顯Struts 2的做法就正好相反,因此Struts 2中作為控制器的Action類都是每個(gè)請(qǐng)求對(duì)應(yīng)一個(gè)實(shí)例。
下面的這些說(shuō)法才是對(duì)并發(fā)客觀的認(rèn)識(shí):
-編寫(xiě)并發(fā)程序會(huì)在代碼上增加額外的開(kāi)銷 -正確的并發(fā)是非常復(fù)雜的,即使對(duì)于很簡(jiǎn)單的問(wèn)題 -并發(fā)中的缺陷因?yàn)椴灰字噩F(xiàn)也不容易被發(fā)現(xiàn)
-并發(fā)往往需要對(duì)設(shè)計(jì)策略從根本上進(jìn)行修改并發(fā)編程的原則和技巧 單一職責(zé)原則 分離并發(fā)相關(guān)代碼和其他代碼(并發(fā)相關(guān)代碼有自己的開(kāi)發(fā)、修改和調(diào)優(yōu)生命周期)。限制數(shù)據(jù)作用域 兩個(gè)線程修改共享對(duì)象的同一字段時(shí)可能會(huì)相互干擾,導(dǎo)致不可預(yù)期的行為,解決方案之一是構(gòu)造臨界區(qū),但是必須限制臨界區(qū)的數(shù)量。使用數(shù)據(jù)副本 數(shù)據(jù)副本是避免共享數(shù)據(jù)的好方法,復(fù)制出來(lái)的對(duì)象只是以只讀的方式對(duì)待。
Java 5的java.util.concurrent包中增加一個(gè)名為CopyOnWriteArrayList的類,它是List接口的子類型,所以你可以認(rèn)為它是ArrayList的線程安全的版本,它使用了寫(xiě)時(shí)復(fù)制的方式創(chuàng)建數(shù)據(jù)副本進(jìn)行操作來(lái)避免對(duì)共享數(shù)據(jù)并發(fā)訪問(wèn)而引發(fā)的問(wèn)題。線程應(yīng)盡可能獨(dú)立 讓線程存在于自己的世界中,不與其他線程共享數(shù)據(jù)。有過(guò)Java Web開(kāi)發(fā)經(jīng)驗(yàn)的人都知道,Servlet就是以單實(shí)例多線程的方式工作,和每個(gè)請(qǐng)求相關(guān)的數(shù)據(jù)都是通過(guò)Servlet子類的service方法(或者是doGet或doPost方法)的參數(shù)傳入的。只要Servlet中的代碼只使用局部變量,Servlet就不會(huì)導(dǎo)致同步問(wèn)題。
springMVC的控制器也是這么做的,從請(qǐng)求中獲得的對(duì)象都是以方法的參數(shù)傳入而不是作為類的成員,很明顯Struts 2的做法就正好相反,因此Struts 2中作為控制器的Action類都是每個(gè)請(qǐng)求對(duì)應(yīng)一個(gè)實(shí)例。 Java 5以前的并發(fā)編程 Java的線程模型建立在搶占式線程調(diào)度的基礎(chǔ)上,也就是說(shuō): 所有線程可以很容易的共享同一進(jìn)程中的對(duì)象。能夠引用這些對(duì)象的任何線程都可以修改這些對(duì)象。為了保護(hù)數(shù)據(jù),對(duì)象可以被鎖住。 Java基于線程和鎖的并發(fā)過(guò)于底層,而且使用鎖很多時(shí)候都是很萬(wàn)惡的,因?yàn)樗喈?dāng)于讓所有的并發(fā)都變成了排隊(duì)等待。
在Java 5以前,可以用synchronized關(guān)鍵字來(lái)實(shí)現(xiàn)鎖的功能,它可以用在代碼塊和方法上,表示在執(zhí)行整個(gè)代碼塊或方法之前線程必須取得合適的鎖。對(duì)于類的非靜態(tài)方法(成員方法)而言,這意味這要取得對(duì)象實(shí)例的鎖,對(duì)于類的靜態(tài)方法(類方法)而言,要取得類的Class對(duì)象的鎖,對(duì)于同步代碼塊,程序員可以指定要取得的是那個(gè)對(duì)象的鎖。 不管是同步代碼塊還是同步方法,每次只有一個(gè)線程可以進(jìn)入,如果其他線程試圖進(jìn)入(不管是同一同步塊還是不同的同步塊),JVM會(huì)將它們掛起(放入到等鎖池中)。這種結(jié)構(gòu)在并發(fā)理論中稱為臨界區(qū)(critical section)。
這里我們可以對(duì)Java中用synchronized實(shí)現(xiàn)同步和鎖的功能做一個(gè)總結(jié): 只能鎖定對(duì)象,不能鎖定基本數(shù)據(jù)類型被鎖定的對(duì)象數(shù)組中的單個(gè)對(duì)象不會(huì)被鎖定同步方法可以視為包含整個(gè)方法的synchronized(this) { … }代碼塊靜態(tài)同步方法會(huì)鎖定它的Class對(duì)象內(nèi)部類的同步是獨(dú)立于外部類的 synchronized修飾符并不是方法簽名的組成部分,所以不能出現(xiàn)在接口的方法聲明中非同步的方法不關(guān)心鎖的狀態(tài),它們?cè)谕椒椒ㄟ\(yùn)行時(shí)仍然可以得以運(yùn)行 synchronized實(shí)現(xiàn)的鎖是可重入的鎖。在JVM內(nèi)部,為了提高效率,同時(shí)運(yùn)行的每個(gè)線程都會(huì)有它正在處理的數(shù)據(jù)的緩存副本,當(dāng)我們使用synchronzied進(jìn)行同步的時(shí)候,真正被同步的是在不同線程中表示被鎖定對(duì)象的內(nèi)存塊(副本數(shù)據(jù)會(huì)保持和主內(nèi)存的同步,現(xiàn)在知道為什么要用同步這個(gè)詞匯了吧),簡(jiǎn)單的說(shuō)就是在同步塊或同步方法執(zhí)行完后,對(duì)被鎖定的對(duì)象做的任何修改要在釋放鎖之前寫(xiě)回到主內(nèi)存中;在進(jìn)入同步塊得到鎖之后,被鎖定對(duì)象的數(shù)據(jù)是從主內(nèi)存中讀出來(lái)的,持有鎖的線程的數(shù)據(jù)副本一定和主內(nèi)存中的數(shù)據(jù)視圖是同步的 。 在Java最初的版本中,就有一個(gè)叫Volatile的關(guān)鍵字,它是一種簡(jiǎn)單的同步的處理機(jī)制,因?yàn)楸籿olatile修飾的變量遵循以下規(guī)則: 變量的值在使用之前總會(huì)從主內(nèi)存中再讀取出來(lái)。對(duì)變量值的修改總會(huì)在完成之后寫(xiě)回到主內(nèi)存中。使用volatile關(guān)鍵字可以在多線程環(huán)境下預(yù)防編譯器不正確的優(yōu)化假設(shè)(編譯器可能會(huì)將在一個(gè)線程中值不會(huì)發(fā)生改變的變量?jī)?yōu)化成常量),但只有修改時(shí)不依賴當(dāng)前狀態(tài)(讀取時(shí)的值)的變量才應(yīng)該聲明為volatile變量。
不變模式也是并發(fā)編程時(shí)可以考慮的一種設(shè)計(jì)。讓對(duì)象的狀態(tài)是不變的,如果希望修改對(duì)象的狀態(tài),就會(huì)創(chuàng)建對(duì)象的副本并將改變寫(xiě)入副本而不改變?cè)瓉?lái)的對(duì)象,這樣就不會(huì)出現(xiàn)狀態(tài)不一致的情況,因此不變對(duì)象是線程安全的。Java中我們使用頻率極高的String類就采用了這樣的設(shè)計(jì)。如果對(duì)不變模式不熟悉,可以閱讀閻宏博士的《Java與模式》一書(shū)的第34章。說(shuō)到這里你可能也體會(huì)到final關(guān)鍵字的重要意義了。 Java 5的并發(fā)編程 不管今后的Java向著何種方向發(fā)展或者滅亡,Java 5絕對(duì)是Java發(fā)展史中一個(gè)極其重要的版本,這個(gè)版本提供的各種語(yǔ)言特性我們不在這里討論(有興趣的可以閱讀我的另一篇文章《Java的第20年:從Java版本演進(jìn)看編程技術(shù)的發(fā)展》),但是我們必須要感謝Doug Lea在Java 5中提供了他里程碑式的杰作java.util.concurrent包,它的出現(xiàn)讓Java的并發(fā)編程有了更多的選擇和更好的工作方式。Doug Lea的杰作主要包括以下內(nèi)容: 更好的線程安全的容器線程池和相關(guān)的工具類可選的非阻塞解決方案顯示的鎖和信號(hào)量機(jī)制下面我們對(duì)這些東西進(jìn)行一一解讀。 原子類 Java 5中的java.util.concurrent包下面有一個(gè)atomic子包,其中有幾個(gè)以Atomic打頭的類,例如AtomicInteger和AtomicLong。
它們利用了現(xiàn)代處理器的特性,可以用非阻塞的方式完成原子操作,代碼如下所示: /** ID序列生成器 */ public class IdGenerator { private final AtomicLong sequenceNumber = new AtomicLong(0); public long next() { return sequenceNumber.getAndIncrement(); } } 顯示鎖 基于synchronized關(guān)鍵字的鎖機(jī)制有以下問(wèn)題: 鎖只有一種類型,而且對(duì)所有同步操作都是一樣的作用鎖只能在代碼塊或方法開(kāi)始的地方獲得,在結(jié)束的地方釋放線程要么得到鎖,要么阻塞,沒(méi)有其他的可能性 Java 5對(duì)鎖機(jī)制進(jìn)行了重構(gòu),提供了顯示的鎖,這樣可以在以下幾個(gè)方面提升鎖機(jī)制: 可以添加不同類型的鎖,例如讀取鎖和寫(xiě)入鎖可以在一個(gè)方法中加鎖,在另一個(gè)方法中解鎖可以使用tryLock方式嘗試獲得鎖,如果得不到鎖可以等待、回退或者干點(diǎn)別的事情,當(dāng)然也可以在超時(shí)之后放棄操作顯示的鎖都實(shí)現(xiàn)了java.util.concurrent.Lock接口,主要有兩個(gè)實(shí)現(xiàn)類: ReentrantLock – 比synchronized稍微靈活一些的重入鎖 ReentrantReadWriteLock – 在讀操作很多寫(xiě)操作很少時(shí)性能更好的一種重入鎖對(duì)于如何使用顯示鎖,只有一點(diǎn)需要提醒,解鎖的方法unlock的調(diào)用最好能夠在finally塊中,因?yàn)檫@里是釋放外部資源最好的地方,當(dāng)然也是釋放鎖的最佳位置,因?yàn)椴还苷.惓?赡芏家尫诺翩i來(lái)給其他線程以運(yùn)行的機(jī)會(huì)。 CountDownLatch CountDownLatch是一種簡(jiǎn)單的同步模式,它讓一個(gè)線程可以等待一個(gè)或多個(gè)線程完成它們的工作從而避免對(duì)臨界資源并發(fā)訪問(wèn)所引發(fā)的各種問(wèn)題。
下面借用別人的一段代碼(我對(duì)它做了一些重構(gòu))來(lái)演示CountDownLatch是如何工作的。 import java.util.concurrent.CountDownLatch; /** * 工人類 * @author 駱昊 * */ class Worker { private String name; // 名字 private long workDuration; // 工作持續(xù)時(shí)間 /** * 構(gòu)造器 */ public Worker(String name, long workDuration) { this.name = name; this.workDuration = workDuration; } /** * 完成工作 */ public void doWork() { System.out.println(name + " begins to work..."); try { Thread.sleep(workDuration); // 用休眠模擬工作執(zhí)行的時(shí)間 } catch(InterruptedException ex) { ex.printStackTrace(); } System.out.println(name + " has finished the job..."); } } /** * 測(cè)試線程 * @author 駱昊 * */ class WorkerTestThread implements Runnable { private Worker worker; private CountDownLatch cdLatch; public WorkerTestThread(Worker worker, CountDownLatch cdLatch) { this.worker = worker; this.cdLatch = cdLatch; } ConcurrentHashMap ConcurrentHashMap是HashMap在并發(fā)環(huán)境下的版本,大家可能要問(wèn),既然已經(jīng)可以通過(guò)Collections.synchronizedMap獲得線程安全的映射型容器,為什么還需要ConcurrentHashMap呢?因?yàn)橥ㄟ^(guò)Collections工具類獲得的線程安全的HashMap會(huì)在讀寫(xiě)數(shù)據(jù)時(shí)對(duì)整個(gè)容器對(duì)象上鎖,這樣其他使用該容器的線程無(wú)論如何也無(wú)法再獲得該對(duì)象的鎖,也就意味著要一直等待前一個(gè)獲得鎖的線程離開(kāi)同步代碼塊之后才有機(jī)會(huì)執(zhí)行。實(shí)際上,HashMap是通過(guò)哈希函數(shù)來(lái)確定存放鍵值對(duì)的桶(桶是為了解決哈希沖突而引入的),修改HashMap時(shí)并不需要將整個(gè)容器鎖住,只需要鎖住即將修改的“桶”就可以了。HashMap的數(shù)據(jù)結(jié)構(gòu)如下圖所示。
此外,ConcurrentHashMap還提供了原子操作的方法,如下所示: putIfAbsent:如果還沒(méi)有對(duì)應(yīng)的鍵值對(duì)映射,就將其添加到HashMap中。 remove:如果鍵存在而且值與當(dāng)前狀態(tài)相等(equals比較結(jié)果為true),則用原子方式移除該鍵值對(duì)映射 replace:替換掉映射中元素的原子操作 CopyOnWriteArrayList CopyOnWriteArrayList是ArrayList在并發(fā)環(huán)境下的替代品。CopyOnWriteArrayList通過(guò)增加寫(xiě)時(shí)復(fù)制語(yǔ)義來(lái)避免并發(fā)訪問(wèn)引起的問(wèn)題,也就是說(shuō)任何修改操作都會(huì)在底層創(chuàng)建一個(gè)列表的副本,也就意味著之前已有的迭代器不會(huì)碰到意料之外的修改。這種方式對(duì)于不要嚴(yán)格讀寫(xiě)同步的場(chǎng)景非常有用,因?yàn)樗峁┝烁玫男阅。記住,要盡量減少鎖的使用,因?yàn)槟莿?shì)必帶來(lái)性能的下降(對(duì)數(shù)據(jù)庫(kù)中數(shù)據(jù)的并發(fā)訪問(wèn)不也是如此嗎?如果可以的話就應(yīng)該放棄悲觀鎖而使用樂(lè)觀鎖),CopyOnWriteArrayList很明顯也是通過(guò)犧牲空間獲得了時(shí)間(在計(jì)算機(jī)的世界里,時(shí)間和空間通常是不可調(diào)和的矛盾,可以犧牲空間來(lái)提升效率獲得時(shí)間,當(dāng)然也可以通過(guò)犧牲時(shí)間來(lái)減少對(duì)空間的使用)。 可以通過(guò)下面兩段代碼的運(yùn)行狀況來(lái)驗(yàn)證一下CopyOnWriteArrayList是不是線程安全的容器。 上面的代碼會(huì)在運(yùn)行時(shí)產(chǎn)生ArrayIndexOutOfBoundsException,試一試將上面代碼25行的ArrayList換成CopyOnWriteArrayList再重新運(yùn)行。 List list = new CopyOnWriteArrayList<>(); undefinedQueue 隊(duì)列是一個(gè)無(wú)處不在的美妙概念,它提供了一種簡(jiǎn)單又可靠的方式將資源分發(fā)給處理單元(也可以說(shuō)是將工作單元分配給待處理的資源,這取決于你看待問(wèn)題的方式)。實(shí)現(xiàn)中的并發(fā)編程模型很多都依賴隊(duì)列來(lái)實(shí)現(xiàn),因?yàn)樗梢栽诰程之間傳遞工作單元。 Java 5中的BlockingQueue就是一個(gè)在并發(fā)環(huán)境下非常好用的工具,在調(diào)用put方法向隊(duì)列中插入元素時(shí),如果隊(duì)列已滿,它會(huì)讓插入元素的線程等待隊(duì)列騰出空間;在調(diào)用take方法從隊(duì)列中取元素時(shí),如果隊(duì)列為空,取出元素的線程就會(huì)阻塞。
可以用BlockingQueue來(lái)實(shí)現(xiàn)生產(chǎn)者-消費(fèi)者并發(fā)模型(下一節(jié)中有介紹),當(dāng)然在Java 5以前也可以通過(guò)wait和notify來(lái)實(shí)現(xiàn)線程調(diào)度,比較一下兩種代碼就知道基于已有的并發(fā)工具類來(lái)重構(gòu)并發(fā)代碼到底好在哪里了。 基于wait和notify的實(shí)現(xiàn) 使用BlockingQueue后代碼優(yōu)雅了很多。 并發(fā)模型 在繼續(xù)下面的探討之前,我們還是重溫一下幾個(gè)概念: 概念 解釋臨界資源 并發(fā)環(huán)境中有著固定數(shù)量的資源互斥 對(duì)資源的訪問(wèn)是排他式的饑餓 一個(gè)或一組線程長(zhǎng)時(shí)間或永遠(yuǎn)無(wú)法取得進(jìn)展死鎖 兩個(gè)或多個(gè)線程相互等待對(duì)方結(jié)束活鎖 想要執(zhí)行的線程總是發(fā)現(xiàn)其他的線程正在執(zhí)行以至于長(zhǎng)時(shí)間或永遠(yuǎn)無(wú)法執(zhí)行重溫了這幾個(gè)概念后,我們可以探討一下下面的幾種并發(fā)模型。 生產(chǎn)者-消費(fèi)者 一個(gè)或多個(gè)生產(chǎn)者創(chuàng)建某些工作并將其置于緩沖區(qū)或隊(duì)列中,一個(gè)或多個(gè)消費(fèi)者會(huì)從隊(duì)列中獲得這些工作并完成之。這里的緩沖區(qū)或隊(duì)列是臨界資源。當(dāng)緩沖區(qū)或隊(duì)列放滿的時(shí)候,生產(chǎn)這會(huì)被阻塞;而緩沖區(qū)或隊(duì)列為空的時(shí)候,消費(fèi)者會(huì)被阻塞。生產(chǎn)者和消費(fèi)者的調(diào)度是通過(guò)二者相互交換信號(hào)完成的。 讀者-寫(xiě)者 當(dāng)存在一個(gè)主要為讀者提供信息的共享資源,它偶爾會(huì)被寫(xiě)者更新,但是需要考慮系統(tǒng)的吞吐量,又要防止饑餓和陳舊資源得不到更新的問(wèn)題。
在這種并發(fā)模型中,如何平衡讀者和寫(xiě)者是最困難的,當(dāng)然這個(gè)問(wèn)題至今還是一個(gè)被熱議的問(wèn)題,恐怕必須根據(jù)具體的場(chǎng)景來(lái)提供合適的解決方案而沒(méi)有那種放之四海而皆準(zhǔn)的方法(不像我在國(guó)內(nèi)的科研文獻(xiàn)中看到的那樣)。 哲學(xué)家進(jìn)餐 1965年,荷蘭計(jì)算機(jī)科學(xué)家圖靈獎(jiǎng)得主Edsger Wybe Dijkstra提出并解決了一個(gè)他稱之為哲學(xué)家進(jìn)餐的同步問(wèn)題。這個(gè)問(wèn)題可以簡(jiǎn)單地描述如下:五個(gè)哲學(xué)家圍坐在一張圓桌周?chē)總(gè)哲學(xué)家面前都有一盤(pán)通心粉。由于通心粉很滑,所以需要兩把叉子才能夾住。相鄰兩個(gè)盤(pán)子之間放有一把叉子如下圖所示。哲學(xué)家的生活中有兩種交替活動(dòng)時(shí)段:即吃飯和思考。當(dāng)一個(gè)哲學(xué)家覺(jué)得餓了時(shí),他就試圖分兩次去取其左邊和右邊的叉子,每次拿一把,但不分次序。如果成功地得到了兩把叉子,就開(kāi)始吃飯,吃完后放下叉子繼續(xù)思考。 把上面問(wèn)題中的哲學(xué)家換成線程,把叉子換成競(jìng)爭(zhēng)的臨界資源,上面的問(wèn)題就是線程競(jìng)爭(zhēng)資源的問(wèn)題。如果沒(méi)有經(jīng)過(guò)精心的設(shè)計(jì),系統(tǒng)就會(huì)出現(xiàn)死鎖、活鎖、吞吐量下降等問(wèn)題。 下面是用信號(hào)量原語(yǔ)來(lái)解決哲學(xué)家進(jìn)餐問(wèn)題的代碼,使用了Java 5并發(fā)工具包中的Semaphore類(代碼不夠漂亮但是已經(jīng)足以說(shuō)明問(wèn)題了)。 //import java.util.concurrent.ExecutorService; //import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; 現(xiàn)實(shí)中的并發(fā)問(wèn)題基本上都是這三種模型或者是這三種模型的變體。 測(cè)試并發(fā)代碼 對(duì)并發(fā)代碼的測(cè)試也是非常棘手的事情,棘手到無(wú)需說(shuō)明大家也很清楚的程度,所以這里我們只是探討一下如何解決這個(gè)棘手的問(wèn)題。
我們建議大家編寫(xiě)一些能夠發(fā)現(xiàn)問(wèn)題的測(cè)試并經(jīng)常性的在不同的配置和不同的負(fù)載下運(yùn)行這些測(cè)試。不要忽略掉任何一次失敗的測(cè)試,線程代碼中的缺陷可能在上萬(wàn)次測(cè)試中僅僅出現(xiàn)一次。具體來(lái)說(shuō)有這么幾個(gè)注意事項(xiàng): 不要將系統(tǒng)的失效歸結(jié)于偶發(fā)事件,就像拉不出屎的時(shí)候不能怪地球沒(méi)有引力。先讓非并發(fā)代碼工作起來(lái),不要試圖同時(shí)找到并發(fā)和非并發(fā)代碼中的缺陷。編寫(xiě)可以在不同配置環(huán)境下運(yùn)行的線程代碼。編寫(xiě)容易調(diào)整的線程代碼,這樣可以調(diào)整線程使性能達(dá)到最優(yōu)。讓線程的數(shù)量多于CPU或CPU核心的數(shù)量,這樣CPU調(diào)度切換過(guò)程中潛在的問(wèn)題才會(huì)暴露出來(lái)。
讓并發(fā)代碼在不同的平臺(tái)上運(yùn)行。通過(guò)自動(dòng)化或者硬編碼的方式向并發(fā)代碼中加入一些輔助測(cè)試的代碼。 Java 7的并發(fā)編程 Java 7中引入了TransferQueue,它比BlockingQueue多了一個(gè)叫transfer的方法,如果接收線程處于等待狀態(tài),該操作可以馬上將任務(wù)交給它,否則就會(huì)阻塞直至取走該任務(wù)的線程出現(xiàn)?梢杂肨ransferQueue代替BlockingQueue,因?yàn)樗梢垣@得更好的性能。 剛才忘記了一件事情,Java 5中還引入了Callable接口、Future接口和FutureTask接口,通過(guò)他們也可以構(gòu)建并發(fā)應(yīng)用程序,代碼如下所示。 Callable接口也是一個(gè)單方法接口,顯然這是一個(gè)回調(diào)方法,類似于函數(shù)式編程中的回調(diào)函數(shù),在Java 8 以前,Java中還不能使用Lambda表達(dá)式來(lái)簡(jiǎn)化這種函數(shù)式編程。和Runnable接口不同的是Callable接口的回調(diào)方法call方法會(huì)返回一個(gè)對(duì)象,這個(gè)對(duì)象可以用將來(lái)時(shí)的方式在線程執(zhí)行結(jié)束的時(shí)候獲得信息。上面代碼中的call方法就是將計(jì)算出的10000個(gè)0到1之間的隨機(jī)小數(shù)的平均值返回,我們通過(guò)一個(gè)Future接口的對(duì)象得到了這個(gè)返回值。目前最新的Java版本中,Callable接口和Runnable接口都被打上了@FunctionalInterface的注解,也就是說(shuō)它可以用函數(shù)式編程的方式(Lambda表達(dá)式)創(chuàng)建接口對(duì)象。 下面是Future接口的主要方法: get():獲取結(jié)果。如果結(jié)果還沒(méi)有準(zhǔn)備好,get方法會(huì)阻塞直到取得結(jié)果;當(dāng)然也可以通過(guò)參數(shù)設(shè)置阻塞超時(shí)時(shí)間。 cancel():在運(yùn)算結(jié)束前取消。 isDone():可以用來(lái)判斷運(yùn)算是否結(jié)束。 Java 7中還提供了分支/合并(fork/join)框架,它可以實(shí)現(xiàn)線程池中任務(wù)的自動(dòng)調(diào)度,并且這種調(diào)度對(duì)用戶來(lái)說(shuō)是透明的。
為了達(dá)到這種效果,必須按照用戶指定的方式對(duì)任務(wù)進(jìn)行分解,然后再將分解出的小型任務(wù)的執(zhí)行結(jié)果合并成原來(lái)任務(wù)的執(zhí)行結(jié)果。這顯然是運(yùn)用了分治法(divide-and-conquer)的思想。下面的代碼使用了分支/合并框架來(lái)計(jì)算1到10000的和,當(dāng)然對(duì)于如此簡(jiǎn)單的任務(wù)根本不需要分支/合并框架,因?yàn)榉种Ш秃喜⒈旧硪矔?huì)帶來(lái)一定的開(kāi)銷,但是這里我們只是探索一下在代碼中如何使用分支/合并框架,讓我們的代碼能夠充分利用現(xiàn)代多核CPU的強(qiáng)大運(yùn)算能力。 伴隨著Java 7的到來(lái),Java中默認(rèn)的數(shù)組排序算法已經(jīng)不再是經(jīng)典的快速排序(雙樞軸快速排序)了,新的排序算法叫TimSort,它是歸并排序和插入排序的混合體,TimSort可以通過(guò)分支合并框架充分利用現(xiàn)代處理器的多核特性,從而獲得更好的性能(更短的排序時(shí)間)。
【java編程的總結(jié)與思考】相關(guān)文章:
java編程基礎(chǔ)07-26
java編程術(shù)語(yǔ)03-09
Java編程語(yǔ)言02-10
java教程之Java編程基礎(chǔ)04-18
java語(yǔ)法基本編程04-01
java編程語(yǔ)言分析07-11
Java語(yǔ)言編程簡(jiǎn)介03-04
Java編程環(huán)境的搭建06-03
java面向接口編程08-01