Monday, January 11, 2010

After load testing, the caching optimization still had performance issues, so a synchronized HashMap.get got turned into an unsynchronized HashMap.get.

The code becomes something like

for (;;) {
Object entry = null;
// cache is a jdk1.4 HashMap, so if there is a concurrent modification,
// the worst can happen is either an erroneous null is returned or an
// ArrayIndexOutOfBoundsException is thrown. This HashMap.get is not
// synchronized for performance reasons, as revealed in load tests.
//
// In general, a concurrent modification could result a HashMap.get
// to return a (very recently) stale value, but that is not a concern
// in this case, since the only stale value could be an expired lock
// object, which will result in retrying the HashMap.get.
//
// The ArrayIndexOutOfBoundsException is not likely to ever be thrown,
// but the HashMap.remove in the finally block could theoretically
// cause it. The catch block causes the ArrayIndexOutOfBoundsException
// to be the same as a potentially erroneous null.
//
// The erroneous null is not a problem, because if null is returned,
// because the subsequent HashMap.get is synchronized.
try {
entry = cache.get(key);
} catch (ArrayIndexOutOfBoundsException e) {
}
if (entry == null) {
Object lock = new Object();
synchronized (lock) {
try {
synchronized (cache) {
if (cache.get(key) != null) {
lock = null;
continue;
}
cache.put(key, lock);
}
CacheData data = computeData(key);
synchronized (cache) {
cache.put(key, data);
}
lock = null;
return data;
} finally {
if (lock != null) {
synchronized (cache) {
cache.remove(key);
}
}
}
}
} else if (entry instanceof CacheData) {
return (CacheData) entry;
} else {
synchronized (entry) {}
}
}

I hope that some big fat comment like the one I wrote above about why the HashMap.get is unsynchronized and why it should still work correctly gets put in the actual code.

I manually decompiled the jdk1.4 HashMap.get to investigate the consequences of the unsynchronized HashMap.get. In this case, the modifications to the HashMap are adding a key and a lock object, replacing a lock object with a value, and (only if there errors) removing a key that maps to a lock object. The keys are Strings, so stupid code (in computeData(), for example) that mutates the keys is not a concern. The CacheData is also immutable, and is actually a String, but that's irrelevant.

static Object maskNull(Object a0) {
return a0 == null ? NULL_KEY : a0;
}

static int hash(Object a0) {
int i1 = a0.hash();
i1 += (i1 << 9) ^ -1;
i1 ^= i1 >>> 14;
i1 += i1 << 4;
i1 ^= i1 >>> 10;
return i1;
}

static int indexFor(int i0, int i1) {
return i0 & (i1 - 1);
}

static boolean eq(Object a0, Object a1) {
return a0 == a1 ? true : a0.equals(a1) ? true : false;
}

public Object get(Object a1) {
Object a2 = maskNull(a1);
int i3 = hash(a2);
int i4 = indexFor(i3, this.table.length);
HashMap.Entry a5 = this.table[i4];
for (;;) {
if (a5 == null)
return a5;
if (a5.hash == i3 && eq(a2, a5.key))
return a5.value;
a5 = a5.next;
}
}

No comments:

Post a Comment