Search code examples
javajakarta-eeconcurrencyconcurrenthashmap

Multiple threads checking map size and conccurency


I have a method that's supposed to feed a map from a queue and it only does that if the map size is not exceeding a certain number. This prompted concurrency problem as the size I get from every thread is non coherent globaly. I replicated the problem by this code

import java.sql.Timestamp;
import java.util.Date;
import java.util.concurrent.ConcurrentHashMap;

public class ConcurrenthashMapTest {
private ConcurrentHashMap<Integer, Integer> map = new ConcurrentHashMap<Integer, Integer>();
private ThreadUx[] tArray = new ThreadUx[999];

public void parallelMapFilling() {
for ( int i = 0; i < 999; i++ ) {
    tArray[i] = new ThreadUx( i );
}
for ( int i = 0; i < 999; i++ ) {
    tArray[i].start();
}
}

public class ThreadUx extends Thread {
private int seq = 0;

public ThreadUx( int i ) {
    seq = i;
}

@Override
public void run() {
    while ( map.size() < 2 ) {
    map.put( seq, seq );
    System.out.println( Thread.currentThread().getName() + " || The size is: " + map.size() + " || " + new Timestamp( new Date().getTime() ) );
    }
}
}

public static void main( String[] args ) {
new ConcurrenthashMapTest().parallelMapFilling();
}
}

Normally I should have only one line of output and the size not exceeding 1, but I do have some stuff like this

Thread-1 || The size is: 2 || 2016-06-07 18:32:55.157
Thread-0 || The size is: 2 || 2016-06-07 18:32:55.157

I tried marking the whole run method as synchronized but that didn't work, only when I did this

@Override
    public void run() {
        synchronized ( map ) {
        if ( map.size() < 1 ) {
            map.put( seq, seq );
            System.out.println( Thread.currentThread().getName() + " || The size is: " + map.size() + " || " + new Timestamp( new Date().getTime() ) );
        }
        }
    }

It worked, why is only the synch block working and the synch method? Also I don't want to use something as old as a synch block as I am working on a Java EE app, is there a Spring or Java EE task executor or annotation that can help?


Solution

  • From Java Concurrency in Practice:

    The semantics of methods of ConcurrentHashMap that operate on the entire Map, such as size and isEmpty, have been slightly weakened to reflect the concurrent nature of the collection. Since the result of size could be out of date by the time it is computed, it is really only an estimate, so size is allowed to return an approximation instead of an exact count. While at first this may seem disturbing, in reality methods like size and isEmpty are far less useful in concurrent environments because these quantities are moving targets. So the requirements for these operations were weakened to enable performance optimizations for the most important operations, primarily get, put, containsKey, and remove.

    The one feature offered by the synchronized Map implementations but not by ConcurrentHashMap is the ability to lock the map for exclusive access. With Hashtable and synchronizedMap, acquiring the Map lock prevents any other thread from accessing it. This might be necessary in unusual cases such as adding several mappings atomically, or iterating the Map several times and needing to see the same elements in the same order. On the whole, though, this is a reasonable tradeoff: concurrent collections should be expected to change their contents continuously.

    Solutions:

    1. Refactor design and do not use size method with concurrent access.

    2. To use methods as size and isEmpty you can use synchronized collection Collections.synchronizedMap. Synchronized collections achieve their thread safety by serializing all access to the collection's state. The cost of this approach is poor concurrency; when multiple threads contend for the collection-wide lock, throughput suffers. Also you will need to synchronize the block where it checks-and-puts with map instance, because it's a compound action.

    Third. Use third-party implementation or write your own.

    public class BoundConcurrentHashMap <K,V> {
        private final Map<K, V> m;
        private final Semaphore semaphore;
        public BoundConcurrentHashMap(int size) {
            m = new ConcurrentHashMap<K, V>();
            semaphore = new Semaphore(size);
        }
    
        public V get(V key) {
            return m.get(key);
        }
    
        public boolean put(K key, V value) {
            boolean hasSpace = semaphore.tryAcquire();
            if(hasSpace) {
                m.put(key, value);
            }
            return hasSpace;
        }
    
        public void remove(Object key) {
            m.remove(key);
            semaphore.release();
        }
    
        // approximation, do not trust this method
        public int size(){
            return m.size();
        }
    }
    

    Class BoundConcurrentHashMap is as effective as ConcurrentHashMap and almost thread-safe. Because removing an element and releasing semaphore in remove method are not simultaneous as it should be. But in this case it is tolerable. size method still returns approximated value, but put method will not allow to exceed map size.