Когда AtomicInteger предпочтительнее, чем синхронизированный?

Поскольку AtomicInteger может быть как минимум на порядок медленнее, чем int, защищенный synchronized, зачем мне вообще использовать AtomicInteger?

Например, если все, что я хочу, это увеличить значение int потокобезопасным способом, почему бы всегда использовать:

synchronized(threadsafeint) {
  threadsafeint++;
}

вместо использования гораздо более медленного AtomicInteger.incrementAndGet ()?


person ef2011    schedule 26.07.2012    source источник


Ответы (2)


Поскольку AtomicInteger может быть как минимум на порядок медленнее, чем int, защищенный с помощью synchronized, зачем мне вообще использовать AtomicInteger?

AtomicInteger намного быстрее.

static final Object LOCK1 = new Object();
static final Object LOCK2 = new Object();
static int i1 = 0;
static int i2 = 0;
static final AtomicInteger ai1 = new AtomicInteger();
static final AtomicInteger ai2 = new AtomicInteger();

public static void main(String... args) throws IOException {
    for(int i=0;i<5;i++) {
        testSyncInt();
        testAtomicInt();
    }
}

private static void testSyncInt() {
    long start = System.nanoTime();
    int runs = 10000000;
    for(int i=0;i< runs;i+=2) {
        synchronized (LOCK1) {
            i1++;
        }
        synchronized (LOCK2) {
            i2++;
        }
    }
    long time = System.nanoTime() - start;
    System.out.printf("sync + incr: Each increment took an average of %.1f ns%n", (double) time/runs);
}

private static void testAtomicInt() {
    long start = System.nanoTime();
    int runs = 10000000;
    for(int i=0;i< runs;i+=2) {
        ai1.incrementAndGet();
        ai2.incrementAndGet();
    }
    long time = System.nanoTime() - start;
    System.out.printf("incrementAndGet: Each increment took an average of %.1f ns%n", (double) time/runs);
}

отпечатки

sync + incr: Each increment took an average of 32.4 ns
incrementAndGet: Each increment took an average of 20.6 ns
sync + incr: Each increment took an average of 31.4 ns
incrementAndGet: Each increment took an average of 12.9 ns
sync + incr: Each increment took an average of 29.6 ns
incrementAndGet: Each increment took an average of 12.9 ns
sync + incr: Each increment took an average of 35.1 ns
incrementAndGet: Each increment took an average of 16.6 ns
sync + incr: Each increment took an average of 29.9 ns
incrementAndGet: Each increment took an average of 13.0 ns

Добавление некоторых разногласий, как предлагает @assylias. Это показывает, что когда вы действительно используете только один поток, ЦП может оптимизировать доступ.

static final Object LOCK1 = new Object();
static final Object LOCK2 = new Object();
static int i1 = 0;
static int i2 = 0;
static final AtomicInteger ai1 = new AtomicInteger();
static final AtomicInteger ai2 = new AtomicInteger();

public static void main(String... args) throws  ExecutionException, InterruptedException {
    for(int i=0;i<5;i++) {
        testSyncInt();
        testAtomicInt();
    }
}

private static void testSyncInt() throws ExecutionException, InterruptedException {
    long start = System.nanoTime();
    final int runs = 1000000;
    ExecutorService es = Executors.newFixedThreadPool(2);
    List<Future<Void>> futures = new ArrayList<>();
    for(int t=0;t<8;t++) {
        futures.add(es.submit(new Callable<Void>() {
            public Void call() throws Exception {
                for (int i = 0; i < runs; i += 2) {
                    synchronized (LOCK1) {
                        i1++;
                    }
                    synchronized (LOCK2) {
                        i2++;
                    }
                }
                return null;
            }
        }));
    }
    for (Future<Void> future : futures) {
        future.get();
    }
    es.shutdown();
    long time = System.nanoTime() - start;
    System.out.printf("sync + incr: Each increment took an average of %.1f ns%n", (double) time/runs/2);
}

private static void testAtomicInt() throws ExecutionException, InterruptedException {
    long start = System.nanoTime();
    final int runs = 1000000;
    ExecutorService es = Executors.newFixedThreadPool(2);
    List<Future<Void>> futures = new ArrayList<>();
    for(int t=0;t<8;t++) {
        futures.add(es.submit(new Callable<Void>() {
            public Void call() throws Exception {
                for (int i = 0; i < runs; i += 2) {
                    ai1.incrementAndGet();
                    ai2.incrementAndGet();
                }
                return null;
            }
        }));
    }
    for (Future<Void> future : futures) {
        future.get();
    }
    es.shutdown();
    long time = System.nanoTime() - start;
    System.out.printf("incrementAndGet: Each increment took an average of %.1f ns%n", (double) time/runs/2);
}

отпечатки

sync + incr: Each increment took an average of 478.6 ns
incrementAndGet: Each increment took an average of 191.5 ns
sync + incr: Each increment took an average of 437.5 ns
incrementAndGet: Each increment took an average of 169.8 ns
sync + incr: Each increment took an average of 408.1 ns
incrementAndGet: Each increment took an average of 180.8 ns
sync + incr: Each increment took an average of 511.5 ns
incrementAndGet: Each increment took an average of 313.4 ns
sync + incr: Each increment took an average of 441.6 ns
incrementAndGet: Each increment took an average of 219.7 ns
person Peter Lawrey    schedule 26.07.2012
comment
Я теперь действительно смущен. Это не то, что я понял из ответа @Gray здесь: stackoverflow.com/a/11125474/722603 Что мне не хватает? - person ef2011; 26.07.2012
comment
Я думаю, он имеет в виду, что AtomicInteger медленнее, чем несинхронизированный int; он говорит о том, почему вы не должны заменять всех своих int членов на AtomicInteger без объяснения причин. Но это не на порядок медленнее, не более, чем на порядок быстрее. По большей части это все небольшие различия, о которых мы говорим. - person Ernest Friedman-Hill; 26.07.2012
comment
Помню, я где-то читал, что механизм блокировки также отличается для вещей Atomic, но прямо сейчас не смог получить эту ссылку. Я тоже могу ошибаться. - person kosa; 26.07.2012
comment
@thinksteep - атомарные типы обычно не используют блокировку. Они используют атомарные инструкции сравнения и замены и тому подобное. Ссылка: ibm.com/developerworks/java/library/j-jtp11234 - person Stephen C; 26.07.2012
comment
@StephenC: Спасибо за ссылку, она освежит мое понимание. - person kosa; 26.07.2012
comment
@PeterLawrey Вы были нежны - вы могли бы добавить немного разногласий, чтобы аргументировать CAS против монитора ;-) - person assylias; 26.07.2012
comment
Ух ты. Если это так, то ответ прост: когда вам нужна дополнительная производительность, и AtomicInteger могут по-прежнему удовлетворять требованиям безопасности потоков (хотя иногда это может быть сложно в отношении условий гонки). Я правильно понял на этот раз? - person ef2011; 26.07.2012
comment
@assylias Это значительно замедлило обновления, но относительный результат остался прежним. - person Peter Lawrey; 26.07.2012
comment
@ef2011 ef2011 Я согласен, что AtomicInteger — лучший выбор, если он делает именно то, что вам нужно. Если у вас есть другие операции, которые вам нужны, это может быть не так. - person Peter Lawrey; 26.07.2012
comment
@PeterLawrey Интересно - я бы думал иначе. Спасибо за обновления. - person assylias; 26.07.2012
comment
Проверка JCiP 15.3.2 — эффект кажется более заметным при умеренном уровне конкуренции, но он может измениться при высоком уровне конкуренции. - person assylias; 26.07.2012
comment
При 8 потоках на 8 ядрах производительность та же. Если это все, что делает ваша программа, вам нужен редизайн ИМХО. ;) - person Peter Lawrey; 26.07.2012

если вы действительно хотите получить более подробную информацию о том, почему java.util.concurrent лучше и в чем разница по сравнению с классическим синхронизированным подходом, прочитайте эта ссылка (и вообще весь блог)

person Andrey Borisov    schedule 26.07.2012