Singleton Pattern trong Java – Cách triển khai và khi nào dùng

Trong bài viết này ta sẽ tìm hiểu Singleton Pattern trong Java. Đây là một khái niệm hoàn toàn mới khi theo học lập trình Java.

Qua bài này sẽ giúp các bạn hiểu được các khái niệm liên quan đến Singleton Design và cách hoạt động cũng như cách sử dụng nó như thế nào trong ngôn ngữ lập trình Java.

1. Singleton Pattern là gì?

Singleton Pattern là một trong những loại Design Pattern nằm trong Gangs of Four Design patterns (là một cuốn sách rất nổi tiếng viết về Design Pattern) và nó thuộc danh mục Creational Design Pattern.

Singleton Pattern đảm bảo rằng một lớp chỉ có một instance duy nhất và trong đó cung cấp một cổng giao tiếp chung nhất để truy cập vào lớp đó.

Bài viết này được đăng tại [kiso.vn]

Việc triển khai mẫu Java Singleton luôn là một chủ đề gây tranh cãi giữa các Developers. Ở đây chúng ta sẽ tìm hiểu về các nguyên tắc Singleton Design, các cách khác nhau để thực hiện Singleton Design và một số ví tốt khi sử dụng nó.

Đặc điểm của Singleton Pattern

Dưới đây là một số đặc điểm chung:

  • Giúp hạn chế việc khởi tạo một class và đảm bảo rằng chỉ tồn tại một instance duy nhất trong class.
  • Class Singleton phải cung cấp một điểm truy cập toàn cục để lấy instance của lớp.
  • Được sử dụng để ghi nhật ký, trình điều khiển đối tượng, bộ đệm và nhóm luồng.
  • Được sử dụng trong các pattern khác như: Abstract Factory, Builder, Prototype, Facade etc.
  • Được sử dụng trong các lớp Java như: java.lang.Runtime, java.awt.Desktop.

Triển khai Java Singleton Pattern

Để thực hiện một Singleton Pattern, chúng ta có các cách tiếp cận khác nhau nhưng tất cả chúng đều có các khái niệm chung sau đây:

  • Private Constructor của class để đảm bảo rằng class khác không thể truy cập vào constructor và tạo ra instance mới.
  • Tạo một biến Private static để đảm bảo rằng nó là duy nhất và chỉ được tạo ra trong class đó thôi.
  • Để các class khác có thể truy cập vào instance của class này thì chúng ta cần tạo một Public static method trả về giá trị instance trên.

Trong các phần tiếp theo, chúng ta sẽ tìm hiểu các cách tiếp cận khác nhau về việc triển khai Singleton Pattern.

  • Eager initialization.
  • Static block initialization.
  • Lazy Initialization.
  • Thread Safe Singleton.
  • Bill Pugh Singleton Implementation.
  • Using Reflection to destroy Singleton Pattern.
  • Enum Singleton.
  • Serialization and Singleton.

2. Eager initialization

Trong cách khởi tạo Eager initialization, instance của Singleton class được tạo tại thời điểm loading class (tải lớp). Đây là một phương pháp dể nhất để tạo một class Singleton, nhưng có một nhược điểm là cá thể đó được tạo ra mặc dù người dùng có thể không sử dụng nó.

Dưới đây là việc khởi tạo Initialization Singleton:

public class EagerInitializedSingleton {
    
    private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();
    
    private EagerInitializedSingleton(){}

    public static EagerInitializedSingleton getInstance(){
        return instance;
    }
}

Nếu lớp Singleton của bạn không sử dụng nhiều tài nguyên, đây là cách tiếp cận để sử dụng. Nhưng trong hầu hết các trường hợp, các lớp Singleton được tạo cho các tài nguyên như: Hệ thống tệp (File System), kết nối cơ sở dữ liệu (Database connections), v.v.

Chúng ta nên tránh việc khởi tạo cho đến khi khách hàng gọi phương thức getInstance. Ngoài ra, phương pháp này không cung cấp bất kỳ tùy chọn nào cho việc xử lý ngoại lệ.

3. Static block initialization

Triển khai khởi tạo Static block initilization tương tự như khởi tạo Eager initilization.

public class StaticBlockSingleton {

    private static StaticBlockSingleton instance;
    
    private StaticBlockSingleton(){}
    
    static{
        try{
            instance = new StaticBlockSingleton();
        }catch(Exception e){
            throw new RuntimeException("Exception occured in creating singleton instance");
        }
    }
    
    public static StaticBlockSingleton getInstance(){
        return instance;
    }
}

Cả khởi tạo Eager initilization và khởi tạo Static block initilization đều tạo ra cá thể ngay cả trước khi nó được sử dụng và đó không phải là cách thực hành tốt nhất để sử dụng. Vì vậy, trong các phần tiếp theo, chúng ta sẽ tìm hiểu cách tạo một lớp Lazy Initialization.

4. Lazy Initialization

Phương thức khởi tạo Lazy Initialization để triển khai mẫu Singleton tạo ra thể hiện trong phương thức truy cập toàn cầu. Dưới đây là mẫu để tạo lớp Singleton với phương pháp này.

public class LazyInitializedSingleton {

    private static LazyInitializedSingleton instance;
    
    private LazyInitializedSingleton(){}
    
    public static LazyInitializedSingleton getInstance(){
        if(instance == null){
            instance = new LazyInitializedSingleton();
        }
        return instance;
    }
}

Việc triển khai ở trên hoạt động tốt trong trường hợp môi trường đơn luồng (single-threaded) nhưng khi nói đến các hệ thống đa luồng (multithreaded systems), nó có thể gây ra sự cố nếu nhiều luồng nằm trong điều kiện if cùng một lúc. Nó sẽ phá hủy mẫu singleton và cả hai luồng sẽ nhận được các thể hiện khác nhau của lớp singleton.

5. Thread Safe Singleton

Cách dễ dàng hơn để tạo một lớp Thread Safe Singleton là làm cho phương thức truy cập toàn cục được đồng bộ hóa, để chỉ một luồng có thể thực thi phương thức này tại một thời điểm. Thực hiện chung của phương pháp này giống như lớp dưới đây.

public class ThreadSafeSingleton {

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

Việc triển khai Thread Safe Singleton hoạt động khá tốt và cung cấp sự an toàn của luồng, nhưng nó chạy rất chậm và tốn hiệu năng, hãy cùng xem đoạn code đã được cải tiến dưới đây:

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

Thay vì chúng ta Thread Safe cả menthod getInstance() thì chúng ta chỉ cần Thread Safe một đoạn code quan trọng thôi.

6. Bill Pugh Singleton Implementation

Các cách tiếp cận ở trên không thể sử dụng trong một số trường hợp nhất định khi có quá nhiều luồng cố gắng lấy instance của lớp Singleton. Vì vậy, Bill Pugh đã đưa ra một cách tiếp cận khác để tạo ra lớp Singleton bằng cách sử dụng một lớp trợ giúp tĩnh bên trong. Bill Pugh Singleton Implementation được khởi tạo như sau:

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;
    }
}

Đây là cách tiếp cận được sử dụng rộng rãi nhất cho lớp Singleton vì nó không yêu cầu đồng bộ hóa.

7. Sử dụng Reflection để hủy Singleton Pattern

Sử dụng Reflection để hủy Singleton Pattern.

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) {
                constructor.setAccessible(true);
                instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
                break;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(instanceOne.hashCode());
        System.out.println(instanceTwo.hashCode());
    }
}

Khi bạn chạy thử đoạn code trên, bạn sẽ thấy rằng hashCode của hai trường hợp không giống nhau Reflection được sử dụng rất nhiều trong các Framework như: Spring, Hibernate,..

8. Enum Singleton

Để khắc phục tình trạng này với Reflection, Joshua Bloch đề nghị sử dụng Enum để triển khai mẫu thiết kế Singleton vì Java đảm bảo rằng mọi giá trị Enum chỉ được khởi tạo một lần trong chương trình Java. Vì các giá trị Java Enum có thể truy cập được trên toàn cầu, nên singleton cũng vậy. Hạn chế là loại enum có phần không linh hoạt; ví dụ, nó không cho phép khởi tạo Eager initialization.

public enum EnumSingleton {

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

9. Serialization và Singleton

Đôi khi trong các hệ thống phân tán, chúng ta cần triển khai giao diện Serialization trong lớp Singleton để có thể lưu trữ trạng thái của nó trong hệ thống tệp và truy xuất nó vào một thời điểm sau. Đây là một lớp singleton nhỏ cũng thực hiện giao diện Serialization.

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;
    }
}

Sau đây chúng ta sẽ thực hiện một chương trình đơn giản để kiểm tra:

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();

        ObjectInput in = new ObjectInputStream(new FileInputStream(
                "filename.ser"));
        SerializedSingleton instanceTwo = (SerializedSingleton) in.readObject();
        in.close();

        System.out.println("Giá trị hashCode thứ nhất: "+instanceOne.hashCode());
        System.out.println("Giá trị hashCode thứ hai: "+instanceTwo.hashCode());
        System.out.println("-----------------------------");
        System.out.println("Chương trình này được đăng tại Kiso.vn");
    }
}

Kết quả sau khi chạy chương trình:

singleton pattern JPG

Như vậy là chúng ta đã lần lượt tìm hiểu về Singleton Pattern trong mục Design Pattern Java. Mình hy vọng qua bài viết này sẽ giúp các bạn nắm bắt được các kiến thức cơ bản về Singleton Pattern. Bởi vì đây là một trong những kỹ thuật rất quan trọng khi lập trình, nó giúp các bạn tối ưu hóa và kiểm soát code rất tốt.

Bài viết liên quan

Trả lời

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 *