热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

轻松使用线程:减少争用

http:www.ibm.comdeveloperworkscnjavaj-threadsindex2.html抛开您自己的习惯,提高应用程序的性能在本系列的第1部分

http://www.ibm.com/developerworks/cn/java/j-threads/index2.html



抛开您自己的习惯,提高应用程序的性能

在本系列的 第 1 部分,我们讨论了无争用同步的性能开销。尽管常常听说同步方法调用的开销是非同步方法调用开销的 50 倍,这个数字实际上仍然相当容易产生误导。JVM 的每个后继版本在整体性能上的提高和无争用同步代价的降低使得无争用同步开销问题不再显得那么突出。但争用同步的代价仍然非常高昂。而且,严重的争用将严重损害应用程序的可伸缩性 — 随着负载的增加,存在严重争用同步的应用程序的性能将显著降低。本文将探讨能够减少争用的几种技术,以提高您程序的可伸缩性。

参加 Brian 的 多线程 Java 编程讨论论坛以获得您工程中的线程和并发问题的帮助。

0 评论:

Brian Goetz (brian@quiotix.com), 软件顾问

2001 年 9 月 01 日

  • +内容


当我们说一个程序“太慢”时,我们通常是指两个性能属性 — 等待时间和可伸缩性 — 中的一个。 等待时间指完成一个给定任务所花费的时间,而 可伸缩性则指随着负载的增加或给定计算资源的增加,程序的性能将怎样变化。严重的争用对等待时间和可伸缩性都不利。

争用为什么是这样一个问题

争用同步之所以慢,是因为它涉及多个线程切换和系统调用。当多个线程争用同一个管程时,JVM 将不得不维护一个等待该管程的线程队列(并且这个队列在多个处理器间必须是同步的),这就意味着花费在 JVM 或 OS 代码上的时间相对多了,而花费在程序代码上的时间则相对少了。而且,争用还削弱了可伸缩性,因为它迫使调度程序把操作序列化,即使有可用的空闲处理器也是如此。当一个线程正在执行一个同步块时,任何等待进入该块的线程都将被阻塞。如果没有其他的线程可供执行,那么处理器就将空闲。

如果想编写具可伸缩性的多线程程序,我们就必须减少对临界资源的争用。有很多技术可以做到这一点,但在应用它们之前,您需要仔细研究一下您的代码,判断出在什么情况下您需要在公共管程上同步。判断哪些锁是瓶颈很困难:有时候锁隐藏在类库中,有时候又通过同步方法隐式地指定,因此在阅读代码时,锁并不那么明显。而且,目前的争用检测工具也很差。

回页首

技术 1:放进去,取出来

使同步块尽可能小显然是降低争用可能性的一种技术。一个线程占用一个给定锁的时间越短,另一个线程在该线程仍占用锁时请求该锁的可能性就越小。因此在您应该使用同步去访问或更新共享变量时,在同步块的外面进行线程安全的预处理或后处理通常会更好些。

清单 1 演示了这种技术。我们的应用程序维护一个代表各种实体的属性的 HashMap ,给定用户的访问权列表就是这种属性中的一种。访问权被存储成一个以逗号隔开的权限列表。方法 userHasAdminAccess() 在全局属性表中查找用户的访问权,并判断该用户是否有被称为“ADMIN”的访问权。

清单 1. 在同步块中花费太多(多于必要时间)时间

public boolean userHasAdminAccess(String userName) {synchronized (attributesMap) {String rights = attributesMap.get("users." + userName + ".accessRights");if (rights == null) return false;elsereturn (rights.indexOf("ADMIN") >= 0);}}

这个版本的 userHasAdminAccess 是线程安全的,但它占用锁的时间比必要占用时间长太多。为创建串联的字符串“users.brian.accessRights” ,编译器将创建一个临时的 StringBuffer 对象,调用 StringBuffer.append 三次,然后调用StringBuffer.toString ,这意味着至少两个对象的创建和几个方法调用。接着,程序将调用 HashMap.get 检索该字符串,然后调用String.indexOf 抽取想要的权限标识符。 就占这个方法所做的全部工作的百分比而言,预处理和后处理的比重是很大的;因为它们是线程安全的,所以将它们移到同步块外面是有意义的,如清单 2 所示。

清单 2. 减少花费在同步块中的时间

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));}

另一方面,有可能会过度使用这种技术。要是您想用一小块线程安全代码把要求同步的两个操作隔开,那么只使用一个同步块一般会更好些。

技术 2:减小锁的粒度

把您的同步分散在更多的锁上是减少争用的另一种有价值的技术。例如,假设您有一个类,它用两个单独的散列表来存储用户信息和服务信息,如清单 3 所示。

清单 3. 一个减小锁的粒度的机会

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);}
}

这里,用户和服务数据的访问器方法是同步的,这意味着它们在 AttributesStore 对象上同步。虽然这样做是完全线程安全的,但却增加了毫无实际意义的争用可能性。如果一个线程正在执行 setUserInfo ,就不仅意味着其它线程将被锁在 setUserInfo 和 getUserInfo 外面(这是我们希望的),而且意味着它们也将被锁在 getServiceInfo 和 setServiceInfo 外面。

通过使访问器只在共享的实际对象( userMap 和 servicesMap 对象)上同步可以避免这个问题,如清单 4 所示。

清单 4. 减小锁的粒度

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);}}
}

现在,访问服务 map(servicesMap)的线程将不会与试图访问用户 map(usersMap)的线程发生争用。(在这种情况下,通过使用 Collections 框架提供的同步包装机制,即 Collections.synchronizedMap 来创建 map 可以达到同样的效果。)假设对两个 map 的请求是平均分布的,那么这种技术在这种情况下将把可能的争用数目减半。

回页首

在 HashMap 中应用技术 2

服务器端的 Java 应用程序中最普通的争用瓶颈之一是 HashMap 。应用程序使用 HashMap 来高速缓存所有类别的临界共享数据(用户概要文件、会话信息、文件内容), HashMap.get 方法可能对应于许多条字节码指令。例如,如果您正在编写一个 Web 服务器,而且所有的高速缓存的页都存储在 HashMap 中,那么每个请求都将需要获得并占用那个 map 上的锁,这就将成为一个瓶颈。

我们可以扩展锁粒度技术以应付这种情形,尽管我们必须很小心,因为有与这种方法有关的一些 Java 内存模型(Java Memory Model,JMM)危害。清单 5 中的 LockPoolMap 展示了线程安全的 get() 和 put() 方法,但把同步分散在了锁池中,充分降低了争用可能性。

LockPoolMap 是线程安全的,其功能类似于简化的 HashMap ,但却有更多吸引人的争用属性。同步不是在每个 get() 或 put() 操作上对整个 map 进行,而是在散列单元(bucket)级上完成。每个 bucket 都有一个锁,而且该锁在遍历 bucket(为了读或写)的时候被获取。锁在创建 map 的时候被创建(如果不在此时创建锁,将会出现 JMM 问题。)

如果您创建了带有很多 bucket 的 LockPoolMap ,那么将有很多线程可以并发地使用该 map,同时争用的可能性也被大大降低了。然而,减少争用并不是免费的午餐。由于没有在全局锁上同步,使得将 map 作为一个整体进行操作,例如 size() 方法,变得更加困难。 size() 的实现将不得不依次获取各个 bucket 的锁,对该 bucket 中的节点进行计数,释放锁,然后继续到下一个 bucket。然而前面的锁一旦被释放,其它的线程就将可以自由修改前面的 bucket。到 size() 完成对元素的计数时,结果很可能是错的。不过, LockPoolMap 技术在某些方面还是可以做得相当好的,例如共享高速缓存。

清单 5. 减小 HashMap 上锁的粒度

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 }

表 1 比较了共享 map 的三种实现的性能:同步的 HashMap ,非同步的 HashMap (线程不安全的)和 LockPoolMap 。提供非同步的版本只是为了展示争用的开销。我们在使用 Sun 1.3 JDK 的双处理器系统 Linux 系统上,用不同数目的线程,运行了在 map 上执行随机进行 put() 和get() 操作的测试。该表展示了每个组合的运行时间。这个测试是有点极端的一个案例,测试程序只是访问 map,而不做任何别的事,因此它比实际的程序存在多得多的争用,设计这个测试只是为了说明争用对性能的损害。

表 1. HashMap 和 LockPoolMap 之间的可伸缩性比较

线程 非同步的 HashMap (不安全的) 同步的 HashMap LockPoolMap
  1.1 1.4 1.6
  1.1 57.6 3.7
  2.1 123.5 7.7
  3.7 272.3 16.7
16 6.8 577.0 37.9
32 13.5 1233.3 80.5

虽然在线程数量很多的情况下,所有的实现都表现出相似的伸缩性特征,但 HashMap 实现在从一个线程变到两个线程时却表现出对性能影响的巨大变化,因为此时每一个 put() 和 get() 操作都存在争用。在线程数大于 1 时, LockPoolMap 技术几乎比 HashMap 技术快 15 倍。这个差别反映了调度开销上的时间损失和用于等待获取锁的空闲时间。 LockPoolMap 的优势在拥有更多处理器的系统中将表现得更加明显。

技术 3:锁崩溃

另一种能提高性能的技术称为“锁崩溃”(请参阅清单 6)。回想一下, Vector 类的方法几乎都是同步的。假设您有一个 String 值的 Vector ,并想搜索最长的 String 。进一步假设您已经知道只会在末端添加元素,而且元素不会被删除,那么,像 getLongest() 方法所展示的那样访问数据是安全的(通常),该方法只是调用 elementAt() 来检索每个元素,简单地对 Vector 的元素作循环。

getLongest2() 方法非常相似,除了在开始循环之前获取 Vector 上的锁之外。这样做的结果是当 elementAt() 试图获取锁时,JVM 将注意到当前线程已经拥有锁,而且将不会参与争用。 getLongest2() 加大了同步块,这似乎违背了“放进去,取出来”的原则,但因为避免了很大量可能的同步,调度开销的时间损失也少了,速度仍然快得多。

在运行 Sun 1.3 JDK 的双处理器 Linux 系统上,拥有两个线程,仅仅循环调用 getLongest2() 的的测试程序比调用 getLongest() 的要快 10 倍以上。虽然两个程序的序列化程度相同,但前者调度开销的时间损失要少得多。这又是一个极端的示例,但它表明争用的调度开销并不是微不足道的。即使只运行一个线程,崩溃版的速度也要快约 30% :获取您已占用的锁比获取无人占用的锁要快得多。

清单 6. 锁崩溃

Vector v;...public String getLongest() {int maxLen = 0;String longest = null;for (int i=0; i maxLen) {maxLen = s.length();longest = s;}}return longest;}public String getLongest2() {int maxLen = 0;String longest = null;synchronized (v) { for (int i=0; i maxLen) {maxLen = s.length();longest = s;} } return longest;}}

回页首

结论

争用同步会严重影响程序的可伸缩性。更糟的是,除非您进行实际的负载测试,否则与争用相关的性能问题并不总是会在开发和测试过程中表现出来。本文提供的技术能有效地降低程序的争用代价,并增大程序在出现非线性伸缩行为之前所能承受的负载。但在应用这些技术之前,您必须首先分析您的程序,以判断哪里可能出现争用。






推荐阅读
  • 本文介绍了操作系统的定义和功能,包括操作系统的本质、用户界面以及系统调用的分类。同时还介绍了进程和线程的区别,包括进程和线程的定义和作用。 ... [详细]
  • 本文介绍了Hyperledger Fabric外部链码构建与运行的相关知识,包括在Hyperledger Fabric 2.0版本之前链码构建和运行的困难性,外部构建模式的实现原理以及外部构建和运行API的使用方法。通过本文的介绍,读者可以了解到如何利用外部构建和运行的方式来实现链码的构建和运行,并且不再受限于特定的语言和部署环境。 ... [详细]
  • 微软头条实习生分享深度学习自学指南
    本文介绍了一位微软头条实习生自学深度学习的经验分享,包括学习资源推荐、重要基础知识的学习要点等。作者强调了学好Python和数学基础的重要性,并提供了一些建议。 ... [详细]
  • Nginx使用AWStats日志分析的步骤及注意事项
    本文介绍了在Centos7操作系统上使用Nginx和AWStats进行日志分析的步骤和注意事项。通过AWStats可以统计网站的访问量、IP地址、操作系统、浏览器等信息,并提供精确到每月、每日、每小时的数据。在部署AWStats之前需要确认服务器上已经安装了Perl环境,并进行DNS解析。 ... [详细]
  • 本文介绍了lua语言中闭包的特性及其在模式匹配、日期处理、编译和模块化等方面的应用。lua中的闭包是严格遵循词法定界的第一类值,函数可以作为变量自由传递,也可以作为参数传递给其他函数。这些特性使得lua语言具有极大的灵活性,为程序开发带来了便利。 ... [详细]
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • Android Studio Bumblebee | 2021.1.1(大黄蜂版本使用介绍)
    本文介绍了Android Studio Bumblebee | 2021.1.1(大黄蜂版本)的使用方法和相关知识,包括Gradle的介绍、设备管理器的配置、无线调试、新版本问题等内容。同时还提供了更新版本的下载地址和启动页面截图。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • 本文介绍了计算机网络的定义和通信流程,包括客户端编译文件、二进制转换、三层路由设备等。同时,还介绍了计算机网络中常用的关键词,如MAC地址和IP地址。 ... [详细]
  • 本文介绍了在Windows环境下如何配置php+apache环境,包括下载php7和apache2.4、安装vc2015运行时环境、启动php7和apache2.4等步骤。希望对需要搭建php7环境的读者有一定的参考价值。摘要长度为169字。 ... [详细]
  • 预备知识可参考我整理的博客Windows编程之线程:https:www.cnblogs.comZhuSenlinp16662075.htmlWindows编程之线程同步:https ... [详细]
  • Java和JavaScript是什么关系?java跟javaScript都是编程语言,只是java跟javaScript没有什么太大关系,一个是脚本语言(前端语言),一个是面向对象 ... [详细]
  • 篇首语:本文由编程笔记#小编为大家整理,主要介绍了软件测试知识点之数据库压力测试方法小结相关的知识,希望对你有一定的参考价值。 ... [详细]
  • 深入解析Linux下的I/O多路转接epoll技术
    本文深入解析了Linux下的I/O多路转接epoll技术,介绍了select和poll函数的问题,以及epoll函数的设计和优点。同时讲解了epoll函数的使用方法,包括epoll_create和epoll_ctl两个系统调用。 ... [详细]
  • BZOJ1233 干草堆单调队列优化DP
    本文介绍了一个关于干草堆摆放的问题,通过使用单调队列来优化DP算法,求解最多可以叠几层干草堆。具体的解题思路和转移方程在文章中进行了详细说明,并给出了相应的代码示例。 ... [详细]
author-avatar
amroc_394
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有