Локальные переменные и потоки

Помогите, пожалуйста, разобраться, что происходит "под капотом" у следующего кода:

    public void incrementSharedAtomic() throws InterruptedException {
        AtomicInteger sharedAtomic = new AtomicInteger(0);

        Thread thread1 = new Thread(() -> {
            sharedAtomic.incrementAndGet();
        });

        Thread thread2 = new Thread(() -> {
            sharedAtomic.incrementAndGet();
        });

        thread1.start();
        thread2.start();

        Thread.sleep(3000);
        System.out.println("Thread: result " + sharedAtomic.get());
        
        //output "Thread: result 2"
    }

Я читала, что у каждого потока свои копии локальных переменных, а в методе у нас переменные локальные, поэтому у меня получаются следующие рассуждения:

  1. Каждый поток (thread1 и thread2) получает свою копию sharedAtomic.
  2. AtomicInteger является ссылочным типом и хранится в куче, поэтому потоки получают не копию самого объекта, а копию его ссылки на объект в куче.
  3. Обе копии ссылки ссылаются на один и тот же объект в куче, поэтому incrementAndGet() изменяет значение у объекта в куче

Подскажите, пожалуйста, правильны ли рассуждения? Правильно ли я понимаю, что значение sharedAtomic будет видно обоим потокам (как если бы это было поле класса или поле с класса с volatile) и если выполнить по 10000 вызовов в каждом потоке, то итоговое значение будет 20000?


Ответы (1 шт):

Автор решения: Alex Krass

Да, все правильно, но стоит упомянуть еще пару моментов. В принципе вы их и так рассмотрите при дальнейшем изучении многопоточности и как с ними бороться, можно не вчитываться особо, но на всякий случай пусть будут.

  1. Типы копируются, но так же стоит иметь ввиду, что Java накладывает ограничения. Изначальная переменная используемая в lambda должна быть неизменяемая в дальнейшем.

Изменение ссылки приведет к ошибке, IDE такие моменты подсвечивает.

public static void main(String[] args) {
    Food food = new Food();

    Thread thread = new Thread(() -> {
        food.setType(1);

        // Error: Variable used in lambda expression should be final or effectively final
        // food = new Food();
    });

    // Error: Variable used in lambda expression should be final or effectively final
    // food = new Food();
}

Но если исходную переменную нужно будет изменить вне lambda функции, можно скопировать ссылку в другую переменную и использовать эту новую переменную в потоках.

  1. Класс AtomicInteger обеспечивает внутри атомарность операции.

При его использовании вы получите спокойно 20000. Если использовать volatile или просто обычный объект как в примере выше, то при определенных стечениях обстоятельств вы получите неверные результаты.

Thread thread = new Thread(() -> {
    food.setPrice(food.getPrice() + 20);
});

В примере выше, если вызывается много потоков, то один может считать цену (допустим 100) и не успеть её обновить. Второй поток вклинится в код и считает "старую цену" (опять 100). В итоге вы получите не 140, а 120 из-за этого. Поэтому с многопоточностью стоит быть внимательным.

→ Ссылка