Java 101: הבנת שרשורי Java, חלק 1: הצגת שרשורים וריצות

מאמר זה הוא הראשון בסדרת Java 101 בת ארבעה חלקים החוקרים את שרשורי Java. למרות שאתה עשוי לחשוב על השחלה בג'אווה יהיה מאתגר לתפיסה, אני מתכוון להראות לך כי קל להבין את האשכולות. במאמר זה, אני מציג בפניכם את שרשורי ה- Java וה- Runnables. במאמרים הבאים נחקור סנכרון (באמצעות נעילות), בעיות סנכרון (כגון מבוי סתום), מנגנון ההמתנה / התראה, תזמון (עם ובלי עדיפות), הפרעת חוטים, טיימרים, תנודתיות, קבוצות שרשור ומשתנים מקומיים לשרשור. .

שים לב שמאמר זה (חלק מארכיוני JavaWorld) עודכן עם רשימות קוד חדשות וקוד מקור להורדה במאי 2013.

הבנת נושאי Java - קרא את כל הסדרה

  • חלק 1: הצגת חוטים ורצים
  • חלק 2: סנכרון
  • חלק 3: תזמון חוטים והמתנה / הודעה
  • חלק 4: קבוצות חוטים ותנודתיות

מהו חוט?

מבחינה מושגית, מושג החוט אינו קשה לתפיסה: זהו נתיב ביצוע עצמאי באמצעות קוד התוכנית. כאשר מספר אשכולות מבוצעים, בדרך כלל נתיב פתיל אחד דרך אותו קוד שונה מהאחרים. לדוגמה, נניח ששרשור אחד מבצע את המקבילה של קוד בתים לחלק של משפט אם-אחר if, בעוד שרשור אחר מבצע את המקבילה של קוד בתים של elseהחלק. כיצד JVM עוקב אחר ביצוע כל שרשור? ה- JVM מעניק לכל שרשור מחסנית שיחת שיטות משלה. בנוסף למעקב אחר הוראת קוד בתים הנוכחית, ערימת שיחת השיטה עוקבת אחר משתנים מקומיים, פרמטרים ש- JVM מעביר לשיטה וערך ההחזר של השיטה.

כאשר אשכולות מרובים מבצעים רצפי הוראה של קוד בתים באותה תוכנית, פעולה זו ידועה כ- Multithreading . Multithreading מועיל לתוכנית בדרכים שונות:

  • תוכניות מבוססות GUI מרובות הליכי (ממשק משתמש גרפי) נותרות מגיבות למשתמשים בזמן ביצוע משימות אחרות, כמו להדביק מחדש או להדפיס מסמך.
  • תוכניות הברגה מסתיימות בדרך כלל מהר יותר מעמיתיהן שאינם מושחלים. זה נכון במיוחד לגבי שרשורים הפועלים במכונת ריבוי מעבדים, כאשר לכל שרשור יש מעבד משלו.

ג'אווה מבצעת ריבוי הליכי משנה באמצעות java.lang.Threadהכיתה שלה . כל Threadאובייקט מתאר חוט יחיד של ביצוע. ביצוע המתרחשת Threadים" run()שיטה. מכיוון run()ששיטת ברירת המחדל אינה עושה דבר, עליך לסווג את המשנה Threadולדרוס אותה run()כדי לבצע עבודה שימושית. לטעימה של חוטים ורב-הברגה בהקשר של Thread, בחנו את רישום 1:

רישום 1. ThreadDemo.java

// ThreadDemo.java class ThreadDemo { public static void main (String [] args) { MyThread mt = new MyThread (); mt.start (); for (int i = 0; i < 50; i++) System.out.println ("i = " + i + ", i * i = " + i * i); } } class MyThread extends Thread { public void run () { for (int count = 1, row = 1; row < 20; row++, count++) { for (int i = 0; i < count; i++) System.out.print ('*'); System.out.print ('\n'); } } }

רישום 1 מציג את קוד המקור ליישום המורכב משיעורים ThreadDemoו MyThread. Class ThreadDemoמניע את היישום על ידי יצירת MyThreadאובייקט, הפעלת שרשור המשויך לאובייקט זה והפעלת קוד כלשהו להדפסת טבלת ריבועים. לעומת זאת, MyThreadעוקפת Threadאת run()השיטה להדפיס (בזרם הפלט הסטנדרטי) משולש בזווית ישרה המורכבת מתווי כוכבית.

תזמון חוטים ו- JVM

רוב יישומי JVM (אם לא כולם) משתמשים ביכולות ההשחלה הבסיסיות של הפלטפורמה. מכיוון שיכולות אלה ספציפיות לפלטפורמה, סדר הפלט של התוכניות הרביות שלך עשוי להיות שונה מסדר הפלט של מישהו אחר. ההבדל נובע מתזמון, נושא אותו אני בוחן בהמשך הסדרה.

כאשר אתה מקליד java ThreadDemoלהפעלת היישום, ה- JVM יוצר שרשור התחלה של ביצוע, שמבצע את main()השיטה. על ידי ביצוע mt.start ();, חוט ההתחלה אומר ל- JVM ליצור חוט שני של ביצוע שמבצע את הוראות קוד בתים הכוללות את שיטת MyThreadהאובייקט run(). כאשר start()השיטה חוזרת, החוט ההתחלתי מבצע את forהלולאה שלו כדי להדפיס טבלת ריבועים, ואילו החוט החדש מבצע את run()השיטה להדפסת המשולש הזווית.

איך נראית התפוקה? רוץ ThreadDemoלברר. תוכלו להבחין בפלט של כל פתיל נוטה להשתלב בפלט של השני. זה נובע מכיוון ששני הנושאים שולחים את הפלט לאותו זרם פלט רגיל.

כיתת החוט

כדי להיות בקיאים בכתיבת קוד רב-הברגה, עליך להבין תחילה את השיטות השונות המרכיבות את Threadהכיתה. חלק זה בוחן רבות מאותן שיטות. באופן ספציפי, אתה לומד על שיטות להפעלת חוטים, מתן שמות לשרשורים, הרדמת חוטים, קביעה אם שרשור חי, צירוף שרשור אחד לשרשור אחר וספירת כל השרשורים הפעילים בקבוצת החוטים הנוכחית ותתי-הקבוצות. אני דן גם Threadבעזרי ניפוי באגים ובשרשורי המשתמשים לעומת נושאי הדמון.

אני אציג את יתרת Threadהשיטות במאמרים הבאים, למעט השיטות שהוצאו משימוש.

שיטות שהוצאו משימוש

סאן זנח במגוון Threadשיטות, כגון suspend()ו resume(), משום שהם יכולים לכלוא חפצי תוכניות או ניזק שלך. כתוצאה מכך, אתה לא צריך להתקשר אליהם בקוד שלך. עיין בתיעוד של SDK לעקיפת הבעיה לשיטות אלה. אני לא מכסה שיטות שהוצאו משימוש בסדרה זו.

בניית חוטים

Threadיש לו שמונה בונים. הפשוטים ביותר הם:

  • Thread(), שיוצר Threadאובייקט עם שם ברירת מחדל
  • Thread(String name), שיוצר Threadאובייקט עם שם nameשהארגומנט מציין

הבונים הפשוטים הבאים הם Thread(Runnable target)ו Thread(Runnable target, String name). מלבד Runnableהפרמטרים, אותם בונים זהים לבונים הנ"ל. ההבדל: Runnableהפרמטרים מזהים אובייקטים מחוץ Threadלמספקים את run()השיטות. (אתה לומד על Runnableבהמשך מאמר זה.) הקבלנים בונים ארבעה סופי דומה Thread(String name), Thread(Runnable target), ו Thread(Runnable target, String name); עם זאת, הבונים הסופיים כוללים גם ThreadGroupטיעון למטרות ארגוניות.

אחד מארבעת הקונסטרוקטורים האחרונים,, Thread(ThreadGroup group, Runnable target, String name, long stackSize)מעניין בכך שהוא מאפשר לך לציין את הגודל הרצוי של ערימת קריאת השיטה של ​​השרשור. היכולת לציין שגודל מוכיח מועיל בתוכניות עם שיטות המשתמשות ברקורסיה - טכניקת ביצוע לפיה שיטה קוראת לעצמה שוב ושוב - לפתור אלגנטיות של בעיות מסוימות. על ידי הגדרה מפורשת של גודל הערימה, אתה יכול לפעמים למנוע StackOverflowErrors. עם זאת, גודל גדול מדי יכול לגרום ל- OutOfMemoryErrors. כמו כן, סאן רואה בגודל ערימת שיחת השיטה תלוי בפלטפורמה. בהתאם לפלטפורמה, גודל ערימת שיחת השיטה עשוי להשתנות. לכן, חשוב היטב על ההשלכות לתוכנית שלך לפני שאתה כותב קוד שמתקשר Thread(ThreadGroup group, Runnable target, String name, long stackSize).

התניע את כלי הרכב שלך

חוטים דומים לרכבים: הם מעבירים תוכניות מההתחלה ועד הסוף. Threadו Threadחפצים תת אינם נושאים. במקום זאת, הם מתארים את מאפייני השרשור, כגון שמו, ומכילים קוד (באמצעות run()שיטה) שהשרשור מבצע. כשמגיע הזמן לבצע שרשור חדש run(), חוט אחר מכנה את השיטה Threadשל האובייקט תת-קלאסי start(). לדוגמא, כדי להתחיל שרשור שני, שרשור ההתחלה של היישום - שמבצע - main()מתקשר start(). בתגובה, קוד הטיפול בחוטים של JVM עובד עם הפלטפורמה כדי להבטיח שהחוט יותחל כראוי וקורא לשיטה Threadשל אובייקט תת-מחלקה run().

לאחר start()השלמתם, מספר אשכולות מבוצעים. מכיוון שאנו נוטים לחשוב בצורה לינארית, לעתים קרובות אנו מתקשים להבין את הפעילות במקביל (סימולטנית) המתרחשת כאשר שני או יותר שרשורים פועלים. לכן, עליכם לבחון תרשים המראה היכן שרשור מבצע (מיקומו) לעומת הזמן. האיור שלהלן מציג תרשים כזה.

התרשים מציג מספר פרקי זמן משמעותיים:

  • אתחול פתיל ההתחלה
  • הרגע שהחוט הזה מתחיל להתבצע main()
  • הרגע שהחוט הזה מתחיל להתבצע start()
  • הרגע start()יוצר חוט חדש וחוזר לmain()
  • אתחול השרשור החדש
  • הרגע שהחוט החדש מתחיל להופיע run()
  • הרגעים השונים שכל חוט מסתיים

שים לב שהאתחול של השרשור החדש, הביצוע run()שלו והסיום שלו מתרחשים במקביל לביצוע החוט החל. שים לב גם שאחרי שיחות שרשור start(), שיחות שלאחר מכן לשיטה זו לפני run()יציאת השיטה גורמות start()לזריקת java.lang.IllegalThreadStateExceptionאובייקט.

מה בשם?

במהלך מפגש איתור באגים, הבחנה בין חוט אחד למשנהו באופן ידידותי למשתמש מועילה. כדי להבדיל בין שרשורים, ג'אווה משייך שם לשרשור. שם זה הוא ברירת מחדל Thread, תו מקף ומספר שלם מבוסס אפס. אתה יכול לקבל את שמות החוטים המוגדרים כברירת מחדל של Java או לבחור את עצמך. כדי להתאים שמות מותאמים אישית, Threadמספק בונים שלוקחים nameטיעונים setName(String name)ושיטה. Threadמספק גם getName()שיטה שמחזירה את השם הנוכחי. רישום 2 מדגים כיצד ליצור שם מותאם אישית באמצעות Thread(String name)הבנאי ולקבל את השם הנוכחי run()בשיטה על ידי קריאה getName():

רישום 2. NameThatThread.java

// NameThatThread.java class NameThatThread { public static void main (String [] args) { MyThread mt; if (args.length == 0) mt = new MyThread (); else mt = new MyThread (args [0]); mt.start (); } } class MyThread extends Thread { MyThread () { // The compiler creates the byte code equivalent of super (); } MyThread (String name) { super (name); // Pass name to Thread superclass } public void run () { System.out.println ("My name is: " + getName ()); } }

אתה יכול להעביר טיעון שם אופציונלי MyThreadבשורת הפקודה. לדוגמה, java NameThatThread Xקובע Xכשם השרשור. אם לא תציין שם, תראה את הפלט הבא:

My name is: Thread-1

אם אתה מעדיף, תוכל לשנות את super (name);השיחה בבנאי MyThread (String name)לשיחה ל setName (String name)- כמו שנמצאת setName (name);. אותה קריאת שיטה אחרונה משיגה את אותה מטרה - הקמת שם החוט - כמו super (name);. אני משאיר את זה כתרגיל עבורך.

שמות עיקריים

Java מקצה את השם mainלשרשור שמפעיל את main()השיטה, לשרשור ההתחלה. בדרך כלל אתה רואה את השם הזה Exception in thread "main"בהודעה שמדפיס מטפל החריגים של ברירת המחדל של JVM כאשר שרשור ההתחלה זורק אובייקט חריג.

לישון או לא לישון

Later in this column, I will introduce you to animation— repeatedly drawing on one surface images that slightly differ from each other to achieve a movement illusion. To accomplish animation, a thread must pause during its display of two consecutive images. Calling Thread's static sleep(long millis) method forces a thread to pause for millis milliseconds. Another thread could possibly interrupt the sleeping thread. If that happens, the sleeping thread awakes and throws an InterruptedException object from the sleep(long millis) method. As a result, code that calls sleep(long millis) must appear within a try block—or the code's method must include InterruptedException in its throws clause.

כדי להדגים sleep(long millis)כתבתי CalcPI1בקשה. יישום זה מתחיל שרשור חדש המשתמש באלגוריתם מתמטי כדי לחשב את הערך של הקבוע המתמטי pi. בזמן שהחוט החדש מחשב, החוט ההתחלתי מושהה למשך 10 אלפיות השנייה על ידי שיחה sleep(long millis). לאחר חוט ההתחלה מתעורר, הוא מדפיס את ערך ה- pi, אותו שומר השרשור החדש במשתנה pi. רישום 3 CalcPI1קוד המקור של המתנות :

רישום 3. CalcPI1.java

// CalcPI1.java class CalcPI1 { public static void main (String [] args) { MyThread mt = new MyThread (); mt.start (); try { Thread.sleep (10); // Sleep for 10 milliseconds } catch (InterruptedException e) { } System.out.println ("pi = " + mt.pi); } } class MyThread extends Thread { boolean negative = true; double pi; // Initializes to 0.0, by default public void run () { for (int i = 3; i < 100000; i += 2) { if (negative) pi -= (1.0 / i); else pi += (1.0 / i); negative = !negative; } pi += 1.0; pi *= 4.0; System.out.println ("Finished calculating PI"); } }

אם אתה מפעיל תוכנית זו, תראה פלט דומה (אך כנראה לא זהה) לתכונות הבאות:

pi = -0.2146197014017295 Finished calculating PI