[VN] Thiết kế Singleton

Huỳnh Quang Thảo
6 min readJan 30, 2016

Phiên bản tiếng Anh https://medium.com/@huynhquangthao/2c63dfcfccf2

Đây là bài đầu tiên cho blog về lập trình của mình. Mình sẽ viết tất cả chủ đề liên quan về Android, iOS, Design Pattern, Algorithm and Architecture Design. Mỗi chủ đề sẽ có hai phiên bản:Tiếng Anh và Tiếng Việt. Hope you enjoy :)

Giới thiệu

Hôm nay mình sẽ nói về design pattern Singleton Pattern. Đây là mẫu thiết kết đơn giản nhất trong 23 mẫu thiết kế của Gang-Of-Four nhưng có rất nhiều kía cạnh thú vị để thảo luận. Đây cũng là câu hỏi mình gặp phải khi phỏng vấn ở Lazada.

Giải thích: Singleton Pattern được sử dụng khi chúng ta muốn tạo một và chỉ một đối tượng thuộc về một lớp xuyên suốt quá trình chạy của ứng dụng.

Hiện thực:

1. Tạo một đối tượng của chính lớp đó có thuộc tính private static.
2. Tạo một private constructor để lớp này không thể khởi tạo bởi các đối tượng ở ngoài bản thân class đó.
3. Tạo một method tên là getInstance() (Ohhhhh, Ok, bất cứ tên gì chúng ta muốn) và trả về đối tượng được khai báo ở bước 1.

public class Singleton {

// private static instance
private static Singleton sInstance;
// private constructor for prevent outside initialization
private Singleton() {
}
// static getter
public static Singleton getInstance() {
if (sInstance == null) {
sInstance = new Singleton();
}
return sInstance;
}
}

Môi trường đa luồng

Chúng ta tiếp tục hành trình qua thế giới môi trường đa luồng. Khi ứng dụng chạy trong môi trường đa luồng, ứng dụng có thể dừng ở bất cứ dòng nào ở bất cứ thời gian nào để nhường quyền điều khiển cho các luồng khác. Do vậy có thể có nhiều hơn một luồng đồng thời cùng chạy vào code khởi tạo đối tượng. Do vậy hiện thực đúng singleton pattern trong môi trường đa luồng là điều cực kì quan trọng.

  1. Double Check Locking Design Pattern
public static Singleton getInstance() {
if (sInstance == null) {
// block so other threads cannot come into while initialize
synchronized (Singleton.class) {
// recheck again. maybe another thread has initialized before
if (sInstance == null) {
sInstance = new Singleton();
}
}
}
return sInstance;
}

Code mẫu ở trên là một mẫu thiết kế cho các vấn đề về đa luồng nổi tiếng tên gọi là Double Check Locking. Mình sẽ dành thời gian để nói về những thiết kế này vào ngày đẹp trời khác ^^. Trong các buổi phỏng vấn, mình nghĩ chúng ta nên hiện thực phiên bản này (so với cách 2 và 3 mình sẽ đề cập sau), vì nó khá là gây ấn tượng cho nhà tuyển dụng =)))

Có một điều không may mắn là đoạn code trên không làm việc trên Java. :( Một số người gọi đây là broken pattern. Các bạn có thể tham khảo các link ở phần cuối để đọc rõ hơn chỗ này. Đã có rất nhiều đánh giá và nhiều cách sửa “ấn tượng" cho vấn đề này.

Để tiết kiệm thời gian của thế giới, chúng ta có thể sử dụng đơn giản từ khoá volatile với JDK từ 1.5. Java Volatile có ý nghĩa về tính hiện hữu trong bộ nhớ. Nói một cách đơn giản, từ khoá volatile đảm bảo tất cả mọi luồng thấy chung một dữ liệu sau khi một luồng khác kết thúc việc ghi. Nếu không dùng từ khoá volatile, có thể một số luồng sẽ thấy dữ liệu cũ (trước khi được ghi) vì cơ chế cache của CPU.

Và đây là version đúng nhất cho Double Check Locking.

public class Singleton {    // adding volatile keyword here
private static volatile Singleton sInstance;
private Singleton() {
}
public static Singleton getInstance() {
// using local variable for storing volatile object
// for increasing performance
Singleton result = sInstance;
if (result == null) {
synchronized (Singleton.class) {
result = sInstance;
if (result == null) {
sInstance = result = new Singleton();
}
}
}
return result;
}
}

2. Static Block Initialization (Class Loader)

public class Singleton {    private Singleton() {
}
// create a static nested class
public static class Helper {
public static Singleton searchBox = new Singleton();
}
public static Singleton getsInstance() {
return Helper.searchBox;
}
}

Java đảm bảo tính an toàn khi chạy đa luồng cho static class loader. Do vậy, khi một luồng chạy được khởi tạo, không một luồng nào có thể nhảy vào và tạo một đối tượng khác. Cách này cũng đảm bảo đối tượng chỉ khởi tạo khi thực sự sử dụng.

3. Enum

public enum Singleton {

// and just call Singleton.INSTANCE
// no private constructor. no getInstance
INSTANCE;
}

Ah ha. Một cách rất rất là ấn tượng để trém gió với bạn bè. Enum trong Java cũng đảm bảo tính an toàn khi chạy đa luồng. Tuy nhiên, enum chỉ có từ JDK 1.5. Joshua Bloch có nói trong quyển sách Effective Java 2 điều khá thú vị sau về sử dụng enum cho Singleton Pattern:

This approach is functionally equivalent to the public field approach, except that it is more concise, provides the serialization machinery for free, and provides an ironclad guarantee against multiple instantiation, even in the face of sophisticated serialization or reflection attacks. While this approach has yet to be widely adopted, a single-element enum type is the best way to implement a singleton.

Vấn đề về Serialization: So sánh giữa 2 cách sử dụng enum initialization và static block initialization method, enum có một điểm rất mạnh khi bắt đầu thảo luận về vấn đề serialization/deserialization. Đây là ví dụ code của Jon Skeet cho bài toán này.

// Singleton implementation using static class initialization
class ClassSingleton implements Serializable {
public static final ClassSingleton INSTANCE = new
ClassSingleton();
private ClassSingleton() {}
}
// Singleton implementation using enum initialization
enum EnumSingleton {
INSTANCE;
}
public class Test {
public static void main(String[] args) throws IOException {
byte[] data;

// serialize both two single instance to data
try (ByteArrayOutputStream output = new
ByteArrayOutputStream();
ObjectOutputStream oos = new
ObjectOutputStream(output)) {
oos.writeObject(ClassSingleton.INSTANCE);
oos.writeObject(EnumSingleton.INSTANCE);
data = output.toByteArray();
}
// deserialize again from data to memory
try (ByteArrayInputStream input = new
ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(input)) {
ClassSingleton first = (ClassSingleton) ois.readObject();
EnumSingleton second = (EnumSingleton) ois.readObject();
// checking if deserialize obj same with current instance
// FALSE -- Not same
System.out.println(first == ClassSingleton.INSTANCE);
// TRUE -- same
System.out.println(second == EnumSingleton.INSTANCE);
}
}
}

Như trong ví dụ trên, deserialize đối tượng của ClassSingleton khác với đối tượng hiện tại. Tuy nhiên vấn đề này không xảy ra khi sử dụng enum. Thực tế thì vẫn có cách khắc phục khi sử dụng class initialization. Nhưng khi 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 để tiết kiệm thời gian của thế giới ^^

Singleton trong các Framework

Sau đây sẽ là một số class hiện thực mẫu thiết kế Singleton Pattern ở Java và Android framework. Mọi người có ví dụ nào thú vị hơn có thể liên hệ với mình :)

Java Runtime: Cho phép ứng dụng Java giao tiếp với môi trường ứng dụng đang thực thi.

public class Runtime {

/** Holds the Singleton global instance of Runtime.*/
private static final Runtime mRuntime = new Runtime();
/** Returns the single instance for the current application.*/
public static Runtime getRuntime() {
return mRuntime;
}
}

Android Looper: Mỗi luồng trong android có một và chỉ một đối tượng Looper để xử lí hàng đợi các gói tin.

/**
* Returns the application's main looper, which lives in the main thread of the application.
*/
public static Looper getMainLooper() {
synchronized (Looper.class) {
return sMainLooper;
}
}

Link

--

--