Одновременное чтение карты, в то время как один фоновый поток регулярно изменяет ее

у меня есть класс, в котором я заполняю карту 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() метод. Я повторяю liveSockets что это List of SocketHolder в строке A, а затем делает новый SocketHolder объект и добавление в другой новый список. Здесь нормально?

Примечание: SocketHolder - это неизменяемый класс. И вы можете игнорировать ZeroMQ что у меня есть.

2 ответов


используются следующие методы синхронизации.

  1. карта с данными Live socket находится за атомной ссылкой, Это позволяет безопасно переключать карту.
  2. на updateLiveSockets() метод синхронизирован (неявно на этом), это предотвратит переключение карты двумя потоками одновременно.
  3. вы делаете локальную ссылку на карту при ее использовании, чтобы избежать путаницы, если переключатель происходит во время getNextSocket() метод.

это потокобезопасно, как сейчас?

безопасность потока всегда зависит от того, есть ли правильная синхронизация на общих изменяемых данных. В этом случае общая изменяемые данные карте ЦОД в список SocketHolders.

тот факт, что карта находится в AtomicReference, и создание локальной копии для использования достаточно синхронизации на карте. Ваши методы берут версию карты и используют ее, переключая версии является потокобезопасным из-за природы AtomicReference. Это также могло быть достигнуто, просто сделав поле элемента для карты volatile, как все, что вы делаете, это обновить ссылку (вы не делаете никаких операций проверки, а затем действовать на нем).

As scheduleAtFixedRate() гарантирует, что переданный Runnable не будет выполняться одновременно с собой,synchronized on updateLiveSockets() не требуется, однако, это также не наносит никакого реального вреда.

да, этот класс является потокобезопасным, так как он есть.

однако, это не совсем ясно, если SocketHolder может использоваться несколькими потоками одновременно. Как бы то ни было, этот класс просто пытается свести к минимуму параллельное использование SocketHolders, выбрав случайный живой (не нужно перетасовывать весь массив, чтобы выбрать один случайный индекс). Он ничего не делает, чтобы фактически предотвратить одновременное использование.

можно ли сделать его более эффективным?

я верю, что он может. При взгляде на updateLiveSockets() метод, кажется строит точно такую же карту, за исключением того, что SocketHolderS может иметь разные значения для 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.
      }
    }

}

если SocketHolder и Datacenters, неизменяемы, ваши программы выглядят нормально. Вот некоторые незначительные отзывы.

1. Использование AtomicReference

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

эта переменная-член не должна быть завернута в AtomicReference. Вы не делаете с ним никакой операции atomic CAS. Вы можете просто объявить volative Map<Datacenters, List<SocketHolder>>, и при чтении его просто создайте локальную ссылку на него. Этого достаточно, чтобы гарантировать атомарная замена ссылки на новую карту.

2. Synchronized метод

private synchronized void updateLiveSockets()

этот метод вызывается из одного исполнителя потока, поэтому нет необходимости синхронизировать его.

3. Некоторые упрощения

  • из вашего текущего использования этого класса кажется, что вы можете отфильтровать сокеты, которые не живут в updateLiveSockets, избегая фильтр каждый раз, когда клиент звонит getNextSocket

  • вы можете заменить Map<Datacenters, ImmutableList<String>> socketsByDatacenter = Utils.SERVERS by Set<Datacenters> datacenters = Utils.SERVERS.keySet() и работать с ключами.

    4. Java 8

если возможно, переключитесь на Java 8. Потоки вместе с дополнительным Java8 удалят много шаблонного кода и сделают ваш код намного проще для чтения.