مقدمة
لقد تم دمج خاصية Multithreading في لغة Java منذ أيامها الأولى. لذلك دعونا نلقي نظرة سريعة على ماهية تعدد العمليات.
لنأخذ الدرس الرسمي من Oracle كنقطة بداية: "
الدرس: تطبيق "Hello World! ". دعونا نغير رمز تطبيق Hello World الخاص بنا قليلًا إلى ما يلي:
class HelloWorldApp {
public static void main(String[] args) {
System.out.println("Hello, " + args[0]);
}
}
args
عبارة عن مجموعة من معلمات الإدخال التي يتم تمريرها عند بدء تشغيل البرنامج. لنحفظ هذا الرمز في ملف باسم يطابق اسم الفئة والامتداد
.java
. لنقم بالتجميع باستخدام الأداة المساعدة
javac :
javac HelloWorldApp.java
بعد ذلك، قم باستدعاء الكود الخاص بنا مع بعض المعلمات، على سبيل المثال، Roger:
java HelloWorldApp Roger
الكود الخاص بنا به الآن عيب خطير. إذا لم نمرر أي وسيطة (أي فقط قمنا بتنفيذ Java HelloWorldApp)، فسوف نحصل على خطأ:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 0
at HelloWorldApp.main(HelloWorldApp.java:3)
حدث استثناء (أي خطأ) في موضوع اسمه
main
. اتضح أن هناك نوعًا من المواضيع في جافا؟ هذا هو المكان الذي تبدأ رحلتنا.
جافا والخيوط
لفهم ما هو مؤشر الترابط، عليك أن تفهم كيفية تشغيل تطبيق Java. دعونا نغير الكود الخاص بنا كما يلي:
class HelloWorldApp {
public static void main(String[] args) {
while (true) {
}
}
}
الآن دعونا نجمعها مرة أخرى باستخدام javac. بعد ذلك، من أجل الراحة، سنقوم بتشغيل كود Java الخاص بنا في نافذة منفصلة. على نظام التشغيل Windows، يمكنك القيام بذلك على النحو التالي:
start java HelloWorldApp
. الآن، باستخدام الأداة المساعدة
jps ، لنرى ما هي المعلومات التي ستخبرنا بها Java:
الرقم الأول هو PID أو معرف العملية، وهو معرف العملية. ما هي العملية؟
Процесс — это совокупность codeа и данных, разделяющих общее виртуальное addressное пространство.
بمساعدة العمليات، يتم عزل تنفيذ البرامج المختلفة عن بعضها البعض: يستخدم كل تطبيق منطقة الذاكرة الخاصة به دون التدخل في البرامج الأخرى. أنصحك بقراءة المقال بمزيد من التفصيل: "
https://habr.com/post/164487/ ". لا يمكن أن توجد العملية بدون سلاسل عمليات، لذلك إذا كانت العملية موجودة، فهذا يعني وجود مؤشر ترابط واحد على الأقل فيها. كيف يحدث هذا في جافا؟ عندما نقوم بتشغيل برنامج Java، يبدأ تنفيذه بالملحق
main
. نحن ندخل إلى البرنامج نوعًا ما، لذلك
main
تسمى هذه الطريقة الخاصة بنقطة الدخول، أو "نقطة الدخول". يجب أن تكون الطريقة
main
دائمًا
public static void
حتى يتمكن Java Virtual Machine (JVM) من البدء في تنفيذ برنامجنا. راجع "
لماذا تعتبر طريقة Java الرئيسية ثابتة؟ " لمزيد من التفاصيل. اتضح أن مشغل Java (java.exe أو javaw.exe) هو تطبيق بسيط (تطبيق C بسيط): يقوم بتحميل ملفات DLL المختلفة، والتي هي في الواقع JVM. يقوم مشغل Java بإجراء مجموعة محددة من مكالمات Java Native Interface (JNI). JNI هي الآلية التي تربط بين عالم Java Virtual Machine وعالم C++. اتضح أن المشغل ليس JVM، بل هو المُحمل الخاص به. إنه يعرف الأوامر الصحيحة التي يجب تنفيذها لبدء JVM. يعرف كيفية تنظيم كل البيئة اللازمة باستخدام مكالمات JNI. يتضمن تنظيم البيئة أيضًا إنشاء سلسلة رسائل رئيسية تسمى عادةً
main
. لرؤية المواضيع الموجودة في عملية Java بشكل أكثر وضوحًا، نستخدم برنامج
jvisualvm ، المضمن في JDK. بمعرفة معرف عملية ما، يمكننا فتح البيانات عليها على الفور:
jvisualvm --openpid айдипроцесса
ومن المثير للاهتمام أن كل مؤشر ترابط له منطقة منفصلة خاصة به في الذاكرة المخصصة للعملية. تسمى بنية الذاكرة هذه بالمكدس. المكدس يتكون من إطارات. الإطار هو نقطة استدعاء الطريقة ونقطة التنفيذ. يمكن أيضًا تمثيل الإطار باعتباره StackTraceElement (راجع Java API لـ
StackTraceElement ). يمكنك قراءة المزيد عن الذاكرة المخصصة لكل موضوع
هنا . إذا نظرنا إلى
Java API وبحثنا عن كلمة Thread، فسنرى أن هناك فئة
java.lang.Thread . هذه الفئة هي التي تمثل الدفق في Java، وهذا هو ما يتعين علينا العمل عليه.
java.lang.Thread
يتم تمثيل مؤشر الترابط في Java كمثيل للفئة
java.lang.Thread
. من الجدير أن نفهم على الفور أن مثيلات فئة Thread في Java ليست سلاسل رسائل بحد ذاتها. هذا مجرد نوع من واجهة برمجة التطبيقات (API) للسلاسل ذات المستوى المنخفض التي تتم إدارتها بواسطة JVM ونظام التشغيل. عندما نقوم بتشغيل JVM باستخدام مشغل Java، فإنه يقوم بإنشاء سلسلة رسائل رئيسية باسم
main
والعديد من سلاسل رسائل الخدمة الأخرى. كما هو مذكور في JavaDoc لفئة Thread:
When a Java Virtual Machine starts up, there is usually a single non-daemon thread
هناك نوعان من المواضيع: الشياطين وغير الشياطين. خيوط الشيطان هي خيوط خلفية (خيوط الخدمة) تؤدي بعض الأعمال في الخلفية. هذا المصطلح المثير للاهتمام هو إشارة إلى "شيطان ماكسويل"، والذي يمكنك قراءة المزيد عنه في مقالة ويكيبيديا حول "
الشياطين ". كما هو مذكور في الوثائق، يستمر JVM في تنفيذ البرنامج (العملية) حتى:
- لم يتم استدعاء الأسلوب Runtime.exit
- أكملت جميع المواضيع غير الشيطانية عملها (سواء بدون أخطاء أو مع طرح استثناءات)
ومن هنا تأتي التفاصيل المهمة: يمكن إنهاء سلاسل العمليات الشيطانية عند تنفيذ أي أمر. ولذلك، فإن سلامة البيانات الموجودة فيها ليست مضمونة. ولذلك، فإن الخيوط الخفية مناسبة لبعض مهام الخدمة. على سبيل المثال، يوجد في Java مؤشر ترابط مسؤول عن معالجة طرق الإنهاء أو سلاسل الرسائل المتعلقة بـ Garbage Collector (GC). ينتمي كل موضوع إلى مجموعة ما (
ThreadGroup ). ويمكن للمجموعات أن تدخل في بعضها البعض، وتشكل بعض التسلسل الهرمي أو الهيكل.
public static void main(String []args){
Thread currentThread = Thread.currentThread();
ThreadGroup threadGroup = currentThread.getThreadGroup();
System.out.println("Thread: " + currentThread.getName());
System.out.println("Thread Group: " + threadGroup.getName());
System.out.println("Parent Group: " + threadGroup.getParent().getName());
}
تسمح لك المجموعات بتبسيط إدارة التدفقات وتتبعها. بالإضافة إلى المجموعات، المواضيع لديها معالج الاستثناء الخاص بها. لنلقي نظرة على مثال:
public static void main(String []args) {
Thread th = Thread.currentThread();
th.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("An error occurred: " + e.getMessage());
}
});
System.out.println(2/0);
}
سيؤدي القسمة على صفر إلى حدوث خطأ سيكتشفه المعالج. إذا لم تقم بتحديد المعالج بنفسك، فسيعمل تنفيذ المعالج الافتراضي، والذي سيعرض مكدس الأخطاء في StdError. يمكنك قراءة المزيد في المراجعة
http://pro-java.ru/java-dlya-opytnyx/obrabotchik-neperexvachennyx-isklyuchenij-java/ ". بالإضافة إلى ذلك، فإن الموضوع له أولوية. يمكنك قراءة المزيد عن الأولويات في مقالة "
أولوية مؤشر ترابط Java في تعدد مؤشرات الترابط ".
إنشاء موضوع
كما هو مذكور في الوثائق، لدينا طريقتان لإنشاء سلسلة رسائل. الأول هو إنشاء وريثك. على سبيل المثال:
public class HelloWorld{
public static class MyThread extends Thread {
@Override
public void run() {
System.out.println("Hello, World!");
}
}
public static void main(String []args){
Thread thread = new MyThread();
thread.start();
}
}
كما ترون، يتم إطلاق المهمة في الطريقة
run
، ويتم تشغيل الخيط في الطريقة
start
. ولا ينبغي الخلط بينهما، لأن... إذا قمنا بتشغيل الطريقة
run
مباشرة، فلن يتم بدء أي موضوع جديد. إنها الطريقة
start
التي تطلب من JVM إنشاء سلسلة رسائل جديدة. يعد خيار السليل من Thread سيئًا لأننا نقوم بتضمين Thread في التسلسل الهرمي للفئة. العيب الثاني هو أننا بدأنا في انتهاك مبدأ "المسؤولية الفردية" SOLID، لأنه يصبح فصلنا مسؤولاً في نفس الوقت عن إدارة سلسلة المحادثات وعن بعض المهام التي يجب تنفيذها في هذا الموضوع. ايهم صحيح؟ الجواب بنفس الطريقة
run
التي نتجاوزها:
public void run() {
if (target != null) {
target.run();
}
}
إليك
target
بعض العناصر
java.lang.Runnable
التي يمكننا تمريرها إلى Thread عند إنشاء مثيل للفئة. لذلك يمكننا القيام بذلك:
public class HelloWorld{
public static void main(String []args){
Runnable task = new Runnable() {
public void run() {
System.out.println("Hello, World!");
}
};
Thread thread = new Thread(task);
thread.start();
}
}
إنها أيضًا
Runnable
واجهة وظيفية منذ Java 1.8. يتيح لك ذلك كتابة رمز المهمة للسلاسل بشكل أكثر جمالاً:
public static void main(String []args){
Runnable task = () -> {
System.out.println("Hello, World!");
};
Thread thread = new Thread(task);
thread.start();
}
المجموع
لذا، آمل من هذه القصة أن يكون واضحًا ما هو الدفق وكيفية وجوده وما هي العمليات الأساسية التي يمكن إجراؤها به. في
الجزء التالي ، من المفيد فهم كيفية تفاعل الخيوط مع بعضها البعض وما هي دورة حياتها. # فياتشيسلاف
GO TO FULL VERSION