รูปแบบการออกแบบที่ควบคุมให้คลาสมีอินสแตนซ์เดียว และจัดให้มีจุดเข้าถึงอินสแตนซ์นั้นร่วมกัน

บทความนี้อธิบายแนวคิด เปรียบเทียบวิธีเขียน Singleton และทำ workshop ด้วย Java โดยใช้เฉพาะ Java Standard Library ตัวอย่างรองรับ JDK 17 ขึ้นไป และไม่ต้องติดตั้ง Maven หรือ Gradle

#1. Singleton คืออะไร

Singleton เป็น Design Pattern ในกลุ่ม Creational Patterns ซึ่งเกี่ยวข้องกับการสร้างออบเจ็กต์ จุดมุ่งหมายคือควบคุมจำนวนอินสแตนซ์ของคลาส และให้ส่วนต่าง ๆ ของโปรแกรมเข้าถึงอินสแตนซ์เดียวกันได้

ตัวอย่างเช่น แอปพลิเคชันอ่านค่าการตั้งค่าชุดเดียวเมื่อเริ่มทำงาน แล้วให้ส่วนงานจัดการคำสั่งซื้อ รายงาน และแจ้งเตือนอ่านจากออบเจ็กต์เดียวกัน แทนการสร้างออบเจ็กต์การตั้งค่าใหม่ทุกครั้ง

อย่างไรก็ตาม การมีข้อมูลชุดเดียวไม่ได้แปลว่าต้องใช้ Singleton เสมอไป เราสามารถสร้างออบเจ็กต์หนึ่งครั้งในจุดเริ่มต้นของโปรแกรม แล้วส่งออบเจ็กต์นั้นให้ส่วนงานต่าง ๆ ผ่าน constructor ได้เช่นกัน

#ขอบเขตของคำว่า “หนึ่งอินสแตนซ์”

สำหรับตัวอย่างในบทความนี้ ขอบเขตคือคลาสเดียวกันที่ถูกโหลดโดย defining ClassLoader เดียวกัน หาก ClassLoader ต่างกันโหลดคลาสแยกกัน อาจมี Singleton คนละอินสแตนซ์ และเมื่อรันหลาย JVM หรือหลาย container แต่ละ process มีอินสแตนซ์ของตนเอง

ดังนั้น Java Singleton ไม่ใช่กลไกที่รับประกันว่าทั้งระบบกระจายจะมีออบเจ็กต์หรือผู้ทำงานเพียงตัวเดียว หากต้องการประสานงานข้าม process ต้องออกแบบด้วยกลไกอื่น เช่น distributed coordination หรือข้อจำกัดในฐานข้อมูลให้ตรงกับปัญหา

#2. องค์ประกอบหลัก

Singleton แบบคลาสทั่วไปมีองค์ประกอบดังนี้

องค์ประกอบ หน้าที่
private constructor ป้องกันโค้ดภายนอกเรียก constructor ตามปกติ
ฟิลด์ static สำหรับเก็บอินสแตนซ์ เก็บ reference ที่เป็นของคลาส
จุดเข้าถึง เช่น getInstance() ส่งคืน reference ของอินสแตนซ์ร่วมกัน
กลไกควบคุมการสร้าง ป้องกันการสร้างซ้ำเมื่อมีหลายเธรด

private constructor เพียงอย่างเดียวไม่ได้ป้องกันทุกช่องทาง เช่น reflection ที่มีสิทธิ์เข้าถึง หรือ Java serialization หากคลาสประกาศรองรับ ส่วน Enum มีกลไกของภาษาที่จัดการประเด็นเหล่านี้ต่างจากคลาสทั่วไป

#3. Eager Initialization: เรียบง่ายและสร้างเมื่อคลาสถูก initialize

public final class EagerSingleton {
    private static final EagerSingleton INSTANCE = new EagerSingleton();

    private EagerSingleton() {
    }

    public static EagerSingleton getInstance() {
        return INSTANCE;
    }
}

อินสแตนซ์ถูกสร้างระหว่าง initialization ของคลาส ไม่จำเป็นต้องเกิดตั้งแต่ JVM เริ่มทำงาน และไม่ใช่เพียงขั้นตอนโหลดคลาส เพราะ loading กับ initialization เป็นคนละขั้นตอน

Java จัดการ synchronization ของ class initialization ให้ จึงใช้วิธีนี้เพื่อสร้างและเผยแพร่ reference อย่างปลอดภัยระหว่างเธรดได้ เหมาะกับออบเจ็กต์ที่สร้างง่ายและไม่มีเหตุผลต้องเลื่อนการสร้าง [1]

ข้อแลกเปลี่ยนคือ เมื่อคลาสถูก initialize อินสแตนซ์ก็ถูกสร้าง แม้การใช้งานครั้งนั้นอาจไม่ได้ต้องการอินสแตนซ์โดยตรง และหาก constructor ทำงานหนักหรือทำ I/O ก็อาจทำให้ initialization ล่าช้าหรือล้มเหลว

#4. Lazy Initialization แบบธรรมดา: ใช้สอนปัญหา concurrency

// ตัวอย่างที่ไม่ปลอดภัยเมื่อหลายเธรดเรียกพร้อมกัน
public final class UnsafeLazySingleton {
    private static UnsafeLazySingleton instance;

    private UnsafeLazySingleton() {
    }

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

ปัญหาคือเธรด A และ B อาจอ่าน instance == null พร้อมกัน แล้วแต่ละเธรดสร้างออบเจ็กต์ของตนเอง ทำให้มีการสร้างมากกว่าหนึ่งครั้ง นอกจากนี้ยังมีปัญหาการมองเห็นข้อมูลและ safe publication

ตัวอย่างนี้ใช้เพื่อทำความเข้าใจ race condition ไม่ควรนำไปใช้เมื่อโปรแกรมมีหลายเธรด การรันแล้วไม่พบออบเจ็กต์ซ้ำในครั้งหนึ่งไม่ได้พิสูจน์ว่าโค้ดปลอดภัย

#5. Synchronized Accessor: แก้ปัญหาการสร้างด้วย lock

public final class SynchronizedSingleton {
    private static SynchronizedSingleton instance;

    private SynchronizedSingleton() {
    }

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

เมธอด static synchronized ใช้ monitor ของคลาส ทำให้เธรดเข้าถึงเมธอดนี้ทีละเธรด การปล่อยและเข้าถือ monitor ยังสร้างความสัมพันธ์ happens-before ตาม Java Memory Model [3]

วิธีนี้อ่านง่ายและเป็น lazy initialization แต่ทุกครั้งที่เรียก accessor ต้องผ่าน synchronization ผลต่อประสิทธิภาพจริงขึ้นอยู่กับรูปแบบการใช้งาน ควรวัดก่อนเพิ่มความซับซ้อนเพื่อปรับประสิทธิภาพ

#6. Initialization-on-demand Holder: lazy โดยอาศัย class initialization

public final class HolderSingleton {
    private HolderSingleton() {
    }

    private static class Holder {
        private static final HolderSingleton INSTANCE = new HolderSingleton();
    }

    public static HolderSingleton getInstance() {
        return Holder.INSTANCE;
    }
}

เมื่อเรียก getInstance() ครั้งแรกและเข้าถึง Holder.INSTANCE คลาส Holder จะถูก initialize จากนั้นจึงสร้างอินสแตนซ์ การ initialize ของคลาสชั้นนอกเพียงอย่างเดียวไม่ทำให้คลาส Holder ถูก initialize ไปด้วย

ความปลอดภัยในการสร้างและเผยแพร่ออบเจ็กต์อาศัยกลไก class initialization ของ Java โดยไม่ต้องเขียน synchronized ใน accessor เอง [1] นี่เป็นทางเลือกที่ดีเมื่อจำเป็นต้องใช้ Singleton แบบคลาสและต้องการเลื่อนการสร้าง

หาก initialization ล้มเหลว คลาสอาจอยู่ใน erroneous state การเรียกครั้งถัดไปไม่ได้ลองสร้างใหม่โดยอัตโนมัติ จึงควรหลีกเลี่ยงการฝังงานที่ต้อง retry เช่นการเชื่อมต่อเครือข่ายไว้ใน static initialization

#7. Double-Checked Locking: ต้องมี volatile

public final class DclSingleton {
    private static volatile DclSingleton instance;

    private DclSingleton() {
    }

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

การตรวจครั้งแรกหลีกเลี่ยง lock หลังสร้างแล้ว ส่วนการตรวจครั้งที่สองป้องกันกรณีเธรดอื่นสร้างอินสแตนซ์ระหว่างที่เธรดปัจจุบันรอ lock

สำหรับรูปแบบนี้ volatile เป็นส่วนจำเป็นเพื่อให้ reference ถูกเผยแพร่อย่างปลอดภัย โดยการเขียนฟิลด์ volatile มีความสัมพันธ์ happens-before กับการอ่านฟิลด์นั้นที่ตามมา [3] อย่าลบ volatile โดยคิดว่า synchronized ภายในเพียงอย่างเดียวเพียงพอ เพราะเส้นทางหลังสร้างแล้วอ่านจากภายนอก synchronized block

วิธีนี้ซับซ้อนกว่า Holder หากไม่มีข้อจำกัดเฉพาะ การใช้ Holder มักทำให้โค้ดเข้าใจและดูแลได้ง่ายกว่า

#8. Enum Singleton: กระชับและรักษา identity ผ่าน serialization

public enum EnumSingleton {
    INSTANCE;

    public String getApplicationName() {
        return "Singleton Workshop";
    }
}

ใช้งานผ่าน constant โดยตรง

EnumSingleton singleton = EnumSingleton.INSTANCE;
System.out.println(singleton.getApplicationName());

Enum ที่มี constant เดียวมีอินสแตนซ์ของ constant นั้นเพียงตัวเดียวภายในขอบเขตคลาสเดียวกัน Java ไม่อนุญาตให้สร้าง enum ด้วย new และห้าม reflective instantiation ผ่านกลไก reflection ปกติ ส่วน Java serialization จัดการ enum เป็นกรณีพิเศษเพื่อไม่สร้างอินสแตนซ์ซ้ำเมื่อ deserialize [2]

Enum มีเมธอด ฟิลด์ และสามารถ implement interface ได้ แต่ไม่สามารถ extends คลาสอื่น และอินสแตนซ์ถูกสร้างเมื่อ enum class ถูก initialize จึงไม่ใช่ lazy แบบ Holder ที่แยกคลาสเก็บอินสแตนซ์ออกมา

เลือก Enum เมื่อรูปแบบ constant เหมาะกับ API ที่ต้องการ และไม่มีข้อจำกัดด้าน framework หรือ lifecycle ที่ทำให้ใช้งานลำบาก

#9. เปรียบเทียบวิธีเขียน

ตารางนี้เปรียบเทียบเฉพาะความปลอดภัยในการสร้างและเผยแพร่อินสแตนซ์ ไม่ได้หมายความว่าการแก้ไขทุกฟิลด์ภายในปลอดภัยไปด้วย

วิธี เวลาสร้าง สร้างอย่างปลอดภัยระหว่างเธรด สิ่งที่ต้องพิจารณา
Eager เมื่อคลาสถูก initialize ได้ เรียบง่าย แต่ไม่เลื่อนตามการเรียก accessor
Unsafe Lazy เมื่อเรียกครั้งแรก ไม่ได้ ใช้สอนปัญหา race condition
Synchronized เมื่อเรียกครั้งแรก ได้ ทุกการเรียก accessor ผ่าน lock
Holder เมื่อเข้าถึง Holder ครั้งแรก ได้ ใช้ class initialization จัดการ
Double-Checked Locking เมื่อเรียกครั้งแรก ได้ เมื่อเขียนถูกต้องและใช้ volatile ซับซ้อนกว่า Holder
Enum เมื่อ enum class ถูก initialize ได้ รองรับ identity ผ่าน Java serialization

#10. Workshop: แบ่งปันการตั้งค่าและส่ง dependency ผ่าน constructor

#เป้าหมาย

  1. สร้าง configuration ที่แก้ไขไม่ได้หลังสร้าง
  2. ให้จุดเริ่มต้นของโปรแกรมเลือก Singleton
  3. ส่ง configuration ให้ service ผ่าน constructor
  4. ตรวจว่า reference ที่เรียกจาก Singleton เป็นออบเจ็กต์เดียวกัน

สร้างโฟลเดอร์ singleton-workshop และไฟล์ทั้งสามต่อไปนี้ในโฟลเดอร์เดียวกัน

#ไฟล์ AppConfig.java

public enum AppConfig {
    INSTANCE;

    private final String applicationName;
    private final int maxItemsPerOrder;

    AppConfig() {
        applicationName = "Java Singleton Workshop";
        maxItemsPerOrder = 20;
    }

    public String getApplicationName() {
        return applicationName;
    }

    public int getMaxItemsPerOrder() {
        return maxItemsPerOrder;
    }
}

ไม่มี setter และฟิลด์เป็น final โดยค่าที่เก็บเป็น String และ int จึงไม่มี mutable object ถูกเปิดเผยออกไป สำหรับ configuration ที่เก็บ collection ต้องพิจารณา defensive copy หรือ immutable collection ด้วย

#ไฟล์ OrderService.java

public final class OrderService {
    private final AppConfig config;

    public OrderService(AppConfig config) {
        this.config = java.util.Objects.requireNonNull(config);
    }

    public boolean canPlaceOrder(int itemCount) {
        return itemCount > 0 && itemCount <= config.getMaxItemsPerOrder();
    }
}

การรับ configuration ผ่าน constructor ทำให้ dependency มองเห็นได้จาก API อย่างไรก็ตาม enum มีค่าเดียว การทดสอบหลาย configuration ยังไม่ยืดหยุ่น หากต้องการเปลี่ยนค่าในการทดสอบควรแยกเป็น interface หรือใช้ configuration object ธรรมดาที่ส่งเข้าไปได้

#ไฟล์ Main.java

public class Main {
    public static void main(String[] args) {
        AppConfig first = AppConfig.INSTANCE;
        AppConfig second = AppConfig.INSTANCE;
        OrderService orders = new OrderService(first);

        System.out.println("Application: " + first.getApplicationName());
        System.out.println("Same instance: " + (first == second));
        System.out.println("Order with 10 items: " + orders.canPlaceOrder(10));
        System.out.println("Order with 25 items: " + orders.canPlaceOrder(25));
        System.out.println("Order with 0 items: " + orders.canPlaceOrder(0));
    }
}

#Compile และรัน

เปิด terminal ภายในโฟลเดอร์ workshop โดยติดตั้ง JDK ที่มี javac ก่อน

javac -encoding UTF-8 AppConfig.java OrderService.java Main.java
java Main

ผลลัพธ์

Application: Java Singleton Workshop
Same instance: true
Order with 10 items: true
Order with 25 items: false
Order with 0 items: false

ใช้ == เพื่อเปรียบเทียบ reference ว่าชี้ไปยังออบเจ็กต์เดียวกัน อย่าใช้ค่าที่พิมพ์จาก hashCode() เป็นหลักฐานว่าเป็นออบเจ็กต์เดียวกัน เพราะคนละออบเจ็กต์อาจมี hash code เท่ากันได้

#11. สร้างอย่างปลอดภัยไม่ได้แปลว่า mutable state ปลอดภัย

แม้ใช้ Enum หรือ Holder หากมีเมธอดดังนี้ก็ยังเกิด race condition ได้

// ส่วนหนึ่งของคลาสหรือ enum ที่มีหลายเธรดใช้งาน
private int count;

public void increment() {
    count++; // อ่านค่า คำนวณ และเขียนกลับ ไม่ใช่ atomic operation
}

การเปลี่ยน count เป็น volatile int เพียงอย่างเดียวก็ไม่ทำให้ count++ เป็น atomic ต้องเลือก synchronization หรือชนิดข้อมูลที่เหมาะกับ operation เช่น AtomicInteger สำหรับตัวนับเดี่ยว

สร้างไฟล์ RequestCounter.java

import java.util.concurrent.atomic.AtomicInteger;

public enum RequestCounter {
    INSTANCE;

    private final AtomicInteger count = new AtomicInteger();

    public int increment() {
        return count.incrementAndGet();
    }

    public int getCount() {
        return count.get();
    }
}

final ทำให้ reference ของ count ไม่ถูกเปลี่ยน แต่การแก้ไขค่าปลอดภัยเกิดจาก operation ของ AtomicInteger ถ้ามี business rule ที่อัปเดตหลายฟิลด์ร่วมกัน atomic ของแต่ละฟิลด์อาจยังไม่เพียงพอ ต้องออกแบบให้รักษา invariant ทั้งชุด

#12. Workshop ตรวจสอบหลายเธรดและ serialization

นำไฟล์ HolderSingleton.java จากหัวข้อ 6 มาไว้ในโฟลเดอร์เดียวกัน จากนั้นสร้าง SingletonVerification.java ดังนี้

import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

public class SingletonVerification {
    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(8);
        CountDownLatch start = new CountDownLatch(1);

        try {
            // ทดสอบการเข้าถึง Holder ครั้งแรกจากหลายเธรด
            List<Future<HolderSingleton>> references = new ArrayList<>();
            for (int i = 0; i < 8; i++) {
                references.add(pool.submit(() -> {
                    start.await();
                    return HolderSingleton.getInstance();
                }));
            }
            start.countDown();
            HolderSingleton first = references.get(0).get();
            for (Future<HolderSingleton> reference : references) {
                if (reference.get() != first) {
                    throw new AssertionError("Different Holder instances");
                }
            }
            System.out.println("Holder identity: PASS");

            int before = RequestCounter.INSTANCE.getCount();
            List<Callable<Void>> tasks = new ArrayList<>();
            for (int i = 0; i < 8; i++) {
                tasks.add(() -> {
                    for (int j = 0; j < 10_000; j++) {
                        RequestCounter.INSTANCE.increment();
                    }
                    return null;
                });
            }
            for (Future<Void> result : pool.invokeAll(tasks)) {
                result.get(); // ส่งต่อข้อผิดพลาดจาก worker
            }
            int added = RequestCounter.INSTANCE.getCount() - before;
            if (added != 80_000) {
                throw new AssertionError("Unexpected count: " + added);
            }
            System.out.println("Atomic counter: PASS (added " + added + ")");
        } finally {
            pool.shutdownNow();
        }

        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        try (ObjectOutputStream out = new ObjectOutputStream(bytes)) {
            out.writeObject(AppConfig.INSTANCE);
        }
        try (ObjectInputStream in = new ObjectInputStream(
                new ByteArrayInputStream(bytes.toByteArray()))) {
            if (in.readObject() != AppConfig.INSTANCE) {
                throw new AssertionError("Enum identity changed");
            }
        }
        System.out.println("Enum serialization identity: PASS");
    }
}

รันด้วยคำสั่ง

javac -encoding UTF-8 AppConfig.java HolderSingleton.java RequestCounter.java SingletonVerification.java
java SingletonVerification

ผลลัพธ์

Holder identity: PASS
Atomic counter: PASS (added 80000)
Enum serialization identity: PASS

การทดสอบนี้ตรวจตัวอย่างภายใต้การทำงานจริง แต่ไม่ได้แทนการพิสูจน์ความถูกต้องของ concurrency ทุก interleaving ความถูกต้องของ Holder และ volatile ต้องอาศัยข้อกำหนดของภาษา ไม่ใช่จำนวนครั้งที่ stress test ผ่าน

#13. ปัญหาที่มักพบ

#Hidden dependency และ global state

หาก service เรียก SomeSingleton.getInstance() จากภายในโดยตรง ผู้อ่าน constructor จะไม่เห็น dependency นั้นชัดเจน การทดสอบและการเปลี่ยน implementation จึงยากขึ้น ใช้ constructor injection เพื่อเปิดเผย dependency และพิจารณาว่าจำเป็นต้องจำกัดการสร้างจริงหรือเพียงต้องการแชร์ออบเจ็กต์

#ข้อมูลข้ามผู้ใช้หรือข้ามการทดสอบ

อย่าเก็บ current user, shopping cart หรือ request context ไว้ใน Singleton ที่ทุก request ใช้ร่วมกัน เพราะอาจเกิดข้อมูลปะปนระหว่างผู้ใช้ ส่วน mutable state ในการทดสอบก็อาจคงอยู่จาก test ก่อนหน้าและทำให้ผลขึ้นกับลำดับการรัน

#Reflection และ serialization ของคลาสทั่วไป

private constructor ของคลาสทั่วไปอาจถูกเรียกผ่าน reflection เมื่อสภาพแวดล้อมอนุญาต หากคลาส implements Serializable การ deserialize อาจสร้างออบเจ็กต์ใหม่ สามารถออกแบบ readResolve() ให้คืนอินสแตนซ์หลักได้ แต่ยังต้องพิจารณาสัญญา serialization โดยรวม หากไม่จำเป็น อย่าเพิ่ม Serializable ให้คลาสโดยไม่มีเหตุผล

#ใช้ connection เดียวกับทุก request

อย่าใช้ Singleton เพื่อแชร์ JDBC Connection เดียวโดยอัตโนมัติ เพราะมีทั้ง transaction state และ lifecycle ที่ต้องจัดการ สิ่งที่อาจแชร์คือ DataSource หรือ connection pool ที่ถูกออกแบบให้ใช้งานร่วมกัน โดยแต่ละงานยืมและคืน connection ตาม lifecycle ของตน

#Lifecycle และการปิดทรัพยากร

ออบเจ็กต์ที่มี thread pool, file handle หรือ network client ต้องมีเจ้าของ lifecycle ที่ชัดเจน Singleton ไม่ได้จัดการ shutdown, retry หรือ resource cleanup ให้เอง งานลักษณะนี้มักเหมาะกับการให้ application หรือ DI container เป็นผู้สร้างและดูแล

#14. Singleton ต่างจาก static utility และการแชร์ผ่าน DI อย่างไร

วิธี ลักษณะ เหมาะกับ
Singleton Pattern ควบคุมการสร้างอินสแตนซ์และมีจุดเข้าถึงร่วมกัน ออบเจ็กต์ที่มีเหตุผลต้องจำกัดจำนวนในขอบเขตหนึ่ง
Static Utility เรียก static methods โดยไม่ต้องมีอินสแตนซ์ ฟังก์ชันทั่วไปที่ไม่ต้องมี object identity หรือ lifecycle
สร้างครั้งเดียวแล้ว inject จุดประกอบโปรแกรมสร้างออบเจ็กต์และส่ง reference เดียวกัน การแชร์ service พร้อมเปิดเผย dependency และเปลี่ยน implementation ได้

การแชร์ออบเจ็กต์ผ่าน dependency injection ไม่จำเป็นต้องห้ามสร้างอินสแตนซ์อื่นทั่วทั้งโปรแกรม จึงยืดหยุ่นกว่าในการทดสอบและการแยกขอบเขตของแอปพลิเคชัน

#15. แนวทางเลือกใช้และแบบฝึกหัด

เริ่มจากถามว่าจำเป็นต้องควบคุมการสร้างจริงหรือไม่ หากต้องการเพียงแชร์ dependency ให้สร้างครั้งเดียวแล้วส่งผ่าน constructor หากต้องการ Singleton แบบคลาสที่เป็น lazy ให้พิจารณา Holder หากรูปแบบ enum เหมาะสมให้พิจารณา Enum Singleton และเมื่อมี mutable state ให้ตรวจการใช้งานระหว่างเธรดแยกจากวิธีสร้างอินสแตนซ์เสมอ

แบบฝึกหัดสำหรับผู้เรียน

  1. เปลี่ยน AppConfig เป็น immutable class ธรรมดา แล้วสร้างสอง configuration เพื่อทดสอบ OrderService ด้วยขีดจำกัดต่างกัน
  2. เขียน interface สำหรับ configuration เพื่อให้ OrderService ไม่ผูกกับ enum โดยตรง
  3. เปลี่ยนตัวนับเป็น int และทดลองหลายเธรด อธิบายว่าทำไมผลที่บังเอิญถูกต้องยังไม่พิสูจน์ความปลอดภัย
  4. รันโปรแกรมสอง JVM พร้อมกัน แล้วอธิบายว่าทำไมตัวนับของแต่ละ JVM จึงแยกจากกัน
  5. ยกตัวอย่างข้อมูลที่ควรเป็นของแต่ละ request และอธิบายผลเสียหากเก็บไว้ใน Singleton

#แหล่งอ้างอิง

บทความเรียบเรียงใหม่เพื่อการเรียนรู้ โดยตรวจกลไกของ Java กับเอกสารทางการ และใช้ Java SE 21 เป็นฐานอ้างอิงสำหรับข้อกำหนดที่เกี่ยวข้อง ตัวอย่างใช้ syntax ที่รองรับบน JDK 17 ขึ้นไป