Java. Чому не можна синхронізуватись на об'єктах-враперах?

Всі знають, що якщо нам потрібно зробити якусь неатомарну операцію, треба засинхронізуватись.
Давайте так і зробимо. Знизу дефолтний приклад інкрементації змінної з декілької потоків.

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 — Числові врапери є імутабельними. Вони всередині реалізовані за патерном "Одинак" і будь-яка спроба підкрутити йому значення/змінити стан об’єкта, призводить до того, що цей об’єкт зникне.


https://puredanger.github.io/tech.puredanger.com/2009/01/28/java-concurrency-bugs-synchronize-object/

Поділись своїми ідеями в новій публікації.
Ми чекаємо саме на твій довгочит!
Oleksandr Klymenko
Oleksandr Klymenko@overpathz

Java Software Engineer

4.6KПрочитань
1Автори
71Читачі
На Друкарні з 19 квітня

Більше від автора

  • Secure networking. Deep Dive

    Глибоке занурення в протоколи TLS/SSL та інфраструктуру відкритих ключів (PKI). Основні поняття, процес встановлення захищеного з'єднання, роль сертифікатів та ланцюжка довіри

    Теми цього довгочиту:

    Security
  • Поширені помилки у дизайні REST API

    У довгочиті розглядаються поширені помилки при проектуванні REST API та способи їх уникнення: версіонування, використання DTO, підхід CQRS, робота з мікросервісами, та інші практики для підвищення продуктивності, безпеки й зручності API

    Теми цього довгочиту:

    Java
  • Java. Короткий огляд еволюції багатопотоковості

    У перших версіях Java багатопоточність реалізовувалася за допомогою класу Thread, який дозволяв створювати нові потоки. Проте ця модель мала багато недоліків:

    Теми цього довгочиту:

    Java

Вам також сподобається

Коментарі (0)

Підтримайте автора першим.
Напишіть коментар!

Вам також сподобається