Reading Time: 6 minutes

Singleton là một trong design pattern thuộc nhóm creational (khởi tạo) của bộ tứ huyền thoại Erich Gamma, Richard Helm, Ralph Johnson, và John Vlissides (Gang of Four, hay GoF). Chỉ nghe qua tên thì đây có vẻ là một design pattern đơn giản, nhưng khi đi vào triển khai thực tế, nó lại nảy sinh nhiều vấn đề khác nhau cần cân nhắc.

Trong bài viết này, chúng ta sẽ tìm hiểu về các nguyên tắc của kiểu thiết kế này, các cách cách áp dụng Singleton trong Java, và một số lưu ý để sử dụng nó một cách tốt nhất.

Nguyên tắc của Singleton

  • Singleton hạn chế việc khởi tạo (instantiation) của một class (lớp), đảm bảo rằng chỉ có một thực thể (instance) duy nhất của lớp đó tồn tại trong máy ảo Java.
  • Lớp Singleton phải cung cấp một điểm truy cập global để lấy instance của lớp.
  • Singleton được sử dụng cho logging, các đối tượng driver, caching và thread pool.
  • Singleton cũng được sử dụng trong các design pattern khác như Abstract factory, Builder, Prototype, Facade, v.v.
  • Singleton cũng được dùng trong các class của core Java (ví dụ: java.lang.Runtime, java.awt.Desktop).

Áp dụng Singleton trong Java

Để triển khai design pattern này, chúng ta có nhiều cách tiếp cận khác nhau, nhưng tất cả đều dựa trên các khái niệm chung sau:

  • Constructor dạng private để hạn chế việc khởi tạo lớp từ các lớp khác.
  • Biến static private thuộc chính lớp đó, là instance duy nhất của lớp.
  • Phương thức dạng public static trả về instance của lớp, đây là điểm truy cập global để bên ngoài có thể lấy instance của class singleton.

Trong các phần tiếp theo, chúng ta sẽ tìm hiểu các cách khác nhau để triển khai kiểu thiết kế Singleton và các vấn đề cần lưu ý.

Khởi tạo tức thời (Eager initialization)

Với phương pháp eager initialization, instance của lớp Singleton được tạo ngay tại thời điểm nạp lớp (class loading). Nhược điểm của cách khởi tạo này là instance được tạo ra ngay cả khi ứng dụng client có thể chưa sử dụng đến nó.

Dưới đây là cách triển khai Singleton sử dụng khởi tạo static:

package com.journaldev.singleton;
public class EagerInitializedSingleton {
    private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();
    // private constructor to avoid client applications using the constructor
    private EagerInitializedSingleton(){}
    public static EagerInitializedSingleton getInstance() {
        return instance;
    }
Đây là cách tiếp cận phù hợp nếu lớp singleton của bạn không sử dụng nhiều tài nguyên. Nhưng trong hầu hết các trường hợp, lớp singleton được tạo ra với các tài nguyên như hệ thống tập tin, kết nối cơ sở dữ liệu, v.v.

Chúng ta nên tránh việc khởi tạo instance trừ khi client gọi phương thức getInstance. Ngoài ra, cách này không cung cấp tùy chọn nào cho việc xử lý lỗi (exception handling).

Khởi tạo bằng block static (Static block initialization)

Cách triển khai này tương tự như eager initialization. Điểm khác biệt là instance của lớp được tạo trong một block static, cho phép thêm tùy chọn xử lý lỗi.

package com.journaldev.singleton;

public class StaticBlockSingleton {

    private static StaticBlockSingleton instance;

    private StaticBlockSingleton(){}

    // static block initialization for exception handling
    static {
        try {
            instance = new StaticBlockSingleton();
        } catch (Exception e) {
            throw new RuntimeException("Exception occurred in creating singleton instance");
        }
    }

    public static StaticBlockSingleton getInstance() {
        return instance;
    }
}
Cả hai phương pháp khởi tạo tức thời và khởi tạo bằng block static đều tạo instance trước khi nó thực sự được dùng, do đó chúng không phải là phương pháp tối ưu nhất.

Khởi tạo trì hoãn (Lazy Initialization)

Trong phương pháp này, instance của lớp Singleton sẽ được tạo bên trong phương thức truy cập global.

Dưới đây là code ví dụ cho việc tạo lớp singleton theo cách này:

package com.journaldev.singleton;

public class LazyInitializedSingleton {

    private static LazyInitializedSingleton instance;

    private LazyInitializedSingleton(){}

    public static LazyInitializedSingleton getInstance() {
        if (instance == null) {
            instance = new LazyInitializedSingleton();
        }
        return instance;
    }
}
Cách triển khai trên hoạt động tốt trong môi trường đơn luồng (single-threaded), nhưng với các hệ thống đa luồng (multi-threaded), nó có thể gây ra vấn đề nếu nhiều thread ở trong khối lệnh if cùng một lúc. Điều này sẽ phá vỡ kiểu thiết kế Singleton và các thread sẽ nhận được những instance khác nhau của lớp Singleton.

Trong phần tiếp theo, chúng ta sẽ xem xét các cách khác nhau để tạo ra một lớp singleton an toàn luồng (thread-safe).

Singleton An toàn luồng (Thread Safe Singleton)

Một cách đơn giản để tạo lớp singleton an toàn luồng là đặt từ khóa synchronized cho phương thức truy cập global để đảm bảo tại bất kì thời điểm nào chỉ có tối đa một thread có thể thực thi phương thức đó.

Dưới đây là cách triển khai chung của phương pháp này:

package com.journaldev.singleton;

public class ThreadSafeSingleton {

    private static ThreadSafeSingleton instance;

    private ThreadSafeSingleton(){}

    public static synchronized ThreadSafeSingleton getInstance() {
        if (instance == null) {
            instance = new ThreadSafeSingleton();
        }
        return instance;
    }

}
Cách triển khai trên đảm bảo an toàn luồng, nhưng việc dùng synchronized có ảnh hưởng đến hiệu suất trong khi chúng ta thực sự chỉ cần cơ chế này cho một vài thread đầu tiên (chúng có thể tạo ra các instance riêng biệt).

Để tránh gánh nặng xử lý không cần thiết này mỗi lần gọi, chúng ta sử dụng nguyên tắc double-checked locking. Trong phương pháp này, khối code synchronized được sử dụng bên trong câu lệnh if, cùng với một lần kiểm tra bổ sung, để đảm bảo chỉ một instance của lớp singleton được tạo ra.

Đoạn code sau minh họa cách triển khai double-checked locking:

public static ThreadSafeSingleton getInstanceUsingDoubleLocking() {
    if (instance == null) {
        synchronized (ThreadSafeSingleton.class) {
            if (instance == null) {
                instance = new ThreadSafeSingleton();
            }
        }
    }
    return instance;
}

Triển khai Singleton của Bill Pugh

Trước Java phiên bản 5, mô hình bộ nhớ Java có nhiều vấn đề, và các cách tiếp cận ở trên thường thất bại trong một số trường hợp khi có quá nhiều thread cố gắng lấy instance của lớp singleton cùng một lúc.

Do đó, Bill Pugh (một nhà khoa học máy tính nổi tiếng) đã đề xuất một cách khác để tạo lớp singleton bằng cách sử dụng một lớp helper static nội bộ (inner static helper class).

Ví dụ về cách triển khai Singleton của Bill Pugh:

package com.journaldev.singleton;

public class BillPughSingleton {

    private BillPughSingleton(){}

    private static class SingletonHelper {
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }

    public static BillPughSingleton getInstance() {
        return SingletonHelper.INSTANCE;
    }
}
Lưu ý rằng lớp nội bộ static private SingletonHelper chứa instance của lớp singleton. Khi lớp Singleton được load, SingletonHelper không được nạp vào bộ nhớ.

Chỉ khi getInstance() được gọi, lớp này mới được load và tạo ra instance của lớp Singleton. Đây là phương thức Singleton phổ biến nhất vì nó không yêu cầu đồng bộ hóa.

Dùng Reflection để phá vỡ thiết kế Singleton

Reflection có thể được sử dụng để phá vỡ tất cả các cách triển khai singleton đã được trình bày. Dưới đây là một lớp ví dụ:

package com.journaldev.singleton;

import java.lang.reflect.Constructor;

public class ReflectionSingletonTest {

    public static void main(String[] args) {
        EagerInitializedSingleton instanceOne = EagerInitializedSingleton.getInstance();
        EagerInitializedSingleton instanceTwo = null;
        try {
            Constructor[] constructors = EagerInitializedSingleton.class.getDeclaredConstructors();
            for (Constructor constructor : constructors) {
                // This code will destroy the singleton pattern
                constructor.setAccessible(true);
                instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
                break;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(instanceOne.hashCode());
        System.out.println(instanceTwo.hashCode());
    }

}
Khi chạy lớp ReflectionSingletonTest ở trên, bạn sẽ thấy hashCode của hai instance khác nhau, trái ngược với nguyên tắc của Singleton. Reflection là một công cụ rất mạnh và được sử dụng trong nhiều framework như Spring và Hibernate.

Enum Singleton

Để khắc phục vấn đề này với Reflection, Joshua Bloch đề xuất sử dụng enum để triển khai kiểu thiết kế Singleton. Lý do là Java đảm bảo rằng mỗi giá trị enum chỉ được khởi tạo một lần duy nhất trong chương trình, mà các giá trị Java Enum có thể truy cập global, nên instance Singleton cũng vậy.

Nhược điểm là type enum khá kém linh hoạt (một ví dụ là nó không cho phép khởi tạo trì hoãn – lazy initialization).

package com.journaldev.singleton;

public enum EnumSingleton {

    INSTANCE;

    public static void doSomething() {
        // do something
    }
}

Serialization và Singleton

Đôi khi trong các hệ thống phân tán (distributed), chúng ta cần triển khai interface Serializable cho lớp Singleton để có thể lưu trạng thái của nó vào file và khôi phục lại vào thời điểm sau đó.

Dưới đây là một lớp Singleton nhỏ cũng triển khai interface Serializable:

package com.journaldev.singleton;

import java.io.Serializable;

public class SerializedSingleton implements Serializable {

    private static final long serialVersionUID = -7604766932017737115L;

    private SerializedSingleton(){}

    private static class SingletonHelper {
        private static final SerializedSingleton instance = new SerializedSingleton();
    }

    public static SerializedSingleton getInstance() {
        return SingletonHelper.instance;
    }

}
Vấn đề với lớp Singleton đã được serialize là mỗi khi chúng ta deserialize nó, một instance mới của lớp sẽ được tạo ra. Dưới đây là một ví dụ:
package com.journaldev.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 instanceOne = SerializedSingleton.getInstance();
        ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
                "filename.ser"));
        out.writeObject(instanceOne);
        out.close();

        // deserialize from file to object
        ObjectInput in = new ObjectInputStream(new FileInputStream(
                "filename.ser"));
        SerializedSingleton instanceTwo = (SerializedSingleton) in.readObject();
        in.close();

        System.out.println("instanceOne hashCode="+instanceOne.hashCode());
        System.out.println("instanceTwo hashCode="+instanceTwo.hashCode());

    }

}
Đoạn code chạy ra kết quả sau:
instanceOne hashCode=2011117821
instanceTwo hashCode=109647522

Như vậy, nó đã phá vỡ kiểu thiết kế Singleton. Để khắc phục tình huống này, tất cả những gì chúng ta cần làm là cung cấp hàm readResolve().

protected Object readResolve() {
    return getInstance();
}

Sau khi làm điều này, bạn sẽ thấy hashCode của cả hai instance trong chương trình test là giống nhau.

Kết luận

Bài viết này đã trình bày về mẫu thiết kế Singleton. Hy vọng những thông tin trên sẽ hữu ích cho bạn trong quá trình học lập trình Java. Đừng ngần ngại khám phá thêm các bài hướng dẫn Java khác của chúng tôi để mở rộng kiến thức và nâng cao kỹ năng nhé!

0 Bình luận

Đăng nhập để thảo luận

Chuyên mục Hướng dẫn

Tổng hợp các bài viết hướng dẫn, nghiên cứu và phân tích chi tiết về kỹ thuật, các xu hướng công nghệ mới nhất dành cho lập trình viên.

Đăng ký nhận bản tin của chúng tôi

Hãy trở thành người nhận được các nội dung hữu ích của CyStack sớm nhất

Xem chính sách của chúng tôi Chính sách bảo mật.