Java Concurrency Utility עם JCTools

1. סקירה כללית

במדריך זה נציג את ספריית JCTools (Java Concurrency Tools).

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

2. אלגוריתמים שאינם חוסמים

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

לגישה זו יש מספר חסרונות:

  • אשכולות עשויים להיחסם בניסיון להשיג נעילה, ולא להתקדם עד לסיום פעולת חוט אחר - זה למעשה מונע מקביליות
  • מחלוקת הנעילה הכבדה יותר, ככל שה- JVM משקיע זמן רב יותר בתזמון שרשורים, ניהול מחלוקת ותורים של שרשורי המתנה ועבודה פחות אמיתית היא עושה
  • נעילה אפשרית אם מדובר במנעול אחד בלבד והם נרכשים / משוחררים בסדר שגוי
  • סכנת היפוך עדיפות אפשרית - חוט בעדיפות גבוהה נעול בניסיון להשיג נעילה המוחזקת על ידי חוט בעדיפות נמוכה
  • לרוב משתמשים במנעולים עם גרגר גס, ופוגעים מאוד במקביליות - נעילה דקה דורשת תכנון זהיר יותר, מגדילה את נעילת התקורה ונוטה יותר לטעויות

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

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

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

הנה לא חסימה לַעֲרוֹם דוגמה מתוך ספר Java Concurrency in Practice המצוין; הוא מגדיר את המצב הבסיסי:

כיתה ציבורית ConcurrentStack {AtomicReference למעלה = AtomicReference חדש(); מחלקה סטטית פרטית צומת {פריט E ציבורי; הצומת הציבורי הבא; // קונסטרוקטור סטנדרטי}}

וגם כמה שיטות API:

דחיקת חלל ציבורית (פריט E) {Node newHead = צומת חדש (פריט); צומת oldHead; לעשות {oldHead = top.get (); newHead.next = oldHead; } תוך (! top.compareAndSet (oldHead, newHead)); } פופ E פומבי () {צומת oldHead; צומת newHead; בצע {oldHead = top.get (); אם (oldHead == null) {return null; } newHead = oldHead.next; } תוך (! top.compareAndSet (oldHead, newHead)); להחזיר oldHead.item; }

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

3. תלות

ראשית, בואו נוסיף את התלות ב- JCTools pom.xml:

 org.jctools jctools-core 2.1.2 

לידיעתך, הגרסה האחרונה הזמינה זמינה ב- Maven Central.

4. תורי JCTools

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

הממשק המשותף לכולם תוֹר יישומים הם org.jctools.queues.MessagePassingQueue.

4.1. סוגי תורים

ניתן לסווג את כל התורים במדיניות המפיק / הצרכן שלהם:

  • מפיק יחיד, צרכן יחיד - שיעורים כאלה נקראים באמצעות הקידומת Spsc, למשל SpscArrayQueue
  • מפיק יחיד, מספר צרכנים - להשתמש Spmc קידומת, למשל SpmcArrayQueue
  • יצרנים מרובים, צרכן יחיד - להשתמש Mpsc קידומת, למשל MpscArrayQueue
  • מספר יצרנים, מספר צרכנים - להשתמש Mpmc קידומת, למשל MpmcArrayQueue

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

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

תור SpscArrayQue = SpscArrayQueue חדש (2); מפיק חוט 1 = חוט חדש (() -> תור. מציע (1)); producer1.start (); מפיק 1. להצטרף (); מפיק חוט 2 = חוט חדש (() -> תור. מציע (2)); producer2.start (); producer2.join (); הגדר מ-Queue = HashSet חדש (); צרכני חוטים = חוט חדש (() -> תור.drain (מ-Queue :: add)); consumer.start (); צרכן.צטרף (); assertThat (fromQueue) .containsOnly (1, 2);

4.2. יישומי תור

לסיכום הסיווגים לעיל, הנה רשימת התורים של JCTools:

  • SpscArrayQueue מפיק יחיד, צרכן יחיד, משתמש במערך יכולת מאוגדת פנימית
  • SpscLinkedQueue יצרן יחיד, צרכן יחיד, משתמש ברשימה מקושרת פנימית, בקיבולת לא מאוגדת
  • SpscChunkedArrayQueue יצרן יחיד, צרכן יחיד, מתחיל בקיבולת ראשונית וגדל עד לקיבולת מקסימלית
  • SpscGrowableArrayQueue יצרן יחיד, צרכן יחיד, מתחיל בקיבולת ראשונית וגדל עד לקיבולת מקסימלית. זהו אותו חוזה כמו SpscChunkedArrayQueueההבדל היחיד הוא ניהול נתחים פנימי. מומלץ להשתמש SpscChunkedArrayQueue כי יש לו יישום פשוט
  • SpscUnboundedArrayQueue יצרן יחיד, צרכן יחיד, משתמש במערך יכולת פנימית ובלתי מאוגדת
  • SpmcArrayQueue יצרן יחיד, מספר צרכנים, משתמש במערך יכולת מאוגד פנימית
  • MpscArrayQueue יצרנים מרובים, צרכנים בודדים, משתמשים במערך יכולת מאוגד פנימית
  • MpscLinkedQueue יצרנים מרובים, צרכנים בודדים, משתמשים ברשימה מקושרת באופן פנימי, בקיבולת לא מאוגדת
  • MpmcArrayQueue יצרנים מרובים, צרכנים מרובים, משתמשים במערך יכולת מאוגדת פנימית

4.3. תורים אטומיים

כל התורים שהוזכרו בסעיף הקודם משתמשים sun.misc. לא בטוח. עם זאת, עם כניסתם של Java 9 ו- JEP-260 API זה הופך לנגיש כברירת מחדל.

אז יש תורים חלופיים שמשתמשים בהם java.util.concurrent.atomic.AtomicLongFieldUpdater (ממשק API ציבורי, פחות ביצועי) במקום sun.misc. לא בטוח.

הם נוצרים מהתורים שלמעלה ושמם מכיל את המילה אָטוֹמִי מוכנס בין לבין, למשל SpscChunkedAtomicArrayQueue אוֹ MpmcAtomicArrayQueue.

מומלץ להשתמש בתורים 'רגילים' במידת האפשר ולפנות אליהם AtomicQueues רק בסביבות שבהן sun.misc. לא בטוח אסור / לא יעיל כמו HotSpot Java9 + ו- JRockit.

4.4. קיבולת

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

בדוגמה הבאה אנו:

  • למלא את התור
  • ודא שהוא מפסיק לקבל אלמנטים חדשים לאחר מכן
  • ניקוז ממנו וודא שאפשר להוסיף אחר כך אלמנטים נוספים

שים לב כי כמה הצהרות קוד מושמטות לקריאות. ניתן למצוא את היישום המלא ב- GitHub:

תור SpscChunkedArrayQue = חדש SpscChunkedArrayQueue (8, 16); CountDownLatch startConsuming = CountDownLatch חדש (1); CountDownLatch awakeProducer = CountDownLatch חדש (1); מפיק אשכול = שרשור חדש (() -> {IntStream.range (0, queue.capacity ()). ForEach (i -> {assertThat (queue.offer (i)). IsTrue ();}); assertThat (queue .offer (queue.capacity ())). isFalse (); startConsuming.countDown (); awakeProducer.await (); assertThat (queue.offer (queue.capacity ())). isTrue ();}); producer.start (); startConsuming.await (); הגדר מ-Queue = HashSet חדש (); queue.drain (fromQueue :: add); awakeProducer.countDown (); producer.join (); queue.drain (fromQueue :: add); assertThat (fromQueue) .containsAll (IntStream.range (0, 17) .boxed (). collect (toSet ()));

5. מבני נתונים אחרים של JCTools

JCTools מציעה גם כמה מבני נתונים שאינם תורים.

כולם מפורטים להלן:

  • NonBlockingHashMap ללא נעילה ConcurrentHashMap חלופה עם מאפייני שינוי גודל טוב יותר ובדרך כלל עלויות מוטציה נמוכות יותר. זה מיושם באמצעות sun.misc. לא בטוחלכן, לא מומלץ להשתמש בכיתה זו בסביבת HotSpot Java9 + או JRockit
  • NonBlockingHashMapLong כמו NonBlockingHashMap אבל משתמש פרימיטיבי ארוך מקשים
  • NonBlockingHashSet עטיפה פשוטה מסביב NonBlockingHashMapכמו של JDK java.util.Collections.newSetFromMap ()
  • NonBlockingIdentityHashMap כמו NonBlockingHashMap אך משווה מפתחות לפי זהות.
  • NonBlockingSetIntמערך וקטור סיביות רב-הברגה המיושם כמערך של פרימיטיבי מייחל. עובד בצורה לא יעילה במקרה של תיבת דואר אלקטרוני אוטומטית שקטה

6. בדיקת ביצועים

בואו נשתמש ב- JMH להשוואה בין ה- JDK ArrayBlockingQueue לעומת ביצועי התור של JCTools. JMH היא מסגרת קוד פתוח למיקרו-בנצ'מרק מגורואים של Sun / Oracle JVM המגנה עלינו מחוסר קביעות של אלגוריתמי אופטימיזציה של מהדר / jvm). אל תהסס לקבל פרטים נוספים על כך במאמר זה.

שים לב שקטע הקוד שלמטה מחמיץ כמה הצהרות על מנת לשפר את הקריאות. אנא מצא את קוד המקור המלא ב- GitHub:

מחלקה ציבורית MpmcBenchmark {@Param ({PARAM_UNSAFE, PARAM_AFU, PARAM_JDK}) יישום מחרוזת תנודתי ציבורי; תור תור תנודתי ציבורי; @Benchmark @Group (GROUP_NAME) @ GroupThreads (PRODUCER_THREADS_NUMBER) כתיבת חלל ציבורית (בקרת בקרה) {// noinspection StatementWithEmptyBody while (! Control.stopMeasurement &&! Queue.offer (1L)) {// הושאר ריק בכוונה}} @enchmark @ קבוצה (GROUP_NAME) @ GroupThreads (CONSUMER_THREADS_NUMBER) קריאה בטלנית ציבורית (בקרת בקרה) {// noinspection StatementWithEmptyBody בזמן (! Control.stopMeasurement && queue.poll () == null) {// בכוונה נשאר ריק}}}

תוצאות (קטע לאחוזון 95, ננו שניות לכל פעולה):

MpmcBenchmark.MyGroup: MyGroup · p0.95 MpmcArrayQueue sample 1052.000 ns / op MpmcBenchmark.MyGroup: MyGroup · p0.95 MpmcAtomicArrayQueue sample 1106.000 ns / op MpmcBenchmark.MyGroup: MyGroupB64

אנחנו יכולים לראות את זהMpmcArrayQueue מבצע רק מעט יותר טוב מ- MpmcAtomicArrayQueue ו ArrayBlockingQueue הוא איטי יותר מגורם שניים.

7. חסרונות בשימוש ב- JCTools

השימוש ב- JCTools יש חסרון חשוב - לא ניתן לאכוף כי נעשה שימוש נכון בשיעורי הספרייה. לדוגמא, שקול מצב בו אנו מתחילים להשתמש MpscArrayQueue בפרויקט הגדול והבוגר שלנו (שימו לב שחייב להיות צרכן יחיד).

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

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

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

8. מסקנה

כעת יש לנו הבנה בסיסית בשיעורי השירות המוצעים על ידי JCTools וראינו עד כמה הם מבצעים בהשוואה לעמיתיהם של ה- JDK בעומס כבד.

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

כמו תמיד, את קוד המקור המלא של הדוגמאות לעיל ניתן למצוא ב- GitHub.


$config[zx-auto] not found$config[zx-overlay] not found