📄 轻松使用线程:减少争用.htm
字号:
height=5><B>相关内容:</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 应用程序</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>抛开您自己的习惯,提高应用程序的性能</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>,我们讨论了无争用同步的性能开销。尽管常常听说同步方法调用的开销是非同步方法调用开销的 50
倍,这个数字实际上仍然相当容易产生误导。JVM
的每个后继版本在整体性能上的提高和无争用同步代价的降低使得无争用同步开销问题不再显得那么突出。但争用同步的代价仍然非常高昂。而且,严重的争用将严重损害应用程序的可伸缩性
— 随着负载的增加,存在严重争用同步的应用程序的性能将显著降低。本文将探讨能够减少争用的几种技术,以提高您程序的可伸缩性。
<P>参加 Brian 的<A href="javascript:void%20forumWindow()">多线程 Java
编程讨论论坛</A>以获得您工程中的线程和并发问题的帮助。</P></BLOCKQUOTE>
<P>当我们说一个程序“太慢”时,我们通常是指两个性能属性 — 等待时间和可伸缩性 —
中的一个。<I>等待时间</I>指完成一个给定任务所花费的时间,而<I>可伸缩性</I>则指随着负载的增加或给定计算资源的增加,程序的性能将怎样变化。严重的争用对等待时间和可伸缩性都不利。</P>
<P><A name=1><SPAN
class=atitle2>争用为什么是这样一个问题</SPAN></A><BR>争用同步之所以慢,是因为它涉及多个线程切换和系统调用。当多个线程争用同一个管程时,JVM
将不得不维护一个等待该管程的线程队列(并且这个队列在多个处理器间必须是同步的),这就意味着花费在 JVM 或 OS
代码上的时间相对多了,而花费在程序代码上的时间则相对少了。而且,争用还削弱了可伸缩性,因为它迫使调度程序把操作序列化,即使有可用的空闲处理器也是如此。当一个线程正在执行一个同步块时,任何等待进入该块的线程都将被阻塞。如果没有其他的线程可供执行,那么处理器就将空闲。</P>
<P>如果想编写具可伸缩性的多线程程序,我们就必须减少对临界资源的争用。有很多技术可以做到这一点,但在应用它们之前,您需要仔细研究一下您的代码,判断出在什么情况下您需要在公共管程上同步。判断哪些锁是瓶颈很困难:有时候锁隐藏在类库中,有时候又通过同步方法隐式地指定,因此在阅读代码时,锁并不那么明显。而且,目前的争用检测工具也很差。</P>
<P><A name=2><SPAN class=atitle2>技术
1:放进去,取出来</SPAN></A><BR>使同步块尽可能小显然是降低争用可能性的一种技术。一个线程占用一个给定锁的时间越短,另一个线程在该线程仍占用锁时请求该锁的可能性就越小。因此在您应该使用同步去访问或更新共享变量时,在同步块的外面进行线程安全的预处理或后处理通常会更好些。</P>
<P>清单 1 演示了这种技术。我们的应用程序维护一个代表各种实体的属性的
<CODE>HashMap</CODE>,给定用户的访问权列表就是这种属性中的一种。访问权被存储成一个以逗号隔开的权限列表。方法
<CODE>userHasAdminAccess()</CODE>
在全局属性表中查找用户的访问权,并判断该用户是否有被称为“ADMIN”的访问权。</P><A name=listing1><B>清单 1.
在同步块中花费太多(多于必要时间)时间</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>这个版本的 <CODE>userHasAdminAccess</CODE>
是线程安全的,但它占用锁的时间比必要占用时间长太多。为创建串联的字符串<CODE>“users.brian.accessRights”</CODE>,编译器将创建一个临时的
<CODE>StringBuffer</CODE> 对象,调用 <CODE>StringBuffer.append</CODE> 三次,然后调用
<CODE>StringBuffer.toString</CODE>,这意味着至少两个对象的创建和几个方法调用。接着,程序将调用
<CODE>HashMap.get</CODE> 检索该字符串,然后调用 <CODE>String.indexOf</CODE>
抽取想要的权限标识符。
就占这个方法所做的全部工作的百分比而言,预处理和后处理的比重是很大的;因为它们是线程安全的,所以将它们移到同步块外面是有意义的,如清单 2
所示。</P><A name=listing2><B>清单 2. 减少花费在同步块中的时间</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>另一方面,有可能会过度使用这种技术。要是您想用一小块线程安全代码把要求同步的两个操作隔开,那么只使用一个同步块一般会更好些。</P>
<P><A name=3><SPAN class=atitle2>技术
2:减小锁的粒度</SPAN></A><BR>把您的同步分散在更多的锁上是减少争用的另一种有价值的技术。例如,假设您有一个类,它用两个单独的散列表来存储用户信息和服务信息,如清单
3 所示。</P><A name=listing3><B>清单 3. 一个减小锁的粒度的机会</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>这里,用户和服务数据的访问器方法是同步的,这意味着它们在 <CODE>AttributesStore</CODE>
对象上同步。虽然这样做是完全线程安全的,但却增加了毫无实际意义的争用可能性。如果一个线程正在执行
<CODE>setUserInfo</CODE>,就不仅意味着其它线程将被锁在 <CODE>setUserInfo</CODE> 和
<CODE>getUserInfo</CODE> 外面(这是我们希望的),而且意味着它们也将被锁在
<CODE>getServiceInfo</CODE> 和 <CODE>setServiceInfo</CODE> 外面。</P>
<P>通过使访问器只在共享的实际对象(<CODE>userMap</CODE> 和 <CODE>servicesMap</CODE>
对象)上同步可以避免这个问题,如清单 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>现在,访问服务 map(servicesMap)的线程将不会与试图访问用户 map(usersMap)的线程发生争用。(在这种情况下,通过使用
Collections 框架提供的同步包装机制,即 <CODE>Collections.synchronizedMap</CODE> 来创建 map
可以达到同样的效果。)假设对两个 map 的请求是平均分布的,那么这种技术在这种情况下将把可能的争用数目减半。</P>
<P><A name=4><SPAN class=atitle2>在 HashMap 中应用技术 2</SPAN></A><BR>服务器端的
Java 应用程序中最普通的争用瓶颈之一是 <CODE>HashMap</CODE>。应用程序使用 <CODE>HashMap</CODE>
来高速缓存所有类别的临界共享数据(用户概要文件、会话信息、文件内容),<CODE>HashMap.get</CODE>
方法可能对应于许多条字节码指令。例如,如果您正在编写一个 Web 服务器,而且所有的高速缓存的页都存储在 <CODE>HashMap</CODE>
中,那么每个请求都将需要获得并占用那个 map 上的锁,这就将成为一个瓶颈。</P>
<P>我们可以扩展锁粒度技术以应付这种情形,尽管我们必须很小心,因为有与这种方法有关的一些 Java 内存模型(Java Memory
Model,JMM)危害。清单 5 中的 <CODE>LockPoolMap</CODE> 展示了线程安全的 <CODE>get()</CODE>
和 <CODE>put()</CODE> 方法,但把同步分散在了锁池中,充分降低了争用可能性。</P>
<P><CODE>LockPoolMap</CODE> 是线程安全的,其功能类似于简化的
<CODE>HashMap</CODE>,但却有更多吸引人的争用属性。同步不是在每个 <CODE>get()</CODE> 或
<CODE>put()</CODE> 操作上对整个 map 进行,而是在散列单元(bucket)级上完成。每个 bucket
都有一个锁,而且该锁在遍历 bucket(为了读或写)的时候被获取。锁在创建 map 的时候被创建(如果不在此时创建锁,将会出现 JMM
问题。)</P>
<P>如果您创建了带有很多 bucket 的 <CODE>LockPoolMap</CODE>,那么将有很多线程可以并发地使用该
map,同时争用的可能性也被大大降低了。然而,减少争用并不是免费的午餐。由于没有在全局锁上同步,使得将 map 作为一个整体进行操作,例如
<CODE>size()</CODE> 方法,变得更加困难。<CODE>size()</CODE> 的实现将不得不依次获取各个 bucket
的锁,对该 bucket 中的节点进行计数,释放锁,然后继续到下一个 bucket。然而前面的锁一旦被释放,其它的线程就将可以自由修改前面的
bucket。到 <CODE>size()</CODE>
完成对元素的计数时,结果很可能是错的。不过,<CODE>LockPoolMap</CODE>
技术在某些方面还是可以做得相当好的,例如共享高速缓存。</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
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -