views:

7374

answers:

6

Is the following code set up to correctly synchronize the calls on synchronizedMap?

public class MyClass {
  private static Map<String, List<String>> synchronizedMap = Collections.synchronizedMap(new HashMap<String, List<String>>());

  public void doWork(String key) {
    List<String> values = null;
    while ((values = synchronizedMap.remove(key)) != null) {
      //do something with values
    }
  }

  public static void addToMap(String key, String value) {
    synchronized (synchronizedMap) {
      if (synchronizedMap.containsKey(key)) {
        synchronizedMap.get(key).add(value);
      }
      else {
        List<String> valuesList = new ArrayList<String>();
        valuesList.add(value);
        synchronizedMap.put(key, valuesList);
      }
    }
  }
}

From my understanding, I need the synchronized block in addToMap() to prevent another thread from calling remove() or containsKey() before I get through the call to put() but I do not need a synchronized block in doWork() because another thread cannot enter the synchronized block in addToMap() before remove() returns because I created the Map originally with Collections.synchronizedMap(). Is that correct? Is there a better way to do this?

+3  A: 

If you are using JDK 6 then you might want to check out ConcurrentHashMap

Note the putIfAbsent method in that class.

TofuBeer
It is actually JDK 1.5
Bogdan
+2  A: 

That looks correct to me. If I were to change anything, I would stop using the Collections.synchronizedMap() and synchronize everything the same way, just to make it clearer.

Also, I'd replace

  if (synchronizedMap.containsKey(key)) {
    synchronizedMap.get(key).add(value);
  }
  else {
    List<String> valuesList = new ArrayList<String>();
    valuesList.add(value);
    synchronizedMap.put(key, valuesList);
  }

with

List<String> valuesList = synchronziedMap.get(key);
if (valuesList == null)
{
  valuesList = new ArrayList<String>();
  synchronziedMap.put(key, valuesList);
}
valuesList.add(value);
Paul Tomblin
+1  A: 

Check out Google Collections' Multimap, e.g. page 28 of this presentation.

If you can't use that library for some reason, consider using ConcurrentHashMap instead of SynchronizedHashMap; it has a nifty putIfAbsent(K,V) method with which you can atomically add the element list if it's not already there. Also, consider using CopyOnWriteArrayList for the map values if your usage patterns warrant doing so.

Barend
+5  A: 

Collections.synchronizedMap() guarantees that each atomic operation you want to run on the map will be synchronized.

Running two (or more) operations on the map however, must be synchronized in a block. So yes - you are synchronizing correctly.

Yuval A
I think it would be good to mention that this works because the javadocs explicitly state that synchronizedMap synchronizes on the map itself, and not some internal lock. If that were the case synchronized(synchronizedMap) would not be correct.
extraneon
+4  A: 

There is the potential for a subtle bug in your code.

[UPDATE: Since he's using map.remove() this description isn't totally valid. I missed that fact the first time thru. :( Thanks to the question's author for pointing that out. I'm leaving the rest as is, but changed the lead statement to say there is potentially a bug.]

In doWork() you get the List value from the Map in a thread-safe way. Afterward, however, you are accessing that list in an unsafe matter. For instance, one thread may be using the list in doWork() while another thread invokes synchronizedMap.get(key).add(value) in addToMap(). Those two access are not synchronized. The rule of thumb is that a collection's thread-safe guarantees don't extend to the keys or values they store.

You could fix this by inserting a synchronized list into the map like

List<String> valuesList = new ArrayList<String>();
valuesList.add(value);
synchronizedMap.put(key, Collections.synchronizedList(valuesList)); // sync'd list

Alternatively you could synchronize on the map while you access the list in doWork():

  public void doWork(String key) {
    List<String> values = null;
    while ((values = synchronizedMap.remove(key)) != null) {
      synchronized (synchronizedMap) {
          //do something with values
      }
    }
  }

The last option will limit concurrency a bit, but is somewhat clearer IMO.

Also, a quick note about ConcurrentHashMap. This is a really useful class, but is not always an appropriate replacement for synchronized HashMaps. Quoting from its Javadocs,

This class is fully interoperable with Hashtable in programs that rely on its thread safety but not on its synchronization details.

In other words, putIfAbsent() is great for atomic inserts but does not guarantee other parts of the map won't change during that call; it guarantees only atomicity. In your sample program, you are relying on the synchronization details of (a synchronized) HashMap for things other than put()s.

Last thing. :) This great quote from Java Concurrency in Practice always helps me in designing an debugging multi-threaded programs.

For each mutable state variable that may be accessed by more than one thread, all accesses to that variable must be performed with the same lock held.

JLR
I see your point about the bug if I were accessing the list with synchronizedMap.get(). Since I'm using remove(), shouldn't the next add with that key create a new ArrayList and not interfere with the one that I am using in doWork?
Ryan Ahearn
Correct! I totally breezed past your remove.
JLR
A: 

Hello,

I'm facing a simular problem I think. Perhaps you can help me out.

At web application startup I need to load a Map which is thereafter used by multiple incoming threads. That is, requests comes in and the Map is used to find out whether it contains a particular key and if so the value (the object) is associated to the object I'm

Now, at times the content of the Map changes. I don't want to restart my application to reload the new situation. Instead I want to do this dynamically.

However, at the time the Map is re-loading (removing all items and replacing them with the new ones), concurrent read requests on that Map still arrive.

What should I do to prevent all read threads from accessing that Map while it's being reloaded ? How can I do this in the most performant way, because I only need this when the Map is reloading which will only occur sporadically (each every x weeks) ? If the above is not an option (blocking) how can I make sure that while reloading my read request won't suffer from unexpected exceptions (because a key is no longer there, or a value is no longer present or being reloaded) ?

Thanks, E

EdwinDhondt
Edwin, You'd need a ReadWriteLock for your situation. See: http://java.sun.com/javase/6/docs/api/java/util/concurrent/locks/ReadWriteLock.html You should ask this in its own question to get more detailed answers and let other people search for this situation better.
Ryan Ahearn
Thanks RyanI've posted a seperate question.Can you provide me with an actual example of ReadWriteLock ?What will happen if my writer thread is writing and reads keep coming in ? Will the reads block and resume after the lock is released by the writer ?
EdwinDhondt