Design Pattern 101 — Singleton Pattern

Phayao Boonon
3 min readAug 17, 2019

--

สำหรับ Design Pattern ตัวต่อไปของ GoF คือ Singleton Pattern ที่อยู่ในกลุ่มของ Creation Pattern ซึ่งเป็น pattern ที่หลายท่านอาจจะใช้อยู่เป็นประจำอยู่แล้ว วันนี้จะมาทบทวนและเรียนรู้ว่า Singleton Pattern มีประโยชน์อย่างไรบ้างและ ถ้าเราจะใช้ pattern นี้กับ Multitheading Application นั้นต้องทำอย่างไรบ้าง

https://refactoring.guru/design-patterns/singleton

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 นี้

  1. เพิ่ม synchronized keyword เข้าไปใน method getInstance()
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 นั้นเอง 😅

--

--

Phayao Boonon

Software Engineer 👨🏻‍💻 Stay Hungry Stay Foolish