Memori Transaksional Perisian di Java Menggunakan Multiverse

1. Gambaran keseluruhan

Dalam artikel ini, kita akan melihat perpustakaan Multiverse - yang membantu kita menerapkan konsep Memori Transaksional Perisian di Java.

Dengan menggunakan konstruksi dari perpustakaan ini, kita dapat membuat mekanisme penyegerakan pada keadaan bersama - yang merupakan penyelesaian yang lebih elegan dan dapat dibaca daripada pelaksanaan standard dengan perpustakaan inti Java.

2. Ketergantungan Maven

Untuk memulakan, kita perlu menambahkan perpustakaan inti multiverse ke dalam pom kami:

 org.multiverse multiverse-core 0.7.0 

3. API Multiverse

Mari kita mulakan dengan beberapa asas.

Perisian Transaksional Memori (STM) adalah konsep yang diangkut dari dunia pangkalan data SQL - di mana setiap operasi dijalankan dalam transaksi yang memenuhi sifat ACID (Atomicity, Consistency, Isolation, Durability) . Di sini, hanya Atomisiti, Konsistensi dan Pengasingan yang berpuas hati kerana mekanisme berjalan dalam ingatan.

Antara muka utama di perpustakaan Multiverse adalah TxnObject - setiap objek transaksional perlu melaksanakannya, dan perpustakaan menyediakan sejumlah subkelas tertentu yang dapat kita gunakan.

Setiap operasi yang perlu diletakkan dalam bahagian kritikal, dapat diakses hanya dengan satu utas dan menggunakan objek transaksional - perlu dibungkus dalam kaedah StmUtils.atomic () . Bahagian kritikal adalah tempat program yang tidak dapat dijalankan oleh lebih dari satu utas secara serentak, jadi akses ke program tersebut harus dijaga oleh mekanisme penyegerakan.

Sekiranya tindakan dalam transaksi berjaya, transaksi akan dilakukan, dan keadaan baru akan dapat diakses oleh utas lain. Sekiranya terdapat kesilapan, transaksi tidak akan dilakukan, dan oleh itu keadaannya tidak akan berubah.

Akhirnya, jika dua utas ingin mengubah keadaan yang sama dalam transaksi, hanya satu yang berjaya dan melakukan perubahannya. Urutan seterusnya akan dapat melakukan tindakannya dalam transaksi.

4. Melaksanakan Logik Akaun Menggunakan STM

Mari kita lihat contohnya .

Katakanlah bahawa kami ingin membuat logik akaun bank menggunakan STM yang disediakan oleh perpustakaan Multiverse . Kami Akaun objek akan mempunyai lastUpadate cap waktu itu adalah daripada TxnLong jenis, dan kira-kira bidang yang menyimpan baki semasa untuk akaun tertentu dan daripada TxnInteger jenis.

The TxnLong dan TxnInteger ialah kelas dari Multiverse . Mereka mesti dilaksanakan dalam transaksi. Jika tidak, pengecualian akan dilemparkan. Kita perlu menggunakan StmUtils untuk membuat contoh objek transaksional baru:

public class Account { private TxnLong lastUpdate; private TxnInteger balance; public Account(int balance) { this.lastUpdate = StmUtils.newTxnLong(System.currentTimeMillis()); this.balance = StmUtils.newTxnInteger(balance); } }

Seterusnya, kami akan membuat kaedah adjustBy () - yang akan menambah baki dengan jumlah yang diberikan. Tindakan itu perlu dilaksanakan dalam transaksi.

Sekiranya ada pengecualian yang dilemparkan di dalamnya, transaksi akan berakhir tanpa melakukan perubahan:

public void adjustBy(int amount) { adjustBy(amount, System.currentTimeMillis()); } public void adjustBy(int amount, long date) { StmUtils.atomic(() -> { balance.increment(amount); lastUpdate.set(date); if (balance.get() <= 0) { throw new IllegalArgumentException("Not enough money"); } }); }

Sekiranya kita ingin mendapatkan baki semasa untuk akaun yang diberikan, kita perlu mendapatkan nilai dari bidang keseimbangan, tetapi ia juga perlu dipanggil dengan semantik atom:

public Integer getBalance() { return balance.atomicGet(); }

5. Menguji Akaun

Mari uji logik Akaun kami . Pertama, kami ingin mengurangkan baki dari akaun dengan jumlah yang diberikan hanya:

@Test public void givenAccount_whenDecrement_thenShouldReturnProperValue() { Account a = new Account(10); a.adjustBy(-5); assertThat(a.getBalance()).isEqualTo(5); }

Seterusnya, katakan bahawa kami menarik diri dari akaun menjadikan baki negatif. Tindakan itu harus membuat pengecualian, dan membiarkan akaun utuh kerana tindakan itu dilakukan dalam transaksi dan tidak dilakukan:

@Test(expected = IllegalArgumentException.class) public void givenAccount_whenDecrementTooMuch_thenShouldThrow() { // given Account a = new Account(10); // when a.adjustBy(-11); } 

Sekarang mari kita menguji masalah serentak yang boleh timbul apabila dua utas ingin mengurangkan keseimbangan pada masa yang sama.

Sekiranya satu utas ingin mengurangkannya dengan 5 dan yang kedua dengan 6, salah satu daripada kedua tindakan tersebut akan gagal kerana baki akaun semasa adalah sama dengan 10.

Kami akan menyerahkan dua utas ke ExecutorService , dan menggunakan CountDownLatch untuk memulakannya pada masa yang sama:

ExecutorService ex = Executors.newFixedThreadPool(2); Account a = new Account(10); CountDownLatch countDownLatch = new CountDownLatch(1); AtomicBoolean exceptionThrown = new AtomicBoolean(false); ex.submit(() -> { try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } try { a.adjustBy(-6); } catch (IllegalArgumentException e) { exceptionThrown.set(true); } }); ex.submit(() -> { try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } try { a.adjustBy(-5); } catch (IllegalArgumentException e) { exceptionThrown.set(true); } });

Setelah menatap kedua-dua tindakan pada masa yang sama, salah satu daripada mereka akan membuat pengecualian:

countDownLatch.countDown(); ex.awaitTermination(1, TimeUnit.SECONDS); ex.shutdown(); assertTrue(exceptionThrown.get());

6. Pindah dari Satu Akaun ke Yang Lain

Katakan bahawa kita mahu memindahkan wang dari satu akaun ke akaun yang lain. Kita boleh melaksanakan kaedah transferTo () pada kelas Akaun dengan meneruskan Akaun lain yang ingin kita pindahkan sejumlah wang yang diberikan:

public void transferTo(Account other, int amount) { StmUtils.atomic(() -> { long date = System.currentTimeMillis(); adjustBy(-amount, date); other.adjustBy(amount, date); }); }

Semua logik dilaksanakan dalam transaksi. Ini akan menjamin bahawa apabila kita ingin memindahkan jumlah yang lebih tinggi daripada baki pada akaun yang diberikan, kedua-dua akaun akan utuh kerana transaksi tidak akan dilakukan.

Mari kita uji logik pemindahan:

Account a = new Account(10); Account b = new Account(10); a.transferTo(b, 5); assertThat(a.getBalance()).isEqualTo(5); assertThat(b.getBalance()).isEqualTo(15);

Kami hanya membuat dua akaun, kami memindahkan wang dari satu ke yang lain, dan semuanya berjalan seperti yang diharapkan. Seterusnya, katakan bahawa kami ingin memindahkan lebih banyak wang daripada yang ada di akaun. The transferTo () panggilan akan membuang IllegalArgumentException, dan perubahan tidak akan dilakukan:

try { a.transferTo(b, 20); } catch (IllegalArgumentException e) { System.out.println("failed to transfer money"); } assertThat(a.getBalance()).isEqualTo(5); assertThat(b.getBalance()).isEqualTo(15);

Perhatikan bahawa baki untuk kedua-dua akaun a dan b adalah sama seperti sebelum kaedah panggilan ke transferTo () .

7. STM Selamat Mati

Apabila kita menggunakan mekanisme penyegerakan Java standard, logik kita akan rentan terhadap kebuntuan, tanpa ada jalan untuk memulihkannya.

Kebuntuan boleh berlaku ketika kita ingin memindahkan wang dari akaun a ke akaun b . Dalam pelaksanaan Java standard, satu utas perlu mengunci akaun a , kemudian akaun b . Katakan bahawa, sementara itu, utas lain ingin memindahkan wang dari akaun b ke akaun a . Benang yang lain mengunci akaun b menunggu akaun a dibuka.

Malangnya, kunci untuk akaun a dipegang oleh utas pertama, dan kunci untuk akaun b dipegang oleh utas kedua. Keadaan seperti itu akan menyebabkan program kita disekat selama-lamanya.

Nasib baik, semasa melaksanakan logik transferTo () menggunakan STM, kita tidak perlu bimbang tentang kebuntuan kerana STM adalah Deadlock Safe. Mari kita uji bahawa menggunakan kaedah transferTo () kami .

Katakan bahawa kita mempunyai dua utas. Benang pertama ingin memindahkan sejumlah wang dari akaun a ke akaun b , dan utas kedua ingin memindahkan sejumlah wang dari akaun b ke akaun a . Kita perlu membuat dua akaun dan memulakan dua utas yang akan melaksanakan kaedah transferTo () pada masa yang sama:

ExecutorService ex = Executors.newFixedThreadPool(2); Account a = new Account(10); Account b = new Account(10); CountDownLatch countDownLatch = new CountDownLatch(1); ex.submit(() -> { try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } a.transferTo(b, 10); }); ex.submit(() -> { try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } b.transferTo(a, 1); });

Setelah memulakan pemprosesan, kedua-dua akaun akan mempunyai bidang keseimbangan yang betul:

countDownLatch.countDown(); ex.awaitTermination(1, TimeUnit.SECONDS); ex.shutdown(); assertThat(a.getBalance()).isEqualTo(1); assertThat(b.getBalance()).isEqualTo(19);

8. Kesimpulannya

Dalam tutorial ini, kami melihat perpustakaan Multiverse dan bagaimana kami dapat menggunakannya untuk membuat logik bebas kunci dan selamat menggunakan konsep dalam Memori Transaksional Perisian.

Kami menguji tingkah laku logik yang dilaksanakan dan melihat bahawa logik yang menggunakan STM bebas dari kebuntuan.

Pelaksanaan semua contoh dan coretan kod ini terdapat dalam projek GitHub - ini adalah projek Maven, jadi mudah untuk diimport dan dijalankan sebagaimana adanya.