Spooky nulls in an ArrayList

I had a crash reported for one of my Android applications. Thankfully that includes a stack trace, because I would never have found this bug without it. I learned something new about concurrently accessing an ArrayList.


What was the error?
java.lang.NullPointerException
How embarrassing.

while(mPendingPath.size() >= 3) {
    Vec3 a = mPendingPath.get(0);
    Vec3 b = mPendingPath.get(1);
    Vec3 c = mPendingPath.get(2);
    ...
    mSections.addAll(generateCurved(a, b, c));
    ...
    mPendingPath.remove(0);
}

generateCurved threw the exception because argument “c” was null. Really? I put a null in a list? How embarrassing. Let’s see, where does that list get added-to…

mPendingPath.add(mMaze.resolvePosition(next));

Just this one place. resolvePosition has multiple implementations, but they all return directly from new, like so:

return new Vec3(p.x, p.y, p.z).scale(mSpacing).add(mSpatialOrigin);

new is returning null? Can’t be, it’s not allowed to. Even if it did return null, how did that get past the calls to scale and add? Those return this, so there’s no way this expression comes out null. What the hell?

Whatever’s going on, surely it hinges on there being multiple threads accessing this list, but usually that kind of thing throws ConcurrentModificationException when it’s not done correctly. ConcurrentModificationException is not a good litmus test, though, as it actually has little to do with multithreading. You can get it thrown with a single thread; all you have to do is get an iterator, modify the collection, and then try to use the iterator. This will do the trick:

for(Object o : objectList) {
    objectList.clear()
}

The iterator is the essential ingredient, here. The broken code is not using iterators, so of course there is no ConcurrentModificationException, but if mPendingPath was mishandled I would expect to see an IndexOutOfBoundsException.

I recreated the problem under laboratory conditions. This reliably fails:

final ArrayList list = new ArrayList();

timer_a.schedule(new TimerTask() {
    public void run() {
        if(list.size() < 1000) {
            list.add(new Object());
        }
    }
}, 1, 1);

timer_b.schedule(new TimerTask() {
    public void run() {
        int n = list.size();
        for(int i = 0; i < n; i++) {
            if(list.get(i) == null) {
                fail();
            }
        }

        if(n > 0) {
            list.remove(0);
        }
    }
}, 1, 1);

An ArrayList is, underneath it all, an array. Obviously. That array sometimes needs to grow larger, so ArrayList will allocate a new, larger array and then copy the contents of the old array. Is that it? Am I catching it in the middle of copying to a new array? Nope, adding a call to ensureCapacity beforehand doesn’t fix it. I’m removing the first element, so am I catching it in the middle of shifting the other elements down? Nope, removing the last element instead doesn’t fix it, and synchronizing the add with the null search doesn’t fix it, either.

So ArrayList is explicitly writing nulls when elements are removed? Those elements fall out of the list’s range, so why bother?

Because of garbage collection.

ArrayList.remove looks like this:

public E remove(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    modCount++;
    E oldValue = (E) elementData[index];
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
    elementData[--size] = null; // clear to let GC do its work
    return oldValue;
}

Damnit, ArrayList is explicitly writing nulls. I didn’t synchronize add with remove because rare off-by-one errors would be relatively harmless in my application. I did not expect magically-appearing nulls. So I’ve synchronized them.

I’m still not totally sold on garbage collection. You aren’t required to handle deallocation, but on the other hand you don’t get to control deallocation, and you do still have to worry about memory management details and leaks and such. If I have to set references to null for GC to work, I might as well just free the memory myself.

This entry was posted in Uncategorized and tagged . Bookmark the permalink.