لمحة موجزة عن ميزات تفاعل الخيط. لقد نظرنا سابقًا في كيفية مزامنة الخيوط مع بعضها البعض. هذه المرة سنتعمق في المشكلات التي يمكن أن تنشأ عندما تتفاعل سلاسل الرسائل ونتحدث عن كيفية تجنبها. وسنقدم أيضًا بعض الروابط المفيدة لدراسة أعمق.
يمكن العثور على مثال رائع هنا: " Java - Thread Starvation and Fairness ". يوضح هذا المثال كيفية عمل الخيوط في Starvation وكيف يمكن لتغيير واحد صغير من Thread.sleep إلى Thread.wait توزيع الحمل بالتساوي.
ربما يكون من الأفضل ألا يخبرنا هذا الفيديو بأي شيء عنه. لذلك سأترك رابطًا للفيديو. يمكنك قراءة " جافا - الفهم يحدث قبل العلاقات ".
مقدمة
لذلك، نحن نعلم أن هناك خيوط في Java، يمكنك أن تقرأ عنها في المراجعة " Thread Can't Spoil Java: Part I - Threads " وأنه يمكن مزامنة الخيوط مع بعضها البعض، وهو ما تناولناه في المراجعة " الموضوع لا يمكن أن يفسد Java ” Spoil: الجزء الثاني - المزامنة ." حان الوقت للحديث عن كيفية تفاعل المواضيع مع بعضها البعض. كيف يتقاسمون الموارد المشتركة؟ ما هي المشاكل التي يمكن أن تكون هناك مع هذا؟طريق مسدود
أسوأ مشكلة هي الجمود. عندما ينتظر خيطان أو أكثر لبعضهما البعض إلى الأبد، يُسمى هذا باسم Deadlock. لنأخذ مثالاً من موقع Oracle على الويب من وصف مفهوم " Deadlock ":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 has bowed to me!%n",
this.name, bower.getName());
bower.bowBack(this);
}
public synchronized void bowBack(Friend bower) {
System.out.format("%s: %s has 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
: إذا تم تثبيت مكون إضافي في 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-0
) من الدخول إلى الطريقة bow
، فإن القفل مشغول بالفعل وينتظر Thread-1
، Thread-0
والعكس صحيح. الحجب غير قابل للحل، لذا فهو ميت، أي ميت. قبضة الموت (التي لا يمكن تحريرها) والكتلة الميتة التي لا يمكن الهروب منها. حول موضوع الجمود، يمكنك مشاهدة الفيديو: " Deadlock - Concurrency #1 - Advanced Java ".
لايفلوك
إذا كان هناك طريق مسدود، فهل هناك 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();
}
}
يعتمد نجاح هذا الرمز على الترتيب الذي يبدأ به برنامج جدولة سلاسل المحادثات Java. إذا بدأ أولاً Thead-1
، فسنحصل على Livelock:
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، نرى فترات السكون وفترة الإيقاف (وهذا عندما يحاول خيط ما احتلال قفل، فإنه ينتقل إلى حالة الإيقاف، كما ناقشنا سابقًا عند الحديث عن مزامنة الخيط ). فيما يتعلق بموضوع القفل المباشر، يمكنك رؤية مثال: " Java - Thread Livelock ".
مجاعة
بالإضافة إلى الحظر (طريق مسدود وLivelock)، هناك مشكلة أخرى عند العمل مع تعدد مؤشرات الترابط - الجوع، أو "المجاعة". تختلف هذه الظاهرة عن الحظر في أن المواضيع غير محظورة، لكنها ببساطة لا تحتوي على موارد كافية للجميع. لذلك، في حين أن بعض الخيوط تتولى كل وقت التنفيذ، لا يمكن تنفيذ بعضها الآخر:https://www.logicbig.com/
حالة السباق
عند العمل مع مؤشرات الترابط المتعددة، هناك شيء مثل "حالة السباق". تكمن هذه الظاهرة في حقيقة أن الخيوط تشترك في مورد معين فيما بينها وأن الكود مكتوب بطريقة لا توفر التشغيل الصحيح في هذه الحالة. لنلقي نظرة على مثال: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
بين هذين الفريقين. وكما نرى فقد ظهر سباق بين الخيوط. تخيل الآن مدى أهمية عدم ارتكاب أخطاء مماثلة في المعاملات المالية... يمكن أيضًا العثور على الأمثلة والرسوم البيانية هنا: " رمز لمحاكاة حالة السباق في موضوع Java ".
متقلب
عند الحديث عن تفاعل سلاسل الرسائل، تجدر الإشارة بشكل خاص إلى الكلمة الرئيسية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
يوصى بشدة بقراءة ترجمة " الأسئلة الشائعة حول JSR 133 (نموذج ذاكرة Java) ". أنصحك أيضًا بقراءة المزيد عن المواد " Java Memory Model " و" Java Volatile Keyword ". بالإضافة إلى ذلك، من المهم أن نتذكر أن volatile
الأمر يتعلق بالرؤية، وليس بذرية التغييرات. إذا أخذنا الكود من "حالة السباق"، فسنرى تلميحًا في IntelliJ Idea: تمت إضافة هذا الفحص (الفحص) إلى IntelliJ Idea كجزء من الإصدار IDEA-61117 ، والذي تم إدراجه في ملاحظات الإصدار في عام 2010.
الذرية
العمليات الذرية هي عمليات لا يمكن تقسيمها. على سبيل المثال، عملية إسناد قيمة إلى متغير هي عملية ذرية. لسوء الحظ، الزيادة ليست عملية ذرية، لأن تتطلب الزيادة ما يصل إلى ثلاث عمليات: الحصول على القيمة القديمة، وإضافة واحدة إليها، وحفظ القيمة. لماذا تعتبر الذرية مهمة؟ في مثال الزيادة، في حالة حدوث حالة سباق، في أي وقت قد يتغير المورد المشترك (أي القيمة المشتركة) فجأة. بالإضافة إلى ذلك، من المهم أن تكون هياكل 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());
}
}
Integer
ستظهر لنا دائمًا فئة خاصة للعمل مع الذرة 30000، لكنها value
ستتغير من وقت لآخر. هناك نظرة عامة قصيرة حول هذا الموضوع " مقدمة للمتغيرات الذرية في جافا ". يعتمد Atomic على خوارزمية المقارنة والمبادلة. يمكنك قراءة المزيد عنها في مقالة حبري " مقارنة الخوارزميات الخالية من القفل - CAS وFAA باستخدام مثال JDK 7 و8 " أو على ويكيبيديا في المقالة حول " المقارنة مع التبادل ".
http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-Java.html
يحدث من قبل
هناك شيء مثير للاهتمام وغامض - حدث من قبل. عند الحديث عن التدفقات، من المفيد أيضًا القراءة عنها. تشير العلاقة "يحدث قبل" إلى الترتيب الذي سيتم به رؤية الإجراءات بين سلاسل العمليات. هناك العديد من التفسيرات والتفسيرات. ومن أحدث التقارير حول هذا الموضوع هو هذا التقرير:نتائج
في هذه المراجعة، نظرنا إلى ميزات تفاعل الخيط. ناقشنا المشاكل التي قد تنشأ وطرق اكتشافها والقضاء عليها. قائمة المواد الإضافية حول الموضوع:- مرة أخرى حول القفل المزدوج التحقق
- الأسئلة الشائعة JSR 133 (نموذج ذاكرة Java) (ترجمة)
- جافا المتقدمة - التزامن (يوري تكاتش)
- مفاهيم التزامن في جافا بقلم دوغلاس هوكينز (2017)
GO TO FULL VERSION