1. متى ستحدث مشاكل السلامة في الموضوع؟
لن تكون هناك مشاكل سلامة مؤشرات الترابط في موضوع واحد ، ولكن في البرمجة متعددة الخيوط ، من الممكن الوصول إلى نفس المورد في نفس الوقت. يمكن أن يكون هذا المورد أنواعًا مختلفة من الموارد: متغير ، كائن ، ملف ، جدول قاعدة بيانات ، إلخ. عندما تصل مؤشرات الترابط المتعددة إلى نفس المورد في نفس الوقت ، ستكون هناك مشكلة:
نظرًا لأن العملية التي ينفذها كل مؤشر ترابط لا يمكن السيطرة عليها ، فمن المحتمل أن تكون النتيجة النهائية مخالفة للرغبة الفعلية أو ستؤدي مباشرة إلى أخطاء في البرنامج.
دعونا نعطي مثالًا بسيطًا:
يوجد الآن مؤشر ترابط يقرأان البيانات من الشبكة بشكل منفصل ثم أدخلها في جدول قاعدة البيانات ، مما يتطلب إدراج البيانات المكررة.
ثم يجب أن يكون هناك عمليتان في عملية إدخال البيانات:
1) تحقق مما إذا كانت البيانات موجودة في قاعدة البيانات ؛
2) إذا كانت موجودة ، فلن يتم إدخالها ؛ إذا لم تكن موجودة ، فسيتم إدراجها في قاعدة البيانات.
إذا تم تمثيل مؤشر الترابط بواسطة Thread-1 و Thread-2 على التوالي ، وفي مرحلة ما ، فإن Thread-1 و Thread-2 كلا البيانات ، قد يحدث هذا:
يقوم Thread-1 بفحص ما إذا كانت البيانات X موجودة في قاعدة البيانات ، ويقوم مؤشر الترابط 2 أيضًا بالتحقق مما إذا كانت البيانات X موجودة في قاعدة البيانات.
ونتيجة لذلك ، فإن نتيجة فحص الخيوطين هي أن البيانات X غير موجودة في قاعدة البيانات ، لذلك يقوم كلا الموضوعين بإدخال بيانات X في جدول قاعدة البيانات على التوالي.
هذه مشكلة سلامة مؤشرات الترابط ، أي عندما تصل مؤشرات الترابط المتعددة إلى مورد في نفس الوقت ، فإن نتيجة تشغيل البرنامج ليست النتيجة التي تريد رؤيتها.
هنا ، يسمى هذا المورد: المورد الحرج (المعروف أيضًا باسم المورد المشترك).
وهذا يعني ، عندما تصل سلاسل مؤشرات الترابط المتعددة إلى الموارد الحرجة (كائن واحد ، سمات في كائن ، ملف ، قاعدة بيانات ، إلخ) ، قد تنشأ مشكلات سلامة مؤشرات الترابط.
ومع ذلك ، عندما تنفذ مؤشرات ترابط متعددة طريقة ما ، فإن المتغيرات المحلية داخل الطريقة ليست موارد حرجة ، لأن الطريقة يتم تنفيذها على المكدس ، في حين أن مكدس Java هو مؤشر ترابط خاص ، لذلك لن تكون هناك مشكلات سلامة مؤشر ترابط.
2. كيفية حل مشكلات سلامة الموضوع؟
لذلك بشكل عام ، كيف تحل مشاكل السلامة في الموضوع؟
في الأساس ، عند حل مشاكل سلامة الخيوط ، تتبنى جميع أوضاع التزامن حل "الوصول المسلسل إلى الموارد الحرجة" ، أي في الوقت نفسه ، يمكن لخيط واحد فقط الوصول إلى الموارد الحرجة ، والمعروف أيضًا باسم الوصول الحصري المتزامن.
بشكل عام ، تتم إضافة قفل قبل الكود الذي يصل إلى المورد الحرج. بعد الوصول إلى المورد الحرج ، يتم إصدار القفل ويستمر مؤشرات الترابط الأخرى في الوصول.
في Java ، يتم توفير طريقتين لتنفيذ وصول متزامن Mutex: متزامن وقفل.
تتحدث هذه المقالة بشكل أساسي عن استخدام المزامنة ، ويتم وصف استخدام القفل في منشور المدونة التالي.
ثلاثة. طريقة التزامن أو كتلة التزامن
قبل فهم استخدام الكلمات الرئيسية المتزامنة ، دعونا نلقي نظرة أولاً على مفهوم: أقفال Mutex ، كما يوحي الاسم: الأقفال التي يمكن أن تحقق الغرض من الوصول إلى Mutex.
لإعطاء مثال بسيط: إذا تمت إضافة مورد حرج إلى Mutex ، عندما يصل مؤشر ترابط واحد إلى المورد الحرج ، يمكن للموضوعات الأخرى الانتظار فقط.
في Java ، يحتوي كل كائن على علامة قفل (شاشة) ، والمعروفة أيضًا باسم الشاشة. عندما تصل مؤشرات الترابط المتعددة إلى كائن في نفس الوقت ، لا يمكن للمعلومات الوصول إليه إلا إذا كان يكتسب قفل الكائن.
في Java ، يمكن استخدام الكلمة الرئيسية المتزامنة لتمييز طريقة أو كتلة رمز. عندما يقوم مؤشر ترابط بالاتصال بالطريقة المتزامنة للكائن أو يصل إلى كتلة الكود المتزامن ، يحصل مؤشر الترابط على قفل الكائن. لا يمكن للمواضيع الأخرى الوصول إلى الطريقة في الوقت الحالي. فقط عند تنفيذ الطريقة أو يتم تنفيذ كتلة التعليمات البرمجية ، هل سيقوم مؤشر ترابط بإصدار قفل الكائن ، ويمكن أن تقوم مؤشرات الترابط الأخرى بتنفيذ الطريقة أو كتلة الكود.
فيما يلي بعض الأمثلة البسيطة لتوضيح استخدام الكلمات الرئيسية المتزامنة:
1. طريقة متزامنة
في الكود التالي ، يقوم ترابطان بإجراء كائن insertData لإدراج البيانات:
اختبار الفئة العامة {public static void main (string [] args) {Final insertData insertData = new ersertdata () ؛ new thread () {public void run () {insertData.Insert (thread.currentThRead ()) ؛ } ؛ }.يبدأ()؛ new thread () {public void run () {insertData.Insert (thread.currentThRead ()) ؛ } ؛ }.يبدأ()؛ }} class insertData {private ArrayList <integer> ArrayList = new ArrayList <integer> () ؛ public void insert (thread thread) {for (int i = 0 ؛ i <5 ؛ i ++) {system.out.println (thread.getName ()+"insert data"+i) ؛ ArrayList.Add (i) ؛ }}} في هذا الوقت ، نتيجة إخراج البرنامج هي:
هذا يدل على أن ترابطين يقومون بتنفيذ طريقة الإدراج في نفس الوقت.
إذا تم إضافة كلمة الكلمة الرئيسية قبل طريقة الإدراج ، فإن نتيجة التشغيل هي:
class insertData {private ArrayList <integer> ArrayList = new ArrayList <integer> () ؛ إدراج void المزامن العام (مؤشر ترابط مؤشر الترابط) {for (int i = 0 ؛ i <5 ؛ i ++) {system.out.println (thread.getName ()+"insert data"+i) ؛ ArrayList.Add (i) ؛ }}}من الإخراج أعلاه ، يتم تنفيذ البيانات التي تم إدراجها في Thread-1 فقط بعد إدخال مؤشر الترابط 0. هذا يوضح أن Thread-0 و Thread-1 ينفذون طرق إدراج بالتتابع.
هذه هي الطريقة المتزامنة.
ومع ذلك ، هناك بعض النقاط التي يجب ملاحظة:
1) عندما يصل مؤشر الترابط إلى الطريقة المتزامنة لكائن ما ، لا يمكن لخيوط أخرى الوصول إلى الطرق الأخرى المتزامنة للكائن. هذا السبب بسيط للغاية ، لأن الكائن لديه قفل واحد فقط. عندما يكتسب مؤشر ترابط قفل الكائن ، لا يمكن لخيوط أخرى الحصول على قفل الكائن ، بحيث لا يمكنهم الوصول إلى طرق أخرى متزامنة للكائن.
2) عندما يصل مؤشر الترابط إلى الطريقة المتزامنة لكائن ، يمكن أن تصل مؤشرات الترابط الأخرى إلى الطريقة غير المتزامنة للكائن. سبب هذا بسيط للغاية. لا يتطلب الوصول إلى الطرق غير المتزامنة قفل الكائن. إذا لم يتم تعديل طريقة مع الكلمة الرئيسية المتزامنة ، فهذا يعني أنها لن تستخدم الموارد الحرجة ، ثم يمكن لخيوط أخرى الوصول إلى هذه الطريقة.
3) إذا كان مؤشر ترابط ما يحتاج إلى الوصول إلى الطريقة المتزامنة المرح 1 للكائن 1 ، ويحتاج مؤشر ترابط آخر B إلى الوصول إلى الطريقة المتزامنة Fun1 من Object2 ، حتى لو كانت الكائن 1 و Object2 هما نفس النوع) ، فلن تكون هناك مشكلة في أمان مؤشر ترابط ، لأنها تصل إلى كائنات مختلفة ، لذلك لا توجد مشكلة استبعاد متبادلة.
2. كتلة الكود المتزامن
تشبه كتل التعليمات البرمجية المتزامنة النموذج التالي:
متزامن (syncobject) {
}
عند تنفيذ كتلة الكود هذه في مؤشر ترابط ، يكتسب مؤشر الترابط قفل كائن كائن ، مما يجعل من المستحيل على مؤشرات الترابط الأخرى الوصول إلى كتلة الكود في نفس الوقت.
يمكن أن يكون Synobject هذا ، يمثل القفل الذي يكتسب الكائن الحالي ، أو يمكن أن يكون سمة في الفئة ، تمثل القفل الذي يكتسب السمة.
على سبيل المثال ، يمكن تغيير طريقة الإدراج أعلاه إلى النموذجين التاليين:
class insertData {private ArrayList <integer> ArrayList = new ArrayList <integer> () ؛ public void insert (thread thread) {synchronized (this) {for (int i = 0 ؛ i <100 ؛ i ++) {system.out.println (thread.getName ()+"insert data"+i) ؛ ArrayList.Add (i) ؛ }}}} class insertData {private ArrayList <integer> ArrayList = new ArrayList <integer> () ؛ كائن خاص = كائن جديد () ؛ public void insert (thread thread) {synchronized (object) {for (int i = 0 ؛ i <100 ؛ i ++) {system.out.println (thread.getName ()+"insert data"+i) ؛ ArrayList.Add (i) ؛ }}}}كما يتضح من ما ورد أعلاه ، فإن كتل التعليمات البرمجية المتزامنة أكثر مرونة بكثير للاستخدام من الطرق المتزامنة. لأنه ربما يجب مزامنة جزء فقط من الكود في الطريقة ، إذا تم مزامنة الطريقة بأكملها في هذا الوقت ، فسيؤثر ذلك على كفاءة تنفيذ البرنامج. يمكن تجنب هذه المشكلة باستخدام كتل التعليمات البرمجية المتزامنة. يمكن أن تتم مزامنة كتل التعليمات البرمجية المتزامنة فقط عند الحاجة إلى التزامن.
بالإضافة إلى ذلك ، يحتوي كل فئة أيضًا على قفل ، يمكن استخدامه للتحكم في الوصول المتزامن إلى أعضاء البيانات الثابتة.
وإذا قام مؤشر ترابط بتنفيذ طريقة متزامنة غير منتظمة لكائن ما ، ويحتاج مؤشر ترابط آخر إلى تنفيذ الطريقة المتزامنة الثابتة للفئة التي ينتمي إليها الكائن ، فلن يكون هناك أي استبعاد متبادل في هذا الوقت ، لأن الوصول إلى الطريقة المتزامنة الثابتة يحتل قفلًا فئة ، أثناء الوصول إلى طريقة متزامنة غير متزامنة غير متزامنة ، لذلك لا يوجد أي استثناء طبيعي.
سوف تفهم من خلال النظر في الكود التالي:
اختبار الفئة العامة {public static void main (string [] args) {Final insertData insertData = new ersertdata () ؛ new thread () {Override public void run () {insertData.insert () ؛ } }.يبدأ()؛ New Thread () {Override public void run () {insertData.insert1 () ؛ } }.يبدأ()؛ }} class insertData {public synchronized void insert () {system.out.println ("execute insert") ؛ حاول {thread.sleep (5000) ؛ } catch (interruptedException e) {E.PrintStackTrace () ؛ } system.out.println ("execute insert1") ؛ } insert1 () {system.out.println (تنفيذ insert1 " System.out.println ("تنفيذ insert1") ؛ }} نتائج التنفيذ ؛
يتم تنفيذ طريقة الإدراج في الخيط الأول ، والتي لن تتسبب في حظر الخيط الثاني طريقة insert1.
دعنا نلقي نظرة على ما تفعله الكلمة الرئيسية المتزامنة. دعونا نفكّت رمزها. رمز BYTECODE المغطى بالرمز التالي هو:
الفئة العامة insertData {private Object = new Object () ؛ insert public void (مؤشر ترابط مؤشر الترابط) {Synchronized (Object) {}} Public Synchronized void insert1 (موضوع مؤشر الترابط) {}من رمز Bytecode الذي تم الحصول عليه عن طريق إلغاء الإصلاح ، يمكن ملاحظة أن كتلة الكود المتزامن لديها بالفعل تعليمين: Monitorenter و Monitorexit. عند تنفيذ تعليمات المراقبة ، سيتم زيادة عدد قفل الكائن بمقدار 1 ، بينما عندما يتم تنفيذ تعليمات monitorexit ، سيتم تخفيض عدد قفل الكائن بمقدار 1. في الواقع ، يشبه هذا العملية الكهروضوئية في نظام التشغيل. يتم استخدام التشغيل الكهروضوئي في نظام التشغيل للتحكم في مؤشرات ترابط متعددة للوصول إلى الموارد الحرجة. بالنسبة للطريقة المتزامنة ، يدرك مؤشر الترابط في التنفيذ ما إذا كان بنية Method_info للطريقة تحتوي على إعداد علامة ACC_Synchronized ، ثم يكتسب تلقائيًا قفل الكائن ، ويتصل بالطريقة ، ويطلق أخيرًا القفل. في حالة حدوث استثناء ، يطلق الخيط تلقائيًا القفل.
شيء واحد يجب ملاحظة: بالنسبة للطرق المتزامنة أو كتل التعليمات البرمجية المتزامنة ، عند حدوث استثناء ، ستقوم JVM تلقائيًا بإصدار القفل الذي يشغله الخيط الحالي ، لذلك لن يكون هناك طريق مسدود بسبب الاستثناء.
3. بعض الأشياء البارزة الأخرى حول المزامنة
1. الفرق بين متزامن و static متزامن
أقفال متزامنة المثيل الحالي للفئة لمنع مؤشرات الترابط الأخرى من الوصول إلى جميع الكتل المتزامنة في مثيل الفئة في نفس الوقت. لاحظ أن هذا هو "الحالة الحالية للفئة" ، ولا يوجد مثل هذا القيد على حالتين مختلفتين من الفصل. ثم يحدث المزامنة الثابتة للتحكم في الوصول إلى جميع مثيلات الفصل. يقيد متزامن ثابت مؤشرات الترابط للوصول إلى جميع مثيلات الفصل في JVM في نفس الوقت والوصول إلى الكود المقابل بسرعة. في الواقع ، إذا تمت مزامنتها في طريقة أو كتلة رمز في فئة ما ، ثم بعد إنشاء مثيل من هذه الفئة ، سيكون للفئة مراقبة سريعة ، ويتم حماية وصول مؤشر الترابط المتزامن إلى المثيل المعدل بسرعة. متزامن ثابت هو الجمهور من المراقبة ، وهو الفرق بين الاثنين. وهذا هو ، متزامنه يعادل هذا.
يحتوي مؤلف ياباني ، "نمط التصميم متعدد مؤشرات الترابط على Jai Chenghao" على عمود مثل هذا:
الفئة pulbic some () {{public synchronized void issynca () {} issyncb () issyncb () {} ثم ، إذا تمت إضافة حالتان A و B من فئة شيء ما ، فلماذا يمكن الوصول إلى المجموعة التالية من الطرق في وقت واحد بواسطة أكثر من موضوع واحد؟ Axissynca () و x.issyncb ()
bxissynca () و y.issynca ()
cxcsynca () و y.csyncb ()
dxissynca () وشيء. csynca ()
هنا ، من الواضح أنه يمكن الحكم عليه:
A ، كل من الوصول إلى المجال المتزامن لنفس الحالة ، لذلك لا يمكن الوصول إليه في نفس الوقت. B مخصص لحالات مختلفة ، بحيث يمكن الوصول إليها في نفس الوقت. نظرًا لأنه متزامن ثابت ، فستظل مثيلات مختلفة مقيدة ، وهو ما يعادل شيئًا. sissynca () وشيء. sissyncb () ، لذلك لا يمكن الوصول إليه في نفس الوقت.
لذا ، ماذا عن د؟ ، يمكن الوصول إلى الإجابة في الكتاب في وقت واحد. سبب الإجابة هو أن متزامنه هو أن طريقة المثيل وطريقة الفئة المتزامنة تختلفان عن القفل.
يعني التحليل الشخصي أن المتزامنة المتزامنة والساكنة تعادل عصابتين ، كل منها له سيطرته الخاصة ، ولا يوجد أي قيد على بعضهما البعض ويمكن الوصول إليه في نفس الوقت. ليس من الواضح كيف يتم تنفيذ متزامن في التصميم الداخلي Java.
الخلاصة: A: ثابت متزامن هو نطاق فئة معينة. يمنع CSYNC {} المتزامنة المتزامنة من الوصول إلى الطريقة الثابتة المتزامنة في هذه الفئة في نفس الوقت. إنه يعمل على جميع مثيلات الكائنات من الفصل.
ب: متزامن هو نطاق مثيل. يمنع requync () {} مؤشرات ترابط متعددة من الوصول إلى الطريقة المتزامنة في هذه الحالة في نفس الوقت.
2. الفرق بين الطريقة المتزامنة والرمز المتزامن بسرعة
لا يوجد فرق بين الأساليب المتزامنة () {} والمزامنة (هذا) {} ، ولكن الطرق المتزامنة () {} مريحة لفهم القراءة ، بينما يمكن أن تتم مزامنة (هذا) {} بشكل أكثر دقة تحكم مناطق الوصول إلى الصراع ، وأحيانًا تؤدي بشكل أكثر كفاءة.
3. لا يمكن أن يتم مورث الكلمة الرئيسية المتزامنة