Приращение AtomicInteger ведет себя не так, как ожидалось

Я читал об AtomicInteger и о том, как его операции атомарны и как эти свойства делают его полезным для многопоточности.

Я написал следующую программу, чтобы проверить то же самое.

Я ожидаю, что окончательный размер набора должен быть 1000, так как каждый поток зацикливается 500 раз и предполагаю, что каждый раз, когда поток вызывает getNext(), он должен получить уникальный номер.

Но результат всегда меньше 1000. Что мне здесь не хватает?

public class Sequencer {

private final AtomicInteger i = new AtomicInteger(0);

public int getNext(){
    return i.incrementAndGet();
}

public static void main(String[] args) {

    final Sequencer seq = new Sequencer();

    final Set<Integer> set = new HashSet<Integer>();

    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i=0; i<500; i++)
                set.add(seq.getNext());

        }
    },"T1");
    t1.start();


    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i=0; i<500; i++)
                set.add(seq.getNext());

        }
    },"T2");

    t2.start();

    try {
        t1.join();
        t2.join();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    System.out.println(set.size());

}

}


person Kapil Raju    schedule 25.01.2014    source источник


Ответы (4)


Вы упускаете из виду, что HashSet не является потокобезопасным. Кроме того, свойства набора сотрут все повторяющиеся числа, поэтому ваш тест завершится ошибкой, если AtomicInteger не будет потокобезопасным.

Вместо этого попробуйте использовать ConcurrentLinkedQueue.

Редактировать: Потому что это было задано дважды: использование синхронизированного набора работает, но разрушает идею использования алгоритма без блокировки, такого как классы Atomic. Если в приведенном выше коде вы замените набор синхронизированным набором, то потоки должны будут блокироваться каждый раз, когда вызывается add.

Это эффективно уменьшит ваше приложение до однопоточного, потому что единственная выполняемая работа происходит синхронно. На самом деле он будет даже медленнее, чем однопоточный, потому что synchronized тоже берет свое. Поэтому, если вы действительно хотите использовать многопоточность, старайтесь избегать synchronized любой ценой.

person TwoThe    schedule 25.01.2014
comment
Я попробовал это - final Set<Integer> set = Collections.synchronizedSortedSet(new TreeSet<Integer>()); и это сработало. Спасибо, парни. - person Kapil Raju; 25.01.2014
comment
Японял твою точку зрения. Идея использования Set выше заключалась в том, чтобы проверить: а) если AtomicInteger incrementAndGet() является атомарным, б) если операция является атомарной, set вернет мне size() 1000, если они уникальны. Но я не понимал, что операции Hashset не являются потокобезопасными и вызовут эту проблему. Когда я переключился на synchronizedSortedSet, я протестировал как AtomicInteger, так и примитивный int и обнаружил, что с AtomicInteger счетчик всегда был 1000, а с примитивным int он не был последовательным. Но я согласен с вашей точкой зрения, что если вы запустите потокобезопасную операцию с ненужной синхронизацией, это не принесет пользы. - person Kapil Raju; 26.01.2014

HashSet не является потокобезопасным, поэтому вы сталкиваетесь с проблемой. Вы можете использовать Vector или любой класс коллекции, который является потокобезопасным, или запускать два потока последовательно, если вам строго необходимо использовать HashSet.

t1.start();
t1.join();

t2.start();
t2.join();
person Kick    schedule 25.01.2014
comment
если я запускаю два потока последовательно, это превзойдет цель моего теста :) - person Kapil Raju; 26.01.2014

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

Во-первых, давайте проверим ради вашего теста, что AtomicInteger действительно потокобезопасен, а затем перейдем к тому, почему ваш тест не удался. Немного измените свой тест. Используйте два набора хэшей, по одному для каждого потока. Наконец, после соединений объедините второй набор с первым, перебирая второй и добавляя его к первому, что устранит дубликаты (свойство набора). Затем сделайте счет в первом подходе. Счет будет таким, как вы ожидаете. Это доказывает, что HashSet не является потокобезопасным, а не AtomicInteger.

Итак, давайте посмотрим, какой аспект не является потокобезопасным. Вы делаете только add(), поэтому ясно, что add() - это операция, которая не является потокобезопасной, что приводит к потере чисел. Давайте посмотрим на пример псевдокода HashMap add(), небезопасного для потоков, который будет терять числа (очевидно, это не так, как он реализован, просто попробуем указать один способ, которым он может быть небезопасным для потоков):

class HashMap {
 int valueToAdd;
 public add(int valueToAdd) {
   this.valueToAdd = valueToAdd;
   addToBackingStore(this.valueToAdd);
 }
}

Если несколько потоков вызывают add() и все они достигают addToBackingStore() после изменения this.valueToAdd, добавляется только окончательное значение valueToAdd, все остальные значения перезаписываются и теряются.

Что-то подобное, вероятно, произошло в вашем тесте.

person user2565750    schedule 01.04.2015

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

public class Sequencer {

    private final AtomicInteger i = new AtomicInteger(0);

    public static void main(String[] args) {

        final Sequencer seq = new Sequencer();

        final Set<Integer> notSafe = new HashSet<Integer>();
        final Set<Integer> set = Collections.synchronizedSet(notSafe);
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 500; i++)
                    set.add(seq.getNext());

            }
        }, "T1");
        t1.start();


        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 500; i++)
                    set.add(seq.getNext());

            }
        }, "T2");

        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(set.size());

    }

    public int getNext() {
        return i.incrementAndGet();
    }
}
person RMachnik    schedule 25.01.2014
comment
Хотя синхронизированный набор будет работать, он полностью лишает смысла использовать атомарные алгоритмы lock-free. - person TwoThe; 25.01.2014
comment
Почему? Я думаю, что это никак не повлияет на то, что в первую очередь набор может содержать только уникальные значения, поэтому, если бы у вас не было atomicInteger, у вас также была бы плохая синхронизация, даже если будет синхронизированный набор, потому что иногда он будет добавлять 1 и добавлять 1, так что набор вообще не позволит вам добавить два одинаковых значения. Поэтому я думаю, что concurrentHashSet не нарушит эту концепцию этого вопроса. - person RMachnik; 25.01.2014
comment
Я понятия не имею, что вы говорите, извините. Но я добавил правку в свой пост, может быть, это поможет. - person TwoThe; 26.01.2014
comment
Хотя синхронизированный набор будет работать, он полностью лишает смысла использовать атомарные алгоритмы без блокировок. -› Это не разрушит его. Вы должны использовать atomic int или какую-то блокировку, потому что «++» не является атомарной операцией :) Другое дело, что вы храните свои данные. Если вы используете atomic int и сохраняете результаты в несинхронизированном контейнере, это также не будет работать вообще, вам нужно использовать все синхронизированные и потокобезопасные вещи, если весь алгоритм должен быть потокобезопасным и работать в многопоточном окружении. :) Так что, по сути, ваша первая похвала под моим ответом бесполезна и бессмысленна. - person RMachnik; 26.01.2014