Panduan untuk sun.misc.Unsafe

1. Gambaran keseluruhan

Dalam artikel ini, kita akan melihat kelas menarik yang disediakan oleh pakej JRE - Tidak selamat dari pakej sun.misc . Kelas ini memberi kami mekanisme tingkat rendah yang dirancang untuk digunakan hanya oleh perpustakaan inti Java dan bukan oleh pengguna standard.

Ini memberi kita mekanisme tahap rendah yang terutama dirancang untuk penggunaan dalaman di perpustakaan teras.

2. Mendapatkan Contoh Tidak Selamat

Pertama, untuk dapat menggunakan kelas Tidak Selamat , kita perlu mendapatkan contoh - yang tidak mudah memandangkan kelas itu dirancang hanya untuk penggunaan dalaman.

Cara untuk mendapatkan contoh adalah dengan kaedah statik getUnsafe (). Peringatan adalah bahawa secara lalai - ini akan menimbulkan SecurityException.

Nasib baik, kita dapat memperoleh contoh menggunakan refleksi:

Field f = Unsafe.class.getDeclaredField("theUnsafe"); f.setAccessible(true); unsafe = (Unsafe) f.get(null);

3. Memulakan Kelas Menggunakan Tidak Selamat

Katakan bahawa kita mempunyai kelas sederhana dengan konstruktor yang menetapkan nilai berubah semasa objek dibuat:

class InitializationOrdering { private long a; public InitializationOrdering() { this.a = 1; } public long getA() { return this.a; } }

Apabila kita menginisialisasi objek menggunakan konstruktor, kaedah getA () akan mengembalikan nilai 1:

InitializationOrdering o1 = new InitializationOrdering(); assertEquals(o1.getA(), 1);

Tetapi kita boleh menggunakan kaedah mengalokasikanInstance () menggunakan Tidak selamat. Ia hanya akan memperuntukkan memori untuk kelas kami, dan tidak akan menggunakan pembina:

InitializationOrdering o3 = (InitializationOrdering) unsafe.allocateInstance(InitializationOrdering.class); assertEquals(o3.getA(), 0);

Perhatikan bahawa konstruktor tidak dipanggil dan disebabkan oleh fakta itu, kaedah getA () mengembalikan nilai lalai untuk jenis panjang - iaitu 0.

4. Mengubah Kawasan Persendirian

Katakan bahawa kita mempunyai kelas yang mempunyai nilai peribadi rahsia :

class SecretHolder { private int SECRET_VALUE = 0; public boolean secretIsDisclosed() { return SECRET_VALUE == 1; } }

Dengan menggunakan kaedah putInt () dari Tidak Selamat, kita dapat mengubah nilai bidang SECRET_VALUE peribadi , mengubah / merosakkan keadaan contoh itu:

SecretHolder secretHolder = new SecretHolder(); Field f = secretHolder.getClass().getDeclaredField("SECRET_VALUE"); unsafe.putInt(secretHolder, unsafe.objectFieldOffset(f), 1); assertTrue(secretHolder.secretIsDisclosed());

Sebaik sahaja kita mendapat medan dengan panggilan refleksi, kita dapat mengubah nilainya menjadi nilai int yang lain menggunakan Tidak Selamat .

5. Melemparkan Pengecualian

Kod yang dipanggil melalui Tidak Selamat tidak diperiksa dengan cara yang sama oleh penyusun seperti kod Java biasa. Kita boleh menggunakan kaedah throwException () untuk membuang pengecualian tanpa menyekat pemanggil untuk menangani pengecualian itu, walaupun itu adalah pengecualian yang diperiksa:

@Test(expected = IOException.class) public void givenUnsafeThrowException_whenThrowCheckedException_thenNotNeedToCatchIt() { unsafe.throwException(new IOException()); }

Setelah membuang IOException, yang diperiksa, kami tidak perlu menangkapnya atau menentukannya dalam deklarasi kaedah.

6. Memori Di Luar Tumpukan

Sekiranya aplikasi kehabisan memori yang tersedia di JVM, kami akhirnya memaksa proses GC berjalan terlalu kerap. Sebaik-baiknya, kita mahukan kawasan memori khas, di luar timbunan dan tidak dikawal oleh proses GC.

Kaedah allowateMemory () dari kelas Tidak Selamat memberi kita kemampuan untuk memperuntukkan objek besar dari timbunan, yang bermaksud bahawa memori ini tidak akan dilihat dan diambil kira oleh GC dan JVM .

Ini boleh menjadi sangat berguna, tetapi kita perlu ingat bahawa memori ini perlu dikendalikan secara manual dan menuntut semula dengan percuma dengan memori percuma () apabila tidak diperlukan lagi.

Katakanlah bahawa kita ingin membuat pelbagai byte memori luar timbunan yang besar. Kita boleh menggunakan kaedah allowateMemory () untuk mencapainya:

class OffHeapArray { private final static int BYTE = 1; private long size; private long address; public OffHeapArray(long size) throws NoSuchFieldException, IllegalAccessException { this.size = size; address = getUnsafe().allocateMemory(size * BYTE); } private Unsafe getUnsafe() throws IllegalAccessException, NoSuchFieldException { Field f = Unsafe.class.getDeclaredField("theUnsafe"); f.setAccessible(true); return (Unsafe) f.get(null); } public void set(long i, byte value) throws NoSuchFieldException, IllegalAccessException { getUnsafe().putByte(address + i * BYTE, value); } public int get(long idx) throws NoSuchFieldException, IllegalAccessException { return getUnsafe().getByte(address + idx * BYTE); } public long size() { return size; } public void freeMemory() throws NoSuchFieldException, IllegalAccessException { getUnsafe().freeMemory(address); }
}

Dalam konstruktor OffHeapArray, kami memulakan array dengan ukuran tertentu . Kami menyimpan alamat awal array di medan alamat . The set () kaedah mengambil indeks dan diberi nilai yang akan disimpan dalam tatasusunan. Kaedah get () adalah mendapatkan nilai bait menggunakan indeksnya yang merupakan ofset dari alamat permulaan array.

Seterusnya, kita dapat memperuntukkan array luar timbunan menggunakan konstruktornya:

long SUPER_SIZE = (long) Integer.MAX_VALUE * 2; OffHeapArray array = new OffHeapArray(SUPER_SIZE);

Kami dapat memasukkan N bilangan nilai byte ke dalam array ini dan kemudian mengambil nilai tersebut, menjumlahkannya untuk menguji apakah pengalamatan kami berfungsi dengan betul:

int sum = 0; for (int i = 0; i < 100; i++) { array.set((long) Integer.MAX_VALUE + i, (byte) 3); sum += array.get((long) Integer.MAX_VALUE + i); } assertEquals(array.size(), SUPER_SIZE); assertEquals(sum, 300);

Pada akhirnya, kita perlu melepaskan memori kembali ke OS dengan memanggil freeMemory ().

7. Bandingkan Operasi AndSwap

Konstruk yang sangat efisien dari pakej java.concurrent , seperti AtomicInteger, menggunakan kaedah membandingkanAndSwap () daripada Tidak Selamat di bawahnya, untuk memberikan prestasi terbaik. Konstruk ini banyak digunakan dalam algoritma bebas kunci yang dapat memanfaatkan arahan pemproses CAS untuk memberikan kepantasan yang hebat berbanding dengan mekanisme penyegerakan pesimis standard di Jawa.

Kita boleh membina kaunter berdasarkan CAS menggunakan kaedah membandingkanAndSwapLong () dari Tidak Selamat :

class CASCounter { private Unsafe unsafe; private volatile long counter = 0; private long offset; private Unsafe getUnsafe() throws IllegalAccessException, NoSuchFieldException { Field f = Unsafe.class.getDeclaredField("theUnsafe"); f.setAccessible(true); return (Unsafe) f.get(null); } public CASCounter() throws Exception { unsafe = getUnsafe(); offset = unsafe.objectFieldOffset(CASCounter.class.getDeclaredField("counter")); } public void increment() { long before = counter; while (!unsafe.compareAndSwapLong(this, offset, before, before + 1)) { before = counter; } } public long getCounter() { return counter; } }

In the CASCounter constructor we are getting the address of the counter field, to be able to use it later in the increment() method. That field needs to be declared as the volatile, to be visible to all threads that are writing and reading this value. We are using the objectFieldOffset() method to get the memory address of the offset field.

The most important part of this class is the increment() method. We're using the compareAndSwapLong() in the while loop to increment previously fetched value, checking if that previous value changed since we fetched it.

If it did, then we are retrying that operation until we succeed. There is no blocking here, which is why this is called a lock-free algorithm.

We can test our code by incrementing the shared counter from multiple threads:

int NUM_OF_THREADS = 1_000; int NUM_OF_INCREMENTS = 10_000; ExecutorService service = Executors.newFixedThreadPool(NUM_OF_THREADS); CASCounter casCounter = new CASCounter(); IntStream.rangeClosed(0, NUM_OF_THREADS - 1) .forEach(i -> service.submit(() -> IntStream .rangeClosed(0, NUM_OF_INCREMENTS - 1) .forEach(j -> casCounter.increment())));

Next, to assert that state of the counter is proper, we can get the counter value from it:

assertEquals(NUM_OF_INCREMENTS * NUM_OF_THREADS, casCounter.getCounter());

8. Park/Unpark

There are two fascinating methods in the Unsafe API that are used by the JVM to context switch threads. When the thread is waiting for some action, the JVM can make this thread blocked by using the park() method from the Unsafe class.

It is very similar to the Object.wait() method, but it is calling the native OS code, thus taking advantage of some architecture specifics to get the best performance.

Apabila utas disekat dan perlu dijalankan semula, JVM menggunakan kaedah buka tanda () . Kita sering akan melihat kaedah tersebut dalam pembuangan benang, terutama dalam aplikasi yang menggunakan kumpulan benang.

9. Kesimpulannya

Dalam artikel ini, kami melihat kelas Tidak Selamat dan konstruknya yang paling berguna.

Kami melihat bagaimana mengakses medan peribadi, bagaimana memperuntukkan memori di luar timbunan, dan bagaimana menggunakan konstruk perbandingan-dan-pertukaran untuk menerapkan algoritma bebas kunci.

Pelaksanaan semua contoh dan potongan kode ini boleh didapati di GitHub - ini adalah projek Maven, jadi mudah untuk diimport dan dijalankan sebagaimana adanya.