Summary:
ConcurrentHashMap
, part of Doug Lea's util.concurrent
package, offers a higher degree of concurrency than Hashtable
or synchronizedMap
. In addition, it manages to avoid locking completely for most successful get()
operations, which results in excellent throughput in concurrent applications. This month, Brian Goetz dives into the code of ConcurrentHashMap
and looks at how this impressive feat is accomplished without compromising thread safety.
In July's installment of Java theory and practice
("Concurrent collections classes
"),
we reviewed scalability bottlenecks and discussed how to achieve higher
concurrency and throughput in shared data structures. Sometimes, the
best way to learn is to examine the work of the experts, so this month
we're going to look at the implementation of ConcurrentHashMap
from Doug Lea's util.concurrent
package. A version of ConcurrentHashMap
optimized for the new Java Memory Model (JMM), which is being specified by JSR 133, will be included in the java.util.concurrent
package in JDK 1.5; the version in util.concurrent
has been audited for thread-safety under both the old and new memory models.
ConcurrentHashMap
uses several tricks to achieve a high
level of concurrency and avoid locking, including using multiple write
locks for different hash buckets and exploiting the uncertainties of
the JMM to minimize the time that locks are held -- or avoid acquiring
locks at all. It is optimized for the most common usage, which is
retrieving a value likely to already exist in the map. In fact, most
successful get()
operations will run without any locking
at all. (Warning: don't try this at home! Trying to outsmart the JMM is
much harder than it looks and is not to be undertaken lightly. The util.concurrent
classes were written by concurrency experts and have been extensively peer-reviewed for JMM safety.)
Recall that the major scalability impediment of Hashtable
(or alternatively, Collections.synchronizedMap
)
is that it uses a single map-wide lock, which must be held for the
entirety of an insertion, removal, or retrieval operation, and
sometimes even for the entirety of an iterator traversal. This prevents
other threads from accessing the Map
at all while the lock is held, even if idle processors are available, significantly limiting concurrency.
ConcurrentHashMap
dispenses with the single map-wide lock,
instead using a collection of 32 locks, each of which guards a subset
of the hash buckets. Locks are primarily used by mutative (put()
and remove()
)
operations. Having 32 separate locks means that a maximum of 32 threads
can be modifying the map at once. This doesn't necessarily mean that if
there are fewer than 32 threads writing to the map currently, that
another write operation will not block -- 32 is the theoretical
concurrency limit for writers, but may
not always be achieved in practice. Still, 32 is a lot better than one
and should be more than adequate for most applications running on the
current generation of computer systems.
Having 32 separate locks, each guarding a subset of the hash
buckets, means that operations that require exclusive access to the map
must acquire all 32 locks. Some map-wide operations, such as size()
and isEmpty()
may be able to get away without locking the entire map at once (by
suitably qualifying the semantics of these operations), but some
operations, such as map rehashing (expanding the number of hash buckets
and redistributing elements as the map grows) must guarantee exclusive
access. The Java language does not provide a simple way to acquire a
variable-sized set of locks; in the infrequent cases where this must be
done, it is done by recursion.
Before we jump into the implementation of put()
, get()
, and remove()
,
let's briefly review the JMM, which governs how actions on memory
(reads and writes) by one thread affect actions on memory by other
threads. Because of the performance benefits of using processor
registers and per-processor caches to speed up memory access, the Java
Language Specification (JLS) permits some memory operations not to be
immediately visible to all other threads. There are two language
mechanisms for guaranteeing consistency of memory operations across
threads -- synchronized
and volatile
.
According to the JLS, "In the absence of explicit synchronization, an implementation is free to update the main memory in an order that may be surprising." This means that without synchronization, writes that occur in one order in a given thread may appear to occur in a different order to a different thread, and that updates to memory variables may take an unspecified time to propagate from one thread to another.
While the most common reason for using synchronization is to guarantee atomic access to critical sections of code, synchronization actually provides three separate functions -- atomicity, visibility, and ordering. Atomicity is straightforward enough -- synchronization enforces a reentrant mutex, preventing more than one thread from executing a block of code protected by a given monitor at the same time. Unfortunately, most texts focus on the atomicity aspects of synchronization to the exclusion of the other aspects. But synchronization also plays a significant role in the JMM, causing the JVM to execute memory barriers when acquiring and releasing monitors.
When a thread acquires a monitor, it executes a read barrier -- invalidating any variables cached in thread-local memory (such as an on-processor cache or processor registers), which will cause the processor to re-read any variables used in the synchronized block from main memory. Similarly, upon monitor release, the thread executes a write barrier -- flushing any variables that have been modified back to main memory. The combination of mutual exclusion and memory barriers means that as long as programs follow the correct synchronization rules (that is, synchronize whenever writing a variable that may next be read by another thread or when reading a variable that may have been last written by another thread), each thread will see the correct value of any shared variables it uses.
Some very strange things can happen if you fail to synchronize when
accessing shared variables. Some changes may be reflected across
threads instantly, while others may take some time (due to the nature
of associative caches). As a result, without synchronization you cannot
be sure that you have a consistent view of memory (related variables
may not be consistent with each other) or a current view of memory
(some values may be stale). The common -- and recommended -- way to
avoid these hazards is of course to synchronize properly. However, in
some cases, such as in very widely used library classes like ConcurrentHashMap
,
it may be worth applying some extra expertise and effort in development
(which may well be many times as much effort) to achieve higher
performance.
ConcurrentHashMap implementation
As suggested earlier, the data structure used by ConcurrentHashMap
is similar in implementation to that of Hashtable
or HashMap
, with a resizable array of hash buckets, each consisting of a chain of Map.Entry
elements, as shown in Listing 1. Instead of a single collection lock, ConcurrentHashMap
uses a fixed pool of locks that form a partition over the collection of buckets.
Listing 1. Map.Entry elements
used by ConcurrentHashMap
protected static class Entry implements Map.Entry { protected final Object key; protected volatile Object value; protected final int hash; protected final Entry next; ... } |
Traversing data structures without locking
Unlike Hashtable
or a typical lock-pool Map
implementation, ConcurrentHashMap.get()
operations do not necessarily entail acquiring the lock associated with
the relevant bucket. In the absence of locking, the implementation must
be prepared to deal with stale or inconsistent values of any variables
it uses, such as the list head pointer and the fields of the Map.Entry
elements (including the link pointers that comprise the linked list of entries for each hash bucket).
Most concurrent classes use synchronization to ensure exclusive access
to -- and a consistent view of -- a data structure. Instead of assuming
exclusivity and consistency, the linked list used by ConcurrentHashMap
is designed carefully so that the implementation can detect
that its view of the list is inconsistent or stale. If it detects that
its view is inconsistent or stale, or simply does not find the entry it
is looking for, it then synchronizes on the appropriate bucket lock and
searches the chain again. This optimizes lookup in the common case --
where most
retrievals are successful and retrievals outnumber insertions and
removals.
One significant source of inconsistency is avoided by making the Entry
elements nearly immutable -- all fields are final, except for the value
field, which is volatile. This means that elements cannot be added to
or removed from the middle or end of the hash chain -- elements can
only be added at the beginning, and removal involves cloning all or
part of the chain and updating the list head pointer. So once you have
a reference into a hash chain, while you may not know whether you have
a reference to the head of the list, you do know that the rest of the
list will not change its structure. Also, since the value field is
volatile, you will be able to see updates to the value field
immediately, greatly simplifying the process of writing a Map
implementation that can deal with a potentially stale view of memory.
While the new JMM provides initialization safety for final variables,
the old JMM does not, which means that it is possible for another
thread to see the default value for a final field, rather than the
value placed there by the object's constructor. The implementation must
be prepared to detect this as well, which it does by ensuring that the
default value for each field of Entry
is not a valid value. The list is constructed such that if any of the Entry
fields appear to have their default value (zero or null
), the search will fail, prompting the get()
implementation to synchronize and traverse the chain again.
Retrieval operations proceed by first finding the head pointer for the desired bucket (which is done without locking, so it could be stale), and traversing the bucket chain without acquiring the lock for that bucket. If it doesn't find the value it is looking for, it synchronizes and tries to find the entry again, as shown in Listing 2:
Listing 2. ConcurrentHashMap.get() implementation
public Object get(Object key) { int hash = hash(key); // throws null pointer exception if key is null // Try first without locking... Entry[] tab = table; int index = hash & (tab.length - 1); Entry first = tab[index]; Entry e; for (e = first; e != null; e = e.next) { if (e.hash == hash && eq(key, e.key)) { Object value = e.value; // null values means that the element has been removed if (value != null) return value; else break; } } // Recheck under synch if key apparently not there or interference Segment seg = segments[hash & SEGMENT_MASK]; synchronized(seg) { tab = table; index = hash & (tab.length - 1); Entry newFirst = tab[index]; if (e != null || first != newFirst) { for (e = newFirst; e != null; e = e.next) { if (e.hash == hash && eq(key, e.key)) return e.value; } } return null; } } |
Because a thread could see stale values for the link pointers in a
hash chain, simply removing an element from the chain would not be
sufficient to ensure that other threads will not continue to see the
removed value when performing a lookup. Instead, as Listing 3
illustrates, removal is a two-step process -- first the appropriate Entry
object is found and its value field is set to null
,
and then the portion of the chain from the head to the removed element
is cloned and joined to the remainder of the chain following the
removed element. Since the value field is volatile, if another thread
is traversing the stale chain looking for the removed element, it will
see the null value field immediately, and know to retry the retrieval
with synchronization. Eventually, the initial part of the hash chain
ending in the removed element will be garbage collected.
Listing 3. ConcurrentHashMap.remove() implementation
protected Object remove(Object key, Object value) { /* Find the entry, then 1. Set value field to null, to force get() to retry 2. Rebuild the list without this entry. All entries following removed node can stay in list, but all preceding ones need to be cloned. Traversals rely on this strategy to ensure that elements will not be repeated during iteration. */ int hash = hash(key); Segment seg = segments[hash & SEGMENT_MASK]; synchronized(seg) { Entry[] tab = table; int index = hash & (tab.length-1); Entry first = tab[index]; Entry e = first; for (;;) { if (e == null) return null; if (e.hash == hash && eq(key, e.key)) break; e = e.next; } Object oldValue = e.value; if (value != null && !value.equals(oldValue)) return null; e.value = null; Entry head = e.next; for (Entry p = first; p != e; p = p.next) head = new Entry(p.hash, p.key, p.value, head); tab[index] = head; seg.count--; return oldValue; } } |
Figure 1 illustrates a hash chain before an element is removed:
Figure 2 illustrates the chain with element 3 removed:
Figure 2. Removal of an element
Insertion and update operations
The implementation of put()
is straightforward. Like remove()
, put()
holds the bucket lock for the duration of its execution, but because get()
doesn't always need to acquire the lock, this doesn't necessarily block
readers from executing (nor does it block other writers from accessing
other buckets). First, put()
searches the appropriate hash chain for the desired key. If it is found, then the value
field (which is volatile) is simply updated. If it is not found, a new Entry
object is created to describe the new mapping and is inserted at the head of the list for that bucket.
The semantics of iterators returned by ConcurrentHashMap
differ from those of java.util
collections; rather than being fail-fast
(throwing an exception if the underlying collection is modified while an iterator is being used), they are instead weakly consistent
. When a user calls keySet().iterator()
to retrieve an iterator for the set of hash keys, the implementation
briefly synchronizes to make sure the head pointers for each chain are
current. The next()
and hasNext()
operations
are defined in the obvious way, traversing each hash chain and then
moving on to the next chain until all the chains have been traversed.
Weakly consistent iterators may or may not reflect insertions made
during iteration, but they will definitely reflect updates or removals
for keys that have not yet been reached by the iterator, and will not
return any value more than once. The iterators returned by ConcurrentHashMap
will not throw ConcurrentModificationException
.
As the number of elements in the map grows, the hash chains will
grow longer and retrieval time will increase. At some point, it makes
sense to increase the number of buckets and rehash the values. In
classes like Hashtable
, this is easy because it is possible to hold an exclusive lock on the entire map. In ConcurrentHashMap
,
each time an entry is inserted, if the length of that chain exceeds
some threshold, that chain is marked as needing to be resized. When
enough chains vote
that resizing is needed, ConcurrentHashMap
will use
recursion to acquire the lock on each bucket and rehash the elements
from each bucket into a new, larger hash table. Most of the time, this
will occur automatically and transparently to the caller.
To say that successful get()
operations proceed without locking is a bit of an overstatement, as the value
field of Entry
is volatile, and this is used to detect updates and removals. At the
machine level, volatile and synchronization often end up getting
translated into the same cache coherency primitives, so there
effectively is some
locking going on here, albeit at a finer
granularity and without the scheduling or JVM overhead of acquiring and
releasing monitors. But, semantics aside, the concurrency
achieved by ConcurrentHashMap
in many common situations, where retrievals outnumber insertions and removals, is quite impressive.
ConcurrentHashMap
is both a very useful class for many
concurrent applications and a fine example of a class that understands
and exploits the subtle details of the JMM to achieve higher
performance. ConcurrentHashMap
is an impressive feat of
coding, one that requires a deep understanding of concurrency and the
JMM. Use it, learn from it, enjoy it -- but unless you're an expert on
Java concurrency, you probably shouldn't try this on your own.
发表评论
-
Java theory and practice: To mutate or not to mutate?
2009-08-15 00:32 711Summary: Immutable objects h ... -
Java theory and practice: Concurrent collections classes
2009-08-15 00:23 967Summary: In addition to ma ... -
Java theory and practice: Hashing it out --hashCode
2009-08-14 22:08 746Summary: Every Java object h ...
相关推荐
在Java编程语言中,集合框架是开发者日常工作中不可或缺的一部分,HashMap作为其中的重要成员,它的实现原理对于理解Java性能优化和数据结构有深远的意义。HashMap是一个基于哈希表的数据结构,它实现了Map接口,...
Java集合专题总结:HashMap和HashTable源码学习和面试总结 本文总结了Java集合专题中的HashMap和HashTable,涵盖了它们的源码学习和面试总结。HashMap是一种基于哈希表的集合类,它的存储结构是一个数组,每个元素...
`HashMap` 和 `HashTable` 都是 Java 集合框架中非常重要的数据结构,它们都实现了 `Map` 接口,用于存储键值对。尽管它们在功能上有很多相似之处,但在实现细节和性能特性上存在显著差异。 #### 二、主要区别 1. ...
在Java编程语言中,HashMap是一种常用的集合类,用于存储键值对数据。它提供O(1)的平均时间复杂度来插入、删除和查找元素,这得益于其内部的数据结构设计。"学习笔记:三数组实现的HashMap"这篇博客文章可能讨论了一...
《Java大学简明教程:实例程序设计》是一本面向初学者和在校大学生的Java编程教材。这本书通过丰富的实例,深入浅出地介绍了Java语言的基础知识和应用技巧,旨在帮助读者快速掌握Java编程技能。 首先,从"Java简明...
在Java编程语言中,HashMap是一种常用的集合类,用于存储键值对数据。它实现了Map接口,提供了快速的插入、删除和查找操作。本项目“Generic-HashMap-Java”旨在提供一个泛型化的HashMap实现,以增强类型安全性并...
JavaScript中的HashMap并不是内置的数据结构,但在许多开发场景中,我们需要实现类似Java中HashMap的功能,用于存储键值对数据。在JavaScript中,我们通常使用对象(Object)来模拟HashMap的行为,因为对象的属性名...
在Java编程中,HashMap是一个非常重要的数据结构,它实现了Map接口,提供了键值对的存储功能,具有快速存取和高效查找的特点。HashMap基于哈希表(也称为散列表)原理,通过键对象的哈希码来定位元素,进而实现O(1)...
·基于JDK 11,将Java8、Java9、Java10、Java11新特性一网打尽 ·课程中,Eclipse和IDEA这两种企业一线开发环境都使用到了 3.技术讲解更深入、更全面: ·课程共30天,715个知识视频小节,涉及主流Java使用的...
3. **并发集合与并发容器**:涵盖了Java并发集合框架,包括线程安全的ArrayList、LinkedList、HashMap等,并介绍了ConcurrentHashMap、CopyOnWriteArrayList等高效率的并发容器。 4. **并发工具**:讨论了Executor...
Java HashMap 类详解 本资源详细介绍了 Java 中的 HashMap 类,包括其实现机制、Hash 存储机制、集合存储机制等方面的知识点。 1. HashMap 和 HashSet 的关系 HashMap 和 HashSet 是 Java Collection Framework ...
Java集合详解4:HashMap和HashTable Java集合详解5:深入理解LinkedHashMap和LRU缓存 Java集合详解6:TreeMap和红黑树 Java集合详解7:HashSet,TreeSet与LinkedHashSet Java集合详解8:Java集合类细节精讲 JavaWeb
HashMap 是 Java 中最常用的集合类之一,它是基于哈希表数据结构实现的,提供快速的存取操作。在深入理解 HashMap 的实现原理之前,我们先要明白哈希表的基本概念。哈希表是一种通过哈希函数将键(Key)映射到数组...
Java SE程序 HashMap类Java SE程序 HashMap类Java SE程序 HashMap类Java SE程序 HashMap类Java SE程序 HashMap类Java SE程序 HashMap类Java SE程序 HashMap类Java SE程序 HashMap类Java SE程序 HashMap类Java SE程序...
HashMap是Java编程语言中的一种重要数据结构,它是`java.util.HashMap`类的实例,实现了`Map`接口。HashMap主要用于存储键值对,其中键是唯一的,且键和值之间通过哈希函数建立关联,使得在理想情况下,查询、插入和...
java代码-使用java解决手写hashMap的源代码 ——学习参考资料:仅用于个人学习使用!
在这个项目"CrudHashMapJava"中,我们将探讨如何利用Java中的HashMap类来实现这些基本功能。HashMap是Java集合框架的一部分,它提供了键值对存储的高效实现。 **HashMap概述** HashMap是一种散列映射容器,它存储...
4. **集合框架**: Java集合框架包括List、Set、Queue、Map等接口及其实现,如ArrayList、HashSet、LinkedList、HashMap等。学习如何有效地使用这些数据结构对于优化代码性能至关重要。 5. **IO流**: Java的输入/...
Java HashMap原理分析 Java HashMap是一种基于哈希表的数据结构,它的存储原理是通过将Key-Value对存储在一个数组中,每个数组元素是一个链表,链表中的每个元素是一个Entry对象,Entry对象包含了Key、Value和指向...
在Java编程语言中,`HashMap`是`java.util`包中的一个核心类,它属于集合框架的一部分,主要用于存储键值对的数据结构。`HashMap`基于哈希表(散列表)实现,提供了快速的插入、删除和查找操作,平均时间复杂度为O(1...