כיצד להפעיל חוט בג'אווה

1. הקדמה

במדריך זה אנו נבדוק דרכים שונות להתחיל שרשור ולבצע משימות מקבילות.

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

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

2. היסודות בהפעלת חוט

אנו יכולים לכתוב בקלות לוגיקה שעוברת בשרשור מקביל באמצעות ה- פְּתִיל מִסגֶרֶת.

בואו ננסה דוגמה בסיסית על ידי הרחבת ה- פְּתִיל מעמד:

מחלקה ציבורית NewThread מרחיב אשכול {ריצה ציבורית ריקה () {long startTime = System.currentTimeMillis (); int i = 0; בעוד (נכון) {System.out.println (this.getName () + ": שרשור חדש פועל ..." + i ++); נסה {// המתן לשנייה אחת כדי שלא ידפיס מהר מדי Thread.sleep (1000); } לתפוס (InterruptedException e) {e.printStackTrace (); } ...}}}

ועכשיו אנו כותבים מחלקה שנייה לאתחל ולהתחיל את השרשור שלנו:

מחלקה ציבורית SingleThreadExample {public static void main (String [] args) {NewThread t = newThread new (); t.start (); }}

עלינו להתקשר ל הַתחָלָה() שיטה על חוטים ב חָדָשׁ (המקבילה של לא התחיל). אחרת, ג'אווה תשליך מופע של IllegalThreadStateException יוצא מן הכלל.

בואו נניח שעלינו להתחיל מספר שרשורים:

מחלקה ציבורית MultipleThreadsExample {public static void main (String [] args) {NewThread t1 = newThread new (); t1.setName ("MyThread-1"); NewThread t2 = NewThread new (); t2.setName ("MyThread-2"); t1.start (); t2.start (); }}

הקוד שלנו עדיין נראה די פשוט ודומה מאוד לדוגמאות שאנחנו יכולים למצוא באינטרנט.

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

לכן, כדי להיות מוכנים לייצור, עלינו לכתוב עוד צלחת להתמודד עם:

  • היצירה העקבית של חוטים חדשים
  • מספר האשכולות החיים במקביל
  • מיקום האשכולות: חשוב מאוד לשרשורי הדמון כדי למנוע דליפות

אם נרצה, נוכל לכתוב קוד משלנו לכל תרחישי המקרים הללו ואף לעוד כמה, אך מדוע שנמציא את הגלגל מחדש?

3. ה שירות ExecutorService מִסגֶרֶת

ה שירות ExecutorService מיישם את דפוס העיצוב של Thread Pool (המכונה גם עובד משוכפל או מודל צוות עובדים) ודואג לניהול החוטים שהזכרנו לעיל, בנוסף הוא מוסיף כמה תכונות שימושיות מאוד כמו שימוש חוזר בתבריג ותורי משימות.

במיוחד לשימוש חוזר בחוטים, חשוב מאוד: ביישום בקנה מידה גדול, הקצאה והקצאה של אובייקטים רבים של חוטים יוצרת תקורה משמעותית לניהול זיכרון.

בעזרת חוטי עובדים אנו ממזערים את התקורה הנגרמת כתוצאה מיצירת חוטים.

כדי להקל על תצורת הבריכה, שירות ExecutorService מגיע עם קונסטרוקטור קל וכמה אפשרויות התאמה אישית, כגון סוג התור, המספר המינימלי והמקסימלי של האשכולות ומוסכמת השמות שלהם.

לפרטים נוספים אודות שירות ExecutorService, אנא קרא את המדריך שלנו ל- Java ExecutorService.

4. התחלת משימה עם מוציאים לפועל

הודות למסגרת עוצמתית זו, אנו יכולים לעבור על הלך הרוח שלנו משרשור התחלה להגשת משימות.

בואו נסתכל כיצד נוכל להגיש משימה אסינכרונית למבצע שלנו:

ExecutorService executor = Executors.newFixedThreadPool (10); ... executor.submit (() -> {משימה חדשה ();});

ישנן שתי שיטות בהן אנו יכולים להשתמש: לבצע, שלא מחזיר דבר, ו שלח, שמחזירה א עתיד עוטף את תוצאת החישוב.

למידע נוסף אודות עתיד, אנא קרא את המדריך ל- java.util.concurrent.Future.

5. התחלת משימה עם CompleteFutures

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

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

Java 1.8 הציגה מסגרת חדשה על גבי עתיד לבנות לעבוד טוב יותר עם תוצאת החישוב: העתיד.

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

היישום להגשת משימה פשוט בהרבה:

CompletableFuture.supplyAsync (() -> "שלום");

supplyAsync לוקח ספק המכיל את הקוד אותו אנו רוצים לבצע באופן אסינכרוני - במקרה שלנו הפרמטר lambda.

המשימה מוגשת כעת במרומז ForkJoinPool.commonPool (), או שנוכל לציין את מוציא להורג אנו מעדיפים כפרמטר שני.

לדעת יותר אודות עתיד, אנא קרא את המדריך שלנו ל- CompletableFuture.

6. הפעלת משימות מושהות או תקופתיות

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

ל- Java יש כמה כלים שיכולים לעזור לנו להפעיל פעולות מאוחרות או חוזרות:

  • java.util.Timer
  • java.util.concurrent.ScheduledThreadPoolExecutor

6.1. שָׁעוֹן עֶצֶר

שָׁעוֹן עֶצֶר הוא מתקן לתזמון משימות לביצוע עתידי בשרשור הרקע.

משימות עשויות להיות מתוזמנות לביצוע חד פעמי, או לביצוע חוזר במרווחי זמן קבועים.

בואו נראה איך נראה הקוד אם אנחנו רוצים להפעיל משימה אחרי שנייה אחת של עיכוב:

משימה TimerTask = TimerTask חדש () {ריצה ציבורית ריקה () {System.out.println ("המשימה בוצעה בתאריך:" + תאריך חדש () + "n" + "שם הברגה:" + Thread.currentThread (). GetName ( )); }}; טיימר טיימר = טיימר חדש ("טיימר"); עיכוב ארוך = 1000L; timer.schedule (משימה, עיכוב);

עכשיו בואו נוסיף לוח זמנים חוזר:

timer.scheduleAtFixedRate (משימה חוזרת, עיכוב, נקודה);

הפעם, המשימה תפעל לאחר העיכוב שצוין והיא תחזור על עצמה לאחר פרק הזמן שחלף.

לקבלת מידע נוסף, קרא את המדריך שלנו ל- Java Timer.

6.2. ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor יש שיטות דומות ל שָׁעוֹן עֶצֶר מעמד:

ScheduledExecutorService executorService = Executors.newScheduledThreadPool (2); ScheduledFuture resultFuture = executorService.schedule (callableTask, 1, TimeUnit.SECONDS);

לסיום הדוגמה שלנו, אנו משתמשים scheduleAtFixedRate () למשימות חוזרות:

ScheduledFuture resultFuture = executorService.scheduleAtFixedRate (runnableTask, 100, 450, TimeUnit.MILLISECONDS);

הקוד שלמעלה יבצע משימה לאחר עיכוב ראשוני של 100 אלפיות השנייה, ולאחר מכן, היא תבצע את אותה משימה כל 450 אלפיות השנייה.

אם המעבד אינו יכול לסיים את עיבוד המשימה בזמן לפני ההתרחשות הבאה, ScheduledExecutorService ימתין עד שהמשימה הנוכחית תושלם, לפני שתתחיל במשימה הבאה.

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

לפרטים נוספים אודות שירות מתוזמן, אנא קרא את המדריך שלנו ל- Java ExecutorService.

6.3. איזה כלי עדיף?

אם נריץ את הדוגמאות לעיל, תוצאת החישוב נראית זהה.

כך, איך נבחר את הכלי הנכון?

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

בואו ננסה לצלול קצת יותר עמוק מתחת למכסה המנוע.

שָׁעוֹן עֶצֶר:

  • אינו מציע ערבויות בזמן אמת: הוא מתזמן משימות באמצעות Object.wait (ארוך) שיטה
  • יש חוט רקע יחיד, כך שמשימות פועלות ברצף ומשימה ארוכת טווח יכולה לעכב אחרים
  • חריגים בזמן ריצה שנזרקו בתוך TimerTask יהרוג את החוט היחיד שיש, וכך יהרוג שָׁעוֹן עֶצֶר

ScheduledThreadPoolExecutor:

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

הבחירה כעת ברורה, נכון?

7. ההבדל בין עתיד ו ScheduledFuture

בדוגמאות הקוד שלנו, אנו יכולים לבחון זאת ScheduledThreadPoolExecutor מחזירה סוג ספציפי של עתיד: ScheduledFuture.

ScheduledFuture מרחיב את שניהם עתיד ו מוּשׁהֶה ממשקים, וכך יורשים את השיטה הנוספת getDelay המחזירה את העיכוב שנותר המשויך למשימה הנוכחית. זה מורחב ב RunnableScheduledFuture שמוסיף שיטה לבדוק אם המשימה תקופתית.

ScheduledThreadPoolExecutor מיישם את כל המבנים הללו דרך המעמד הפנימי ScheduledFutureTask ומשתמש בהם כדי לשלוט על מחזור חיי המשימה.

8. מסקנות

במדריך זה התנסינו במסגרות השונות הזמינות להפעלת שרשורים ולהפעלת משימות במקביל.

ואז, העמקנו יותר בהבדלים בין שָׁעוֹן עֶצֶר ו ScheduledThreadPoolExecutor.

קוד המקור של המאמר זמין באתר GitHub.