Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Как сделать счётчик потокобезопасным

Короткий ответ

AtomicInteger увеличивает значение атомарно — как одну неделимую операцию, даже если потоков много:

void main() {
    var counter = new AtomicInteger();
 
    try (var pool = Executors.newFixedThreadPool(4)) {
        for (int i = 0; i < 10_000; i++) {
            pool.submit(counter::incrementAndGet);
        }
    }
    IO.println(counter.get());
}

Результат:

10000

synchronized

Блок synchronized пускает внутрь только один поток за раз. Это подходит, когда нужно изменить несколько значений вместе:

class Account {
    private int balance;
    private int operations;
 
    synchronized void deposit(int amount) {
        balance += amount;
        operations++;
    }
 
    synchronized String summary() {
        return balance + " in " + operations + " operations";
    }
}
 
void main() {
    var account = new Account();
    try (var pool = Executors.newFixedThreadPool(4)) {
        for (int i = 0; i < 1000; i++) {
            pool.submit(() -> account.deposit(5));
        }
    }
    IO.println(account.summary());
}

Результат:

5000 in 1000 operations

Счётчики по ключам

ConcurrentHashMap.merge() безопасно обновляет значение по ключу:

void main() {
    var hits = new ConcurrentHashMap<String, Integer>();
    var pages = List.of("home", "blog", "home");
 
    try (var pool = Executors.newFixedThreadPool(4)) {
        for (int i = 0; i < 1000; i++) {
            String page = pages.get(i % pages.size());
            pool.submit(() -> hits.merge(page, 1, Integer::sum));
        }
    }
    IO.println(new TreeMap<>(hits));
}

Результат:

{blog=333, home=667}

Частая ошибка

Увеличивать обычное поле через count++ из нескольких потоков. Операция состоит из чтения, сложения и записи, и потоки затирают результаты друг друга:

int count = 0;
 
void main() {
    try (var pool = Executors.newFixedThreadPool(4)) {
        for (int i = 0; i < 100_000; i++) {
            pool.submit(() -> count++);
        }
    }
    IO.println(count);
}

Результат:

99430

Результат почти всегда меньше 100 000 и меняется от запуска к запуску.

Функции из этого ответа

  • AtomicInteger — Целое число, которое можно безопасно менять из нескольких потоков без блокировок.
  • synchronized — Разрешает выполнять блок кода только одному потоку за раз.
  • ConcurrentHashMap — Потокобезопасный словарь для одновременной работы из многих потоков.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.