تعد البرمجة المتزامنة واحدة من أهم المهارات لمبرمجي Java وواحدة من أصعب المهارات التي يجب إتقانها. إنه يتطلب من المبرمجين أن يكون لديهم فهم عميق لأدنى مبادئ تشغيل للكمبيوتر ، وفي الوقت نفسه ، يتطلب من المبرمجين أن يكون لديهم منطق واضطراب واضح ، حتى يتمكنوا من كتابة برامج متزامنة فعالة وآمنة وموثوقة. ستبدأ هذه السلسلة من طبيعة التنسيق بين الخيوط (انتظر ، إخطار ، إخطار) ، متزامن ومتقلب ، وشرح بالتفصيل كل أداة تزامن وآلية التنفيذ الأساسية التي توفرها JDK. على هذا الأساس ، سنقوم بتحليل فئات الأدوات في حزمة java.util.concurrent ، بما في ذلك استخدامها ، وتنفيذ رمز المصدر والمبادئ التي تقف وراءها. هذه المقالة هي المقالة الأولى في هذه السلسلة وهي الجزء النظري الأساسي في هذه السلسلة. سيتم تحليل المقالات اللاحقة وشرحها بناءً على هذا.
1. المشاركة
مشاركة البيانات هي واحدة من الأسباب الرئيسية لسلامة مؤشرات الترابط. إذا كانت جميع البيانات صالحة فقط في مؤشر الترابط ، فلا توجد مشكلة سلامة مؤشر ترابط ، وهي أحد الأسباب الرئيسية التي تجعلنا في كثير من الأحيان لا نحتاج إلى النظر في سلامة مؤشرات الترابط عند البرمجة. ومع ذلك ، في البرمجة المتعددة ، مشاركة البيانات أمر لا مفر منه. السيناريو الأكثر نموذجية هو البيانات الموجودة في قاعدة البيانات. من أجل ضمان اتساق البيانات ، عادة ما نحتاج إلى مشاركة البيانات في نفس قاعدة البيانات. حتى في حالة Master و Slave ، يتم الوصول إلى نفس البيانات. يقوم السيد والعبد فقط بنسخ نفس البيانات لكفاءة الوصول وأمن البيانات. نوضح الآن المشكلات الناتجة عن مشاركة البيانات تحت مؤشرات ترابط متعددة من خلال مثال بسيط:
قصاصة الكود 1:
package com.paddx.test.concurrent ؛ الطبقة العامة sharedata {public static int count = 0 ؛ public static void main (string [] args) {final shareata data = new shareata () ؛ لـ (int i = 0 ؛ i <10 ؛ i ++) {new thread (new runnable () {Override public void run () {try {// pause for 1 millisecond at to there to ther that than thread of concerty thread. data.addcount () ؛ } جرب {// يتم إيقاف البرنامج الرئيسي لمدة 3 ثوان لضمان أن تنفيذ البرنامج أعلاه قد اكتمل thread.sleep (3000) ؛ } catch (interruptedException e) {E.PrintStackTrace () ؛ } system.out.println ("count =" + count) ؛ } public void addCount () {count ++ ؛ }}الغرض من الكود أعلاه هو إضافة عملية واحدة لحساب وتنفيذ 1000 مرة ، ولكن يتم تنفيذها من خلال 10 مؤشرات ترابط ، ويتم تنفيذ كل مؤشر ترابط 100 مرة ، وفي ظل الظروف العادية ، يجب إخراج 1000. ومع ذلك ، إذا قمت بتشغيل البرنامج أعلاه ، فستجد أن النتيجة ليست كذلك. فيما يلي نتيجة التنفيذ لوقت معين (قد لا تكون نتائج كل تشغيل هي نفسها ، وأحيانًا قد يتم الحصول على النتيجة الصحيحة):
يمكن ملاحظة أنه بالنسبة للعمليات المتغيرة المشتركة ، يمكن رؤية العديد من النتائج غير المتوقعة بسهولة في بيئة متعددة الخيوط.
2. الاستبعاد المتبادل
يعني الاستبعاد المتبادل للموارد أنه لا يُسمح إلا للزائر بالوصول إليه في نفس الوقت ، وهو فريد من نوعه وحصري. عادةً ما نسمح لروابط سلسلة متعددة بقراءة البيانات في نفس الوقت ، ولكن يمكن لخيط واحد فقط كتابة البيانات في نفس الوقت. لذلك عادةً ما نقسم الأقفال إلى أقفال مشتركة وأقفال حصرية ، تسمى أيضًا أقفال القراءة وكتابة الأقفال. إذا لم تكن الموارد حصرية بشكل متبادل ، فلن نحتاج إلى القلق بشأن سلامة الخيط حتى لو كانت موارد مشتركة. على سبيل المثال ، بالنسبة لمشاركة البيانات غير القابلة للتغيير ، يمكن لجميع مؤشرات الترابط قراءتها فقط ، لذا فإن مشكلات سلامة مؤشرات الترابط ليست ضرورية. ومع ذلك ، تتطلب عمليات الكتابة للبيانات المشتركة عمومًا استبعادًا متبادلاً. في المثال أعلاه ، تحدث مشاكل تعديل البيانات بسبب عدم الاستبعاد المتبادل. يوفر Java آليات متعددة لضمان الاستبعاد المتبادل ، والأسهل طريقة هي استخدام متزامن. الآن نضيف متزامن إلى البرنامج أعلاه وننفذ:
قصاصة الكود الثاني:
package com.paddx.test.concurrent ؛ الطبقة العامة sharedata {public static int count = 0 ؛ public static void main (string [] args) {final shareata data = new shareata () ؛ لـ (int i = 0 ؛ i <10 ؛ i ++) {new thread (new runnable () {Override public void run () {try {// pause for 1 millisecond at to there to ther that than thread of concerty thread. data.addcount () ؛ } جرب {// يتم إيقاف البرنامج الرئيسي لمدة 3 ثوان لضمان أن تنفيذ البرنامج أعلاه قد اكتمل thread.sleep (3000) ؛ } catch (interruptedException e) {E.PrintStackTrace () ؛ } system.out.println ("count =" + count) ؛ } / *** إضافة الكلمة الرئيسية المتزامنة* / public synchronized void addCount () {count ++ ؛ }}الآن بعد تنفيذ الكود أعلاه ، ستجد أنه بغض النظر عن عدد المرات التي تنفذ فيها ، ستكون النتيجة النهائية 1000.
ثالثا. الذرة
يشير Atomicity إلى تشغيل البيانات ككل مستقل وغير قابل للتجزئة. بمعنى آخر ، إنها عملية مستمرة وغير متوقعة. لا يتم تعديل نصف تنفيذ البيانات بواسطة مؤشرات الترابط الأخرى. أسهل طريقة لضمان تعليمات نظام التشغيل ، أي إذا كانت عملية واحدة تتوافق مع تعليمات نظام التشغيل في وقت واحد ، فستضمن بالتأكيد الذرة. ومع ذلك ، لا يمكن إكمال العديد من العمليات بتعليم واحد. على سبيل المثال ، بالنسبة للعمليات الطويلة من النوع ، يجب تقسيم العديد من الأنظمة إلى تعليمات متعددة للعمل في المواضع العالية والمنخفضة على التوالي. على سبيل المثال ، يجب تقسيم تشغيل INTEGER I ++ الذي نستخدمه غالبًا إلى ثلاث خطوات: (1) اقرأ قيمة عدد صحيح I ؛ (2) إضافة عملية واحدة إلى أنا ؛ (3) اكتب النتيجة مرة أخرى إلى الذاكرة. قد تحدث هذه العملية في MultiTreading:
هذا هو السبب أيضًا في أن نتيجة تنفيذ قطاع الكود غير صحيحة. بالنسبة لهذه العملية المركب ، يمكن تنفيذ الطريقة الأكثر شيوعًا لضمان قفل الذرة ، مثل المزامنة أو القفل في Java ، ويتم تنفيذ قطاع الكود 2 من خلال متزامن. بالإضافة إلى الأقفال ، هناك طريقة أخرى لـ CAS (مقارنة ومبادلة) ، أي قبل تعديل البيانات ، قارن ما إذا كانت القيم التي تقرأ قبل أن تكون الثقافات السابقة متسقة. إذا كانت متسقة ، فقم بتعديلها ، وإذا كانت غير متسقة ، فسيتم تنفيذها مرة أخرى. هذا هو أيضا مبدأ تحسين تنفيذ القفل. ومع ذلك ، قد لا تكون CAS فعالة في بعض السيناريوهات. على سبيل المثال ، يقوم مؤشر ترابط آخر أولاً بتعديل قيمة معينة ثم يغيرها مرة أخرى إلى القيمة الأصلية. في هذه الحالة ، لا يمكن لـ CAS الحكم.
4. الرؤية
لفهم الرؤية ، تحتاج إلى فهم معين لنموذج ذاكرة JVM. يشبه نموذج ذاكرة JVM نظام التشغيل ، كما هو موضح في الشكل:
من هذا الشكل ، يمكننا أن نرى أن كل مؤشر ترابط له ذاكرة العمل الخاصة به (أي ما يعادل عازلة وحدة المعالجة المركزية المتقدمة. الغرض من ذلك هو تضييق نطاق السرعة بين نظام التخزين ووحدة المعالجة المركزية وتحسين الأداء). بالنسبة للمتغيرات المشتركة ، في كل مرة يقرأ مؤشر الترابط نسخة من المتغير المشترك في الذاكرة العاملة. عند الكتابة ، يقوم بتعديل قيمة النسخة في الذاكرة العاملة مباشرة ، ثم يزامن الذاكرة العاملة مع القيمة في الذاكرة الرئيسية في وقت معين. المشكلة التي يسببها هذا هي أنه إذا قام مؤشر الترابط 1 بتعديل متغير معين ، فقد لا يرى مؤشر الترابط 2 التعديلات التي تم إجراؤها بواسطة مؤشر الترابط 1 إلى المتغير المشترك. من خلال البرنامج التالي ، يمكننا إظهار المشكلة غير المرئية:
package com.paddx.test.concurrent ؛ public class prosibilitytest {private static boolean ready ؛ رقم int ثابت خاص ؛ يمتد قارئ الفئة الثابتة الخاصة بتوسيع الموضوع {public void run () {try {thread.sleep (10) ؛ } catch (interruptedException e) {E.PrintStackTrace () ؛ } if (! ready) {system.out.println (ready) ؛ } system.out.println (number) ؛ }} private static class constrathread يمتد thread {public void run () {try {thread.sleep (10) ؛ } catch (interruptedException e) {E.PrintStackTrace () ؛ } الرقم = 100 ؛ جاهز = صحيح ؛ }} public static void main (string [] args) {new constrathread (). start () ؛ new readerThRead (). start () ؛ }}بشكل حدسي ، يجب أن يخرج هذا البرنامج فقط 100 ، ولن تتم طباعة القيمة الجاهزة. في الواقع ، إذا قمت بتنفيذ الكود أعلاه عدة مرات ، فقد يكون هناك العديد من النتائج المختلفة. فيما يلي نتائج بعض الشوطين:
بالطبع ، لا يمكن القول أن هذه النتيجة ممكنة فقط بسبب الرؤية. عندما يتم تعيين مؤشر ترابط الكتابة (الكاتب) جاهزًا = صحيح ، لا يمكن للقارئ رؤية النتيجة المعدلة ، لذلك سيتم طباعة خطأ. بالنسبة للنتيجة الثانية ، لم يتم قراءة نتيجة مؤشر ترابط الكتابة عند التنفيذ إذا (! جاهزة) ، ولكن يتم قراءة نتيجة تنفيذ مؤشر ترابط الكتابة عند تنفيذ System.out.println (جاهز). ومع ذلك ، قد تكون هذه النتيجة أيضًا ناتجة عن التنفيذ البديل للمواضيع. يمكن ضمان الرؤية من خلال المزامنة أو المتقلبة في Java ، وسيتم تحليل التفاصيل المحددة في المقالات اللاحقة.
5. التسلسل
لتحسين الأداء ، يجوز للمترجم والمعالج إعادة ترتيب التعليمات. هناك ثلاثة أنواع من إعادة الترتيب:
(1) إعادة ترتيب التحسين المترجم. يمكن للمترجم إعادة جدولة ترتيب تنفيذ البيانات دون تغيير دلالات برنامج واحد.
(2) إعادة ترتيب التوازي على مستوى التعليمات. تستخدم المعالجات الحديثة تقنية متوازية على مستوى التعليمات (ICP) للتداخل في تنفيذ تعليمات متعددة. إذا لم يكن هناك اعتماد على البيانات ، يمكن للمعالج تغيير ترتيب تنفيذ البيان المقابل لتعليمات الجهاز.
(3) إعادة ترتيب نظام الذاكرة. نظرًا لأن المعالج يستخدم ذاكرة التخزين المؤقت وقراءة/كتابة المخازن المؤقتة ، فإن هذا يجعل عمليات التحميل والتخزين قد تم تنفيذها خارج الترتيب.
يمكننا الرجوع مباشرة إلى وصف إعادة ترتيب المشكلات في JSR 133:
(1) (2)
دعونا نلقي نظرة أولاً على جزء الكود المصدري (1) في الصورة أعلاه. من رمز المصدر ، يتم تنفيذ الإما تعليمات 1 أولاً أو يتم تنفيذ التعليمات 3 أولاً. إذا تم تنفيذ التعليمات 1 أولاً ، يجب ألا يرى R2 القيمة المكتوبة في التعليمات 4. إذا تم تنفيذ التعليمات 3 أولاً ، يجب ألا يرى R1 القيمة المكتوبة بواسطة التعليمات 2. ومع ذلك ، قد يكون لنتيجة التشغيل R2 == 2 و R1 == 1 ، وهي نتيجة "إعادة الترتيب". الشكل أعلاه (2) هو نتيجة تجميع قانونية محتملة. بعد التجميع ، قد يتم تبادل ترتيب التعليمات 1 والتعليم 2. لذلك ، ستظهر نتيجة R2 == 2 و R1 == 1. يمكن أيضًا استخدام متزامن أو متقلبة في Java لضمان الطلب.
ستة ملخص
تشرح هذه المقالة الأساس النظري للبرمجة المتزامنة Java ، وسيتم مناقشة بعض الأشياء بمزيد من التفصيل في التحليل اللاحق ، مثل الرؤية ، والترتيب ، وما إلى ذلك. ستتم مناقشة المقالات اللاحقة بناءً على محتوى هذا الفصل. إذا تمكنت من فهم المحتوى أعلاه جيدًا ، فأعتقد أنه سيكون مفيدًا لك ما إذا كان من المفترض أن يفهم مقالات البرمجة المتزامنة الأخرى أو في أعمال البرمجة اليومية المتزامنة.