Design Pattern 101 — Singleton Pattern
สำหรับ Design Pattern ตัวต่อไปของ GoF คือ Singleton Pattern ที่อยู่ในกลุ่มของ Creation Pattern ซึ่งเป็น pattern ที่หลายท่านอาจจะใช้อยู่เป็นประจำอยู่แล้ว วันนี้จะมาทบทวนและเรียนรู้ว่า Singleton Pattern มีประโยชน์อย่างไรบ้างและ ถ้าเราจะใช้ pattern นี้กับ Multitheading Application นั้นต้องทำอย่างไรบ้าง
Singleton Pattern
Problem
ในโรงงานซ็อกโกแลตสมัยใหม่จะมีหม้อต้นไอน้ำซ็อกโกเลต (Chocolate Boiler) ที่ควบคุมด้วยคอมพิวเตอร์ ซึ่งหน้าที่ของหม้อต้มไอน้ำคือรับ ซ็อกโกเลต และ นม เข้าไปในหม้อต้อ และนำมาต้นรวมกัน และส่งต่อยังขั้นตอนต่อไป ของการทำซ็อกโกเลต
ChocolateBoiler
เป็น class ที่ควบคุมของหม้อต้มไอน้ำ จะเห็นได้ว่ามีการตรวจสอบหม้อต้นไอ้นำก่อนที่จะ fill()
เติมซ็อกโกเลตและนม หรือ boil()
ต้มซ็อกโกเลตและนม หรือ drain()
ปล่อยซ็อกโกเลตและนมที่ต้มเรียบร้อยแล้วออกมาจากหม้อต้มไอ้น้ำ
public class ChocolateBoiler {
private boolean empty;
private boolean boiled;
public ChocolateBoiler() {
empty = true;
boiled = false;
}
public void fill() {
if(isEmpty()) {
empty = false;
boiled = false;
}
}
public void drain() {
if(!isEmpty() && isBoiled()) {
empty = true;
}
}
public void boil() {
if(!isEmpty() && !isBoiled()) {
boiled = true;
}
}
private boolean isEmpty() {
return empty;
}
private boolean isBoiled() {
return boiled;
}
}
ถ้า ChocolateBoiler
มีเพียง instance เดียวจะไม่มีปัญหาอะไรเพราะสถานะของหม้อต้มไอน้ำจะควบคุมด้วย instance เดียว แต่ถ้ามี 2 instance ล่ะ การควบคุมหม้อมต้มไอน้ำจะรวนไปหมด
Solution
ดังนั้นเราจะทำให้ class ChocolateBoiler
ให้มีเพียง instance เดียวด้วย Singleton Pattern ดังนี้
โดยเปลี่ยนให้ constructor ของ class ChocolateBoiler
เป็น private เพื่อให้แน่ใจว่าไม่มีการสร้าง instance ได้จากภายนอก class และประกาศตัวแปล static เป็น type ของ class ChocolateBoiler
เอง เพื่อเป็น instance เดียวของ singleton class นี้ และใช้งาน instance นี้ได้ด้วย method getInstance()
ถ้า variable instance
ไม่ได้สร้างก็จะสร้างในครั้งแรก
public class ChocolateBoiler {
private boolean empty;
private boolean boiled;
private static ChocolateBoiler instance;
private ChocolateBoiler() {
empty = true;
boiled = false;
}
public static ChocolateBoiler getInstance() {
if(instance == null) {
instance = new ChocolateBoiler();
}
return instance;
}
public void fill() {
if(isEmpty()) {
empty = false;
boiled = false;
}
}
public void drain() {
if(!isEmpty() && isBoiled()) {
empty = true;
}
}
public void boil() {
if(!isEmpty() && !isBoiled()) {
boiled = true;
}
}
private boolean isEmpty() {
return empty;
}
private boolean isBoiled() {
return boiled;
}
}
เวลาใช้งานเราจะไม่สามารถสร้าง instance ของ class ChocolateBoiler
ได้แต่สามารถเรียกด้วย method getInstance()
ChocolateBoiler boiler1 = ChocolateBoiler.getInstance();
ChocolateBoiler boiler2 = ChocolateBoiler.getInstance();
boiler1.fill();
boiler2.boil();
boiler2.fill();
boiler1.boil();
boiler1.drain();
boiler2.drain();
Singleton Pattern จะมีเพียงแค่ instance เดียวเท่านั้น และมี global point เพื่อเข้าถึง instance นี้
จะเห็นได้วา singleton class มี instance ของ class นั้นเพียง instance เดียวนั้นก็คือ variable instance
และใช้ method getInstance()
เพื่อเข้าถึง instance หนึ่งเดียวนั้น
ถ้าเราใช้ตัวควบคุม class ChocolateBoiler
มากกว่า 2 ที่คนละเมือง แต่ต้องการควนคุมหม้อต้มไอน้ำซ็อกโกเลตเดียวกัน เปรียบเสมือนรัน instance ของ ChocolateBoiler
บน 2 Thread
Thread t1 = new Thread() {
@Override
public void run() {
ChocolateBoiler boiler = ChocolateBoiler.getInstance();
System.out.println(boiler.toString());
boiler.fill();
boiler.boil();
boiler.drain();
}
};
Thread t2 = new Thread() {
@Override
public void run() {
ChocolateBoiler boiler = ChocolateBoiler.getInstance();
System.out.println(boiler.toString());
boiler.fill();
boiler.boil();
boiler.drain();
}
};
t1.start();
t2.start();
เมื่อลองรัน จะกลายเป็นว่า instance ของ class ChocolateBoiler
มี 2 instance ในแต่ละ Thread ซึ่งจะทำให้หม้อต้มไอน้ำซ็อกโกเลตล้นออกมาได้
com.iphayao.ChocolateBoiler@614b201a
com.iphayao.ChocolateBoiler@3c23e9a7
ดังนั้นเราจะต้องจัดการกับ Multithreading นี้
- เพิ่ม
synchronized
keyword เข้าไปใน methodgetInstance()
public static synchronized ChocolateBoiler getInstance() {
if (instance == null) {
instance = new ChocolateBoiler();
}
return instance;
}
และลองรันดูจะเห็นได้ว่า ทั้ง 2 Thread ใช้ instance เดียวกัน
com.iphayao.ChocolateBoiler@163e638a
com.iphayao.ChocolateBoiler@163e638a
2. ย้ายตำแหน่งสร้าง instance ของ ChocolateBoiler
ไปไว้นอก getInstance()
private static ChocolateBoiler instance = new ChocolateBoiler();
public static ChocolateBoiler getInstance() {
return instance;
}
และลองรันดูจะเห็นได้ว่า ทั้ง 2 Thread ใช้ instance เดียวกัน เช่นเดียวกัน
com.iphayao.ChocolateBoiler@75da154b
com.iphayao.ChocolateBoiler@75da154b
3. ใช้วิธี double-checked locking โดยใช้ volatile
ในตอนประกาศ instance และตรวจสอบ instance ว่าสร้างแล้วหรือยัง 2 ครั้งโดยครั้งที่ 2 จะใช้ synchronized
มา lock ให้เฉพราะบาง Thread สร้าง instance เท่านั้น
private volatile static ChocolateBoiler instance;
public static ChocolateBoiler getInstance() {
if (instance == null) {
synchronized (ChocolateBoiler.class) {
if(instance == null) {
instance = new ChocolateBoiler();
}
}
}
return instance;
}
ลองรันดูจะเห็นได้ว่า ทั้ง 2 Thread ใช้ instance เดียวกัน แน่นอน
com.iphayao.ChocolateBoiler@77dfdf51
com.iphayao.ChocolateBoiler@77dfdf51
สรุป
จากการที่ทบทวนลองใช้ Singleton Pattern มาสร้าง class ChocolateBoiler
นี้จะเห็นได้ว่า Singleton class จะเหมาะสำหรับในกรณีที่เราต้องการให้แน่ใจว่าทั้ง application context มีเพียง instacne object เดียวเท่านั้น และไม่ต้องการการขัดแย้งของข้อมูล และเมื่อนำ Singleton class ไปใช้กับ Thread มากกว่า 1 thread แล้วพบว่ามีปัญหาจริงและได้เอาแนวทางแก้ไขด้วยวิธีต่างๆ มาใช้ก็จะทำให้เรายังใช้ instance เดียวอยู่แม้ว่าจะทำงานบนแต่ละ Thread ก็ตาม ซึ่งตัวอย่างและวิธีแก้ปัญหาก็มาจาก Head First Desing Patterns นั้นเอง 😅