Petua Java 67: Instansiasi malas

Tidak lama dahulu kita gembira dengan prospek mempunyai memori on-board dalam lonjakan mikrokomputer 8-bit dari 8 KB ke 64 KB. Dilihat dari aplikasi yang semakin meningkat dan lapar sumber yang kita gunakan sekarang, sangat mengagumkan bahawa ada orang yang berjaya menulis program untuk memasukkan sedikit memori. Walaupun kita mempunyai lebih banyak ingatan untuk dimainkan hari ini, beberapa pelajaran berharga dapat dipelajari dari teknik-teknik yang ada untuk bekerja dalam kekangan yang ketat.

Lebih-lebih lagi, pengaturcaraan Java bukan hanya mengenai menulis applet dan aplikasi untuk penggunaan di komputer peribadi dan stesen kerja; Java juga telah melaju ke pasar sistem tertanam juga. Sistem terbenam semasa mempunyai sumber daya memori dan kekuatan pengkomputeran yang agak langka, jadi banyak masalah lama yang dihadapi oleh pengaturcara telah muncul kembali bagi pemaju Java yang bekerja di dunia perangkat.

Mengimbangi faktor-faktor ini adalah masalah reka bentuk yang menarik: Penting untuk menerima hakikat bahawa tiada penyelesaian dalam bidang reka bentuk tertanam akan sempurna. Oleh itu, kita perlu memahami jenis teknik yang akan berguna dalam mencapai keseimbangan halus yang diperlukan untuk bekerja dalam batasan platform penyebaran.

Salah satu teknik pemeliharaan memori yang difikirkan berguna oleh pengaturcara Java adalah instansiasi malas. Dengan instansiasi malas, program menahan diri dari membuat sumber tertentu sehingga sumber pertama diperlukan - membebaskan ruang memori yang berharga. Dalam tip ini, kami mengkaji teknik instansiasi malas dalam pemuatan kelas Java dan pembuatan objek, dan pertimbangan khas yang diperlukan untuk corak Singleton. Bahan dalam tip ini berasal dari karya dalam Bab 9 buku kami, Java in Practice: Styles Styles & Idioms for Effective Java (lihat Sumber).

Contoh yang bersemangat dan malas: contoh

Sekiranya anda sudah biasa dengan penyemak imbas Web Netscape dan telah menggunakan kedua-dua versi 3.x dan 4.x, sudah pasti anda dapat melihat perbezaan cara pemuatan Java. Sekiranya anda melihat skrin percikan semasa Netscape 3 dimulakan, anda akan melihat bahawa ia memuat pelbagai sumber, termasuk Java. Walau bagaimanapun, ketika anda memulakan Netscape 4.x, ia tidak memuat masa berjalan Java - ia menunggu sehingga anda mengunjungi halaman Web yang menyertakan tag. Kedua pendekatan ini menggambarkan teknik instansiasi bersemangat (memuatkannya sekiranya diperlukan) dan instansiasi malas (tunggu sehingga diminta sebelum anda memuatnya, kerana mungkin tidak diperlukan).

Terdapat kelemahan untuk kedua pendekatan: Di satu pihak, selalu memuatkan sumber yang berpotensi membuang memori berharga jika sumber itu tidak digunakan semasa sesi itu; sebaliknya, jika belum dimuat, anda membayar harganya dari segi masa memuatkan ketika sumber pertama diperlukan.

Pertimbangkan instansiasi malas sebagai dasar pemuliharaan sumber

Instansiasi malas di Jawa tergolong dalam dua kategori:

  • Memuatkan kelas yang malas
  • Pembuatan objek malas

Memuatkan kelas yang malas

Runtime Java mempunyai instansiasi malas untuk kelas. Kelas dimuat ke dalam memori hanya ketika pertama kali dirujuk. (Mereka juga mungkin dimuat dari pelayan Web melalui HTTP terlebih dahulu.)

MyUtils.classMethod (); // panggilan pertama ke kaedah kelas statik Vektor v = Vektor baru (); // panggilan pertama ke operator baru

Pemuatan kelas malas adalah ciri penting dalam persekitaran runtime Java kerana dapat mengurangkan penggunaan memori dalam keadaan tertentu. Sebagai contoh, jika sebahagian daripada program tidak pernah dijalankan semasa sesi, kelas yang dirujuk hanya pada bahagian program tidak akan dimuat.

Pembuatan objek malas

Pembuatan objek malas digabungkan dengan pemuatan kelas yang malas. Kali pertama anda menggunakan kata kunci baru pada jenis kelas yang sebelumnya belum dimuat, runtime Java akan memuatkannya untuk anda. Pembuatan objek malas dapat mengurangkan penggunaan memori jauh lebih banyak daripada pemuatan kelas malas.

Untuk memperkenalkan konsep penciptaan objek malas, mari kita lihat contoh kod ringkas di mana Framemenggunakan MessageBoxuntuk memaparkan mesej ralat:

kelas awam MyFrame memperluas Frame {privateBoxBox mb_ = MessageBox baru (); // pembantu peribadi yang digunakan oleh showMessage void peribadi kelas ini (String message) {// tetapkan teks mesej mb_.setMessage (mesej); mb_.pack (); mb_.show (); }}

Dalam contoh di atas, ketika instance MyFramedibuat, MessageBoxinstance mb_ juga dibuat. Peraturan yang sama berlaku secara berulang. Oleh itu, sebarang pemboleh ubah contoh yang diinisialisasi atau ditugaskan dalam MessageBoxkonstruktor kelas juga diperuntukkan dari timbunan dan sebagainya. Sekiranya contoh MyFrametidak digunakan untuk menampilkan pesan ralat dalam sesi, kami membuang memori tanpa perlu.

Dalam contoh yang agak mudah ini, kita tidak akan mendapat banyak keuntungan. Tetapi jika anda menganggap kelas yang lebih kompleks, yang menggunakan banyak kelas lain, yang pada gilirannya menggunakan dan menjadikan lebih banyak objek secara berulang, potensi penggunaan memori lebih jelas.

Pertimbangkan instansiasi malas sebagai dasar untuk mengurangkan keperluan sumber

Pendekatan malas untuk contoh di atas disenaraikan di bawah, di mana object mb_terdapat pada panggilan pertama ke showMessage(). (Maksudnya, tidak sampai ia benar-benar diperlukan oleh program.)

kelas akhir awam MyFrame memperluas Frame {privateBoxBox mb_; // null, implisit // pembantu peribadi yang digunakan oleh kelas ini void showMessage (String message) {if (mb _ == null) // panggilan pertama ke kaedah ini mb_ = newBoxBox (); // tetapkan teks mesej mb_.setMessage (mesej); mb_.pack (); mb_.show (); }}

Sekiranya anda melihat lebih dekat showMessage(), anda akan melihat bahawa kita menentukan terlebih dahulu sama ada pemboleh ubah contoh mb_ sama dengan nol. Oleh kerana kita belum menginisialisasi mb_ pada titik pengisytiharannya, waktu runtime Java telah mengurus ini untuk kita. Oleh itu, kita dapat meneruskan dengan selamat dengan membuat MessageBoxcontoh. Semua panggilan masa depan showMessage()akan mendapati bahawa mb_ tidak sama dengan nol, oleh itu melangkau penciptaan objek dan menggunakan contoh yang ada.

Contoh dunia nyata

Sekarang mari kita teliti contoh yang lebih realistik, di mana instansiasi malas boleh memainkan peranan penting dalam mengurangkan jumlah sumber yang digunakan oleh program.

Anggaplah kita telah diminta oleh pelanggan untuk menulis sistem yang akan membolehkan pengguna membuat katalog gambar pada sistem fail dan memberikan kemudahan untuk melihat gambar kecil atau gambar yang lengkap. Percubaan pertama kami mungkin ialah menulis kelas yang memuatkan gambar di konstruktornya.

kelas awam ImageFile {nama String peribadi_; gambar Imej peribadi_; publicFileFile (String nama fail) {filename_ = nama fail; // muatkan gambar} public String getName () {return filename_;} public Image getImage () {return image_; }}

Dalam contoh di atas, ImageFilemenerapkan pendekatan berlebihan untuk memberi contoh Imageobjek. Oleh itu, reka bentuk ini menjamin bahawa gambar akan tersedia dengan segera semasa panggilan ke getImage(). Namun, bukan hanya hal ini sangat lambat (dalam hal direktori yang berisi banyak gambar), tetapi reka bentuk ini dapat menghabiskan memori yang tersedia. Untuk mengelakkan masalah yang berpotensi ini, kita dapat memperdagangkan keuntungan prestasi akses seketika untuk penggunaan memori yang berkurang. Seperti yang anda duga, kami dapat mencapainya dengan menggunakan instansiasi malas.

Berikut adalah ImageFilekelas yang dikemas kini menggunakan pendekatan yang sama dengan kelas MyFramedengan MessageBoxpemboleh ubah contohnya:

kelas awam ImageFile {nama String peribadi_; gambar Imej peribadi_; // = null, ImageFile awam tersirat (String nama fail) {// hanya menyimpan nama fail nama__ nama fail; } public String getName () {return filename_;} public Image getImage () {if (image _ == null) {// panggilan pertama untuk getImage () // muatkan gambar ...} return image_; }}

Dalam versi ini, gambar sebenar hanya dimuat pada panggilan pertama ke getImage(). Oleh itu, pertukarannya adalah untuk mengurangkan keseluruhan penggunaan memori dan masa permulaan, kami membayar harga untuk memuatkan gambar pada kali pertama diminta - memperkenalkan prestasi yang berjaya pada saat itu dalam pelaksanaan program. Ini adalah ungkapan lain yang mencerminkan Proxycorak dalam konteks yang memerlukan penggunaan memori yang terhad.

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 pelbagai utas di Java boleh menjadi sangat kompleks. Sebenarnya, topik bersamaan sangat luas sehingga Doug Lea telah menulis keseluruhan buku tentangnya: Pengaturcaraan Bersama di Jawa. Sekiranya anda baru dalam pengaturcaraan serentak, kami mengesyorkan anda mendapatkan salinan buku ini sebelum anda memulakan penulisan sistem Java yang rumit yang bergantung pada banyak utas.