Java - Lớp Singleton

Xin chào các nhà pháp sư Java tương lai! Hôm nay, chúng ta sẽ cùng nhau khám phá một trong những khái niệm thú vị nhất trong lập trình Java: Lớp Singleton. Đừng lo lắng nếu bạn mới bắt đầu học lập trình; tôi sẽ dẫn dắt bạn từng bước trong hành trình này, giống như tôi đã làm cho hàng trăm sinh viên trong những năm dạy học của mình. Vậy hãy chuẩn bị đồ uống yêu thích của bạn, ngồi舒适的, và cùng nhau bắt đầu cuộc phiêu lưu thú vị này nhé!

Java - Singleton Class

Lớp Singleton là gì?

Hãy tưởng tượng bạn là quản lý của một câu lạc bộ rất độc quyền. Câu lạc bộ này độc quyền đến mức chỉ có thể có một bản sao của nó trên toàn thế giới. Đó chính là gì một lớp Singleton trong Java - một lớp cho phép chỉ tạo ra một bản sao của chính nó.

Tại sao sử dụng Singleton?

Bạn có thể tự hỏi, "Tại sao chúng ta lại muốn giới hạn mình chỉ có một bản sao?" Có nhiều lý do:

  1. Điểm truy cập toàn cục: Nó cung cấp một điểm truy cập duy nhất để đến một bản sao cụ thể, giúp dễ dàng duy trì trạng thái toàn cục trong ứng dụng.
  2. Khởi tạo muộn: Bản sao chỉ được tạo ra khi cần thiết, giúp tiết kiệm tài nguyên.
  3. Bảo vệ đa luồng: Khi triển khai đúng cách, nó có thể bảo vệ đa luồng, cho phép chỉ có một bản sao trong môi trường đa luồng.

Bây giờ, hãy cùng xem cách chúng ta có thể tạo một lớp Singleton trong Java.

Tạo một lớp Singleton

Có nhiều cách để tạo một lớp Singleton, nhưng chúng ta sẽ bắt đầu với cách đơn giản và phổ biến nhất: khởi tạo sớm (eager initialization).

public class EagerSingleton {
// Bản sao私家 tĩnh của lớp
private static final EagerSingleton instance = new EagerSingleton();

// Constructor私家 để ngăn chặn khởi tạo
private EagerSingleton() {}

// Phương thức công khai để trả về bản sao
public static EagerSingleton getInstance() {
return instance;
}
}

Hãy phân tích này:

  1. Chúng ta khai báo một biến private static final instance của lớp. Đây là bản sao duy nhất sẽ tồn tại.
  2. Constructor là私家, ngăn chặn các lớp khác tạo mới bản sao.
  3. Chúng ta cung cấp một phương thức public static getInstance() để trả về bản sao duy nhất.

Để sử dụng Singleton này:

EagerSingleton singleton = EagerSingleton.getInstance();

Đơn giản phải không? Nhưng nếu chúng ta muốn tạo bản sao chỉ khi cần thiết? Đó là khi khởi tạo muộn (lazy initialization) được sử dụng.

Khởi tạo muộn

public class LazySingleton {
private static LazySingleton instance;

private LazySingleton() {}

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

Trong phiên bản này, bản sao chỉ được tạo ra khi getInstance() được gọi lần đầu tiên. Tuy nhiên, cách này không bảo vệ đa luồng. Trong môi trường đa luồng, chúng ta có thể tạo ra nhiều bản sao. Hãy sửa lỗi này!

Singleton bảo vệ đa luồng

public class ThreadSafeSingleton {
private static ThreadSafeSingleton instance;

private ThreadSafeSingleton() {}

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

Bằng cách thêm từ khóa synchronized vào phương thức getInstance(), chúng ta đảm bảo rằng chỉ có một luồng có thể thực thi phương thức này tại một thời điểm. Tuy nhiên, đồng bộ hóa là tốn kém, và chúng ta chỉ cần nó khi bản sao được tạo ra lần đầu tiên. Đó là khi mẫu double-checked locking xuất hiện!

Double-Checked Locking

public class DoubleCheckedSingleton {
private static volatile DoubleCheckedSingleton instance;

private DoubleCheckedSingleton() {}

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

Mẫu này kiểm tra null hai lần: một lần không khóa và một lần khóa. Từ khóa volatile đảm bảo rằng nhiều luồng xử lý biến instance một cách chính xác.

Thực thi Singleton của Bill Pugh

Bây giờ, hãy chia sẻ với các bạn cách thực thi Singleton yêu thích của tôi, được đặt tên theo người sáng tạo ra nó, Bill Pugh:

public class BillPughSingleton {
private BillPughSingleton() {}

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

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

Phương pháp này sử dụng một lớp inner tĩnh để giữ bản sao. Nó bảo vệ đa luồng mà không cần đồng bộ hóa và tải lazily bản sao khi getInstance() được gọi lần đầu tiên.

Khi nào nên sử dụng Singleton

Singletons rất hữu ích cho:

  1. Quản lý tài nguyên chia sẻ (như kết nối cơ sở dữ liệu)
  2. Điều phối các hành động toàn hệ thống
  3. Quản lý bộ tài nguyên (như bộ thread pool)

Tuy nhiên, hãy cẩn thận! Sử dụng Singleton quá mức có thể làm cho mã của bạn khó khăn hơn để kiểm tra và bảo trì.

Phương thức của Singleton

Dưới đây là bảng các phương thức phổ biến mà bạn có thể tìm thấy trong một lớp Singleton:

Phương thức Mô tả
getInstance() Trả về bản sao duy nhất của lớp
readResolve() Sử dụng cho serialization để bảo vệ tính chất Singleton
clone() Thường ném CloneNotSupportedException để ngăn chặn克隆

Kết luận

Uf! Chúng ta đã cùng nhau bao quát rất nhiều nội dung hôm nay. Từ việc hiểu Singleton là gì, đến việc triển khai các loại Singleton khác nhau, bạn现在已经 được trang bị đầy đủ để sử dụng mẫu thiết kế mạnh mẽ này trong các dự án Java của mình.

Nhớ rằng, giống như câu lạc bộ độc quyền mà chúng ta đã thảo luận ở đầu, Singleton nên được sử dụng một cách审慎. Nó là một công cụ mạnh mẽ, nhưng với quyền lực lớn đi kèm với trách nhiệm lớn!

Trong hành trình Java của bạn, bạn sẽ gặp nhiều khái niệm thú vị khác. Hãy tiếp tục lập trình, tiếp tục học hỏi, và quan trọng nhất là hãy vui vẻ! Ai biết được, có lẽ một ngày nào đó bạn sẽ dạy thế hệ lập trình tiếp theo về Singleton và những khái niệm khác.

Đến gặp lại lần sau, chúc bạn lập trình vui vẻ!

Credits: Image by storyset