Hướng dẫn Java Design Pattern – Singleton

Thỉnh thoảng, trong quá trình phân tích thiết kế một hệ thống, chúng ta mong muốn sở hữu những đối tượng cần tồn tại duy nhất và sở hữu thể truy xuất mọi lúc mọi nơi. Làm thế nào để hiện thực được một đối tượng như thế lúc xây dựng mã nguồn? Chúng ta sở hữu thể nghĩ tới việc sử dụng một biến toàn cục (global variable : public static final). Tuy nhiên, việc sử dụng biến toàn cục nó phá vỡ quy tắc của OOP (encapsulation). Để giải bài toán trên, người ta hướng tới một giải pháp là sử dụng Singleton pattern.

Singleton Pattern là gì?

Singleton là một trong 5 design pattern của nhóm Creational Design Pattern.

Singleton đảm bảo chỉ duy nhất một thể hiện (instance) được tạo ra và nó sẽ sản xuất cho bạn một method để sở hữu thể truy xuất được thể hiện duy nhất đó mọi lúc mọi nơi trong chương trình.

Sử dụng Singleton lúc chúng ta muốn:

  • Đảm bảo rằng chỉ sở hữu một instance của lớp.
  • Việc quản lý việc truy cập tốt hơn vì chỉ sở hữu một thể hiện duy nhất.
  • Sở hữu thể quản lý số lượng thể hiện của một lớp trong giớn hạn chỉ định.

Implement Singleton Pattern như thế nào?

Sở hữu rất nhiều cách để implement Singleton Pattern. Nhưng dù cho việc implement bằng cách nào đi nữa cũng dựa vào nguyên tắc dưới đây cơ bản dưới đây:

  • private constructor để hạn chế truy cập từ class bên ngoài.
  • Đặt private static final variable đảm bảo biến chỉ được khởi tạo trong class.
  • Sở hữu một method public static để return instance được khởi tạo ở trên.

Những cách nào để implement Singleton Pattern

Dựa trên những nguyên tắc thiết kế Singleton ở trên, chúng ta sở hữu những cách implement singleton như sau:

Eager initialization

Singleton Class được khởi tạo ngay lúc được gọi tới. Đây là cách dễ nhất nhưng nó sở hữu một nhược điểm mặc dù instance đã được khởi tạo mà sở hữu thể sẽ ko sử dụng tới.

Ví dụ:

 package com.gpcoder.patterns.creational.singleton; public class EagerInitializedSingleton {	private static final EagerInitializedSingleton INSTANCE = new EagerInitializedSingleton();	// Private constructor to avoid client applications to use constructor	private EagerInitializedSingleton() {	}	public static EagerInitializedSingleton getInstance() {	return INSTANCE;	} } 

Eager initialization là cách tiếp cận tốt, dễ cài đặt, tuy nhiên, nó thuận tiện bị phá vỡ bởi Reflection.

Static block initialization

Cách làm tương tự như Eager initialization chỉ khác phần static block sản xuất thêm lựa chọn cho việc handle exception hay những xử lý khác.

Ví dụ:

 package com.gpcoder.patterns.creational.singleton; public class StaticBlockSingleton {	private static final StaticBlockSingleton INSTANCE;	private StaticBlockSingleton() {	}	// Static block initialization for exception handling	static {	try {	INSTANCE = new StaticBlockSingleton();	} catch (Exception e) {	throw new RuntimeException("Exception occured in creating singleton instance");	}	}	public static StaticBlockSingleton getInstance() {	return INSTANCE;	} } 

Lazy Initialization

Là một cách làm mang tính mở rộng hơn so với hai cách làm trên và hoạt động tốt trong môi trường đơn luồng (single-thread).

Ví dụ:

 package com.gpcoder.patterns.creational.singleton; public class LazyInitializedSingleton {	private static LazyInitializedSingleton instance;	private LazyInitializedSingleton() {	}	public static LazyInitializedSingleton getInstance() {	if (instance == null) {	instance = new LazyInitializedSingleton();	}	return instance;	} } 

Cách này đã khắc phục được nhược điểm của cách Eager initialization, chỉ lúc nào getInstance() được gọi thì instance mới được khởi tạo. Tuy nhiên, cách này chỉ sử dụng tốt trong trường hợp đơn luồng (single-thread), trường hợp nếu có nhiều luồng (multi-thread) cùng chạy và cùng gọi hàm getInstance() tại cùng một thời điểm thì sở hữu thể sở hữu nhiều hơn một thể hiện của instance. Để khắc phục nhược điểm này chúng ta sử dụng Thread Safe Singleton.

Một nhược điểm nữa của Lazy Initialization cần quan tâm là: đối với thao tác create instance quá chậm thì người sử dụng sở hữu phải chờ lâu cho lần sử dụng trước tiên.

Thread Safe Singleton

Cách đơn giản nhất là chúng ta gọi phương thức synchronized của hàm getInstance() và như vậy hệ thống đảm bảo rằng tại cùng một thời điểm chỉ có thể có một luồng có thể truy cập vào hàm getInstance() và đảm bảo rằng chỉ có duy nhất một thể hiện của class.

Ví dụ:

 package com.gpcoder.patterns.creational.singleton; public class ThreadSafeLazyInitializedSingleton {	private static volatile ThreadSafeLazyInitializedSingleton instance;	private ThreadSafeLazyInitializedSingleton() {	}	public static synchronized ThreadSafeLazyInitializedSingleton getInstance() {	if (instance == null) {	instance = new ThreadSafeLazyInitializedSingleton();	}	return instance;	} } 

Cách này sở hữu nhược điểm là một phương thức synchronized sẽ chạy rất chậm và tốn hiệu năng, bất kỳ Thread nào gọi tới đều phải chờ nếu sở hữu một Thread khác đang sử dụng. Sở hữu những tác vụ xử lý trước và sau lúc tạo thể hiện ko cần thiết phải block. Vì vậy chúng ta cần cải tiến nó đi một chút với Double Check Locking Singleton.

Double Check Locking Singleton

Để implement theo cách này, chúng ta sẽ rà soát sự tồn tại thể hiện của lớp, với sự hổ trợ của đồng bộ hóa, hai lần trước lúc khởi tạo. Phải khai báo volatile cho instance để tránh lớp làm việc ko xác thực do quá trình tối ưu hóa của trình biên dịch.

 package com.gpcoder.patterns.creational.singleton; public class DoubleCheckLockingSingleton {	private static volatile DoubleCheckLockingSingleton instance;	private DoubleCheckLockingSingleton() {	}	public static DoubleCheckLockingSingleton getInstance() {	// Do something before get instance ...	if (instance == null) {	// Do the task too long before create instance ...	// Block so other threads cannot come into while initialize	synchronized (DoubleCheckLockingSingleton.class) {	// Re-check again. Maybe another thread has initialized before	if (instance == null) {	instance = new DoubleCheckLockingSingleton();	}	}	}	// Do something after get instance ...	return instance;	} } 

Bill Pugh Singleton Implementation

Với cách làm này bạn sẽ tạo ra static nested class với vai trò một Helper lúc muốn tách biệt chức năng cho một class function rõ ràng hơn. Đây là cách thường hay được sử dụng và sở hữu hiệu suất tốt (theo những chuyên gia thẩm định 🙂 ).

 package com.gpcoder.patterns.creational.singleton; public class BillPughSingleton {	private BillPughSingleton() {	}	public static BillPughSingleton getInstance() {	return SingletonHelper.INSTANCE;	}	private static class SingletonHelper {	private static final BillPughSingleton INSTANCE = new BillPughSingleton();	} } 

Lúc Singleton được tải vào bộ nhớ thì SingletonHelper chưa được tải vào. Nó chỉ được tải lúc và chỉ lúc phương thức getInstance() được gọi. Với cách này tránh được lỗi cơ chế khởi tạo instance của Singleton trong Multi-Thread, performance cao do tách biệt được quá trình xử lý. Do đó, cách làm này được thẩm định là cách triển khai Singleton nhanh và hiệu quả nhất.

Phá vỡ cấu trúc Singleton Pattern bằng Reflection

Reflection sở hữu thể được sử dụng để phá vỡ Pattern của Eager Initialization ở trên. Ví dụ:

 package com.gpcoder.patterns.creational.singleton; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; public class ReflectionBreakSingleton {	public static void main(String[] args)	throws InstantiationException, IllegalAccessException, InvocationTargetException {	EagerInitializedSingleton instanceOne = EagerInitializedSingleton.getInstance();	EagerInitializedSingleton instanceTwo = null;	Constructor[] constructors = EagerInitializedSingleton.class.getDeclaredConstructors();	for (Constructor constructor : constructors) {	constructor.setAccessible(true);	instanceTwo = (EagerInitializedSingleton) constructor.newInstance();	}	System.out.println(instanceOne.hashCode());	System.out.println(instanceTwo.hashCode());	} } 

Output của chương trình:

 2018699554 1311053135 

Tương tự Eager Initialization, implement theo Bill Pugh Singleton cũng bị break bởi Reflection.

Enum Singleton

Lúc sử dụng enum thì những params chỉ được khởi tạo một lần duy nhất, đây cũng là cách giúp bạn tạo ra Singleton instance.

Ví dụ:

 package com.gpcoder.patterns.creational.singleton; /** * Singleton implementation using enum initialization */ public enum EnumSingleton {	INSTANCE; } 

Lưu ý:

  • Enum sở hữu thể sử dụng như một Singleton, nhưng nó sở hữu nhược điểm là ko thể extends từ một lớp được, nên lúc sử dụng cần xem xét vấn đề này.
  • Hàm constructor của enumlazy, tức thị lúc được sử dụng mới chạy hàm khởi tạo và nó chỉ chạy duy nhất một lần. Nếu muốn sử dụng như một eager singleton thì cần gọi thực thi trong một static block lúc start chương trình.

So sánh giữa hai cách sử dụng enum initializationstatic block initialization method, enum sở hữu một điểm rất mạnh lúc khắc phục về vấn đề Serialization/ Deserialization.

Serialization and Singleton

Thỉnh thoảng trong những hệ thống phân tán (distributed system), chúng ta cần implement interface Serializable trong lớp Singleton để chúng ta sở hữu thể lưu trữ trạng thái của nó trong file hệ thống và truy xuất lại nó sau.

Ví dụ:

 package com.gpcoder.patterns.creational.singleton; import java.io.ObjectStreamException; import java.io.Serializable; public class SerializedSingleton implements Serializable {	private static final long serialVersionUID = 1741825395699241705L;	private SerializedSingleton() {	}	private static class SingletonHelper {	private static final SerializedSingleton instance = new SerializedSingleton();	}	public static SerializedSingleton getInstance() {	return SingletonHelper.instance;	}	/** * Special hook provided by serialization where developer can control what object needs to sent. * However this method is invoked on the new object instance created by de serialization process. * * @return * @throws ObjectStreamException */ // private Object readResolve() throws ObjectStreamException { // return SingletonHelper.instance; // } } 

Đoạn code test quá trình Serialize/ Deserialize:

 package com.gpcoder.patterns.creational.singleton; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectInputStream; import java.io.ObjectOutput; import java.io.ObjectOutputStream; public class SingletonSerializedTest {	public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {	SerializedSingleton serializedSingleton1 = SerializedSingleton.getInstance();	EnumSingleton enumSingleton1 = EnumSingleton.INSTANCE;	ObjectOutput out = new ObjectOutputStream(new FileOutputStream("SingletonSerializedTest.txt"));	out.writeObject(serializedSingleton1);	out.writeObject(enumSingleton1);	out.close();	// De-serialize from file to object	ObjectInput in = new ObjectInputStream(new FileInputStream("SingletonSerializedTest.txt"));	SerializedSingleton serializedSingleton2 = (SerializedSingleton) in.readObject();	EnumSingleton enumSingleton2 = (EnumSingleton) in.readObject();	in.close();	System.out.println("serializedSingleton1 hashCode=" + serializedSingleton1.hashCode());	System.out.println("serializedSingleton2 hashCode=" + serializedSingleton2.hashCode());	System.out.println("enumSingleton1 hashCode=" + enumSingleton1.hashCode());	System.out.println("enumSingleton2 hashCode=" + enumSingleton2.hashCode());	} } 

Output của chương trình:

 serializedSingleton1 hashCode=1028566121 serializedSingleton2 hashCode=1747585824 enumSingleton1 hashCode=1118140819 enumSingleton2 hashCode=1118140819 

Như trong ví dụ trên, Deserialize đối tượng của SerializedSingleton khác với đối tượng gốc. Tuy nhiên vấn đề này ko xảy ra lúc sử dụng enum.

Thực tế thì vẫn sở hữu cách khắc phục lúc sử dụng class SerializedSingleton là implement một phương thức readResolve(). Nhưng lúc chúng ta thật sự gặp vấn đề và cần sử dụng Serialize/ Deserialize, thì nên sử dụng enum sẽ thuần tuý hơn.

Sử dụng Singleton Pattern lúc nào?

Dưới đây là một số trường hợp sử dụng của Singleton Pattern thường gặp:

  • Vì class sử dụng Singleton chỉ tồn tại một Instance (thể hiện) nên nó thường được sử dụng cho những trường hợp khắc phục những bài toán cần truy cập vào những ứng dụng như: Shared resource, Logger, Configuration, Caching, Thread pool, …
  • Một số design pattern khác cũng sử dụng Singleton để triển khai: Abstract Factory, Builder, Prototype, Facade,…
  • Đã được sử dụng trong một số class của core java như: java.lang.Runtime, java.awt.Desktop.

Tổng kết

Có rất nhiều cách implement cho Singleton, mình thường sử dụng BillPughSingleton vì sở hữu hiệu suất cao, sử dụng LazyInitializedSingleton cho những ứng dụng chỉ làm việc với ứng dụng single-thread và sử dụng DoubleCheckLockingSingleton lúc làm việc với ứng dụng multi-thread. Tùy theo trường hợp cụ thể, bạn hãy chọn cho mình cách implement phù hợp.

Tài liệu tham khảo:

Chuyên mục: Creational Pattern, Design pattern Được gắn thẻ: Creational Design Pattern, Design pattern, Enum

Bình luận

bình luận

Leave a Comment

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *