Заден план
AtomicIntger
е клас, безопасен за нишки, чиито операции са атомарни. Осигурява операции, които манипулират с цели числа, като addAndGet
, getAndAdd
и т.н. AtomicIntger
е полезно в многонишкова среда, когато множество нишки се опитват да актуализират стойността едновременно, но все пак гарантират последователността
В обхвата на тази статия бих искал да покажа различни подходи за внедряване на AtomicInteger
Как се различават един от друг и кой се представя по-добре
Машината се използва за сравнителен анализ има AMD Ryzen™ 7 3700U (4 ядра, 8 нишки, 2,3 GHz) и 14 GB RAM
Подход за заспиване
public int addAndGet(int diff) { synchronized (this) { this.value += diff; return this.value; } }
Идеята на Java synchronous
е в даден момент само една нишка да може да влезе в критичната секция и да получи заключването. И само собственикът на ключалката може да освободи ключалката. Нишките не успяват да придобият ключалката ще заспи и ще чака да бъде събудена от нишката собственик на ключалката.
public addAndGet(I)I ... MONITORENTER ... MONITOREXIT L1 IRETURN L2 LINENUMBER 30 L2 FRAME FULL [com/chien/atomic/MutexAtomicInteger I java/lang/Object] [java/lang/Throwable] ASTORE 3 ALOAD 2 MONITOREXIT L3 ALOAD 3 ATHROW L6 LOCALVARIABLE this Lcom/chien/atomic/MutexAtomicInteger; L4 L6 0 LOCALVARIABLE diff I L4 L6 1 MAXSTACK = 3 MAXLOCALS = 4
От байт кодовете има два специални инструктора MONITORENTER
и MONITOREXIT
. Когато нишка се опитва да получи заключване на обект. Това ще задейства MONITORENTER
Това се прави с помощта на операция за сравняване и размяна. Ако заключването е налично, нишката може да придобие заключването и да влезе в критичната секция. Ако ключалката вече е придобита от друга нишка. Нишката трябва да промени състоянието си на BLOCKED
(това е състояние на ниво JVM, а не състояние на ниво OS) и заспиване. След като нишката на собственика на заключването приключи, тя ще извика MONITOREXIT
и ще се опита да намери нишка BLOCKED
, която чака своя ред да придобие ключалката и да я събуди.
От изхода на vmstat можете да видите, когато има много нишки, които се опитват да извикат addAndGet
едновременно, броят на контекстните превключвания за секунда (cs) скача много високо, но използването на процесора е само около 35%
1090.718 ±(99.9%) 28.689 ns/op [Average] (min, avg, max) = (1084.842, 1090.718, 1100.176), stdev = 7.451 CI (99.9%): [1062.028, 1119.407] (assumes normal distribution)
Резултатът от сравнителния тест на JMH показва, че подходът за заключване в режим на заспиване отнема 28,689 наносекунди за всяка операция. Тя е много близка до pthread_mutex
производителност за заключване/отключване
Подход на заета линия
public int getAndAdd(int diff) { return unsafe.getAndAddInt(this, valueOffset, diff); }
unsafe.getAndAddInt
под капака е заетwhile
цикъл, Нишката продължава да извиква операция за сравняване и обмен, докато не успее да актуализира стойността. Операция за сравняване и обмен се актуализира успешно до нова стойност само ако и само ако стойността в паметта е равна на старата стойност
За разлика от заключването в режим на заспиване, статистиката от vmstat
показва, че когато има голямо съревнование, използването на процесора винаги е 100%, но превключването на контекста е многократно по-ниско от подхода на заключване в режим на заспиване
124.593 ±(99.9%) 3.285 ns/op [Average] (min, avg, max) = (123.583, 124.593, 125.801), stdev = 0.853 CI (99.9%): [121.308, 127.878] (assumes normal distribution)
Резултатът от бенчмарка е доста впечатляващ Отнема около 2,285 наносекунди за завършване на операция, което е около 12,5 пъти по-бързо от подхода за заключване в спящ режим
Заключение
Можете да видите, че в случай на изграждане на AtomicInteger
, подходът на зает цикъл надвишава подхода на спящо заключване по отношение на производителността. Компромисът тук е, че в случай на голямо съревнование вашият процесор ще бъде по-натоварен, ако използвате подхода на цикъла на заетостта