I have the following class. I use ConcurrentHashMap. I have many threads writing to the maps and a Timer that saves the data in the map every 5 minutes. I manage to achieve thread safety by using putIfAbsent() when I write entries in the map. However, when I read from it and then remove all entries by clear() method, I want no other thread writes to map while I’m in the process of reading the map contents and then removing them. Obviously my code is not threadsafe even with synchronized(lock){}, b/c the thread that owns the lock in saveEntries(), is not necessarily the same thread that writes into my maps in log() method! Unless I lock the whole code in log() with the same lock object!
I was wondering is there any other way to achieve thread safety w/o enforcing synchronizing by an external lock? Any help is greatly appreciated.
public class Logging {
private static Logging instance;
private static final String vendor1 = "vendor1";
private static final String vendor2 = "vendor2";
private static long delay = 5 * 60 * 1000;
private ConcurrentMap<String, Event> vendor1Calls = new ConcurrentHashMap<String, Event>();
private ConcurrentMap<String, Event> vendor2Calls = new ConcurrentHashMap<String, Event>();
private Timer timer;
private final Object lock = new Object();
private Logging(){
timer = new Timer();
timer.schedule(new TimerTask() {
public void run() {
try {
saveEntries();
} catch (Throwable t) {
timer.cancel();
timer.purge();
}
}
}, 0, delay);
}
public static synchronized Logging getInstance(){
if (instance == null){
instance = new Logging();
}
return instance;
}
public void log(){
ConcurrentMap<String, Event> map;
String key = "";
if (vendor1.equalsIgnoreCase(engine)){
map = vendor1Calls;
}else if(vendor2.equalsIgnoreCase(engine)){
map = vendor2Calls;
}else{
return;
}
key = service + "." + method;
// It would be the code if I use a regular HashMap instead of ConcurrentHashMap
/*Event event = map.get(key);
// Map does not contain this service.method, create an Event for the first time.
if(event == null){
event = new Event();
map.put(key, event);
// Map already contains this key, just adjust the numbers.
}else{
// Modify the object fields
}*/
//}
// Make it thread-safe using CHM
Event newEvent = new Event();
Event existingEvent= map.putIfAbsent(key, newEvent);
if(existingEvent!=null && existingEvent!=newEvent){
// Modify the object fields
}
private void saveEntries(){
Map<String, List<Event>> engineCalls = null;
try {
engineCalls = new HashMap<String, List<Event>>();
List<Event> events = null;
// How can I achieve therad safety here w/o applying any lock?
//synchronized(lock){
if(!vendor1Calls.isEmpty()){
events = new ArrayList<Event>();
events.addAll(vendor1Calls.values());
engineCalls.put(vendor1, events);
vendor1Calls.clear();
}
if(!vendor2Calls.isEmpty()){
events = new ArrayList<Event>();
events.addAll(vendor2Calls.values());
engineCalls.put(vendor2, events);
vendor2Calls.clear();
}
//}
// logICalls() saves the events in the DB.
DBHandle.logCalls(engineCalls);
} catch (Throwable t) {
} finally {
if(engineCalls!=null){
engineCalls.clear();
}
}
}
}
ConcurrentHashMap is divided into different segments based on concurrency level. So different threads can access different segments concurrently in java.
The default size is 16, meaning max 16 threads can work at a time. Each thread can work on each segment during high concurrency and at most 16 threads can operate at max which simply maintains 16locks to guard each bucket of the ConcurrentHashMap.
The ConcurrentHashMap class allows multiple threads to access its entries concurrently. By default, the concurrent hashmap is divided into 16 segments. This is the reason why 16 threads are allowed to concurrently modify the map at the same time.
You can make HashMap thread safe by wrapping it with Collections. synchronizedMap() . What's the difference? @naXa ConcurrentHashMap allows concurrent access and a synchronized one doesn't.
However, when I read from it and then remove all entries by clear() method, I want no other thread writes to map while I’m in the process of reading the map contents and then removing them.
I think what you're trying to say is that you don't really care about strictly locking the maps. Instead, you only really care about the loss of any log entries between the vender1Calls.values() and vendor1Calls.clear(), correct?
In that is the case, I can imagine that you can replace
events.addAll(vendor1Calls.values());
vendor1Calls.clear();
with this in saveEntries:
for (Iterator<Event> iter = vendor1Calls.values().iterator(); iter.hasNext(); ) {
Event e = iter.next();
events.add(e);
iter.remove();
}
That way, you only remove the Events that you added to the events List already. You can still write to the vendor1Calls maps while saveEntries() is still executing, but the iterator skips the values added.
Without any external synchronization you cannot achieve this with a CHM. The Iterator views returned are weakly consistent which means the contents of the Map can change while you are actually iterating over it.
It appears you would need to use a Collections.synchronizedMap
to get the functionality you are looking for.
Edit to make my point more clear:
To achieve this with a synchronizedMap
You would first have to synchronize
on the Map and then you can iterate or copy the contents into another map an then clear.
Map map = Collections.synchronizedMap(new HashMap());
public void work(){
Map local = new HashMap();
synchronized(map){
local.putAll(map);
map.clear();
}
//do work on local instance
}
Instead of the local
instance, as I mentioned, you can iterate + remove
similar to @Kevin Jin's answer.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With