Въведение в атомните променливи в Java

1. Въведение

Просто казано, споделеното изменяемо състояние много лесно води до проблеми, когато е налице паралелност. Ако достъпът до споделени променливи обекти не се управлява правилно, приложенията могат бързо да станат податливи на някои трудно откриваеми грешки при съвпадение.

В тази статия ще преразгледаме използването на ключалки за обработка на едновременен достъп, ще изследваме някои недостатъци, свързани със заключванията, и накрая ще въведем атомни променливи като алтернатива.

2. Брави

Нека да разгледаме класа:

public class Counter { int counter; public void increment() { counter++; } }

В случай на еднонишна среда това работи перфектно; обаче, щом позволим да пишем повече от една нишка, започваме да получаваме противоречиви резултати.

Това се дължи на простата операция за увеличаване ( брояч ++ ), която може да изглежда като атомна операция, но всъщност е комбинация от три операции: получаване на стойността, увеличаване и записване на актуализираната стойност обратно.

Ако две нишки се опитат да получат и актуализират стойността едновременно, това може да доведе до загубени актуализации.

Един от начините за управление на достъпа до обект е използването на ключалки. Това може да се постигне чрез използване на синхронизираната ключова дума в подписа на метода за увеличаване . На синхронизирани , осигурява ключови думи, които само една нишка могат да влизат в метода по едно време (за да научите повече за заключване и синхронизиране отнасят до - Ръководство за синхронно ключови думи в Java):

public class SafeCounterWithLock { private volatile int counter; public synchronized void increment() { counter++; } }

Освен това трябва да добавим летливата ключова дума, за да осигурим правилна видимост на референцията между нишките.

Използването на ключалки решава проблема. Изпълнението обаче е хит.

Когато няколко нишки се опитват да получат ключалка, една от тях печели, докато останалите нишки са или блокирани, или спряни.

Процесът на спиране и след това възобновяване на нишка е много скъп и засяга общата ефективност на системата.

В малка програма, като брояча , времето, прекарано в превключване на контекста, може да стане много повече от действителното изпълнение на кода, като по този начин значително намалява общата ефективност.

3. Атомни операции

Има клон от изследвания, фокусиран върху създаването на неблокиращи алгоритми за едновременна среда. Тези алгоритми използват инструкции за ниско ниво на атомна машина, като сравнение и размяна (CAS), за да осигурят целостта на данните.

Типична CAS операция работи върху три операнда:

  1. Местоположението в паметта, на което да работите (M)
  2. Съществуващата очаквана стойност (A) на променливата
  3. Новата стойност (B), която трябва да бъде зададена

Операцията CAS актуализира атомно стойността в M до B, но само ако съществуващата стойност в M съвпада с A, в противен случай не се предприемат действия.

И в двата случая се връща съществуващата стойност в M. Това съчетава три стъпки - получаване на стойността, сравняване на стойността и актуализиране на стойността - в една операция на ниво машина.

Когато няколко нишки се опитват да актуализират една и съща стойност чрез CAS, една от тях печели и актуализира стойността. Въпреки това, за разлика от бравите, никоя друга нишка не се спира ; вместо това те просто са информирани, че не са успели да актуализират стойността. След това нишките могат да продължат да извършват по-нататъшна работа и превключванията на контекста са напълно избегнати.

Друго следствие е, че основната програмна логика става по-сложна. Това е така, защото трябва да се справим със сценария, когато операцията CAS не е успяла. Можем да опитаме отново и отново, докато успее, или не можем да направим нищо и да продължим в зависимост от случая на употреба.

4. Атомни променливи в Java

Най-често използваните класове на атомни променливи в Java са AtomicInteger, AtomicLong, AtomicBoolean и AtomicReference. Тези класове представляват съответно int , long , boolean и object reference, които могат да бъдат атомно актуализирани. Основните методи, изложени от тези класове, са:

  • get () - получава стойността от паметта, така че промените, направени от други нишки, да са видими; еквивалентно на четене на променлива променлива
  • set () - записва стойността в паметта, така че промяната да е видима за други нишки; еквивалентно на писане на променлива променлива
  • lazySet () - в крайна сметка записва стойността в паметта, може би пренаредена с последващи съответни операции с памет. Един от случаите на употреба са анулиране на препратки, заради събирането на боклука, което никога повече няма да бъде достъпно. В този случай се постига по-добра производителност чрез забавяне на нулевото изменчиво писане
  • compareAndSet () - същото, както е описано в раздел 3, връща true, когато успее, иначе false
  • слабCompareAndSet () - същото, както е описано в раздел 3, но по-слабо в смисъл, че не създава поръчки, случващи се преди. Това означава, че може да не вижда непременно актуализации на други променливи. От Java 9 този метод е остарял във всички атомни изпълнения в полза на слабCompareAndSetPlain () . Ефектите от паметта на слабCompareAndSet () бяха обикновени, но имената му предполагаха нестабилни ефекти на паметта. За да се избегне това объркване, те отхвърлиха този метод и добавиха четири метода с различни ефекти на паметта като слабCompareAndSetPlain () или слабCompareAndSetVolatile ()

Безопасен за нишки брояч, реализиран с AtomicInteger, е показан в примера по-долу:

public class SafeCounterWithoutLock { private final AtomicInteger counter = new AtomicInteger(0); public int getValue() { return counter.get(); } public void increment() { while(true) { int existingValue = getValue(); int newValue = existingValue + 1; if(counter.compareAndSet(existingValue, newValue)) { return; } } } }

Както можете да видите, ние повторете compareAndSet операцията и отново на неуспех, тъй като искаме да гарантира, че поканата към нарастване метод винаги увеличава стойността от 1.

5. Заключение

В този бърз урок описахме алтернативен начин за работа с паралелност, при който недостатъците, свързани със заключването, могат да бъдат избегнати. Също така разгледахме основните методи, изложени от класовете на атомните променливи в Java.

Както винаги, всички примери са налични в GitHub.

За да изследвате повече класове, които вътрешно използват неблокиращи алгоритми, вижте ръководство за ConcurrentMap.