1. Gambaran keseluruhan
Dalam tutorial ini, kita akan melihat pelbagai cara untuk melaksanakan mutex di Java .
2. Mutex
Dalam aplikasi multithread, dua atau lebih utas mungkin perlu mengakses sumber yang dikongsi pada masa yang sama, yang mengakibatkan tingkah laku yang tidak dijangka. Contoh sumber yang dikongsi seperti itu adalah struktur data, peranti input-output, fail, dan sambungan rangkaian.
Kami memanggil senario ini sebagai keadaan perlumbaan . Dan, bahagian program yang mengakses sumber yang dikongsi dikenali sebagai bahagian kritikal . Oleh itu, untuk mengelakkan keadaan perlumbaan, kita perlu menyegerakkan akses ke bahagian kritikal.
Mutex (atau pengecualian bersama) adalah jenis penyegerak yang paling mudah - ia memastikan bahawa hanya satu utas yang dapat melaksanakan bahagian kritikal program komputer pada satu masa .
Untuk mengakses bahagian kritikal, utas memperoleh mutex, kemudian mengakses bahagian kritikal, dan akhirnya melepaskan mutex. Sementara itu, semua utas lain menyekat sehingga mutex dilepaskan. Sebaik sahaja benang keluar dari bahagian kritikal, utas lain dapat memasuki bahagian kritikal.
3. Mengapa Mutex?
Pertama, mari kita ambil contoh kelas SequenceGeneraror , yang menghasilkan urutan seterusnya dengan meningkatkan nilai semasa oleh satu setiap kali:
public class SequenceGenerator { private int currentValue = 0; public int getNextSequence() { currentValue = currentValue + 1; return currentValue; } }
Sekarang, mari buat kes ujian untuk melihat bagaimana kaedah ini berkelakuan apabila banyak utas cuba mengaksesnya secara serentak:
@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; }
Sebaik sahaja kita melaksanakan kes ujian ini, kita dapat melihat bahawa ia gagal sepanjang masa dengan alasan yang serupa 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 unikSequences seharusnya mempunyai ukuran yang sama dengan berapa kali kita menjalankan kaedah getNextSequence dalam kes ujian kita. Namun, ini tidak berlaku kerana keadaan perlumbaan. Sudah tentu, kita tidak mahu tingkah laku ini.
Oleh itu, untuk mengelakkan keadaan perlumbaan seperti itu, kita perlu memastikan bahawa hanya satu utas yang dapat melaksanakan kaedah getNextSequence pada satu masa . Dalam senario seperti itu, kita dapat menggunakan mutex untuk menyegerakkan utas.
Ada berbagai cara, kita dapat menerapkan mutex di Java. Jadi, seterusnya, kita akan melihat pelbagai cara untuk melaksanakan mutex untuk kelas SequenceGenerator kami .
4. Menggunakan Kata Kunci yang disegerakkan
Pertama, kita akan membincangkan kata kunci yang disegerakkan , yang merupakan kaedah termudah untuk melaksanakan mutex di Java.
Setiap objek di Java mempunyai kunci intrinsik yang berkaitan dengannya. Yang disegerakkan kaedah dan yang disegerakkan blok menggunakan kunci intrinsik ini untuk menyekat akses seksyen kritikal kepada hanya satu thread pada satu masa.
Oleh itu, apabila utas menggunakan kaedah yang disegerakkan atau memasuki blok yang disegerakkan , ia secara automatik memperoleh kunci. Kunci dilepaskan apabila kaedah atau blok selesai atau pengecualian dilemparkan daripadanya.
Mari ubah getNextSequence untuk memiliki mutex, hanya dengan menambahkan kata kunci yang disegerakkan :
public class SequenceGeneratorUsingSynchronizedMethod extends SequenceGenerator { @Override public synchronized int getNextSequence() { return super.getNextSequence(); } }
Yang disegerakkan blok adalah sama dengan serentak kaedah, dengan lebih kawalan ke atas bahagian yang kritikal dan objek yang kita boleh gunakan untuk mengunci.
Jadi, mari kita lihat sekarang bagaimana kita boleh menggunakan disegerakkan blok untuk menyegerakkan pada objek mutex adat :
public class SequenceGeneratorUsingSynchronizedBlock extends SequenceGenerator { private Object mutex = new Object(); @Override public int getNextSequence() { synchronized (mutex) { return super.getNextSequence(); } } }
5. Menggunakan ReentrantLock
The ReentrantLock kelas telah diperkenalkan di Jawa 1.5. Ini memberikan lebih banyak fleksibiliti dan kawalan daripada pendekatan kata kunci yang diselaraskan .
Mari 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.
Walaupun dalam keadaan mutex hanya satu utas yang dapat mengakses bahagian kritikal, Semaphore membenarkan sejumlah utas untuk mengakses bahagian kritikal . Oleh itu, kita juga dapat melaksanakan mutex dengan menetapkan bilangan utas yang dibenarkan dalam Semaphore menjadi satu .
Let's now create another thread-safe version of SequenceGenerator using 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. Using Guava's Monitor Class
So far, we've seen the options to implement mutex using features provided by Java.
However, the Monitor class of Google's Guava library is a better alternative to the ReentrantLock class. As per its documentation, code using Monitor is more readable and less error-prone than the code using ReentrantLock.
First, we'll add the Maven dependency for Guava:
com.google.guava guava 28.0-jre
Now, we'll write another subclass of SequenceGenerator using the Monitor class:
public class SequenceGeneratorUsingMonitor extends SequenceGenerator { private Monitor mutex = new Monitor(); @Override public int getNextSequence() { mutex.enter(); try { return super.getNextSequence(); } finally { mutex.leave(); } } }
8. Conclusion
Dalam tutorial ini, kami telah melihat konsep mutex. Juga, kami telah melihat pelbagai cara untuk menerapkannya di Java.
Seperti biasa, kod sumber lengkap contoh kod yang digunakan dalam tutorial ini terdapat di GitHub.