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!