Java 在迭代器内的线程内删除集合的元素

Java 在迭代器内的线程内删除集合的元素,java,concurrency,Java,Concurrency,我有一个循环: for (Iterator<Socket> it = collection.iterator(); it.hasNext();) { Object obj = it.next(); new Thread( () -> { if(myCondition(obj)){ it.remove(); } }).start(); } 您应该阅读更多关于线程安全的内容。我可以告诉您要在代码中修复

我有一个循环:

for (Iterator<Socket> it = collection.iterator(); it.hasNext();) {
    Object obj = it.next();
    new Thread( () -> {
        if(myCondition(obj)){
            it.remove();
        }
    }).start();
}

您应该阅读更多关于线程安全的内容。我可以告诉您要在代码中修复什么,但在您正确理解并发性之前,您将在每次编写一行代码时不断添加bug

你应该:

  • 使用线程安全列表,例如
    list socketsConectados=new CopyOnWriteArrayList()
  • 使用线程池,而不是为每个套接字创建一个线程:
    ExecutorService executor=Executors.newCachedThreadPool()
  • 提交将消息发送到线程池的任务,而不是手动创建线程
  • 此外,您正在使用的那些标签令人困惑,它们应该用来打破外部循环-如果您只想对代码进行注释,请使用注释

作为参考,代码可以如下所示:

public class SocketServidor {
  static ServerSocket serverSocket;
  private static final ExecutorService executor = Executors.newCachedThreadPool();
  private static final List<Socket> sockets = new CopyOnWriteArrayList<>();

  public static void main(String[] args) throws Exception {
    serverSocket = new ServerSocket(5963);
    new Thread(SocketServidor::acceptSockets).start();
    sendMessages();
  }

  private static void acceptSockets() {
    while (true) {
      try {
        sockets.add(serverSocket.accept());
      } catch (IOException ex) {
        Logger.getLogger(SocketServidor.class.getName()).log(Level.SEVERE, null, ex);
      }
    }
  }

  private static void sendMessages() throws InterruptedException {
    while (true) {
      int valor = (int) (Math.random() * 3) + 1;
      for (Socket socket : sockets) {
        executor.submit(() -> {
          if (socket.isClosed()) sockets.remove(socket);
          else sendMessage(socket, valor);
        });
      }
      System.out.println("Sockets conectados: " + sockets.size());
      Thread.sleep(valor * 1000);
    }
  }

  private static void sendMessage(Socket socket, int valor) {
    try {
      PrintWriter out = new PrintWriter(socket.getOutputStream());
      out.println("Você acabou de ganhar R$ " + new DecimalFormat("###,##0.00").format(valor) + " às " + new SimpleDateFormat("HH:mm:ss").format(
              new Date()) + "!");
      if (out.checkError()) {
        socket.close();
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}
公共类SocketServisidor{
静态服务器套接字服务器套接字;
private static final executor service executor=Executors.newCachedThreadPool();
私有静态最终列表套接字=新CopyOnWriteArrayList();
公共静态void main(字符串[]args)引发异常{
serverSocket=新的serverSocket(5963);
新线程(SocketServidor::acceptSockets).start();
sendMessages();
}
私有静态void acceptSockets(){
while(true){
试一试{
sockets.add(serverSocket.accept());
}捕获(IOEX异常){
Logger.getLogger(SocketServidor.class.getName()).log(Level.SEVERE,null,ex);
}
}
}
私有静态void sendMessages()引发InterruptedException{
while(true){
int valor=(int)(Math.random()*3)+1;
用于(插座:插座){
执行人提交(()->{
if(socket.isClosed())套接字。移除(socket);
else发送消息(socket、valor);
});
}
System.out.println(“Sockets-conconnectados:+Sockets.size());
线程。睡眠(valor*1000);
}
}
私有静态void sendMessage(套接字、int-valor){
试一试{
PrintWriter out=新的PrintWriter(socket.getOutputStream());
out.println(“Voc#acabou de ganhar R$”+新的十进制格式(“0.00”)。格式(valor)+“as”+新的简化格式(“HH:mm:ss”)。格式(
新日期())+“!”;
if(out.checkError()){
socket.close();
}
}捕获(例外e){
e、 printStackTrace();
}
}
}

您的代码不是线程安全的。它既有可见性问题,也有并发性问题

开始迭代并将对象和迭代器引用传递给另一个线程,然后在主线程中继续迭代。在第二个线程上执行.remove()之前,主线程已经完成了迭代。即使它没有完成迭代,您的“检查然后执行”操作也不是原子的。不能保证您的“it”引用仍然指向您决定删除的“obj”引用

此外,您的“it”引用不会以线程安全的方式传递给另一个线程,因此存在可见性问题。迭代器对象中的更改不能保证对其他线程可见

使用当前方法从多个线程的列表中删除对象没有意义。因为你在整个列表上按顺序迭代。当您决定使用迭代器删除一个对象时,您必须确保没有使用锁或同步块迭代到下一个对象,这会使多线程无用

您应该使用并发数据结构,例如ConcurrentHashMap,或者在线程中提供适当的同步


列表接口,并且列表接口的任何并发或非并发实现都不适合此用例。除非连接到服务器的顺序很重要,否则为随机的一组客户端套接字使用列表是没有意义的。如果更新数量很大,CopyOnWriteArrayList的效率就不高。如果客户机的数量很大,则从带有O(n)的列表中删除项目也没有效率。客户端可能会随机连接和断开连接,而没有订购保证等。最好使用ConcurrentHashMap。

为什么在线程中?另外,您是否使用线程安全的集合?我不知道Java集合包中有任何集合具有threadsafe
Iterator
s。有些具有线程安全迭代(您可以在迭代时修改来自另一个线程的集合,如使用
ConcurrentHashMap
派生迭代器或
copyonwritearaylist
),但没有(我知道)具有线程安全迭代器(在来自多个线程的单个迭代器实例上执行操作),但如果我在循环中执行此操作,我将得到一个
java.util.ConcurrentModificationException
,因为我正在遍历列表并从中删除元素。嗯,
集合
具有
.removeIf()
;但这并不能解决为什么迭代删除会在第一个线程中发生的问题place@fge我相信removeIf是连续的。@MateusViccari在我提出的解决方案中不需要循环。您可能想首先解释为什么要使用线程:这可能有助于我们提出更好的解决方案。例如,如果您使用一个列表,并且觉得
remove
操作很慢,那么将集合从列表更改为集合将比使用线程更能提高性能。@Mateusvicari这是一个非常不同的问题-例如,使用一种带有阻塞队列的生产者/消费者系统形式更有意义。稍后我将尝试详细说明。
public class SocketServidor {
  static ServerSocket serverSocket;
  private static final ExecutorService executor = Executors.newCachedThreadPool();
  private static final List<Socket> sockets = new CopyOnWriteArrayList<>();

  public static void main(String[] args) throws Exception {
    serverSocket = new ServerSocket(5963);
    new Thread(SocketServidor::acceptSockets).start();
    sendMessages();
  }

  private static void acceptSockets() {
    while (true) {
      try {
        sockets.add(serverSocket.accept());
      } catch (IOException ex) {
        Logger.getLogger(SocketServidor.class.getName()).log(Level.SEVERE, null, ex);
      }
    }
  }

  private static void sendMessages() throws InterruptedException {
    while (true) {
      int valor = (int) (Math.random() * 3) + 1;
      for (Socket socket : sockets) {
        executor.submit(() -> {
          if (socket.isClosed()) sockets.remove(socket);
          else sendMessage(socket, valor);
        });
      }
      System.out.println("Sockets conectados: " + sockets.size());
      Thread.sleep(valor * 1000);
    }
  }

  private static void sendMessage(Socket socket, int valor) {
    try {
      PrintWriter out = new PrintWriter(socket.getOutputStream());
      out.println("Você acabou de ganhar R$ " + new DecimalFormat("###,##0.00").format(valor) + " às " + new SimpleDateFormat("HH:mm:ss").format(
              new Date()) + "!");
      if (out.checkError()) {
        socket.close();
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}