Je veux une classe simple qui implémente un tampon circulaire de taille fixe. Il devrait être efficace, facile à regarder, génériquement typé.
EDIT: Il n’est pas nécessaire que ce soit compatible avec la technologie de traduction, pour le moment. Je peux toujours ajouter un verrou plus tard, il ne s'agira en aucun cas d'une concurrence élevée.
Les méthodes devraient être: .Add et je suppose .List, où je récupère toutes les entrées. À la réflexion, la récupération devrait être effectuée via un indexeur. A tout moment, je souhaiterai pouvoir récupérer n'importe quel élément du tampon par index. Mais gardez à l'esprit que, d'un moment à l'autre, l'élément [n] peut être différent, car la mémoire tampon circulaire se remplit et se renverse.
Ce n'est pas une pile, c'est un tampon circulaire. En ce qui concerne le "débordement": je m'attendrais en interne à un tableau contenant les éléments et, avec le temps, la tête et la queue du tampon pivoteront autour de ce tableau fixe. Mais cela devrait être invisible pour l'utilisateur. Il ne devrait y avoir aucun événement ou comportement de "débordement" détectable de manière externe.
Ce n'est pas un devoir d'école - il est généralement utilisé pour un cache MRU ou une transaction ou un journal d'événements de taille fixe.
J'utiliserais un tableau de T, un pointeur de tête et de queue, et ajouterais des méthodes.
Comme: (la recherche de bogues est laissée à l'utilisateur)
// Hijack these for simplicity
import Java.nio.BufferOverflowException;
import Java.nio.BufferUnderflowException;
public class CircularBuffer<T> {
private T[] buffer;
private int tail;
private int head;
@SuppressWarnings("unchecked")
public CircularBuffer(int n) {
buffer = (T[]) new Object[n];
tail = 0;
head = 0;
}
public void add(T toAdd) {
if (head != (tail - 1)) {
buffer[head++] = toAdd;
} else {
throw new BufferOverflowException();
}
head = head % buffer.length;
}
public T get() {
T t = null;
int adjTail = tail > head ? tail - buffer.length : tail;
if (adjTail < head) {
t = (T) buffer[tail++];
tail = tail % buffer.length;
} else {
throw new BufferUnderflowException();
}
return t;
}
public String toString() {
return "CircularBuffer(size=" + buffer.length + ", head=" + head + ", tail=" + tail + ")";
}
public static void main(String[] args) {
CircularBuffer<String> b = new CircularBuffer<String>(3);
for (int i = 0; i < 10; i++) {
System.out.println("Start: " + b);
b.add("One");
System.out.println("One: " + b);
b.add("Two");
System.out.println("Two: " + b);
System.out.println("Got '" + b.get() + "', now " + b);
b.add("Three");
System.out.println("Three: " + b);
// Test Overflow
// b.add("Four");
// System.out.println("Four: " + b);
System.out.println("Got '" + b.get() + "', now " + b);
System.out.println("Got '" + b.get() + "', now " + b);
// Test Underflow
// System.out.println("Got '" + b.get() + "', now " + b);
// Back to start, let's shift on one
b.add("Foo");
b.get();
}
}
}
C'est ainsi que je (ou ai) écris un tampon circulaire efficace en Java. Il est soutenu par un tableau simple. Pour mon cas d'utilisation particulier, j'avais besoin d'un débit simultané élevé. J'ai donc utilisé CAS pour l'attribution de l'index. J'ai ensuite créé des mécanismes pour des copies fiables, notamment une copie CAS de l'intégralité de la mémoire tampon. Je l'ai utilisé dans un cas qui est décrit plus en détail dans court article .
import Java.util.concurrent.atomic.AtomicLong;
import Java.lang.reflect.Array;
/**
* A circular array buffer with a copy-and-swap cursor.
*
* <p>This class provides an list of T objects who's size is <em>unstable</em>.
* It's intended for capturing data where the frequency of sampling greatly
* outweighs the frequency of inspection (for instance, monitoring).</p>
*
* <p>This object keeps in memory a fixed size buffer which is used for
* capturing objects. It copies the objects to a snapshot array which may be
* worked with. The size of the snapshot array will vary based on the
* stability of the array during the copy operation.</p>
*
* <p>Adding buffer to the buffer is <em>O(1)</em>, and lockless. Taking a
* stable copy of the sample is <em>O(n)</em>.</p>
*/
public class ConcurrentCircularBuffer <T> {
private final AtomicLong cursor = new AtomicLong();
private final T[] buffer;
private final Class<T> type;
/**
* Create a new concurrent circular buffer.
*
* @param type The type of the array. This is captured for the same reason
* it's required by {@link Java.util.List.toArray()}.
*
* @param bufferSize The size of the buffer.
*
* @throws IllegalArgumentException if the bufferSize is a non-positive
* value.
*/
public ConcurrentCircularBuffer (final Class <T> type,
final int bufferSize)
{
if (bufferSize < 1) {
throw new IllegalArgumentException(
"Buffer size must be a positive value"
);
}
this.type = type;
this.buffer = (T[]) new Object [ bufferSize ];
}
/**
* Add a new object to this buffer.
*
* <p>Add a new object to the cursor-point of the buffer.</p>
*
* @param sample The object to add.
*/
public void add (T sample) {
buffer[(int) (cursor.getAndIncrement() % buffer.length)] = sample;
}
/**
* Return a stable snapshot of the buffer.
*
* <p>Capture a stable snapshot of the buffer as an array. The snapshot
* may not be the same length as the buffer, any objects which were
* unstable during the copy will be factored out.</p>
*
* @return An array snapshot of the buffer.
*/
public T[] snapshot () {
T[] snapshots = (T[]) new Object [ buffer.length ];
/* Determine the size of the snapshot by the number of affected
* records. Trim the size of the snapshot by the number of records
* which are considered to be unstable during the copy (the amount the
* cursor may have moved while the copy took place).
*
* If the cursor eliminated the sample (if the sample size is so small
* compared to the rate of mutation that it did a full-wrap during the
* copy) then just treat the buffer as though the cursor is
* buffer.length - 1 and it was not changed during copy (this is
* unlikley, but it should typically provide fairly stable results).
*/
long before = cursor.get();
/* If the cursor hasn't yet moved, skip the copying and simply return a
* zero-length array.
*/
if (before == 0) {
return (T[]) Array.newInstance(type, 0);
}
System.arraycopy(buffer, 0, snapshots, 0, buffer.length);
long after = cursor.get();
int size = buffer.length - (int) (after - before);
long snapshotCursor = before - 1;
/* Highly unlikely, but the entire buffer was replaced while we
* waited...so just return a zero length array, since we can't get a
* stable snapshot...
*/
if (size <= 0) {
return (T[]) Array.newInstance(type, 0);
}
long start = snapshotCursor - (size - 1);
long end = snapshotCursor;
if (snapshotCursor < snapshots.length) {
size = (int) snapshotCursor + 1;
start = 0;
}
/* Copy the sample snapshot to a new array the size of our stable
* snapshot area.
*/
T[] result = (T[]) Array.newInstance(type, size);
int startOfCopy = (int) (start % snapshots.length);
int endOfCopy = (int) (end % snapshots.length);
/* If the buffer space wraps the physical end of the array, use two
* copies to construct the new array.
*/
if (startOfCopy > endOfCopy) {
System.arraycopy(snapshots, startOfCopy,
result, 0,
snapshots.length - startOfCopy);
System.arraycopy(snapshots, 0,
result, (snapshots.length - startOfCopy),
endOfCopy + 1);
}
else {
/* Otherwise it's a single continuous segment, copy the whole thing
* into the result.
*/
System.arraycopy(snapshots, startOfCopy,
result, 0, endOfCopy - startOfCopy + 1);
}
return (T[]) result;
}
/**
* Get a stable snapshot of the complete buffer.
*
* <p>This operation fetches a snapshot of the buffer using the algorithm
* defined in {@link snapshot()}. If there was concurrent modification of
* the buffer during the copy, however, it will retry until a full stable
* snapshot of the buffer was acquired.</p>
*
* <p><em>Note, for very busy buffers on large symmetric multiprocessing
* machines and supercomputers running data processing intensive
* applications, this operation has the potential of being fairly
* expensive. In practice on commodity hardware, dualcore processors and
* non-processing intensive systems (such as web services) it very rarely
* retries.</em></p>
*
* @return A full copy of the internal buffer.
*/
public T[] completeSnapshot () {
T[] snapshot = snapshot();
/* Try again until we get a snapshot that's the same size as the
* buffer... This is very often a single iteration, but it depends on
* how busy the system is.
*/
while (snapshot.length != buffer.length) {
snapshot = snapshot();
}
return snapshot;
}
/**
* The size of this buffer.
*/
public int size () {
return buffer.length;
}
}
Voici une implémentation CircularArrayList prête à l’emploi pour Java que j’utilise dans le code de production. En remplaçant AbstractList de la manière recommandée par Java, il prend en charge toutes les fonctionnalités attendues d'une implémentation List standard dans Java Collections Framework (type d'élément générique, sous-liste, itération, etc.).
Les appels suivants se terminent en O (1):
J'utiliserais/ ArrayBlockingQueue ou l'une des autres implémentations pré-construites de la file d'attente, en fonction des besoins. Très rarement, il est nécessaire de mettre en place une telle structure de données vous-même (à moins qu'il s'agisse d'un devoir scolaire).
EDIT: Maintenant que vous avez ajouté l’obligation "de récupérer tout élément du tampon par index", je suppose que vous devez implémenter votre propre classe (à moins que - google-collections ou une autre bibliothèque en fournit une). Un tampon circulaire est assez facile à mettre en œuvre, comme le montre l'exemple de JeeBee. Vous pouvez également consulter le code source d'ArrayBlockingQueue - son code est relativement propre, il suffit de supprimer les méthodes de verrouillage et inutiles et d'ajouter des méthodes pour y accéder par index.
Utiliser Java/ ArrayDeque
Il suffit d'utiliser l'implémentation de quelqu'un d'autre:
Le Power CollectionsDeque<T>
est implémenté par un tampon circulaire.
La bibliothèque de collections d’alimentation est fragmentée mais le Deque est un tampon circulaire en expansion parfaitement acceptable.
Puisque vous indiquez que vous ne voulez pas d’expansion et que vous souhaitez remplacer, vous pouvez assez facilement modifier le code pour le remplacer. Cela impliquerait simplement de supprimer la vérification des pointeurs étant logiquement adjacents et ne faisant qu’écrire. Dans le même temps, le tampon privé peut être créé en lecture seule.
Je veux répondre à cette question dans la perspective Java.
Pour implémenter un tampon circulaire avec Java, vous avez probablement besoin de trois choses, notamment: une classe de tampon circulaire, générique et quelques opérations dessus (pour savoir de quelles opérations vous avez besoin et le mécanisme interne de ces opérations, vous devrez peut-être lire wiki pour tampon circulaire au début).
Deuxièmement, le jugement du tampon plein ou vide doit être traité avec beaucoup de soin. Ici, je donne deux solutions instinctives pour le jugement plein/vide. Dans la première solution, vous devez créer deux variantes de nombre entier pour stocker à la fois la taille actuelle de votre tampon et la taille maximale de votre tampon. Évidemment, si la taille actuelle est égale à la taille maximale, la mémoire tampon est saturée.
Dans une autre solution, nous définissons le dernier emplacement de stockage en veille (pour le tampon circulaire de taille sept, nous définissons le stockage à sept en veille). Selon cela, nous pouvons déterminer que le tampon est plein lorsque l'expression (rp+1)%MAXSIZE == fp;
est satisfaite.
Pour plus de précisions, voici une implémentation avec le langage Java.
import Java.nio.BufferOverflowException;
import Java.nio.BufferUnderflowException;
public class CircularBuffer<T> {
private int front;
private int rear;
private int currentSize;
private int maxSize;
private T[] buffer;
public CircularBuffer(int n) {
buffer = (T[]) new Object[n];
front = 0;
rear = 0;
currentSize = 0;
maxSize = n;
}
public void Push(T e) {
if (!isFull()) {
buffer[rear] = e;
currentSize++;
rear = (rear + 1) % maxSize;
} else throw new BufferOverflowException();
}
public T pop() {
if (!isEmpty()) {
T temp = buffer[front];
buffer[front] = null;
front = (front + 1) % maxSize;
currentSize--;
return temp;
} else throw new BufferUnderflowException();
}
public T peekFirst() {
if (!isEmpty()) {
return buffer[front];
} else return null;
}
public T peekLast() {
if (!isEmpty()) {
return buffer[rear - 1];
} else return null;
}
public int size() {
return currentSize;
}
public boolean isEmpty() {
if (currentSize == 0) {
return true;
} else return false;
}
public boolean isFull() {
if (currentSize == maxSize) {
return true;
} else return false;
}
public boolean clean() {
front = 0;
rear = 0;
while (rear != 0) {
buffer[rear] = null;
rear = (rear + 1) % maxSize;
}
return true;
}
public static void main(String[] args) {
CircularBuffer<Integer> buff = new CircularBuffer<>(7);
buff.Push(0);
buff.Push(1);
buff.Push(2);
System.out.println(buff.size());
System.out.println("The head element is: " + buff.pop());
System.out.println("Size should be twoo: " + buff.size());
System.out.println("The last element is one: " + buff.peekLast());
System.out.println("Size should be two: " + buff.size());
buff.clean();
System.out.println("Size should be zero: " + buff.size());
}
}
Dans Guava 15, nous avons introduit EvictingQueue
, qui est une file d'attente limitée, non bloquante, qui supprime automatiquement (supprime) les éléments de l'en-tête de la file d'attente lors d'une tentative d'ajout d'éléments à une file d'attente complète. Cela diffère des files d'attente limitées classiques, qui bloquent ou rejettent les nouveaux éléments lorsqu'ils sont pleins.
Il semble que cela convienne à vos besoins et présente une interface beaucoup plus simple que d’utiliser une variable ArrayDeque
directement (elle en utilise cependant une sous le capot!).
Plus d'informations peuvent être trouvées ici .
System.Collections.Generic.Queue - est un simple tampon circulaire à l'intérieur (T [] avec tête et queue, comme dans exemple de JeeBee ).
si un cache lru fonctionne, envisagez simplement d’utiliser http://Java.Sun.com/javase/6/docs/api/Java/util/LinkedHashMap.html#LinkedHashMap(int,%20float,%20boolean) , http://Java.Sun.com/javase/6/docs/api/Java/util/LinkedHashMap.html#removeEldestEntry(Java.util.Map.Entry)
Voici une implémentation que j'ai codée pour mon propre usage mais qui pourrait être utile.
Le tampon contient un ensemble maximal d'éléments fixes. L'ensemble est circulaire, les anciens éléments sont automatiquement supprimés. L'appelant peut obtenir des éléments en fin de liste grâce à un index incrémentiel absolu (long), mais des éléments peuvent avoir été perdus entre des appels trop éloignés. Cette classe est entièrement thread-safe.
public sealed class ConcurrentCircularBuffer<T> : ICollection<T>
{
private T[] _items;
private int _index;
private bool _full;
public ConcurrentCircularBuffer(int capacity)
{
if (capacity <= 1) // need at least two items
throw new ArgumentException(null, "capacity");
Capacity = capacity;
_items = new T[capacity];
}
public int Capacity { get; private set; }
public long TotalCount { get; private set; }
public int Count
{
get
{
lock (SyncObject) // full & _index need to be in sync
{
return _full ? Capacity : _index;
}
}
}
public void AddRange(IEnumerable<T> items)
{
if (items == null)
return;
lock (SyncObject)
{
foreach (var item in items)
{
AddWithLock(item);
}
}
}
private void AddWithLock(T item)
{
_items[_index] = item;
_index++;
if (_index == Capacity)
{
_full = true;
_index = 0;
}
TotalCount++;
}
public void Add(T item)
{
lock (SyncObject)
{
AddWithLock(item);
}
}
public void Clear()
{
lock (SyncObject)
{
_items = new T[Capacity];
_index = 0;
_full = false;
TotalCount = 0;
}
}
// this gives raw access to the underlying buffer. not sure I should keep that
public T this[int index]
{
get
{
return _items[index];
}
}
public T[] GetTail(long startIndex)
{
long lostCount;
return GetTail(startIndex, out lostCount);
}
public T[] GetTail(long startIndex, out long lostCount)
{
if (startIndex < 0 || startIndex >= TotalCount)
throw new ArgumentOutOfRangeException("startIndex");
T[] array = ToArray();
lostCount = (TotalCount - Count) - startIndex;
if (lostCount >= 0)
return array;
lostCount = 0;
// this maybe could optimized to not allocate the initial array
// but in multi-threading environment, I suppose this is arguable (and more difficult).
T[] chunk = new T[TotalCount - startIndex];
Array.Copy(array, array.Length - (TotalCount - startIndex), chunk, 0, chunk.Length);
return chunk;
}
public T[] ToArray()
{
lock (SyncObject)
{
T[] items = new T[_full ? Capacity : _index];
if (_full)
{
if (_index == 0)
{
Array.Copy(_items, items, Capacity);
}
else
{
Array.Copy(_items, _index, items, 0, Capacity - _index);
Array.Copy(_items, 0, items, Capacity - _index, _index);
}
}
else if (_index > 0)
{
Array.Copy(_items, items, _index);
}
return items;
}
}
public IEnumerator<T> GetEnumerator()
{
return ToArray().AsEnumerable().GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
bool ICollection<T>.Contains(T item)
{
return _items.Contains(item);
}
void ICollection<T>.CopyTo(T[] array, int arrayIndex)
{
if (array == null)
throw new ArgumentNullException("array");
if (array.Rank != 1)
throw new ArgumentException(null, "array");
if (arrayIndex < 0)
throw new ArgumentOutOfRangeException("arrayIndex");
if ((array.Length - arrayIndex) < Count)
throw new ArgumentException(null, "array");
T[] thisArray = ToArray();
Array.Copy(thisArray, 0, array, arrayIndex, thisArray.Length);
}
bool ICollection<T>.IsReadOnly
{
get
{
return false;
}
}
bool ICollection<T>.Remove(T item)
{
return false;
}
private static object _syncObject;
private static object SyncObject
{
get
{
if (_syncObject == null)
{
object obj = new object();
Interlocked.CompareExchange(ref _syncObject, obj, null);
}
return _syncObject;
}
}
}
Voici une autre implémentation qui utilise BoundedFifoBuffer de la collection commune Apache. Veuillez utiliser CircularFifoQueue si vous utilisez le dernier fichier JAR d'Apache, car la classe ci-dessous est obsolète.
BoundedFifoBuffer apiCallHistory = new BoundedFifoBuffer(20);
for(int i =1 ; i < 25; i++){
if(apiCallHistory.isFull()){
System.out.println("removing :: "+apiCallHistory.remove());
}
apiCallHistory.add(i);
}