Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Behavior of entrySet().removeIf in ConcurrentHashMap

Tags:

I would like to use ConcurrentHashMap to let one thread delete some items from the map periodically and other threads to put and get items from the map at the same time.

I'm using map.entrySet().removeIf(lambda) in the removing thread. I'm wondering what assumptions I can make about its behavior. I can see that removeIf method uses iterator to go through elements in the map, check the given condition and then remove them if needed using iterator.remove().

Documentation gives some info about ConcurrentHashMap iterators behavior:

Similarly, Iterators, Spliterators and Enumerations return elements reflecting the state of the hash table at some point at or since the creation of the iterator/enumeration. hey do not throw ConcurrentModificationException. However, iterators are designed to be used by only one thread at a time.

As the whole removeIf call happens in one thread I can be sure that the iterator is not used by more than one thread at the time. Still I'm wondering if the course of events described below is possible:

  1. Map contains mapping: 'A'->0
  2. Deleting Thread starts executing map.entrySet().removeIf(entry->entry.getValue()==0)
  3. Deleting Thread calls .iteratator() inside removeIf call and gets the iterator reflecting the current state of the collection
  4. Another thread executes map.put('A', 1)
  5. Deleting thread still sees 'A'->0 mapping (iterator reflects the old state) and because 0==0 is true it decides to remove A key from the map.
  6. The map now contains 'A'->1 but deleting thread saw the old value of 0 and the 'A' ->1 entry is removed even though it shouldn't be. The map is empty.

I can imagine that the behavior may be prevented by the implementation in many ways. For example: maybe iterators are not reflecting put/remove operations but are always reflecting value updates or maybe the remove method of the iterator checks if the whole mapping (both key and value) is still present in the map before calling remove on the key. I couldn't find info about any of those things happening and I'm wondering if there's something which makes that use case safe.

like image 616
Paweł Chorążyk Avatar asked Apr 26 '15 09:04

Paweł Chorążyk


People also ask

How do I remove an item from a map in Java?

HashMap remove() Method in Java remove() is an inbuilt method of HashMap class and is used to remove the mapping of any particular key from the map. It basically removes the values for any particular key in the Map. Parameters: The method takes one parameter key whose mapping is to be removed from the Map.

Is ConcurrentHashMap keyset thread safe?

The ConcurrentHashMap operations are thread-safe. ConcurrentHashMap doesn't allow null for keys and values.

How to add and remove elements from a ConcurrentHashMap in Java?

ConcurrentHashMap in Java 1 Adding Elements To insert mappings into a ConcurrentHashMap, we can use put () or putAll () methods. The below example code explains these two methods. ... 2 Removing Elements To remove a mapping, we can use remove (Object key) method of class ConcurrentHashmap. ... 3 Accessing the Elements

What is concureenthashmap in Java?

ConcurrentHashMap ConcurrentHashMap class is introduced in JDK 1.5, which implements ConcurrentMap as well as Serializable interface also. ConcureentHashMap is enhancement of HashMap as we know that while dealing with Threads in our application HashMap is not a good choice because performance wise HashMap is not upto the mark.

What are the key points of ConcurrentHashMap?

Key points of ConcurrentHashMap: 1 The underlined data structure for ConcurrentHashMap is Hashtable. 2 ConcurrentHashMap class is thread-safe i.e. ... 3 At a time any number of threads are applicable for a read operation without locking the ConcurrentHashMap object which is not there in HashMap. More items...

What is the behavior of ConcurrentHashMap iterators?

Documentation gives some info about ConcurrentHashMap iterators behavior: Similarly, Iterators, Spliterators and Enumerations return elements reflecting the state of the hash table at some point at or since the creation of the iterator/enumeration. hey do not throw ConcurrentModificationException.


2 Answers

I also managed to reproduce such case on my machine. I think, the problem is that EntrySetView (which is returned by ConcurrentHashMap.entrySet()) inherits its removeIf implementation from Collection, and it looks like:

    default boolean removeIf(Predicate<? super E> filter) {         Objects.requireNonNull(filter);         boolean removed = false;         final Iterator<E> each = iterator();         while (each.hasNext()) {             // `test` returns `true` for some entry             if (filter.test(each.next())) {                 // entry has been just changed, `test` would return `false` now                each.remove(); // ...but we still remove                removed = true;             }         }         return removed;     } 

In my humble opinion, this cannot be considered as a correct implementation for ConcurrentHashMap.

like image 72
Vlad Avatar answered Sep 16 '22 17:09

Vlad


After discussion with user Zielu in comments below Zielu's answer I have gone deeper into the ConcurrentHashMap code and found out that:

  • ConcurrentHashMap implementation provides remove(key, value) method which calls replaceNode(key, null, value)
  • replaceNode checks if both key and value are still present in the map before removing so using it should be fine. Documentation says that it

Replaces node value with v, conditional upon match of cv if * non-null.

  • In the case mentioned in the question ConcurrentHashMap's .entrySet() is called which returns EntrySetView class. Then removeIf method calls .iterator() which returns EntryIterator.
  • EntryIterator extends BaseIterator and inherits remove implementation that calls map.replaceNode(p.key, null, null) which disables conditional removal and just always removes the key.

The negative course of events could be still prevented if iterators always iterated over 'current' values and never returned old ones if some value is modified. I still don't know if that happens or not, but the test case mentioned below seems to verify the whole thing.

I think that have created a test case which shows that the behavior described in my question can really happen. Please correct me if I there are any mistakes in the code.

The code starts two threads. One of them (DELETING_THREAD) removes all entries mapped to 'false' boolean value. Another one (ADDING_THREAD) randomly puts (1, true) or (1,false) values into the map. If it puts true in the value it expects that the entry will still be there when checked and throws an exception if it is not. It throws an exception quickly when I run it locally.

package test;  import java.util.Random; import java.util.concurrent.ConcurrentHashMap;  public class MainClass {      private static final Random RANDOM = new Random();      private static final ConcurrentHashMap<Integer, Boolean> MAP = new ConcurrentHashMap<Integer, Boolean>();      private static final Integer KEY = 1;      private static final Thread DELETING_THREAD = new Thread() {          @Override         public void run() {             while (true) {                 MAP.entrySet().removeIf(entry -> entry.getValue() == false);             }         }      };      private static final Thread ADDING_THREAD = new Thread() {          @Override         public void run() {             while (true) {                 boolean val = RANDOM.nextBoolean();                  MAP.put(KEY, val);                 if (val == true && !MAP.containsKey(KEY)) {                     throw new RuntimeException("TRUE value was removed");                 }              }         }      };      public static void main(String[] args) throws InterruptedException {         DELETING_THREAD.setDaemon(true);         ADDING_THREAD.start();         DELETING_THREAD.start();         ADDING_THREAD.join();     } } 
like image 44
Paweł Chorążyk Avatar answered Sep 16 '22 17:09

Paweł Chorążyk