我目前正在Java中编写一个简单的聊天应用程序。它具有用Java编写的服务器应用程序和客户端应用程序,并使用RMI进行通信。
在服务器上,有一个 ArrayList
的活动用户,当用户访问和脱机时会进行更新。每个客户端连接到服务器都有其自己的线程。
建立了新的连接(即新客户端加入)时,创建了一个新线程,并且在此线程中,ArrayList
会更新以反映新用户。同样,当连接丢失时,ArrayList
会更新以反映断开连接。
显然,有很多客户将同时访问ArrayList
。因此,我的问题是,是否应该同步访问/更新此ArrayList
的任何方法?
使用Collections.synchronizedList()
提出了其他答案。
List<User> list = Collections.synchronizedList(new ArrayList<>());
此可能会工作,但是您必须非常小心。此列表只是直接在该列表上的单个方法调用的线程安全。但是,许多操作涉及列表中的多个操作,例如在列表上进行迭代。例如,可能会写下以下代码以向聊天中的每个用户发送消息:
for (User u : list)
u.sendMessage(msg);
如果在迭代期间将用户添加到或从列表中删除,则将使用ConcurrentModificationException
失败。之所以发生这种情况,是因为这种形式的For-loop从列表中获取Iterator
,并通过hasNext
和next
调用重复访问列表。可以在这些访问之间修改列表。如果列表进行了修改,则Iterator
检测到并抛出异常。
在Java SE 8中,可以写下以下内容:
list.forEach(u -> u.sendMesage(msg));
这是安全的,因为列表的锁在forEach
调用的整个过程中保存。
(另一个建议是使用Set
代替List
。这可能是一个好主意,但由于其他原因,它与List
相同的并发问题。)
处理此问题的最佳方法可能是创建自己的对象,包含用户和任何相关数据的列表(或集合),并在您的容器上定义了一组特定的操作用户和相关数据。然后,在容器对象的方法周围添加适当的同步。这允许除了您的列表或一组用户外,还允许对其他数据进行线程安全更新。
最后,如果在对象可见到其他线程之前,在施工时间初始初始化了列表字段,则不需要volatile
。用RMI术语,如果在对象出口之前完成施工,则您是安全的。最好将字段 final
做出,以便在初始化之后,已知所有其他线程都可以看到初始化值。
使用Collections.synchronizedList(new ArrayList<User>());
其次,您也可以做
volatile List<User> list = Collections.synchronizedList(new ArrayList<User>());
保证线程之间可见性。
注意:我还建议使用设置而不是列表以避免在任何错误或角案例中重复。请参阅以下代码以获取参考。
volatile Set<User> onlineUsers = Collections.synchronizedSet(new HashSet<User>());
如果要使用多个线程中的列表,则应使用Collections.synchronizedList()
。
此建议已经在此处给出。