Заден план

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, подходът на зает цикъл надвишава подхода на спящо заключване по отношение на производителността. Компромисът тук е, че в случай на голямо съревнование вашият процесор ще бъде по-натоварен, ако използвате подхода на цикъла на заетостта