Атомарные переменные и синхронизированные методы

У меня есть класс счетчика, у которого есть метод увеличения и уменьшения, оба метода синхронизированы.

public class Counter {
   int count = 0;
   public synchronized void increment(){
       count++;
   }

   public synchronized void decrement(){
       count--;
   }
}

Из этого примера очень ясно, что состояние гонки не произойдет, только один поток может получить доступ к методу увеличения или уменьшения.

Теперь вместо целочисленных примитивов, если я изменил класс счетчика с помощью Atomic Integer и удалил ключевое слово synchronized, сможем ли мы добиться того же?

public class Counter {
    AtomicInteger count = new AtomicInteger();

    public void increment(){
       count.incrementAndGet();
    }

    public void decrement(){
       count.decrementAndGet();
    }
}
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
11
0
4 324
2

Ответы 2

Что ж, это тот вопрос, в котором вы могли бы написать быструю и грязную программу для оценки:

public class ThreadExperiment {

    public static class CounterSync {
        int count = 0;
        public synchronized void increment(){
            count++;
        }

        public synchronized void decrement(){
            count--;
        }
    }

    public static class CounterAtomic {
        AtomicInteger count = new AtomicInteger();

        public  void increment(){
            count.incrementAndGet();
        }

        public void decrement(){
            count.decrementAndGet();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        final CounterSync counterSync = new CounterSync();
        final CounterAtomic counterAtomic = new CounterAtomic();

        Runnable runnable = () -> {
            for (int i = 0; i < 1_000_000; i++) {
                int j = i & 1;
                if (j == 0) {
                    counterSync.increment();
                    counterAtomic.increment();
                } else {
                    counterSync.decrement();
                    counterAtomic.decrement();
                }
            }
        };

        Thread[] threads = new Thread[10];

        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(runnable);
        }

        for (Thread t : threads)
            t.start();

        for (Thread t : threads)
            t.join();

        System.out.println("Sync count = " + counterSync.count);
        System.out.println("Atomic count = " + counterAtomic.count.intValue());
    }
}

В конце концов, оба счетчика должны и будут равны 0. Удалив "синхронизированные" ключевые слова, вы увидите, что все идет совсем не так.

Так что да, оба достигают одного и того же: безопасности потоков.

Oracle документирует этот пример здесь: https://docs.oracle.com/javase/tutorial/essential/concurrency/atomicvars.html

For this simple class, synchronization is an acceptable solution. But for a more complicated class, we might want to avoid the liveness impact of unnecessary synchronization. Replacing the int field with an AtomicInteger allows us to prevent thread interference without resorting to synchronization

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

спасибо за ответ, но мой вопрос в том, в каких случаях мы можем использовать атомарные переменные, а когда мы можем использовать "синхронизированные"

Kuldeep 21.05.2018 11:03

Что ж, лучше, если возможно, атомарность превосходит синхронизацию. Синхронизация методов имеет широкий (r) «блокирующий» эффект. Я рекомендую прочитать следующую ссылку и все ее подстраницы: docs.oracle.com/javase/tutorial/essential/concurrency/sync.h‌ tml

Marcus K. 21.05.2018 11:07

@MarcusK. небольшой пример ничего не доказывает - за исключением того, что он работает в текущей среде для фактического выполнения. правильный способ доказать это ТОЛЬКО документацией

Eugene 21.05.2018 11:19

Хорошо, это может быть слишком общим. Подумайте о классе с 5 методами или около того. И они делают довольно сложные (отнимающие много времени) вещи. Если вы синхронизируете каждый метод, то только один поток за раз может вызывать метод вашего объекта, даже если другие потоки вызывают другие методы. Синхронизированная блокировка блокирует весь объект, а не только поле в вашем классе, которое вы хотите синхронизировать. Используя Atomic___, все потоки могут получить доступ к вашему объекту одновременно. - Вам нужно хорошо подумать, будет ли код вашего приложения корректно работать без этого условия. Если нет, лучше ограничьтесь и синхронизируйте.

Marcus K. 21.05.2018 11:20

@Eugene Конечно, это не доказательство. Иногда даже документация не является доказательством, поскольку детали реализации могли измениться или условия гонки не были приняты во внимание. Что касается заданного вопроса, я думаю, что этот пример достаточно хорош, чтобы показать эффект, по крайней мере, если вы удалите синхронизированные ключевые слова. Но не стесняйтесь делать лучший ответ, включая ссылки на соответствующую спецификацию Java :-)

Marcus K. 21.05.2018 11:26

@MarcusK. в этом случае вы могли бы сделать это намного проще, я думаю: AtomicInteger ai = new AtomicInteger(); IntStream.range(0, 100_000_000) .parallel() .forEach(x -> ai.incrementAndGet()); System.out.println(ai.get() == 100_000_000);

Eugene 21.05.2018 11:33

@Eugene, если вы хотите показать, что AtomicInteger работает и используется как минимум Java 8, да, вы правы. Вот почему я начал с «быстрого и грязного», так что даже начинающие программисты могут попробовать, не имея глубоких знаний об элегантном, но сложном Streaming API.

Marcus K. 21.05.2018 11:42

Разве CounterSync.count не должен быть атомарным? В противном случае метод increment можно вызывать из потоков в разных процессорах, а значение counter можно кэшировать в кеше процессора.

Pablo Cavalieri 10.12.2019 19:41

Начиная с java-8, вместо AtomicInteger есть опция лучше (более быстрое чтение в действительно конкурирующих средах), она называется LongAdder и таким же образом гарантирует атомарные обновления:

Under low update contention, the two classes have similar characteristics (AtomicLong). But under high contention, expected throughput of this class is significantly higher, at the expense of higher space consumption.

Его реализация великолепна. Говоря простым языком (упрощенно): если нет конфликта между потоками, не будь умницей и просто работай как с AtomicLong; если есть, попробуйте создать отдельное рабочее пространство для каждого потока, чтобы свести к минимуму конкуренцию.

В нем есть те же методы, которые вам нужны: add и decrement.

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

Attila 22.05.2019 11:13

Другие вопросы по теме