The code is shared as a public project at Github. Fell free to share or fork it.
The idea is simple. All returned objects (which need to be cached) are stored in a stack, which is held in an AtomicReferenceArray instance and an AtomicInteger-typed variable stores the top position of the stack. allocation or freeing of an object contains two steps:
- Firstly, a change of the top variable needs to be reserved,
- Then we try get/put the element in the reserved position,
- Since some other thread may also reserve the same place because the stack could expand and shrink, we may have to start over if this happens, i.e. Step 2 failed.
Step 1 utilizes the method of compareAndSet of AtomicInteger in a loop and step 2 uses getAndSet/compareAndSet of AtomicReferenceArray. Both are in a lock-free style.
This framework is also known as an optimisitic locking. It is useful for cases when contentions are relatively rare events.
Here are some code fragments:
public E alloc() { while (true) { // Try reserve a cached object in objects int n; do { n = top.get(); if (n == 0) { // No cached oobjects, allocate a new one return factory.alloc(); } } while (!top.compareAndSet(n, n - 1)); // Try fetch the cached object E e = objects.getAndSet(n, null); if (e != null) { return e; } // It is possible that the reserved object was extracted before // the current thread tried to get it. Let's start over again. } } public void free(E e) { while (true) { // Try reserve a place in this.objects for e. int n; do { n = top.get(); if (n == objects.length()) { // the pool is full, e is not cached. factory.free(e); } } while (!top.compareAndSet(n, n + 1)); // Try put e at the reserved place. if (objects.compareAndSet(n + 1, null, e)) { return; } // It is possible that the reserved place was occupied before // the current thread tried to put e in it. Let's start over again. } }
Any comments are welcome!