LongAdder ו- LongAccumulator בג'אווה

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

במאמר זה נבחן שני קונסטרוקציות מה- java.util.concurrent חֲבִילָה: LongAdder ו LongAccumulator.

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

2. LongAdder

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

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

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

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

בוא ניצור מופע של ה- LongAdder בכיתה ועדכן אותה ממספר שרשורים:

מונה LongAdder = LongAdder חדש (); ExecutorService executorService = Executors.newFixedThreadPool (8); int numberOfThreads = 4; int numberOfIncrements = 100; IncrementAction ניתן להריצה = () -> IntStream .range (0, numberOfIncrements) .forEach (i -> counter.increment ()); עבור (int i = 0; i <numberOfThreads; i ++) {executorService.execute (incrementAction); }

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

assertEquals (counter.sum (), numberOfIncrements * numberOfThreads);

לפעמים, אחרי שאנחנו מתקשרים סְכוּם(), אנו רוצים לנקות את כל המצב הקשור למופע של LongAdder ולהתחיל לספור מההתחלה. אנחנו יכולים להשתמש ב- sumThenReset () שיטה להשיג זאת:

assertEquals (counter.sumThenReset (), numberOfIncrements * numberOfThreads); assertEquals (counter.sum (), 0);

שים לב שהשיחה שלאחר מכן אל סְכוּם() השיטה מחזירה אפס ומשמעותה שהמצב אופס בהצלחה.

יתר על כן, Java מספקת גם DoubleAdder לקיים סיכום של לְהַכפִּיל ערכים עם ממשק API דומה לזה LongAdder.

3. LongAccumulator

LongAccumulator הוא גם מחלקה מעניינת מאוד - המאפשרת לנו ליישם אלגוריתם ללא נעילה במספר תרחישים. לדוגמא, בעזרתו ניתן לצבור תוצאות בהתאם לסיפוק LongBinaryOperator - זה עובד באופן דומה ל לְהַפחִית() פעולה מזרם API.

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

מצבר LongAccumulator = LongAccumulator חדש (Long :: sum, 0L);

אנחנו יוצרים LongAccumulator מה?ch יוסיף ערך חדש לערך שהיה כבר בצבר. אנו קובעים את הערך ההתחלתי של ה- LongAccumulator לאפס, כך בשיחה הראשונה של לִצְבּוֹר() השיטה, הקודםערך יהיה ערך אפס.

בואו נקרא את לִצְבּוֹר() שיטה ממספר האשכולות:

int numberOfThreads = 4; int numberOfIncrements = 100; הצטבר Rackable accumulateAction = () -> IntStream .rangeClosed (0, numberOfIncrements) .forEach (צבר :: לצבור) עבור (int i = 0; i <numberOfThreads; i ++) {executorService.execute (accumulateAction); }

שימו לב כיצד אנו מעבירים מספר כטיעון ל לִצְבּוֹר() שיטה. שיטה זו תפעיל את שלנו סְכוּם() פוּנקצִיָה.

ה LongAccumulator משתמש ביישום השוואה והחלפה - מה שמוביל לסמנטיקה מעניינת אלה.

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

כעת אנו יכולים לטעון כי סכום כל הערכים מכל האיטרציות היה 20200:

assertEquals (accumulator.get (), 20200);

מעניין שגם ג'אווה מספקת DoubleAccumulator עם אותה מטרה ו- API אבל עבור לְהַכפִּיל ערכים.

4. פסים דינמיים

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

הנה תיאור פשוט של מה 64. פסים עושה:

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

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

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

כדי למנוע שיתוף כוזב. ה 64. פסים היישום מוסיף ריפוד מספיק בכל מדינה כדי לוודא שכל מדינה מתגוררת בשורת המטמון שלה:

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

5. מסקנה

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

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