以线程安全的方式从数组列表中返回一个对象?

时间:2017-10-30 21:36:39

标签: java multithreading arraylist concurrency thread-safety

我有一个类,我在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是一个不可变类。

1 个答案:

答案 0 :(得分:2)

代码B或C都不是线程安全的。

代码B

当您在enpoints列表上进行迭代以复制它时,没有什么可以阻止另一个线程修改,即。要添加和/或删除的元素。

代码C

假设endpoints不为空,您正在对列表对象进行三次调用:isEmptysizeget。从并发角度来看存在几个问题:

  1. 根据参数的类型List<SocketHolder>,无法保证这些方法强制对列表进行内部更改以传播到其他线程(内存可见性),让竞争条件分开(如果在您的线程执行此函数之一时修改列表)。

  2. 我们假设列表endpoints提供了之前描述的保证 - 例如它已被Collections.synchronizedList()包裹。在这种情况下,仍然缺少线程安全性,因为在对isEmptysizeget的每次调用之间,可以在线程执行getLiveSocketX时修改列表方法。这可能会使您的代码使用列表的过时状态。例如,您可以使用endpoints.size()返回的大小,该大小不再有效,因为已在列表中添加或删除了元素。

  3. 编辑 - 代码更新后

    在您提供的代码中,乍一看似乎:

    1. 您确实没有在endpoints方法中共同修改我们之前讨论的getLiveSocketX列表,因为方法updateLiveSockets创建了一个新列表{{1}您从现有的liveUpdatedSockets

    2. 填充的
    3. 您使用liveSocketsAtomicReference的地图保存到感兴趣的套接字列表中。此Datacenters的后果是强制从此映射到所有列表及其元素的内存可见性。这意味着, by side-effect ,可以防止“生产者”和“消费者”线程之间的内存不一致(分别执行AtomicReferenceupdateLiveSockets)。但是你仍然面临着竞争条件 - 让我们想象getLiveSocketupdateLiveSockets同时运行。考虑一个套接字getLiveSocket,其状态只是从alive切换到closed。 S会将套接字updateLiveSockets的状态视为非活动状态,并相应地创建新的S。但是,同时运行的SocketHolder会看到过时状态getLiveSocket - 因为它仍然会使用S正在重新创建的套接字列表。

    4. 方法updateLiveSockets上使用的synchronized关键字在此不提供任何保证,因为代码的其他部分也不是updateLiveSockets

      < / LI>

      总结一下,我会说:

      1. 编写的synchronized代码是 本身线程安全;
      2. 但是,复制列表的方式可以防止并发修改;并且您从getLiveSocketX副作用中受益,以获得内存可见性的最小保证,人们希望在AtomicReference之后得到一致的套接字列表从另一个线程生成;
      3. 您仍然会遇到如(2)所述的竞争条件,但这可能会很好,具体取决于您希望赋予getNextSocketgetLiveSocket方法的规范 - 您可以接受一个套接字由getNextSocket返回以使其不可用并具有重试机制。
      4. 所有这一切,我将彻底审查和重构代码,以展示更具可读性和显式线程安全的消费者/生产者模式。使用getLiveSocket和单AtomicReference时应该格外小心,这似乎是我使用不当 - 尽管很好 synchronized确实有帮助你如前所述。