To be thread safe, your code must synchronize any access to all common mutable states.
Here you share liveSocketsByDatacenter , a HashMap instance of an unsafe Map implementation that can potentially be read simultaneously (via updateLiveSockets and getNextSocket ) and modified (via updateLiveSockets and updateLiveSockets ) without synchronizing any access that is already enough to make your code unsafe. Moreover, the values โโof this Map are instances of ArrayList a non thread-safe List implementation, which can also be read at the same time ( getNextSocket and updateLiveSockets ) and more precisely ( getLiveSocket ) Collections.shuffle ).
An easy way to fix your thread 2 security issues could be:
- use
ConcurrentHashMap instead of HashMap for your liveSocketsByDatacenter variable, as it is a robust Map implementation . - put the unmodifiable version of your
ArrayList instances as the value of your map using Collections.unmodifiableList(List<? extends T> list) , then your lists will be immutable, so the streams will be safe.
For instance:
liveSocketsByDatacenter.put( entry.getKey(), Collections.unmodifiableList(liveUpdatedSockets) );`
getLiveSocket your getLiveSocket method to not call Collections.shuffle directly on your list, for example, you can only shuffle the list of live sockets instead of all sockets or use a copy of your list (for example, new ArrayList<>(listOfEndPoints) ) instead of the list itself.
For instance:
private Optional<SocketHolder> getLiveSocket(final List<SocketHolder> listOfEndPoints) { if (!CollectionUtils.isEmpty(listOfEndPoints)) { // The list of live sockets List<SocketHolder> liveOnly = new ArrayList<>(listOfEndPoints.size()); for (SocketHolder obj : listOfEndPoints) { if (obj.isLive()) { liveOnly.add(obj); } } if (!liveOnly.isEmpty()) { // The list is not empty so we shuffle it an return the first element Collections.shuffle(liveOnly); return Optional.of(liveOnly.get(0)); } } return Optional.absent(); }
For # 1, as you seem to read often and rarely (only once every 30 seconds) change your map, you can think about restoring your map and then share your immutable version (using Collections.unmodifiableMap(Map<? extends K,? extends V> m) ) every 30 seconds, this approach is very effective in the main reading scenario, since you no longer pay the price for any synchronization mechanism to access the contents of your map.
Then your code will look like this:
// Your variable is no more final, it is now volatile to ensure that all // threads will see the same thing at all time by getting it from // the main memory instead of the CPU cache private volatile Map<Datacenters, List<SocketHolder>> liveSocketsByDatacenter = Collections.unmodifiableMap(new HashMap<>()); private void connectToZMQSockets() { Map<Datacenters, ImmutableList<String>> socketsByDatacenter = Utils.SERVERS; // The map in which I put all the live sockets Map<Datacenters, List<SocketHolder>> liveSockets = new HashMap<>(); for (Map.Entry<Datacenters, ImmutableList<String>> entry : socketsByDatacenter.entrySet()) { List<SocketHolder> addedColoSockets = connect( entry.getKey(), entry.getValue(), ZMQ.PUSH ); liveSockets.put(entry.getKey(), Collections.unmodifiableList(addedColoSockets)); } // Set the new content of my map as an unmodifiable map this.liveSocketsByDatacenter = Collections.unmodifiableMap(liveSockets); } public Optional<SocketHolder> getNextSocket() { // For the sake of consistency make sure to use the same map instance // in the whole implementation of my method by getting my entries // from the local variable instead of the member variable Map<Datacenters, List<SocketHolder>> liveSocketsByDatacenter = this.liveSocketsByDatacenter; ... } ... // Added the modifier synchronized to prevent concurrent modification // it is needed because to build the new map we first need to get the // old one so both must be done atomically to prevent concistency issues private synchronized void updateLiveSockets() { // Initialize my new map with the current map content Map<Datacenters, List<SocketHolder>> liveSocketsByDatacenter = new HashMap<>(this.liveSocketsByDatacenter); Map<Datacenters, ImmutableList<String>> socketsByDatacenter = Utils.SERVERS; // The map in which I put all the live sockets Map<Datacenters, List<SocketHolder>> liveSockets = new HashMap<>(); for (Entry<Datacenters, ImmutableList<String>> entry : socketsByDatacenter.entrySet()) { ... liveSockets.put(entry.getKey(), Collections.unmodifiableList(liveUpdatedSockets)); } // Set the new content of my map as an unmodifiable map this.liveSocketsByDatacenter = Collections.unmodifiableMap(liveSocketsByDatacenter); }
Your liveSocketsByDatacenter field liveSocketsByDatacenter also be of type AtomicReference<Map<Datacenters, List<SocketHolder>>> , then it will be final , your card will still be stored in the volatile variable, but in the AtomicReference class.
Then the previous code:
private final AtomicReference<Map<Datacenters, List<SocketHolder>>> liveSocketsByDatacenter = new AtomicReference<>(Collections.unmodifiableMap(new HashMap<>())); ... private void connectToZMQSockets() { ...