RFR(trivial): 8222394: HashMap.compute() throws CME on an empty Map if clear() called concurrently
stuart.marks at oracle.com
Thu May 2 00:39:21 UTC 2019
>> ...merely to serve as a discussion point about the policy for throwing
> Yes, for the time being, I want to see and welcome more ideas on this. It
> seems to me that the policy for throwing CME here is not a unified one,
> mostly based on experience and testing. Clear, compute, and computeIfAbsent
> are more special as I described.
OK. For reference, here are some of the words from the
ConcurrentModificationException specification: 
> This exception may be thrown by methods that have detected concurrent
> modification of an object when such modification is not permissible.
> For example, it is not generally permissible for one thread to modify a
> Collection while another thread is iterating over it. In general, the results
> of the iteration are undefined under these circumstances. Some Iterator
> implementations (including those of all the general purpose collection
> implementations provided by the JRE) may choose to throw this exception if
> this behavior is detected. Iterators that do this are known as fail-fast
> iterators, as they fail quickly and cleanly, rather that risking arbitrary,
> non-deterministic behavior at an undetermined time in the future.
> Note that this exception does not always indicate that an object has been
> concurrently modified by a different thread. If a single thread issues a
> sequence of method invocations that violates the contract of an object, the
> object may throw this exception. For example, if a thread modifies a
> collection directly while it is iterating over the collection with a
> fail-fast iterator, the iterator will throw this exception.
> Note that fail-fast behavior cannot be guaranteed as it is, generally
> speaking, impossible to make any hard guarantees in the presence of
> unsynchronized concurrent modification. Fail-fast operations throw
> ConcurrentModificationException on a best-effort basis. Therefore, it would
> be wrong to write a program that depended on this exception for its
> correctness: ConcurrentModificationException should be used only to detect
Similar words are repeated in several different locations around the
specification, such as in the ArrayList and HashMap class specifications.
I'm not entirely sure what your concerns are with
ConcurrentModificationException (and the "fail-fast" concurrent modification
policy), but let me discuss a few points.
1. throwing of CME is not guaranteed - "best effort"
Unlike most Java specifications, the specification around CME is fairly
indefinite. The wording is hedged -- "This exception may be thrown...." This
implies that CME might or might not be thrown, even in cases where one might
expect it to be.
It also says that CME is thrown on a "best effort" basis. This doesn't mean that
the library makes the maximum possible effort to throw CME in every possible
situation. Maybe "best effort" is somewhat misleading. Perhaps "reasonable"
effort is more descriptive.
For example, ArrayList keeps a modCount field and increments and checks it
occasionally. No synchronization is done. If the ArrayList is modified by
another thread, the update to modCount might not be visible to all threads,
which might result in data corruption instead of a CME.
One way to "fix" this would be to make access to modCount synchronized (or to
make it volatile, or to make it an AtomicInteger or something) to improve the
reliability of detecting concurrent modifications from other threads. This would
add complexity to the code and also slow down common operations. Making this
extra effort doesn't seem to be worthwhile.
2. throwing CME sometimes done even when not absolutely necessary
Another point is that the detection and throwing of a CME is an approximation of
when concurrent modification would have any impact. In some cases CME will be
thrown even when one wouldn't think it strictly necessary.
For example, consider a loop in the middle of iterating an ArrayList.
ArrayList's iterator simply keeps an index to represent its current position. If
an element is added to or removed from the front of the list, this would result
in the iteration skipping or repeating elements. Thus, throwing CME seems
warranted in this case.
Now consider an iteration in the middle of an ArrayList, and an addition or
removal is made to the *end* of the list. This doesn't affect the current
iteration; yet CME is thrown anyway. Why?
To avoid throwing CME in this case (but to throw it in the previous case) the
ArrayList and its Iterators would have to keep track of more information about
what changes were made and would have to do more checking at each iteration
step. This could increase code complexity considerably. Again, this seems like
it isn't worthwhile. Keeping a simple counter (modCount) and checking it at each
iteration step is quite cheap, although it arguably does throw CME unnecessarily
in this case.
Some of the cases you're talking about seem to fall into this category. A CME is
thrown from compute() if an operation is merely attempted, even if the actual
operation performed would have no ill effect.
3. state-dependent behavior
I discussed this in a previous message. My personal design style is to try to
avoid this, although the library isn't wholly consistent in this regard. The
discussion regarding CME and the compute() and similar methods is also related
to state-dependent behavior.
4. edge cases
There are a number of edge cases that aren't treated wholly consistently across
the libraries. Again with ArrayList, consider the following:
List<Integer> list = new ArrayList<>(List.of(0, 1, 2))
[0, 1, 2]
var it = list.iterator()
Arguably, hasNext() should throw CME. If this were in a for-loop, the concurrent
modification would be missed and the loop would terminate normally. Many
iterators check for concurrent modification in their next() method but not in
hasNext(). Perhaps this should be fixed, but it might break code that is
apparently behaving well today, so we've left it unchanged.
There is also the case of JDK-8114832, where a failed attempt at modification
will still cause a CME. This is more state-dependent behavior: should modCount
be incremented when a modification is *attempted* or when modification
*actually* occurs? (Martin says, "attempted murder is still a crime!")
This bug is still open, though Martin and I agree that no change should be made
here. It's questionable to me whether we want to go through the old collections
and update things to be more consistent. The effort is high, the benefit is
fairly low, in my estimation, and there is a risk of breaking existing code. So
we live with the inconsistencies.
I'm kind of rambling here. Is this the kind of discussion you're interested in?
Do you have any specific questions?
More information about the core-libs-dev