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

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

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

  • Як насправді працює @Async у Spring і коли його використання створює більше проблем, ніж вирішує

    Розбираємо небезпеки анотації @Async у Spring — як вона працює за кулісами, чому втрачається контекст логування, підводні камені з транзакціями та self-invocation

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

    Java
  • RFC 7807. Що це і для чого він потрібен бекенд розробникам

    Як стандарт RFC 7807 змінює підхід до обробки помилок у Java розробці. У статті: що це таке, як працює формат "Problem Details", приклади використання та готовий код для інтеграції у Spring Boot

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

    Java
  • Java. jOOQ

    Довгочит буде про jOOQ — бібліотеку, яка зручно поєднує світ Java і SQL. Якщо ви працюєте з базами даних у Java, то, скоріш за все, зустрічались з такими дилемами:

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

    Java

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

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

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

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