Cara menavigasi corak Singleton yang mudah menipu

Corak Singleton sangat sederhana, bahkan terutama bagi pemaju Java. Dalam artikel JavaWorld klasik ini, David Geary menunjukkan bagaimana pembangun Java melaksanakan single, dengan contoh kod untuk multithreading, classloader, dan serialisasi menggunakan corak Singleton. Dia menyimpulkan dengan melihat pelaksanaan pendaftaran single untuk menentukan singleton pada waktu runtime.

Kadang-kadang wajar untuk mempunyai satu contoh kelas: pengurus tetingkap, spooler cetak, dan sistem fail adalah contoh prototaip. Biasanya, jenis objek tersebut - dikenal sebagai singleton - diakses oleh objek yang berbeza di seluruh sistem perisian, dan oleh itu memerlukan titik akses global. Sudah tentu, apabila anda yakin bahawa anda tidak akan memerlukan lebih daripada satu contoh, ada baiknya anda mengubah fikiran anda.

Corak reka bentuk Singleton menangani semua masalah ini. Dengan corak reka bentuk Singleton anda boleh:

  • Pastikan hanya satu contoh kelas dibuat
  • Berikan titik akses global ke objek
  • Benarkan banyak kejadian pada masa akan datang tanpa mempengaruhi pelanggan kelas tunggal

Walaupun corak reka bentuk Singleton - seperti yang dibuktikan oleh gambar di bawah - adalah salah satu corak reka bentuk termudah, ia memberikan sejumlah perangkap untuk pemaju Java yang tidak berhati-hati. Artikel ini membincangkan corak reka bentuk Singleton dan menangani masalah tersebut.

Lebih banyak mengenai corak reka bentuk Java

Anda boleh membaca semua lajur Pola Reka Bentuk Java David Geary , atau melihat senarai artikel terbaru JavaWorld mengenai corak reka bentuk Java. Lihat " Corak reka bentuk, gambaran besar " untuk perbincangan mengenai kebaikan dan keburukan penggunaan corak Gang of Four. Mahu lebih? Dapatkan surat berita Enterprise Java dihantar ke peti masuk anda.

Corak Singleton

Dalam Corak Reka Bentuk: Elemen Perisian Berorientasikan Objek yang Boleh Digunakan Semula , Gang of Four menerangkan corak Singleton seperti ini:

Pastikan kelas hanya mempunyai satu contoh, dan berikan pusat akses global ke kelas tersebut.

Gambar di bawah menggambarkan rajah kelas corak reka bentuk Singleton.

Seperti yang anda lihat, tidak banyak corak reka bentuk Singleton. Singleton mengekalkan rujukan statik ke satu-satunya instance tunggal dan mengembalikan rujukan ke contoh itu dari instance()kaedah statik .

Contoh 1 menunjukkan pelaksanaan corak reka bentuk Singleton klasik:

Contoh 1. Singleton klasik

public class ClassicSingleton { private static ClassicSingleton instance = null; protected ClassicSingleton() { // Exists only to defeat instantiation. } public static ClassicSingleton getInstance() { if(instance == null) { instance = new ClassicSingleton(); } return instance; } }

Singleton yang dilaksanakan dalam Contoh 1 senang difahami. The ClassicSingletonkelas mengekalkan rujukan statik untuk singleton contoh tunggal dan pulangan yang rujukan dari statik getInstance()kaedah.

Terdapat beberapa perkara menarik mengenai ClassicSingletonkelas. Pertama, ClassicSingletonmenggunakan teknik yang dikenali sebagai pemalas malas untuk membuat singleton; sebagai hasilnya, contoh tunggal tidak dibuat sehingga getInstance()kaedah dipanggil untuk pertama kalinya. Teknik ini memastikan bahawa contoh tunggal dibuat hanya apabila diperlukan.

Kedua, perhatikan bahawa ClassicSingletonmenerapkan konstruktor yang dilindungi sehingga pelanggan tidak dapat membuat ClassicSingletoncontoh; namun, anda mungkin terkejut apabila mengetahui bahawa kod berikut adalah sah:

public class SingletonInstantiator { public SingletonInstantiator() { ClassicSingleton instance = ClassicSingleton.getInstance(); ClassicSingleton anotherInstance =new ClassicSingleton(); ... } }

Bagaimana kelas dalam fragmen kod sebelumnya - yang tidak meluas - dapat ClassicSingletonmembuat ClassicSingletoncontoh jika ClassicSingletonkonstruktor dilindungi? Jawapannya adalah bahawa pembina yang dilindungi dapat dipanggil oleh subkelas dan oleh kelas lain dalam pakej yang sama . Kerana ClassicSingletondan SingletonInstantiatordalam paket yang sama (paket lalai), SingletonInstantiator()kaedah dapat membuat ClassicSingletoncontoh. Dilema ini mempunyai dua penyelesaian: Anda boleh menjadikan ClassicSingletonkonstruktor itu peribadi sehingga hanya ClassicSingleton()kaedah yang menyebutnya; namun, itu ClassicSingletontidak boleh dikelaskan. Kadang-kadang, itu adalah penyelesaian yang diinginkan; jika ya, ada baiknya anda menyatakan kelas single andafinal, yang menjadikan niat itu eksplisit dan membolehkan penyusun menggunakan pengoptimuman prestasi. Penyelesaian lain adalah meletakkan kelas singleton anda dalam pakej eksplisit, jadi kelas dalam pakej lain (termasuk pakej lalai) tidak dapat memberi contoh kejadian tunggal.

Titik menarik ketiga mengenai ClassicSingleton: ada kemungkinan terdapat banyak contoh single jika kelas yang dimuat oleh pemuat kelas yang berbeza mengakses singleton. Senario itu tidak terlalu tepat; sebagai contoh, beberapa kontena servlet menggunakan pemuat kelas yang berbeza untuk setiap servlet, jadi jika dua servlet mengakses singleton, masing-masing mempunyai contoh tersendiri.

Keempat, jika ClassicSingletonmengimplementasikan java.io.Serializableantara muka, contoh kelas dapat dibuat secara bersiri dan deserialisasi. Walau bagaimanapun, jika anda membuat satu siri objek tunggal dan kemudiannya mendeserialisasikan objek tersebut lebih dari satu kali, anda akan mempunyai banyak kejadian tunggal.

Akhirnya, dan mungkin yang paling penting, ClassicSingletonkelas Contoh 1 tidak selamat menggunakan utas. Sekiranya dua utas — kami akan memanggilnya Thread 1 dan Thread 2 — call ClassicSingleton.getInstance()pada masa yang sama, dua ClassicSingletoncontoh boleh dibuat jika Thread 1 diprediksi tepat setelah memasuki ifblok dan kawalan seterusnya diberikan kepada Thread 2.

Seperti yang anda lihat dari perbincangan sebelumnya, walaupun corak Singleton adalah salah satu corak reka bentuk termudah, menerapkannya di Java adalah sesuatu yang sederhana. Selebihnya artikel ini membahas pertimbangan khusus Java untuk corak Singleton, tetapi pertama-tama mari kita jalan memutar pendek untuk melihat bagaimana anda dapat menguji kelas singleton anda.

Uji single

Sepanjang artikel ini, saya menggunakan JUnit bersama dengan log4j untuk menguji kelas singlet. Sekiranya anda tidak biasa dengan JUnit atau log4j, lihat Sumber.

Contoh 2 menyenaraikan kes ujian JUnit yang menguji singleton Contoh 1:

Contoh 2. Kes ujian tunggal

import org.apache.log4j.Logger; import junit.framework.Assert; import junit.framework.TestCase; public class SingletonTest extends TestCase { private ClassicSingleton sone = null, stwo = null; private static Logger logger = Logger.getRootLogger(); public SingletonTest(String name) { super(name); } public void setUp() { logger.info("getting singleton..."); sone = ClassicSingleton.getInstance(); logger.info("...got singleton: " + sone); logger.info("getting singleton..."); stwo = ClassicSingleton.getInstance(); logger.info("...got singleton: " + stwo); } public void testUnique() { logger.info("checking singletons for equality"); Assert.assertEquals(true, sone == stwo); } }

Contoh kes ujian 2 memanggil ClassicSingleton.getInstance()dua kali dan menyimpan rujukan yang dikembalikan dalam pemboleh ubah anggota. The testUnique()kaedah memeriksa untuk melihat bahawa rujukan adalah sama. Contoh 3 menunjukkan bahawa output kes ujian:

Contoh 3. Uji kes keluaran

Buildfile: build.xml init: [echo] Build 20030414 (14-04-2003 03:08) compile: run-test-text: [java] .INFO main: getting singleton... [java] INFO main: created singleton: [email protected] [java] INFO main: ...got singleton: [email protected] [java] INFO main: getting singleton... [java] INFO main: ...got singleton: [email protected] [java] INFO main: checking singletons for equality [java] Time: 0.032 [java] OK (1 test)

Seperti yang ditunjukkan oleh senarai sebelumnya, ujian sederhana Contoh 2 berlalu dengan warna terbang — kedua-dua rujukan tunggal yang diperolehi ClassicSingleton.getInstance()memang serupa; namun, rujukan tersebut diperoleh dalam satu utas. Bahagian seterusnya menguji kelas singleton kami dengan pelbagai utas.

Pertimbangan multithreading

ClassicSingleton.getInstance()Kaedah Contoh 1 tidak selamat dari benang kerana kod berikut:

1: if(instance == null) { 2: instance = new Singleton(); 3: }

If a thread is preempted at Line 2 before the assignment is made, the instance member variable will still be null, and another thread can subsequently enter the if block. In that case, two distinct singleton instances will be created. Unfortunately, that scenario rarely occurs and is therefore difficult to produce during testing. To illustrate this thread Russian roulette, I've forced the issue by reimplementing Example 1's class. Example 4 shows the revised singleton class:

Example 4. Stack the deck

import org.apache.log4j.Logger; public class Singleton { private static Singleton singleton = null; private static Logger logger = Logger.getRootLogger(); private static boolean firstThread = true; protected Singleton() { // Exists only to defeat instantiation. } public static Singleton getInstance() { if(singleton == null) { simulateRandomActivity(); singleton = new Singleton(); } logger.info("created singleton: " + singleton); return singleton; } private static void simulateRandomActivity() { try { if(firstThread) { firstThread = false; logger.info("sleeping..."); // This nap should give the second thread enough time // to get by the first thread.Thread.currentThread().sleep(50); } } catch(InterruptedException ex) { logger.warn("Sleep interrupted"); } } }

Example 4's singleton resembles Example 1's class, except the singleton in the preceding listing stacks the deck to force a multithreading error. The first time the getInstance() method is called, the thread that invoked the method sleeps for 50 milliseconds, which gives another thread time to call getInstance() and create a new singleton instance. When the sleeping thread awakes, it also creates a new singleton instance, and we have two singleton instances. Although Example 4's class is contrived, it stimulates the real-world situation where the first thread that calls getInstance() gets preempted.

Example 5 tests Example 4's singleton:

Example 5. A test that fails

import org.apache.log4j.Logger; import junit.framework.Assert; import junit.framework.TestCase; public class SingletonTest extends TestCase { private static Logger logger = Logger.getRootLogger(); private static Singleton singleton = null; public SingletonTest(String name) { super(name); } public void setUp() { singleton = null; } public void testUnique() throws InterruptedException { // Both threads call Singleton.getInstance(). Thread threadOne = new Thread(new SingletonTestRunnable()), threadTwo = new Thread(new SingletonTestRunnable()); threadOne.start();threadTwo.start(); threadOne.join(); threadTwo.join(); } private static class SingletonTestRunnable implements Runnable { public void run() { // Get a reference to the singleton. Singleton s = Singleton.getInstance(); // Protect singleton member variable from // multithreaded access. synchronized(SingletonTest.class) { if(singleton == null) // If local reference is null... singleton = s; // ...set it to the singleton } // Local reference must be equal to the one and // only instance of Singleton; otherwise, we have two // Singleton instances. Assert.assertEquals(true, s == singleton); } } }

Example 5's test case creates two threads, starts each one, and waits for them to finish. The test case maintains a static reference to a singleton instance, and each thread calls Singleton.getInstance(). If the static member variable has not been set, the first thread sets it to the singleton obtained with the call to getInstance(), and the static member variable is compared to the local variable for equality.

Inilah yang berlaku apabila kes ujian dijalankan: Benang pertama memanggil getInstance(), memasuki ifblok, dan tidur. Selepas itu, utas kedua juga memanggil getInstance()dan membuat contoh tunggal. Thread kedua kemudian menetapkan pemboleh ubah anggota statik ke instance yang dibuatnya. Urutan kedua memeriksa pemboleh ubah anggota statik dan salinan tempatan untuk kesamaan, dan ujian lulus. Apabila utas pertama terbangun, ia juga membuat contoh tunggal, tetapi utas itu tidak menetapkan pemboleh ubah anggota statik (kerana utas kedua sudah menetapkannya), jadi pemboleh ubah statik dan pemboleh ubah tempatan tidak selari, dan ujian kerana kesamarataan gagal. Contoh 6 menyenaraikan output kes ujian Contoh 5: