使用putIfAbsent重叠ConcurrentHashMap

时间:2014-03-26 22:36:00

标签: java multithreading hashmap wrapper concurrenthashmap

插入哈希表似乎存在问题。我创建了大约8个线程,并在每个线程中我执行以下代码。每个线程都接收一个char []数组。每个线程的工作是标记化此数组(查找空格)。一旦找到令牌,我需要将它添加到哈希表中(如果它不存在)。如果确实存在,那么我需要将1添加到该令牌的当前值(密钥)。

您可能会问的问题:

为什么不从char []转换为String?

我试过这个,因为字符串是不可变的,我最终耗尽了内存(我处理的是10g文件),或者我花了太长时间收集垃圾。使用Character [],我可以重用相同的变量,而不占用内存中的额外空间。

有什么问题?

当我完成整个文件的处理后,我运行代码:

for (Entry<Character [], Integer> e : wordCountMap.entrySet()) {
    System.out.println(Arrays.toString(e.getKey()) + " = " + e.getValue());
}

在我的主要功能中。我得到的结果是少于100个键/值对。我知道应该有大约20,000。似乎有些重叠。

    Character [] charArray = new Character[8];
    for (i = 0; i < newbyte.length; i++) { //newbyte is a char[] from main
        if (newbyte[i] != ' ') {
            charArray[counter] = newbyte[i];
            counter++;
        }
        else { 
            check = wordCountMap.putIfAbsent(charArray, 1);
            if (check != null) { 
                wordCountMap.put(charArray, wordCountMap.get(charArray) + 1);
            }
            for (j = 0; j < counter; j++) {
                charArray[j] = null;
            }//Null out the array

ConcurrentMap<Character [], Integer> wordCountMap //this is the definition in main

正如下面的一些评论所暗示的那样,我实际上是在将行引用传递给charArray时:

wordCountMap.put(charArray, wordCountMap.get(charArray) + 1);

已执行。所以我的问题是,如何传递该值?它现在实际上非常有意义,因为最终有大约320个键/值对--8个线程,40个循环(每个线程获得250个/每次迭代8 MB)。

4 个答案:

答案 0 :(得分:1)

如果不同步get()和put()操作,我不相信这是可以实现的。

根据ConcurrentHashMap docs

  

检索操作(包括get)一般不会阻塞,因此可能与更新操作重叠(包括put和remove)。检索反映了最近完成的更新操作的结果。

这意味着如果你的两个线程同时遇到同一个计数器, get()将返回相同的值(比如2),并且它们都将插入 2 + 1 = 3 。因此,令牌的数量将被低估 - 即3而不是4。

为了保持一致,您需要在 get()操作之前进行同步,这将大大降低多线程的优势。

如果您愿意,请按照以下方式执行此操作:

class Key {
   char[] buffer = new char[8];
   Key copy() {
       Key copy = new Key();
       for ( int i =0; i < 8; i++) {
          copy.buffer[i] = this.buffer[i];        
       }
   }
   public int hashCode() {
      return Arrays.hashCode(buffer);
   }
   public boolean equals(Object obj) {
      if ( obj instanceof Key) {
        return Arrays.equals(((Key) obj).buffer, this.buffer); 
      }
      return false;
   }
}
//YOur code modified:
Key checker = new Key();
for (i = 0; i < newbyte.length; i++) { //newbyte is a char[] from main
    if (newbyte[i] != ' ') {
        checker.buffer[counter] = newbyte[i];
        counter++;
    }
    else { 
            synchronized (wordCountMap) {
               Integer value = workCountMap.get(checker);
               if ( value == null ) {
                  workCountMap.put(checker.copy(), 1);    
               } else {
                  wordCountMap.put(checker.copy(), value + 1);
               }
            }
        for (j = 0; j < counter; j++) {
            checker.buffer[j] = null;
        }//Null out the array
   }

这将解决您的内存问题,因为只有在必须插入表格时才执行 new()(通过 copy())。因此,使用的内存是您需要的最小值(不包括i,j,检查器等)。但是,你几乎失去了所有并行性。

如果我是你,我会将文件分解为多个片段,并在一个单独的线程中处理每个片段。每个线程都可以维护自己的hashmap。在整个文件的末尾,您将有n个哈希表(n是线程数)。然后,您可以合并n hashmap。所需的内存将是 n 乘以前一个hashmap的大小。

如果您想了解更多有关此方法的详细信息,请与我联系。我将尽力提供帮助。

答案 1 :(得分:0)

当您使用数组作为键时,它是对数组本身的引用,而不是内容。因此,更改内容不会在地图中创建更多条目,它只是继续更新相同的值。考虑一下简单的程序:

public static void main(String[] args) throws Exception {
    Character[] charArray = new Character[8];
    charArray[1] = 'A';
    Set<Character[]> set = new HashSet<Character[]>();
    set.add(charArray);
    charArray[1] = 'B';
    System.out.println(set.contains(charArray));
}

输出为true,因为charArray仍然是相同的数组,其内容未被考虑。

如果您希望以后能够在最后获取内容,例如:

for (Entry<Character [], Integer> e : wordCountMap.entrySet()) {
    System.out.println(Arrays.toString(e.getKey()) + " = " + e.getValue());
}

你必须把它放在某个地方!如果内存太大,则需要分配更多内存或使用某种外部存储。也许键入字符串的MD5上的地图,并在MD5->原始字符串的磁盘上保留一个NoSQL数据库,以便以后可以将它们取回?
在您的代码中,您在去的时候删除了数据,但预计它最终仍会存在!

答案 2 :(得分:0)

我认为不需要使用Character[]作为映射键,而是需要定义自己的类来表示8个字符的数组(*)。您需要在该课程中重新定义equals()hashCode();定义equals(),以便在所有8个字符相同时返回true,并将hashCode()定义为取决于这8个字符的某个值。您无法为数组重新定义equals()hashCode();这就是为什么你需要定义自己的类。该课程将在内部使用char[]Character[]

该类还应具有某种copyclone方法或复制构造函数,以便您可以创建一个新对象,其数据(8个字符)与现有对象。

现在,而不是这个:

check = wordCountMap.putIfAbsent(charArray, 1);
if (check != null) { 
    wordCountMap.put(charArray, wordCountMap.get(charArray) + 1);
}

在将键放入地图时,您需要确保使用副本。如上所述使用putIfAbsent会在地图中引用您的局部变量,这是错误的,因为您的局部变量可能会发生变化。这也是错误的:

check = wordCountMap.putIfAbsent(new CharArray(charArray), 1);

其中new CharArray(charArray)制作现有数组的副本 - 这就是&#34;复制构造函数&#34;的含义。 (我假设CharArray是您给新课程的名称。)这是错误的,因为在您不需要新对象的情况下,您将创建新对象,你试图避免。所以可能像

Integer existing = wordCountMap.get(charArray);
if (existing == null) {
    wordCountMap.put(new CharArray(charArray), 1);
} else {
    wordCountMap.put(charArray, existing + 1);
}

这应该只在需要时创建一个新的CharArray,并且它不会在地图中添加您计划不断更改的CharArray的引用。您可能需要在上面添加一些锁定以防止竞争条件。

(*)再次查看您的帖子后,我不确定8个字符的数组是否是您真正想要的,但您确实在代码中说new Character[8]。但是,该技术应适用于任何缓冲区大小。您可以设置您的类,以便可变的实例具有更大的缓冲区,并且您在哈希映射中放置的实例仅保留所需的字符数。

答案 3 :(得分:0)

至少有三个问题:你修改了地图的键,数组的hashCode是基于引用的,并且这里有竞争条件:

check = wordCountMap.putIfAbsent(charArray, 1);
if (check != null) { 
    wordCountMap.put(charArray, wordCountMap.get(charArray) + 1);
}

其他答案解决了哈希问题,因此我将解决竞争状况。 putIfAbsent是原子的,但put(increment(get()))不是。您可以使用AtomicInteger而不是普通的Integer来解决此问题:

AtomicInteger check = wordCountMap.putIfAbsent(key.copy(), new AtomicInteger(1));
if (check != null) { 
    check.incrementAndGet();
}

这里有密钥和值的分配,但如果密钥已经存在,它们将很容易被垃圾收集。如果你想避免它们,你可能会产生额外的get()开销,或者你可以使用@ Chip的答案中的其他建议之一。