9 أخطاء يجب تجنبها أثناء البرمجة بلغة جافا
ما الأخطاء التي يجب تجنبها أثناء البرمجة بلغة جافا؟ في المقالة التالية نجيب على هذا السؤال.
اقرأ الجزء الأول من سلسلة مدونتنا المخصصة للتزامن في Java. في المقالة التالية سوف نلقي نظرة فاحصة على الاختلافات بين مؤشرات الترابط والعمليات، وتجمعات مؤشرات الترابط، والمنفذين وغيرها الكثير!
بشكل عام، يكون نهج البرمجة التقليدي متسلسلًا. كل شيء في البرنامج يحدث خطوة بخطوة في كل مرة.
ولكن، في الواقع، فإن التوازي هو الطريقة التي يعمل بها العالم بأسره - إنها القدرة على تنفيذ أكثر من مهمة في وقت واحد.
لمناقشة مواضيع متقدمة مثل التزامن في جافا أو تعدد مؤشرات الترابط، علينا أن نتفق على بعض التعريفات المشتركة لنتأكد من أننا على نفس الصفحة.
لنبدأ بالأساسيات. في العالم غير المتسلسل، لدينا نوعان من ممثلي التزامن: العمليات و
الخيوط. العملية هي مثيل للبرنامج قيد التشغيل. وعادةً ما تكون معزولة عن العمليات الأخرى.
نظام التشغيل مسؤول عن تخصيص الموارد لكل عملية. وعلاوة على ذلك، فإنه يعمل كموصل
الجداول الزمنية والتحكم فيها.
الخيط هو نوع من العمليات ولكن على مستوى أقل، ولذلك يُعرف أيضًا باسم الخيط الخفيف. يمكن تشغيل عدة خيوط في عملية واحدة
عملية. هنا يعمل البرنامج كجدول زمني ومراقب للخيوط. بهذه الطريقة تظهر البرامج الفردية
مهام متعددة في نفس الوقت.
الفرق الأساسي بين الخيوط والعمليات هو مستوى العزل. العملية لديها مجموعة خاصة بها من
الموارد، بينما يشارك مؤشر الترابط البيانات مع مؤشرات ترابط أخرى. قد يبدو أنه نهج معرض للخطأ وهو كذلك بالفعل. بالنسبة لـ
الآن، دعنا لا نركز على ذلك لأنه خارج نطاق هذا المقال.
العمليات، الخيوط - حسناً... ولكن ما هو التزامن بالضبط؟ التزامن يعني أنه يمكنك تنفيذ مهام متعددة في نفس الوقت
الوقت. لا يعني ذلك أن هذه المهام يجب أن تعمل في وقت واحد - وهذا هو معنى التوازي. كونكورينيك في جافاي أيضًا لا
تتطلب أن يكون لديك وحدات معالجة مركزية متعددة أو حتى أنوية متعددة. يمكن تحقيق ذلك في بيئة أحادية النواة من خلال الاستفادة من
تبديل السياق.
من المصطلحات المرتبطة بالتزامن هو تعدد مؤشرات الترابط. هذه ميزة للبرامج تسمح لها بتنفيذ عدة مهام في وقت واحد. لا تستخدم كل البرامج هذا النهج ولكن البرامج التي تستخدمه يمكن تسميتها متعددة الخيوط.
نحن جاهزون تقريبًا للبدء، فقط تعريف آخر. عدم التزامن يعني أن البرنامج ينفذ عمليات غير متوقفة.
فهو يبدأ مهمة ما ثم يمضي قدماً في أشياء أخرى أثناء انتظار الاستجابة. وعندما يحصل على الاستجابة، يمكنه التفاعل معها.
بشكل افتراضي، كل تطبيق جافا يعمل في عملية واحدة. في هذه العملية، يوجد مؤشر ترابط واحد مرتبط بـ الرئيسي()
طريقة
تطبيق. ومع ذلك، كما ذكرنا، من الممكن الاستفادة من آليات الخيوط المتعددة ضمن
البرنامج.
الخيط
هو جافا الذي يحدث فيه السحر. هذا هو تمثيل الكائن للخيط المذكور سابقًا. إلى
إنشاء مؤشر ترابط خاص بك، يمكنك تمديد الخيط
الفصل. ومع ذلك، فإنه ليس نهجاً موصى به. الخيوط
يجب استخدامها كآلية لتشغيل المهمة. المهام هي أجزاء من الكود التي نريد تشغيلها في وضع متزامن. يمكننا تعريفها باستخدام قابل للتشغيل
الواجهة.
لكن كفى نظريات، دعونا نضع شفرتنا في مكانها الصحيح.
افترض أن لدينا مصفوفتين من الأعداد. لكل مصفوفة، نريد معرفة مجموع الأعداد في مصفوفة. لنفترض أن
تظاهر بوجود الكثير من هذه المصفوفات وكل منها كبير نسبيًا. في مثل هذه الظروف، نريد الاستفادة من التزامن وجمع كل مصفوفة كمهمة منفصلة.
int[] a1 = {1, 2, 3, 3, 4, 5, 5, 6, 7, 8, 9, 10};
int[] a2 = {10، 10، 10، 10، 10، 10، 10، 10، 10، 10، 10};
int[] a3 = {3، 4، 4، 3، 3، 4، 3، 4، 4، 2، 1، 3، 3، 7};
المهمة القابلة للتشغيل1 = () -> { {
int sum = Arrays.stream(a1).sum();
System.out.println("1. المجموع هو: " + المجموع);
};
المهمة القابلة للتشغيل2 = () -> { {
int sum = Arrays.stream(a2).sum();
System.out.println("2. المجموع هو: " + المجموع);
};
المهمة القابلة للتشغيل3 = () -> { {
int sum = Arrays.stream(a3).sum();
System.out.println("3. المجموع هو: " + المجموع);
};
مؤشر ترابط جديد(task1).start();
مؤشر ترابط جديد(task2).start()؛
مؤشر ترابط جديد(task3).start()؛ مؤشر ترابط جديد(task3).start();
كما ترى من الكود أعلاه قابل للتشغيل
واجهة وظيفية. تحتوي على طريقة تجريدية واحدة تشغيل()
بدون حجج. إن قابل للتشغيل
يجب أن تُنفذ الواجهة من قبل أي صنف يُفترض أن تكون مثيلاته
أعدم بواسطة خيط.
بمجرد تحديد مهمة يمكنك إنشاء مؤشر ترابط لتشغيلها. يمكن تحقيق ذلك عبر مؤشر ترابط جديد()
المُنشئ الذي
يأخذ قابل للتشغيل
كحجة لها.
الخطوة الأخيرة هي بدء()
مؤشر ترابط تم إنشاؤه حديثًا. يوجد في واجهة برمجة التطبيقات أيضًا تشغيل()
الطرق في قابل للتشغيل
وفيالخيط
. ومع ذلك، هذه ليست طريقة للاستفادة من التزامن في Java. يؤدي الاستدعاء المباشر لكل من هذه الطرق إلى
تنفيذ المهمة في نفس مؤشر ترابط الرئيسي()
طريقة التشغيل.
عندما يكون هناك الكثير من المهام، فإن إنشاء مؤشر ترابط منفصل لكل منها ليس فكرة جيدة. إنشاء مؤشر ترابط الخيط
هو
عملية ثقيلة الوزن ومن الأفضل بكثير إعادة استخدام الخيوط الموجودة بدلاً من إنشاء خيوط جديدة.
عندما يقوم برنامج ما بإنشاء العديد من سلاسل الرسائل قصيرة الأجل، فمن الأفضل استخدام تجمع سلاسل رسائل. يحتوي تجمع مؤشرات الترابط على عدد من
خيوط جاهزة للتشغيل ولكنها غير نشطة حاليًا. إعطاء قابل للتشغيل
إلى التجمع يتسبب في قيام أحد الخيوط باستدعاءتشغيل()
طريقة المعطى قابل للتشغيل
. بعد إكمال المهمة يظل مؤشر الترابط موجودًا وفي حالة خمول.
حسنًا، لقد فهمت - تفضل تجمع الخيوط بدلاً من الإنشاء اليدوي. ولكن كيف يمكنك الاستفادة من تجمعات الخيوط؟ إن المنفذون
على عدد من طرائق المصنع الثابتة لإنشاء تجمعات مؤشرات الترابط. على سبيل المثال newCachedThredPool()
ينشئ
تجمع يتم فيه إنشاء خيوط جديدة حسب الحاجة ويتم الاحتفاظ بالخيوط الخاملة لمدة 60 ثانية. في المقابل,newFixedThreadPool()
يحتوي على مجموعة ثابتة من الخيوط، حيث يتم الاحتفاظ بالخيوط الخاملة إلى أجل غير مسمى.
دعونا نرى كيف يمكن أن تعمل في مثالنا. الآن لا نحتاج إلى إنشاء سلاسل رسائل يدويًا. بدلًا من ذلك، علينا إنشاءخدمة المنفذ
والتي توفر مجموعة من الخيوط. ثم يمكننا تعيين المهام إليها. الخطوة الأخيرة هي إغلاق الخيط
لتجنب تسرب الذاكرة. تبقى بقية الشيفرة السابقة كما هي.
ExecutorSecutorService execor = Executors.newCachedThreadPool();
المنفذ.submit(task1);
execor.submit(task2)؛
execor.submit(task3);
إيقاف التشغيل();
قابل للتشغيل
تبدو طريقة أنيقة لإنشاء مهام متزامنة ولكنها تعاني من عيب رئيسي واحد. لا يمكنها إرجاع أي
القيمة. علاوة على ذلك، لا يمكننا تحديد ما إذا كانت المهمة قد اكتملت أم لا. كما أننا لا نعرف ما إذا كانت قد اكتملت أم لا
بشكل طبيعي أو استثنائي. والحل لهذه العلل هو قابل للاستدعاء
.
قابل للاستدعاء
مشابه ل قابل للتشغيل
بطريقة ما يلتف أيضًا على المهام غير المتزامنة. الفرق الرئيسي هو أنها قادرة على
إرجاع قيمة. يمكن أن تكون القيمة المُرجَعة من أي نوع (غير بدائي) مثل قابل للاستدعاء
الواجهة عبارة عن نوع معلمات.قابل للاستدعاء
هي واجهة وظيفية تحتوي على استدعاء()
التي يمكن أن ترمي طريقة الاستثناء
.
والآن لنرى كيف يمكننا الاستفادة من قابل للاستدعاء
في مشكلتنا المصفوفة.
int[] a1 = {1, 2, 3, 3, 4, 5, 5, 6, 7, 8, 9, 10};
int[] a2 = {10، 10، 10، 10، 10، 10، 10، 10، 10، 10، 10};
int[] a3 = {3، 4، 4، 3، 3، 4، 3، 4، 4، 2، 1، 3، 3، 7};
Callable task1 = () -> Arrays.stream(a1).sum();
Callable task2 = () = () -> Arrays.stream(a2).sum()؛
Callable task3 = () -> Arrays.stream(a3).sum()؛
ExecutorSecutorService execor = Executors.newCachedThreadPool();
Future Future1 = execor.submit(task1);
المستقبل المستقبل2 = executor.submit(task2);
Future Future3 = executor.submit(task3)؛ والمستقبلالمستقبل3 = executor.submit(task3);
System.out.println("1. المجموع هو: " + future1.get());
System.out.println("2. المجموع هو: " + future2.get())؛
System.out.println("3. المجموع هو: " + future3.get())؛ System.out.println("3. المجموع هو: " + future3.get());
المنفذ.إيقاف التشغيل();
حسناً، يمكننا أن نرى كيف قابل للاستدعاء
ثم يتم إنشاؤه ثم إرساله إلى خدمة المنفذ
. ولكن ما هو المستقبل
?المستقبل
يعمل كجسر بين الخيوط. يتم إنتاج مجموع كل مصفوفة في خيط منفصل ونحتاج إلى طريقة لـ
الحصول على هذه النتائج إلى الرئيسي()
.
لاسترداد النتيجة من المستقبل
نحتاج إلى الاتصال ب الحصول على ()
الطريقة. هنا يمكن أن يحدث أحد أمرين. أولاً، يمكن أن يحدث
نتيجة العملية الحسابية التي أجراها قابل للاستدعاء
متاحة. ثم نحصل عليها على الفور. ثانيًا، النتيجة ليست
جاهز بعد. في هذه الحالة الحصول على ()
حتى تصبح النتيجة متاحة.
المشكلة في المستقبل
هو أنه يعمل في "نموذج الدفع". عند استخدام المستقبل
عليك أن تكون مثل الرئيس الذي
يسأل باستمرار: "هل أنجزت مهمتك؟ هل هي جاهزة؟" حتى تقدم نتيجة. العمل تحت ضغط مستمر هو
باهظة الثمن. أفضل طريقة أفضل بكثير هي أن تطلب المستقبل
ما يجب القيام به عندما يكون جاهزًا لمهمته. لسوء الحظالمستقبل
لا يمكن أن تفعل ذلك ولكن كومبيوتابل فيوتشر
يمكن.
كومبيوتابل فيوتشر
يعمل في "نموذج السحب". يمكننا إخبارها بما يجب أن تفعله بالنتيجة عندما تكمل مهامها. إنه
هو مثال على النهج غير المتزامن.
كومبيوتابل فيوتشر
يعمل بشكل مثالي مع قابل للتشغيل
ولكن ليس مع قابل للاستدعاء
. بدلاً من ذلك، من الممكن توفير مهمة إلىكومبيوتابل فيوتشر
في شكل المورد
.
لنرى كيف يرتبط ما سبق بمشكلتنا.
int[] a1 = {1, 2, 3, 3, 4, 5, 5, 6, 7, 8, 9, 10};
int[] a2 = {10، 10، 10، 10، 10، 10، 10، 10، 10، 10، 10};
int[] a3 = {3، 4، 4، 3، 3، 4، 3، 4، 4، 2، 1، 3، 3، 7};
CompletableFuture.supplyAsync(() -> Arrays.stream(a1).sum())
.thenAccept(System.out::println);
CompletableFuture.supplyAsync(() -> Arrays.stream(a2).sum()))
.thenAccept(System.out::println);
CompletableFuture.supplyAsync(() -> Arrays.stream(a3).sum()))
.thenAccept(System.out::println);
أول ما يلفت انتباهك هو مدى قصر هذا الحل. إلى جانب ذلك، يبدو أيضاً أنيقاً ومرتباً.
المهمة إلى اكتمالالمستقبل
يمكن توفيرها بواسطة توريد مزامنة()
التي تأخذ المورد
أو بواسطة تشغيل مزامنة()
أن
يأخذ قابل للتشغيل
. يتم تعريف رد النداء - وهو جزء من التعليمات البرمجية التي يجب تشغيلها عند اكتمال المهمة - بواسطة ثم قبول()
الطريقة.
جافا يوفر الكثير من الأساليب المختلفة للتزامن. في هذه المقالة، بالكاد تطرقنا إلى هذا الموضوع.
ومع ذلك، فقد غطينا أساسيات الخيط
, قابل للتشغيل
, قابل للاستدعاء
و قابل للاستدعاء في المستقبل
وهو ما يطرح نقطة جيدة
لمزيد من التحقيق في الموضوع.