Bagaimana menavigasi pola Singleton yang tampak sederhana

Pola Singleton tampak sederhana, bahkan dan terutama untuk pengembang Java. Dalam artikel JavaWorld klasik ini, David Geary mendemonstrasikan bagaimana developer Java mengimplementasikan singletons, dengan contoh kode untuk multithreading, classloader, dan serialisasi menggunakan pola Singleton. Dia menyimpulkan dengan melihat penerapan registri tunggal untuk menentukan lajang pada waktu proses.

Terkadang tepat untuk memiliki tepat satu instance kelas: manajer jendela, spooler cetak, dan sistem file adalah contoh prototipe. Biasanya, jenis objek tersebut — yang dikenal sebagai singletons — diakses oleh objek yang berbeda di seluruh sistem perangkat lunak, dan oleh karena itu memerlukan titik akses global. Tentu saja, hanya ketika Anda yakin Anda tidak akan membutuhkan lebih dari satu contoh, ada baiknya Anda berubah pikiran.

Pola desain Singleton menangani semua masalah ini. Dengan pola desain Singleton Anda bisa:

  • Pastikan hanya satu instance kelas yang dibuat
  • Berikan titik akses global ke objek
  • Izinkan beberapa instance di masa mendatang tanpa memengaruhi klien kelas tunggal

Meskipun pola desain Singleton — seperti yang dibuktikan di bawah ini oleh gambar di bawah ini — adalah salah satu pola desain yang paling sederhana, pola ini menghadirkan sejumlah kesulitan bagi pengembang Java yang tidak waspada. Artikel ini membahas pola desain Singleton dan mengatasi masalah tersebut.

Lebih lanjut tentang pola desain Java

Anda dapat membaca semua kolom Pola Desain Java David Geary , atau melihat daftar artikel JavaWorld terbaru tentang pola desain Java. Lihat " Pola desain, gambaran besarnya " untuk diskusi tentang pro dan kontra penggunaan pola Gang of Four. Ingin lebih? Dapatkan buletin Enterprise Java dikirim ke kotak masuk Anda.

Pola Singleton

Dalam Design Patterns: Elements of Reusable Object-Oriented Software , Gang of Four menggambarkan pola Singleton seperti ini:

Pastikan kelas hanya memiliki satu instance, dan berikan titik akses global ke sana.

Gambar di bawah mengilustrasikan diagram kelas pola desain Singleton.

Seperti yang Anda lihat, tidak banyak hal pada pola desain Singleton. Singletons memelihara referensi statis ke satu-satunya instance tunggal dan mengembalikan referensi ke instance itu dari instance()metode statis .

Contoh 1 menunjukkan implementasi pola desain 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 diterapkan di Contoh 1 mudah dipahami. The ClassicSingletonkelas mempertahankan referensi statis untuk contoh tunggal tunggal dan pengembalian yang referensi dari statis getInstance()metode.

Ada beberapa poin menarik tentang ClassicSingletonkelas. Pertama, ClassicSingletongunakan teknik yang disebut lazy instantiation untuk membuat singleton; Akibatnya, instance tunggal tidak dibuat hingga getInstance()metode tersebut dipanggil untuk pertama kalinya. Teknik ini memastikan bahwa instance tunggal dibuat hanya jika diperlukan.

Kedua, perhatikan bahwa ClassicSingletonmengimplementasikan konstruktor yang dilindungi sehingga klien tidak dapat membuat ClassicSingletoninstance; namun, Anda mungkin terkejut saat mengetahui bahwa kode berikut benar-benar legal:

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

Bagaimana kelas dalam fragmen kode sebelumnya — yang tidak diperluas ClassicSingleton—membuat ClassicSingletoninstance jika ClassicSingletonkonstruktor dilindungi? Jawabannya adalah bahwa konstruktor yang dilindungi dapat dipanggil oleh subclass dan oleh kelas lain dalam paket yang sama . Karena ClassicSingletondan SingletonInstantiatorberada dalam paket yang sama (paket default), SingletonInstantiator()metode dapat membuat ClassicSingletoninstance. Dilema ini memiliki dua solusi: Anda dapat membuat ClassicSingletonkonstruktor privat sehingga hanya ClassicSingleton()metode yang memanggilnya; namun, itu berarti ClassicSingletontidak dapat disubkelas. Terkadang, itu adalah solusi yang diinginkan; jika demikian, ada baiknya untuk mendeklarasikan kelas singleton Andafinal, yang membuat maksud tersebut menjadi eksplisit dan memungkinkan compiler untuk menerapkan pengoptimalan performa. Solusi lainnya adalah menempatkan kelas singleton Anda dalam paket eksplisit, sehingga kelas dalam paket lain (termasuk paket default) tidak dapat membuat instance tunggal.

Hal menarik ketiga tentang ClassicSingleton: adalah mungkin untuk memiliki beberapa instance tunggal jika class yang dimuat oleh classloader yang berbeda mengakses sebuah singleton. Skenario itu tidak terlalu dibuat-buat; misalnya, beberapa kontainer servlet menggunakan classloader yang berbeda untuk setiap servlet, jadi jika dua servlet mengakses sebuah singleton, masing-masing akan memiliki instance-nya sendiri.

Keempat, jika ClassicSingletonmengimplementasikan java.io.Serializableantarmuka, instance kelas dapat diserialkan dan dideserialisasi. Namun, jika Anda membuat serial objek tunggal dan kemudian melakukan deserialisasi objek itu lebih dari sekali, Anda akan memiliki beberapa contoh tunggal.

Terakhir, dan mungkin yang paling penting, ClassicSingletonkelas Contoh 1 tidak aman untuk thread. Jika dua utas — kami akan menyebutnya Utas 1 dan Utas 2 — memanggil ClassicSingleton.getInstance()pada saat yang sama, dua ClassicSingletoncontoh dapat dibuat jika Utas 1 didahului tepat setelah memasuki ifblok dan kontrol selanjutnya diberikan ke Utas 2.

Seperti yang Anda lihat dari pembahasan sebelumnya, meskipun pola Singleton adalah salah satu pola desain yang paling sederhana, mengimplementasikannya di Java sama sekali tidak sederhana. Sisa artikel ini membahas pertimbangan khusus Java untuk pola Singleton, tetapi pertama-tama mari kita mengambil jalan memutar singkat untuk melihat bagaimana Anda dapat menguji kelas tunggal Anda.

Uji lajang

Sepanjang sisa artikel ini, saya menggunakan JUnit bersama dengan log4j untuk menguji kelas tunggal. Jika Anda tidak terbiasa dengan JUnit atau log4j, lihat Sumberdaya.

Contoh 2 mencantumkan kasus uji JUnit yang menguji singleton Contoh 1:

Contoh 2. Kasus uji 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); } }

Kasus uji Contoh 2 dipanggil ClassicSingleton.getInstance()dua kali dan menyimpan referensi yang dikembalikan dalam variabel anggota. The testUnique()Metode pemeriksaan untuk melihat bahwa referensi identik. Contoh 3 menunjukkan bahwa keluaran kasus uji:

Contoh 3. Keluaran kasus uji

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 diilustrasikan oleh daftar sebelumnya, uji sederhana Contoh 2 lolos dengan warna-warna terbang — dua referensi tunggal yang diperoleh dengan ClassicSingleton.getInstance()memang identik; Namun, referensi tersebut diperoleh dalam satu utas. Bagian selanjutnya menguji kelas tunggal kita dengan beberapa utas.

Pertimbangan multithreading

ClassicSingleton.getInstance()Metode contoh 1 tidak aman untuk thread karena kode 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 terjadi saat kasus uji berjalan: Thread pertama memanggil getInstance(), memasuki ifblok, dan tidur. Selanjutnya, thread kedua juga memanggil getInstance()dan membuat instance tunggal. Utas kedua kemudian menetapkan variabel anggota statis ke instance yang dibuatnya. Utas kedua memeriksa variabel anggota statis dan salinan lokal untuk kesetaraan, dan pengujian berhasil. Ketika utas pertama aktif, itu juga membuat instance tunggal, tetapi utas itu tidak menyetel variabel anggota statis (karena utas kedua telah menyetelnya), sehingga variabel statis dan variabel lokal tidak selaras, dan pengujian karena kesetaraan gagal. Contoh 6 daftar keluaran kasus uji Contoh 5: