Tip Java 67: Instansiasi malas

Belum lama berselang kami sangat senang dengan prospek memiliki memori on-board dalam lompatan mikrokomputer 8-bit dari 8 KB menjadi 64 KB. Dilihat dari aplikasi yang terus meningkat dan membutuhkan sumber daya yang sekarang kita gunakan, sungguh menakjubkan bahwa ada orang yang pernah berhasil menulis program agar sesuai dengan jumlah memori yang kecil itu. Sementara kita memiliki lebih banyak memori untuk dimainkan saat ini, beberapa pelajaran berharga dapat dipelajari dari teknik yang ditetapkan untuk bekerja dalam batasan yang ketat seperti itu.

Selain itu, pemrograman Java tidak hanya tentang menulis applet dan aplikasi untuk penyebaran pada komputer dan workstation pribadi; Java juga telah membuat terobosan kuat ke pasar sistem tertanam. Sistem tertanam saat ini memiliki sumber daya memori dan daya komputasi yang relatif langka, sehingga banyak masalah lama yang dihadapi programmer telah muncul kembali untuk pengembang Java yang bekerja di ranah perangkat.

Menyeimbangkan faktor-faktor ini adalah masalah desain yang menarik: Penting untuk menerima kenyataan bahwa tidak ada solusi di bidang desain yang disematkan yang akan sempurna. Jadi, kita perlu memahami jenis teknik yang akan berguna dalam mencapai keseimbangan yang diperlukan untuk bekerja dalam batasan platform penerapan.

Salah satu teknik konservasi memori yang berguna bagi programmer Java adalah lazy instantiation. Dengan lazy instantiation, program menahan diri dari membuat sumber daya tertentu hingga sumber daya tersebut dibutuhkan pertama kali - membebaskan ruang memori yang berharga. Dalam tip ini, kita memeriksa teknik lazy instantiation dalam pemuatan kelas Java dan pembuatan objek, dan pertimbangan khusus yang diperlukan untuk pola Singleton. Materi dalam tip ini berasal dari pekerjaan di Bab 9 buku kami, Java in Practice: Design Styles & Idiom for Effective Java (lihat Sumberdaya).

Instansiasi bersemangat vs. malas: sebuah contoh

Jika Anda terbiasa dengan browser Web Netscape dan telah menggunakan versi 3.x dan 4.x, Anda pasti telah melihat perbedaan dalam cara pemuatan runtime Java. Jika Anda melihat layar pembuka saat Netscape 3 dijalankan, Anda akan melihat bahwa ia memuat berbagai sumber daya, termasuk Java. Namun, saat Anda menjalankan Netscape 4.x, ia tidak memuat runtime Java - ia menunggu hingga Anda mengunjungi halaman Web yang menyertakan tag tersebut. Kedua pendekatan ini menggambarkan teknik eager instantiation (muat jika diperlukan) dan lazy instantiation (tunggu hingga diminta sebelum Anda memuatnya, karena mungkin tidak pernah diperlukan).

Ada kekurangan dari kedua pendekatan ini: Di ​​satu sisi, selalu memuat sumber daya berpotensi membuang memori yang berharga jika sumber daya tidak digunakan selama sesi itu; di sisi lain, jika belum dimuat, Anda membayar harga dalam hal waktu pemuatan saat sumber daya pertama kali diperlukan.

Pertimbangkan lazy instantiation sebagai kebijakan konservasi sumber daya

Instansiasi malas di Java terbagi dalam dua kategori:

  • Pemuatan kelas malas
  • Pembuatan objek malas

Pemuatan kelas malas

Runtime Java memiliki instance malas bawaan untuk kelas. Kelas memuat ke dalam memori hanya saat mereka pertama kali direferensikan. (Mereka juga dapat dimuat dari server Web melalui HTTP terlebih dahulu.)

MyUtils.classMethod (); // panggilan pertama ke metode kelas statis Vector v = new Vector (); // panggilan pertama ke operator baru

Pemuatan kelas malas adalah fitur penting dari lingkungan runtime Java karena dapat mengurangi penggunaan memori dalam keadaan tertentu. Misalnya, jika bagian dari program tidak pernah dijalankan selama sesi, kelas yang direferensikan hanya di bagian program tersebut tidak akan pernah dimuat.

Pembuatan objek malas

Pembuatan objek yang malas terkait erat dengan pemuatan kelas yang lambat. Pertama kali Anda menggunakan kata kunci baru pada jenis kelas yang sebelumnya belum dimuat, runtime Java akan memuatnya untuk Anda. Pembuatan objek yang lambat dapat mengurangi penggunaan memori jauh lebih besar daripada pemuatan kelas yang lambat.

Untuk memperkenalkan konsep pembuatan objek malas, mari kita lihat contoh kode sederhana di mana a Framemenggunakan a MessageBoxuntuk menampilkan pesan kesalahan:

public class MyFrame extends Frame {private MessageBox mb_ = new MessageBox (); // private helper yang digunakan oleh kelas ini private void showMessage (String message) {// setel teks pesan mb_.setMessage (message); mb_.pack (); mb_.show (); }}

Dalam contoh di atas, ketika sebuah instance MyFramedibuat, MessageBoxinstance mb_ juga dibuat. Aturan yang sama berlaku secara rekursif. Jadi setiap variabel instan yang diinisialisasi atau ditugaskan di MessageBoxkonstruktor kelas juga dialokasikan dari heap dan seterusnya. Jika contoh dari MyFrametidak digunakan untuk menampilkan pesan kesalahan dalam sebuah sesi, kami membuang-buang memori secara tidak perlu.

Dalam contoh yang agak sederhana ini, kita tidak akan mendapatkan terlalu banyak. Tetapi jika Anda mempertimbangkan kelas yang lebih kompleks, yang menggunakan banyak kelas lain, yang pada gilirannya menggunakan dan membuat instance lebih banyak objek secara rekursif, potensi penggunaan memori lebih jelas.

Pertimbangkan lazy instantiation sebagai kebijakan untuk mengurangi persyaratan resource

Pendekatan malas untuk contoh di atas dicantumkan di bawah ini, di mana object mb_dibuat instance-nya pada panggilan pertama ke showMessage(). (Artinya, tidak sampai benar-benar dibutuhkan oleh program.)

public final class MyFrame extends Frame {private MessageBox mb_; // null, implisit // pembantu pribadi yang digunakan oleh kelas ini private void showMessage (String message) {if (mb _ == null) // panggilan pertama ke metode ini mb_ = new MessageBox (); // setel teks pesan mb_.setMessage (message); mb_.pack (); mb_.show (); }}

Jika Anda melihat lebih dekat showMessage(), Anda akan melihat bahwa kami pertama-tama menentukan apakah variabel instance mb_ sama dengan null. Karena kita belum menginisialisasi mb_ pada titik deklarasinya, runtime Java telah menangani ini untuk kita. Jadi, kita dapat melanjutkan dengan aman dengan membuat MessageBoxinstance. Semua panggilan selanjutnya ke showMessage()akan menemukan bahwa mb_ tidak sama dengan null, oleh karena itu melewatkan pembuatan objek dan menggunakan instance yang ada.

Contoh dunia nyata

Sekarang mari kita periksa contoh yang lebih realistis, di mana lazy instantiation dapat memainkan peran kunci dalam mengurangi jumlah resource yang digunakan oleh sebuah program.

Asumsikan bahwa kita telah diminta oleh klien untuk menulis sistem yang akan memungkinkan pengguna membuat katalog gambar pada sistem file dan menyediakan fasilitas untuk melihat thumbnail atau gambar lengkap. Upaya pertama kami mungkin menulis kelas yang memuat gambar di konstruktornya.

public class ImageFile {private String filename_; gambar pribadi image_; publik ImageFile (String nama file) {filename_ = nama file; // muat gambar} public String getName () {return filename_;} public Image getImage () {return image_; }}

Dalam contoh di atas, ImageFilemengimplementasikan pendekatan yang berlebihan untuk membuat instance Imageobjek. Yang menguntungkan, desain ini menjamin bahwa gambar akan segera tersedia pada saat panggilan ke getImage(). Namun, ini tidak hanya bisa sangat lambat (dalam kasus direktori yang berisi banyak gambar), tetapi desain ini dapat menghabiskan memori yang tersedia. Untuk menghindari potensi masalah ini, kami dapat menukar manfaat kinerja dari akses instan dengan pengurangan penggunaan memori. Seperti yang sudah Anda duga, kami dapat melakukannya dengan menggunakan lazy instantiation.

Berikut ImageFilekelas yang diperbarui menggunakan pendekatan yang sama seperti yang MyFramedilakukan kelas dengan MessageBoxvariabel instansinya:

public class ImageFile {private String filename_; gambar pribadi image_; // = null, imageFile publik implisit (String nama file) {// hanya menyimpan nama file namafile_ = namafile; } public String getName () {return filename_;} public Image getImage () {if (image _ == null) {// panggilan pertama ke getImage () // muat image ...} return image_; }}

Dalam versi ini, gambar sebenarnya dimuat hanya pada panggilan pertama ke getImage(). Singkatnya, trade-off di sini adalah untuk mengurangi penggunaan memori secara keseluruhan dan waktu startup, kami membayar harga untuk memuat gambar saat pertama kali diminta - memperkenalkan kinerja yang berhasil pada saat itu dalam eksekusi program. Ini adalah idiom lain yang mencerminkan Proxypola dalam konteks yang membutuhkan penggunaan memori yang terbatas.

The policy of lazy instantiation illustrated above is fine for our examples, but later on you'll see how the design has to alter in the context of multiple threads.

Lazy instantiation for Singleton patterns in Java

Let's now take a look at the Singleton pattern. Here's the generic form in Java:

public class Singleton { private Singleton() {} static private Singleton instance_ = new Singleton(); static public Singleton instance() { return instance_; } //public methods } 

In the generic version, we declared and initialized the instance_ field as follows:

static final Singleton instance_ = new Singleton(); 

Readers familiar with the C++ implementation of Singleton written by the GoF (the Gang of Four who wrote the book Design Patterns: Elements of Reusable Object-Oriented Software -- Gamma, Helm, Johnson, and Vlissides) may be surprised that we didn't defer the initialization of the instance_ field until the call to the instance() method. Thus, using lazy instantiation:

public static Singleton instance() { if(instance_==null) //Lazy instantiation instance_= new Singleton(); return instance_; } 

The listing above is a direct port of the C++ Singleton example given by the GoF, and frequently is touted as the generic Java version too. If you already are familiar with this form and were surprised that we didn't list our generic Singleton like this, you'll be even more surprised to learn that it is totally unnecessary in Java! This is a common example of what can occur if you port code from one language to another without considering the respective runtime environments.

For the record, the GoF's C++ version of Singleton uses lazy instantiation because there is no guarantee of the order of static initialization of objects at runtime. (See Scott Meyer's Singleton for an alternative approach in C++ .) In Java, we don't have to worry about these issues.

The lazy approach to instantiating a Singleton is unnecessary in Java because of the way in which the Java runtime handles class loading and static instance variable initialization. Previously, we have described how and when classes get loaded. A class with only public static methods gets loaded by the Java runtime on the first call to one of these methods; which in the case of our Singleton is

Singleton s=Singleton.instance(); 

The first call to Singleton.instance() in a program forces the Java runtime to load the class Singleton. As the field instance_ is declared as static, the Java runtime will initialize it after successfully loading the class. Thus guarantees that the call to Singleton.instance() will return a fully initialized Singleton -- get the picture?

Lazy instantiation: dangerous in multithreaded applications

Using lazy instantiation for a concrete Singleton is not only unnecessary in Java, it's downright dangerous in the context of multithreaded applications. Consider the lazy version of the Singleton.instance() method, where two or more separate threads are attempting to obtain a reference to the object via instance(). If one thread is preempted after successfully executing the line if(instance_==null), but before it has completed the line instance_=new Singleton(), another thread can also enter this method with instance_ still ==null -- nasty!

The outcome of this scenario is the likelihood that one or more Singleton objects will be created. This is a major headache when your Singleton class is, say, connecting to a database or remote server. The simple solution to this problem would be to use the synchronized key word to protect the method from multiple threads entering it at the same time:

synchronized static public instance() {...} 

However, this approach is a bit heavy-handed for most multithreaded applications using a Singleton class extensively, thereby causing blocking on concurrent calls to instance(). By the way, invoking a synchronized method is always much slower than invoking a nonsynchronized one. So what we need is a strategy for synchronization that doesn't cause unnecessary blocking. Fortunately, such a strategy exists. It is known as the double-check idiom.

The double-check idiom

Use the double-check idiom to protect methods using lazy instantiation. Here's how to implement it in Java:

public static Singleton instance() { if(instance_==null) //don't want to block here { //two or more threads might be here!!! synchronized(Singleton.class) { //must check again as one of the //blocked threads can still enter if(instance_==null) instance_= new Singleton();//safe } } return instance_; } 

The double-check idiom improves performance by using synchronization only if multiple threads call instance() before the Singleton is constructed. Once the object has been instantiated, instance_ is no longer ==null, allowing the method to avoid blocking concurrent callers.

Menggunakan banyak utas di Java bisa sangat rumit. Faktanya, topik konkurensi sangat luas sehingga Doug Lea telah menulis seluruh buku tentangnya: Pemrograman Konkurensi di Java. Jika Anda baru mengenal pemrograman serentak, kami sarankan Anda mendapatkan salinan buku ini sebelum Anda mulai menulis sistem Java kompleks yang mengandalkan banyak utas.