synchronized এবং Lock: অপারেশনগুলোকে আবার অ্যাটমিক করা
আগের পর্বে দেখা গেছে যে count++-তে আপডেট হারিয়ে যায়, কারণ একটি রিড-মডিফাই-রাইট ধাপ ডিফল্টভাবে অ্যাটমিক নয়। synchronized হলো Java-র আদি ও বিল্ট-ইন উপায়, যা ঠিক এই সমস্যাটি সমাধান করে: এটি একটি থ্রেডকে কোনো কোড ব্লকের স্থায়িত্বকালের জন্য একটি মনিটর লকের একচেটিয়া মালিকানা নিয়ে নিতে দেয়, ফলে একই লক দিয়ে পাহারা দেওয়া অন্য কোনো ব্লক একই সময়ে অন্য থ্রেড চালাতে পারে না।
হারানো আপডেট সমস্যা সমাধান
public class SynchronizedCounter {
private int count = 0;
private final Object lock = new Object();
public void increment() {
synchronized (lock) {
count++;
}
}
public int get() {
synchronized (lock) {
return count;
}
}
public static void main(String[] args) throws InterruptedException {
SynchronizedCounter counter = new SynchronizedCounter();
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()); // reliably 200000
}
}
উপরের কোডে একই lock অবজেক্ট দিয়ে পাহারা দেওয়া synchronized (lock) ব্লকের ভিতরে একসঙ্গে কেবল একটি থ্রেড থাকতে পারে, তাই increment()-এর রিড-ইনক্রিমেন্ট-রাইট ধাপ অন্য থ্রেড দ্বারা মাঝপথে বিঘ্নিত হতে পারে না। synchronized পাশাপাশি আগের পর্বের দৃশ্যমানতার (visibility) সমস্যাও সমাধান করে: মনিটরে প্রবেশ ও প্রস্থান একটি হ্যাপেনস-বিফোর সম্পর্ক তৈরি করে, ফলে একটি synchronized ব্লকের ভিতরে লেখা মান পরবর্তীতে একই লকে সিঙ্ক্রোনাইজ করা ব্লকে প্রবেশকারী যেকোনো থ্রেড নিশ্চিতভাবে দেখতে পায়।
সঠিক বস্তুতে লক করা
Java-র প্রতিটি অবজেক্টের একটি নিজস্ব ইন্ট্রিনসিক (intrinsic) লক থাকে এবং synchronized পুরো মেথড বা যেকোনো ব্লক পাহারা দিতে পারে। আপনি যে লকের উপর সিঙ্ক্রোনাইজ করবেন তা গুরুত্বপূর্ণ: সব কলকারীকে একই অবজেক্টে সিঙ্ক্রোনাইজ করতে হবে, না হলে পারস্পরিক বর্জনের নিশ্চয়তা হারিয়ে যায়। SynchronizedCounter-এর মতো একটি প্রাইভেট, ডেডিকেটেড লক অবজেক্টে সিঙ্ক্রোনাইজ করা this-এ সিঙ্ক্রোনাইজ করার চেয়ে সাধারণত নিরাপদ, কারণ এটি ক্লাস হায়ারার্কির অন্য কোনো জায়গার কোডকে অসাবধানে আপনার লক ভাগাভাগি করতে বাধা দেয়। ইন্ট্রিনসিক লক রিএন্ট্রান্ট (reentrant) প্রকৃতির: যে থ্রেড ইতিমধ্যে একটি লক ধরে রেখেছে, সে একই লক দ্বারা সুরক্ষিত আরেকটি synchronized ব্লকে প্রবেশ করলে নিজের সাথে ডেডলক হয় না—একটি সিঙ্ক্রোনাইজড মেথড আরেকটিকে কল করলে এই বৈশিষ্ট্যটি কাজে আসে।
ReentrantLock: একই ধারণা, আরও স্পষ্ট
java.util.concurrent.locks.ReentrantLock synchronized-এর মতোই মিউচুয়াল এক্সক্লুশন দেয়, তবে এটি একটি কীওয়ার্ড না হয়ে একটি অবজেক্ট যার উপর মেথড কল করা হয়; এর ফলে synchronized-এর নেই এমন কিছু সুবিধা পাওয়া যায়:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockedCounter {
private int count = 0;
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock(); // অবশ্যই finally-তে, নাহলে ব্যর্থতা ঘটলে লক আটকে যাবে
}
}
public int get() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
LockedCounter counter = new LockedCounter();
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
}
}
উপরের কোড SynchronizedCounter-এর মতোই অ্যাটমিক ইনক্রিমেন্ট ও রিড নিশ্চিত করে, তবে ReentrantLock নিয়ে কাজ করতে lock() ও unlock() কল করতে হয়। unlock()-কে finally ব্লকের ভিতরে রাখা জরুরি, কারণ কোনো এক্সেপশন ঘটলে লকটি চিরতরে আটকে থাকতে পারে। ReentrantLock অতিরিক্ত কিছু ক্ষমতা আনে—যেমন tryLock() দিয়ে নির্দিষ্ট সময় অপেক্ষা করে লক নেওয়ার চেষ্টা, ন্যায্যতা (fairness) নীতি এবং লক নেওয়ার সময় ইন্টারাপ্ট হ্যান্ডলিং—যা সাধারণ synchronized দিয়ে সম্ভব নয়।