【问题标题】:Java ConcurrentHashMap corrupt valuesJava ConcurrentHashMap 损坏的值
【发布时间】:2012-07-09 18:49:31
【问题描述】:

我有一个 ConcurrentHashMap,它有时会表现出奇怪的行为。

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

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

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”。此外,我添加了行

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

由于我打印出来的变量reply 不包含字符串“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;
}

【问题讨论】:

  • 我根本不相信foo.indexOf("empty")永远为非空字符串返回foo.length() - 1。这意味着String.indexOf 非常糟糕。我不相信 ConcurrentHashMapString 坏了 - 我强烈怀疑你的代码在某个地方坏了。
  • 你能显示getDataFromFileSystem(name);的代码吗?
  • 那是 actual getData() 方法,还是您将其重新编写到此处发布?
  • actual getData() 方法具有额外的日志记录,已在本文中删除。
  • 我将在下面的帖子中定义函数 getDataFromFileSystem(name);...

标签: java concurrency thread-safety hashmap concurrenthashmap


【解决方案1】:

我认为您的问题是您的某些操作应该是原子的,而它们不是。

例如,一种可能的线程交错方案如下:

  • 线程 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)
    
  • 1234563 p>

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

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

【讨论】:

  • 感谢 assylias 的帮助。我将更新我的函数以确保线程安全,然后监控行为。
【解决方案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;
}

顺便说一句,这是您应该唯一一次使用== 来比较字符串。在这种情况下,您想通过引用来测试它,而 not 通过内容来测试,因为文件中的行实际上可能包含魔术字符串。

以下是其他几点:

  • 一般来说,当您在程序的多个位置使用相同的String 时,应该将其拉到static final 字段中。无论如何,Java 可能会为您执行此操作,但它也会使代码更简洁。
  • @assylias 在您向ConcurrentHashMap 拨打 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 提到的那样,在引入竞争条件的 put 之前无需删除。

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

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

    这对于相同的reply 字符串是不可能的。我怀疑您正在查看不同的线程或以其他方式误解了输出。同样,不要误以为java.lang.String 中有错误。

【讨论】:

  • 我不建议在 EMPTY_STRING 上使用 ==,因为代码分析工具会报告错误,并且下一个维护开发人员可能会被误导去“修复”它。查看 OPs 实现,如果没有读取任何内容,没有明显的理由不只返回一个空字符串。
  • 我不确定代码分析工具会报告这一点,但也许可以。但是下一个开发者观点是一个很好的观点。我已将其重命名为 _REFERENCE。
  • 在空字符串方面是个好主意,尽管我更喜欢参考 ID。
【解决方案3】:

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

其次,奇怪的 indexOf 调用结果要么是由于编程错误,要么是内存损坏。您的应用程序中是否涉及任何本机代码?你在getDataFromFileSystem做什么?我在多个线程中使用 FileChannel 对象时观察到内存损坏。

【讨论】:

  • 我的应用程序中没有任何本机调用。 getDataFromFileSystem 现在在我的原始帖子中定义。该函数只是使用 BufferedFileReader 读取文件。
  • 另外,我改变了访问地图的方式——我不是先调用 containsKey 再调用 get,而是调用 get 然后检查 null。感谢您的提示:)
猜你喜欢
  • 2010-12-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-05-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多