如何确保我不是同时在两个线程之间共享相同的套接字?

时间:2017-11-04 04:18:50

标签: java multithreading synchronization thread-safety thread-local

我有一个代码,我正在处理套接字,我需要确保我在两个线程之间共享相同的套接字。在我的下面的代码中,我有一个后台线程,每60秒运行一次并调用updateLiveSockets()方法。在updateLiveSockets()方法中,我迭代我拥有的所有套接字,然后通过调用send类的SendToQueue方法逐个ping它们,并根据响应我将它们标记为活动或死。

现在所有读者线程将同时调用getNextSocket()方法以获取下一个可用的套接字,因此它必须是线程安全的,我需要确保所有读者线程都应该看到{{{{ 1}}和SocketHolder

以下是我的Socket课程:

SocketManager

这是我的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<>(); private final ZContext ctx = new ZContext(); // ... private SocketManager() { connectToZMQSockets(); scheduler.scheduleAtFixedRate(this::updateLiveSockets, 60, 60, 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); } } private List<SocketHolder> connect(List<String> paddes, int socketType) { List<SocketHolder> socketList = new ArrayList<>(); // .... return socketList; } // this method will be called by multiple threads concurrently 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)); if (liveSocket.isPresent()) { return liveSocket; } } return Optional.absent(); } 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(); } // runs every 60 seconds to ping all the socket to make sure whether they are alive or not 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) { Socket socket = liveSocket.getSocket(); String endpoint = liveSocket.getEndpoint(); Map<byte[], byte[]> holder = populateMap(); Message message = new Message(holder, Partition.COMMAND); // pinging to see whether a socket is live or not boolean status = SendToQueue.getInstance().send(message.getAddress(), message.getEncodedRecords(), 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)); } } } 课程:

SendToQueue

问题陈述

现在您可以看到我在两个线程之间共享相同的套接字。似乎 // this method will be called by multiple threads concurrently to send the data public boolean sendAsync(final long address, final byte[] encodedRecords) { Optional<SocketHolder> liveSockets = SocketManager.getInstance().getNextSocket(); PendingMessage m = new PendingMessage(address, encodedRecords, liveSockets.get().getSocket(), true); cache.put(address, m); return doSendAsync(m, socket); } private boolean doSendAsync(final PendingMessage pendingMessage, final Socket socket) { ZMsg msg = new ZMsg(); msg.add(pendingMessage.getEncodedRecords()); try { // send data on a socket LINE A return msg.send(socket); } finally { msg.destroy(); } } public boolean send(final long address, final byte[] encodedRecords, final Socket socket) { PendingMessage m = new PendingMessage(address, encodedRecords, socket, false); cache.put(address, m); try { if (doSendAsync(m, socket)) { return m.waitForAck(); } return false; } finally { // Alternatively (checks that address points to m): // cache.asMap().remove(address, m); cache.invalidate(address); } } 可以将getNextSocket()返回0MQ socket。同时,thread A可以访问相同的timer thread来ping它。在这种情况下,0MQ socketthread A正在改变相同的timer thread,这可能会导致问题。所以我试图找到一种方法,以便我可以防止不同的线程同时将数据发送到同一个套接字并破坏我的数据。

所以我决定同步套接字,这样两个线程就不能同时访问同一个套接字。以下是我在0MQ socket方法中所做的更改。我通过以下方法在套接字上同步:

updateLiveSockets

以下是我在 // runs every 60 seconds to ping all the socket to make sure whether they are alive or not 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) { Socket socket = liveSocket.getSocket(); String endpoint = liveSocket.getEndpoint(); Map<byte[], byte[]> holder = populateMap(); Message message = new Message(holder, Partition.COMMAND); // using the socket as its own lock synchronized (socket) { // pinging to see whether a socket is live or not boolean status = SendToQueue.getInstance().execute(message.getAddress(), message.getEncodedRecords(), 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)); } } 方法中所做的更改。在此我也在发送它之前在socket上同步。

doSendAsync

我可以确保在两个线程之间不共享相同套接字的最佳方法是什么?一般来说,我有大约60个套接字和20个线程访问这些套接字。

如果许多线程使用相同的套接字,则资源利用率不高。此外,如果阻塞 private boolean doSendAsync(final PendingMessage pendingMessage, final Socket socket) { ZMsg msg = new ZMsg(); msg.add(pendingMessage.getEncodedRecords()); try { // send data on a socket LINE A by synchronizing on it synchronized (socket) { return msg.send(socket); } } finally { msg.destroy(); } } (技术上不应该),则阻止等待此套接字的所有线程。所以我想可能有更好的方法来确保每个线程同时使用不同的单个实时套接字而不是特定套接字上的同步。还有我错过的任何角落案例或边缘案例都可能导致一些错误吗?

4 个答案:

答案 0 :(得分:2)

首先,您需要一种方法让客户通知您使用Socket完成了这些操作。您可以添加一个允许它发出信号的方法。这是合法的,它会起作用,但你必须依靠你的客户表现得很好。或者更确切地说,使用套接字的程序员不会忘记返回它。有一种模式可以帮助解决这个问题:execute around模式。您不是发出Socket,而是创建一个接受Consumer<Socket>的方法,然后执行消费者,并返回Socket本身。

public void useSocket(Consumer<Socket> socketUser) {
    Socket socket = getSocket();
    try {
        socketUser.accept(socket);
    } finally {
        returnSocket(socket);
    }
}

现在让我们看看我们将如何实施getSocket()returnSocket()。显然,它涉及从某种集合中获取它们,并将它们返回到该集合。 Queue是一个很好的选择(正如其他人也注意到的)。它允许从一侧获取它,并在另一侧返回,此外还有大量高效的线程安全实现,并且接收者和加法器通常不会相互争用。既然您事先知道套接字的数量,我会选择ArrayBlockingQueue

此处另一个问题是您的实现返回Optional。如果没有可用的Socket,我不确定您的客户会做什么,但如果它正在等待并重试,我建议您只是在队列中阻止getSocket()。事实上,我会尊重您的方法的这一方面,并​​考虑到可能没有Socket可用。对于执行方法,如果没有useSocket()可用,则会将此转换为false方法返回Socket

private final BlockingQueue<Socket> queue;

public SocketPool(Set<Socket> sockets) {
    queue = new ArrayBlockingQueue<>(sockets.size());
    queue.addAll(sockets);
}

public boolean useSocket(Consumer<Socket> socketUser) throws InterruptedException {
    Optional<Socket> maybeSocket = getSocket();
    try {
        maybeSocket.ifPresent(socketUser);
        return maybeSocket.isPresent();
    } finally {
        maybeSocket.ifPresent(this::returnSocket);
    }
}

private void returnSocket(Socket socket) {
    queue.add(socket);
}

private Optional<Socket> getSocket() throws InterruptedException {
    return Optional.ofNullable(queue.poll());
}

那就是,那是你的SocketPool

啊,但接着是吝啬的一点:检查活着。这很吝啬,因为你的活动检查实际上与你的常客有竞争。

为了解决这个问题,我建议如下:让您的客户报告他们获得的Socket是否有效。由于检查实时性归结为使用Socket,这对您的客户来说应该是直截了当的。

因此,我们将取Consumer<Socket>而不是Function<Socket, Boolean>。如果函数返回false,我们会认为Socket不再存在。在这种情况下,我们不是将其添加回常规队列,而是将其添加到死套接字的集合中,我们将有一个计划任务,间歇性地重新检查死套接字。由于这发生在单独的集合中,因此计划的检查不再与常规客户竞争。

现在,您可以制作SocketManager Map,将数据中心映射到SocketPool个实例。此映射不需要更改,因此您可以将其设置为final并在SocketManager的构造函数中初始化它。

这是我SocketPool(未经测试)的初步代码:

class SocketPool implements AutoCloseable {

    private final BlockingQueue<Socket> queue;
    private final Queue<Socket> deadSockets = new ConcurrentLinkedQueue<>();
    private final ScheduledFuture<?> scheduledFuture;

    public SocketPool(Set<Socket> sockets, ScheduledExecutorService scheduledExecutorService) {
        queue = new ArrayBlockingQueue<>(sockets.size());
        queue.addAll(sockets);
        scheduledFuture = scheduledExecutorService.scheduleAtFixedRate(this::recheckDeadSockets, 60, 60, TimeUnit.SECONDS);
    }

    public boolean useSocket(Function<Socket, Boolean> socketUser) throws InterruptedException {
        Optional<Socket> maybeSocket = getSocket();
        boolean wasLive = true;
        try {
            wasLive = maybeSocket.map(socketUser).orElse(false);
            return wasLive && maybeSocket.isPresent();
        } finally {
            boolean isLive = wasLive;
            maybeSocket.ifPresent(socket -> {
                if (isLive) {
                    returnSocket(socket);
                } else {
                    reportDead(socket);
                }
            });
        }
    }

    private void reportDead(Socket socket) {
        deadSockets.add(socket);
    }

    private void returnSocket(Socket socket) {
        queue.add(socket);
    }

    private Optional<Socket> getSocket() throws InterruptedException {
        return Optional.ofNullable(queue.poll());
    }

    private void recheckDeadSockets() {
        for (int i = 0; i < deadSockets.size(); i++) {
            Socket socket = deadSockets.poll();
            if (checkAlive(socket)) {
                queue.add(socket);
            } else {
                deadSockets.add(socket);
            }
        }
    }

    private boolean checkAlive(Socket socket) {
        // do actual live check with SendSocket class, or implement directly in this one
        return true;
    }

    @Override
    public void close() throws Exception {
        scheduledFuture.cancel(true);
    }
}

答案 1 :(得分:1)

我想说这段代码有几个问题:

  1. getLiveSocket()可以为多个线程返回相同的套接字。
  2. java.util.Random不适用于多个线程。
  3. getNextSocket()中的实时套接字快照可能是陈旧的,因为并发调用了修改该快照的updateLiveSockets()方法。
  4. 如果connectToZMQSockets()没有检查套接字的活跃度,则由于scheduleAtFixedRate方法的延迟,60秒内没有活动套接字。
  5. 此外,没有用于检查套接字是否正在使用的标志,并且在线程完成其工作后套接字是否返回池时不清楚。

    考虑以下列方式简化代码:

    1. 你的课程互相循环引用,对我来说,这是一个应该只有单一课程的信号。
    2. 我认为定期检查所有套接字是否存活是有意义的,因为它不能保证套接字状态在检查后和实际发送之前不会发生变化,更好的策略是如果发送给它失败,请验证特定套接字。
    3. 最好将套接字管理限制在线程安全数据结构中,而不是使用显式锁,例如在阻塞队列中。这样的策略可以很好地利用所有可用的套接字。
    4. 以下是代码示例:

      public class SendToSocket {
        private final BlockingQueue<Socket> queue;
      
        public SendToSocket() {
          this.queue = new LinkedBlockingQueue<>();
          // collect all available sockets
          List<Socket> sockets = new ArrayList<>();
          for (Socket socket : sockets) {
            queue.add(socket);
          }
        }
      
        public boolean send(final byte[] reco) throws InterruptedException {
          // can be replaced with poll() method
          Socket socket = queue.take();
          // handle exceptions if needed
          boolean status = sendInternal(socket, reco);
          if (!status) {
            // check whether socket is live
            boolean live = ping(socket);
            if (!live) {
              // log error
              return status;
            }
          }
          // return socket back to pool
          queue.add(socket);
          return status;
        }
      
        private boolean sendInternal(Socket socket, byte[] reco) {
          return true;
        }
      
        private boolean ping(Socket socket) {
          return true;
        }
      }
      

答案 2 :(得分:0)

正如我在您的其他问题中解释的那样,您问题的最佳解决方案是使用ConcurrentQueue

例如,这是如何删除不活动的套接字并保留活动的套接字。

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

//填充队列和地图

// runs every 60 seconds to ping 70 sockets the socket to make sure whether they are alive or not (it does not matter if you ping more sockets than there are in the list because you are rotating the que)
  private void updateLiveSockets() {
    Map<Datacenters, List<String>> socketsByDatacenter = Utils.SERVERS;

    for (Map.Entry<Datacenters, List<String>> entry : socketsByDatacenter.entrySet()) {
    Queue<SocketHolder> liveSockets = liveSocketsByDatacenter.get(entry.getKey());
      for (int i = 0; i<70; i++) {
            SocketHolder s = liveSockets.poll();
        Socket socket = s.getSocket();
        String endpoint = s.getEndpoint();
        Map<byte[], byte[]> holder = populateMap();
        Message message = new Message(holder, Partition.COMMAND);

        // pinging to see whether a socket is live or not
        boolean status = SendToSocket.getInstance().execute(message.getAdd(), holder, socket);
        boolean isLive = (status) ? true : false;

        SocketHolder zmq = new SocketHolder(socket, s.getContext(), endpoint, isLive);
        liveSockets.add(zmq);
      }
    }
  }

答案 3 :(得分:0)

您不需要锁定活动状态的刷新,因为更新布尔值是原子操作。在从池中检出后,只需在后台线程中定期刷新活动。您可能希望在Socket实例级别添加时间戳,以便在发送消息时添加,因此您可以再等待30秒来ping。未使用的套接字需要比使用过的套接字更快地ping。

我想这个想法是synchronized块读取2个布尔值并设置一个布尔值,所以它应该立即返回。您不想在同步时发送,因为它会在很长一段时间内阻止其他线程。

我见过的任何类型的同步实际上只需要同步两个或三个原子操作

boolean got = false;

synchronized(obj) {
    if(alive && available) {
        available = false;
        got = true;
    }
}

if(got) {
    ...               // all thread safe because available must be false
    available = true; // atomic, no need to synchronize when done
}

通常,您可以通过小心处理原子更新的顺序来找出完全避免同步的方法。

例如,您可以通过使用映射而不是列表来存储套接字来完成没有同步的工作。我不得不考虑它,但是你可以在完全没有同步的情况下使这个线程安全,然后它会更快。

我绝不会考虑使用像Hashtable这样的线程安全集合而不是HashMap。

class Socket {
    Socket() {
        alive = true;
        available = true;
        last = System.currentTimeMillis();
    }

    private synchronized boolean tryToGet() {
        // should return pretty fast - only 3 atomic operations

        if(alive && available) {
            available = false;
            return true;
        }

        return false;
    }

    public boolean send() {
        if(tryToGet()) {
            // do it
            last = System.currentTimeMillis();
            available = true; // no need to lock atomic operation
            return true;
        }

        return false;
    }

    private boolean ping() { // ... }

    public void pingIfNecessary() {
        // long update may not be atomic, cast to int if not

        if(alive && (System.currentTimeMillis() - last) > 30000) {
            if(tryToGet()) {
                // other pingIfNecessary() calls have to wait

                if(ping()) {
                    last = System.currentTimeMillis();
                } else {
                    alive = false;
                }

                available = true;
            }
        }
    }

    private boolean alive;
    private boolean available;
    private long last;
};

void sendUsingPool(String s) {    
    boolean sent = false;

    while(!sent) {
        for(Socket socket : sockets) {
            if(socket.send(s)) {
                sent = true;
                break;
            }
        }

        if(!sent) {    
            // increase this number if you want to be nicer
            try { Thread.sleep(1); } catch (Exception e) { }
        }
    }
}

public void run() {
    while(true) {
        for(Socket socket : sockets) {
            socket.pingIfNecessary();
        }

        try { Thread.sleep(100); } catch (Exception e) { }
    }
}