在单个后台线程定期修改地图的同时读取地图

时间:2017-10-29 07:51:59

标签: java multithreading hashmap thread-safety race-condition

我有一个类,我在liveSocketsByDatacenter方法中每隔30秒从一个后台线程填充一个地图updateLiveSockets()然后我有一个方法getNextSocket()将被调用多个读取器线程,以获取可用的实时套接字,该套接字使用相同的映射来获取此信息。

public class SocketManager {
  private static final Random random = new Random();
  private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
  private final AtomicReference<Map<Datacenters, List<SocketHolder>>> liveSocketsByDatacenter =
      new AtomicReference<>(Collections.unmodifiableMap(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);
  }

  // during startup, making a connection and populate once
  private void connectToZMQSockets() {
    Map<Datacenters, ImmutableList<String>> socketsByDatacenter = Utils.SERVERS;
    // The map in which I put all the live sockets
    Map<Datacenters, List<SocketHolder>> updatedLiveSocketsByDatacenter = new HashMap<>();
    for (Map.Entry<Datacenters, ImmutableList<String>> entry : socketsByDatacenter.entrySet()) {
      List<SocketHolder> addedColoSockets = connect(entry.getKey(), entry.getValue(), ZMQ.PUSH);
      updatedLiveSocketsByDatacenter.put(entry.getKey(),
          Collections.unmodifiableList(addedColoSockets));
    }
    // Update the map content
    this.liveSocketsByDatacenter.set(Collections.unmodifiableMap(updatedLiveSocketsByDatacenter));
  }

  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
  // is there any concurrency or thread safety issue or race condition here?
  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();
    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;
  }

  // is there any concurrency or thread safety issue or race condition here?
  private Optional<SocketHolder> getLiveSocketX(final List<SocketHolder> endpoints) {
    if (!CollectionUtils.isEmpty(endpoints)) {
      // The list of live sockets
      List<SocketHolder> liveOnly = new ArrayList<>(endpoints.size());
      for (SocketHolder obj : endpoints) {
        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();
  }

  // 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() {
    Map<Datacenters, ImmutableList<String>> socketsByDatacenter = Utils.SERVERS;

    // Initialize my new map with the current map content
    Map<Datacenters, List<SocketHolder>> liveSocketsByDatacenter =
        new HashMap<>(this.liveSocketsByDatacenter.get());

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

        boolean status = SendToSocket.getInstance().execute(message.getAdd(), holder, socket);
        boolean isLive = (status) ? true : false;
        // is there any problem the way I am using `SocketHolder` class?
        SocketHolder zmq = new SocketHolder(socket, liveSocket.getContext(), endpoint, isLive);
        liveUpdatedSockets.add(zmq);
      }
      liveSocketsByDatacenter.put(entry.getKey(),
          Collections.unmodifiableList(liveUpdatedSockets));
    }
    this.liveSocketsByDatacenter.set(Collections.unmodifiableMap(liveSocketsByDatacenter));
  }
}

正如我在课堂上看到的那样:

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

我的代码工作正常,没有任何问题,想看看是否有更好或更有效的方式来编写它。我还希望得到关于线程安全问题或任何竞争条件的意见,如果有的话,但到目前为止我还没有看到任何,但我可能是错的。

我最担心的是updateLiveSockets()方法和getLiveSocketX()方法。我在LINE A处迭代liveSockets List SocketHolder,然后创建一个新的SocketHolder对象并添加到另一个新列表。这可以吗?

注意: SocketHolder是一个不可变的类。你可以忽略我拥有的ZeroMQ个东西。

2 个答案:

答案 0 :(得分:8)

您使用以下同步技术。

  1. 带有实时套接字数据的地图位于原子参考之后,这样可以安全地切换地图。
  2. updateLiveSockets()方法已同步(隐式在此),这将阻止同时通过两个线程切换地图。
  3. 使用它时,您可以对地图进行本地引用,以避免在getNextSocket()方法期间发生切换时出现混乱。
  4. 它现在是线程安全吗?

    线程安全始终取决于共享可变数据是否存在正确的同步。在这种情况下,共享可变数据是数据中心到其SocketHolders列表的映射。

    地图位于AtomicReference并制作本地副本以供使用的事实是地图上的足够同步。您的方法采用地图版本并使用它,由于AtomicReference的性质,切换版本是线程安全的。这也可以通过为地图volatile创建成员字段来实现,因为您所做的只是更新引用(您不对其执行任何check-then-act操作)。

    由于scheduleAtFixedRate()保证传递的Runnable不会与自身同时运行,因此不需要synchronized updateLiveSockets(),但是,它也不会任何真正的伤害。

    所以是的,这个类是线程安全的,就像它一样。

    然而,并不完全清楚多个线程是否可以同时使用SocketHolder。实际上,这个类只是尝试通过选择一个随机的实时来最小化SocketHolder的并发使用(尽管不需要随机抽取整个数组来选择一个随机索引)。它实际上没有阻止并发使用。

    可以提高效率吗?

    我相信它可以。在查看updateLiveSockets()方法时,它似乎构建完全相同的映射,除了SocketHolder s可能具有isLive标志的不同值。这使我得出结论,我只想切换地图中的每个列表,而不是切换整个地图。为了以线程安全的方式更改地图中的条目,我可以使用ConcurrentHashMap

    如果我使用ConcurrentHashMap,并且不切换地图,而是切换地图中的值,我可以摆脱AtomicReference

    要更改映射,我可以构建新列表并将其直接放入地图中。这更有效率,因为我更快地发布数据,并且我创建的对象更少,而我的同步只是建立在现成的组件上,这有利于可读性。

    这是我的构建(为简洁起见,省略了一些不太相关的部分)

    public class SocketManager {
        private static final Random random = new Random();
        private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
        private final Map<Datacenters, List<SocketHolder>> liveSocketsByDatacenter = new ConcurrentHashMap<>(); // use ConcurrentHashMap
        private final ZContext ctx = new ZContext();
    
        // ...
    
        private SocketManager() {
          connectToZMQSockets();
          scheduler.scheduleAtFixedRate(this::updateLiveSockets, 30, 30, TimeUnit.SECONDS);
        }
    
        // during startup, making a connection and populate once
        private void connectToZMQSockets() {
          Map<Datacenters, List<String>> socketsByDatacenter = Utils.SERVERS;
          for (Map.Entry<Datacenters, List<String>> entry : socketsByDatacenter.entrySet()) {
            List<SocketHolder> addedColoSockets = connect(entry.getValue(), ZMQ.PUSH);
            liveSocketsByDatacenter.put(entry.getKey(), addedColoSockets); // we can put it straight into the map
          }
        }
    
        // ...      
    
        // this method will be called by multiple threads to get the next live socket
        // is there any concurrency or thread safety issue or race condition here?
        public Optional<SocketHolder> getNextSocket() {
          for (Datacenters dc : Datacenters.getOrderedDatacenters()) {
            Optional<SocketHolder> liveSocket = getLiveSocket(liveSocketsByDatacenter.get(dc)); // no more need for a local copy, ConcurrentHashMap, makes sure I get the latest mapped List<SocketHolder>
            if (liveSocket.isPresent()) {
              return liveSocket;
            }
          }
          return Optional.absent();
        }
    
        // is there any concurrency or thread safety issue or race condition here?
        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
              return Optional.of(liveOnly.get(random.nextInt(liveOnly.size()))); // just pick one
            }
          }
          return Optional.absent();
        }
    
        // no need to make this synchronized
        private void updateLiveSockets() {
          Map<Datacenters, List<String>> socketsByDatacenter = Utils.SERVERS;
    
          for (Map.Entry<Datacenters, List<String>> entry : socketsByDatacenter.entrySet()) {
            List<SocketHolder> liveSockets = liveSocketsByDatacenter.get(entry.getKey());
            List<SocketHolder> liveUpdatedSockets = new ArrayList<>();
            for (SocketHolder liveSocket : liveSockets) { // LINE A
              Socket socket = liveSocket.getSocket();
              String endpoint = liveSocket.getEndpoint();
              Map<byte[], byte[]> holder = populateMap();
              Message message = new Message(holder, Partition.COMMAND);
    
              boolean status = SendToSocket.getInstance().execute(message.getAdd(), holder, socket);
              boolean isLive = (status) ? true : false;
    
              SocketHolder zmq = new SocketHolder(socket, liveSocket.getContext(), endpoint, isLive);
              liveUpdatedSockets.add(zmq);
            }
            liveSocketsByDatacenter.put(entry.getKey(), Collections.unmodifiableList(liveUpdatedSockets)); // just put it straigth into the map, the mapping will be updated in a thread safe manner.
          }
        }
    
    }
    

答案 1 :(得分:4)

如果SocketHolderDatacenters,是不可变的,那么您的程序看起来很不错。不过,这里有一些小的反馈。

<强> 1。 AtomicReference的用法

  

AtomicReference<Map<Datacenters, List<SocketHolder>>> liveSocketsByDatacenter

此成员变量不需要包含在AtomicReference中。你没有用它进行任何原子CAS操作。您可以简单地声明volative Map<Datacenters, List<SocketHolder>>,并在阅读它时,只需创建一个本地引用即可。这足以保证对新Map的引用的原子交换。

<强> 2。同步方法

  

private synchronized void updateLiveSockets()

此方法是从单个线程执行程序调用的,因此不需要对其进行同步。

第3。一些简化

  • 根据您当前对此类的使用情况,您似乎可以过滤掉updateLiveSockets中未处于活动状态的套接字,避免每次客户端调用时都会过滤getNextSocket

  • 您可以替换 Map<Datacenters, ImmutableList<String>> socketsByDatacenter = Utils.SERVERSSet<Datacenters> datacenters = Utils.SERVERS.keySet()并使用密钥。

    <强> 4。 Java 8

如果可能的话,切换到Java 8. Streams和Java8的“可选”将删除大量的样板代码并使代码更容易阅读。