Всі знають, що якщо нам потрібно зробити якусь неатомарну операцію, треба засинхронізуватись.
Давайте так і зробимо. Знизу дефолтний приклад інкрементації змінної з декілької потоків.
public class Test {
public static int counter = 0;
public static Integer LOCK = Integer.MIN_VALUE;
public static void increment() {
// synchronize using lock of wrapper class
synchronized (LOCK) {
counter++;
LOCK++;
}
}
public static void main(String[] args) throws InterruptedException {
// Runnable which processing increment of LOCK variable
Runnable runnable = () -> {
for (int i = 0; i < 1_000_000; i++) {
increment();
}
};
// starting threads
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
Thread t3 = new Thread(runnable);
Thread t4 = new Thread(runnable);
startAndJoin(t1, t2, t3, t4);
System.out.println(counter);
}
private static void startAndJoin(Thread... threads) throws InterruptedException {
for (Thread thread : threads) {
thread.start();
}
for (Thread thread : threads) {
thread.join();
}
}
}
Локом буде виступає об’єкт класу Integer.
Результат виконання main методу “2075471”.
А мало бути 4кк, оскільки кожен із 4 потоків накручує по 1кк:
Чому так відбувається?
Перший потік займає монітор, виконує свою логіку, в цей час інший потік який також хотів прокрутити свою логіку натрапив на монітор, який його не пустив і монітор поклав цей потік собі в чергу(*1).
І повідомив про це операційній системі, яка тепер, при виході першого потоку(вона буде за цим слідкувати) має витягнути інший потік(який “чекав в черзі“) і покласти його в іншу чергу яка називається ready queue, яку ж потім запустить на виконання сама ОС згідно свого планувальника.
Але, від об’єкта нічого не залишилось(*2), бо під час інкрементування першим потоком він зник і на його місце прийшов новий об’єкт(а з ним, його монітор, зі своєю чергою очікуючих потоків).
Тому ті інші потоки/потік, які хотіли виконувати свої операції — вони так їх і не виконають, тому і відбуваються “прослизання“ в інкрементації лічильника.
*1 — кожен монітор володіє зовнішньою чергою, зовнішня вона тому, що за нею слідкує сама ОС.
Це waiting-list потоків, які хочуть зайняти монітор і виконати відповідні операції.
*2 — Числові врапери є імутабельними. Вони всередині реалізовані за патерном "Одинак" і будь-яка спроба підкрутити йому значення/змінити стан об’єкта, призводить до того, що цей об’єкт зникне.