SafeArrayList not so safe?

How is SafeArrayList actually safe, in which way?

Need some parallel execution to trigger this.
Here is a piece of code that triggers some exception:

import com.jme3.app.*;
import com.jme3.scene.*;
import com.jme3.system.*;
import java.util.concurrent.*;

public class ManyNodes extends SimpleApplication {
  public static void main(String[] args) {
    new ManyNodes().start(JmeContext.Type.Headless);
  }

  final ExecutorService POOL = Executors.newWorkStealingPool();

  @Override
  public void simpleInitApp() {
    // 10k happens sporadically
    // 100k always often/always
    for (int i = 0; i < 100_000; i++) {
      var n = new Node();
      POOL.submit(() -> rootNode.attachChild(n));
    }
  }
}
java.lang.NullPointerException: Cannot invoke "com.jme3.scene.Spatial.requiresUpdates()" because "child" is null
	at com.jme3.scene.Node.addUpdateChildren(Node.java:188)
	at com.jme3.scene.Node.getUpdateList(Node.java:221)
	at com.jme3.scene.Node.updateLogicalState(Node.java:238)
	at com.jme3.app.SimpleApplication.update(SimpleApplication.java:265)
	at com.jme3.system.NullContext.run(NullContext.java:149)
	at java.base/java.lang.Thread.run(Thread.java:1589)

Engine version: 3.6.0-beta1

Let me load the javadoc for you:

Provides a list with similar modification semantics to java.util.concurrent’s CopyOnWriteArrayList except that it is not concurrent and also provides direct access to the current array. This List allows modification of the contents while iterating as any iterators will be looking at a snapshot of the list at the time they were created. Similarly, access the raw internal array is only presenting a snap shot and so can be safely iterated while the list is changing.

All modifications, including set() operations will cause a copy of the data to be created that replaces the old version. Because this list is not designed for threading concurrency it further optimizes the “many modifications” case by buffering them as a normal ArrayList until the next time the contents are accessed.

Normal list modification performance should be equal to ArrayList in a many situations and always better than CopyOnWriteArrayList. Optimum usage is when modifications are done infrequently or in batches… as is often the case in a scene graph. Read operations perform superior to all other methods as the array can be accessed directly.

Important caveats over normal java.util.Lists:

Even though this class supports modifying the list, the subList() method returns a read-only list. This technically breaks the List contract.

The ListIterators returned by this class only support the remove() modification method. add() and set() are not supported on the iterator. Even after ListIterator.remove() or Iterator.remove() is called, this change is not reflected in the iterator instance as it is still referring to its original snapshot.

Because it is safe to modify the list while iterating over it. For example, in JME you can remove/detach a child node while iterating over the list of children. Or a listener in a SafeArrayList can remove itself from the list as part of handling an event.

…but again, I think the javadoc spells all of this out pretty clearly.

3 Likes

Your question is about SafeArrayList and that was already answered.

But as a side note or warning… You can’t attach stuff to the scene graph outside the render thread. You’ll also get errors at some point. This is what I understand your example is implying to do. You need to funnel the scene graph modifications from different threads to application.enqueue to be added on the next frame from the render thread.

1 Like