Cách vận dụng Singleton pattern p2: Singleton trong môi trường đa luồng
1 : if ( instance == null ) { 2 : instance = new Singleton ( ) ; 3 : } Phương thức trên là không thread-safe. Nếu một luồng ưu tiên chiếm dòng 2 trước khi gán giá trị cho biến được thực hiện, biến instance có thể vẫn đang là null, và rồi sau đó luồng khác có thể ...
1: if(instance == null) { 2: instance = new Singleton(); 3: }
Phương thức trên là không thread-safe. Nếu một luồng ưu tiên chiếm dòng 2 trước khi gán giá trị cho biến được thực hiện, biến instance có thể vẫn đang là null, và rồi sau đó luồng khác có thể truy cập vào khối lệnh if. Trong trường hợp đó, hai thể hiện singleton khác nhau có thể được tạo ra. Không may, kịch bản đó rất hiếm khi xảy ra và do đó rất khó tái hiện trong khi test. Dưới đây, mình sẽ cài đặt lại ví dụ trong phần một để có thể tái hiện vấn đề ở trên:
import org.apache.log4j.Logger; public class Singleton { private static Singleton singleton = null; private static Logger logger = Logger.getRootLogger(); private static boolean firstThread = true; protected Singleton() { // Exists only to defeat instantiation. } public static Singleton getInstance() { if(singleton == null) { simulateRandomActivity(); singleton = new Singleton(); } logger.info("created singleton: " + singleton); return singleton; } private static void simulateRandomActivity() { try { if(firstThread) { firstThread = false; logger.info("sleeping..."); // This nap should give the second thread enough time // to get by the first thread. Thread.currentThread().sleep(50); } } catch(InterruptedException ex) { logger.warn("Sleep interrupted"); } } }
Lần đầu khi gọi phương thức getInstance được gọi, thread đó sẽ ngủ trong 50 mili giây. Và có một thread khác gọi đến phương thức getInstance thì sẽ tạo ra một thể hiện singleton. Khi thread đang ngủ, thức dậy nó cũng sẽ tạo một thể hiện singleton mới. Vậy chúng ta có ít nhất 2 thể hiện của singleton. Mặc dù ví dụ ở trên là do chúng ta cố tình tạo ra, nhưng nó cho thấy tình huống trong môi trường đa luồng. Chúng ta vẫn có thể có 2 thể hiện của singleton.
Synchronization
Từ ví dụ trên, chúng ta đặt ra bài toán làm thế nào để chỉ có một thể hiện của singleton ? Đơn giản là chúng ta sử dụng Synchronization khi gọi phương thức getInstance() như sau:
public synchronized static Singleton getInstance() { if(singleton == null) { simulateRandomActivity(); singleton = new Singleton(); } logger.info("created singleton: " + singleton); return singleton; }
Sử dụng synchronization rất tốn hiệu năng và chậm (một phương thức synchronization chạy chậm tới 100 lần so với unsynchronization phương thức). Do đó, chúng ta sẽ tiếp cận cách khác để cải thiện hiệu năng.
public static Singleton getInstance() { if(singleton == null) { synchronized(Singleton.class) { singleton = new Singleton(); } } return singleton; }
Thay vì synchronization toàn bộ phương thức, chúng ta chỉ cần synchronization cho một đoạn mã quan trọng. Tuy nhiên, đoạn mã trên không thread-safe, chúng ta phải khắc phục điều này bằng cách double-checking
public static Singleton getInstance() { if(singleton == null) { synchronized(Singleton.class) { if(singleton == null) { singleton = new Singleton(); } } } return singleton; }
Thật không may, việc double-checking lại không đảm bảo để làm việc vì trình biên dịch sẽ giải phóng giá trị được gán của biến thành viên của singleton, trước khi khởi tạo singleton. Nếu điều đó xảy ra, thread 1 có thể bị chặn trước khi tham chiếu singleton đã được gán, nhưng trước khi singleton khởi tạo, thì thread 2 có thể trả về một tham chiếu tới một thể hiện chưa được khởi tạo của singleton.
Tuy nhiên, có một cách khác đơn giản, nhanh và thread-safe hơn.
public class Singleton { public final static Singleton INSTANCE = new Singleton(); private Singleton() { // Exists only to defeat instantiation. } }
Đầu tiên cách cài đặt singleton ở trên là thread-safe bởi vì biến static được tạo khi khai báo đã đảm bảo việc tạo ra lần đầu khi chúng ta truy cập. Không sử dụng synchronization sẽ giúp chương trình chạy nhanh hơn.
Như vậy, chúng ta đã đưa ra một giải pháp giúp chúng ta tạo ra một singleton hoàn hảo. Nhưng trong thực tế chúng ta sẽ có rất nhiều singleton trong một ứng dụng. Vậy làm thế nào để quản lý sử dụng nó một cách hiệu quả ? Chúng ta sẽ theo dõi ở phần tiếp theo