Immutability & Thread-Safe Design

บทนำ: ปัญหาของ Shared Mutable State

ในโลกของการเขียนโปรแกรมสมัยใหม่ แอปพลิเคชันส่วนใหญ่ต้องทำงานหลายอย่างพร้อมกัน (concurrent) เช่น web server ที่ต้องรับคำขอจากผู้ใช้หลายคนพร้อมกัน หรือ mobile app ที่ต้องประมวลผลข้อมูลขณะที่ UI ยังคงตอบสนอง

ปัญหาใหญ่ที่สุดของ concurrent programming คือ shared mutable state – สถานการณ์ที่หลาย threads เข้าถึงและแก้ไข object เดียวกันพร้อมกัน:

java// ตัวอย่างปัญหา: BankAccount ที่ไม่ปลอดภัย
public class BankAccount {
    private int balance = 1000;
    
    public void withdraw(int amount) {
        if (balance >= amount) {
            // ช่วงนี้อาจมี thread อื่นเข้ามาแทรก!
            balance = balance - amount;
        }
    }
    
    public int getBalance() {
        return balance;
    }
}

สมมติมี 2 threads พยายาม withdraw พร้อมกัน:

  • Thread A: ตรวจสอบ balance >= 600 (true, balance = 1000)
  • Thread B: ตรวจสอบ balance >= 600 (true, balance = 1000)
  • Thread A: balance = 1000 - 600 = 400
  • Thread B: balance = 1000 - 600 = 400 (ควรเป็น -200!)

ผลลัพธ์คือ withdraw 600 สองครั้ง แต่ balance เหลือ 400 แทนที่จะเป็น -200 หรือ reject ครั้งที่สอง นี่คือ race condition – ผลลัพธ์ขึ้นอยู่กับ timing ของ threads

ปัญหาเหล่านี้ยากมากในการ debug เพราะ:

  • Non-deterministic – เกิดขึ้นแบบสุ่ม ขึ้นอยู่กับ thread scheduling
  • Hard to reproduce – ทดสอบผ่าน 1000 ครั้ง แต่ครั้งที่ 1001 เกิด bug
  • Subtle – code ดูถูกต้อง แต่มีช่องว่างเล็กๆ ที่เกิดปัญหา

Immutability และ Thread-Safe Design คือวิธีแก้ปัญหาเหล่านี้อย่างมีประสิทธิภาพ


Immutability: Objects ที่ไม่เปลี่ยนแปลง

แนวคิดพื้นฐาน

Immutable object คือ object ที่เมื่อสร้างแล้ว ไม่สามารถเปลี่ยนแปลง state ได้ ตัวอย่างที่คุ้นเคยคือ String ใน Java:

javaString text = "Hello";
text.toUpperCase();  // สร้าง String ใหม่ "HELLO"
System.out.println(text);  // ยังคงเป็น "Hello"

Method toUpperCase() ไม่ได้แก้ไข text แต่ return String object ใหม่ นี่คือคุณสมบัติของ immutable object

ทำไม Immutability สำคัญ?

  1. Thread-safe โดยธรรมชาติ – ถ้า object ไม่เปลี่ยนแปลง ไม่มี race condition เกิดขึ้น
  2. ปลอดภัยในการ share – สามารถ pass object ไปมาโดยไม่กังวลว่าจะถูกแก้ไข
  3. Hash code คงที่ – ใช้เป็น key ใน HashMap ได้อย่างปลอดภัย
  4. Easier to reason about – ไม่ต้องติดตามว่า state เปลี่ยนแปลงที่ไหน
  5. Caching-friendly – สามารถ cache ได้เพราะรู้ว่าค่าไม่เปลี่ยน

Rules สำหรับสร้าง Immutable Class

มีกฎหลายข้อที่ต้องปฏิบัติตาม:

  1. ทำ class เป็น final – ป้องกันไม่ให้ subclass override methods
  2. ทำ fields เป็น private final – ไม่สามารถเปลี่ยนค่าหลัง construction
  3. ไม่มี setter methods – ไม่มีทางแก้ไข fields
  4. Defensive copying – ถ้ารับ mutable objects ต้อง copy
  5. Don’t share references – ไม่ return references ของ mutable fields

ตัวอย่างที่ 1: Creating Immutable Class

Immutable Person Class

มาสร้าง immutable class ตามกฎข้างต้น:

java// Immutable Person class
public final class ImmutablePerson {  // ← final class
    
    private final String name;         // ← final fields
    private final int age;
    private final String email;
    
    // Constructor - ทางเดียวที่ set values
    public ImmutablePerson(String name, int age, String email) {
        this.name = name;
        this.age = age;
        this.email = email;
    }
    
    // Getters only - ไม่มี setters
    public String getName() {
        return name;
    }
    
    public int getAge() {
        return age;
    }
    
    public String getEmail() {
        return email;
    }
    
    // แทนที่จะ modify ให้ return object ใหม่
    public ImmutablePerson withAge(int newAge) {
        return new ImmutablePerson(this.name, newAge, this.email);
    }
    
    public ImmutablePerson withEmail(String newEmail) {
        return new ImmutablePerson(this.name, this.age, newEmail);
    }
    
    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + 
               ", email='" + email + "'}";
    }
}

// การใช้งาน
public class ImmutablePersonDemo {
    public static void main(String[] args) {
        ImmutablePerson person1 = new ImmutablePerson("Alice", 25, "[email protected]");
        System.out.println("Original: " + person1);
        
        // "แก้ไข" age - จริงๆ สร้าง object ใหม่
        ImmutablePerson person2 = person1.withAge(26);
        System.out.println("After 'modification': " + person2);
        System.out.println("Original unchanged: " + person1);
        
        // ไม่มี setter - code นี้ compile ไม่ผ่าน
        // person1.setAge(30);  // ← Error!
    }
}

// OUTPUT:
// Original: Person{name='Alice', age=25, email='[email protected]'}
// After 'modification': Person{name='Alice', age=26, email='[email protected]'}
// Original unchanged: Person{name='Alice', age=25, email='[email protected]'}

สังเกตว่า person1 ไม่เปลี่ยนแปลง method withAge() return object ใหม่ที่มี age ต่างออกไป


ตัวอย่างที่ 2: Defensive Copying

ปัญหาของ Mutable Fields

ถ้า immutable class มี field ที่เป็น mutable object (เช่น DateList) ต้องระวังพิเศษ:

javaimport java.util.*;

// ❌ Immutable class ที่มีช่องโหว่
public final class Person {
    private final String name;
    private final Date birthDate;  // ← Date เป็น mutable!
    
    public Person(String name, Date birthDate) {
        this.name = name;
        this.birthDate = birthDate;  // ← ปัญหา!
    }
    
    public Date getBirthDate() {
        return birthDate;  // ← ปัญหา!
    }
}

// การโจมตี immutability
public class ImmutabilityBreak {
    public static void main(String[] args) {
        Date date = new Date(90, 0, 1);  // 1 Jan 1990
        Person person = new Person("Alice", date);
        
        System.out.println("Before: " + person.getBirthDate());
        
        // โจมตีผ่าน original reference
        date.setYear(95);  // เปลี่ยน 1990 → 1995
        System.out.println("After attack 1: " + person.getBirthDate());
        
        // โจมตีผ่าน getter
        person.getBirthDate().setYear(100);  // เปลี่ยน 1995 → 2000
        System.out.println("After attack 2: " + person.getBirthDate());
    }
}

// OUTPUT:
// Before: Mon Jan 01 00:00:00 ICT 1990
// After attack 1: Fri Jan 01 00:00:00 ICT 1995  ← เปลี่ยนได้!
// After attack 2: Sat Jan 01 00:00:00 ICT 2000  ← เปลี่ยนอีก!

วิธีแก้: Defensive Copying

วิธีแก้คือ defensive copying – copy mutable objects เมื่อรับและคืน:

javaimport java.util.*;

// ✓ Immutable class ที่ถูกต้อง
public final class ImmutablePerson {
    private final String name;
    private final Date birthDate;
    
    public ImmutablePerson(String name, Date birthDate) {
        this.name = name;
        // ← Defensive copy ตอน constructor
        this.birthDate = new Date(birthDate.getTime());
    }
    
    public String getName() {
        return name;
    }
    
    public Date getBirthDate() {
        // ← Defensive copy ตอน return
        return new Date(birthDate.getTime());
    }
    
    @Override
    public String toString() {
        return "Person{name='" + name + "', birthDate=" + birthDate + "}";
    }
}

// ทดสอบความปลอดภัย
public class DefensiveCopyDemo {
    public static void main(String[] args) {
        Date date = new Date(90, 0, 1);
        ImmutablePerson person = new ImmutablePerson("Alice", date);
        
        System.out.println("Original: " + person);
        
        // พยายามโจมตีผ่าน original reference
        date.setYear(95);
        System.out.println("After modifying original date: " + person);
        // ← ไม่เปลี่ยน เพราะ constructor copy แล้ว
        
        // พยายามโจมตีผ่าน getter
        person.getBirthDate().setYear(100);
        System.out.println("After modifying via getter: " + person);
        // ← ไม่เปลี่ยน เพราะ getter return copy
    }
}

// OUTPUT:
// Original: Person{name='Alice', birthDate=Mon Jan 01 00:00:00 ICT 1990}
// After modifying original date: Person{name='Alice', birthDate=Mon Jan 01 00:00:00 ICT 1990}
// After modifying via getter: Person{name='Alice', birthDate=Mon Jan 01 00:00:00 ICT 1990}

ตอนนี้ person object ป้องกันตัวเองได้ ไม่ว่าจะโจมตีจากทางไหนก็ไม่เปลี่ยน


Thread-Safe Design: ป้องกัน Race Conditions

ปัญหา: Unsynchronized Access

กลับมาดูปัญหา BankAccount แรก แต่ละ test อย่างละเอียด:

javapublic class UnsafeBankAccount {
    private int balance = 1000;
    
    public void withdraw(int amount) {
        if (balance >= amount) {
            // จุดอันตราย: thread อื่นอาจแทรกที่นี่
            try {
                Thread.sleep(10);  // จำลอง processing delay
            } catch (InterruptedException e) {}
            balance = balance - amount;
            System.out.println(Thread.currentThread().getName() + 
                             " withdrew " + amount + ", balance: " + balance);
        } else {
            System.out.println(Thread.currentThread().getName() + 
                             " insufficient funds");
        }
    }
    
    public int getBalance() {
        return balance;
    }
}

// ทดสอบด้วย multiple threads
public class RaceConditionDemo {
    public static void main(String[] args) throws InterruptedException {
        UnsafeBankAccount account = new UnsafeBankAccount();
        
        // สร้าง 3 threads พยายาม withdraw พร้อมกัน
        Thread t1 = new Thread(() -> account.withdraw(600), "Thread-1");
        Thread t2 = new Thread(() -> account.withdraw(600), "Thread-2");
        Thread t3 = new Thread(() -> account.withdraw(600), "Thread-3");
        
        t1.start();
        t2.start();
        t3.start();
        
        t1.join();
        t2.join();
        t3.join();
        
        System.out.println("\nFinal balance: " + account.getBalance());
        System.out.println("Expected: 1000 (only one withdrawal should succeed)");
    }
}

// OUTPUT (อาจแตกต่างกันในแต่ละครั้ง):
// Thread-1 withdrew 600, balance: 400
// Thread-2 withdrew 600, balance: -200
// Thread-3 withdrew 600, balance: -800
// 
// Final balance: -800
// Expected: 1000 (only one withdrawal should succeed)

เห็นปัญหาชัดเจน หลาย threads ผ่านการ check balance >= amount พร้อมกัน แล้ว withdraw พร้อมกัน ส่งผลให้ balance เป็นลบ

วิธีที่ 1: Synchronized Methods

วิธีแรกคือใช้ keyword synchronized:

javapublic class SynchronizedBankAccount {
    private int balance = 1000;
    
    // synchronized method - มี thread เดียวเข้าได้ในแต่ละครั้ง
    public synchronized void withdraw(int amount) {
        if (balance >= amount) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {}
            balance = balance - amount;
            System.out.println(Thread.currentThread().getName() + 
                             " withdrew " + amount + ", balance: " + balance);
        } else {
            System.out.println(Thread.currentThread().getName() + 
                             " insufficient funds");
        }
    }
    
    public synchronized int getBalance() {
        return balance;
    }
}

// ทดสอบ
public class SynchronizedDemo {
    public static void main(String[] args) throws InterruptedException {
        SynchronizedBankAccount account = new SynchronizedBankAccount();
        
        Thread t1 = new Thread(() -> account.withdraw(600), "Thread-1");
        Thread t2 = new Thread(() -> account.withdraw(600), "Thread-2");
        Thread t3 = new Thread(() -> account.withdraw(600), "Thread-3");
        
        t1.start();
        t2.start();
        t3.start();
        
        t1.join();
        t2.join();
        t3.join();
        
        System.out.println("\nFinal balance: " + account.getBalance());
    }
}

// OUTPUT:
// Thread-1 withdrew 600, balance: 400
// Thread-2 insufficient funds
// Thread-3 insufficient funds
// 
// Final balance: 400

ตอนนี้ปลอดภัยแล้ว! synchronized keyword ทำให้มี thread เดียวเข้า method ได้ในแต่ละครั้ง

แต่ synchronized มี trade-off:

  • Performance overhead – locking มีค่าใช้จ่าย
  • Potential deadlock – ถ้าใช้ multiple locks ไม่ระวัง
  • Reduced concurrency – threads ต้องรอคิว ไม่ได้ทำงานพร้อมกัน

ตัวอย่างที่ 3: Thread-Safe Collections

ปัญหา: Concurrent Modification

Collections ธรรมดาไม่ thread-safe:

javaimport java.util.*;

public class UnsafeListDemo {
    public static void main(String[] args) throws InterruptedException {
        List<Integer> list = new ArrayList<>();
        
        // Thread 1: เพิ่มเลข 0-999
        Thread writer = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                list.add(i);
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {}
            }
        });
        
        // Thread 2: อ่านและ print
        Thread reader = new Thread(() -> {
            try {
                Thread.sleep(100);
                // ← อันตราย: iterate ขณะที่ thread อื่นเพิ่ม
                for (Integer num : list) {
                    System.out.println(num);
                }
            } catch (Exception e) {
                System.out.println("Error: " + e.getClass().getSimpleName());
            }
        });
        
        writer.start();
        reader.start();
        
        writer.join();
        reader.join();
    }
}

// OUTPUT (มักเกิด exception):
// Error: ConcurrentModificationException

วิธีแก้: Thread-Safe Collections

Java มี thread-safe collections ใน java.util.concurrent:

javaimport java.util.concurrent.*;
import java.util.*;

public class ThreadSafeCollectionsDemo {
    
    public static void main(String[] args) throws InterruptedException {
        
        // 1. CopyOnWriteArrayList - thread-safe list
        List<String> safeList = new CopyOnWriteArrayList<>();
        
        Thread writer1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                safeList.add("Item-" + i);
                System.out.println("Added: Item-" + i);
                try { Thread.sleep(100); } catch (InterruptedException e) {}
            }
        });
        
        Thread reader1 = new Thread(() -> {
            try { Thread.sleep(200); } catch (InterruptedException e) {}
            for (String item : safeList) {
                System.out.println("Reading: " + item);
                try { Thread.sleep(50); } catch (InterruptedException e) {}
            }
        });
        
        writer1.start();
        reader1.start();
        writer1.join();
        reader1.join();
        
        System.out.println("\nFinal list: " + safeList);
        
        // 2. ConcurrentHashMap - thread-safe map
        Map<String, Integer> safeMap = new ConcurrentHashMap<>();
        
        // Multiple threads updating map
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                safeMap.put("Key-" + i, i);
            }
        });
        
        Thread t2 = new Thread(() -> {
            for (int i = 100; i < 200; i++) {
                safeMap.put("Key-" + i, i);
            }
        });
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        
        System.out.println("\nMap size: " + safeMap.size());  // 200
    }
}

Thread-Safe Collections:

  • CopyOnWriteArrayList – list ที่ copy ทุกครั้งที่ modify (ดีสำหรับ read-heavy)
  • ConcurrentHashMap – map ที่ lock เฉพาะส่วนที่ต้องการ
  • BlockingQueue – queue สำหรับ producer-consumer pattern
  • ConcurrentSkipListMap – sorted map ที่ thread-safe

ตัวอย่างที่ 4: Immutable Design Pattern

Builder Pattern สำหรับ Immutable Objects

สร้าง immutable objects ที่มีหลาย fields ยากเพราะต้อง pass ทุก parameter ใน constructor Builder pattern แก้ปัญหานี้:

java// Immutable Configuration class
public final class DatabaseConfig {
    private final String host;
    private final int port;
    private final String username;
    private final String password;
    private final int maxConnections;
    private final int timeout;
    private final boolean useSSL;
    
    // Private constructor - ใช้ผ่าน Builder เท่านั้น
    private DatabaseConfig(Builder builder) {
        this.host = builder.host;
        this.port = builder.port;
        this.username = builder.username;
        this.password = builder.password;
        this.maxConnections = builder.maxConnections;
        this.timeout = builder.timeout;
        this.useSSL = builder.useSSL;
    }
    
    // Getters
    public String getHost() { return host; }
    public int getPort() { return port; }
    public String getUsername() { return username; }
    public String getPassword() { return password; }
    public int getMaxConnections() { return maxConnections; }
    public int getTimeout() { return timeout; }
    public boolean isUseSSL() { return useSSL; }
    
    // Builder class
    public static class Builder {
        // Required parameters
        private final String host;
        private final int port;
        
        // Optional parameters - default values
        private String username = "admin";
        private String password = "";
        private int maxConnections = 10;
        private int timeout = 30000;
        private boolean useSSL = true;
        
        public Builder(String host, int port) {
            this.host = host;
            this.port = port;
        }
        
        public Builder username(String username) {
            this.username = username;
            return this;
        }
        
        public Builder password(String password) {
            this.password = password;
            return this;
        }
        
        public Builder maxConnections(int maxConnections) {
            this.maxConnections = maxConnections;
            return this;
        }
        
        public Builder timeout(int timeout) {
            this.timeout = timeout;
            return this;
        }
        
        public Builder useSSL(boolean useSSL) {
            this.useSSL = useSSL;
            return this;
        }
        
        public DatabaseConfig build() {
            // Validation ก่อน create
            if (maxConnections <= 0) {
                throw new IllegalArgumentException("maxConnections must be positive");
            }
            if (timeout < 0) {
                throw new IllegalArgumentException("timeout must be non-negative");
            }
            return new DatabaseConfig(this);
        }
    }
    
    @Override
    public String toString() {
        return "DatabaseConfig{" +
               "host='" + host + '\'' +
               ", port=" + port +
               ", username='" + username + '\'' +
               ", maxConnections=" + maxConnections +
               ", timeout=" + timeout +
               ", useSSL=" + useSSL +
               '}';
    }
}

// การใช้งาน
public class BuilderPatternDemo {
    public static void main(String[] args) {
        // สร้าง config ด้วย builder - ชัดเจนและยืดหยุ่น
        DatabaseConfig config1 = new DatabaseConfig.Builder("localhost", 5432)
            .username("admin")
            .password("secret123")
            .maxConnections(50)
            .timeout(60000)
            .useSSL(false)
            .build();
        
        System.out.println("Config 1: " + config1);
        
        // สร้าง config อีกตัว - ใช้ default บางค่า
        DatabaseConfig config2 = new DatabaseConfig.Builder("192.168.1.10", 3306)
            .username("app_user")
            .maxConnections(20)
            .build();
        
        System.out.println("Config 2: " + config2);
        
        // ไม่สามารถแก้ไข config ที่สร้างแล้ว
        // config1.setMaxConnections(100);  // ← No setter!
        
        // Thread-safe - share ได้อย่างปลอดภัย
        shareConfigAcrossThreads(config1);
    }
    
    private static void shareConfigAcrossThreads(DatabaseConfig config) {
        // หลาย threads ใช้ config เดียวกัน - ปลอดภัย
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + 
                                 " using config: " + config.getHost());
            }).start();
        }
    }
}

// OUTPUT:
// Config 1: DatabaseConfig{host='localhost', port=5432, username='admin', maxConnections=50, timeout=60000, useSSL=false}
// Config 2: DatabaseConfig{host='192.168.1.10', port=3306, username='app_user', maxConnections=20, timeout=30000, useSSL=true}
// Thread-0 using config: localhost
// Thread-1 using config: localhost
// Thread-2 using config: localhost
// Thread-3 using config: localhost
// Thread-4 using config: localhost

Builder pattern ให้ประโยชน์:

  • Readable – code อ่านง่าย ชัดเจนว่า set ค่าอะไร
  • Flexible – set เฉพาะค่าที่ต้องการ ส่วนอื่นใช้ default
  • Validation – ตรวจสอบ validity ใน build() method
  • Immutable result – object ที่ได้เป็น immutable

Advanced: Volatile และ Atomic Variables

Volatile Keyword

volatile keyword บอก JVM ว่าตัวแปรนี้อาจถูกแก้ไขโดยหลาย threads:

javapublic class VolatileDemo {
    
    // ❌ Without volatile - อาจไม่เห็นการเปลี่ยนแปลง
    // private boolean running = true;
    
    // ✓ With volatile - รับประกันว่าเห็นการเปลี่ยนแปลง
    private volatile boolean running = true;
    
    public void start() {
        new Thread(() -> {
            System.out.println("Thread started");
            while (running) {
                // Do work
            }
            System.out.println("Thread stopped");
        }).start();
    }
    
    public void stop() {
        System.out.println("Stopping thread...");
        running = false;
    }
    
    public static void main(String[] args) throws InterruptedException {
        VolatileDemo demo = new VolatileDemo();
        demo.start();
        
        Thread.sleep(1000);
        demo.stop();
    }
}

volatile รับประกัน:

  • Visibility – การเปลี่ยนแปลงจาก thread หนึ่งมองเห็นได้ทันทีโดย threads อื่น
  • Ordering – ป้องกัน compiler reordering

แต่ volatile ไม่รับประกัน atomicity สำหรับ compound operations (เช่น counter++)

Atomic Variables

สำหรับ atomic operations ใช้ java.util.concurrent.atomic:

javaimport java.util.concurrent.atomic.*;

public class AtomicDemo {
    
    // ❌ Non-atomic counter
    private int unsafeCounter = 0;
    
    // ✓ Atomic counter
    private AtomicInteger safeCounter = new AtomicInteger(0);
    
    public void incrementUnsafe() {
        unsafeCounter++;  // ← NOT thread-safe!
        // เทียบเท่า: temp = unsafeCounter; temp = temp + 1; unsafeCounter = temp;
    }
    
    public void incrementSafe() {
        safeCounter.incrementAndGet();  // ← Thread-safe atomic operation!
    }
    
    public static void main(String[] args) throws InterruptedException {
        AtomicDemo demo = new AtomicDemo();
        
        // สร้าง 10 threads แต่ละตัว increment 1000 ครั้ง
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    demo.incrementUnsafe();
                    demo.incrementSafe();
                }
            });
            threads[i].start();
        }
        
        // รอ threads ทั้งหมด
        for (Thread t : threads) {
            t.join();
        }
        
        System.out.println("Expected: 10000");
        System.out.println("Unsafe counter: " + demo.unsafeCounter);  // น้อยกว่า 10000
        System.out.println("Safe counter: " + demo.safeCounter.get());  // = 10000
    }
}

// OUTPUT:
// Expected: 10000
// Unsafe counter: 9847  (อาจต่างกันในแต่ละครั้ง)
// Safe counter: 10000

Atomic classes:

  • AtomicIntegerAtomicLong – atomic numeric operations
  • AtomicBoolean – atomic boolean operations
  • AtomicReference<T> – atomic reference operations

Best Practices & Guidelines

เมื่อใช้ Immutability:

ข้อดี:

  • Thread-safe โดยธรรมชาติ – ไม่ต้อง synchronization
  • ปลอดภัยในการ share – pass ไปมาได้สบายใจ
  • Simpler reasoning – ไม่ต้องติดตามว่า state เปลี่ยนที่ไหน
  • Cacheable – ใช้เป็น keys ใน maps ได้

ข้อควรพิจารณา:

  • Memory overhead – แต่ละการ “แก้ไข” สร้าง object ใหม่
  • GC pressure – object เยอะขึ้น garbage collector ทำงานหนัก
  • ไม่เหมาะกับ frequently-changing data

แนวทางปฏิบัติ:

  • ใช้ immutable objects สำหรับ value objects (Person, Address, Money)
  • ใช้ Builder pattern สำหรับ complex objects
  • Defensive copying สำหรับ mutable fields
  • พิจารณา performance trade-off

เมื่อออกแบบ Thread-Safe:

Strategies:

  1. Immutability (best) – ไม่มี shared mutable state
  2. Synchronization – lock เมื่อ access shared state
  3. Thread-safe collections – ใช้ concurrent collections
  4. Atomic variables – สำหรับ simple atomic operations
  5. Thread-local – แต่ละ thread มี copy ของตัวเอง

หลีกเลี่ยง:

  • Over-synchronization – lock มากเกินทำให้ช้า
  • Holding locks too long – block threads อื่นนาน
  • Nested locks – ง่ายต่อ deadlock
  • Shared mutable state – source ของปัญหาทั้งหมด

สรุป

Immutability & Thread-Safe Design เป็นหลักการสำคัญในการพัฒนาซอฟต์แวร์ concurrent:

Immutability ให้ประโยชน์:

  • Thread-safe โดยธรรมชาติ – ไม่มี race conditions
  • ปลอดภัยในการ share objects
  • Simpler เพราะ state ไม่เปลี่ยน
  • Cacheable และใช้เป็น keys ได้

Thread-Safe Design ต้องการ:

  • เข้าใจ race conditions และ visibility problems
  • ใช้ synchronization เมื่อจำเป็น
  • เลือก thread-safe collections ที่เหมาะสม
  • พิจารณา atomic variables สำหรับ simple operations

ตัวอย่างการใช้จริง:

  • Configuration objects – immutable configs ที่ share ได้
  • Value objects – Money, Date, Coordinate เป็น immutable
  • Cache keys – immutable objects เป็น keys ที่ปลอดภัย
  • Concurrent systems – thread-safe collections สำหรับ shared data

การออกแบบ immutable และ thread-safe ไม่ใช่ง่าย แต่เมื่อทำถูกต้อง มันช่วยป้องกัน bugs ที่ยากต่อการ debug และทำให้ระบบมีความเสถียร โดยเฉพาะในยุคที่ multi-core processors เป็นมาตรฐาน