Melaksanakan Ring Buffer di Java

1. Gambaran keseluruhan

Dalam tutorial ini, kita akan belajar bagaimana menerapkan Ring Buffer di Java.

2. Penyangga Cincin

Ring Buffer (atau Circular Buffer) adalah struktur data bulat terikat yang digunakan untuk buffering data antara dua atau lebih utas . Ketika kita terus menulis ke penyangga cincin, ia akan ditutup ketika ia sampai ke penghujungnya.

2.1. Bagaimana ia berfungsi

Ring Buffer dilaksanakan menggunakan susunan ukuran tetap yang melilit batas .

Selain dari array, ia mengesan tiga perkara:

  • slot yang tersedia seterusnya dalam penyangga untuk memasukkan elemen,
  • elemen yang belum dibaca seterusnya dalam penyangga,
  • dan akhir susunan - titik di mana penyangga membungkus ke permulaan array

Mekanik bagaimana penyangga cincin menangani keperluan ini berbeza dengan pelaksanaannya. Sebagai contoh, entri Wikipedia mengenai subjek menunjukkan kaedah menggunakan empat titik.

Kami akan meminjam pendekatan dari penerapan penyangga cincin penyangga menggunakan urutan.

Perkara pertama yang perlu kita ketahui ialah kapasiti - ukuran penyangga maksimum yang tetap. Seterusnya, kami akan menggunakan dua urutan yang meningkat secara monoton :

  • Urutan Tulis: bermula pada -1, kenaikan sebanyak 1 semasa kita memasukkan elemen
  • Urutan Baca: bermula pada 0, kenaikan sebanyak 1 ketika kita menggunakan elemen

Kita dapat memetakan urutan ke indeks dalam array dengan menggunakan operasi mod:

arrayIndex = sequence % capacity 

The operasi mod wrap urutan di sekeliling sempadan untuk memperolehi slot dalam buffer :

Mari lihat bagaimana kita memasukkan elemen:

buffer[++writeSequence % capacity] = element 

Kami menambah urutan sebelum memasukkan elemen.

Untuk menggunakan elemen, kita melakukan kenaikan pasca:

element = buffer[readSequence++ % capacity] 

Dalam kes ini, kami melakukan kenaikan pasca pada urutan. Mengkonsumsi elemen tidak menghapusnya dari penyangga - ia tetap berada dalam array sehingga diganti .

2.2. Penampan Kosong dan Penuh

Semasa kita membungkus array, kita akan mula menimpa data dalam penyangga. Sekiranya penyangga penuh, kita boleh memilih untuk menimpa data tertua tanpa mengira sama ada pembaca telah menggunakannya atau mencegah penimpaan data yang belum dibaca .

Sekiranya pembaca mampu kehilangan nilai pertengahan atau lama (misalnya, harga saham saham), kita dapat menimpa data tanpa menunggu ia habis. Sebaliknya, jika pembaca mesti menggunakan semua nilai (seperti transaksi e-commerce), kita harus menunggu (block / busy-waiting) sehingga buffer mempunyai slot yang tersedia.

Penyangga penuh jika ukuran penyangga sama dengan kapasitinya , di mana ukurannya sama dengan bilangan elemen yang belum dibaca:

size = (writeSequence - readSequence) + 1 isFull = (size == capacity) 

Sekiranya urutan tulis ketinggalan dari urutan baca, penyangga kosong :

isEmpty = writeSequence < readSequence 

Penyangga mengembalikan nilai nol jika kosong.

2.2. Kelebihan dan kekurangan

Penyangga cincin adalah penyangga FIFO yang cekap. Ia menggunakan susunan ukuran tetap yang dapat diperuntukkan terlebih dahulu dan memungkinkan corak akses memori yang cekap. Semua operasi penyangga adalah masa tetap O (1) , termasuk memakan elemen, kerana tidak memerlukan pergeseran elemen.

Di sisi lain, menentukan ukuran penyangga cincin yang betul adalah penting. Sebagai contoh, operasi menulis mungkin menyekat untuk masa yang lama jika penyangga kurang berukuran dan bacaannya perlahan. Kita boleh menggunakan ukuran dinamik, tetapi memerlukan data bergerak dan kita akan kehilangan sebahagian besar kelebihan yang dibincangkan di atas.

3. Pelaksanaan di Jawa

Sekarang setelah kita memahami bagaimana penyangga cincin berfungsi, mari lanjutkan untuk menerapkannya di Java.

3.1. Permulaan

Pertama, mari tentukan konstruktor yang menginisialisasi penyangga dengan kapasiti yang telah ditentukan:

public CircularBuffer(int capacity) { this.capacity = capacity; this.data = (E[]) new Object[capacity]; this.readSequence = 0; this.writeSequence = -1; } 

Ini akan membuat buffer kosong dan memulakan bidang urutan seperti yang dibincangkan di bahagian sebelumnya.

3.3. Tawaran

Seterusnya, kami akan melaksanakan operasi penawaran yang memasukkan elemen ke dalam penyangga pada slot yang tersedia seterusnya dan kembali benar pada kejayaan. Ia kembali palsu jika penyangga tidak dapat mencari slot kosong, iaitu, kita tidak dapat menimpa nilai yang belum dibaca .

Mari kita laksanakan kaedah tawaran di Java:

public boolean offer(E element) { boolean isFull = (writeSequence - readSequence) + 1 == capacity; if (!isFull) { int nextWriteSeq = writeSequence + 1; data[nextWriteSeq % capacity] = element; writeSequence++; return true; } return false; } 

Oleh itu, kami meningkatkan urutan menulis dan mengira indeks dalam array untuk slot yang tersedia seterusnya. Kemudian, kami menulis data ke penyangga dan menyimpan urutan penulisan yang dikemas kini.

Mari mencubanya:

@Test public void givenCircularBuffer_whenAnElementIsEnqueued_thenSizeIsOne() { CircularBuffer buffer = new CircularBuffer(defaultCapacity); assertTrue(buffer.offer("Square")); assertEquals(1, buffer.size()); } 

3.4. Poll

Finally, we'll implement the poll operation that retrieves and removes the next unread element. The poll operation doesn't remove the element but increments the read sequence.

Let's implement it:

public E poll() { boolean isEmpty = writeSequence < readSequence; if (!isEmpty) { E nextValue = data[readSequence % capacity]; readSequence++; return nextValue; } return null; } 

Here, we're reading the data at the current read sequence by computing the index in the array. Then, we're incrementing the sequence and returning the value, if the buffer is not empty.

Let's test it out:

@Test public void givenCircularBuffer_whenAnElementIsDequeued_thenElementMatchesEnqueuedElement() { CircularBuffer buffer = new CircularBuffer(defaultCapacity); buffer.offer("Triangle"); String shape = buffer.poll(); assertEquals("Triangle", shape); } 

4. Producer-Consumer Problem

We've talked about the use of a ring buffer for exchanging data between two or more threads, which is an example of a synchronization problem called the Producer-Consumer problem. In Java, we can solve the producer-consumer problem in various ways using semaphores, bounded queues, ring buffers, etc.

Let's implement a solution based on a ring buffer.

4.1. volatile Sequence Fields

Our implementation of the ring buffer is not thread-safe. Let's make it thread-safe for the simple single-producer and single-consumer case.

The producer writes data to the buffer and increments the writeSequence, while the consumer only reads from the buffer and increments the readSequence. So, the backing array is contention-free and we can get away without any synchronization.

But we still need to ensure that the consumer can see the latest value of the writeSequence field (visibility) and that the writeSequence is not updated before the data is actually available in the buffer (ordering).

We can make the ring buffer concurrent and lock-free in this case by making the sequence fields volatile:

private volatile int writeSequence = -1, readSequence = 0; 

In the offer method, a write to the volatile field writeSequence guarantees that the writes to the buffer happen before updating the sequence. At the same time, the volatile visibility guarantee ensures that the consumer will always see the latest value of writeSequence.

4.2. Producer

Let's implement a simple producer Runnable that writes to the ring buffer:

public void run() { for (int i = 0; i < items.length;) { if (buffer.offer(items[i])) { System.out.println("Produced: " + items[i]); i++; } } } 

The producer thread would wait for an empty slot in a loop (busy-waiting).

4.3. Consumer

We'll implement a consumer Callable that reads from the buffer:

public T[] call() { T[] items = (T[]) new Object[expectedCount]; for (int i = 0; i < items.length;) { T item = buffer.poll(); if (item != null) { items[i++] = item; System.out.println("Consumed: " + item); } } return items; } 

Benang pengguna berterusan tanpa mencetak jika menerima nilai nol dari penyangga.

Mari tulis kod pemandu kami:

executorService.submit(new Thread(new Producer(buffer))); executorService.submit(new Thread(new Consumer(buffer))); 

Menjalankan program pengeluar-pengguna kami menghasilkan output seperti di bawah:

Produced: Circle Produced: Triangle Consumed: Circle Produced: Rectangle Consumed: Triangle Consumed: Rectangle Produced: Square Produced: Rhombus Consumed: Square Produced: Trapezoid Consumed: Rhombus Consumed: Trapezoid Produced: Pentagon Produced: Pentagram Produced: Hexagon Consumed: Pentagon Consumed: Pentagram Produced: Hexagram Consumed: Hexagon Consumed: Hexagram 

5. Kesimpulannya

Dalam tutorial ini, kami telah belajar bagaimana menerapkan Ring Buffer dan meneroka bagaimana ia dapat digunakan untuk menyelesaikan masalah pengeluar-pengguna.

Seperti biasa, kod sumber untuk semua contoh boleh didapati di GitHub.