У меня есть класс счетчика, у которого есть метод увеличения и уменьшения, оба метода синхронизированы.
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();
}
}




Что ж, это тот вопрос, в котором вы могли бы написать быструю и грязную программу для оценки:
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
В конце концов, поведение ваших методов будет иметь значение, если вам нужна явная синхронизация или если будет достаточно атомарных полей.
Что ж, лучше, если возможно, атомарность превосходит синхронизацию. Синхронизация методов имеет широкий (r) «блокирующий» эффект. Я рекомендую прочитать следующую ссылку и все ее подстраницы: docs.oracle.com/javase/tutorial/essential/concurrency/sync.h tml
@MarcusK. небольшой пример ничего не доказывает - за исключением того, что он работает в текущей среде для фактического выполнения. правильный способ доказать это ТОЛЬКО документацией
Хорошо, это может быть слишком общим. Подумайте о классе с 5 методами или около того. И они делают довольно сложные (отнимающие много времени) вещи. Если вы синхронизируете каждый метод, то только один поток за раз может вызывать метод вашего объекта, даже если другие потоки вызывают другие методы. Синхронизированная блокировка блокирует весь объект, а не только поле в вашем классе, которое вы хотите синхронизировать. Используя Atomic___, все потоки могут получить доступ к вашему объекту одновременно. - Вам нужно хорошо подумать, будет ли код вашего приложения корректно работать без этого условия. Если нет, лучше ограничьтесь и синхронизируйте.
@Eugene Конечно, это не доказательство. Иногда даже документация не является доказательством, поскольку детали реализации могли измениться или условия гонки не были приняты во внимание. Что касается заданного вопроса, я думаю, что этот пример достаточно хорош, чтобы показать эффект, по крайней мере, если вы удалите синхронизированные ключевые слова. Но не стесняйтесь делать лучший ответ, включая ссылки на соответствующую спецификацию Java :-)
@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, если вы хотите показать, что AtomicInteger работает и используется как минимум Java 8, да, вы правы. Вот почему я начал с «быстрого и грязного», так что даже начинающие программисты могут попробовать, не имея глубоких знаний об элегантном, но сложном Streaming API.
Разве CounterSync.count не должен быть атомарным? В противном случае метод increment можно вызывать из потоков в разных процессорах, а значение counter можно кэшировать в кеше процессора.
Начиная с 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.
Соревнование означает конкуренцию за ресурсы. Этот термин используется особенно в сетях для описания ситуации, когда два или более узла пытаются передать сообщение по одному и тому же проводу в одно и то же время.
спасибо за ответ, но мой вопрос в том, в каких случаях мы можем использовать атомарные переменные, а когда мы можем использовать "синхронизированные"