目次
1 はじめに
前回の記事「【JUC Advanced】11. BlockingQueue」では、物理的には配列ですが、論理的にはリング構造となるArrayBlockingQueueを紹介しました。これが、今日紹介するトピックであるリング バッファにつながります。
2. 基本概要
2.1. リングバッファとは
循環バッファは、固定サイズのバッファにデータを効率的に格納および読み取りできるようにするデータ構造です。このようなバッファは通常、ネットワーク データ ストリームやファイル データ ストリームなどのストリーミング データを処理するために使用されます。
データを循環的に格納するため、リング バッファと呼ばれます。データは FIFO (先入れ先出し) 方式でバッファから読み取られます。つまり、最も古いデータが最初に読み取られます。バッファを使用して、2 つのポイント (たとえば、プロデューサーとコンシューマー) 間でデータを保存および転送します。
その一般的な構造を図に示します。
循環バッファにはバッファ内の次の空の位置へのポインタがあり、新しいエントリごとにそのポインタがインクリメントされます。これは、バッファーがいっぱいになったときに新しい要素を追加すると、最も古い要素が上書きされることを意味します。これにより、バッファがオーバーフローせず、新しいデータが重要なデータを上書きすることがなくなります。バッファーがいっぱいになった場合、循環バッファーは新しいデータ用のスペースを確保するために要素を移動する必要がありません。
代わりに、バッファがいっぱいになると、新しいデータが最も古いデータを上書きします。循環バッファーに要素を追加する時間計算量は定数 O(1) です。これにより、データを迅速に追加および削除する必要があるリアルタイム システムで非常に効率的になります。
2.2. 構造解析
循環バッファには 2 つのポインタがあり、1 つはバッファの先頭を指し、もう 1 つはバッファの末尾を指します。先頭ポインタは次の要素を挿入する位置を指し、末尾ポインタはバッファ内の最も古い要素の位置を指します。
先頭ポインタと末尾ポインタが一致すると、バッファがいっぱいであると見なされます。循環バッファを実装する 1 つの方法は、配列の終わりに達したときにラップアラウンドするモジュロ演算子を含む配列を使用することです。
2.3. 利点
- メモリの節約: リング バッファはリサイクルできるため、常に固定サイズのメモリ領域を割り当てる必要はありません。バッファがいっぱいになると、新しいデータが最も古いデータを上書きするため、メモリ使用量が削減されます。これは、大量のデータや限られたメモリ リソースを扱う場合に非常に重要です。
- 高性能: リングバッファにより、データの読み取りと書き込みの効率が向上します。データはバッファ内に循環的に格納されるため、頻繁にメモリを割り当てたり解放したりする必要はなく、読み取り/書き込みポインタは常に移動するだけで済みます。このため、リング バッファは、ネットワーク転送やリアルタイム データ処理などの高速データ ストリームの処理に最適です。
- 同時シナリオに適用可能: リング バッファは、複数のリーダーとライターによる同時アクセスをサポートできます。複数のスレッドが同時にデータの読み取りまたは書き込みを行う必要がある場合、ミューテックスまたはその他の同期メカニズムを使用して、データの正確性と一貫性を確保できます。このため、リング バッファは同時処理やマルチスレッド プログラミングに最適です。
2.4. 欠点
- データの上書き: バッファがいっぱいになると、新しいデータが最も古いデータを上書きするため、データが失われたり、重要な情報が上書きされたりする可能性があります。一部のアプリケーション シナリオでは、このデータの上書きによって問題が発生する可能性があるため、特別な注意が必要です。
- データの不整合: リング バッファの性質上、データは周期的に読み書きされるため、データの不整合が発生する可能性があります。たとえば、複数のスレッドが同時にデータの読み取りと書き込みを行うと、データの衝突やデータの破損が発生する可能性があります。
- 拡張が難しい:リングバッファの容量は固定されており、動的に拡張できません。バッファがいっぱいの場合、より多くのデータを処理する必要がある場合、より大きなメモリ空間を再割り当てする必要があり、パフォーマンスの低下やメモリ使用量の増加につながる可能性があります。
- 複雑なポインタ管理: リング バッファの特殊な性質により、読み取り/書き込みポインタには、データの正確性と一貫性を確保するための特別な管理が必要です。これにより、コードが複雑になり、潜在的なバグが発生する可能性があります。
- 同時実行制御のオーバーヘッド: マルチスレッド環境では、リング バッファーはデータの読み取りおよび書き込み操作を保護するために同期メカニズム (ミューテックスなど) を使用する必要があります。これにより、同時実行制御のオーバーヘッドが増加し、システムのパフォーマンスが低下する可能性があります。
3. 使用方法
3.1、リングバッファを定義する
/**
* @author Shamee loop
* @date 2023/7/11
*/
public class CircularBuffer {
private int[] buffer;
// 头部指针
private int head;
// 尾部指针
private int tail;
private int size;
// 初始容量
private int capacity;
public CircularBuffer(int capacity) {
this.capacity = capacity;
buffer = new int[capacity];
head = 0;
tail = 0;
size = 0;
}
/**
* 向缓冲区中添加数据,如果满了则覆盖
* @param value
*/
public synchronized void push(int value) {
if (size == capacity) {
// 缓冲区已满,覆盖最早的数据
head = (head + 1) % capacity;
}
buffer[tail] = value;
tail = (tail + 1) % capacity;
size++;
}
/**
* 向缓冲区中弹出数据,如果空了,则抛出异常
* @return
*/
public synchronized int pop() {
if (size == 0) {
throw new NoSuchElementException("Buffer is empty");
}
int value = buffer[head];
head = (head + 1) % capacity;
size--;
return value;
}
/**
* 获取缓冲区中第一个数据,但不会弹出数据
* @return
*/
public synchronized int peek() {
if (size == 0) {
throw new NoSuchElementException("Buffer is empty");
}
return buffer[head];
}
/**
* 判断缓冲区是否空了
* @return
*/
public synchronized boolean isEmpty() {
return size == 0;
}
/**
* 判断缓冲区是否满了
* @return
*/
public synchronized boolean isFull() {
return size == capacity;
}
}
3.2. デモの使用
public static void main(String[] args) {
CircularBuffer buffer = new CircularBuffer(5);
for (int i = 0; i < 10; i++) {
buffer.push(i);
}
for (int i = 0; i < 10; i++) {
int value = buffer.pop();
System.out.println("Received value: " + value);
}
}
出力結果:
定義した容量は 5 であるため、10 個の値をプッシュすると、新しい値が前の値を上書きするため、出力は常に 5、6、7、8、9 になることがわかります。複数回読み取られると、循環バッファが再利用されます。