Ось пояснення та приклад того, як це здійснюється. Повідомте мене, чи є деталі, які не зрозумілі.
Суть із джерелом
Універсальний
Ініціалізація:
Показники ниток застосовуються атомним чином. Це управляється за допомогою AtomicInteger
найменування nextIndex
. Ці індекси присвоюються потокам через ThreadLocal
екземпляр, який ініціалізує себе, отримуючи наступний індекс nextIndex
і збільшуючи його. Це відбувається вперше, коли індекс кожної нитки буде отримано вперше. A ThreadLocal
створено для відстеження останньої послідовності, яку створив цей потік. Він ініціалізований 0. Послідовні заводські посилання на об'єкт передаються та зберігаються. Два AtomicReferenceArray
екземпляри створюються за розміром n
. Хвостовий об’єкт присвоюється кожній посилання, після ініціалізації з початковим станом, наданим Sequential
заводом. n
- максимальна кількість дозволених потоків. Кожен елемент у цих масивах 'належить' відповідному індексу потоку.
Застосувати метод:
Це метод, який робить цікаву роботу. Він робить наступне:
- Створіть новий вузол для цього виклику: мій
- Встановіть цей новий вузол у масиві оголошень у індексі поточного потоку
Потім починається цикл послідовності. Це триватиме до тих пір, поки поточне виклик не буде простежено
- знайти вузол у масиві оголошень, використовуючи послідовність останнього вузла, створеного цим потоком. Детальніше про це пізніше.
- якщо на кроці 2 знайдено вузол, він ще не є послідовними, продовжте його, інакше просто зосередиться на поточному виклику. Це спробує допомогти лише одному іншому вузлу на виклик.
- Який би вузол не був обраний на кроці 3, намагайтеся його послідувати після останнього секведованого вузла (інші потоки можуть заважати.) Незалежно від успіху, встановіть поточну посилання головного потоку на послідовність, повернуту
decideNext()
Ключем до вкладеного циклу, описаного вище, є decideNext()
метод. Щоб зрозуміти це, нам потрібно подивитися клас Node.
Клас вузла
Цей клас визначає вузли у подвійно пов'язаному списку. У цьому класі не так багато дій. Більшість методів є простими методами пошуку, які повинні бути досить зрозумілими.
хвостовий метод
це повертає спеціальний екземпляр вузла з послідовністю 0. Він просто виступає як власник місця, поки виклик не замінить його.
Властивості та ініціалізація
seq
: порядковий номер, ініціалізований на -1 (означає непослідовність)
invocation
: значення виклику apply()
. Встановлюють при будівництві.
next
: AtomicReference
для прямого посилання. Після присвоєння це ніколи не буде змінено
previous
: AtomicReference
для зворотного зв'язку, присвоєного при послідовності та очищеномуtruncate()
Вирішіть далі
Цей метод лише один у Вузлі з нетривіальною логікою. У двох словах, вузол пропонується як кандидат, який буде наступним вузлом у пов'язаному списку. compareAndSet()
Метод перевірятиме , якщо це посилання є недійсним , і якщо так, то встановіть посилання на кандидата. Якщо посилання вже встановлено, воно нічого не робить. Ця операція є атомною, тому якщо одночасно будуть запропоновані два кандидати, буде обраний лише один. Це гарантує, що тільки один вузол коли-небудь буде обраний як наступний. Якщо вибрано вузол-кандидат, його послідовність встановлюється на наступне значення, і попереднє посилання встановлюється на цей вузол.
Стрибки назад до класу Universal застосовують метод застосування ...
Після виклику decideNext()
останнього секведованого вузла (коли він перевіряється) або з нашим вузлом, або з вузлом з announce
масиву, є два можливі випадки: 1. Вузол було успішно секведовано 2. Деякі інші потоки попередньо видалили цю нитку.
Наступний крок - перевірити, чи створено вузол для цього виклику. Це може статися через те, що цей потік успішно секвенував його або інший потік взяв його з announce
масиву та прослідкував його для нас. Якщо вона не була послідована, процес повторюється. В іншому випадку виклик закінчується очищенням масиву оголошень для цього індексу потоку та поверненням значення результату виклику. Масив оголошень очищений, щоб гарантувати відсутність посилань на вузол, який залишився навколо, що не дозволило б вузлу збирати сміття і, таким чином, зберігати всі вузли у пов'язаному списку з цього моменту живими на купі.
Метод оцінки
Тепер, коли вузол виклику був успішно секвенований, його потрібно оцінити. Для цього першим кроком є переконання, що виклики, що передують цьому, були оцінені. Якщо у них немає цієї теми, вона не чекатиме, але зробить цю роботу негайно.
Метод EnsurePrior
ensurePrior()
Метод робить цю роботу, перевіряючи попередній вузол у зв'язаному списку. Якщо його стан не встановлено, попередній вузол буде оцінено. Вузол, що це рекурсивно. Якщо вузол до попереднього вузла не був оцінений, він буде викликати оцінку для цього вузла тощо.
Тепер, коли, як відомо, попередній вузол має стан, ми можемо оцінити цей вузол. Останній вузол витягується та присвоюється локальній змінній. Якщо ця посилання є нульовою, це означає, що якийсь інший потік попередньо видалив цей і вже оцінив цей вузол; встановлення стану. В іншому випадку стан попереднього вузла передається Sequential
методу застосовування об'єкта разом із викликом цього вузла. Повертається стан встановлюється на вузлі, і truncate()
метод викликається, очищаючи зворотній зв'язок від вузла, оскільки він більше не потрібен.
Метод MoveForward
Метод руху вперед намагатиметься перемістити всі посилання заголовків на цей вузол, якщо вони вже не вказують на щось далі. Це потрібно для того, щоб, якщо потік перестає дзвонити, голова не збереже посилання на вузол, який більше не потрібен. compareAndSet()
Метод буде переконатися , що ми оновлюємо тільки вузол , якщо якийсь - то інший потік не змінив його , так як він був отриманий.
Оголосити масив та допомогти
Ключовим моментом зробити цей підхід без очікування на відміну від просто незамкненого є те, що ми не можемо припустити, що планувальник потоків надасть кожному потоку пріоритет, коли він цього потребує. Якщо кожен потік просто спробував послідовувати власні вузли, можливо, що потоки можуть бути попередньо спущені під навантаженням. Щоб врахувати цю можливість, кожен потік спершу спробує "допомогти" іншим потокам, які можуть не мати можливості секвенуватися.
Основна ідея полягає в тому, що як кожен потік успішно створює вузли, присвоєні послідовності монотонно зростають. Якщо потік або нитки постійно випереджають інший потік, індекс, який використовує для пошуку непослідовних вузлів у announce
масиві, рухатиметься вперед. Навіть якщо кожен потік, який в даний час намагається послідувати даний вузол, постійно попереджується іншим потоком, з часом всі потоки намагатимуться послідувати цей вузол. Для ілюстрації ми побудуємо приклад із трьох ниток.
На початковій точці всі головні та оповіщувальні елементи всіх трьох ниток спрямовані на tail
вузол. lastSequence
Для кожного потоку дорівнює 0.
У цей момент Нитка 1 виконується з викликом. Він перевіряє масив оголошень на його останню послідовність (нуль), який вузол планується індексувати. Це послідовність вузла, і він lastSequence
встановлений на 1.
Нитка 2 тепер виконується з викликом, вона перевіряє масив оголошень у останній послідовності (нульовій) і бачить, що йому не потрібна допомога, і тому намагається послідувати її викликом. Це вдається, і тепер його lastSequence
встановлено на 2.
Нитка 3 тепер виконується, і він також бачить, що вузол at announce[0]
вже секвенсований і послідовності - це власне виклик. Зараз lastSequence
це встановлено на 3.
Тепер Тема 1 викликається знову. Він перевіряє масив оголошень в індексі 1 і виявляє, що він вже секвенсований. Одночасно викликається нитка 2 . Він перевіряє масив оголошень в індексі 2 і виявляє, що він вже секвенсований. І нитка 1, і нитка 2 зараз намагаються послідовно виконувати свої власні вузли. Нитка 2 виграє, і це послідовність її виклику. Встановлено lastSequence
значення 4. Тим часом, три нитки було викликано. Він перевіряє його індекс lastSequence
(mod 3) і виявляє, що вузол at announce[0]
не був секвенсований. Нитка 2 знову викликається в той же час, коли нитка 1 знаходиться у другій спробі. Нитка 1знаходить невикликане виклик, при announce[1]
якому вузол щойно створений Thread 2 . Він намагається послідувати виклик теми 2 і досягає успіху. Нитка 2 знаходить власний вузол у, announce[1]
і він був відстежений. Це встановлено так, lastSequence
щоб це було 5. Нитка 3 потім викликається і виявляє, що вузол, на який розміщена нитка 1 announce[0]
, все ще не є послідовними, і намагається це зробити. Тим часом, нитка 2 також була викликана і попередньо спорожнює нитку 3. Вона послідовно працює з вузлом і встановлює lastSequence
значення 6.
Погана нитка 1 . Навіть незважаючи на те, що Thread 3 намагається послідовно виконувати її, обидва потоки постійно перешкоджають планувальник. Але в цей момент. Нитка 2 також тепер вказує на announce[0]
(6 мод 3). Усі три потоки встановлені для спроби послідовності одного виклику. Незалежно від того, який потік має успіх, наступним вузлом, який буде послідовно, буде виклик очікування з потоку 1, тобто вузол, на який посилається announce[0]
.
Це неминуче. Для того, щоб потоки були попередньо спорожнені, інші потоки повинні бути вузлами послідовності, і коли вони це робитимуть, вони будуть постійно рухатись lastSequence
вперед. Якщо вузол заданого потоку постійно не секвенується, з часом всі потоки будуть вказувати на його індекс у масиві оголошень. Жоден потік не зробить нічого іншого, поки вузол, якому він намагається допомогти, не секвенується, найгірший сценарій полягає в тому, що всі потоки вказують на той самий непідпорядкований вузол. Тому час, необхідний для послідовності будь-якого виклику, є функцією кількості потоків, а не розміру введення.