trix
trix

Reputation: 1835

Can a Java HashMap's size() be out of sync with its actual entries' size?

I have a Java HashMap called statusCountMap.
Calling size() results in 30.
But if I count the entries manually, it's 31
This is in one of my TestNG unit tests. These results below are from Eclipse's Display window (type code -> highlight -> hit Display Result of Evaluating Selected Text).

statusCountMap.size()
     (int) 30
statusCountMap.keySet().size()
     (int) 30
statusCountMap.values().size()
     (int) 30
statusCountMap
     (java.util.HashMap) {40534-INACTIVE=2, 40526-INACTIVE=1, 40528-INACTIVE=1, 40492-INACTIVE=3, 40492-TOTAL=4, 40513-TOTAL=6, 40532-DRAFT=4, 40524-TOTAL=7, 40526-DRAFT=2, 40528-ACTIVE=1, 40524-DRAFT=2, 40515-ACTIVE=1, 40513-DRAFT=4, 40534-DRAFT=1, 40514-TOTAL=3, 40529-DRAFT=4, 40515-TOTAL=3, 40492-ACTIVE=1, 40528-TOTAL=4, 40514-DRAFT=2, 40526-TOTAL=3, 40524-INACTIVE=2, 40515-DRAFT=2, 40514-ACTIVE=1, 40534-TOTAL=3, 40513-ACTIVE=2, 40528-DRAFT=2, 40532-TOTAL=4, 40524-ACTIVE=3, 40529-ACTIVE=1, 40529-TOTAL=5}
statusCountMap.entrySet().size()
     (int) 30

What gives ? Anyone has experienced this ?
I'm pretty sure statusCountMap is not being modified at this point.
There are 2 methods (lets call them methodA and methodB) that modify statusCountMap concurrently, by repeatedly calling incrementCountInMap.

private void incrementCountInMap(Map map, Long id, String qualifier) {
    String key = id + "-" + qualifier;
    if (map.get(key) == null) {
        map.put(key, 0);
    }
    synchronized (map) {
        map.put(key, map.get(key).intValue() + 1);
    }
}

methodD is where I'm getting the issue. methodD has a TestNG @dependsOnMethods = { "methodA", "methodB" } so when methodD is executing, statusCountMap is pretty much static already. I'm mentioning this because it might be a bug in TestNG.
I'm using Sun JDK 1.6.0_24. TestNG is testng-5.9-jdk15.jar

Hmmm ... after rereading my post, could it be because of concurrent execution of outside-of-synchronized-block map.get(key) == null & map.put(key,0) that's causing this issue ?

Upvotes: 8

Views: 8528

Answers (4)

nsfyn55
nsfyn55

Reputation: 15363

If you using the default initial capacity of 16 and accessing them map in a non thread safe manner your ripe for an inconsistent state. Size is a state member in the Map getting updated as each item is entered(size++). This is because the map itself is an array of linked lists and cannot really return its actual size because its not indicative of the number of items it contains. Once the Map reaches a percentage(load_factor) of the intial capacity it has to resize itself to accomodate more items. If a rogue thread is attempting to add items as the map is resizing who knows what state the map will be in.

Upvotes: 4

Stephen C
Stephen C

Reputation: 718788

Hmmm ... after rereading my post, could it be because of concurrent execution of outside-of-synchronized-block map.get(key) == null & map.put(key,0) that's causing this issue ?

In a word ... yes.

HashMap is not thread-safe. Therefore, if there is any point where two threads could update a HashMap without proper synchronization, the map could get into an inconsistent state. And even if one of the threads is only reading, that thread could see an inconsistent state for the map.

The correct way to write that method is:

private void incrementCountInMap(Map map, Long id, String qualifier) {
    String key = id + "-" + qualifier;
    synchronized (map) {
        Integer count = map.get(key);
        map.put(key, count == null ? 1 : count + 1);
    }
}

Upvotes: 4

Thomas Mueller
Thomas Mueller

Reputation: 50097

The problem that the first map.put(..) isn't synchronized. Either synchronize it, or use Collections.synchronizedMap(..). Test case:

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;

public class Test {
    public static void main(String... args) throws InterruptedException {
        final Random random = new Random();
        final int max = 10;
        for (int j = 0; j < 100000; j++) {
            // final Map<String, Integer> map = Collections.synchronizedMap(new HashMap<String, Integer>());
            final HashMap<String, Integer> map = new HashMap<String, Integer>();
            Thread t = new Thread() {
                public void run() {
                    for (int i = 0; i < 100; i++) {
                        incrementCountInMap(map, random.nextInt(max));
                    }
                }
            };
            t.start();
            for (int i = 0; i < 100; i++) {
                incrementCountInMap(map, random.nextInt(max));
            }
            t.join();
            if (map.size() != max) {
                System.out.println("size: " + map.size() + " entries: " + map);
            }
        }
    }
    static void incrementCountInMap(Map<String, Integer> map, int id) {
        String key = "k" + id;
        if (map.get(key) == null) {
            map.put(key, 0);
        }
        synchronized (map) {
            map.put(key, map.get(key).intValue() + 1);
        }
    }

}

Some results I get:

size: 11 entries: {k3=24, k4=20, k5=16, k6=30, k7=16, k8=18, k9=11, k0=18, k1=16, k1=13, k2=18}
size: 11 entries: {k3=18, k4=19, k5=21, k6=20, k7=18, k8=26, k9=20, k0=16, k1=25, k2=15}
size: 11 entries: {k3=25, k4=20, k5=27, k6=15, k7=17, k8=17, k9=24, k0=21, k1=16, k1=1, k2=17}
size: 11 entries: {k3=13, k4=21, k5=18, k6=21, k7=13, k8=17, k9=25, k0=20, k1=23, k2=28}
size: 11 entries: {k3=21, k4=25, k5=19, k6=12, k7=17, k8=14, k9=23, k0=24, k1=26, k2=18}
size: 9 entries: {k3=13, k4=17, k5=23, k6=24, k7=18, k8=19, k9=28, k0=21, k1=17, k2=20}
size: 9 entries: {k3=15, k4=24, k5=21, k6=18, k7=21, k8=30, k9=20, k0=17, k1=15, k2=19}
size: 11 entries: {k3=15, k4=13, k5=21, k6=21, k7=15, k8=19, k9=23, k0=30, k1=15, k2=27}
size: 11 entries: {k3=29, k4=15, k5=19, k6=19, k7=15, k8=23, k9=14, k0=31, k1=18, k2=12}
size: 11 entries: {k3=17, k4=18, k5=20, k6=11, k6=13, k7=20, k8=22, k9=30, k0=12, k1=21, k2=16}

Upvotes: 3

Peter Lawrey
Peter Lawrey

Reputation: 533500

I believe you can achieve this if you modify a key after it is added to a HashMap.

However in your case it appears to be just a case of modifying the same map in two threads without proper synchronization. e.g. in thread A, map.put(key, 0), thread B map.put(key2, 0) can results in a size of 1 or 2. If you do the same with remove you can end up with a size larger than you should.

Upvotes: 12

Related Questions