Java ConcurrentHashMap不是线程安全的?怎么回事?

8

我之前使用的是HashMap,像这样:

   public Map<SocketChannel, UserProfile> clients = new HashMap<SocketChannel, UserProfile>();

现在我已经切换到ConcurrentHashMap来避免同步块,但是现在我遇到了问题,我的服务器每秒钟有200-400个并发客户端,预计随着时间的推移会增加负载。

现在代码看起来像这样:

public ConcurrentHashMap<SocketChannel, UserProfile> clients = new ConcurrentHashMap<SocketChannel, UserProfile>();

我的服务器设计如下。我有一个工作线程来处理大量的数据包。每个数据包都会被检查,使用packetHandler子程序(不是线程的一部分),几乎任何客户端都可以随时调用它,就像静态方法一样。
我的整个服务器大部分都是单线程的,除了数据包处理部分。
无论如何,当有人使用命令来计算在线客户端数量并获取一些信息时,可能会出现问题。同时,在计数过程中,客户端也可能会断开连接并从ConcurrentHashMap中删除(这会导致问题)。
此外,我想在这里添加一些代码。
                int txtGirls=0;
                int vidGirls=0;
                int txtBoys=0;
                int vidBoys=0;
                Iterator i = clients.values().iterator();
                while (i.hasNext()) {
                    UserProfile person = (UserProfile)i.next();
                    if(person != null) {
                        if(person.getChatType()) {
                            if(person.getGender().equals("m"))
                                vidBoys++;
                            else //<-- crash occurs here.
                                vidGirls++;
                        } else if(!person.getChatType()) {
                            if(person.getGender().equals("m"))
                                txtBoys++;
                            else
                                txtGirls++;
                        }
                    }
                }

我的意思是,我会通过在迭代器中添加try-catch异常来解决这个问题,以跳过这些空客户端。

但是我不明白,如果上面检查了if(person != null),那么嵌套的代码不应该自动工作吗?

如果它不工作,那就意味着在迭代时已经移除了该项,但这应该是不可能的,因为它是线程安全的,怎么回事?

我该怎么做?还是使用try-catch异常是最好的方法?

这里是异常信息:

java.lang.NullPointerException
    at Server.processPackets(Server.java:398)
    at PacketWorker.run(PacketWorker.java:43)
    at java.lang.Thread.run(Thread.java:636)

processPackets函数包含上述代码,注释中标注了行数 #。

感谢您的启示。


你尝试过Collections.synchronizedMap(map)吗? - zengr
5
"TF" 的含义是ConcurrentHashMap已经具备线程安全的特性,但你希望在此基础上获得其他功能。 - Stephen C
1
如果您能解释“crash occurs here”是什么意思,那将有所帮助。这里的“crash”指的是什么类型的错误?异常是什么? - Stephen C
请回答@Stephen C的问题,并将其改写为一个问题,而不是一个故事。 - Jacob Tomaw
抱歉,我对这整个问题都是新手,但我编辑了问题并放置了异常错误,但堆栈跟踪仍然非常模糊。你们有什么建议,而不是扩展类的简单方法?我会尝试复制集合的想法,但我不确定是否可以100%安全,因为所有对象都通过引用链接在一起,所以如果复制的对象在其他地方被拆除/销毁,那么这种方法就有点无意义了。 - SSpoke
JAVA DOC(http://docs.oracle.com/javase/6/docs/api/index.html?overview-summary.html)表示ConcurrentHashMap类在依赖于其线程安全性而不依赖于其同步细节的程序中与Hashtable完全互操作。 - Kanagavelu Sugumar
5个回答

16
你需要阅读 javadocs 的 ConcurrentHashMap.values() 方法,并特别注意该集合的迭代器如下所述:

“此视图的迭代器是一种‘弱一致性’的迭代器,永远不会抛出 ConcurrentModificationException 异常,并保证遍历构造迭代器时存在的元素,可能(但不保证)会反映构造之后的任何修改。”

该迭代器不能给你值集合状态的一致快照,但它是线程安全的,并且期望的行为范围已经被明确定义了。
如果你想要一个能同时允许你并发修改和给你一致快照的 Map 实现(包括值、键或条目),那么你可能需要创建一个自定义的 Map 包装类(可以原子地复制集合)……或者完整的自定义 Map 实现。两者都比 ConcurrentHashMap 在你的用例中慢得多。

“原子性”指的是作为单个不可中断的操作;请参阅http://en.wikipedia.org/wiki/Atomicity_%28programming%29。顺便说一句,在Java中没有所谓的(原子)本地复制方法。在Java中保证原子性的唯一方法是使用基元或`java.util.concurrent.*`锁定,或使用`volatile`。这两种方法都有注意事项。 - Stephen C
3
如果NPE确实发生在您指定的位置,那么很可能不是由迭代器返回的null引起的。更有可能的是person.getGender()返回了null - Stephen C
好的,这是getGender()。我忽视了性别不是和类一起构建的,而是稍后从响应包中传来,可能正是这种延迟导致了空值。 - SSpoke
我想问最后一个问题,我现在似乎得到的印象是,在迭代ConcurrentHashMap时,我不能进行修改,例如使用put/remove?无论在哪个位置?如果是这样,它会跳过使用put/remove或设置块的代码吗?这两种情况都很糟糕,但希望它会阻塞..我会设置测试,但也许你们可以更快地给我答案。 - SSpoke
1
@SSpoke - (我没有放弃这个话题,但是我的生活中也有其他事情发生。)您可以在迭代时修改地图。然而,您可能会或可能不会观察到并行迭代的修改结果。这就是javadoc所说的。 - Stephen C
显示剩余3条评论

3

我认为你的代码没有问题。因为很少有情况是崩溃发生在else语句,更可能的是getGender()方法返回了null


是的,就是这样。但那是因为 person 为空。看起来我会采用将值复制到集合中的想法?不知道那是否能解决它? - SSpoke
2
@SSpoke 在你的代码中,person.getChatType() 不可能不实现空值检查,然后再对 person.getGender() 进行空值检查。我认为你误解了你的 NullPointerException。 - Jacob Tomaw

3

java.util.concurrent.ConcurrentHashMap不允许使用null值。因此,你代码中的null检查(person != null)是不必要的。

如果你想在迭代时禁止修改Map,你必须在上述代码和所有修改操作代码中使用同步块。


谢谢你,我会把它删除(希望你没有提供错误信息,抱歉有些失礼)。但是感谢你们,我每天都学到新的技巧! - SSpoke
2
@SSpoke JavaDoc清楚地表明@heekyu并没有发布虚假信息。http://download.oracle.com/javase/6/docs/api/java/util/concurrent/ConcurrentHashMap.html - Jacob Tomaw

1

你可能会发现,在迭代过程中无法修改映射表。如果是这种情况,你可以将值和键存储在一个单独的集合中,并遍历该集合,因为它是不可变的。

这并不完美,但另一种选择是扩展ConcurrentHashMap,当添加或删除某些内容时,更新这四个变量,这样你就不必每次都遍历整个列表,因为那似乎是浪费CPU周期。

以下是几个可能有用的链接:

这篇文章谈到了改进并发性的原因是因为放松了一些承诺。 http://www.ibm.com/developerworks/java/library/j-jtp07233.html

内存一致性属性解释: http://download-llnw.oracle.com/javase/6/docs/api/java/util/concurrent/package-summary.html#MemoryVisibility


true。我浪费了很多CPU周期,但覆盖CocurrentHashMap似乎太麻烦了,因为这只是一个命令...还有许多类似的命令要跟随。 - SSpoke
嗨,詹姆斯,我能不能百分之百确定一件事情?如果分配给每个客户端的UserProfile类被复制到一个新的集合中,并且我要迭代它,如果ConcurrentHashMap删除了那个UserProfile,那么这个引用(指针?)是不是相同的?这意味着两者都会被删除吗?你能帮我澄清一下吗? - SSpoke

1
ConcurrentHashMap是线程安全的,它的功能也是线程安全的,但这并不意味着你使用ConcurrentHashMap的所有代码行都是线程安全的。 你需要同步代码块。你正在迭代并尝试从ConcurrentHashMap获取对象,但在中间某个其他线程有机会从相同的ConcurrentHashMap中删除对象,这导致你的线程抛出了NPE错误。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接