تتبع هذه المقالة أساسًا مقالتين سابقتين من الخيوط المتعددة لتلخيص مشكلات سلامة الخيوط في Java Multi-Threading.
1. مثال على سلامة موضوع Java النموذجي
الفئة العامة threadtestest {public static void main (string [] args) {حساب حساب = حساب جديد ("123456" ، 1000) ؛ drawMoneyRunnable DrawMoneyRunnable = جديد drawMoneyRunnable (حساب ، 700) ؛ الموضوع MyThread1 = موضوع جديد (DrawMoneyRunnable) ؛ الموضوع MyThread2 = موضوع جديد (DrawMoneyRunnable) ؛ mythread1.start () ؛ mythread2.start () ؛ }} class drawMoneyrunnable الأدوات Runnable {account private account ؛ DrawAmount مزدوج خاص ؛ Public DrawMoneyRunnable (حساب الحساب ، Double DrawAmount) {super () ؛ this.Account = حساب ؛ this.drawAmount = drawAmount ؛ } public void run () {if (account.getbalance ()> = drawAmount) {// 1 system.out.println ("كان الانسحاب ناجحًا ، وسحب المال هو:" + drawamount) ؛ رصيد مزدوج = حساب. حساب. التوازن (الرصيد) ؛ system.out.println ("التوازن هو:" + التوازن) ؛ }}} حساب الفئة {private string accountNo ؛ التوازن المزدوج الخاص ؛ الحساب العام () {} الحساب العام (string accountno ، رصيد مزدوج) {this.accountno = accountNo ؛ هذا. التوازن ؛ } السلسلة العامة getAccountno () {return accountNo ؛ } public void setAccountNo (string accountno) {this.accountno = accountNo ؛ } public double getBalance () {return Balance ؛ } public void setbalance (رصيد مزدوج) {this.balance = التوازن ؛ }}المثال أعلاه سهل الفهم. هناك بطاقة مصرفية برصيد قدره 1000. يحاكي البرنامج المشهد الذي تسحب فيه أنت وزوجتك المال في أجهزة الصراف الآلي في نفس الوقت. قم بتشغيل هذا البرنامج عدة مرات وقد يكون له نتائج إخراج في مجموعات مختلفة متعددة. أحد المخرجات المحتملة هو:
1 إن سحب المال ناجح ، وسحب المال هو: 700.0
2 التوازن هو: 300.0
3 سحب المال ناجح ، وسحب المال هو: 700.0
4 الرصيد هو: -400.0
بمعنى آخر ، بالنسبة لبطاقة مصرفية مع رصيد قدره 1000 فقط ، يمكنك سحب ما مجموعه 1400 ، وهو أمر من الواضح أنه يمثل مشكلة.
بعد التحليل ، تكمن المشكلة في عدم اليقين في التنفيذ في بيئة Java متعددة الخيوط. قد يتم تبديل وحدة المعالجة المركزية بشكل عشوائي بين مؤشرات ترابط متعددة في الحالة الجاهزة ، لذلك من المحتمل جدًا أن يحدث الموقف التالي: عندما ينفذ Thread1 الرمز على // 1 ، يكون شرط الحكم صحيحًا. في هذا الوقت ، يتم تبديل وحدة المعالجة المركزية إلى Thread2 ، وتنفيذ الكود على // 1 ، ويجد أنه لا يزال صحيحًا. ثم ، يتم تنفيذ Thread2 ، ثم قم بالتبديل إلى Thread1 ، ثم يتم الانتهاء من التنفيذ. في هذا الوقت ، ستظهر النتائج أعلاه.
لذلك ، عندما يتعلق الأمر بمشكلات سلامة الخيوط ، فهذا يعني في الواقع أن الوصول إلى الموارد المشتركة في بيئة متعددة الخيوط قد يتسبب في عدم تناسق في هذا المورد المشترك. لذلك ، لتجنب مشكلات سلامة الخيوط ، ينبغي تجنب الوصول المتزامن إلى هذا المورد المشترك في بيئة متعددة الخيوط.
2. طريقة التزامن
تتم إضافة تعديل الكلمات الرئيسية المتزامنة إلى تعريف الطريقة للوصول إلى الموارد المشتركة ، مما يجعل هذه الطريقة تسمى طريقة التزامن. يمكن أن يكون مفهوما ببساطة أن هذه الطريقة مغلقة ، وكائنها المقفل هو الكائن نفسه حيث توجد الطريقة الحالية. في بيئة متعددة الخيوط ، عند تنفيذ هذه الطريقة ، يجب عليك أولاً الحصول على قفل المزامنة (وفي معظم الأحيان يمكن الحصول على مؤشر ترابط واحد فقط). فقط عندما ينفذ مؤشر الترابط طريقة التزامن هذه ، سيتم إصدار كائن القفل ، ويمكن أن تحصل مؤشرات الترابط الأخرى على قفل المزامنة ، وهكذا ...
في المثال أعلاه ، يكون المورد المشترك هو كائن حساب ، وعند استخدام طريقة التزامن ، يمكنه حل مشكلات سلامة مؤشرات الترابط. فقط أضف الكلمة الرئيسية المتزامنة قبل طريقة Run ().
تشغيل void متزامن عام () {// ....}3. مزامنة كتل الكود
كما هو موضح أعلاه ، يتطلب حل مشاكل سلامة الخيوط فقط الحد من عدم اليقين في الوصول إلى الموارد المشتركة. عند استخدام طريقة التزامن ، يصبح جسم الطريقة بأكمله حالة تنفيذ متزامنة ، مما قد يتسبب في حدوث نطاق التزامن. لذلك ، يمكن حل طريقة التزامن أخرى - كتلة رمز التزامن - مباشرة للرمز الذي يحتاج إلى التزامن.
تنسيق كتلة الكود المتزامن هو:
متزامن (OBJ) {// ...}من بينها ، OBJ هو كائن القفل ، لذلك من الأهمية بمكان اختيار الكائن الذي سيتم قفله. بشكل عام ، يتم تحديد كائن المورد المشترك هذا ككائن القفل.
كما في المثال أعلاه ، من الأفضل استخدام كائن الحساب ككائن قفل. (بالطبع ، من الممكن أيضًا اختيار ذلك ، لأن مؤشر ترابط الإنشاء يستخدم طريقة Runnable. إذا كان مؤشر ترابط تم إنشاؤه مباشرةً على ورث طريقة مؤشر الترابط ، فإن استخدام هذا الكائن كقفل المزامنة لن يلعب في الواقع أي دور لأنه كائن مختلف. لذلك ، يجب أن تكون حذرًا جدًا عند اختيار قفل المزامنة ...)
4.lock كائن التزامن قفل
كما نرى أعلاه ، على وجه التحديد لأننا بحاجة إلى أن نكون حذرين للغاية بشأن اختيار كائنات القفل المتزامنة ، هل هناك أي حل بسيط؟ يمكن أن يسهل فصل كائنات القفل المتزامنة من الموارد المشتركة ، مع حل مشاكل سلامة الخيوط بشكل جيد.
يمكن أن يؤدي استخدام أقفال مزامنة كائن القفل إلى حل هذه المشكلة بسهولة. الشيء الوحيد الذي يجب ملاحظته هو أن كائن القفل يحتاج إلى وجود علاقة فردية مع كائن المورد. التنسيق العام لقفل التزامن كائن القفل هو:
Class X {// عرض الكائن الذي يحدد قفل التزامن القفل ، والذي له علاقة فردية مع قفل المورد النهائي المشترك = جديد reentrantlock () ؛ public void m () {// lock.lock () ؛ // ... رمز يتطلب مزامنة مؤشر الترابط // إطلاق قفل القفل. unlock () ؛ }}5.wait ()/إخطار ()/إعلام الاتصالات ()
تم ذكر هذه الطرق الثلاثة في منشور المدونة "سلسلة ملخص Java: java.lang.object". على الرغم من أن هذه الطرق الثلاثة تستخدم بشكل أساسي في MultiTreading ، إلا أنها في الواقع طرق محلية في فئة الكائن. لذلك ، من الناحية النظرية ، يمكن استخدام أي كائن كائن كنغمة رئيسية لهذه الطرق الثلاث. في البرمجة الفعلية متعددة الخيوط ، فقط عن طريق مزامنة كائن القفل لضبط هذه الطرق الثلاثة ، يمكن إكمال اتصال الخيط بين مؤشرات الترابط المتعددة.
انتظر (): يتسبب في انتظار الخيط الحالي وجعله يدخل حالة حظر الانتظار. حتى يقوم مؤشر ترابط آخر باستدعاء طريقة الإخطار () أو الإخطار () لكائن القفل المتزامن لإيقاظ الخيط.
إخطار (): استيقظ مؤشر ترابط واحد في انتظار هذا كائن القفل المتزامن. إذا كانت مؤشرات ترابط متعددة تنتظر على كائن القفل المتزامن هذا ، فسيتم اختيار أحد مؤشرات الترابط لتشغيل الاستيقاظ. فقط عندما يتخلى الخيط الحالي عن القفل على كائن القفل المتزامن ، يمكن تنفيذ الخيط المستيقظ.
إخطار (): استيقظ جميع المواضيع تنتظر كائن القفل المتزامن هذا. فقط عندما يتخلى الخيط الحالي عن القفل على كائن القفل المتزامن ، يمكن تنفيذ الخيط المستيقظ.
Package com.qyumidi ؛ threadtest {public static void main (string [] args) {حساب حساب = حساب جديد ("123456" ، 0) ؛ Thread DrawMoneyThread = New DrawMoneyThread ("Get Money Thread" ، حساب ، 700) ؛ Thread DeposterMoneyThread = New DeposterMoneyThread ("Save Money Thread" ، حساب ، 700) ؛ DrawMoneyThread.start () ؛ DepositMoneyThread.start () ؛ }} class drawMoneyThread يمتد Thread {حساب خاص ؛ مبلغ مزدوج خاص ؛ Public DrawMoneyThread (string threadname ، حساب الحساب ، المبلغ المزدوج) {super (threadname) ؛ this.Account = حساب ؛ this.amount = المبلغ ؛ } public void run () {for (int i = 0 ؛ i <100 ؛ i ++) {account.draw (cause ، i) ؛ }}} Class DepositEmoneyThread يمتد Thread {حساب خاص ؛ مبلغ مزدوج خاص ؛ Public DeposterMoneyThread (String Threadname ، حساب الحساب ، المبلغ المزدوج) {super (threadname) ؛ this.Account = حساب ؛ this.amount = المبلغ ؛ } public void run () {for (int i = 0 ؛ i <100 ؛ i ++) {account.deposit (leat ، i) ؛ }}} حساب الفئة {private string accountNo ؛ التوازن المزدوج الخاص ؛ // حدد ما إذا كان هناك بالفعل إيداع في حساب العلم المنطقي الخاص = خطأ ؛ الحساب العام () {} الحساب العام (string accountno ، رصيد مزدوج) {this.accountno = accountNo ؛ هذا. التوازن ؛ } السلسلة العامة getAccountno () {return accountNo ؛ } public void setAccountNo (string accountno) {this.accountno = accountNo ؛ } public double getBalance () {return Balance ؛ } public void setbalance (رصيد مزدوج) {this.balance = التوازن ؛ } / ** * توفير المال * * param depositamount * / إيداع باطل متزامن عام (إيداع مزدوج ، int i) {if (flag) {// شخص ما في الحساب قد وفر بالفعل الأموال ، ويحتاج الموضوع الحالي إلى الانتظار إلى حظر try انتظر()؛ // 1 system.out.println (thread.currentThRead (). getName () + "تم تنفيذ عملية الانتظار" + " - i =" + i) ؛ } catch (interruptedException e) {E.PrintStackTrace () ؛ }} else {// ابدأ حفظ النظام. setbalance (الرصيد + deportAmount) ؛ العلم = صحيح ؛ // استيقظ مؤشرات ترابط أخرى على الإخطار () ؛ // 2 حاول {thread.sleep (3000) ؛ } catch (interruptedException e) {E.PrintStackTrace () ؛ } system.out.println (thread.currentThRead (). getName () + "- توفير المال- يتم الانتهاء من التنفيذ" + "- i =" + i) ؛ }} / ** * سحب الأموال * * param drawAmount * / public synchronised void draw (double drawamount ، int i) {if (! flag) {// لا يوجد أحد في الحساب قد حفظ المال حتى الآن ، ويحتاج الموضوع الحالي إلى الانتظار حتى يتم التنفيذ ". أنا)؛ انتظر()؛ system.out.println (thread.currentThRead (). getName () + "تنفيذ عملية الانتظار" + "تنفيذ عملية الانتظار" + " - i =" + i) ؛ } catch (interruptedException e) {E.PrintStackTrace () ؛ }} else {// ابدأ في سحب الأموال system.out.println (thread.currentThRead (). getName () + "سحب الأموال:" + drawamount + " - i =" + i) ؛ setbalance (getBalance () - DrawAmount) ؛ العلم = خطأ ؛ // استيقظ مؤشرات ترابط أخرى على الإخطار () ؛ System.out.println (thread.currentThRead (). getName () + "-سحب الأموال-يتم الانتهاء من" + "-i =" + i) ؛ // 3}}} يوضح المثال أعلاه استخدام WAIT ()/elefy ()/eletifyall (). بعض نتائج الإخراج هي:
يبدأ موضوع سحب الأموال في تنفيذ عملية الانتظار وتنفيذ عملية الانتظار- i = 0
إيداع مؤشر الترابط: 700.0 - i = 0
توفير الأموال ذات الأموال ذات الأموال المميزة-I = 0
يحتاج موضوع توفير الأموال إلى إجراء عملية الانتظار- أنا = 1
ينفذ موضوع سحب الأموال عملية الانتظار وانتظار التشغيل- أنا = 0
سحب موضوع الأموال سحب الأموال: 700.0 - i = 1
موضوع انسحاب الأموال-مع الإسقاط-التنفيذ-i = 1
يجب أن يبدأ الموضوع لسحب الأموال في تنفيذ عملية الانتظار وتنفيذ عملية الانتظار- i = 2
يقوم مؤشر ترابط توفير الأموال بتنفيذ عملية الانتظار- أنا = 1
إيداع مؤشر الترابط: 700.0 - i = 2
توفير الأموال ذات الأموال ذات الأموال المميزة-I = 2
ينفذ مؤشر ترابط السحب عملية الانتظار وتنفيذ عملية الانتظار- i = 2
سحب موضوع المال سحب المال: 700.0 - i = 3
موضوع انسحاب الأموال-مع الإسقاط-التنفيذ-أنا = 3
يجب أن يقوم مؤشر الترابط بسحب الأموال بتنفيذ عملية الانتظار وتنفيذ عملية الانتظار- أنا = 4
إيداع مؤشر الترابط: 700.0 - i = 3
توفير الأموال ذات الأموال ذات الأموال المميزة-أنا = 3
يحتاج موضوع توفير الأموال إلى إجراء عملية الانتظار- أنا = 4
ينفذ موضوع سحب الأموال عملية الانتظار وانتظار التشغيل- أنا = 4
سحب موضوع المال سحب المال: 700.0 - أنا = 5
موضوع انسحاب الأموال-مع الإسقاط-الإعداد-أنا = 5
يجب أن يبدأ الموضوع لسحب الأموال في إجراء عملية الانتظار وتنفيذ عملية الانتظار- أنا = 6
يقوم مؤشر ترابط توفير الأموال بتنفيذ عملية الانتظار- أنا = 4
إيداع مؤشر الترابط: 700.0 - i = 5
توفير الأموال ذات الأموال ذات الأموال-I = 5
يحتاج موضوع توفير الأموال إلى إجراء عملية الانتظار- أنا = 6
ينفذ موضوع سحب الأموال عملية الانتظار وانتظار التشغيل- أنا = 6
سحب موضوع المال سحب المال: 700.0 - أنا = 7
موضوع انسحاب الأموال-مع الإسقاط-التنفيذ-أنا = 7
يبدأ موضوع سحب الأموال في تنفيذ عملية الانتظار وتنفيذ عملية الانتظار- i = 8
يقوم مؤشر ترابط توفير الأموال بتنفيذ عملية الانتظار- أنا = 6
إيداع مؤشر الترابط: 700.0 - i = 7
لذلك ، نحتاج إلى الانتباه إلى النقاط التالية:
1. بعد تنفيذ طريقة الانتظار () ، يدخل مؤشر الترابط الحالي على الفور حالة حظر الانتظار ، ولن يتم تنفيذ الكود اللاحق ؛
2. بعد تنفيذ طريقة الإخطار ()/etrifyall () ، سيتم إيقاظ كائن مؤشر الترابط (أي notify ()/all-notifyall ()) على كائن قفل التزامن هذا. ومع ذلك ، لم يتم إطلاق كائن قفل التزامن في هذا الوقت. وهذا يعني ، إذا كان هناك رمز وراء الإخطار ()/إخطار () ، فسيستمر المتابعة. فقط عند تنفيذ الخيط الحالي ، سيتم إصدار كائن قفل التزامن ؛
3. بعد تنفيذ الإخطار ()/elefyall () ، إذا كانت هناك طريقة للنوم () على اليمين ، فإن الخيط الحالي سوف يدخل حالة حظر ، ولكن لم يتم إطلاق قفل كائن التزامن ولا يزال يحتفظ به بمفرده. بعد ذلك ، سيستمر تنفيذ الخيط بعد فترة زمنية معينة ، في الثانية التالية ؛
4. انتظر ()/إخطار ()/nitifyall () يكمل الاتصالات أو التعاون بين مؤشرات الترابط بناءً على أقفال كائن مختلفة. لذلك ، إذا كان قفل كائن التزامن مختلف ، فسوف يفقد معناه. في الوقت نفسه ، يكون قفل كائن التزامن هو الأفضل للحفاظ على مراسلات فردية مع كائن المورد المشترك ؛
5. عندما يستيقظ مؤشر ترابط الانتظار وتنفذ ، يستمر تنفيذ رمز طريقة WAIT () الذي تم تنفيذه في المرة الأخيرة.
بالطبع ، فإن المثال أعلاه بسيط نسبيًا ، فقط لاستخدام طريقة Wait ()/eletify ()/noitifyall () ، ولكن في جوهرها ، إنه بالفعل نموذج بسيط للمنتج والمستهلك.
سلسلة من المقالات:
شرح مثيلات جافا متعددة الخيوط (ط)
شرح مفصل للحالات متعددة الخيوط Java (II)
شرح مفصل للحالات متعددة الخيوط Java (III)