当前位置:天才代写 > tutorial > JAVA 教程 > Java理论与实践: 并发荟萃类

Java理论与实践: 并发荟萃类

2017-11-11 08:00 星期六 所属: JAVA 教程 浏览:398

副标题#e#

在Java类库中呈现的第一个关联的荟萃类是 Hashtable ,它是JDK 1.0的一 部门。 Hashtable 提供了一种易于利用的、线程安详的、关联的map成果,这当 然也是利便的。然而,线程安详性是凭价钱换来的―― Hashtable 的所有要领 都是同步的。此时,无竞争的同步会导致可观的机能价钱。 Hashtable 的后继 者 HashMap 是作为JDK1.2中的荟萃框架的一部门呈现的,它通过提供一个差异 步的基类和一个同步的包装器 Collections.synchronizedMap ,办理了线程安 全性问题。通过将根基的成果从线程安详性中分分开来, Collections.synchronizedMap 答允需要同步的用户可以拥有同步,而不需要同 步的用户则不必为同步支付价钱。

Hashtable 和 synchronizedMap 所采纳的得到同步的简朴要领(同步 Hashtable 中可能同步的 Map 包装器工具中的每个要领)有两个主要的不敷。 首先,这种要领对付可伸缩性是一种障碍,因为一次只能有一个线程可以会见 hash表。同时,这样仍不敷以提供真正的线程安详性,很多公用的殽杂操纵仍然 需要特另外同步。固然诸如 get() 和 put() 之类的简朴操纵可以在不需要特别 同步的环境下安详地完成,但照旧有一些公用的操纵序列,譬喻迭代可能put- if-absent(空则放入),需要外部的同步,以制止数据争用。

有条件的线程安详性

同步的荟萃包装器 synchronizedMap 和 synchronizedList ,有时也被称作 有条件地线程安详――所有单个的操纵都是线程安详的,可是多个操纵构成的操 作序列却大概导致数据争用,因为在操纵序列中节制流取决于前面操纵的功效。 清单1中第一片断展示了公用的put-if-absent语句块――假如一个条目不在 Map 中,那么添加这个条目。不幸的是,在 containsKey() 要领返回到 put() 要领 被挪用这段时间内,大概会有另一个线程也插入一个带有沟通键的值。假如您想 确保只有一次插入,您需要用一个对 Map m 举办同步的同步块将这一对语句包 装起来。

清单1中其他的例子与迭代有关。在第一个例子中, List.size() 的功效在 轮回的执行期间大概会变得无效,因为另一个线程可以从这个列表中删除条目。 假如机缘不恰当,在恰好进入轮回的最后一次迭代之后有一个条目被另一个线程 删除了,则 List.get() 将返回 null ,而 doSomething() 则很大概会抛出一 个 NullPointerException 异常。那么,采纳什么法子才气制止这种环境呢?如 果当您正在迭代一个 List 时另一个线程也大概正在会见这个 List ,那么在进 行迭代时您必需利用一个 synchronized 块将这个 List 包装起来,在 List 1 上同步,从而锁住整个 List 。这样做固然办理了数据争用问题,可是在并发性 方面支付了更多的价钱,因为在迭代期间锁住整个 List 会阻塞其他线程,使它 们在很长一段时间内不能会见这个列表。

荟萃框架引入了迭代器,用于遍历一个列表可能其他荟萃,从而优化了对一 个荟萃中的元素举办迭代的进程。然而,在 java.util 荟萃类中实现的迭代器 极易瓦解,也就是说,假如在一个线程正在通过一个 Iterator 遍历集适时,另 一个线程也来修改这个荟萃,那么接下来的 Iterator.hasNext() 或 Iterator.next() 挪用将抛出 ConcurrentModificationException 异常。就拿 适才这个例子来讲,假如想要防备呈现 ConcurrentModificationException 异 常,那么当您正在举办迭代时,您必需利用一个在 List l 上同步的 synchronized 块将该 List 包装起来,从而锁住整个 List 。(可能,您也可 以挪用 List.toArray() ,在差异步的环境下对数组举办迭代,可是假如列表比 较大的话这样做价钱很高)。

清单 1. 同步的map中的公用竞争条件

Map m = Collections.synchronizedMap(new HashMap());
  List l = Collections.synchronizedList(new ArrayList());
  // put-if-absent idiom -- contains a race condition
  // may require external synchronization
  if (!map.containsKey(key))
   map.put(key, value);
  // ad-hoc iteration -- contains race conditions
  // may require external synchronization
  for (int i=0; i<list.size(); i++) {
   doSomething(list.get(i));
  }
  // normal iteration -- can throw ConcurrentModificationException
  // may require external synchronization
  for (Iterator i=list.iterator(); i.hasNext(); ) {
   doSomething(i.next());
  }

信任的错觉

synchronizedList 和 synchronizedMap 提供的有条件的线程安详性也带来 了一个隐患 ―― 开拓者会假设,因为这些荟萃都是同步的,所以它们都是线程 安详的,这样一来他们对付正确地同步殽杂操纵这件事就会疏忽。其功效是尽量 外貌上这些措施在负载较轻的时候可以或许正常事情,可是一旦负载较重,它们就会 开始抛出 NullPointerException 或 ConcurrentModificationException 。


#p#副标题#e#

可伸缩性问题

#p#分页标题#e#

可伸缩性指的是一个应用措施在事情负载和可用处理惩罚资源增加时其吞吐量的 表示环境。一个可伸缩的措施可以或许通过利用更多的处理惩罚器、内存可能I/O带宽来 相应地处理惩罚更大的事情负载。锁住某个共享的资源以得到独有式的会见这种做法 会形成可伸缩性瓶颈――它使其他线程不能会见谁人资源,纵然有空闲的处理惩罚器 可以挪用那些线程也无济于事。为了取得可伸缩性,我们必需消除可能淘汰我们 对独有式资源锁的依赖。

同步的荟萃包装器以赶早期的 Hashtable 和 Vector 类带来的更大的问题是 ,它们在单个的锁长举办同步。这意味着一次只有一个线程可以会见荟萃,假如 有一个线程正在读一个 Map ,那么所有其他想要读可能写这个 Map 的线程就必 须期待。最常见的 Map 操纵, get() 和 put() ,大概比外貌上要举办更多的 处理惩罚――当遍历一个hash表的bucket以期找到某一特定的key时, get() 必需对 大量的候选bucket挪用 Object.equals() 。假如key类所利用的 hashCode() 函 数不能将value匀称地漫衍在整个hash表范畴内,可能存在大量的hash斗嘴,那 么某些bucket链就会比其他的链长许多,而遍历一个长的hash链以及对该hash链 上必然百分比的元素挪用 equals() 是一件很慢的工作。在上述条件下,挪用 get() 和 put() 的价钱高的问题不只仅是指会见进程的迟钝,并且,当有线程 正在遍历谁人hash链时,所有其他线程都被锁在外面,不能会见这个 Map 。

(哈希表按照一个叫做hash的数字要害字(key)将工具存储在bucket中。 hash value是从工具中的值计较得来的一个数字。每个差异的hash value城市创 建一个新的bucket。要查找一个工具,您只需要计较这个工具的hash value并搜 索相应的bucket就行了。通过快速地找到相应的bucket,就可以淘汰您需要搜索 的工具数量了。译者注)

get() 执行起来大概会占用大量的时间,而在某些环境下,前面已经作了讨 论的有条件的线程安详性问题会让这个问题变得还要糟糕得多。 清单1 中演示 的争用条件经常使得对单个荟萃的锁在单个操纵执行完毕之后还必需继承保持一 段较长的时间。假如您要在整个迭代期间都保持对荟萃的锁,那么其他的线程就 会在锁外逗留很长的一段时间,期待解锁。

实例:一个简朴的cache

Map 在处事器应用中最常见的应用之一就是实现一个 cache。 处事器应用可 能需要缓存文件内容、生成的页面、数据库查询的功效、与颠末理会的XML文件 相关的DOM树,以及很多其他范例的数据。cache的主要用途是重用前一次处理惩罚得 出的功效以淘汰处事时间和增加吞吐量。cache事情负载的一个典范的特征就是 检索大大多于更新,因此(抱负环境下)cache可以或许提供很是好的 get() 机能。 不外,利用会故障机能的cache还不如完全不消cache。

假如利用 synchronizedMap 来实现一个cache,那么您就在您的应用措施中 引入了一个潜在的可伸缩性瓶颈。因为一次只有一个线程可以会见 Map ,这些 线程包罗那些要从 Map 中取出一个值的线程以及那些要将一个新的 (key, value) 对插入到该map中的线程。

减小锁粒度

提高 HashMap 的并发性同时还提供线程安详性的一种要领是破除对整个表使 用一个锁的方法,而回收对hash表的每个bucket都利用一个锁的方法(可能,更 常见的是,利用一个锁池,每个锁认真掩护几个bucket)。这意味着多个线程可 以同时地会见一个 Map 的差异部门,而不必争用单个的荟萃范畴的锁。这种方 法可以或许直接提高插入、检索以及移除操纵的可伸缩性。不幸的是,这种并发性是 以必然的价钱换来的――这使得对整个荟萃举办操纵的一些要领(譬喻 size() 或 isEmpty() )的实现越发坚苦,因为这些要领要求一次得到很多的锁,而且 还存在返回不正确的功效的风险。然而,对付某些环境,譬喻实现cache,这样 做是一个很好的折衷――因为检索和插入操纵较量频繁,而 size() 和 isEmpty() 操纵则少得多。

ConcurrentHashMap

util.concurrent 包中的 ConcurrentHashMap 类(也将呈此刻JDK 1.5中的 java.util.concurrent 包中)是对 Map 的线程安详的实现,比起 synchronizedMap 来,它提供了好得多的并发性。多个读操纵险些总可以并发地 执行,同时举办的读和写操纵凡是也能并发地执行,而同时举办的写操纵仍然可 以不时地并发举办(相关的类也提供了雷同的多个读线程的并发性,可是,只允 许有一个勾当的写线程) 。ConcurrentHashMap 被设计用来优化检索操纵;实 际上,乐成的 get() 操纵完成之后凡是基础不会有锁着的资源。要在不利用锁 的环境下取得线程安详性需要必然的能力性,而且需要对Java内存模子(Java Memory Model)的细节有深入的领略。 ConcurrentHashMap 实现,加上 util.concurrent 包的其他部门,已经被研究正确性和线程安详性的并发专家所 正视。在下个月的文章中,我们将看看 ConcurrentHashMap 的实现的细节。

#p#分页标题#e#

ConcurrentHashMap 通过稍微地败坏它对换用者的理睬而得到了更高的并发 性。检索操纵将可以返回由最近完成的插入操纵所插入的值,也可以返回在法式 上是并发的插入操纵所添加的值(可是决不会返回一个没有意义的功效)。由 ConcurrentHashMap.iterator() 返回的 Iterators 将每次最多返回一个元素, 而且决不会抛出 ConcurrentModificationException 异常,可是大概会也大概 不会反应在该迭代器被构建之后产生的插入操纵可能移除操纵。在对荟萃举办迭 代时,不需要表范畴的锁就能提供线程安详性。在任何不依赖于锁整个表来防备 更新的应用措施中,可以利用 ConcurrentHashMap 来替代 synchronizedMap 或 Hashtable 。

上述改造使得 ConcurrentHashMap 可以或许提供比 Hashtable 高得多的可伸缩 性,并且,对付许多范例的公用案例(好比共享的cache)来说,还不消损失其 效率。

#p#副标题#e#

好了几多?

表 1对 Hashtable 和 ConcurrentHashMap 的可伸缩性举办了大致的较量。 在每次运行进程中, n 个线程并发地执行一个死轮回,在这个死轮回中这些线 程从一个 Hashtable 可能 ConcurrentHashMap 中检索随机的key value,发明 在执行 put() 操纵时有80%的检索失败率,在执行操纵时有1%的检索乐成率。测 试地址的平台是一个双处理惩罚器的Xeon系统,操纵系统是Linux。数据显示了 10,000,000次迭代以毫秒计的运行时间,这个数据是在将对 ConcurrentHashMap 的 操纵尺度化为一个线程的环境下举办统计的。您可以看到,当线程增加到多 个时, ConcurrentHashMap 的机能仍然保持上升趋势,而 Hashtable 的机能则 跟着争用锁的环境的呈现而当即降了下来。

比起凡是环境下的处事器应用,这次测试中线程的数量看上去有点少。然而 ,因为每个线程都在不断地对表举办操纵,所以这与实际情况下利用这个表的更 大都量的线程的争用环境根基等同。

表 1.Hashtable 与 ConcurrentHashMap在可伸缩性方面的较量

线程数 ConcurrentHashMap Hashtable
1 1.00 1.03
2 2.59 32.40
4 5.58 78.23
8 13.21 163.48
16 27.58 341.21
32 57.27 778.41

CopyOnWriteArrayList

在那些遍历操纵大大地多于插入或移除操纵的并发应用措施中,一般用 CopyOnWriteArrayList 类替代 ArrayList 。假如是用于存放一个侦听器 (listener)列表,譬喻在AWT或Swing应用措施中,可能在常见的JavaBean中, 那么这种环境很常见(相关的 CopyOnWriteArraySet 利用一个 CopyOnWriteArrayList 来实现 Set 接口)。

假如您正在利用一个普通的 ArrayList 来存放一个侦听器列表,那么只要该 列表是可变的,并且大概要被多个线程会见,您就必需要么在对其举办迭代操纵 期间,要么在迭代前举办的克隆操纵期间,锁定整个列表,这两种做法的开销都 很大。当对列表执行会引起列表产生变革的操纵时, CopyOnWriteArrayList 并 不是为列表建设一个全新的副本,它的迭代器必定可以或许返回在迭代器被建设时列 表的状态,而不会抛出 ConcurrentModificationException 。在对列表举办迭 代之前不必克隆列表可能在迭代期间锁定列表,因为迭代器所看到的列表的副本 是稳定的。换句话说, CopyOnWriteArrayList 含有对一个不行变数组的一个可 变的引用,因此,只要保存好谁人引用,您就可以得到不行变的线程安详性的好 处,并且不消锁定列表。

竣事语

#p#分页标题#e#

同步的荟萃类 Hashtable 和 Vector ,以及同步的包装器类 Collections.synchronizedMap 和 Collections.synchronizedList ,为 Map 和 List 提供了根基的有条件的线程安详的实现。然而,某些因素使得它们并不 合用于具有高度并发性的应用措施中――它们的荟萃范畴的单锁特性对付可伸缩 性来说是一个障碍,并且,许多时候还必需在一段较长的时间内锁定一个荟萃, 以防备呈现 ConcurrentModificationException s异常。 ConcurrentHashMap 和 CopyOnWriteArrayList 实现提供了更高的并发性,同时还保住了线程安详性 ,只不外在对其挪用者的理睬上打了点折扣。 ConcurrentHashMap 和 CopyOnWriteArrayList 并不是在您利用 HashMap 或 ArrayList 的任那里所都 必然有用,可是它们是设计用来优化某些特定的公用办理方案的。很多并发应用 措施将从对它们的利用中得到长处。

 

    关键字:

天才代写-代写联系方式