Atomic ক্লাস ও CAS: লক-মুক্ত থ্রেড নিরাপত্তা

synchronized হারানো আপডেট ঠিক করে অন্য থ্রেডকে অপেক্ষা করিয়ে। java.util.concurrent.atomic প্যাকেজ একই সমস্যা সম্পূর্ণ ভিন্নভাবে সমাধান করে: এটি কোনো থ্রেডকেই ব্লক করে না, বরং CPU-র একটি নির্দেশ compare-and-swap (CAS) ব্যবহার করে সংঘর্ষ শনাক্ত করে এবং প্রয়োজনে পুনরায় চেষ্টা করে।

সহজ সমাধান

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounter {
    private final AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet();
    }

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

    public static void main(String[] args) throws InterruptedException {
        AtomicCounter counter = new AtomicCounter();
        Runnable task = () -> {
            for (int i = 0; i < 100_000; i++) {
                counter.increment();
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("Result: " + counter.get()); // নির্ভরযোগ্যভাবে 200000
    }
}

এই কোডে incrementAndGet() সম্পূর্ণ অ্যাটমিক: একই মেথডে দুইটি থ্রেড কল করলেও কোনো আপডেট হারিয়ে যায় না। এখানে কোনো লক অর্জন, ধরে রাখা বা ছেড়ে দিতে ভুলে যাওয়ার ঝামেলা নেই।

CAS আসলে কী করে

Compare-and-swap একটি একক CPU নির্দেশ, যার তিনটি ইনপুট থাকে: একটি মেমোরি লোকেশন, একটি প্রত্যাশিত বর্তমান মান, এবং একটি নতুন মান। এটি অ্যাটমিকভাবে পরীক্ষা করে যে মেমোরি লোকেশনটি এখনও প্রত্যাশিত মান ধরে রেখেছে কিনা; যদি ধরে রাখে, তাহলে সেটিকে নতুন মান দিয়ে প্রতিস্থাপন করে এবং সফলতার সংকেত দেয়। যদি মানটি পড়ার পর থেকে বদলে গিয়ে থাকে, তাহলে ব্যর্থতার সংকেত দেয় এবং কিছুই পরিবর্তন করে না।

AtomicInteger.incrementAndGet() ঠিক এই আদিম নির্দেশের ওপর ভিত্তি করে একটি লুপের মাধ্যমে তৈরি:

import java.util.concurrent.atomic.AtomicInteger;

public class ManualCas {
    private final AtomicInteger value = new AtomicInteger(0);

    public void incrementTheHardWay() {
        int current;
        int next;
        do {
            current = value.get();
            next = current + 1;
        } while (!value.compareAndSet(current, next)); // অন্য থ্রেড জিতে গেলে আবার চেষ্টা
    }
}

get() এবং compareAndSet()-এর মাঝখানে যদি অন্য কোনো থ্রেড value পরিবর্তন করে ফেলে, তাহলে CAS কলটি ব্যর্থ হয় এবং লুপটি নতুন বর্তমান মান পড়ে আবার চেষ্টা করে। কোনো থ্রেডই লকের জন্য অপেক্ষা করে না; যে থ্রেড প্রতিযোগিতায় হারে সে অবিলম্বে পুনরায় চেষ্টা শুরু করে। এ কারণেই অ্যাটমিক ক্লাসগুলোকে “শুধু দ্রুত লক” না বলে লক-মুক্ত বলা হয় – সংঘর্ষের সময় থ্রেড পার্ক না হয়ে স্পিন করে ও পুনরায় চেষ্টা করে, যা অত্যন্ত ছোট ক্রিটিক্যাল সেকশনের (যেমন একটি কাউন্টার বৃদ্ধি) ক্ষেত্রে ভালো স্কেল করে।

কাউন্টার ছাড়িয়ে: AtomicReference

একই CAS প্যাটার্ন শুধু সংখ্যার ওপর নয়, অবজেক্ট রেফারেন্সের ওপরও কাজ করে। এর ফলে কোনো লক ছাড়াই জটিল immutable স্টেট অ্যাটমিকভাবে হালনাগাদ করা যায়।

import java.util.concurrent.atomic.AtomicReference;

public class AtomicConfig {
    private final AtomicReference<Config> current = new AtomicReference<>(Config.defaults());

    public Config get() {
        return current.get();
    }

    public void reload(Config newConfig) {
        current.set(newConfig); // পুরো রেফারেন্স অ্যাটমিকভাবে বদলে যায়
    }

    public boolean updateIfUnchanged(Config expected, Config newConfig) {
        return current.compareAndSet(expected, newConfig);
    }

    public static void main(String[] args) {
        AtomicConfig configHolder = new AtomicConfig();
        System.out.println("প্রাথমিক কনফিগ: " + configHolder.get());

        // নতুন কনফিগ লোড
        configHolder.reload(new Config("prod", true));
        System.out.println("রিলোডের পর: " + configHolder.get());

        // শর্তসাপেক্ষ আপডেট
        Config currentCfg = configHolder.get();
        Config updatedCfg = new Config(currentCfg.name(), !currentCfg.enabled());
        boolean success = configHolder.updateIfUnchanged(currentCfg, updatedCfg);
        System.out.println("আপডেট সফল? " + success + ", এখন কনফিগ: " + configHolder.get());
    }
}

record Config(String name, boolean enabled) {
    static Config defaults() {
        return new Config("default", false);
    }
}

এখানে AtomicReference<Config> ব্যবহার করে পুরো কনফিগ অবজেক্টটিকেই অ্যাটমিকভাবে বদলানো যায়। compareAndSet মেথডটি তখনই নতুন মান বসায় যখন রেফারেন্সটি এখনও প্রত্যাশিত অবজেক্টকেই নির্দেশ করে – এতে একাধিক থ্রেড নিরাপদে immutable কনফিগ আপডেট করতে পারে, কোনো লক ছাড়াই।

Share