আমি প্রথম বিটা থেকে জাভাতে কোড করেছি; এমনকি তখনও, থ্রেডগুলি আমার প্রিয় বৈশিষ্ট্যগুলির তালিকার শীর্ষে ছিল। জাভাই প্রথম ভাষা যা ভাষাতেই থ্রেড সমর্থন চালু করেছিল; এটি তখন একটি বিতর্কিত সিদ্ধান্ত ছিল।
বিগত দশকে, প্রতিটি ভাষাই অ্যাসিঙ্ক/অপেক্ষা অন্তর্ভুক্ত করার জন্য দৌড়েছিল, এমনকি জাভা এর জন্য কিছু তৃতীয় পক্ষের সমর্থন ছিল … কিন্তু জাভা জাগ করার পরিবর্তে জিগ করেছে এবং অনেক উন্নত ভার্চুয়াল থ্রেড (প্রজেক্ট লুম) চালু করেছে। এই পোস্টটি সে সম্পর্কে নয়।
আমি মনে করি এটা চমৎকার এবং জাভার মূল শক্তি প্রমাণ করে। শুধু ভাষা হিসেবে নয়, সংস্কৃতি হিসেবে। ফ্যাশনেবল প্রবণতায় ছুটে যাওয়ার পরিবর্তে ইচ্ছাকৃত পরিবর্তনের সংস্কৃতি।
এই পোস্টে, আমি জাভাতে থ্রেডিং করার পুরানো উপায়গুলি পুনরায় দেখতে চাই। আমি synchronized
, wait
, notify
ইত্যাদি করতে অভ্যস্ত। কিন্তু জাভাতে থ্রেডিংয়ের জন্য তারা উচ্চতর পদ্ধতির জন্য অনেক দিন হয়ে গেছে।
আমি সমস্যার অংশ; আমি এখনও এই পদ্ধতির সাথে অভ্যস্ত এবং জাভা 5 থেকে আসা কিছু API-তে অভ্যস্ত হওয়া কঠিন বলে মনে হয়। এটি অভ্যাসের শক্তি। টি
থ্রেডগুলির সাথে কাজ করার জন্য অনেকগুলি দুর্দান্ত API রয়েছে যা আমি এখানে ভিডিওগুলিতে আলোচনা করেছি, তবে আমি লকগুলি সম্পর্কে কথা বলতে চাই যা মৌলিক এখনও গুরুত্বপূর্ণ।
সিঙ্ক্রোনাইজড ছেড়ে যাওয়ার সাথে আমার একটি অনিচ্ছা ছিল যে বিকল্পগুলি খুব বেশি ভাল নয়। আজ এটি ছেড়ে দেওয়ার প্রাথমিক প্রেরণা হল যে, এই সময়ে, সিঙ্ক্রোনাইজড তাঁতে থ্রেড পিনিং ট্রিগার করতে পারে যা আদর্শ নয়।
JDK 21 এটি ঠিক করতে পারে (যখন Loom GA যায়), কিন্তু এটি এখনও এটি ছেড়ে দেওয়ার কিছু অর্থ করে।
সিঙ্ক্রোনাইজের জন্য সরাসরি প্রতিস্থাপন হল ReentrantLock। দুর্ভাগ্যবশত, ReentrantLock-এর সিঙ্ক্রোনাইজডের তুলনায় খুব কম সুবিধা রয়েছে, তাই মাইগ্রেট করার সুবিধাটি সর্বোত্তমভাবে সন্দেহজনক।
প্রকৃতপক্ষে, এর একটি প্রধান অসুবিধা রয়েছে; যে একটি ধারনা পেতে, আসুন একটি উদাহরণ তাকান. এইভাবে আমরা সিঙ্ক্রোনাইজ ব্যবহার করব:
synchronized(LOCK) { // safe code } LOCK.lock(); try { // safe code } finally { LOCK.unlock(); }
ReentrantLock
এর প্রথম অসুবিধা হল verbosity. আমাদের চেষ্টা ব্লক দরকার কারণ যদি ব্লকের মধ্যে একটি ব্যতিক্রম ঘটে তবে লকটি থাকবে। সিঙ্ক্রোনাইজড হ্যান্ডলগুলি আমাদের জন্য নির্বিঘ্নে।
কিছু লোক AutoClosable
দিয়ে লকটি মোড়ানোর একটি কৌশল রয়েছে যা মোটামুটি এইরকম দেখায়:
public class ClosableLock implements AutoCloseable { private final ReentrantLock lock; public ClosableLock() { this.lock = new ReentrantLock(); } public ClosableLock(boolean fair) { this.lock = new ReentrantLock(fair); } @Override public void close() throws Exception { lock.unlock(); } public ClosableLock lock() { lock.lock(); return this; } public ClosableLock lockInterruptibly() throws InterruptedException { lock.lock(); return this; } public void unlock() { lock.unlock(); } }
লক্ষ্য করুন আমি লক ইন্টারফেস বাস্তবায়ন করি না যা আদর্শ হত। কারণ লক পদ্ধতিটি void
এর পরিবর্তে স্বয়ংক্রিয়-বন্ধযোগ্য বাস্তবায়ন প্রদান করে।
একবার আমরা এটি করি, আমরা আরও সংক্ষিপ্ত কোড লিখতে পারি যেমন:
try(LOCK.lock()) { // safe code }
আমি হ্রাসকৃত শব্দচয়ন পছন্দ করি, কিন্তু এটি একটি সমস্যাযুক্ত ধারণা কারণ ট্রাই-উথ-রিসোর্স পরিষ্কারের উদ্দেশ্যে ডিজাইন করা হয়েছে এবং আমরা লকগুলি পুনরায় ব্যবহার করি। এটি বন্ধ আহ্বান করছে, কিন্তু আমরা একই বস্তুতে আবার সেই পদ্ধতিটি আহ্বান করব।
আমি মনে করি লক ইন্টারফেস সমর্থন করার জন্য রিসোর্স সিনট্যাক্সের সাথে চেষ্টাটি প্রসারিত করা ভাল হতে পারে। কিন্তু এটি না হওয়া পর্যন্ত, এটি একটি সার্থক কৌশল হতে পারে না।
ReentrantLock
ব্যবহার করার সবচেয়ে বড় কারণ হল লুম সাপোর্ট। অন্যান্য সুবিধাগুলি চমৎকার, তবে তাদের কোনটিই "হত্যাকারী বৈশিষ্ট্য" নয়।
আমরা এটি একটি অবিচ্ছিন্ন ব্লকের পরিবর্তে পদ্ধতির মধ্যে ব্যবহার করতে পারি। এটি সম্ভবত একটি খারাপ ধারণা কারণ আপনি লক এলাকাটি ছোট করতে চান এবং ব্যর্থতা একটি সমস্যা হতে পারে। আমি সেই বৈশিষ্ট্যটিকে সুবিধা হিসাবে বিবেচনা করি না।
এতে ন্যায্যতার বিকল্প রয়েছে। এর মানে হল যে এটি প্রথম থ্রেডটি পরিবেশন করবে যা প্রথমে একটি লক এ থামে। আমি একটি বাস্তবসম্মত অ-জড়িত ব্যবহারের ক্ষেত্রে চিন্তা করার চেষ্টা করেছি যেখানে এটি গুরুত্বপূর্ণ হবে এবং আমি ফাঁকা আঁকছি।
আপনি যদি একটি রিসোর্সে ক্রমাগত অনেক থ্রেডের সাথে সারিবদ্ধ একটি জটিল সময়সূচী লিখছেন, তাহলে আপনি এমন একটি পরিস্থিতি তৈরি করতে পারেন যেখানে একটি থ্রেড "ক্ষুধার্ত" হয় কারণ অন্যান্য থ্রেড আসতে থাকে। .
হয়তো আমি এখানে কিছু মিস করছি...
lockInterruptibly()
আমাদের একটি থ্রেডকে বাধা দিতে দেয় যখন এটি একটি লকের জন্য অপেক্ষা করছে। এটি একটি আকর্ষণীয় বৈশিষ্ট্য, কিন্তু আবার, এমন পরিস্থিতি খুঁজে পাওয়া কঠিন যেখানে এটি বাস্তবিকভাবে একটি পার্থক্য তৈরি করবে।
আপনি যদি এমন কোড লেখেন যা বাধা দেওয়ার জন্য খুবই প্রতিক্রিয়াশীল হতে হবে, তাহলে সেই ক্ষমতা অর্জনের জন্য আপনাকে lockInterruptibly()
API ব্যবহার করতে হবে। কিন্তু আপনি lock()
পদ্ধতিতে গড়ে কতক্ষণ ব্যয় করেন?
এমন কিছু এজ কেস রয়েছে যেখানে এটি সম্ভবত গুরুত্বপূর্ণ কিন্তু এমন কিছু নয় যা আমাদের বেশিরভাগেরই হবে, এমনকি উন্নত মাল্টি-থ্রেড কোড করার সময়ও।
একটি অনেক ভালো পদ্ধতি হল ReadWriteReentrantLock
। বেশিরভাগ সংস্থান ঘন ঘন পঠিত নীতি অনুসরণ করে, এবং কিছু লেখার ক্রিয়াকলাপ। যেহেতু একটি ভেরিয়েবল পড়া থ্রেড-সেফ, তাই লকের কোন প্রয়োজন নেই যদি না আমরা ভেরিয়েবলে লেখার প্রক্রিয়ায় থাকি।
এর মানে আমরা লেখার ক্রিয়াকলাপগুলিকে কিছুটা ধীর করার সময় চরমভাবে পড়াকে অপ্টিমাইজ করতে পারি।
ধরে নিই যে এটি আপনার ব্যবহারের ক্ষেত্রে, আপনি অনেক দ্রুত কোড তৈরি করতে পারেন। একটি রিড-রাইট লক দিয়ে কাজ করার সময়, আমাদের দুটি লক থাকে; একটি রিড লক যেমন আমরা নিচের ছবিতে দেখতে পাচ্ছি। এটি একাধিক থ্রেডের মাধ্যমে যেতে দেয় এবং কার্যকরভাবে একটি "সকলের জন্য বিনামূল্যে"।
একবার আমাদের ভেরিয়েবলে লিখতে হবে, আমাদের একটি রাইট লক পেতে হবে যেমনটি আমরা নিম্নলিখিত চিত্রটিতে দেখতে পাচ্ছি। আমরা লিখতে লক অনুরোধ করার চেষ্টা করি, কিন্তু ভেরিয়েবল থেকে এখনও থ্রেড পড়া আছে তাই আমাদের অপেক্ষা করতে হবে।
একবার থ্রেড পড়া শেষ হয়ে গেলে, সমস্ত রিডিং ব্লক হয়ে যাবে, এবং লেখার কাজটি শুধুমাত্র একটি একক থ্রেড থেকে ঘটতে পারে যেমনটি নিম্নলিখিত ছবিতে দেখা যায়। একবার আমরা রাইট লকটি ছেড়ে দিলে, আমরা প্রথম চিত্রের "সকলের জন্য বিনামূল্যে" অবস্থায় ফিরে যাব।
এটি একটি শক্তিশালী প্যাটার্ন যা আমরা অনেক দ্রুত সংগ্রহ করতে সুবিধা নিতে পারি। একটি সাধারণ সিঙ্ক্রোনাইজড তালিকা উল্লেখযোগ্যভাবে ধীর। এটি সমস্ত ক্রিয়াকলাপ, পড়া বা লিখতে সিঙ্ক্রোনাইজ করে। আমাদের একটি CopyOnWriteArrayList আছে যা পড়ার জন্য দ্রুত, কিন্তু যেকোনো লেখা খুব ধীর।
ধরে নিচ্ছি যে আপনি আপনার পদ্ধতিগুলি থেকে পুনরাবৃত্তিকারী ফেরত এড়াতে পারেন, আপনি তালিকা ক্রিয়াকলাপগুলিকে এনক্যাপসুলেট করতে পারেন এবং এই API ব্যবহার করতে পারেন।
উদাহরণস্বরূপ, নিম্নলিখিত কোডে, আমরা নামের তালিকাটি শুধুমাত্র পঠনযোগ্য হিসাবে প্রকাশ করি, কিন্তু তারপরে যখন আমাদের একটি নাম যুক্ত করার প্রয়োজন হয় তখন আমরা রাইট লক ব্যবহার করি। এটি সহজেই synchronized
তালিকাকে ছাড়িয়ে যেতে পারে:
private final ReadWriteLock LOCK = new ReentrantReadWriteLock(); private Collection<String> listOfNames = new ArrayList<>(); public void addName(String name) { LOCK.writeLock().lock(); try { listOfNames.add(name); } finally { LOCK.writeLock().unlock(); } } public boolean isInList(String name) { LOCK.readLock().lock(); try { return listOfNames.contains(name); } finally { LOCK.readLock().unlock(); } }
StampedLock
সম্পর্কে আমাদের প্রথম যে জিনিসটি বুঝতে হবে তা হল এটি পুনঃপ্রবেশকারী নয়। বলুন আমাদের এই ব্লক আছে:
synchronized void methodA() { // … methodB(); // … } synchronized void methodB() { // … }
এই কাজ হবে. যেহেতু সিঙ্ক্রোনাইজ হচ্ছে রিএন্ট্রান্ট। আমরা ইতিমধ্যেই লকটি ধরে রেখেছি, তাই methodB()
থেকে methodA()
এ যাওয়া ব্লক হবে না। এটি ReentrantLock-এর সাথেও কাজ করে, আমরা একই লক বা একই সিঙ্ক্রোনাইজড অবজেক্ট ব্যবহার করি।
StampedLock
একটি স্ট্যাম্প ফেরত দেয় যা আমরা লকটি প্রকাশ করতে ব্যবহার করি। যে কারণে, এর কিছু সীমাবদ্ধতা রয়েছে। কিন্তু এটা এখনও খুব দ্রুত এবং শক্তিশালী. এটিতে একটি রিড-এন্ড-রাইট স্ট্যাম্পও রয়েছে যা আমরা একটি ভাগ করা সম্পদ রক্ষা করতে ব্যবহার করতে পারি।
কিন্তু ReadWriteReentrantLock,
এটি আমাদের লকটিকে আপগ্রেড করতে দেয়। কেন আমরা যে করতে হবে?
আগে থেকে addName()
পদ্ধতিটি দেখুন… যদি আমি এটিকে "Shai" দিয়ে দুবার ডাকি?
হ্যাঁ, আমি একটি সেট ব্যবহার করতে পারি... কিন্তু এই অনুশীলনের জন্য, আসুন বলি যে আমাদের একটি তালিকা দরকার... আমি সেই যুক্তিটি ReadWriteReentrantLock
দিয়ে লিখতে পারি:
public void addName(String name) { LOCK.writeLock().lock(); try { if(!listOfNames.contains(name)) { listOfNames.add(name); } } finally { LOCK.writeLock().unlock(); } }
এই sucks. কিছু ক্ষেত্রে (অনেকগুলো ডুপ্লিকেট contains()
আছে কিনা চেক করার জন্য আমি একটি রাইট লকের জন্য "অর্থ দিয়েছি"। রাইট লক পাওয়ার আগে আমরা isInList(name)
কল করতে পারি। তারপর আমরা করব:
দখলের উভয় ক্ষেত্রেই, আমরা সারিবদ্ধ হতে পারি এবং এটি অতিরিক্ত ঝামেলার মূল্য নাও হতে পারে।
StampedLock
এর সাহায্যে, আমরা রিড লকটিকে একটি রাইট লক-এ আপডেট করতে পারি এবং প্রয়োজনে ঘটনাস্থলেই পরিবর্তন করতে পারি:
public void addName(String name) { long stamp = LOCK.readLock(); try { if(!listOfNames.contains(name)) { long writeLock = LOCK.tryConvertToWriteLock(stamp); if(writeLock == 0) { throw new IllegalStateException(); } listOfNames.add(name); } } finally { LOCK.unlock(stamp); } }
এই ক্ষেত্রে এটি একটি শক্তিশালী অপ্টিমাইজেশান।
আমি উপরের ভিডিও সিরিজে অনেক অনুরূপ বিষয় কভার; এটি পরীক্ষা করে দেখুন, এবং আপনি কি মনে করেন তা আমাকে জানান।
আমি প্রায়শই তাদের দ্বিতীয় চিন্তা না করেই সিঙ্ক্রোনাইজড সংগ্রহের জন্য পৌঁছাই। এটি কখনও কখনও যুক্তিসঙ্গত হতে পারে, তবে বেশিরভাগের জন্য, এটি সম্ভবত সাবঅপ্টিমাল। থ্রেড-সম্পর্কিত আদিম জিনিসগুলির সাথে কিছুটা সময় ব্যয় করে, আমরা আমাদের কর্মক্ষমতা উল্লেখযোগ্যভাবে উন্নত করতে পারি।
তাঁতের সাথে ডিল করার সময় এটি বিশেষভাবে সত্য যেখানে অন্তর্নিহিত বিরোধ অনেক বেশি সংবেদনশীল। 1M সমবর্তী থ্রেডে স্কেলিং রিড অপারেশন কল্পনা করুন... এই ক্ষেত্রে, লক কনটেন্টেশন কমানোর গুরুত্ব অনেক বেশি।
আপনি ভাবতে পারেন, কেন synchronized
সংগ্রহগুলি ReadWriteReentrantLock
বা এমনকি StampedLock
ব্যবহার করতে পারে না?
এটি সমস্যাযুক্ত কারণ API এর পৃষ্ঠের ক্ষেত্রফল এত বড় যে এটি একটি জেনেরিক ব্যবহারের ক্ষেত্রে অপ্টিমাইজ করা কঠিন। এখানেই নিম্ন-স্তরের আদিমগুলির উপর নিয়ন্ত্রণ উচ্চ থ্রুপুট এবং ব্লকিং কোডের মধ্যে পার্থক্য করতে পারে।