ينصب تركيز هذه المقالة على قضايا الأداء للتطبيقات المتعددة. سنقوم أولاً بتحديد الأداء وقابلية التوسع ، ثم ندرس بعناية قاعدة AMDAHL. في المحتوى التالي ، سنقوم بفحص كيفية استخدام طرق فنية مختلفة لتقليل منافسة القفل وكيفية تنفيذها باستخدام التعليمات البرمجية.
1. الأداء
نعلم جميعًا أنه يمكن استخدام MultiTreading لتحسين أداء البرنامج ، والسبب وراء ذلك هو أن لدينا وحدات المعالجة المركزية متعددة النواة أو وحدات المعالجة المركزية المتعددة. يمكن لكل قلب وحدة المعالجة المركزية إكمال المهام بمفرده ، وبالتالي فإن تقسيم مهمة كبيرة إلى سلسلة من المهام الصغيرة التي يمكن تشغيلها بشكل مستقل عن بعضها البعض يمكن أن يحسن الأداء العام للبرنامج. يمكنك إعطاء مثال. على سبيل المثال ، هناك برنامج يغير حجم جميع الصور في مجلد على القرص الثابت ، ويمكن أن يحسن تطبيق تقنية الخيوط المتعددة أدائها. يمكن أن يؤدي استخدام نهج واحد مترابطة إلى اجتياز جميع ملفات الصور بالتسلسل وإجراء التعديلات. إذا كانت وحدة المعالجة المركزية لدينا تحتوي على العديد من النوى ، فلا شك أنه لا يمكنها استخدام أحدها إلا. باستخدام Multi-Threshence ، يمكن أن يكون لدينا مؤشر ترابط المنتج فحص نظام الملفات لإضافة كل صورة إلى قائمة انتظار ، ثم استخدام مؤشرات ترابط عامل متعددة لتنفيذ هذه المهام. إذا كان عدد مؤشرات ترابط العمال هو نفسه العدد الإجمالي لنوى وحدة المعالجة المركزية ، فيمكننا التأكد من أن كل قلب وحدة المعالجة المركزية يعمل حتى يتم تنفيذ جميع المهام.
بالنسبة لبرنامج آخر يتطلب المزيد من انتظار IO ، يمكن أيضًا تحسين الأداء الكلي باستخدام تقنية متعددة الخيوط. لنفترض أننا نريد أن نكتب مثل هذا البرنامج الذي نحتاج إلى زحف جميع ملفات HTML لموقع ويب معين وتخزينها على القرص المحلي. يمكن أن يبدأ البرنامج من صفحة ويب معينة ، ثم تحليل جميع الروابط إلى موقع الويب هذا في صفحة الويب هذه ، ثم زحف هذه الروابط بدورها ، بحيث يكرر نفسه. نظرًا لأن الأمر يستغرق بعض الوقت للانتظار من الوقت الذي نبدأ فيه طلبًا إلى موقع الويب البعيد إلى الوقت الذي نتلقى فيه جميع بيانات صفحة الويب ، يمكننا تسليم هذه المهمة إلى مؤشرات ترابط متعددة للتنفيذ. اسمحوا بواحد أو أكثر بقليل من صفحات HTML المستلمة ووضع الرابط الموجود في قائمة الانتظار ، تاركًا جميع مؤشرات الترابط الأخرى المسؤولة عن طلب الصفحة. على عكس المثال السابق ، في هذا المثال ، لا يزال بإمكانك الحصول على تحسينات في الأداء حتى إذا كنت تستخدم مؤشرات ترابط أكثر من عدد نوى وحدة المعالجة المركزية.
يخبرنا الأمثلان أعلاه أن الأداء العالي هو القيام بأكبر عدد ممكن من الأشياء في نافذة زمنية قصيرة. هذا بالطبع التفسير الكلاسيكي لمصطلح الأداء. ولكن في الوقت نفسه ، يمكن أن يؤدي استخدام المواضيع أيضًا إلى تحسين سرعة استجابة برامجنا جيدًا. تخيل أن لدينا مثل هذا تطبيق الواجهة الرسومية ، مع مربع إدخال أعلاه وزر يسمى "العملية" أسفل مربع الإدخال. عندما يضغط المستخدم على هذا الزر ، يحتاج التطبيق إلى إعادة تقديم حالة الزر (يبدو أن الزر يتم الضغط عليه ، ويعود إلى حالته الأصلية عند إصدار زر الماوس الأيسر) ، ويبدأ في معالجة إدخال المستخدم. إذا كانت هذه المهمة تستغرق وقتًا طويلاً في معالجة مدخلات المستخدم ، فلن يتمكن برنامج واحد من الخيوط الواحدة من الاستجابة لإجراءات إدخال المستخدم الأخرى ، مثل النقر على المستخدم في حدث الماوس أو مؤشر الماوس الذي ينقل الحدث الذي تم إرساله من نظام التشغيل ، إلخ.
قابلية التوسع تعني أن البرامج لديها القدرة على الحصول على أداء أعلى من خلال إضافة موارد الحوسبة. تخيل أننا نحتاج إلى ضبط حجم العديد من الصور ، لأن عدد نوى وحدة المعالجة المركزية في جهازنا محدود ، مما يزيد من عدد المواضيع لا يحسن الأداء دائمًا وفقًا لذلك. على العكس من ذلك ، نظرًا لأن المجدول يجب أن يكون مسؤولاً عن إنشاء وإغلاق المزيد من المواضيع ، فإنه سيشغل أيضًا موارد وحدة المعالجة المركزية ، مما قد يقلل من الأداء.
1.1 قاعدة Amdahl
ذكرت الفقرة السابقة أنه في بعض الحالات ، يمكن إضافة موارد الحوسبة الإضافية تحسين الأداء العام للبرنامج. من أجل حساب مقدار تحسين الأداء الذي يمكننا الحصول عليه عندما نضيف موارد إضافية ، من الضروري التحقق من أجزاء البرنامج التي يتم تشغيلها بشكل متسلسل (أو متزامن) وأي أجزاء تعمل بالتوازي. إذا قمنا بتحديد نسبة الكود التي يجب تنفيذها بشكل متزامن إلى B (على سبيل المثال ، فإن عدد خطوط الكود التي يجب تنفيذها بشكل متزامن) وتسجيل إجمالي عدد نوى وحدة المعالجة المركزية مثل N ، ثم وفقًا لقانون Amdahl ، فإن الحد الأعلى لتحسين الأداء الذي يمكن أن نحصل عليه هو:
إذا كان N يميل إلى اللانهاية ، فإن (1-B)/N يتقارب إلى 0. لذلك ، يمكننا تجاهل قيمة هذا التعبير ، وبالتالي فإن عدد بت تحسين الأداء يتقارب إلى 1/B ، حيث يمثل B نسبة التعليمات البرمجية التي يجب تشغيلها بشكل متزامن. إذا كان B يساوي 0.5 ، فهذا يعني أن نصف رمز البرنامج لا يمكن أن يعمل بالتوازي ، وأن المتبادل 0.5 هو 2 ، لذلك حتى لو أضفنا عددًا لا يحصى من مراكز وحدة المعالجة المركزية ، نحصل على تحسين أداء 2x كحد أقصى. لنفترض أننا قمنا بتعديل البرنامج الآن ، وبعد التعديل ، يجب تشغيل 0.25 رمز فقط بشكل متزامن. الآن 1/0.25 = 4 يعني أنه إذا كان برنامجنا يعمل على الأجهزة مع عدد كبير من وحدات المعالجة المركزية ، فسيكون ذلك أسرع بنحو 4 مرات من الأجهزة ذات النواة الواحدة.
من ناحية أخرى ، من خلال قانون AMDAHL ، يمكننا أيضًا حساب نسبة رمز التزامن الذي يجب أن يعتمد البرنامج على هدف التسريع الذي نريد الحصول عليه. إذا كنا نريد تحقيق تسريع 100 مرة ، و 1/100 = 0.01 يعني أن الحد الأقصى لعدد التعليمات البرمجية التي ينفذها برنامجنا لا يمكن أن يتجاوز 1 ٪.
لتلخيص قانون AMDAHL ، يمكننا أن نرى أن الحد الأقصى لتحسين الأداء الذي نحصل عليه من خلال إضافة وحدة المعالجة المركزية الإضافية يعتمد على مدى صغر نسبة البرنامج التي تنفذ جزءًا من الكود بشكل متزامن. على الرغم من أنه في الواقع ، ليس من السهل دائمًا حساب هذه النسبة ، ناهيك عن مواجهة بعض تطبيقات النظام التجاري الكبيرة ، فإن قانون Amdahl يمنحنا مصدر إلهام مهم ، أي يجب أن ننظر في الكود الذي يجب تنفيذه بشكل متزامن ومحاولة تقليل هذا الجزء من الكود.
1.2 تأثير على الأداء
كما يكتب المقال هنا ، فقد أوضحنا أن إضافة المزيد من المواضيع يمكن أن يحسن أداء البرنامج والاستجابة. ولكن من ناحية أخرى ، ليس من السهل تحقيق هذه الفوائد ، ويتطلب أيضًا بعض الأسعار. سيؤثر استخدام المواضيع أيضًا على تحسين الأداء.
أولاً ، يأتي التأثير الأول من وقت إنشاء الخيط. أثناء إنشاء مؤشرات الترابط ، يحتاج JVM إلى التقدم للحصول على الموارد المقابلة من نظام التشغيل الأساسي وتهيئة بنية البيانات في المجدول لتحديد ترتيب مؤشرات ترابط التنفيذ.
إذا كان عدد مؤشرات الترابط هو نفس عدد نوى وحدة المعالجة المركزية ، فسيتم تشغيل كل مؤشر ترابط على قلب بحيث لا يتم مقاطعة في كثير من الأحيان. ولكن في الواقع ، عند تشغيل برنامجك ، سيكون لنظام التشغيل أيضًا بعض عملياته الخاصة التي تحتاج إلى معالجتها بواسطة وحدة المعالجة المركزية. لذلك ، حتى في هذه الحالة ، سيتم مقاطعة خيطك وانتظار نظام التشغيل لاستئناف تشغيله. عندما يتجاوز عدد مؤشرتيك عدد نوى وحدة المعالجة المركزية ، قد يصبح الموقف أسوأ. في هذه الحالة ، سيقوم جدولة عملية JVM بقطع بعض المواضيع للسماح بتنفيذ مؤشرات الترابط الأخرى. عند تبديل مؤشرات الترابط ، يجب حفظ الحالة الحالية لخيط التشغيل بحيث يمكن استعادة حالة البيانات في المرة القادمة التي يتم فيها تشغيلها. ليس ذلك فحسب ، فسوف يقوم المجدول أيضًا بتحديث بنية البيانات الداخلية الخاصة به ، والتي تتطلب أيضًا دورات وحدة المعالجة المركزية. كل هذا يعني أن تبديل السياق بين مؤشرات الترابط يستهلك موارد حوسبة وحدة المعالجة المركزية ، مما يؤدي إلى زيادة الأداء مقارنة مع تلك في حالة واحدة مترابطة.
تأتي النفقات العامة الأخرى التي يتم تقديمها بواسطة برامج متعددة مؤشرات الترابط من حماية الوصول المتزامنة للبيانات المشتركة. يمكننا استخدام الكلمة الرئيسية المتزامنة لحماية المزامنة ، أو يمكننا استخدام الكلمة الرئيسية المتطايرة لمشاركة البيانات بين مؤشرات الترابط المتعددة. إذا أراد أكثر من مؤشر ترابط الوصول إلى بنية بيانات مشتركة ، فسيحدث خلاف. في هذا الوقت ، يحتاج JVM إلى تحديد العملية الأولى والعملية التي تقع. إذا لم يكن مؤشر الترابط الذي سيتم تنفيذه هو مؤشر ترابط التشغيل حاليًا ، فسيحدث تبديل مؤشر الترابط. يحتاج الخيط الحالي إلى الانتظار حتى يكتسب كائن القفل بنجاح. يمكن لـ JVM أن تقرر كيفية أداء هذا "الانتظار". إذا كانت JVM تتوقع أن تكون أقصر من الحصول على الكائن المقفل بنجاح ، فيمكن لـ JVM استخدام أساليب الانتظار العدوانية ، مثل محاولة الحصول على الكائن المقفل باستمرار حتى ينجح. في هذه الحالة ، قد تكون هذه الطريقة أكثر كفاءة ، لأنه لا يزال أسرع لمقارنة تبديل سياق العملية. إن نقل موضوع انتظار إلى قائمة انتظار التنفيذ سيؤدي أيضًا إلى إحضار النفقات العامة الإضافية.
لذلك ، يجب أن نبذل قصارى جهدنا لتجنب تبديل السياق الناجم عن منافسة القفل. سيشرح القسم التالي طريقتان لتقليل حدوث مثل هذه المنافسة.
1.3 مسابقة القفل
كما هو مذكور في القسم السابق ، فإن الوصول المتنافس إلى القفل من قِبل خيطين أو أكثر سيجلب النفقات العامة الإضافية لأن المنافسة تحدث لإجبار الجدولة على إدخال حالة انتظار عدوانية ، أو السماح لها بأداء حالة انتظار ، مما تسبب في مفاتيح سياقين. هناك بعض الحالات التي يمكن فيها تخفيف عواقب المنافسة القفل من خلال:
1. تقليل نطاق الأقفال.
2. تقليل تواتر الأقفال التي تحتاج إلى الحصول عليها ؛
3. حاول استخدام عمليات القفل المتفائلة التي تدعمها الأجهزة بدلاً من المزامنة ؛
4. حاول استخدام المزامنة بأقل قدر ممكن ؛
5. تقليل استخدام ذاكرة التخزين المؤقت للكائن
1.3.1 تقليل مجال التزامن
إذا كان الكود يحتفظ بالقفل لأكثر من اللازم ، فيمكن تطبيق هذه الطريقة الأولى. عادةً ما يمكننا نقل خط واحد أو أكثر من الكود خارج منطقة التزامن لتقليل الوقت الذي يحمل فيه الخيط الحالي القفل. يتم تشغيل الكود الأقل في منطقة التزامن ، وسوف يقوم مؤشر الترابط الحالي في وقت سابق بإصدار القفل ، مما يسمح لخيوط أخرى بالحصول على القفل في وقت سابق. وهذا يتفق مع قانون AMDAHL ، لأن القيام بذلك يقلل من كمية الكود التي يجب تنفيذها بشكل متزامن.
لفهم أفضل ، انظر إلى رمز المصدر التالي:
الطبقة العامة RELOCELOCKDURINT تنفذ Runnable {private static Final int number_of_threads = 5 ؛ الخريطة النهائية الثابتة الخاصة <string ، integer> map = new hashmap <string ، integer> () ؛ public void run () {for (int i = 0 ؛ i <10000 ؛ i ++) {synchronized (map) {uuid randomuuid = uuid.randomuuid () ؛ قيمة عدد صحيح = integer.valueof (42) ؛ مفتاح السلسلة = randomuuid.toString () ؛ map.put (المفتاح ، القيمة) ؛ } thread.yield () ؛ }} public static void main (string [] args) remrows interruptedException {thread [] threads = new thread [number_of_threads] ؛ لـ (int i = 0 ؛ i <number_of_threads ؛ i ++) {threads [i] = new thread (new reducelockduration ()) ؛ } long startMillis = System.CurrentTimeMillis () ؛ لـ (int i = 0 ؛ i <number_of_threads ؛ i ++) {threads [i] .start () ؛ } لـ (int i = 0 ؛ i <number_of_threads ؛ i ++) {threads [i] .join () ؛ } system.out.println ((System.CurrentTimeMillis ()-startMillis)+"MS") ؛ }}في المثال أعلاه ، ندع خمسة مؤشرات ترابط تتنافس على الوصول إلى مثيل الخريطة المشتركة. لكي يتمكن مؤشر ترابط واحد فقط من الوصول إلى مثيل MAP في نفس الوقت ، نضع تشغيل إضافة المفتاح/القيمة إلى الخريطة في كتلة التعليمات البرمجية المحمية المتزامنة. عندما ننظر بعناية إلى هذا الرمز ، يمكننا أن نرى أن جمل الكود القليلة التي تحسب المفتاح والقيمة لا تحتاج إلى تنفيذها بشكل متزامن. ينتمي المفتاح والقيمة فقط إلى مؤشر الترابط الذي ينفذ هذا الرمز حاليًا. إنه ذو معنى فقط للخيط الحالي ولن يتم تعديله بواسطة مؤشرات ترابط أخرى. لذلك ، يمكننا نقل هذه الجمل من حماية المزامنة. على النحو التالي:
public void run () {for (int i = 0 ؛ i <10000 ؛ i ++) {uuid randomuuid = uuid.randomuuid () ؛ قيمة عدد صحيح = integer.valueof (42) ؛ مفتاح السلسلة = randomuuid.toString () ؛ Synchronized (map) {map.put (مفتاح ، القيمة) ؛ } thread.yield () ؛ }}تأثير تقليل رمز التزامن قابل للقياس. على الجهاز الخاص بي ، تم تخفيض وقت تنفيذ البرنامج بأكمله من 420 مللي ثانية إلى 370 مللي ثانية. ألقِ نظرة ، ما عليك سوى نقل ثلاثة أسطر من الكود من كتلة حماية المزامنة يمكن أن يقلل من وقت تشغيل البرنامج بنسبة 11 ٪. يتمثل رمز Thread.yield () في تحفيز تبديل سياق مؤشر الترابط ، لأن هذا الرمز سيخبر JVM أن مؤشر الترابط الحالي يريد تسليم موارد الحساب المستخدمة حاليًا بحيث يمكن تشغيل مؤشرات الترابط الأخرى التي تنتظر تشغيلها. سيؤدي ذلك أيضًا إلى مزيد من منافسة القفل ، لأنه إذا لم يكن هذا هو الحال ، فسيشغل مؤشر الترابط جوهرًا معينًا لفترة أطول ، مما يقلل من تبديل سياق الخيط.
1.3.2 قفل الانقسام
هناك طريقة أخرى لتقليل منافسة القفل وهي نشر كتلة من التعليمات البرمجية المحمية في عدد من كتل الحماية الأصغر. ستعمل هذه الطريقة إذا كنت تستخدم قفلًا في البرنامج لحماية كائنات مختلفة متعددة. لنفترض أننا نريد حساب بعض البيانات من خلال برنامج ما ، وتنفيذ فئة عدد بسيطة للاحتفاظ بمؤشرات إحصائية مختلفة متعددة ، وتمثيلها مع متغير عدد أساسي (نوع طويل). نظرًا لأن برنامجنا متعدد الخيوط ، نحتاج إلى حماية العمليات التي تصل إلى هذه المتغيرات بشكل متزامن ، لأن هذه الإجراءات تأتي من مؤشرات ترابط مختلفة. أسهل طريقة لتحقيق ذلك هي إضافة الكلمة الرئيسية المتزامنة إلى كل وظيفة تصل إلى هذه المتغيرات.
فئة ثابتة عامة CounterOnelock تنفس {Private Long CustomerCount = 0 ؛ شحن طويل خاص = 0 ؛ perix void styrementCustomer () {customercount ++ ؛ } perized void suckedshipping () {shippingcount ++ ؛ } متزامن عام طويل getCustomerCount () {return customerCount ؛ } متزامن عام Long GetshipPingCount () {return ShippingCount ؛ }}هذا يعني أن كل تعديل لهذه المتغيرات سيؤدي إلى قفل مثيلات مضادة أخرى. إذا أرادت مؤشرات الترابط الأخرى استدعاء طريقة الزيادة على متغير آخر مختلف ، فيمكنها فقط الانتظار حتى يتم إصدار عنصر التحكم السابق قبل أن تتاح لهم فرصة إكماله. في هذه الحالة ، سيؤدي استخدام حماية متزامنة منفصلة لكل متغير مختلف إلى تحسين كفاءة التنفيذ.
الفئة الثابتة العامة CONSESSERATELOCK تنفذ {private static final object customerlock = new Object () ؛ كائن نهائي ثابت خاص chargetlock = كائن جديد () ؛ خاص طويل customercount = 0 ؛ شحن طويل خاص = 0 ؛ public void sturementCustomer () {synchronized (customerlock) {customerCount ++ ؛ }} public void gutrementshipping () {synchronized (shippinglock) {shippingCount ++ ؛ }} public getCustomerCount () {synchronized (customerlock) {return customercount ؛ }} public long getshippingcount () {synchronized (shippinglock) {return shippingCount ؛ }}}يقدم هذا التنفيذ كائنًا متزامنًا منفصلًا لكل مقياس عدد. لذلك ، عندما يرغب مؤشر ترابط في زيادة عدد العملاء ، يجب أن ينتظر مؤشر ترابط آخر يزيد من إكمال عدد العملاء ، بدلاً من انتظار مؤشر ترابط آخر يزيد من عدد الشحن لإكماله.
باستخدام الفئات التالية ، يمكننا بسهولة حساب تحسينات الأداء التي جلبتها أقفال الانقسام.
قم بتنفيذ أقفال الفئة العامة Runnable {private static Final int number_of_threads = 5 ؛ عداد خاص عداد الواجهة العامة {void sturementCustomer () ؛ باطل زيادة () ؛ طويل getCustomerCount () ؛ long getshippingcount () ؛ } فئة ثابتة من الفئة الثابتة CounterOnelock Counter {...} الفئة الثابتة العامة CounterSeparatelock تنفس Counter {...} Locksplitting (Counter Counter) {this.counter = counter ؛ } public void run () {for (int i = 0 ؛ i <100000 ؛ i ++) {if (threadlocalrandom.current (). nextBoolean ()) {counter.incrementCustomer () ؛ } آخر {counter.incrementshipping () ؛ }}} static void main (string [] args) remrows interruptedException {thread [] threads = new thread [number_of_threads] ؛ عداد العداد = جديد counteronelock () ؛ لـ (int i = 0 ؛ i <number_of_threads ؛ i ++) {threads [i] = new thread (locksplitting (counter)) ؛ } long startMillis = System.CurrentTimeMillis () ؛ لـ (int i = 0 ؛ i <number_of_threads ؛ i ++) {threads [i] .start () ؛ } لـ (int i = 0 ؛ i <number_of_threads ؛ i ++) {threads [i] .join () ؛ } system.out.println ((System.CurrentTimeMillis () - startMillis) + "MS") ؛ }}على الجهاز الخاص بي ، تستغرق طريقة تنفيذ قفل واحد 56 مللي ثانية في المتوسط ، وتنفيذ قفلتين منفصلتين هو 38 مللي ثانية. يتم تخفيض الوقت المستهلكة بنحو 32 ٪.
هناك طريقة أخرى للتحسين وهي أنه يمكننا حتى الذهاب إلى أبعد من ذلك لحماية القراءة والكتابة بأقفال مختلفة. يوفر فئة العداد الأصلية طرقًا لقراءة وكتابة مؤشرات العد على التوالي. ومع ذلك ، في الواقع ، لا تتطلب عمليات القراءة حماية المزامنة. يمكن أن نطمئن إلى أن مؤشرات الترابط المتعددة يمكنها قراءة قيمة المؤشر الحالي بالتوازي. في الوقت نفسه ، يجب أن تكون عمليات الكتابة محمية بشكل متزامن. توفر حزمة java.util.concurrent تنفيذًا لواجهة ReadWritelock ، والتي يمكن أن تحقق هذا التمييز بسهولة.
يحافظ تطبيق ReentRanTreadWritelock على قفلتين مختلفتين ، يحمي أحدهما عملية القراءة والآخر يحمي عملية الكتابة. كلا الأقفال لديها عمليات لاكتساب الأقفال وإصدارها. لا يمكن الحصول على قفل الكتابة بنجاح إلا عندما لا يكتسب أحد قفل قراءة. على العكس ، طالما لم يتم الحصول على قفل الكتابة ، يمكن الحصول على قفل القراءة بواسطة مؤشرات ترابط متعددة في نفس الوقت. لإظهار هذا النهج ، تستخدم فئة العداد التالية ReadWritelock ، على النحو التالي:
الطبقة الثابتة العامة CounterReadWritelock الأدوات Counter {Private Final REENTRANTREADWRITELOCK = جديد reentrantreadwritelock () ؛ قفل نهائي خاص customerwritelock = customerlock.writelock () ؛ قفل نهائي خاص customerreadlock = customerlock.readlock () ؛ REENTRANTREADWRITELOCK SHARGELCH = جديد reentrantreadwritelock () ؛ قفل نهائي خاص shippingwritelock = shippinglock.writelock () ؛ قفل نهائي خاص shippingReadlock = shippinglock.readlock () ؛ خاص طويل customercount = 0 ؛ شحن طويل خاص = 0 ؛ public void sturementCustomer () {customerwritelock.lock () ؛ CustomerCount ++ ؛ customerwritelock.unlock () ؛ } public void gutrementshipping () {shippingwritelock.lock () ؛ ShippingCount ++ ؛ ShippingWritelock.unlock () ؛ } public getCustomerCount () {customerreadlock.lock () ؛ عدد طويل = customercount ؛ customerReadlock.unlock () ؛ عدد العائد } public getshippingcount () {shippingReadlock.lock () ؛ العد الطويل = الشحن. ShippingReadlock.unlock () ؛ عدد العائد }}جميع عمليات القراءة محمية بواسطة أقفال القراءة ، وجميع عمليات الكتابة محمية بواسطة أقفال الكتابة. إذا كانت عمليات القراءة التي تم تنفيذها في البرنامج أكبر بكثير من عمليات الكتابة ، فيمكن أن يؤدي هذا التنفيذ إلى تحسينات في الأداء أكبر من القسم السابق لأنه يمكن تنفيذ عمليات القراءة بشكل متزامن.
1.3.3 قفل الفصل
يوضح المثال أعلاه كيفية فصل قفل واحد في أقفال منفصلة متعددة بحيث يمكن لكل مؤشر ترابط الحصول على قفل الكائن الذي يرغب في تعديله. ولكن من ناحية أخرى ، تزيد هذه الطريقة أيضًا من تعقيد البرنامج ، وقد تتسبب في حدوث طريق مسدود إذا تم تنفيذها بشكل غير لائق.
يعد قفل الانفصال طريقة مشابهة لقفل انفصال ، ولكن قفل الانفصال هو إضافة قفل لحماية مقتطفات أو كائنات مختلفة ، في حين أن قفل الانفصال هو استخدام قفل مختلف لحماية نطاقات القيم المختلفة. concurrenthashmap في JDK's Java.Util.Current تستخدم هذه الفكرة لتحسين أداء البرامج التي تعتمد بشكل كبير على hashmap. فيما يتعلق بالتنفيذ ، يستخدم ConcurrentHashMap 16 قفلًا مختلفًا داخليًا ، بدلاً من تغليف هاشماب محمية بشكل متزامن. كل من الأقفال الـ 16 مسؤولة عن حماية الوصول المتزامن إلى عُشر بتات الجرافة (الدلاء). وبهذه الطريقة ، عندما تريد مؤشرات الترابط المختلفة إدراج مفاتيح في شرائح مختلفة ، سيتم حماية العمليات المقابلة بواسطة أقفال مختلفة. ولكنه سيؤدي أيضًا إلى تحقيق بعض المشكلات السيئة ، مثل الانتهاء من عمليات معينة ، يتطلب الآن أقفال متعددة بدلاً من قفل واحد. إذا كنت ترغب في نسخ الخريطة بأكملها ، فيجب الحصول على جميع الأقفال الـ 16 لإكمالها.
1.3.4 العملية الذرية
هناك طريقة أخرى لتقليل منافسة القفل وهي استخدام العمليات الذرية ، والتي ستوضح المبادئ في مقالات أخرى. توفر حزمة java.util.concurrent فئات مغلفة ذرية لبعض أنواع البيانات الأساسية الشائعة الاستخدام. يعتمد تنفيذ فئة التشغيل الذرية على وظيفة "تقليب المقارنة" (CAS) التي يوفرها المعالج. ستقوم عملية CAS بإجراء عملية تحديث فقط عندما تكون قيمة السجل الحالي هي نفس القيمة القديمة التي توفرها العملية.
يمكن استخدام هذا المبدأ لزيادة قيمة المتغير بطريقة متفائلة. إذا كان موضوعنا يعرف القيمة الحالية ، فسوف يحاول استخدام عملية CAS لإجراء عملية الزيادة. إذا قامت مؤشرات الترابط الأخرى بتعديل قيمة المتغير خلال هذه الفترة ، فإن ما يسمى القيمة الحالية التي يوفرها مؤشر الترابط تختلف عن القيمة الحقيقية. في هذا الوقت ، يحاول JVM استعادة القيمة الحالية والمحاولة مرة أخرى ، مع تكرارها مرة أخرى حتى تنجح. على الرغم من أن عمليات الحلقات ستضيع بعض دورات وحدة المعالجة المركزية ، إلا أن فائدة القيام بذلك هي أننا لا نحتاج إلى أي شكل من أشكال التحكم في المزامنة.
يستخدم تنفيذ فئة العداد أدناه العمليات الذرية. كما ترون ، لا يوجد رمز متزامن مستخدم.
الأدوات المضادة للطبقة العامة الثابتة COUNTER {private AtomicLong CustomerCount = new Atomiclong () ؛ خاص asharlingcount Atomiclong = new Atomiclong () ؛ public void sturementCustomer () {customercount.incrementandget () ؛ } public void gutrementshipping () {shippingcount.incrementandget () ؛ } public getCustomerCount () {return customercount.get () ؛ } public getshippingcount () {return requartcount.get () ؛ }}بالمقارنة مع فئة CounterSeparatelock ، تم تخفيض متوسط وقت التشغيل من 39 مللي ثانية إلى 16ms ، وهو حوالي 58 ٪.
1.3.5 تجنب مقاطع رمز الساخنة
يسجل تطبيق القائمة النموذجية عدد العناصر الواردة في القائمة نفسها من خلال الحفاظ على متغير في المحتوى. في كل مرة يتم فيها حذف عنصر ما أو إضافته من القائمة ، ستتغير قيمة هذا المتغير. إذا تم استخدام القائمة في تطبيق واحد متخلف ، فإن هذه الطريقة مفهومة. في كل مرة تقوم فيها بالاتصال بحجم () ، يمكنك فقط إرجاع القيمة بعد الحساب الأخير. إذا لم يتم الحفاظ على متغير العد هذا داخليًا حسب القائمة ، فإن كل مكالمة إلى Size () ستؤدي إلى إعادة تدوين قائمة وحساب عدد العناصر.
ستصبح طريقة التحسين هذه التي تستخدمها العديد من هياكل البيانات مشكلة عندما تكون في بيئة متعددة الخيوط. لنفترض أننا نشارك قائمة بين مؤشرات الترابط المتعددة ، ومواضيع متعددة في وقت واحد إضافة أو حذف العناصر في القائمة ، والاستعلام عن الطول الكبير. في هذا الوقت ، يصبح متغير العد الداخلي مورد مشترك ، لذلك يجب معالجتها بشكل متزامن. لذلك ، تصبح متغيرات العد نقطة ساخنة في تطبيق القائمة بأكملها.
يوضح مقتطف الكود التالي هذه المشكلة:
الفئة الثابتة العامة carrepositorywithCounter تنفذ carrepository {private map <String ، Car> cars = new hashmap <string ، car> () ؛ الخريطة الخاصة <string ، Car> Trucks = New HashMap <String ، Car> () ؛ كائن خاص carcountsync = كائن جديد () ؛ private int carcount = 0 ؛ public void addcar (Car Car) {if (car.getLiseSplate (). startswith ("c")) {synchronized (cars) {car idecar = cars.get (car.getlicenceplate ()) ؛ if (edudcar == null) {cars.put (car.getLeasesplate () ، car) ؛ متزامن (carcountsync) {carcount ++ ؛ }}}} آخر {synchronized (TRUCKS) {Car eventCar = Trucks.get (car.getlicenceplate ()) ؛ if (eventCar == null) {trucks.put (car.getLeasesplate () ، car) ؛ متزامن (carcountsync) {carcount ++ ؛ }}}}}} public int getCarcount () {synchronized (carcountsync) {return carcount ؛ }}}يحتوي التنفيذ أعلاه لـ Carrepository على متغيرين في القائمة ، ويستخدم أحدهما لوضع عنصر غسيل السيارات ويستخدم الآخر لوضع عنصر الشاحنة. في الوقت نفسه ، يوفر طريقة للاستعلام عن الحجم الإجمالي لهاتين القائمتين. طريقة التحسين المستخدمة هي أنه في كل مرة يضاف فيها عنصر السيارة ، سيتم زيادة قيمة متغير العدد الداخلي. في الوقت نفسه ، يتم حماية العملية المتزايدة بواسطة متزامن ، وينطبق الشيء نفسه على إرجاع قيمة العدد.
لتجنب هذا النفقات العامة لمزامنة الكود الإضافية ، راجع تنفيذ آخر لـ Carrepository أدناه: لم يعد يستخدم متغيرًا داخليًا ، ولكن يحسب هذه القيمة في الوقت الفعلي في طريقة إعادة العدد الإجمالي للسيارات. على النحو التالي:
الفئة الثابتة العامة carrepositorywithoutCounter تنفذ carrepository {private Map <String ، Car> cars = new HashMap <String ، Car> () ؛ الخريطة الخاصة <string ، Car> Trucks = New HashMap <String ، Car> () ؛ public void addcar (Car Car) {if (car.getLiseSplate (). startswith ("c")) {synchronized (cars) {car idecar = cars.get (car.getlicenceplate ()) ؛ if (edudcar == null) {cars.put (car.getLeasesplate () ، car) ؛ }}} else {synchronized (Trucks) {Car eventCar = trucks.get (car.getlicenceplate ()) ؛ if (eventCar == null) {trucks.put (car.getLeasesplate () ، car) ؛ }}}}} public int getCarcount () {synchronized (cars) {synchronized (Trucks) {return cars.size () + trucks.size () ؛ }}}}الآن ، فقط في طريقة getCarcount () ، يحتاج الوصول إلى القائمتين إلى حماية المزامنة. مثل التنفيذ السابق ، فإن النفقات العامة المزامنة في كل مرة يتم فيها إضافة عنصر جديد لم يعد موجودًا.
1.3.6 تجنب إعادة استخدام كائن ذاكرة التخزين المؤقت
في الإصدار الأول من Java VM ، فإن النفقات العامة لاستخدام الكلمة الرئيسية الجديدة لإنشاء كائنات جديدة مرتفع نسبيًا ، لذلك اعتاد العديد من المطورين على استخدام وضع إعادة استخدام الكائن. من أجل تجنب إنشاء الأشياء المتكررة مرارًا وتكرارًا ، يحافظ المطورون على تجمع عازلة. بعد كل إنشاء مثيلات كائن ، يمكن حفظها في تجمع العازلة. في المرة القادمة التي تحتاج المواضيع الأخرى إلى استخدامها ، يمكن استردادها مباشرة من تجمع العازلة.
للوهلة الأولى ، هذه الطريقة معقولة للغاية ، ولكن هذا النمط يمكن أن يسبب مشاكل في التطبيقات متعددة التربعات. نظرًا لمشاركة مجموعة المخزن المؤقت للكائنات بين مؤشرات الترابط المتعددة ، فإن عمليات جميع مؤشرات الترابط عند الوصول إلى الكائنات فيها تحتاج إلى حماية متزامنة. النفقات العامة لهذا التزامن أكبر من إنشاء الكائن نفسه. بالطبع ، سيؤدي إنشاء الكثير من الكائنات إلى زيادة عبء جمع القمامة ، ولكن حتى مع الأخذ في الاعتبار ، لا يزال من الأفضل تجنب تحسينات الأداء التي جلبتها مزامنة الكود بدلاً من استخدام تجمع ذاكرة التخزين المؤقت للكائنات.
توضح مخططات التحسين الموضحة في هذه المقالة مرة أخرى أنه يجب تقييم كل طريقة محتملة للتحسين بعناية عند تطبيقها بالفعل. يبدو أن حل التحسين غير الناضج منطقي على السطح ، ولكن في الواقع من المحتمل أن يصبح عنق الزجاجة الأداء بدوره.