كتاب "التزامن جافا في الممارسة"

صورةمرحبا ، هابروجيتلي! التدفقات هي جزء أساسي من منصة جافا. تعد المعالجات متعددة النواة شائعة ، وأصبح الاستخدام الفعال للتزامن ضروريًا لإنشاء أي تطبيق عالي الأداء. كانت آلة جافا الافتراضية المحسنة ، ودعم الفئات عالية الأداء ومجموعة غنية من اللبنات الأساسية لمهام التوازي في وقت واحد بمثابة اختراق في تطوير التطبيقات المتوازية. في Java Concurrency في الممارسة العملية ، يشرح مبدعو التكنولوجيا الخارقة أنفسهم ليس فقط كيفية عملهم ، ولكنهم يتحدثون أيضًا عن أنماط التصميم. من السهل إنشاء برنامج تنافسي يبدو أنه يعمل. ومع ذلك ، فإن تطوير البرامج متعددة الخيوط واختبارها وتصحيحها يطرح العديد من المشكلات. يتوقف الرمز عن العمل عندما يكون الأكثر أهمية: تحت الحمل الثقيل.ستجد في "Java Concurrency in Practice" كلاً من النظرية والطرق الخاصة لإنشاء تطبيقات متوازية موثوقة وقابلة للتطوير ومدعومة. لا يقدم المؤلفون قائمة بواجهات برمجة التطبيقات وآليات التوازي ؛ إنهم يقدمون قواعد وأنماط ونماذج تصميم مستقلة عن إصدار Java وتبقى ملائمة وفعالة لسنوات عديدة.

مقتطفات. سلامة الخيط


قد تتفاجأ من أن البرمجة التنافسية ترتبط بالخيوط أو الأقفال (1) لا ترتبط الهندسة المدنية إلا بالمسامير والحزم I. بالطبع ، يتطلب بناء الجسور الاستخدام الصحيح لعدد كبير من المسامير والحزم I ، وينطبق الشيء نفسه على بناء برامج تنافسية ، الأمر الذي يتطلب الاستخدام الصحيح للخيوط والأقفال. لكن هذه مجرد آليات - وسيلة لتحقيق الهدف. إن كتابة كود آمن لمؤشر الترابط هو ، في جوهره ، التحكم في الوصول إلى حالة ، وعلى وجه الخصوص ، إلى حالة قابلة للتغيير.

بشكل عام ، حالة الكائن هي بياناته المخزنة في متغيرات الحالة ، مثل المثيل والحقول الثابتة أو الحقول من الكائنات التابعة الأخرى. يتم تخزين حالة تجزئة HashMap جزئيًا في HashMap نفسه ، ولكن أيضًا في العديد من كائنات Map.Entry. تتضمن حالة الكائن أي بيانات قد تؤثر على سلوكه.

(1) lock block, «», , . blocking. lock «», « ». lock , , , «». — . , , , . — . . .

يمكن لعدة سلاسل وصول إلى متغير مشترك ، متحور - يغير قيمته. في الواقع ، نحن نحاول حماية البيانات ، وليس الكود ، من الوصول التنافسي غير المنضبط.

يتطلب إنشاء كائن آمن لمؤشر الترابط المزامنة لتنسيق الوصول إلى حالة متحولة ، والفشل في الوفاء بها مما قد يؤدي إلى تلف البيانات وعواقب أخرى غير مرغوب فيها.

عندما يصل أكثر من مؤشر ترابط واحد إلى متغير الحالة وربما يكتب أحد مؤشرات الترابط إليه ، يجب على جميع سلاسل الرسائل تنسيق وصولها إليه باستخدام المزامنة. يتم توفير التزامن في Java من خلال الكلمة الأساسية المتزامنة ، والتي توفر قفلًا حصريًا ، بالإضافة إلى المتغيرات الذرية والمتغيرة والأقفال الواضحة.

قاوم إغراء التفكير بأن هناك مواقف لا تتطلب التزامن. يمكن للبرنامج العمل واجتياز اختباراته ، ولكن يظل معطلاً ويتعطل في أي وقت.

إذا وصل العديد من سلاسل الرسائل إلى نفس المتغير بحالة متغيرة بدون مزامنة مناسبة ، فإن برنامجك لا يعمل. هناك ثلاث طرق لإصلاحها:

  • لا تشارك متغير الحالة في جميع سلاسل الرسائل
  • جعل متغير الدولة غير قابل للتغيير ؛
  • استخدم مزامنة الحالة في كل مرة تصل فيها إلى متغير الحالة.

قد تتطلب التصحيحات تغييرات كبيرة في التصميم ، لذلك من الأسهل بكثير تصميم سلسلة محادثات آمنة على الفور بدلاً من ترقيتها لاحقًا.

من الصعب معرفة ما إذا كانت سلاسل رسائل متعددة ستصل إلى هذا المتغير أم لا. لحسن الحظ ، تساعد الحلول التقنية الموجهة للكائنات التي تساعد في إنشاء فئات منظمة جيدًا وسهلة الصيانة - مثل تغليف البيانات وإخفائها - على إنشاء فئات آمنة لمؤشر الترابط. كلما قل عدد سلاسل العمليات التي يمكنها الوصول إلى متغير معين ، كان من الأسهل ضمان التزامن وتعيين الشروط التي يمكن الوصول إلى هذا المتغير في ظلها. لا تجبرك لغة Java على تغليف الحالة - من المقبول تمامًا تخزين الحالة في الحقول العامة (حتى الحقول الثابتة العامة) أو نشر رابط لعنصر داخلي بخلاف ذلك - ولكن كلما كانت حالة البرنامج مغلفة بشكل أفضل ،كلما كان من الأسهل جعل سلسلة برامجك آمنة ومساعدة المشرفين على الحفاظ عليها بهذه الطريقة.

عند تصميم فئات آمنة لمؤشر الترابط ، ستكون الحلول التقنية الجيدة الموجهة للكائنات: التغليف ، وقابلية التغيير ، والمواصفات الواضحة للثوابين هي المساعدين الخاصين بك.

إذا اختلفت الحلول الفنية للتصميم الجيد الموجهة للكائنات عن احتياجات المطور ، فمن الجدير التضحية بقواعد التصميم الجيد من أجل الأداء أو التوافق العكسي مع التعليمات البرمجية القديمة. في بعض الأحيان يتعارض التجريد والتغليف مع الأداء - على الرغم من أنه ليس في كثير من الأحيان كما يعتقد العديد من المطورين - ولكن أفضل الممارسات هي جعل الشفرة صحيحة أولاً ثم بسرعة. حاول استخدام التحسين فقط إذا كانت قياسات الإنتاجية والاحتياجات تشير إلى أنه يجب عليك القيام بذلك (2) .

(2)في الكود التنافسي ، يجب عليك الالتزام بهذه الممارسة أكثر من المعتاد. نظرًا لأنه من الصعب للغاية إعادة إنتاج الأخطاء التنافسية وليس من السهل تصحيحها ، فإن ميزة مكاسب الأداء الصغيرة على بعض فروع الرموز التي نادرًا ما تستخدم يمكن أن تكون ضئيلة للغاية مقارنة بمخاطر تعطل البرنامج في ظروف التشغيل.

إذا قررت أنك بحاجة إلى كسر التغليف ، فلن يتم فقدان كل شيء. لا يزال من الممكن جعل برنامجك آمنًا ، ولكن العملية ستكون أكثر تعقيدًا وأكثر تكلفة ، ولن تكون النتيجة موثوقة. يصف الفصل 4 الظروف التي يمكن فيها التخفيف من تغليف متغيرات الحالة بأمان.

حتى الآن ، استخدمنا مصطلحي "فئة الصفحات الآمنة" و "برنامج الصفحات الآمنة" بشكل متبادل تقريبًا. هل تم إنشاء برنامج آمن لمؤشر الترابط بالكامل من فئات سلسلة المحادثات الآمنة؟ اختياري: قد لا يكون البرنامج الذي يتكون بالكامل من الفئات الآمنة لمؤشر الترابط آمنًا لمؤشر الترابط ، وقد يحتوي برنامج آمن لمؤشر الترابط على فئات غير آمنة لمؤشر الترابط. يتم أيضًا مناقشة المشكلات المتعلقة بتخطيط الفئات الآمنة لمؤشر الترابط في الفصل 4. في أي حال ، فإن مفهوم الفئة الآمنة لمؤشر الترابط لا معنى له إلا إذا قام الفصل بتغليف حالته الخاصة. يمكن تطبيق مصطلح "سلامة الخيط" على الشفرة ، لكنه يتحدث عن الحالة ولا يمكن تطبيقه إلا على تلك المجموعة من التعليمات البرمجية التي تحتوي على حالتها (يمكن أن تكون كائنًا أو البرنامج بأكمله).

2.1. ما هي سلامة الخيط؟


ليس من السهل تحديد سلامة الخيط. يمنحك بحث Google السريع العديد من الخيارات مثل:

... يمكن استدعاؤها من سلاسل برامج متعددة دون تفاعلات غير مرغوب فيها بين سلاسل المحادثات.

... يمكن استدعاؤها بسلسلين أو أكثر في نفس الوقت ، دون الحاجة إلى أي إجراء آخر من المتصل.

بالنظر إلى مثل هذه التعريفات ، فإنه ليس من المستغرب أن نجد سلامة الموضوع محيرة! كيفية التمييز بين فئة آمنة لمؤشر الترابط من فئة غير آمنة؟ ماذا نعني بكلمة "آمن"؟

في صميم أي تعريف معقول لسلامة الخيط هو مفهوم الصواب.

يعني الصواب أن الفئة تتوافق مع مواصفاتها. تحدد المواصفة الثوابت التي تحد من حالة الكائن والظروف اللاحقة التي تصف آثار العمليات. كيف تعرف أن مواصفات الصفوف صحيحة؟ مستحيل ، لكن هذا لا يمنعنا من استخدامها بعد أن أقنعنا أنفسنا بأن الشفرة تعمل. لذلك دعونا نفترض أن صحة الخيوط الفردية شيء مرئي. الآن يمكننا أن نفترض أن فئة آمنة لمؤشر الترابط تتصرف بشكل صحيح أثناء الوصول من مؤشرات ترابط متعددة.

تعد الفئة آمنة لمؤشر الترابط إذا تصرفت بشكل صحيح أثناء الوصول من مؤشرات ترابط متعددة ، بغض النظر عن كيفية جدولة هذه الترابط أو تشذيرها من قبل بيئة العمل ، وبدون مزامنة إضافية أو تنسيق آخر من جانب رمز الاستدعاء.

لا يمكن لبرنامج متعدد مؤشرات الترابط أن يكون آمنًا لمؤشر الترابط إذا لم يكن صحيحًا حتى في بيئة مفردة الترابط (3) . إذا تم تنفيذ الكائن بشكل صحيح ، فلا يجب أن ينتهك أي تسلسل للعمليات - الوصول إلى الأساليب العامة والقراءة أو الكتابة إلى الحقول العامة - الثوابت أو الشروط اللاحقة. لا يمكن أن تؤدي أي مجموعة من العمليات التي يتم إجراؤها بالتتابع أو بشكل تنافسي على مثيلات فئة آمنة لمؤشر الترابط إلى أن يكون المثيل في حالة غير صالحة.

(3) إذا كان الاستخدام الفضفاض لمصطلح "الصحيح" يزعجك هنا ، يمكنك التفكير في فئة آمنة لمؤشر الترابط كفئة معيبة في بيئة تنافسية ، وكذلك في بيئة مفردة الخيوط.

تعمل الفئات الآمنة لمؤشر الترابط على تغليف أي مزامنة ضرورية بنفسها ولا تحتاج إلى مساعدة العميل.

2.1.1. مثال: servlet بدون دعم الحالة الداخلية


في الفصل الأول ، قمنا بإدراج الهياكل التي تقوم بإنشاء سلاسل الرسائل واستدعاء المكونات منها التي تتحمل مسؤوليتها عن سلامة سلسلة الرسائل. الآن نحن عازمون على تطوير خدمة عامل servlet وتوسيع وظائفها تدريجيا مع الحفاظ على سلامة الخيوط.

تُظهر القائمة 2.1 خادمًا بسيطًا يقوم بفك ضغط رقم من استعلام ، ومعاملته ، ولف النتائج في الاستجابة.

قائمة 2.1. Servlet بدون دعم داخلي

@ThreadSafe
public class StatelessFactorizer implements Servlet {
      public void service(ServletRequest req, ServletResponse resp) {
            BigInteger i = extractFromRequest(req);
            BigInteger[] factors = factor(i);
            encodeIntoResponse(resp, factors);
      }
}

لا تحتوي حالة StatelessFactorizer ، مثل معظم servlets ، على حالة داخلية: فهي لا تحتوي على حقول ولا تشير إلى حقول من فئات أخرى. توجد حالة عملية حسابية معينة فقط في المتغيرات المحلية التي يتم تخزينها في حزمة الدفق وهي متاحة فقط للدفق التنفيذي. لا يمكن أن يؤثر مؤشر ترابط واحد الوصول إلى StatelessFactorizer على نتيجة مؤشر ترابط آخر القيام بنفس الشيء ، لأن هذه مؤشرات الترابط لا تشارك الحالة.

الكائنات التي لا تدعم الحالة الداخلية تكون دائمًا في أمان.

إن حقيقة أنه يمكن تنفيذ معظم servlets بدون دعم الحالة الداخلية يقلل بشكل كبير من عبء خيوط servlets نفسها. وفقط عندما تحتاج servlets إلى تذكر شيء ما ، تزداد متطلبات سلامة خيطها.

2.2. الذرية


ماذا يحدث عند إضافة عنصر حالة إلى كائن بدون دعم حالة داخلي؟ لنفترض أننا نريد إضافة عداد دخول يقيس عدد الطلبات التي تمت معالجتها. يمكنك إضافة حقل من نوع طويل إلى servlet وزيادته مع كل طلب ، كما هو موضح في UnsafeCountingFactorizer في القائمة 2.2.

قائمة 2.2. خادم يقوم بحساب الطلبات بدون المزامنة اللازمة. ولا ينبغي القيام بذلك.

صورة

@NotThreadSafe
public class UnsafeCountingFactorizer implements Servlet {
      private long count = 0;

      public long getCount() { return count; }

      public void service(ServletRequest req, ServletResponse resp) {
            BigInteger i = extractFromRequest(req);
            BigInteger[] factors = factor(i);
            ++count;
            encodeIntoResponse(resp, factors);
      }
}

لسوء الحظ ، فإن فئة UnsafeCountingFactorizer ليست آمنة لمؤشر الترابط ، حتى إذا كانت تعمل بشكل جيد في بيئة ترابط واحد. مثل UnsafeSequence ، فهي عرضة للتحديثات المفقودة. على الرغم من أن عملية الزيادة في عدد ++ تحتوي على بنية مضغوطة ، إلا أنها ليست ذرية ، وهي غير قابلة للتجزئة ، ولكن سلسلة من ثلاث عمليات: توصيل القيمة الحالية ، وإضافة واحدة إليها وكتابة القيمة الجديدة مرة أخرى. في عمليات "القراءة والتغيير والكتابة" ، يتم الحصول على الحالة الناتجة عن الحالة السابقة.

في التين. 1.1 يظهر ما يمكن أن يحدث إذا حاول خيطان زيادة العداد في نفس الوقت ، دون التزامن. إذا كان العداد 9 ، فبسبب التنسيق غير الناجح للوقت ، سيرى كلا الخيوط القيمة 9 ، ويضيف واحدًا إليها ، ويضبط القيمة على 10. لذلك سيبدأ العداد المتأخر في التأخر بمقدار واحد.

قد تعتقد أن وجود عداد ضرب غير دقيق قليلاً في خدمة ويب خسارة مقبولة ، وأحيانًا يكون ذلك. ولكن إذا تم استخدام العداد لإنشاء تسلسلات أو معرفات فريدة للكائنات ، فإن إعادة القيمة نفسها من التنشيط المتعدد يمكن أن يؤدي إلى مشاكل خطيرة في تكامل البيانات. تنشأ إمكانية ظهور نتائج غير صحيحة بسبب التنسيق الزمني غير الناجح في حالة السباق.

2.2.1. شروط السباق


تحتوي فئة UnsafeCountingFactorizer على العديد من شروط السباق (4) . النوع الأكثر شيوعًا من حالة العرق هو حالة "التحقق ثم التصرف" ، حيث يتم استخدام ملاحظة قديمة محتملة لتقرير ما يجب فعله بعد ذلك.

(4) (data race). , . , , , , , . Java. , , . UnsafeCountingFactorizer . 16.

غالبًا ما نواجه حالة عرق في الحياة الواقعية. لنفترض أنك تخطط لمقابلة أحد الأصدقاء ظهرًا في مقهى ستاربكس في Universitetskiy Prospekt. ولكن ستكتشف أن هناك نوعان من ستاربكس في شارع الجامعة. في الساعة 12:10 لا ترى صديقك في المقهى A وتذهب إلى المقهى B ، لكنه ليس هناك أيضًا. إما أن يتأخر صديقك ، أو يصل إلى المقهى A فور مغادرتك ، أو أنه كان في المقهى B ، لكنه ذهب يبحث عنك وهو الآن في طريقه إلى المقهى A. سنقبل الأخير ، وهو أسوأ سيناريو. الآن 12:15 ، ويتساءل كلاكما عما إذا كان صديقك أوفى بوعده. هل ستعود إلى مقهى آخر؟ كم مرة سوف تذهب ذهابا وإيابا؟ إذا لم تكن قد اتفقت على بروتوكول ، يمكنك قضاء يوم كامل في المشي على طول شارع الجامعة في النشوة الكافيين.
المشكلة في نهج "التنزه ومعرفة ما إذا كان هناك" هو أن المشي على طول الشارع بين مقهيين يستغرق عدة دقائق ، وخلال هذا الوقت يمكن أن تتغير حالة النظام.

يوضح المثال مع Starbucks اعتماد النتيجة على تنسيق الوقت النسبي للأحداث (على المدة التي تنتظرها لصديق أثناء وجودك في مقهى ، وما إلى ذلك). ملاحظة أنه ليس في المقهى "أ" يحتمل أن يكون باطلاً: بمجرد خروجك من الباب الأمامي ، يمكنه الدخول من الباب الخلفي. تتسبب معظم حالات السباق في حدوث مشكلات مثل استثناء غير متوقع وبيانات مكتوبة وفساد في الملف.

2.2.2. مثال: شروط السباق في التهيئة البطيئة


خدعة شائعة باستخدام نهج "الاختيار ثم العمل" هي التهيئة البطيئة (LazyInitRace). الغرض منه هو تأجيل التهيئة للكائن حتى تكون هناك حاجة إليه ، والتأكد من أنه تمت تهيئته مرة واحدة فقط. في القائمة 2.3 ، يضمن أسلوب getInstance أن ExpensiveObject تمت تهيئته وإرجاع مثيل موجود ، أو ، بخلاف ذلك ، إنشاء مثيل جديد وإعادته بعد الاحتفاظ بمرجع إليه.

قائمة 2.3. حالة السباق في التهيئة البطيئة. ولا ينبغي القيام بذلك.

صورة

@NotThreadSafe
public class LazyInitRace {
      private ExpensiveObject instance = null;

      public ExpensiveObject getInstance() {
            if (instance == null)
                instance = new ExpensiveObject();
            return instance;
      }
}

تحتوي فئة LazyInitRace على شروط السباق. افترض أن مؤشرات الترابط A و B تنفذ أسلوب getInstance في نفس الوقت. يرى A أن حقل المثيل فارغ ، ويقوم بإنشاء ExpensiveObject جديد. يتحقق مؤشر الترابط B أيضًا لمعرفة ما إذا كان حقل المثيل هو نفس القيمة الفارغة. يعتمد وجود قيمة فارغة في الحقل في الوقت الحالي على تنسيق الوقت ، بما في ذلك تقلبات التخطيط ومقدار الوقت المطلوب لإنشاء مثيل لـ ExpensiveObject وتعيين القيمة في حقل المثيل. إذا كان حقل المثيل فارغًا عند قيام B بالتحقق منه ، فيمكن لعنصرين من التعليمات البرمجية باستدعاء أسلوب getInstance الحصول على نتيجتين مختلفتين ، حتى إذا كان من المفترض أن تعرض طريقة getInstance دائمًا نفس المثيل.

يحتوي عداد الدخول في UnsafeCountingFactorizer أيضًا على ظروف السباق. يشير نهج "القراءة والتغيير والكتابة" إلى أنه لزيادة العداد ، يجب أن يعرف الدفق قيمته السابقة والتأكد من عدم قيام أي شخص آخر بتغيير هذه القيمة أو استخدامها أثناء عملية التحديث.

مثل معظم الأخطاء التنافسية ، لا تؤدي ظروف العرق دائمًا إلى الفشل: التنسيق المؤقت ناجح. ولكن إذا تم استخدام فئة LazyInitRace لنسخ التسجيل للتطبيق بأكمله ، فعندما سيعود مثيلات مختلفة من عمليات تنشيط متعددة ، فسيتم فقد التسجيلات أو ستتلقى الإجراءات تمثيلات متضاربة لمجموعة الكائنات المسجلة. أو إذا تم استخدام فئة UnsafeSequence لإنشاء معرفات الكيانات في بنية حفظ البيانات ، فيمكن أن يكون لكائنين مختلفين نفس المعرف ، مما ينتهك قيود الهوية.

2.2.3. الإجراءات المركبة


يحتوي كل من LazyInitRace و UnsafeCountingFactorizer على سلسلة من العمليات التي يجب أن تكون ذرية. ولكن لمنع حالة السباق ، يجب أن يكون هناك عقبة أمام مؤشرات الترابط الأخرى لاستخدام المتغير أثناء تعديل مؤشر ترابط واحد.

تكون العمليات A و B ذرية إذا ، من وجهة نظر عملية تنفيذ الخيط A ، تم تنفيذ العملية B بالكامل بواسطة خيط آخر أو حتى لم يتم تنفيذها جزئيًا.

سوف تتجنب ذرية عملية الزيادة في UnsafeSequence حالة السباق الموضحة في الشكل. 1.1. يجب أن تكون عمليات "التحقق ثم التصرف" و "القراءة والتغيير والكتابة" دائمًا ذرية. يطلق عليها الإجراءات المركبة - تسلسل العمليات التي يجب إجراؤها تلقائيًا من أجل الحفاظ على أمان الخيط. في القسم التالي ، سننظر في القفل - آلية مضمنة في Java توفر الذرية. في هذه الأثناء ، سنقوم بإصلاح المشكلة بطريقة أخرى من خلال تطبيق فئة خيط آمنة موجودة ، كما هو موضح في Countingfactorizer في قائمة 2.4.

قائمة 2.4. طلبات حساب Servlet باستخدام AtomicLong

@ThreadSafe
public class CountingFactorizer implements Servlet {
      private final AtomicLong count = new AtomicLong(0);

      public long getCount() { return count.get(); }

      public void service(ServletRequest req, ServletResponse resp) {
            BigInteger i = extractFromRequest(req);
            BigInteger[] factors = factor(i);
            count.incrementAndGet();
            encodeIntoResponse(resp, factors);
      }
}

تحتوي حزمة java.util.concurrent.atomic على متغيرات ذرية لإدارة حالات الطبقة. استبدال نوع العداد من طويل إلى AtomicLong ، نضمن أن جميع الإجراءات التي تشير إلى حالة العداد هي ذرية 1. بما أن حالة servlet هي حالة العداد ، والعداد آمن للخيط ، يصبح servlet لدينا آمنًا للخيط.

عند إضافة عنصر حالة واحد إلى فئة لا تدعم الحالة الداخلية ، فإن الفئة الناتجة ستكون آمنة لمؤشر الترابط إذا تم التحكم في الحالة بالكامل بواسطة الكائن الآمن لمؤشر الترابط. ولكن ، كما سنرى في القسم التالي ، لن يكون الانتقال من متغير حالة إلى آخر بسيطًا مثل الانتقال من صفر إلى واحد.

استخدم ، حيثما كان ذلك مناسبًا ، كائنات خيطية آمنة موجودة ، مثل AtomicLong ، للتحكم في حالة فصلك. من الممكن الحفاظ على الحالات المحتملة للكائنات الآمنة ذات الصلة بالخيط وانتقالها إلى حالات أخرى والتحقق من سلامة الخيط من متغيرات الحالة العشوائية.

»يمكن العثور على مزيد من المعلومات حول الكتاب على موقع الناشر على الويب
» المحتويات
» مقتطفات

لـ Khabrozhiteley خصم 25 ٪ على القسيمة - Java

بعد دفع النسخة الورقية من الكتاب ، يتم إرسال كتاب إلكتروني عبر البريد الإلكتروني.

All Articles