?? 輕松使用線程:減少爭用.htm
字號:
height=5><B>相關(guān)內(nèi)容:</B></TD></TR>
<TR>
<TD width=160 bgColor=#666666 height=1><IMG height=1 alt=""
src="輕松使用線程:減少爭用.files/c.gif" width=160></TD></TR>
<TR>
<TD align=right>
<TABLE cellSpacing=0 cellPadding=3 width="98%" border=0>
<TBODY>
<TR>
<TD><A
href="http://www-900.ibm.com/developerWorks/cn/java/j-thread/index.shtml">編寫多線程的
Java 應(yīng)用程序</A></TD></TR>
<TR>
<TD><A
href="http://www-900.ibm.com/developerWorks/cn/cgi-bin/click.cgi?url=www7.software.ibm.com/vad.nsf/Data/Document2351?OpenDocument&p=1&BCT=3&Footer=1&origin=j">用于
IBM WebSphere 高級版的樂觀鎖定模式</A></TD></TR><!--Related Zone content -->
<TR>
<TD><A
href="http://www-900.ibm.com/developerWorks/cn/java/index.shtml">更多的
dW Java 參考資料</A></TD></TR></TBODY></TABLE></TD></TR></TBODY></TABLE><!-- End Related dW Content Area-->
<TABLE cellSpacing=0 cellPadding=0 width=160 border=0>
<TBODY>
<TR>
<TD width=150 bgColor=#000000 colSpan=2 height=2><IMG height=2
alt="" src="輕松使用線程:減少爭用.files/c.gif" width=160></TD></TR>
<TR>
<TD width=150 bgColor=#ffffff colSpan=2 height=2><IMG height=2
alt="" src="輕松使用線程:減少爭用.files/c.gif"
width=160></TD></TR></TBODY></TABLE><!-- END STANDARD SIDEBAR AREA--></TD></TR></TBODY></TABLE><SPAN
class=atitle2>拋開您自己的習(xí)慣,提高應(yīng)用程序的性能</SPAN>
<P><A
href="http://www-900.ibm.com/developerWorks/cn/java/j-threads/index2.shtml#author1">Brian
Goetz</A> (<A
href="mailto:brian@quiotix.com">brian@quiotix.com</A>)<BR>軟件顧問,Quiotix
Corp。<BR>2001 年 9 月</P>
<BLOCKQUOTE>在本系列的<A
href="http://www-900.ibm.com/developerWorks/cn/java/j-threads/index.shtml">第
1 部分</A>,我們討論了無爭用同步的性能開銷。盡管常常聽說同步方法調(diào)用的開銷是非同步方法調(diào)用開銷的 50
倍,這個(gè)數(shù)字實(shí)際上仍然相當(dāng)容易產(chǎn)生誤導(dǎo)。JVM
的每個(gè)后繼版本在整體性能上的提高和無爭用同步代價(jià)的降低使得無爭用同步開銷問題不再顯得那么突出。但爭用同步的代價(jià)仍然非常高昂。而且,嚴(yán)重的爭用將嚴(yán)重?fù)p害應(yīng)用程序的可伸縮性
— 隨著負(fù)載的增加,存在嚴(yán)重爭用同步的應(yīng)用程序的性能將顯著降低。本文將探討能夠減少爭用的幾種技術(shù),以提高您程序的可伸縮性。
<P>參加 Brian 的<A href="javascript:void%20forumWindow()">多線程 Java
編程討論論壇</A>以獲得您工程中的線程和并發(fā)問題的幫助。</P></BLOCKQUOTE>
<P>當(dāng)我們說一個(gè)程序“太慢”時(shí),我們通常是指兩個(gè)性能屬性 — 等待時(shí)間和可伸縮性 —
中的一個(gè)。<I>等待時(shí)間</I>指完成一個(gè)給定任務(wù)所花費(fèi)的時(shí)間,而<I>可伸縮性</I>則指隨著負(fù)載的增加或給定計(jì)算資源的增加,程序的性能將怎樣變化。嚴(yán)重的爭用對等待時(shí)間和可伸縮性都不利。</P>
<P><A name=1><SPAN
class=atitle2>爭用為什么是這樣一個(gè)問題</SPAN></A><BR>爭用同步之所以慢,是因?yàn)樗婕岸鄠€(gè)線程切換和系統(tǒng)調(diào)用。當(dāng)多個(gè)線程爭用同一個(gè)管程時(shí),JVM
將不得不維護(hù)一個(gè)等待該管程的線程隊(duì)列(并且這個(gè)隊(duì)列在多個(gè)處理器間必須是同步的),這就意味著花費(fèi)在 JVM 或 OS
代碼上的時(shí)間相對多了,而花費(fèi)在程序代碼上的時(shí)間則相對少了。而且,爭用還削弱了可伸縮性,因?yàn)樗仁拐{(diào)度程序把操作序列化,即使有可用的空閑處理器也是如此。當(dāng)一個(gè)線程正在執(zhí)行一個(gè)同步塊時(shí),任何等待進(jìn)入該塊的線程都將被阻塞。如果沒有其他的線程可供執(zhí)行,那么處理器就將空閑。</P>
<P>如果想編寫具可伸縮性的多線程程序,我們就必須減少對臨界資源的爭用。有很多技術(shù)可以做到這一點(diǎn),但在應(yīng)用它們之前,您需要仔細(xì)研究一下您的代碼,判斷出在什么情況下您需要在公共管程上同步。判斷哪些鎖是瓶頸很困難:有時(shí)候鎖隱藏在類庫中,有時(shí)候又通過同步方法隱式地指定,因此在閱讀代碼時(shí),鎖并不那么明顯。而且,目前的爭用檢測工具也很差。</P>
<P><A name=2><SPAN class=atitle2>技術(shù)
1:放進(jìn)去,取出來</SPAN></A><BR>使同步塊盡可能小顯然是降低爭用可能性的一種技術(shù)。一個(gè)線程占用一個(gè)給定鎖的時(shí)間越短,另一個(gè)線程在該線程仍占用鎖時(shí)請求該鎖的可能性就越小。因此在您應(yīng)該使用同步去訪問或更新共享變量時(shí),在同步塊的外面進(jìn)行線程安全的預(yù)處理或后處理通常會更好些。</P>
<P>清單 1 演示了這種技術(shù)。我們的應(yīng)用程序維護(hù)一個(gè)代表各種實(shí)體的屬性的
<CODE>HashMap</CODE>,給定用戶的訪問權(quán)列表就是這種屬性中的一種。訪問權(quán)被存儲成一個(gè)以逗號隔開的權(quán)限列表。方法
<CODE>userHasAdminAccess()</CODE>
在全局屬性表中查找用戶的訪問權(quán),并判斷該用戶是否有被稱為“ADMIN”的訪問權(quán)。</P><A name=listing1><B>清單 1.
在同步塊中花費(fèi)太多(多于必要時(shí)間)時(shí)間</B></A>
<TABLE cellSpacing=0 cellPadding=5 width="100%" bgColor=#cccccc
border=1><TBODY>
<TR>
<TD><PRE><CODE>
public boolean userHasAdminAccess(String userName) {
synchronized (attributesMap) {
String rights = attributesMap.get("users." + userName + ".accessRights");
if (rights == null)
return false;
else
return (rights.indexOf("ADMIN") >= 0);
}
}
</CODE></PRE></TD></TR></TBODY></TABLE>
<P>這個(gè)版本的 <CODE>userHasAdminAccess</CODE>
是線程安全的,但它占用鎖的時(shí)間比必要占用時(shí)間長太多。為創(chuàng)建串聯(lián)的字符串<CODE>“users.brian.accessRights”</CODE>,編譯器將創(chuàng)建一個(gè)臨時(shí)的
<CODE>StringBuffer</CODE> 對象,調(diào)用 <CODE>StringBuffer.append</CODE> 三次,然后調(diào)用
<CODE>StringBuffer.toString</CODE>,這意味著至少兩個(gè)對象的創(chuàng)建和幾個(gè)方法調(diào)用。接著,程序?qū)⒄{(diào)用
<CODE>HashMap.get</CODE> 檢索該字符串,然后調(diào)用 <CODE>String.indexOf</CODE>
抽取想要的權(quán)限標(biāo)識符。
就占這個(gè)方法所做的全部工作的百分比而言,預(yù)處理和后處理的比重是很大的;因?yàn)樗鼈兪蔷€程安全的,所以將它們移到同步塊外面是有意義的,如清單 2
所示。</P><A name=listing2><B>清單 2. 減少花費(fèi)在同步塊中的時(shí)間</B></A>
<TABLE cellSpacing=0 cellPadding=5 width="100%" bgColor=#cccccc
border=1><TBODY>
<TR>
<TD><PRE><CODE>
public boolean userHasAdminAccess(String userName) {
String key = "users." + userName + ".accessRights";
String rights;
synchronized (attributesMap) {
rights = attributesMap.get(key);
}
return ((rights != null)
&& (rights.indexOf("ADMIN") >= 0));
}
</CODE></PRE></TD></TR></TBODY></TABLE>
<P>另一方面,有可能會過度使用這種技術(shù)。要是您想用一小塊線程安全代碼把要求同步的兩個(gè)操作隔開,那么只使用一個(gè)同步塊一般會更好些。</P>
<P><A name=3><SPAN class=atitle2>技術(shù)
2:減小鎖的粒度</SPAN></A><BR>把您的同步分散在更多的鎖上是減少爭用的另一種有價(jià)值的技術(shù)。例如,假設(shè)您有一個(gè)類,它用兩個(gè)單獨(dú)的散列表來存儲用戶信息和服務(wù)信息,如清單
3 所示。</P><A name=listing3><B>清單 3. 一個(gè)減小鎖的粒度的機(jī)會</B></A>
<TABLE cellSpacing=0 cellPadding=5 width="100%" bgColor=#cccccc
border=1><TBODY>
<TR>
<TD><PRE><CODE>
public class AttributesStore {
private HashMap usersMap = new HashMap();
private HashMap servicesMap = new HashMap();
public synchronized void setUserInfo(String user, UserInfo userInfo) {
usersMap.put(user, userInfo);
}
public synchronized UserInfo getUserInfo(String user) {
return usersMap.get(user);
}
public synchronized void setServiceInfo(String service,
ServiceInfo serviceInfo) {
servicesMap.put(service, serviceInfo);
}
public synchronized ServiceInfo getServiceInfo(String service) {
return servicesMap.get(service);
}
}</CODE></PRE></TD></TR></TBODY></TABLE>
<P>這里,用戶和服務(wù)數(shù)據(jù)的訪問器方法是同步的,這意味著它們在 <CODE>AttributesStore</CODE>
對象上同步。雖然這樣做是完全線程安全的,但卻增加了毫無實(shí)際意義的爭用可能性。如果一個(gè)線程正在執(zhí)行
<CODE>setUserInfo</CODE>,就不僅意味著其它線程將被鎖在 <CODE>setUserInfo</CODE> 和
<CODE>getUserInfo</CODE> 外面(這是我們希望的),而且意味著它們也將被鎖在
<CODE>getServiceInfo</CODE> 和 <CODE>setServiceInfo</CODE> 外面。</P>
<P>通過使訪問器只在共享的實(shí)際對象(<CODE>userMap</CODE> 和 <CODE>servicesMap</CODE>
對象)上同步可以避免這個(gè)問題,如清單 4 所示。</P><A name=listing4><B>清單 4. 減小鎖的粒度</B></A>
<TABLE cellSpacing=0 cellPadding=5 width="100%" bgColor=#cccccc
border=1><TBODY>
<TR>
<TD><PRE><CODE>
public class AttributesStore {
private HashMap usersMap = new HashMap();
private HashMap servicesMap = new HashMap();
public void setUserInfo(String user, UserInfo userInfo) {
synchronized(usersMap) {
usersMap.put(user, userInfo);
}
}
public UserInfo getUserInfo(String user) {
synchronized(usersMap) {
return usersMap.get(user);
}
}
public void setServiceInfo(String service,
ServiceInfo serviceInfo) {
synchronized(servicesMap) {
servicesMap.put(service, serviceInfo);
}
}
public ServiceInfo getServiceInfo(String service) {
synchronized(servicesMap) {
return servicesMap.get(service);
}
}
}
</CODE></PRE></TD></TR></TBODY></TABLE>
<P>現(xiàn)在,訪問服務(wù) map(servicesMap)的線程將不會與試圖訪問用戶 map(usersMap)的線程發(fā)生爭用。(在這種情況下,通過使用
Collections 框架提供的同步包裝機(jī)制,即 <CODE>Collections.synchronizedMap</CODE> 來創(chuàng)建 map
可以達(dá)到同樣的效果。)假設(shè)對兩個(gè) map 的請求是平均分布的,那么這種技術(shù)在這種情況下將把可能的爭用數(shù)目減半。</P>
<P><A name=4><SPAN class=atitle2>在 HashMap 中應(yīng)用技術(shù) 2</SPAN></A><BR>服務(wù)器端的
Java 應(yīng)用程序中最普通的爭用瓶頸之一是 <CODE>HashMap</CODE>。應(yīng)用程序使用 <CODE>HashMap</CODE>
來高速緩存所有類別的臨界共享數(shù)據(jù)(用戶概要文件、會話信息、文件內(nèi)容),<CODE>HashMap.get</CODE>
方法可能對應(yīng)于許多條字節(jié)碼指令。例如,如果您正在編寫一個(gè) Web 服務(wù)器,而且所有的高速緩存的頁都存儲在 <CODE>HashMap</CODE>
中,那么每個(gè)請求都將需要獲得并占用那個(gè) map 上的鎖,這就將成為一個(gè)瓶頸。</P>
<P>我們可以擴(kuò)展鎖粒度技術(shù)以應(yīng)付這種情形,盡管我們必須很小心,因?yàn)橛信c這種方法有關(guān)的一些 Java 內(nèi)存模型(Java Memory
Model,JMM)危害。清單 5 中的 <CODE>LockPoolMap</CODE> 展示了線程安全的 <CODE>get()</CODE>
和 <CODE>put()</CODE> 方法,但把同步分散在了鎖池中,充分降低了爭用可能性。</P>
<P><CODE>LockPoolMap</CODE> 是線程安全的,其功能類似于簡化的
<CODE>HashMap</CODE>,但卻有更多吸引人的爭用屬性。同步不是在每個(gè) <CODE>get()</CODE> 或
<CODE>put()</CODE> 操作上對整個(gè) map 進(jìn)行,而是在散列單元(bucket)級上完成。每個(gè) bucket
都有一個(gè)鎖,而且該鎖在遍歷 bucket(為了讀或?qū)懀┑臅r(shí)候被獲取。鎖在創(chuàng)建 map 的時(shí)候被創(chuàng)建(如果不在此時(shí)創(chuàng)建鎖,將會出現(xiàn) JMM
問題。)</P>
<P>如果您創(chuàng)建了帶有很多 bucket 的 <CODE>LockPoolMap</CODE>,那么將有很多線程可以并發(fā)地使用該
map,同時(shí)爭用的可能性也被大大降低了。然而,減少爭用并不是免費(fèi)的午餐。由于沒有在全局鎖上同步,使得將 map 作為一個(gè)整體進(jìn)行操作,例如
<CODE>size()</CODE> 方法,變得更加困難。<CODE>size()</CODE> 的實(shí)現(xiàn)將不得不依次獲取各個(gè) bucket
的鎖,對該 bucket 中的節(jié)點(diǎn)進(jìn)行計(jì)數(shù),釋放鎖,然后繼續(xù)到下一個(gè) bucket。然而前面的鎖一旦被釋放,其它的線程就將可以自由修改前面的
bucket。到 <CODE>size()</CODE>
完成對元素的計(jì)數(shù)時(shí),結(jié)果很可能是錯(cuò)的。不過,<CODE>LockPoolMap</CODE>
技術(shù)在某些方面還是可以做得相當(dāng)好的,例如共享高速緩存。</P><A name=listing5><B>清單 5. 減小 HashMap
上鎖的粒度</B></A>
<TABLE cellSpacing=0 cellPadding=5 width="100%" bgColor=#cccccc
border=1><TBODY>
<TR>
<TD><PRE><CODE>
import java.util.*;
/**
* LockPoolMap implements a subset of the Map interface (get, put, clear)
* and performs synchronization at the bucket level, not at the map
* level. This reduces contention, at the cost of losing some Map
* functionality, and is well suited to simple caches. The number of
* buckets is fixed and does not increase.
*/
public class LockPoolMap {
private Node[] buckets;
private Object[] locks;
private static final class Node {
public final Object key;
public Object value;
public Node next;
public Node(Object key) { this.key = key; }
}
public LockPoolMap(int size) {
buckets = new Node[size];
locks = new Object[size];
for (int i = 0; i < size; i++)
locks[i] = new Object();
}
private final int hash(Object key) {
int hash = key.hashCode() % buckets.length;
if (hash < 0)
hash *= -1;
return hash;
}
public void put(Object key, Object value) {
int hash = hash(key);
synchronized(locks[hash]) {
Node m;
for (m=buckets[hash]; m != null; m=m.next) {
if (m.key.equals(key)) {
m.value = value;
return;
}
}
// We must not have found it, so put it at the beginning of the chain
?? 快捷鍵說明
復(fù)制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -