在迭代器内的线程内删除Collection的元素

时间:2015-02-26 11:29:16

标签: java concurrency

我有这个循环:

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

有时会有效,但有时我会在致电java.lang.IllegalStateException时收到it.remove(); 有安全的方法吗?

--- --- UPDATE

我需要在运行在服务器上的代码中执行此操作,该代码接收来自许多客户端套接字的连接。此代码会不时执行某些查询,并且每次执行查询时,结果都会发送到所有连接的套接字。因此,对于每个套接字,我创建一个新的线程来执行此操作。我想从列表中删除的是封闭的套接字。

这是我的服务器工作方式的示例:

public class SocketServidor {
    static ServerSocket serverSocket;
    static List<Socket> socketsConectados = new ArrayList<>();

    public static void main(String[] args) throws Exception {
        serverSocket = new ServerSocket(5963);

        new Thread( ()-> {
            lacoConexao:
            while(true) {
                try {
                    socketsConectados.add(serverSocket.accept());
                } catch (IOException ex) {
                    Logger.getLogger(SocketServidor.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        }).start();

        lacoMensagens:
        while(true) {
            int valor = (int) (Math.random() * 3) + 1;
            lacoIterators:
            for (Iterator<Socket> it = socketsConectados.iterator(); it.hasNext();) {
                Socket socket = it.next();
                new Thread( () -> {
                    try {
                        if(socket.isClosed()) {
                            return;
                        }
                        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();
                    }
                }).start();
            }
            System.out.println("Sockets conectados: "+socketsConectados.size());
            Thread.sleep(valor*1000);
        }
    }
}

2 个答案:

答案 0 :(得分:2)

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

你应该:

  • 使用线程安全列表,例如List<Socket> 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();
    }
  }
}

答案 1 :(得分:0)

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

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

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

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

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

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