مروری کوتاه بر جزئیات نحوه تعامل موضوعات. قبلاً به نحوه همگام سازی نخ ها با یکدیگر نگاه کردیم. این بار مشکلاتی را که ممکن است در اثر تعامل رشته ها به وجود بیاید بررسی خواهیم کرد و در مورد نحوه اجتناب از آنها صحبت خواهیم کرد. همچنین پیوندهای مفیدی را برای مطالعه عمیق تر ارائه خواهیم داد.
با نصب یک پلاگین JVisualVM (از طریق Tools -> Plugins)، می توانیم ببینیم که بن بست کجا رخ داده است:
صحبت کردیم، وارد حالت پارک میشود) . شما می توانید یک مثال از livelock را در اینجا ببینید: Java - Thread Livelock
.
می توانید یک مثال فوق العاده را در اینجا ببینید: جاوا - Thread Starvation and Fairness
. این مثال نشان می دهد که در هنگام گرسنگی چه اتفاقی برای نخ ها می افتد و چگونه یک تغییر کوچک از

معرفی
بنابراین، ما می دانیم که جاوا دارای موضوعات است. شما می توانید در مورد آن در بررسی با عنوان Better together: Java and the Thread کلاس بخوانید. بخش اول - موضوعات اجرا . و ما این واقعیت را بررسی کردیم که رشته ها می توانند با یکدیگر همگام شوند در بررسی با عنوان Better together: Java and the Thread کلاس. بخش دوم - همگام سازی وقت آن است که در مورد نحوه تعامل رشته ها با یکدیگر صحبت کنیم. چگونه آنها منابع مشترک را به اشتراک می گذارند؟ چه مشکلاتی ممکن است در اینجا ایجاد شود؟
بن بست
ترسناک ترین مشکل، بن بست است. بن بست زمانی است که دو یا چند رشته برای همیشه در انتظار دیگری هستند. مثالی از صفحه وب اوراکل می گیریم که بن بست را توضیح می دهد :public class Deadlock {
static class Friend {
private final String name;
public Friend(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public synchronized void bow(Friend bower) {
System.out.format("%s: %s bowed to me!%n",
this.name, bower.getName());
bower.bowBack(this);
}
public synchronized void bowBack(Friend bower) {
System.out.format("%s: %s bowed back to me!%n",
this.name, bower.getName());
}
}
public static void main(String[] args) {
final Friend alphonse = new Friend("Alphonse");
final Friend gaston = new Friend("Gaston");
new Thread(() -> alphonse.bow(gaston)).start();
new Thread(() -> gaston.bow(alphonse)).start();
}
}
بن بست ممکن است بار اول در اینجا رخ ندهد، اما اگر برنامه شما هنگ کرد، زمان اجرای آن فرا رسیده است jvisualvm
: 
"Thread-1" - Thread t@12
java.lang.Thread.State: BLOCKED
at Deadlock$Friend.bowBack(Deadlock.java:16)
- waiting to lock <33a78231> (a Deadlock$Friend) owned by "Thread-0" t@11
موضوع 1 در انتظار قفل از موضوع 0 است. چرا این اتفاق می افتد؟ Thread-1
شروع به اجرا می کند و Friend#bow
متد را اجرا می کند. با کلمه کلیدی مشخص شده است synchronized
، به این معنی که ما در حال دریافت مانیتور برای this
(شیء فعلی) هستیم. ورودی متد ارجاع به Friend
شی دیگر بود. اکنون، Thread-1
میخواهد متد را روی دیگری اجرا کند Friend
و برای این کار باید قفل آن را بدست آورد. اما اگر thread دیگر (در این مورد Thread-0
) موفق شد وارد bow()
روش شود، قفل قبلاً بدست آمده است و Thread-1
منتظر است Thread-0
و بالعکس. این بن بست حل نشدنی است و ما آن را بن بست می نامیم. مانند یک چنگال مرگ که نمی توان آن را رها کرد، بن بست نیز مانعی متقابل است که نمی توان آن را شکست. برای توضیح دیگری درباره بن بست، می توانید این ویدیو را مشاهده کنید: Deadlock و Livelock Explained
.
Livelock
اگر بن بست وجود دارد، آیا قفل زنده نیز وجود دارد؟ بله، وجود دارد :) Livelock زمانی اتفاق میافتد که نخها ظاهراً زنده به نظر میرسند، اما نمیتوانند کاری انجام دهند، زیرا شرایط (شرایط) مورد نیاز برای ادامه کارشان نمیتواند برآورده شود. اساسا، livelock شبیه به بن بست است، اما نخ ها در انتظار یک مانیتور "آویزان" نمی شوند. در عوض، آنها برای همیشه در حال انجام کاری هستند. مثلا:import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class App {
public static final String ANSI_BLUE = "\u001B[34m";
public static final String ANSI_PURPLE = "\u001B[35m";
public static void log(String text) {
String name = Thread.currentThread().getName(); // Like "Thread-1" or "Thread-0"
String color = ANSI_BLUE;
int val = Integer.valueOf(name.substring(name.lastIndexOf("-") + 1)) + 1;
if (val != 0) {
color = ANSI_PURPLE;
}
System.out.println(color + name + ": " + text + color);
try {
System.out.println(color + name + ": wait for " + val + " sec" + color);
Thread.currentThread().sleep(val * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Lock first = new ReentrantLock();
Lock second = new ReentrantLock();
Runnable locker = () -> {
boolean firstLocked = false;
boolean secondLocked = false;
try {
while (!firstLocked || !secondLocked) {
firstLocked = first.tryLock(100, TimeUnit.MILLISECONDS);
log("First Locked: " + firstLocked);
secondLocked = second.tryLock(100, TimeUnit.MILLISECONDS);
log("Second Locked: " + secondLocked);
}
first.unlock();
second.unlock();
} catch (InterruptedException e) {
e.printStackTrace();
}
};
new Thread(locker).start();
new Thread(locker).start();
}
}
موفقیت این کد بستگی به ترتیبی دارد که زمانبندی رشته جاوا رشته ها را شروع می کند. اگر Thead-1
ابتدا شروع شود، پس ما زنده می شویم:
Thread-1: First Locked: true
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
Thread-0: Second Locked: true
Thread-0: wait for 1 sec
Thread-1: Second Locked: false
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
...
همانطور که از مثال می بینید، هر دو رشته به نوبه خود سعی می کنند هر دو قفل را بدست آورند، اما شکست می خورند. اما، آنها در بن بست نیستند. از نظر ظاهری همه چیز خوب است و آنها کار خود را انجام می دهند. طبق JVisualVM، ما دورههای خواب و یک دوره پارک را میبینیم (این زمانی است که یک نخ سعی میکند یک قفل به دست آورد - همانطور که قبلاً در مورد همگامسازی نخ

گرسنگی
علاوه بر بن بست و زنده بودن، مشکل دیگری نیز وجود دارد که می تواند در طول چند رشته ای اتفاق بیفتد: گرسنگی. این پدیده با اشکال قبلی مسدود کردن تفاوت دارد زیرا رشته ها مسدود نمی شوند - آنها به سادگی منابع کافی ندارند. در نتیجه، در حالی که برخی از رشته ها تمام زمان اجرا را می گیرند، برخی دیگر قادر به اجرا نیستند:
https://www.logicbig.com/
Thread.sleep()
به Thread.wait()
به شما امکان می دهد بار را به طور مساوی توزیع کنید. 
شرایط مسابقه
در multithreading چیزی به نام "شرط مسابقه" وجود دارد. این پدیده زمانی اتفاق میافتد که رشتهها یک منبع را به اشتراک میگذارند، اما کد به گونهای نوشته میشود که از اشتراکگذاری صحیح اطمینان حاصل نمیکند. به یک مثال نگاه کنید:public class App {
public static int value = 0;
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
int oldValue = value;
int newValue = ++value;
if (oldValue + 1 != newValue) {
throw new IllegalStateException(oldValue + " + 1 = " + newValue);
}
}
};
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();
}
}
این کد ممکن است بار اول خطایی ایجاد نکند. وقتی این کار انجام می شود، ممکن است به این شکل باشد:
Exception in thread "Thread-1" java.lang.IllegalStateException: 7899 + 1 = 7901
at App.lambda$main$0(App.java:13)
at java.lang.Thread.run(Thread.java:745)
newValue
همانطور که می بینید، هنگام تخصیص مقدار، مشکلی پیش آمد . newValue
خیلی بزرگ است به دلیل شرایط مسابقه، یکی از رشته ها موفق شد متغیر value
بین دو عبارت را تغییر دهد. معلوم می شود که یک مسابقه بین رشته ها وجود دارد. حالا به این فکر کنید که چقدر مهم است که اشتباهات مشابهی در تراکنشهای پولی مرتکب نشوید... مثالها و نمودارها را میتوانید در اینجا ببینید: کد شبیهسازی شرایط مسابقه در رشته جاوا
.
فرار
در مورد تعامل نخ ها،volatile
کلمه کلیدی قابل ذکر است. بیایید به یک مثال ساده نگاه کنیم:
public class App {
public static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
Runnable whileFlagFalse = () -> {
while(!flag) {
}
System.out.println("Flag is now TRUE");
};
new Thread(whileFlagFalse).start();
Thread.sleep(1000);
flag = true;
}
}
جالبتر از همه، این به احتمال زیاد کار نمی کند. موضوع جدید تغییر را در flag
فیلد مشاهده نخواهد کرد. برای رفع این مشکل برای flag
فیلد، باید از volatile
کلمه کلیدی استفاده کنیم. چگونه و چرا؟ پردازنده تمام اقدامات را انجام می دهد. اما نتایج محاسبات باید در جایی ذخیره شود. برای این کار، حافظه اصلی و حافظه پنهان پردازنده وجود دارد. حافظه نهان پردازنده مانند یک تکه کوچک از حافظه است که برای دسترسی سریعتر به داده ها نسبت به هنگام دسترسی به حافظه اصلی استفاده می شود. اما همه چیز یک جنبه منفی دارد: داده های موجود در حافظه پنهان ممکن است به روز نباشند (مانند مثال بالا، زمانی که مقدار فیلد پرچم به روز نشد). بنابراین، volatile
کلمه کلیدی به JVM می گوید که ما نمی خواهیم متغیر خود را کش کنیم. این اجازه می دهد تا نتیجه به روز در همه رشته ها دیده شود. این یک توضیح بسیار ساده است. در مورد کلمه کلیدی، به شدت توصیه می کنم این مقاله را
volatile
بخوانید . برای اطلاعات بیشتر، به شما توصیه می کنم مدل حافظه جاوا
و کلیدواژه فرار جاوا
را نیز مطالعه کنید . علاوه بر این، مهم است که به یاد داشته باشید که در مورد دید است، و نه در مورد اتمی بودن تغییرات. با نگاهی به کد موجود در بخش "شرایط مسابقه"، یک راهنمای ابزار را در IntelliJ IDEA مشاهده خواهیم کرد: این بازرسی به عنوان بخشی از شماره IDEA-61117
به IntelliJ IDEA اضافه شد که در سال 2010 در یادداشت های انتشار فهرست شده بود.
volatile

اتمی
عملیات اتمی عملیاتی است که قابل تقسیم نیست. به عنوان مثال، عملیات اختصاص دادن یک مقدار به یک متغیر باید اتمی باشد. متأسفانه، عملیات افزایشی اتمی نیست، زیرا افزایش به سه عملیات CPU نیاز دارد: مقدار قدیمی را دریافت کنید، یکی به آن اضافه کنید، سپس مقدار را ذخیره کنید. چرا اتمی مهم است؟ با عملیات افزایش، اگر شرایط مسابقه وجود داشته باشد، منبع مشترک (یعنی مقدار مشترک) ممکن است در هر زمانی ناگهان تغییر کند. علاوه بر این، عملیاتی که شامل ساختارهای 64 بیتی است، به عنوان مثالlong
و double
، اتمی نیستند. جزئیات بیشتر را می توانید در اینجا بخوانید: هنگام خواندن و نوشتن مقادیر 64 بیتی از اتمی بودن اطمینان حاصل کنید
. مشکلات مربوط به اتمی را می توان در این مثال مشاهده کرد:
public class App {
public static int value = 0;
public static AtomicInteger atomic = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
value++;
atomic.incrementAndGet();
}
};
for (int i = 0; i < 3; i++) {
new Thread(task).start();
}
Thread.sleep(300);
System.out.println(value);
System.out.println(atomic.get());
}
}
کلاس ویژه AtomicInteger
همیشه 30000 به ما می دهد، اما value
هر از گاهی تغییر می کند. مروری کوتاه بر این موضوع وجود دارد: مقدمه ای بر متغیرهای اتمی در جاوا
. الگوریتم "مقایسه و مبادله" در قلب کلاس های اتمی قرار دارد. میتوانید در اینجا در مقایسه الگوریتمهای بدون قفل - CAS و FAA در مثال JDK 7 و 8
یا در مقاله مقایسه و تعویض
در ویکیپدیا، درباره آن بیشتر بخوانید. 
http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html
اتفاق می افتد-قبل
یک مفهوم جالب و مرموز به نام "پیش از این اتفاق می افتد" وجود دارد. به عنوان بخشی از مطالعه خود در مورد موضوعات، باید در مورد آن مطالعه کنید. رابطه اتفاق می افتد-پیش از ترتیبی که اعمال بین رشته ها دیده می شود را نشان می دهد. تفاسیر و تفاسیر فراوان است. در اینجا یکی از جدیدترین ارائه ها در مورد این موضوع است: Java "Happens-Before" Relationships .خلاصه
در این بررسی، برخی از ویژگیهای نحوه تعامل رشتهها را بررسی کردهایم. ما مشکلاتی را که ممکن است پیش بیاید و همچنین راه های شناسایی و حذف آنها را مورد بحث قرار دادیم. فهرست مطالب اضافی در مورد موضوع:- قفل دوبار چک شده
- سوالات متداول JSR 133 (مدل حافظه جاوا).
- IQ 35: چگونه از بن بست جلوگیری کنیم؟
- مفاهیم همزمانی در جاوا توسط داگلاس هاوکینز (2017)
GO TO FULL VERSION