从单个线程修改哈希映射并从多个线程读取?

时间:2017-01-31 07:43:17

标签: java multithreading thread-safety atomic

我有一个类,我每30秒从一个后台线程填充一个映射liveSocketsByDatacenter,然后我有一个方法getNextSocket,它将由多个读者线程调用以获得实时套接字可用,它使用相同的地图来获取此信息。

public class SocketManager {
  private static final Random random = new Random();
  private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
  private final Map<Datacenters, List<SocketHolder>> liveSocketsByDatacenter = new HashMap<>();
  private final ZContext ctx = new ZContext();

  // Lazy Loaded Singleton Pattern
  private static class Holder {
    private static final SocketManager instance = new SocketManager();
  }

  public static SocketManager getInstance() {
    return Holder.instance;
  }

  private SocketManager() {
    connectToZMQSockets();
    scheduler.scheduleAtFixedRate(new Runnable() {
      public void run() {
        updateLiveSockets();
      }
    }, 30, 30, TimeUnit.SECONDS);
  }

  private void connectToZMQSockets() {
    Map<Datacenters, ImmutableList<String>> socketsByDatacenter = Utils.SERVERS;
    for (Map.Entry<Datacenters, ImmutableList<String>> entry : socketsByDatacenter.entrySet()) {
      List<SocketHolder> addedColoSockets = connect(entry.getKey(), entry.getValue(), ZMQ.PUSH);
      liveSocketsByDatacenter.put(entry.getKey(), addedColoSockets);
    }
  }

  private List<SocketHolder> connect(Datacenters colo, List<String> addresses, int socketType) {
    List<SocketHolder> socketList = new ArrayList<>();
    for (String address : addresses) {
      try {
        Socket client = ctx.createSocket(socketType);
        // Set random identity to make tracing easier
        String identity = String.format("%04X-%04X", random.nextInt(), random.nextInt());
        client.setIdentity(identity.getBytes(ZMQ.CHARSET));
        client.setTCPKeepAlive(1);
        client.setSendTimeOut(7);
        client.setLinger(0);
        client.connect(address);

        SocketHolder zmq = new SocketHolder(client, ctx, address, true);
        socketList.add(zmq);
      } catch (Exception ex) {
        // log error
      }
    }
    return socketList;
  }

  // this method will be called by multiple threads to get the next live socket
  public Optional<SocketHolder> getNextSocket() {
    Optional<SocketHolder> liveSocket = Optional.absent();
    List<Datacenters> dcs = Datacenters.getOrderedDatacenters();
    for (Datacenters dc : dcs) {
      liveSocket = getLiveSocket(liveSocketsByDatacenter.get(dc));
      if (liveSocket.isPresent()) {
        break;
      }
    }
    return liveSocket;
  }

  private Optional<SocketHolder> getLiveSocket(final List<SocketHolder> listOfEndPoints) {
    if (!CollectionUtils.isEmpty(listOfEndPoints)) {
      Collections.shuffle(listOfEndPoints);
      for (SocketHolder obj : listOfEndPoints) {
        if (obj.isLive()) {
          return Optional.of(obj);
        }
      }
    }
    return Optional.absent();
  }

  private void updateLiveSockets() {
    Map<Datacenters, ImmutableList<String>> socketsByDatacenter = Utils.SERVERS;

    for (Entry<Datacenters, ImmutableList<String>> entry : socketsByDatacenter.entrySet()) {
      List<SocketHolder> liveSockets = liveSocketsByDatacenter.get(entry.getKey());
      List<SocketHolder> liveUpdatedSockets = new ArrayList<>();
      for (SocketHolder liveSocket : liveSockets) {
        Socket socket = liveSocket.getSocket();
        String endpoint = liveSocket.getEndpoint();
        Map<byte[], byte[]> holder = populateMap();

        boolean status = SendToSocket.getInstance().execute(3, holder, socket);
        boolean isLive = (status) ? true : false;
        SocketHolder zmq = new SocketHolder(socket, liveSocket.getContext(), endpoint, isLive);
        liveUpdatedSockets.add(zmq);
      }
      liveSocketsByDatacenter.put(entry.getKey(), liveUpdatedSockets);
    }
  }
}

正如你在我上面的课程中看到的那样:

  • 从每30秒运行一次的后台线程中,我用所有实时套接字填充liveSocketsByDatacenter地图。
  • 然后从多个线程,我调用getNextSocket方法给我实时套接字,使用liveSocketsByDatacenter map来获取所需信息。

我的上述代码线程是否安全且所有读者线程都会准确地看到liveSocketsByDatacenter?由于我每隔30秒从一个后台线程修改liveSocketsByDatacenter映射,然后从很多读者线程修改,我调用getNextSocket方法,所以我不确定这里是否有任何错误。

看起来我的“getLiveSocket”方法中可能存在线程安全问题,因为每次读取都会从地图中获取共享的ArrayList并将其洗牌?而且可能还有一些我可能错过的地方。在我的代码中解决这些线程安全问题的最佳方法是什么?

如果有更好的方法可以重写这一点,那么我也是开放的。

4 个答案:

答案 0 :(得分:1)

看来,您可以放心使用ConcurrentHashMap而不是常规HashMap,它应该可以正常使用。

在您当前的方法中,使用常规HashMap,您需要同步方法:

getNextSocketconnectToZMQSocketsupdateLiveSockets(在您更新或阅读HashMap的任何地方),就像这些方法之前的sychronized字或所有这些方法共用的监视器上的其他锁方法 - 这不是因为ConcurrentModificationException,而是因为没有同步化读取线程可以看到没有更新的值。

getLiveSocket中的并发修改也存在问题,避免此问题的最简单方法之一是在shuffle之前将listOfEndpoints复制到新列表,如下所示:

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();
  }

答案 1 :(得分:1)

正如您可以详细阅读的那样here,如果多个线程同时访问哈希映射,并且至少有一个线程在结构上修改了映射,则必须在外部进行同步以避免内容的不一致视图。 因此,为了保证线程安全,您应该使用Java Collections synchronizedMap()方法或ConcurrentHashMap。

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

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

由于您在不同的线程中具有非常高并发的应用程序修改和读取键值,因此您还应该查看Producer-Consumer原则,例如: here

答案 2 :(得分:1)

为了保证线程安全,您的代码必须同步对所有共享可变状态的任何访问。

在这里,您共享liveSocketsByDatacenterHashMap的一个{strong>非线程安全实现Map的实例,可以同时读取(updateLiveSockets 1}}和getNextSocket)并修改(由connectToZMQSocketsupdateLiveSockets)而不同步任何已足以使您的代码非线程安全的访问权限。此外,此Map的值是ArrayList 非线程安全实现List的实例,也可以同时读取(getNextSocket 1}}和updateLiveSockets)并通过getLiveSocket更精确地修改Collections.shuffle

解决2线程安全问题的简单方法可能是:

  1. 使用ConcurrentHashMap代替HashMap作为变量liveSocketsByDatacenter,因为它是Map的原生线程安全实现。
  2. 使用Collections.unmodifiableList(List<? extends T> list)ArrayList个实例的不可修改的版本作为地图的值,您的列表将是不可变的,因此线程安全。
  3. 例如:

    liveSocketsByDatacenter.put(
        entry.getKey(), Collections.unmodifiableList(liveUpdatedSockets)
    );`
    
    1. 重写您的方法getLiveSocket以避免直接在您的列表中调用Collections.shuffle,例如,您可以仅播放实时套接字列表而不是所有套接字或使用列表的副本(例如new ArrayList<>(listOfEndPoints))而不是列表本身。
    2. 例如:

      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();
      }
      

      对于#1,因为您似乎经常阅读并且很少(每30秒只修改一次)修改您的地图,您可以考虑重建地图,然后每30秒分享一次不可变版本(使用Collections.unmodifiableMap(Map<? extends K,? extends V> m)),这在大多数读取方案中,方法非常有效,因为您不再为访问地图内容的任何同步机制付出代价。

      您的代码将是:

      // 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);
      }
      

      您的字段liveSocketsByDatacenter也可以是AtomicReference<Map<Datacenters, List<SocketHolder>>>类型,然后是final,您的地图仍会存储在volatile变量中但在类AtomicReference内{1}}。

      之前的代码将是:

      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));
      }
      

答案 3 :(得分:-1)

使用ConcurrentHashMap应该使代码线程安全。或者,使用synchronized方法访问现有的hashmap。