diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..30cf57e --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..812c3f9 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..712ab9d --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..d458c91 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Answers.md b/Answers.md new file mode 100644 index 0000000..07cf28e --- /dev/null +++ b/Answers.md @@ -0,0 +1,41 @@ +1 - What are atomic variables? +متغیرهای اتمیک متغیرهایی هستن که عملیات روشون به صورت یکپارچه و تو یه مرحله انجام میشه. یعنی نمیشه وسط کار یه ترد روی این متغیر، یه ترد دیگه بیاد دخالت کنه و کار رو نصفه بذاره. + +فرق اصلیشون با متغیرهای معمولی اینه که مثلا تو یه متغیر عادی یه جمع ساده مثل i++ خودش شامل سه مرحله است: خوندن مقدار فعلی، جمع کردنش با یک، و نوشتن مقدار جدید. اگه دو تا ترد همزمان این کار رو بکنن با هم تداخل پیدا میکنن و دیتامون خراب میشه. اما متغیرهای اتمیک با کمک دستورات سطح سخت‌افزار این کار رو بدون نیاز به قفل کردن و کاملا امن انجام میدن. + +2 - Classes from java.util.concurrent.atomic +چهار تا از کلاس‌های معروف این پکیج این موارد هستن: + +AtomicInteger + +AtomicLong + +AtomicBoolean + +AtomicReference + +مثلا در مورد AtomicInteger، بیشترین کاربردش برای ساختن شمارنده‌های امن هست. فرض کنید میخوایم تعداد بازدیدهای یه سایت رو بشمریم، چون ممکنه همزمان چند تا ترد بخوان این عدد رو ببرن بالا، با استفاده از این کلاس خیالمون راحته که با هم تداخل نمیکنن و هیچ عددی جا نمیفته. + +3 - Compare locks with atomic variables +اتمیک‌ها کلا قفلی ندارن و با الگوریتم‌های سخت‌افزاری کار میکنن که باعث میشه تردها الکی متوقف نشن و خیلی سریع کارشون رو بکنن. اما قفل‌ها بدبینانه‌تر عمل میکنن، یعنی وقتی یه ترد قفلی رو میگیره، بقیه تردها باید تو صف منتظر بمونن تا کارش تموم بشه. + +اگه فقط با یه متغیر کار داریم قطعا استفاده از اتمیک بهتره چون سربار بلاک شدن تردها رو نداره و خیلی سریع‌تره. اما وقتی عملیات ما پیچیده‌تره و روی چند تا متغیر مختلف تاثیر میذاره (مثل همین پروژه که پول از یه حساب کم میشه و به یه حساب دیگه اضافه میشه)، اینجا دیگه نمیشه فقط به اتمیک اعتماد کرد و حتما باید از قفل استفاده کنیم تا کل عملیات با هم انجام بشه. + +4 - High contention and poor performance without race conditions +این اتفاق وقتی میفته که ما از نظر ایمنی کدمون رو کامل ضد ضربه کردیم ولی انقدر همه جا رو قفل کردیم که تردها به جای اینکه موازی کار کنن، تو صف میمونن تا نوبتشون بشه. چند تا دلیل داره که باعث میشه برنامه کند بشه: + +اولیش قفل کردن اضافیه. مثلا به جای اینکه فقط همون متغیر یا حساب خاص رو قفل کنیم، کل متد یا یه بخش بزرگی از برنامه رو قفل میکنیم. +دلیل دوم رقابت شدید سر قفله. وقتی همه تردها بخوان مدام به یه منبع مشترک دسترسی پیدا کنن، سیستم زمان زیادی رو هدر میده تا فقط این صف و انتظارهای تردها رو مدیریت کنه. +دلیل سوم هم گرسنگی تردهاست. یعنی یه سری ترد ممکنه هی عقب بیفتن و نتونن قفل رو بگیرن چون تردهای دیگه سریع‌تر قفل رو تصاحب میکنن، این باعث میشه کلا سرعت پردازش بیاد پایین. + +5 - Why adding more threads does not always improve performance +اضافه کردن ترد همیشه باعث بهبود سرعت نمیشه چون سخت‌افزار ما ظرفیت محدودی داره. وقتی تعداد تردها خیلی زیاد بشه، سیستم عامل مجبوره مدام بینشون سوییچ کنه که خودش به شدت زمان‌بره. + +از طرفی رقابت بالا میره و تردهای بیشتری تو حالت بلاک شده قرار میگیرن. یه مشکل دیگه هم کش پردازنده‌هاست؛ وقتی هسته‌های مختلف میخوان یه متغیر مشترک رو تغییر بدن، دائم باید کش همدیگه رو باطل کنن که این کار پهنای باند مموری رو اشغال میکنه. علاوه بر این، خود مکانیزم قفل کردن و بیدار کردن تردها هم پردازنده رو درگیر میکنه و باعث افت بازدهی میشه. + +6 - Why deadlocks appear in production and how to expose them +بن‌بست‌ها خیلی به زمان‌بندی دقیق تردها ربط دارن. ما وقتی داریم تست میکنیم، بار روی سیستم کمه و کارها راحت انجام میشن. ولی تو محیط واقعی به خاطر ترافیک و درخواست‌های زیاد، سیستم عامل مجبوره تردها رو با ترتیب‌های غیرقابل پیش‌بینی متوقف و اجرا کنه. تو این شلوغی احتمال اینکه دو تا ترد دقیقا تو یه لحظه متقاطع منابع هم رو قفل کنن و گیر بیفتن خیلی زیاده. + +برای اینکه تو زمان تست بتونیم این باگ‌ها رو پیدا کنیم میشه دو تا کار کرد: +اول اینکه بیایم استرس تست با همروندی بالا بنویسیم. یعنی مثلا صدها ترد رو همزمان بندازیم به جون دو سه تا حساب بانکی تا تراکنش‌های ضربدری انجام بدن و شلوغی سیستم واقعی رو شبیه‌سازی کنیم. +راه دوم اینه که عمدا تو بخش‌های حساس کد یه وقفه چند میلی‌ثانیه‌ای بندازیم تا سیستم عامل مجبور بشه ترد رو متوقف کنه. اینطوری اگه مشکلی تو ترتیب قفل‌گذاری باشه زودتر خودش رو نشون میده. diff --git a/src/main/java/dev/banking/model/BankAccount.java b/src/main/java/dev/banking/model/BankAccount.java index 745ede2..0500206 100644 --- a/src/main/java/dev/banking/model/BankAccount.java +++ b/src/main/java/dev/banking/model/BankAccount.java @@ -1,18 +1,13 @@ package dev.banking.model; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + public class BankAccount { private final int accountId; private long balance; - - /* - * Students may introduce additional fields - * such as: - * - Lock / ReentrantLock - * - ReadWriteLock - * - Object monitor - * - etc. - */ + private final Lock lock = new ReentrantLock(); public BankAccount(int accountId, long initialBalance) { this.accountId = accountId; @@ -23,56 +18,48 @@ public class BankAccount { return accountId; } - /* - * TODO: - * Return the current balance in a thread-safe way. - * - * Requirements: - * - Must be safe under concurrent reads/writes - * - Should not block unnecessarily if using read/write locks - */ public long getBalance() { - throw new UnsupportedOperationException("TODO: implement thread-safe balance read"); + lock.lock(); + try { + return balance; + } finally { + lock.unlock(); + } } - /* - * TODO: - * Increase balance atomically. - * - * Requirements: - * - Must not lose updates under concurrency - */ public void deposit(long amount) { - throw new UnsupportedOperationException("TODO: implement thread-safe deposit"); + lock.lock(); + try { + balance += amount; + } finally { + lock.unlock(); + } } - /* - * TODO: - * Decrease balance atomically. - * - * Requirements: - * - Must not cause race conditions - * - Negative balance handling is NOT required unless you decide - * to extend the system (optional) - */ public void withdraw(long amount) { - throw new UnsupportedOperationException("TODO: implement thread-safe withdraw"); + lock.lock(); + try { + balance -= amount; + } finally { + lock.unlock(); + } } - /* - * TODO: - * Transfer money between two accounts atomically. - * - * IMPORTANT REQUIREMENTS: - * - Must be atomic (no partial transfer) - * - Must be deadlock-free - * - Must protect both source and target accounts - * - * HINT: - * - Consider global lock ordering using accountId - * - Or tryLock with retry strategy - */ public void transfer(BankAccount target, long amount) { - throw new UnsupportedOperationException("TODO: implement atomic deadlock-free transfer"); + BankAccount firstLock = this.accountId < target.getAccountId() ? this : target; + BankAccount secondLock = this.accountId < target.getAccountId() ? target : this; + + firstLock.lock.lock(); + try { + secondLock.lock.lock(); + try { + this.balance -= amount; + target.balance += amount; + } finally { + secondLock.lock.unlock(); + } + } finally { + firstLock.lock.unlock(); + } } } \ No newline at end of file