/**
* Copyright 2013 JogAmp Community. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* The views and conclusions contained in the software and documentation are those of the
* authors and should not be interpreted as representing official policies, either expressed
* or implied, of JogAmp Community.
*/
package com.jogamp.common.util;
import java.io.PrintStream;
import java.lang.reflect.Array;
/**
* Simple implementation of {@link Ringbuffer},
* exposing <i>lock-free</i>
* {@link #get() get*(..)} and {@link #put(Object) put*(..)} methods.
* <p>
* Implementation utilizes the <i>Always Keep One Slot Open</i>,
* hence implementation maintains an internal array of <code>capacity</code> <i>plus one</i>!
* </p>
* <p>
* Implementation is thread safe if:
* <ul>
* <li>{@link #get() get*(..)} operations are performed from one thread only.</li>
* <li>{@link #put(Object) put*(..)} operations are performed from one thread only.</li>
* <li>{@link #get() get*(..)} and {@link #put(Object) put*(..)} thread may be the same.</li>
* </ul>
* </p>
* <p>
* Following methods utilize global synchronization:
* <ul>
* <li>{@link #resetFull(Object[])}</li>
* <li>{@link #clear()}</li>
* <li>{@link #growEmptyBuffer(Object[])}</li>
* </ul>
* User needs to synchronize above methods w/ the lock-free
* w/ {@link #get() get*(..)} and {@link #put(Object) put*(..)} methods,
* e.g. by controlling their threads before invoking the above.
* </p>
* <p>
* Characteristics:
* <ul>
* <li>Read position points to the last read element.</li>
* <li>Write position points to the last written element.</li>
* </ul>
* <table border="1">
* <tr><td>Empty</td><td>writePos == readPos</td><td>size == 0</td></tr>
* <tr><td>Full</td><td>writePos == readPos - 1</td><td>size == capacity</td></tr>
* </table>
* </p>
*/
public class LFRingbuffer<T> implements Ringbuffer<T> {
private final Object syncRead = new Object();
private final Object syncWrite = new Object();
private final Object syncGlobal = new Object();
private /* final */ volatile T[] array; // not final due to grow
private /* final */ volatile int capacityPlusOne; // not final due to grow
private volatile int readPos;
private volatile int writePos;
private volatile int size;
@Override
public final String toString() {
return "LFRingbuffer<?>[filled "+size+" / "+(capacityPlusOne-1)+", writePos "+writePos+", readPos "+readPos+"]";
}
@Override
public final void dump(final PrintStream stream, final String prefix) {
stream.println(prefix+" "+toString()+" {");
for(int i=0; i<capacityPlusOne; i++) {
stream.println("\t["+i+"]: "+array[i]);
}
stream.println("}");
}
/**
* Create a full ring buffer instance w/ the given array's net capacity and content.
* <p>
* Example for a 10 element Integer array:
* <pre>
* Integer[] source = new Integer[10];
* // fill source with content ..
* Ringbuffer<Integer> rb = new LFRingbuffer<Integer>(source);
* </pre>
* </p>
* <p>
* {@link #isFull()} returns true on the newly created full ring buffer.
* </p>
* <p>
* Implementation will allocate an internal array with size of array <code>copyFrom</code> <i>plus one</i>,
* and copy all elements from array <code>copyFrom</code> into the internal array.
* </p>
* @param copyFrom mandatory source array determining ring buffer's net {@link #capacity()} and initial content.
* @throws IllegalArgumentException if <code>copyFrom</code> is <code>null</code>
*/
@SuppressWarnings("unchecked")
public LFRingbuffer(final T[] copyFrom) throws IllegalArgumentException {
capacityPlusOne = copyFrom.length + 1;
array = (T[]) newArray(copyFrom.getClass(), capacityPlusOne);
resetImpl(true, copyFrom);
}
/**
* Create an empty ring buffer instance w/ the given net <code>capacity</code>.
* <p>
* Example for a 10 element Integer array:
* <pre>
* Ringbuffer<Integer> rb = new LFRingbuffer<Integer>(10, Integer[].class);
* </pre>
* </p>
* <p>
* {@link #isEmpty()} returns true on the newly created empty ring buffer.
* </p>
* <p>
* Implementation will allocate an internal array of size <code>capacity</code> <i>plus one</i>.
* </p>
* @param arrayType the array type of the created empty internal array.
* @param capacity the initial net capacity of the ring buffer
*/
public LFRingbuffer(final Class<? extends T[]> arrayType, final int capacity) {
capacityPlusOne = capacity+1;
array = newArray(arrayType, capacityPlusOne);
resetImpl(false, null /* empty, nothing to copy */ );
}
@Override
public final int capacity() { return capacityPlusOne-1; }
@Override
public final void clear() {
synchronized ( syncGlobal ) {
resetImpl(false, null);
for(int i=0; i<capacityPlusOne; i++) {
this.array[i] = null;
}
}
}
@Override
public final void resetFull(final T[] copyFrom) throws IllegalArgumentException {
resetImpl(true, copyFrom);
}
private final void resetImpl(final boolean full, final T[] copyFrom) throws IllegalArgumentException {
synchronized ( syncGlobal ) {
if( null != copyFrom ) {
if( copyFrom.length != capacityPlusOne-1 ) {
throw new IllegalArgumentException("copyFrom array length "+copyFrom.length+" != capacity "+this);
}
System.arraycopy(copyFrom, 0, array, 0, copyFrom.length);
array[capacityPlusOne-1] = null; // null 'plus-one' field!
} else if ( full ) {
throw new IllegalArgumentException("copyFrom array is null");
}
readPos = capacityPlusOne - 1;
if( full ) {
writePos = readPos - 1;
size = capacityPlusOne - 1;
} else {
writePos = readPos;
size = 0;
}
}
}
@Override
public final int size() { return size; }
@Override
public final int getFreeSlots() { return capacityPlusOne - 1 - size; }
@Override
public final boolean isEmpty() { return 0 == size; }
@Override
public final boolean isFull() { return capacityPlusOne - 1 == size; }
/**
* {@inheritDoc}
* <p>
* Implementation advances the read position and returns the element at it, if not empty.
* </p>
*/
@Override
public final T get() {
try {
return getImpl(false, false);
} catch (final InterruptedException ie) { throw new RuntimeException(ie); }
}
/**
* {@inheritDoc}
* <p>
* Implementation advances the read position and returns the element at it, if not empty.
* </p>
*/
@Override
public final T getBlocking() throws InterruptedException {
return getImpl(true, false);
}
@Override
public final T peek() {
try {
return getImpl(false, true);
} catch (final InterruptedException ie) { throw new RuntimeException(ie); }
}
@Override
public final T peekBlocking() throws InterruptedException {
return getImpl(true, true);
}
private final T getImpl(final boolean blocking, final boolean peek) throws InterruptedException {
int localReadPos = readPos;
if( localReadPos == writePos ) {
if( blocking ) {
synchronized( syncRead ) {
while( localReadPos == writePos ) {
syncRead.wait();
}
}
} else {
return null;
}
}
localReadPos = (localReadPos + 1) % capacityPlusOne;
final T r = array[localReadPos];
if( !peek ) {
array[localReadPos] = null;
synchronized ( syncWrite ) {
size--;
readPos = localReadPos;
syncWrite.notifyAll(); // notify waiting putter
}
}
return r;
}
/**
* {@inheritDoc}
* <p>
* Implementation advances the write position and stores the given element at it, if not full.
* </p>
*/
@Override
public final boolean put(final T e) {
try {
return putImpl(e, false, false);
} catch (final InterruptedException ie) { throw new RuntimeException(ie); }
}
/**
* {@inheritDoc}
* <p>
* Implementation advances the write position and stores the given element at it, if not full.
* </p>
*/
@Override
public final void putBlocking(final T e) throws InterruptedException {
if( !putImpl(e, false, true) ) {
throw new InternalError("Blocking put failed: "+this);
}
}
/**
* {@inheritDoc}
* <p>
* Implementation advances the write position and keeps the element at it, if not full.
* </p>
*/
@Override
public final boolean putSame(final boolean blocking) throws InterruptedException {
return putImpl(null, true, blocking);
}
private final boolean putImpl(final T e, final boolean sameRef, final boolean blocking) throws InterruptedException {
int localWritePos = writePos;
localWritePos = (localWritePos + 1) % capacityPlusOne;
if( localWritePos == readPos ) {
if( blocking ) {
synchronized( syncWrite ) {
while( localWritePos == readPos ) {
syncWrite.wait();
}
}
} else {
return false;
}
}
if( !sameRef ) {
array[localWritePos] = e;
}
synchronized ( syncRead ) {
size++;
writePos = localWritePos;
syncRead.notifyAll(); // notify waiting getter
}
return true;
}
@Override
public final void waitForFreeSlots(final int count) throws InterruptedException {
synchronized ( syncRead ) {
if( capacityPlusOne - 1 - size < count ) {
while( capacityPlusOne - 1 - size < count ) {
syncRead.wait();
}
}
}
}
@Override
public final void growEmptyBuffer(final T[] newElements) throws IllegalStateException, IllegalArgumentException {
synchronized( syncGlobal ) {
if( null == newElements ) {
throw new IllegalArgumentException("newElements is null");
}
@SuppressWarnings("unchecked")
final Class<? extends T[]> arrayTypeInternal = (Class<? extends T[]>) array.getClass();
@SuppressWarnings("unchecked")
final Class<? extends T[]> arrayTypeNew = (Class<? extends T[]>) newElements.getClass();
if( arrayTypeInternal != arrayTypeNew ) {
throw new IllegalArgumentException("newElements array-type mismatch, internal "+arrayTypeInternal+", newElements "+arrayTypeNew);
}
if( 0 != size ) {
throw new IllegalStateException("Buffer is not empty: "+this);
}
if( readPos != writePos ) {
throw new InternalError("R/W pos not equal: "+this);
}
if( readPos != writePos ) {
throw new InternalError("R/W pos not equal at empty: "+this);
}
final int growAmount = newElements.length;
final int newCapacity = capacityPlusOne + growAmount;
final T[] oldArray = array;
final T[] newArray = newArray(arrayTypeInternal, newCapacity);
// writePos == readPos
writePos += growAmount; // warp writePos to the end of the new data location
if( readPos >= 0 ) {
System.arraycopy(oldArray, 0, newArray, 0, readPos+1);
}
if( growAmount > 0 ) {
System.arraycopy(newElements, 0, newArray, readPos+1, growAmount);
}
final int tail = capacityPlusOne-1-readPos;
if( tail > 0 ) {
System.arraycopy(oldArray, readPos+1, newArray, writePos+1, tail);
}
size = growAmount;
capacityPlusOne = newCapacity;
array = newArray;
}
}
@Override
public final void growFullBuffer(final int growAmount) throws IllegalStateException, IllegalArgumentException {
synchronized ( syncGlobal ) {
if( 0 > growAmount ) {
throw new IllegalArgumentException("amount "+growAmount+" < 0 ");
}
if( capacityPlusOne-1 != size ) {
throw new IllegalStateException("Buffer is not full: "+this);
}
final int wp1 = ( writePos + 1 ) % capacityPlusOne;
if( wp1 != readPos ) {
throw new InternalError("R != W+1 pos at full: "+this);
}
@SuppressWarnings("unchecked")
final Class<? extends T[]> arrayTypeInternal = (Class<? extends T[]>) array.getClass();
final int newCapacity = capacityPlusOne + growAmount;
final T[] oldArray = array;
final T[] newArray = newArray(arrayTypeInternal, newCapacity);
// writePos == readPos - 1
readPos = ( writePos + 1 + growAmount ) % newCapacity; // warp readPos to the end of the new data location
if(writePos >= 0) {
System.arraycopy(oldArray, 0, newArray, 0, writePos+1);
}
final int tail = capacityPlusOne-1-writePos;
if( tail > 0 ) {
System.arraycopy(oldArray, writePos+1, newArray, readPos, tail);
}
capacityPlusOne = newCapacity;
array = newArray;
}
}
@SuppressWarnings("unchecked")
private static <T> T[] newArray(final Class<? extends T[]> arrayType, final int length) {
return ((Object)arrayType == (Object)Object[].class)
? (T[]) new Object[length]
: (T[]) Array.newInstance(arrayType.getComponentType(), length);
}
}