ג'אווה 101: מקביליות ג'אווה ללא הכאב, חלק 1

עם המורכבות ההולכת וגוברת של יישומים מקבילים, מפתחים רבים מוצאים כי יכולות ההשחלה ברמה נמוכה של Java אינן מספקות לצורכי התכנות שלהם. במקרה זה, ייתכן שהגיע הזמן לגלות את תוכנות העזר של Java Concurrency. התחל בעבודה עם java.util.concurrentההקדמה המפורטת של ג'ף פרייזן למסגרת ה- Executor, סוגי הסנכרונים וחבילת ה- Java Concurrent Collections.

Java 101: הדור הבא

המאמר הראשון בסדרת JavaWorld החדשה הזו מציג את ה- Java Date and Time API .

פלטפורמת Java מספקת יכולות השחלה ברמה נמוכה המאפשרות למפתחים לכתוב יישומים בו-זמניים בהם שרשורים שונים פועלים בו זמנית. לשרשור Java רגיל יש כמה חסרונות, עם זאת:

  • הפרימיטיבים מקביליות ברמה נמוכה של ג'אווה ( synchronized, volatile, wait(), notify(), ו notifyAll()) אינם וקל לשימוש נכון. סכנות השחלה כמו מבוי סתום, רעב חוט ותנאי גזע, הנובעות משימוש שגוי בפרימיטיבים, קשה גם לאיתור וניתוח באגים.
  • הישענות על synchronizedתיאום הגישה בין השרשור מובילה לבעיות ביצועים המשפיעות על יכולת הרחבה של היישומים, דרישה ליישומים מודרניים רבים.
  • יכולות ההשחלה הבסיסיות של Java הן ברמה נמוכה מדי . מפתחים זקוקים לרוב למבנים ברמה גבוהה יותר כמו סמפורות ובריכות שרשור, אשר יכולות ההשחלה ברמה נמוכה של Java אינן מציעות. כתוצאה מכך, מפתחים יבנו מבנים משלהם, וזה גם זמן רב וגם נוטה לטעויות.

מסגרת JSR 166: Concurrency Utilities תוכננה לענות על הצורך במתקן השחלה ברמה גבוהה. המסגרת, שהוקמה בתחילת 2002, פורמלית והוטמעה כעבור שנתיים בג'אווה 5. השיפורים באו בג'אווה 6, בג'אווה 7 ובג'אווה 8 הקרוב.

ג'אווה 101 דו-חלקי זה : סדרת הדור הבא מציגה את מפתחי התוכנה המכירים את השחלת Java הבסיסית לחבילות ומסגרות Java Concurrency Utilities. בחלק 1, אני מציג סקירה כללית של מסגרת הכלים של Java Concurrency Utilities ומציג את מסגרת ה- Executor שלה, כלי הסינכרון וחבילת ה- Java Concurrent Collections.

הבנת שרשורי Java

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

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

בתוך תוכניות הכלליות ג'אווה

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

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

  • java.util.concurrent מכיל סוגי שירות ברמה גבוהה המשמשים בדרך כלל בתכנות במקביל. דוגמאות לכך כוללות סמפורות, מחסומים, מאגרי חוטים, וחשמירות במקביל.
    • חבילת המשנה java.util.concurrent.atomic מכילה מחלקות שירות ברמה נמוכה התומכות בתכנות חסין חוטים ללא נעילה על משתנים בודדים.
    • חבילת המשנה java.util.concurrent.locks מכילה סוגי שירות ברמה נמוכה לנעילה ולהמתנה לתנאים, השונים משימוש בסנכרון ובצגים של Java ברמה נמוכה.

מסגרת Java Concurrency Utilities חושפת גם את הוראות החומרה להשוואה והחלפה ברמה נמוכה (CAS) , שגרסאותיה נתמכות בדרך כלל על ידי מעבדים מודרניים. CAS קל משקל הרבה יותר ממנגנון הסנכרון המבוסס על צג של Java ומשמש ליישום כמה שיעורים בו זמנית מדרגיים. java.util.concurrent.locks.ReentrantLockהמחלקה המבוססת על CAS , למשל, מביאה יותר ביצועים synchronizedמהפרימיטיב המקביל המקביל לצג . ReentrantLockמציע יותר שליטה על הנעילה. (בחלק 2 אסביר יותר על אופן הפעולה של CAS java.util.concurrent).

System.nanoTime ()

מסגרת הכלים של Java Concurrency Utilities כוללת long nanoTime(), שהיא חברה java.lang.Systemבכיתה. שיטה זו מאפשרת גישה למקור זמן ננו-שנייה-גרעיניות לביצוע מדידות זמן יחסית.

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

מסגרת המוציא לפועל

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

רישום 1. Server.java (גרסה 1)

import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; class Server { public static void main(String[] args) throws IOException { ServerSocket socket = new ServerSocket(9000); while (true) { final Socket s = socket.accept(); Runnable r = new Runnable() { @Override public void run() { doWork(s); } }; new Thread(r).start(); } } static void doWork(Socket s) { } }

הקוד שלמעלה מתאר יישום שרת פשוט (כשהוא doWork(Socket)נותר ריק לצורך קיצורו). שרשור השרת מתקשר שוב ושוב socket.accept()להמתין לבקשה נכנסת ואז מפעיל שרשור לשרת בקשה זו כאשר היא מגיעה.

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

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

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

בתוך מסגרת ההוצאה לפועל

מסגרת ההוצאה לפועל מבוססת על Executorהממשק, המתאר מבצע כמוצא כל אובייקט המסוגל לבצע java.lang.Runnableמשימות. ממשק זה מצהיר על השיטה הבודדת הבאה לביצוע Runnableמשימה:

void execute(Runnable command)

אתה מגיש Runnableמשימה על ידי העברתה ל execute(Runnable). אם המוציא לפועל לא יכול לבצע את המשימה מסיבה כלשהי (למשל, אם המוציא לפועל הושבת), שיטה זו תזרוק א RejectedExecutionException.

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

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

ExecutorServiceראוי לציון במיוחד חמש מהשיטות:

  • Boolean awaitTermination (פסק זמן ארוך, יחידת TimeUnit) חוסם את השרשור הקורא עד שכל המשימות הסתיימו בביצוע לאחר בקשת כיבוי, זמן הקצוב מתרחש, או השרשור הנוכחי מופרע, מה שקורה הראשון. הזמן המקסימלי להמתין נקבע על ידי timeout, וערך זה מתבטא unitביחידות שצוינו על ידי TimeUnitהאנומום; למשל TimeUnit.SECONDS,. שיטה זו זורקת java.lang.InterruptedExceptionכאשר החוט הנוכחי נקטע. זה מחזיר נכון כאשר המוציא לפועל מסתיים ושקר כשחלוף הזמן הקצוב לפני סיומו.
  • isShutdown בוליאני () מחזיר נכון כאשר המבצע נסגר.
  • כיבוי ריק () יוזם כיבוי מסודר שבו מבוצעות משימות שהוגשו בעבר אך לא מתקבלות משימות חדשות.
  • הגשה עתידית (משימה הניתנת להתקשר) מגישה משימה להחזרת ערך לביצוע ומחזירה מציג Futureאת התוצאות הממתינות של המשימה.
  • הגשה עתידית (משימה Runnable) מגישה Runnableמשימה לביצוע ומחזירה Futureמשימה המייצגת את אותה משימה.

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

Callableהממשק דומה Runnableלממשק בכך שהוא מספק שיטה אחת לתאר משימה לבצע. בשונה Runnableשל void run()השיטה, Callableשל V call() throws Exceptionהשיטה יכולה להחזיר ערך ולזרוק חריג.

שיטות מפעל המוציא לפועל

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

  • ExecutorService newCachedThreadPool () יוצר מאגר חוטים שיוצר אשכולות חדשים לפי הצורך, אך עושה שימוש חוזר בשרשורים שנבנו בעבר כשהם זמינים. חוטים שלא נעשה בהם שימוש במשך 60 שניות מסתיימים ומוסרים מהמטמון. מאגר פתילים זה משפר בדרך כלל את הביצועים של תוכניות שמבצעות משימות אסינכרוניות רבות קצרות מועד.
  • ExecutorService newSingleThreadExecutor () יוצר מבצע המשתמש בשרשור עובד יחיד המפעיל תור בלתי מוגבל - משימות מתווספות לתור ומבוצעות ברצף (לא יותר ממשימה אחת פעילה בכל פעם). אם שרשור זה מסתיים מכישלון במהלך הביצוע לפני כיבוי המבצע, תיווצר שרשור חדש שיתפוס את מקומו כאשר יש לבצע פעולות עוקבות.
  • ExecutorService newFixedThreadPool(int nThreads) creates a thread pool that re-uses a fixed number of threads operating off a shared unbounded queue. At most nThreads threads are actively processing tasks. If additional tasks are submitted when all threads are active, they wait in the queue until a thread is available. If any thread terminates through failure during execution before shutdown, a new thread will be created to take its place when subsequent tasks need to be executed. The pool's threads exist until the executor is shut down.

The Executor framework offers additional types (such as the ScheduledExecutorService interface), but the types you are likely to work with most often are ExecutorService, Future, Callable, and Executors.

See the java.util.concurrent Javadoc to explore additional types.

עבודה עם מסגרת ההוצאה לפועל

תגלה שמסגרת ההוצאה לפועל די קלה לעבוד איתה. ברישום 2, השתמשתי Executorו Executorsלהחליף את דוגמא השרת מרישום 1 עם חוט מדרגים יותר בבריכה מבוססת אלטרנטיבה.

רישום 2. Server.java (גרסה 2)

import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.Executor; import java.util.concurrent.Executors; class Server { static Executor pool = Executors.newFixedThreadPool(5); public static void main(String[] args) throws IOException { ServerSocket socket = new ServerSocket(9000); while (true) { final Socket s = socket.accept(); Runnable r = new Runnable() { @Override public void run() { doWork(s); } }; pool.execute(r); } } static void doWork(Socket s) { } }

רישום 2 משתמש newFixedThreadPool(int)בכדי להשיג מבצע מבוסס חוטים המשמש מחדש חמישה שרשורים. הוא גם מחליף new Thread(r).start();עם pool.execute(r);לביצוע משימות Runnable באמצעות כל הנושאים הללו.

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