简体   繁体   English

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

[英]Concurrently reading a Map while a single background thread regularly modifies it

I have a class in which I am populating a map liveSocketsByDatacenter from a single background thread every 30 seconds inside updateLiveSockets() method and then I have a method getNextSocket() which will be called by multiple reader threads to get a live socket available which uses the same map to get this information. 我有一个类,我在updateLiveSockets()方法中每隔30秒从一个后台线程填充一个地图liveSocketsByDatacenter然后我有一个方法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));
  }
}

As you can see in my class: 正如你在我班上看到的那样:

  • From a single background thread which runs every 30 seconds, I populate liveSocketsByDatacenter map with all the live sockets in updateLiveSockets() method. 从每30秒运行一个后台线程,我使用updateLiveSockets()方法中的所有实时套接字填充liveSocketsByDatacenter映射。
  • And then from multiple threads, I call the getNextSocket() method to give me a live socket available which uses a liveSocketsByDatacenter map to get the required information. 然后从多个线程,我调用getNextSocket()方法给我一个可用的实时套接字,它使用liveSocketsByDatacenter映射来获取所需的信息。

I have my code working fine without any issues and wanted to see if there is any better or more efficient way to write this. 我的代码工作正常,没有任何问题,并希望看看是否有更好或更有效的方法来编写它。 I also wanted to get an opinion on thread safety issues or any race conditions if any are there, but so far I haven't seen any but I could be wrong. 我还希望得到关于线程安全问题或任何竞争条件的意见,如果有的话,但到目前为止我还没有看到任何,但我可能是错的。

I am mostly worried about updateLiveSockets() method and getLiveSocketX() method. 我主要担心的是updateLiveSockets()方法和getLiveSocketX()方法。 I am iterating liveSockets which is a List of SocketHolder at LINE A and then making a new SocketHolder object and adding to another new list. 我正在迭代liveSockets ,它是LINE A的SocketHolder List ,然后创建一个新的SocketHolder对象并添加到另一个新列表。 Is this ok here? 这可以吗?

Note: SocketHolder is an immutable class. 注意: SocketHolder是一个不可变类。 And you can ignore ZeroMQ stuff I have. 你可以忽略ZeroMQ东西。

You use the following synchronization techniques. 您使用以下同步技术。

  1. The map with live socket data is behind an atomic reference, this allows safely switching the map. 带有实时套接字数据的映射位于原子引用之后,这样可以安全地切换映射。
  2. The updateLiveSockets() method is synchronized (implicitly on this), this will prevent switching the map by two threads simultaneously. updateLiveSockets()方法是同步的(隐式地),这将防止同时通过两个线程切换映射。
  3. You make a local reference to the map when using it to avoid mixups if the switch happens during the getNextSocket() method. 如果在getNextSocket()方法期间发生切换,则在使用时对地图进行本地引用以避免混淆。

Is it thread safe, as it is now? 它现在是线程安全吗?

Thread safety always hinges on whether there is proper synchronization on shared mutable data. 线程安全始终取决于共享可变数据是否存在正确的同步。 In this case the shared mutable data is the map of datacenters to their list of SocketHolders. 在这种情况下,共享可变数据是数据中心到其SocketHolders列表的映射。

The fact that the map is in an AtomicReference , and making a local copy for use is enough synchronization on the map. 地图位于AtomicReference中并制作本地副本以供使用的事实是地图上的足够同步。 Your methods take a version of the map and use that, switching versions is thread safe due to the nature of AtomicReference . 您的方法采用地图版本并使用它,由于AtomicReference的性质,切换版本是线程安全的。 This could also have been achieved with just making the member field for the map volatile , as all you do is update the reference (you don't do any check-then-act operations on it). 这也可以通过仅使地图的成员字段变为volatile ,因为您所做的只是更新引用(您不对其执行任何check-then-act操作)。

As scheduleAtFixedRate() guarantees that the passed Runnable will not be run concurrently with itself, the synchronized on updateLiveSockets() is not needed, however, it also doesn't do any real harm. 由于scheduleAtFixedRate()保证传递的Runnable不会与它自己同时运行,因此不需要在updateLiveSockets()synchronized ,但它也不会造成任何真正的伤害。

So yes, this class is thread safe, as it is. 所以,是的,这个类是线程安全的,因为它是。

However, it's not entirely clear if a SocketHolder can be used by multiple threads simultaneously. 但是,并不完全清楚SocketHolder可以同时被多个线程使用。 As it is, this class just tries to minimize concurrent use of SocketHolder s by picking a random live one (no need to shuffle the entire array to pick one random index though). 实际上,这个类只是尝试通过选择一个随机的实时来最小化SocketHolder的并发使用(不需要随机抽取整个数组来选择一个随机索引)。 It does nothing to actually prevent concurrent use. 它实际上没有阻止并发使用。

Can it be made more efficient? 可以提高效率吗?

I believe it can. 我相信它可以。 When looking at the updateLiveSockets() method, it seems it builds the exact same map, except that the SocketHolder s may have different values for the isLive flag. 在查看updateLiveSockets()方法时,似乎它构建完全相同的映射,除了SocketHolder可能具有不同的isLive标志值。 This leads me to conclude that, rather than switching the entire map, i just want to switch each of the lists in the map. 这使我得出结论,我只想切换地图中的每个列表,而不是切换整个地图。 And for changing entries in a map in a thread safe manner, I can just use ConcurrentHashMap . 并且为了以线程安全的方式更改映射中的条目,我可以使用ConcurrentHashMap

If I use a ConcurrentHashMap , and don't switch the map, but rather, the values in the map, I can get rid of the AtomicReference . 如果我使用ConcurrentHashMap ,并且不切换地图,而是切换地图中的值,我可以摆脱AtomicReference

To change the mapping I can just build the new list and put it straight into the map. 要更改映射,我可以构建新列表并将其直接放入映射中。 This is more efficient, as I publish data sooner, and I create fewer objects, while my synchronization just builds on ready made components, which benefits readability. 这更有效率,因为我更快地发布数据,并且我创建了更少的对象,而我的同步只是建立在现成的组件上,这有利于可读性。

Here's my build (omitted some parts that were less relevant, for brevity) 这是我的构建(为简洁起见,省略了一些不太相关的部分)

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.
      }
    }

}

If SocketHolder and Datacenters, are immutable, your programs looks fine. 如果SocketHolderDatacenters,是不可变的,那么你的程序看起来很好。 Here is some minor feedback, though. 不过,这里有一些小的反馈。

1. Usage of AtomicReference 1. AtomicReference的用法

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

This member variable does not need to be wrapped in a AtomicReference. 此成员变量不需要包含在AtomicReference中。 You are not doing any atomic CAS operation with it. 你没有用它进行任何原子CAS操作。 You could simply declare a volative Map<Datacenters, List<SocketHolder>> , and when reading it, simply create a local reference to it. 您可以简单地声明一个volative Map<Datacenters, List<SocketHolder>> ,并在阅读它时,只需创建一个本地引用即可。 This is enough to guarantee an atomic swap of the reference to the new Map. 这足以保证对新Map的引用的原子交换。

2. Synchronized method 2.同步方法

private synchronized void updateLiveSockets()

This method is called from a single thread executor, so there is no need for it to be synchronized. 从单个线程执行程序调用此方法,因此不需要同步它。

3. Some simplifications 3.一些简化

  • From your current usage of this class, it seems like you could filter out sockets which are not alive in updateLiveSockets , avoiding to filter every time a client calls getNextSocket 根据您当前对此类的使用情况,您似乎可以过滤掉updateLiveSockets中不存在的套接字,避免每次客户端调用getNextSocket时进行过滤

  • You can replace Map<Datacenters, ImmutableList<String>> socketsByDatacenter = Utils.SERVERS by Set<Datacenters> datacenters = Utils.SERVERS.keySet() and work with the keys. 您可以通过Set<Datacenters> datacenters = Utils.SERVERS.keySet()替换Map<Datacenters, ImmutableList<String>> socketsByDatacenter = Utils.SERVERS并使用这些键。

    4. Java 8 4. Java 8

If possible, switch to Java 8. Streams together with Java8's Optional would remove a lot of boilerplate code and make your code much easier to read. 如果可能的话,切换到Java 8. Streams和Java8的Optional会删除大量样板代码并使代码更容易阅读。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 从单个线程修改哈希映射并从多个线程读取? - Modifying hash map from a single thread and reading from multiple threads? 如何从不同的线程将条目填充到地图中,然后从单个后台线程填充地图并发送? - How to populate entries into a map from a different thread and then from a single background thread iterate the map and send? RxJava:将单个值映射到一组并发运行的 Completable - RxJava: map Single value to a set of Completable that run concurrently 如何运行后台线程定期清理列表中的某些元素? - How can I run a background thread that cleans up some elements in list regularly? RxJava单个后台线程调度程序 - RxJava single background thread scheduler 如何在同时向地图添加值的同时迭代地图? - How to iterate over a map while concurrently adding values to it? 线程已处于活动状态 xxxx 毫秒并且可能挂起 - 在方法执行中修改 int 原语的 While 循环中 - Thread has been active for xxxx milliseconds and may be Hung - At a While Loop that Modifies int primitive in Method Execution 同时运行两个线程 - Run two thread concurrently 同时读取但通过编辑锁定 - reading concurrently but locking with edits 使用单线程从 2 个编年史队列中读取 - Reading from 2 chronicle queue using single thread
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM