Singletons trong Java

1. Giới thiệu

Trong bài viết nhanh này, chúng tôi sẽ thảo luận về hai cách phổ biến nhất để triển khai Singletons trong Java đơn giản.

2. Singleton dựa trên lớp

Cách tiếp cận phổ biến nhất là triển khai Singleton bằng cách tạo một lớp thông thường và đảm bảo rằng nó có:

  • Một nhà xây dựng riêng
  • Một trường tĩnh chứa trường hợp duy nhất của nó
  • Một phương thức nhà máy tĩnh để lấy phiên bản

Chúng tôi cũng sẽ thêm một thuộc tính thông tin, chỉ để sử dụng sau này. Vì vậy, việc triển khai của chúng tôi sẽ giống như sau:

public final class ClassSingleton { private static ClassSingleton INSTANCE; private String info = "Initial info class"; private ClassSingleton() { } public static ClassSingleton getInstance() { if(INSTANCE == null) { INSTANCE = new ClassSingleton(); } return INSTANCE; } // getters and setters }

Mặc dù đây là một cách tiếp cận phổ biến, nhưng điều quan trọng cần lưu ý là nó có thể có vấn đề trong các tình huống đa luồng , đây là lý do chính để sử dụng Singletons.

Nói một cách đơn giản, nó có thể dẫn đến nhiều trường hợp, phá vỡ nguyên tắc cốt lõi của khuôn mẫu. Mặc dù có các giải pháp khóa cho vấn đề này, nhưng phương pháp tiếp theo của chúng tôi sẽ giải quyết các vấn đề này ở cấp cơ sở.

3. Enum Singleton

Về sau, chúng ta đừng thảo luận về một cách tiếp cận thú vị khác - đó là sử dụng phép liệt kê:

public enum EnumSingleton { INSTANCE("Initial class info"); private String info; private EnumSingleton(String info) { this.info = info; } public EnumSingleton getInstance() { return INSTANCE; } // getters and setters }

Cách tiếp cận này có tuần tự hóa và an toàn luồng được đảm bảo bởi chính việc triển khai enum, điều này đảm bảo nội bộ rằng chỉ có thể hiện duy nhất, khắc phục các vấn đề được chỉ ra trong quá trình triển khai dựa trên lớp.

4. Cách sử dụng

Để sử dụng ClassSingleton của chúng tôi , chúng tôi chỉ cần lấy thể hiện tĩnh:

ClassSingleton classSingleton1 = ClassSingleton.getInstance(); System.out.println(classSingleton1.getInfo()); //Initial class info ClassSingleton classSingleton2 = ClassSingleton.getInstance(); classSingleton2.setInfo("New class info"); System.out.println(classSingleton1.getInfo()); //New class info System.out.println(classSingleton2.getInfo()); //New class info

Đối với EnumSingleton , chúng ta có thể sử dụng nó giống như bất kỳ Java Enum nào khác:

EnumSingleton enumSingleton1 = EnumSingleton.INSTANCE.getInstance(); System.out.println(enumSingleton1.getInfo()); //Initial enum info EnumSingleton enumSingleton2 = EnumSingleton.INSTANCE.getInstance(); enumSingleton2.setInfo("New enum info"); System.out.println(enumSingleton1.getInfo()); // New enum info System.out.println(enumSingleton2.getInfo()); // New enum info

5. Cạm bẫy chung

Singleton là một mẫu thiết kế được coi là đơn giản và có một vài lỗi phổ biến mà một lập trình viên có thể mắc phải khi tạo một singleton.

Chúng tôi phân biệt hai loại vấn đề với singleton:

  • tồn tại (chúng ta có cần một singleton không?)
  • triển khai (chúng ta có triển khai nó đúng cách không?)

5.1. Các vấn đề hiện có

Về mặt khái niệm, một singleton là một loại biến toàn cục. Nói chung, chúng ta biết rằng nên tránh các biến toàn cục - đặc biệt nếu trạng thái của chúng là có thể thay đổi.

Chúng tôi không nói rằng chúng tôi không bao giờ nên sử dụng các đĩa đơn. Tuy nhiên, chúng tôi đang nói rằng có thể có nhiều cách hiệu quả hơn để tổ chức mã của chúng tôi.

Nếu việc triển khai của một phương thức phụ thuộc vào một đối tượng singleton, tại sao không chuyển nó dưới dạng một tham số? Trong trường hợp này, chúng tôi hiển thị rõ ràng những gì phương thức phụ thuộc vào. Do đó, chúng tôi có thể dễ dàng bắt chước các phụ thuộc này (nếu cần) khi thực hiện kiểm thử.

Ví dụ, các singleton thường được sử dụng để bao gồm dữ liệu cấu hình của ứng dụng (tức là kết nối với kho lưu trữ). Nếu chúng được sử dụng như các đối tượng toàn cục, thì việc chọn cấu hình cho môi trường thử nghiệm sẽ trở nên khó khăn.

Do đó, khi chúng tôi chạy thử nghiệm, cơ sở dữ liệu sản xuất bị hỏng với dữ liệu thử nghiệm, điều này khó có thể chấp nhận được.

Nếu chúng ta cần một singleton, chúng ta có thể xem xét khả năng ủy quyền việc khởi tạo nó cho một lớp khác - một loại nhà máy - sẽ đảm bảo rằng chỉ có một phiên bản của singleton đang hoạt động.

5.2. Vấn đề triển khai

Mặc dù các singlelet có vẻ khá đơn giản, nhưng việc triển khai chúng có thể gặp nhiều vấn đề khác nhau. Tất cả dẫn đến thực tế là chúng ta có thể chỉ có nhiều hơn một thể hiện của lớp.

Đồng bộ hóa

Việc triển khai với một hàm tạo riêng mà chúng tôi đã trình bày ở trên không an toàn theo luồng: nó hoạt động tốt trong môi trường đơn luồng, nhưng trong môi trường đa luồng, chúng ta nên sử dụng kỹ thuật đồng bộ hóa để đảm bảo tính nguyên tử của hoạt động:

public synchronized static ClassSingleton getInstance() { if (INSTANCE == null) { INSTANCE = new ClassSingleton(); } return INSTANCE; }

Lưu ý từ khóa được đồng bộ hóa trong khai báo phương thức. Phần thân của phương thức có một số hoạt động (so sánh, khởi tạo và trả về).

Trong trường hợp không đồng bộ hóa, có khả năng hai luồng xen kẽ các quá trình thực thi của chúng theo cách mà biểu thức INSTANCE == null đánh giá là true cho cả hai luồng và kết quả là hai phiên bản ClassSingleton được tạo.

Đồng bộ hóa có thể ảnh hưởng đáng kể đến hiệu suất. Nếu mã này được gọi thường xuyên, chúng ta nên tăng tốc nó bằng nhiều kỹ thuật khác nhau như khởi tạo lười biếng hoặc khóa kiểm tra hai lần (lưu ý rằng điều này có thể không hoạt động như mong đợi do tối ưu hóa trình biên dịch). Chúng tôi có thể xem thêm chi tiết trong hướng dẫn của chúng tôi "Khóa được kiểm tra kép với Singleton".

Nhiều phiên bản

Có một số vấn đề khác với các singleton liên quan đến bản thân JVM có thể khiến chúng ta kết thúc với nhiều trường hợp của một singleton. Những vấn đề này khá phức tạp và chúng tôi sẽ mô tả ngắn gọn cho từng vấn đề:

  1. Một singleton được cho là duy nhất trên mỗi JVM. Đây có thể là vấn đề đối với hệ thống phân tán hoặc hệ thống có nội bộ dựa trên công nghệ phân tán.
  2. Mọi trình nạp lớp đều có thể tải phiên bản singleton của nó.
  3. Một singleton có thể được thu thập rác khi không ai có tham chiếu đến nó. Sự cố này không dẫn đến sự hiện diện của nhiều cá thể singleton cùng một lúc, nhưng khi được tạo lại, cá thể có thể khác với phiên bản trước của nó.

6. Kết luận

Trong hướng dẫn nhanh này, chúng tôi tập trung vào cách triển khai mẫu Singleton chỉ sử dụng Java lõi và cách đảm bảo nó nhất quán và cách sử dụng các triển khai này.

Việc triển khai đầy đủ các ví dụ này có thể được tìm thấy trên GitHub.