我有一个并发HashMap,偶尔会表现出奇怪的行为。
当我的应用程序首次启动时,我从文件系统中读取一个目录,并使用文件名作为键将每个文件的内容加载到并发哈希映射中。有些文件可能是空的,在这种情况下,我将值设置为“空”。
一旦所有文件都被加载,一个工作线程池将等待外部请求。当请求进来时,我调用getData()函数,在那里我检查并发HashMap是否包含键。如果键存在,我获取值并检查值是否为“空”。如果value.包含(“空”),我返回“未找到文件”。否则,返回文件的内容。当键不存在时,我尝试从文件系统加载文件。
private String getData(String name) {
String reply = null;
if (map.containsKey(name)) {
reply = map.get(name);
} else {
reply = getDataFromFileSystem(name);
}
if (reply != null && !reply.contains("empty")) {
return reply;
}
return "file not found";
}
有时,并发HashMap会返回非空文件的内容(即value.包含("空")==false
),但是该行:
if (reply != null && !reply.contains("empty"))
返回FALSE。我将IF语句分解为两部分:if(回复!=null)
和if(!回复.包含("空"))
。IF语句的第一部分返回TRUE。第二部分返回FALSE。所以我决定打印出变量“回复”,以确定字符串的内容是否确实包含“空”。情况并非如此,即内容不包含字符串“空”。此外,我添加了这一行
int indexOf = reply.indexOf("empty");
由于变量回复在我打印出来时不包含字符串“空”,所以我希望indexOf
返回-1。但是该函数返回的值大约是字符串的长度,即如果的值的长度==15100
,那么的值返回15099。
我每周都会遇到这个问题,每周大约2-3次。此过程每天都会重新启动,因此会定期重新生成Concurrent tHashMap。
有人在使用Java的并发HashMap时看到过这种行为吗?
编辑
private String getDataFromFileSystem(String name) {
String contents = "empty";
try {
File folder = new File(dir);
File[] fileList = folder.listFiles();
for (int i = 0; i < fileList.length; i++) {
if (fileList[i].isFile() && fileList[i].getName().contains(name)) {
String fileName = fileList[i].getAbsolutePath();
FileReader fr = null;
BufferedReader br = null;
try {
fr = new FileReader(fileName);
br = new BufferedReader(fr);
String sCurrentLine;
while ((sCurrentLine = br.readLine()) != null) {
contents += sCurrentLine.trim();
}
if (contents.equals("")) {
contents = "empty";
}
return contents;
} catch (Exception e) {
e.printStackTrace();
if (contents.equals("")) {
contents = "empty";
}
return contents;
} finally {
if (fr != null) {
try {
fr.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if (br != null) {
try {
br.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if (map.containsKey(name)) {
map.remove(name);
}
map.put(name, contents);
}
}
}
} catch (Exception e) {
e.printStackTrace();
if (contents.equals("")) {
contents = "empty";
}
return contents;
}
return contents;
}
我认为你的问题是你的一些操作应该是原子的,而它们不是。
例如,一种可能的线程交织场景如下:
>
线程1在getData
方法中读取此行:
if (map.containsKey(name)) // (1)
结果为假,线程1转到
reply = getDataFromFileSystem(name); // (2)
在getDataFromFileSystem
中,您有以下代码:
if (map.containsKey(name)) { // (3)
map.remove(name); // (4)
}
map.put(name, contents); // (5)
想象另一个线程(线程2)到达(1)
,而线程1位于(4)
和(5)
之间:名称不在映射中,因此线程2再次转到(2)
这并不能解释您正在观察的具体问题,但它说明了这样一个事实,即当您让许多线程在一段代码中并发运行而不同步时,奇怪的事情可能并且确实会发生。
就目前而言,我找不到对您描述的场景的解释,除非您在测试中多次调用回复=map. get(name)
,在这种情况下,这两个调用很可能不会返回相同的结果。
首先,甚至不要认为在并发HashMap
中有bug。JDK错误是非常罕见的,即使是有趣的想法也会让你远离正确调试代码。
我认为你的bug如下。既然你使用包含("空")
,如果文件中的行中有单词"空"
会发生什么?这不会把事情搞砸吗?
与其使用包含("空")
,不如使用==
。将“空”设为私有静态最终字符串
,然后您可以对其使用相等。
private final static String EMPTY_STRING_REFERENCE = "empty";
...
if (reply != null && reply != EMPTY_STRING_REFERENCE) {
return reply;
}
...
String contents = EMPTY_STRING_REFERENCE;
...
// really this should be if (contents.isEmpty())
if (contents.equals("")) {
contents = EMPTY_STRING_REFERENCE;
}
顺便说一句,这是您唯一一次应该使用==
来比较字符串。在这种情况下,您希望通过引用而不是内容来测试它,因为文件中的行实际上可能包含魔术字符串。
以下是其他一些要点:
>
String
时,它应该被拉到一个静态最终
字段。无论如何,Java可能会为你做这件事,但它也使代码更简洁。@assylias在您对Concurrent tHashMap
进行2次调用时发现了竞争条件。例如,而不是做:
if (map.containsKey(name)) {
reply = map.get(name);
} else {
您应该执行以下操作,以便只执行一项。
reply = map.get(name);
if (reply == null) {
在您的代码中,您这样做:
if (map.containsKey(name)) {
map.remove(name);
}
map.put(name, contents);
这应该改写如下。没有必要在引入竞争条件的放之前删除@assylias提到的。
map.put(name, contents);
你说:
如果对. long==15100,则对.indexOf("空")返回15099。
对于相同的回复
字符串,这是不可能的。我怀疑您正在查看不同的线程或以其他方式误解了输出。同样,不要被愚弄认为java. lang.String
中存在错误。
首先,如果您按顺序从多个线程调用它的方法,使用ConCurrentHashMap
并不能保护您。如果您之后调用有键
和get
,而另一个线程调用删除
,您将得到一个空结果。请务必只调用get并检查null而不是有键/get。在性能方面也更好,因为这两种方法几乎具有相同的成本。
其次,奇怪的indexOf调用结果要么是由于编程错误,要么指向内存损坏。您的应用程序中是否涉及任何本机代码?您在getDataFromFileSystem
中做什么?我在使用来自多个线程的FileChannel
对象时观察到内存损坏。