Modifying hash map from a single thread and reading from multiple threads?

后端 未结 4 987
余生分开走
余生分开走 2021-01-06 01:15

I have a class in which I am populating a map liveSocketsByDatacenter from a single background thread every 30 seconds and then I have a method getNextSoc

相关标签:
4条回答
  • 2021-01-06 01:53

    To be thread-safe, your code must synchronize any access to all shared mutable state.

    Here you share liveSocketsByDatacenter, an instance of HashMap a non thread-safe implementation of a Map that can potentially be concurrently read (by updateLiveSockets and getNextSocket) and modified (by connectToZMQSockets and updateLiveSockets) without synchronizing any access which is already enough to make your code non thread safe. Moreover, the values of this Map are instances of ArrayList a non thread-safe implementation of a List that can also potentially be concurrently read (by getNextSocket and updateLiveSockets) and modified (by getLiveSocket more precisely by Collections.shuffle).

    The simple way to fix your 2 thread safety issues could be to:

    1. use a ConcurrentHashMap instead of a HashMap for your variable liveSocketsByDatacenter as it is a natively thread safe implementation of a Map.
    2. put the unmodifiable version of your ArrayList instances as value of your map using Collections.unmodifiableList(List<? extends T> list), your lists would then be immutable so thread safe.

    For example:

    liveSocketsByDatacenter.put(
        entry.getKey(), Collections.unmodifiableList(liveUpdatedSockets)
    );`
    
    1. rewrite your method getLiveSocket to avoid calling Collections.shuffle directly on your list, you could for example shuffle only the list of live sockets instead of all sockets or use a copy of your list (with for example new ArrayList<>(listOfEndPoints)) instead of the list itself.

    For example:

    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 frequently read and rarely (only once every 30 seconds) modify your map, you could consider to rebuild your map then share its immutable version (using Collections.unmodifiableMap(Map<? extends K,? extends V> m)) every 30 seconds, this approach is very efficient in mostly read scenario as you no longer pay the price of any synchronization mechanism to access to the content of your map.

    Your code would then be:

    // 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 field liveSocketsByDatacenter could also be of type AtomicReference<Map<Datacenters, List<SocketHolder>>> , it would then be final, your map will still be stored in a volatile variable but within the class AtomicReference.

    The previous code would then be:

    private final AtomicReference<Map<Datacenters, List<SocketHolder>>> liveSocketsByDatacenter 
        = new AtomicReference<>(Collections.unmodifiableMap(new HashMap<>()));
    
    ...
    
    private void connectToZMQSockets() {
        ...
        // Update the map content
        this.liveSocketsByDatacenter.set(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.get();
        ...
    }
    
    // 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.get());
        ...
        // Update the map content
        this.liveSocketsByDatacenter.set(Collections.unmodifiableMap(liveSocketsByDatacenter));
    }
    
    0 讨论(0)
  • 2021-01-06 01:53

    Using ConcurrentHashMap should make your code threadsafe. Alternatively use synchronized methods to access existing hashmap.

    0 讨论(0)
  • 2021-01-06 01:56

    It seems, that you can safely use ConcurrentHashMap here instead of regular HashMap and it should work.

    In your current approach, using regular HashMap, you need to have synchronization of methods:

    getNextSocket, connectToZMQSockets and updateLiveSockets (everywhere you update or read the HashMap) like a sychronized word before those methods or other lock on a monitor common for all these methods - And this is not because of ConcurrentModificationException, but because without synchornization reading threads can see not updated values.

    There is also problem with concurrent modification in the getLiveSocket, one of the simplest way to avoid this problem is to copy the listOfEndpoints to a new list before shuffle, like this:

    private Optional<SocketHolder> getLiveSocket(final List<SocketHolder> endPoints) {
        List<SocketHolder> listOfEndPoints = new ArrayList<SocketHolder>(endPoints);
        if (!CollectionUtils.isEmpty(listOfEndPoints)) {
    
          Collections.shuffle(listOfEndPoints);
          for (SocketHolder obj : listOfEndPoints) {
            if (obj.isLive()) {
              return Optional.of(obj);
            }
          }
        }
        return Optional.absent();
      }
    
    0 讨论(0)
  • 2021-01-06 02:00

    As you can read in detail e.g. here, if multiple threads access a hash map concurrently, and at least one of the threads modifies the map structurally, it must be synchronized externally to avoid an inconsistent view of the contents. So to be thread safe you should use either Java Collections synchronizedMap() method or a ConcurrentHashMap.

    //synchronizedMap
    private final Map<Datacenters, List<SocketHolder>> liveSocketsByDatacenter = Collections.synchronizedMap(new HashMap<Datacenters, List<SocketHolder>>());    
    

    or

    //ConcurrentHashMap
    private final Map<Datacenters, List<SocketHolder>> liveSocketsByDatacenter = new ConcurrentHashMap<Datacenters, List<SocketHolder>>();
    

    As you have very highly concurrent application modifying and reading key value in different threads, you should also have a look at the Producer-Consumer principle, e.g. here.

    0 讨论(0)
提交回复
热议问题