Menggunakan Objek Mutex di Java

1. Ikhtisar

Dalam tutorial ini, kita akan melihat berbagai cara untuk mengimplementasikan mutex di Java .

2. Mutex

Dalam aplikasi multithread, dua atau lebih utas mungkin perlu mengakses sumber daya bersama pada saat yang sama, mengakibatkan perilaku yang tidak terduga. Contoh sumber daya bersama seperti itu adalah struktur data, perangkat input-output, file, dan koneksi jaringan.

Kami menyebut skenario ini kondisi balapan . Dan, bagian dari program yang mengakses sumber daya bersama dikenal sebagai bagian kritis . Jadi, untuk menghindari kondisi balapan, kita perlu melakukan sinkronisasi akses ke bagian kritis.

Mutex (atau mutual exclusion) adalah jenis sinkronisasi paling sederhana - ini memastikan bahwa hanya satu utas yang dapat menjalankan bagian penting dari program komputer pada satu waktu .

Untuk mengakses bagian kritis, utas memperoleh mutex, lalu mengakses bagian kritis, dan akhirnya melepaskan mutex. Sementara itu, semua utas lainnya memblokir hingga mutex dirilis. Segera setelah utas keluar dari bagian kritis, utas lain dapat masuk ke bagian kritis.

3. Mengapa Mutex?

Pertama, mari kita ambil contoh kelas SequenceGeneraror , yang menghasilkan urutan berikutnya dengan menaikkan currentValue satu kali setiap kali:

public class SequenceGenerator { private int currentValue = 0; public int getNextSequence() { currentValue = currentValue + 1; return currentValue; } }

Sekarang, mari buat kasus uji untuk melihat bagaimana metode ini berperilaku ketika beberapa utas mencoba mengaksesnya secara bersamaan:

@Test public void givenUnsafeSequenceGenerator_whenRaceCondition_thenUnexpectedBehavior() throws Exception { int count = 1000; Set uniqueSequences = getUniqueSequences(new SequenceGenerator(), count); Assert.assertEquals(count, uniqueSequences.size()); } private Set getUniqueSequences(SequenceGenerator generator, int count) throws Exception { ExecutorService executor = Executors.newFixedThreadPool(3); Set uniqueSequences = new LinkedHashSet(); List
    
      futures = new ArrayList(); for (int i = 0; i < count; i++) { futures.add(executor.submit(generator::getNextSequence)); } for (Future future : futures) { uniqueSequences.add(future.get()); } executor.awaitTermination(1, TimeUnit.SECONDS); executor.shutdown(); return uniqueSequences; }
    

Setelah kami menjalankan kasus uji ini, kami dapat melihat bahwa kasus tersebut sering kali gagal dengan alasan yang mirip dengan:

java.lang.AssertionError: expected: but was: at org.junit.Assert.fail(Assert.java:88) at org.junit.Assert.failNotEquals(Assert.java:834) at org.junit.Assert.assertEquals(Assert.java:645)

The uniqueSequences seharusnya memiliki ukuran sama dengan jumlah kali kami telah melaksanakan getNextSequence metode dalam kasus pengujian kami. Namun, tidak demikian halnya karena kondisi balapan. Jelas, kami tidak menginginkan perilaku ini.

Jadi, untuk menghindari kondisi balapan seperti itu, kita perlu memastikan bahwa hanya satu thread yang dapat mengeksekusi metode getNextSequence dalam satu waktu . Dalam skenario seperti itu, kita dapat menggunakan mutex untuk menyinkronkan utas.

Ada berbagai cara, kita bisa mengimplementasikan mutex di Java. Jadi, selanjutnya, kita akan melihat berbagai cara untuk mengimplementasikan mutex untuk kelas SequenceGenerator kita .

4. Menggunakan Kata Kunci tersinkronisasi

Pertama, kita akan membahas kata kunci tersinkronisasi , yang merupakan cara termudah untuk mengimplementasikan mutex di Java.

Setiap objek di Java memiliki kunci intrinsik yang terkait dengannya. The disinkronisasi metode dan yang disinkronkan blok menggunakan kunci intrinsik ini untuk membatasi akses dari bagian penting untuk hanya satu thread pada suatu waktu.

Oleh karena itu, ketika utas memanggil metode tersinkronisasi atau memasuki blok tersinkronisasi , secara otomatis memperoleh kunci. Kunci terlepas ketika metode atau blok selesai atau pengecualian dilemparkan darinya.

Mari kita ubah getNextSequence menjadi mutex, cukup dengan menambahkan kata kunci tersinkron :

public class SequenceGeneratorUsingSynchronizedMethod extends SequenceGenerator { @Override public synchronized int getNextSequence() { return super.getNextSequence(); } }

The disinkronkan blok mirip dengan disinkronkan metode, dengan kontrol lebih besar atas bagian kritis dan obyek dapat kita gunakan untuk mengunci.

Jadi, mari kita sekarang melihat bagaimana kita dapat menggunakan disinkronkan blok untuk sinkronisasi pada objek mutex kustom :

public class SequenceGeneratorUsingSynchronizedBlock extends SequenceGenerator { private Object mutex = new Object(); @Override public int getNextSequence() { synchronized (mutex) { return super.getNextSequence(); } } }

5. Menggunakan ReentrantLock

Kelas ReentrantLock diperkenalkan di Java 1.5. Ini memberikan lebih banyak fleksibilitas dan kontrol daripada pendekatan kata kunci tersinkronisasi .

Mari kita lihat bagaimana kita dapat menggunakan ReentrantLock untuk mencapai pengecualian bersama:

public class SequenceGeneratorUsingReentrantLock extends SequenceGenerator { private ReentrantLock mutex = new ReentrantLock(); @Override public int getNextSequence() { try { mutex.lock(); return super.getNextSequence(); } finally { mutex.unlock(); } } }

6. Menggunakan Semaphore

Seperti ReentrantLock , kelas Semaphore juga diperkenalkan di Java 1.5.

Sementara dalam kasus mutex, hanya satu utas yang dapat mengakses bagian penting, Semaphore memungkinkan sejumlah utas tetap untuk mengakses bagian penting . Oleh karena itu, kita juga dapat mengimplementasikan mutex dengan mengatur jumlah utas yang diizinkan dalam Semaphore menjadi satu .

Sekarang mari kita buat versi lain yang aman untuk thread dari SequenceGenerator menggunakan Semaphore :

public class SequenceGeneratorUsingSemaphore extends SequenceGenerator { private Semaphore mutex = new Semaphore(1); @Override public int getNextSequence() { try { mutex.acquire(); return super.getNextSequence(); } catch (InterruptedException e) { // exception handling code } finally { mutex.release(); } } }

7. Menggunakan Kelas Monitor Jambu Biji

Sejauh ini, kami telah melihat opsi untuk mengimplementasikan mutex menggunakan fitur yang disediakan oleh Java.

Namun, kelas Monitor dari pustaka Guava Google adalah alternatif yang lebih baik untuk kelas ReentrantLock . Sesuai dokumentasinya, kode yang menggunakan Monitor lebih mudah dibaca dan tidak terlalu rentan terhadap kesalahan daripada kode yang menggunakan ReentrantLock .

Pertama, kami akan menambahkan dependensi Maven untuk Guava:

 com.google.guava guava 28.0-jre 

Sekarang, kita akan menulis subclass SequenceGenerator lain menggunakan kelas Monitor :

public class SequenceGeneratorUsingMonitor extends SequenceGenerator { private Monitor mutex = new Monitor(); @Override public int getNextSequence() { mutex.enter(); try { return super.getNextSequence(); } finally { mutex.leave(); } } }

8. Kesimpulan

Dalam tutorial ini, kita telah melihat konsep mutex. Selain itu, kami telah melihat berbagai cara untuk menerapkannya di Java.

Seperti biasa, kode sumber lengkap dari contoh kode yang digunakan dalam tutorial ini tersedia di GitHub.