Java ConcurrentHashMap损坏了值

时间:2012-07-09 18:49:32

标签: java concurrency thread-safety hashmap concurrenthashmap

我有一个ConcurrentHashMap,偶尔表现出奇怪的行为。

当我的应用程序第一次启动时,我从文件系统中读取一个目录,并使用文件名作为键将每个文件的内容加载到ConcurrentHashMap中。某些文件可能为空,在这种情况下,我将值设置为“空”。

加载完所有文件后,工作线程池将等待外部请求。当请求进来时,我调用getData()函数,在那里我检查ConcurrentHashMap是否包含密钥。如果密钥存在,我得到值并检查值是否为“空”。如果value.contains(“empty”),我返回“找不到文件”。否则,返回文件的内容。当密钥不存在时,我尝试从文件系统加载文件。

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";
}

有时,ConcurrentHashMap将返回非空文件的内容(即value.contains("empty") == false),但行:

if (reply != null && !reply.contains("empty")) 

返回FALSE。我将IF语句分为两部分:if (reply != null)if (!reply.contains("empty"))。 IF语句的第一部分返回TRUE。第二部分返回FALSE。所以我决定打印出变量“reply”以确定字符串的内容是否确实包含“empty”。这不是这种情况,即内容不包含字符串“empty”。此外,我添加了行

int indexOf = reply.indexOf("empty");

由于变量回复在我打印出来时不包含字符串“empty”,因此我希望indexOf返回-1。但函数返回的值大约是字符串的长度,即if reply.length == 15100,然后reply.indexOf("empty")返回15099。

我每周都会遇到这个问题,大约每周2-3次。此过程每天重新启动,因此会定期重新生成ConcurrentHashMap。

有没有人在使用Java的ConcurrentHashMap时看到过这种行为?

修改

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;
}

3 个答案:

答案 0 :(得分:3)

我认为你的问题是你的一些操作应该是原子的,而不是。

例如,一种可能的线程交错场景如下:

  • 线程1在getData方法中读取此行:

    if (map.containsKey(name)) // (1)
    
  • 结果为false,线程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)再次

现在,这并没有解释您正在观察的具体问题,但它说明了当您让许多线程在没有同步的代码段中并发运行时,可能并且确实会发生奇怪的事情。

就目前而言,除非您在测试中多次调用reply = map.get(name),否则我无法找到您所描述的场景的解释,在这种情况下,2次调用很可能不会返回同样的结果。

答案 1 :(得分:2)

首先,请不要认为ConcurrentHashMap中存在错误。 JDK故障是非常罕见的,甚至有趣的想法将使您远离正确调试代码。

我认为您的错误如下。由于您使用的是contains("empty"),如果文件中的行中包含单词"empty"会发生什么?这不是搞砸了吗?

我会使用contains("empty")而不是==。将“空”设为private static final String,然后就可以使用相等。

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,就应该将其上拉到static final字段。无论如何,Java可能会为你做这件事,但它也使代码更加清晰。
  • 当你对ConcurrentHashMap进行2次通话时,@ isylias会对竞争条件有所了解。例如,而不是:

    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);
    

    应该改写如下。没有必要在引入种族条件的put之前删除@assylias。

    map.put(name, contents);
    
  • 你说:

      

    如果reply.length == 15100,则reply.indexOf(“empty”)返回15099。

    使用相同的reply字符串无法做到这一点。我怀疑你正在寻找不同的线程或以其他方式误解输出。同样,不要误以为java.lang.String中存在错误。

答案 2 :(得分:0)

首先,如果您按顺序从多个线程调用其方法,则使用ConcurrentHashMap不会保护您。如果您之后调用containsKeyget,而另一个线程在其间调用remove,则结果为null。一定要只调用get并检查null而不是containsKey / get。它在性能方面也更好,因为两种方法几乎都有相同的成本。

其次,奇怪的indexOf调用结果要么是由于编程错误,要么是指向内存损坏。您的应用程序中是否包含任何本机代码?你在getDataFromFileSystem做什么?我在使用来自多个线程的FileChannel个对象时发现了内存损坏。