Singleton trong Java là một design pattern đảm bảo chỉ duy nhất một đối tượng của lớp được tồn tại trong suốt quá trình chạy của một chương trình. Pattern này được sử dụng chỉ khi bạn cần một phiên bản duy nhất của lớp trong toàn bộ quá trình chạy chương trình và đối tượng đó phải có khả năng truy cập từ bên ngoài các lớp khác. Trong bài viết này, Stringee sẽ cùng các bạn tìm hiểu về Singleton pattern trong Java.

1. Singleton pattern trong Java là gì

Singleton là 1 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ẽ cung cấp cho bạn một method để có thể truy xuất được thể hiện duy nhất đó mọi lúc mọi nơi trong chương trình. Trong bài viết này, Stringee sẽ cùng các bạn tìm hiểu về Singleton pattern trong Java nhé

2. Một số nguyên tắc về Singleton Pattern trong Java

  • Singleton hạn chế việc khởi tạo đối tượng từ lớp và đảm bảo chỉ có một đối tượng của lớp tồn tại trong máy ảo JVM

  • Lớp singleton phải cung cấp một điểm truy cập mở để có thể lấy ra đối tượng của lớp

  • Singleton pattern được sử dụng để ghi log, đối tượng driver, lưu cache và sử lý threadpool

  • Singleton design pattern cũng được sử dụng trong các pattern khác như Abstract Factory, Builder, Prototype, Facade, etc.

  • Singleton design pattern được sử dụng trong các lớp core Java (ví dụ, java.lang.Runtime, java.awt.Desktop)

3. Implementation của Singleton Pattern trong Java

Để triển khai singleton pattern, chúng ta có thể sử dụng nhiều cách, tuy nhiên các cách này đều có một số đặc điểm chung như sau:

  • Constructor phải là private để hạn chế việc khởi tạo đối tượng từ các lớp khác

  • Các biến private static trong cùng lớp và nó phải là đối tượng duy nhất trong lớp

  • Một phương thức public static trả về đối tượng của lớp, đây là điểm truy cập để cho các lớp bên ngoài có thể lấy đối tượng từ lớp Singleton

Trong phần dưới đây, chúng ta sẽ tìm hiểu các cách khác nhau để thực hiện triển khai Singleton pattern và thiết kế các điều kiện cần phải để tâm với các cách triển khai khác nhau

3.1. Eager Initialization

Trong cách triển khai này, đối tượng của lớp Singleton được tạo ngay khi lớp đang được tải bởi JVM. Nhược điểm của cách triển khai này đó là đối tượng sẽ được khởi tạo kể cả khi mà client có thể không sử dụng đến nó.

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;

    }

}

Nếu lớp Singleton của bạn không sử dụng nhiều tài nguyên, cách cài đặt này là một cách tiếp cận nên được sử dụng. Tuy nhiên, trong đa số các trường hợp, Singleton được sử dụng cho các lớp như FileSystem, kết nối tới Database, ... Chúng ta nên tránh việc khởi tạo nếu như client không gọi đến phương thức getInstance. Và cũng do cách khởi tạo ngay khi tải lớp nên cách tiếp cận này không cung cấp một cách xử lý exception nào cả.

3.2. Static block initialization

Cách tiếp cận này tương tự với Eager Initialization, thế nhưng đối tượng sẽ được khởi tạo trong static block. Do đó, chúng ta có thể xử lý exception trong block này:

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

Nhược điểm chung của cách tiếp cận này cũng giống như eager initialization đó là đối tượng được khởi tạo ngay cả khi client không cần sử dụng đến nó.

3.3. Lazy Initialization

Lazy initialization khởi tạo một đối tượng trong một phương thức cho phép truy cập từ bên ngoài. Dưới đây là ví dụ cho cách triển khai này:

public class LazyInitializedSingleton {

    private static LazyInitializedSingleton instance;

    private LazyInitializedSingleton(){}

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

}

Cách tiếp cận này có thể hoạt động đúng đắn với môi trường đơn luồng, tuy nhiên, nếu bạn làm việc với môi trường đa luồng(đa số các chương trình production hiện nay), vấn đề có thể xảy ra tại điều kiện if. Khi đó, pattern sẽ bị sai do JVM sẽ tạo hai đối tượng khác nhau cho hai thread khác nhau đang cùng xét điều kiện if này. Trong các phần tiếp theo, chúng ta sẽ cùng tìm hiểu các cách khác nhau để tạo ra các lớp Singleton thread-safe

3.4. Thread Safe Singleton

Một cách đơn giản để tạo một lớp Singleton thread-safe đó là chỉ định phương thức truy cập đối tượng là synchronized, do đó chỉ một thread có thể thực thi phương thức này tại một thời điểm.

public class ThreadSafeSingleton {



    private static ThreadSafeSingleton instance;

    private ThreadSafeSingleton(){}

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

    }

}

Cách này có 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 đến đều phải chờ nếu có một Thread khác đang sử dụng. Có những tác vụ xử lý trước và sau khi tạo thể hiện không cần thiết phải block. Vì vậy chúng ta cần cải tiến nó đi 1 chút với Double Check Locking Singleton.

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

3.5. 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ò 1 Helper khi muốn tách biệt chức năng cho 1 class function rõ ràng hơn. Đây là cách thường hay được sử dụng và có hiệu suất tốt .

public class BillPughSingleton {

    private BillPughSingleton(){}

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

    public static BillPughSingleton getInstance() {
        return SingletonHelper.INSTANCE;
    }

}

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

Cấu trúc Singleton có thể bị phá vỡ bằng cách sử dụng Reflection

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

}

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.

3.7. Enum Singleton

Để tránh việc cấu trúc bị phá vỡ bởi Reflection, Josua Bloch khuyên chúng ta nên sử dụng enum để triển khai Singleton bởi vì Java đảm bảo việc Enum sẽ chỉ được khởi tạo một lần duy nhất trong một chương trình Java. Vì các giá trị enum của Java có thể được truy cập từ bên ngoài Lớp của nó, vì vậy nó cũng là Singleton. Nhược điểm là kiểu enum sẽ không linh động:

public enum EnumSingleton {

    INSTANCE;

    public static void doSomething() {
        // implement what to do with this class
    }

}

3.8. Serialization và Singleton

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

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 đề ở đây là khi chúng ra serialize một lớp Singleton thì khi chúng ta deserialize nó, nó sẽ tạo một đối tượng mới của lớp

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

    }

}

Output

instanceOne hashCode=2011117821
instanceTwo hashCode=109647522

Để xử lý trường hợp này, chúng ta cần implement phương thức readResolve()

protected Object readResolve() {

    return getInstance();

}

Kết

Có rất nhiều cách implement cho Singleton, thông thường Bill Pugh Singleton hay được các lập trình viên sử dụng vì có 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 khi 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.

Stringee API cung cấp các tính năng như gọi thoại, gọi video, tin nhắn chat, SMS hay tổng đài chăm sóc khách hàng (CSKH) có thể được nhúng trực tiếp vào các ứng dụng/website của doanh nghiệp nhanh chóng. Điều này giúp tiết kiệm đến 80% thời gian và chi phí cho doanh nghiệp, trong khi nếu tự phát triển các tính năng này có thể mất từ 1 - 3 năm.

Quý doanh nghiệp quan tâm xin mời đăng ký nhận tư vấn tại đây: