Одновременное добавление потоков в список ArrayList одновременно — что происходит?

#java #parallel-processing #java-stream

Вопрос:

У нас есть несколько потоков, вызывающих add(obj) an ArrayList .

Моя теория заключается в том, что когда add вызывается одновременно двумя потоками, то только один из двух добавляемых объектов действительно добавляется в ArrayList . Можно ли это обосновать?

Если да, то как вы обходите это? Использовать синхронизированную коллекцию, например Vector ?

Ответ №1:

Не существует гарантированного поведения для того, что происходит, когда add вызывается одновременно двумя потоками в списке ArrayList. Тем не менее, по моему опыту, оба объекта были добавлены нормально. Большинство проблем безопасности потоков, связанных со списками, связаны с итерацией при добавлении/удалении. Несмотря на это, я настоятельно рекомендую не использовать ванильный ArrayList с несколькими потоками и параллельным доступом.

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

Также я настоятельно рекомендую параллелизм Java на практике от Goetz и др., если вы собираетесь тратить какое-либо время на работу с потоками на Java. В книге этот вопрос освещен гораздо более подробно.

Комментарии:

1. «по моему опыту, оба объекта были добавлены нормально», просто хочу отметить, что это просто чистая удача. Вероятно, окно для проблемы с повреждением данных в ArrayList чрезвычайно мало, но оно все еще существует

2. Верно, и это в некотором роде моя точка зрения. В подавляющем большинстве случаев проблем не возникнет, но мы не программируем на большинство случаев. Поэтому я настоятельно рекомендовал не использовать ArrayList.

Ответ №2:

Могло случиться что угодно. Вы могли бы правильно добавить оба объекта. Вы могли бы добавить только один из объектов. Вы можете получить исключение ArrayIndexOutOfBounds, поскольку размер базового массива не был отрегулирован должным образом. Или могут произойти другие вещи. Достаточно сказать, что вы не можете полагаться на какое-либо происходящее поведение.

В качестве альтернативы вы могли бы использовать Vector , вы могли бы использовать Collections.synchronizedList , вы могли бы использовать CopyOnWriteArrayList или вы могли бы использовать отдельный замок. Все зависит от того, что еще вы делаете и какой у вас контроль над доступом к коллекции.

Ответ №3:

Вы также можете получить a null , an ArrayOutOfBoundsException или что-то еще , оставшееся до реализации. HashMap было замечено, что s входят в бесконечный цикл в производственных системах. Вам действительно не нужно знать, что может пойти не так, просто не делайте этого.

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

Комментарии:

1. Я также видел null , как добавлялись каждый раз, когда у меня были потоки, выполняемые add() в одно и то же время.

Ответ №4:

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

100 задач выполняются параллельно, и они обновляют свой статус завершенных до основной программы. Я использую обратный отсчет, чтобы дождаться завершения задачи.

 import java.util.concurrent.*;
import java.util.*;

public class Runner {

    // Should be replaced with Collections.synchronizedList(new ArrayList<Integer>())
    public List<Integer> completed = new ArrayList<Integer>();

    /**
     * @param args
     */
    public static void main(String[] args) {
        Runner r = new Runner();
        ExecutorService exe = Executors.newFixedThreadPool(30);
        int tasks = 100;
        CountDownLatch latch = new CountDownLatch(tasks);
        for (int i = 0; i < tasks; i  ) {
            exe.submit(r.new Task(i, latch));
        }
        try {
            latch.await();
            System.out.println("Summary:");
            System.out.println("Number of tasks completed: "
                      r.completed.size());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        exe.shutdown();
    }

    class Task implements Runnable {

        private int id;
        private CountDownLatch latch;

        public Task(int id, CountDownLatch latch) {
            this.id = id;
            this.latch = latch;
        }

        public void run() {
            Random r = new Random();
            try {
                Thread.sleep(r.nextInt(5000)); //Actual work of the task
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            completed.add(id);
            latch.countDown();
        }
    }
}
 

Когда я запускал приложение 10 раз и по крайней мере 3-4 раза, программа не печатала правильное количество выполненных задач. В идеале он должен печатать 100(если исключений не происходит). Но в некоторых случаях это была печать 98, 99 и т. Д.

Таким образом, это доказывает, что одновременные обновления ArrayList не дадут правильных результатов.

Если я заменю список массивов синхронизированной версией, программа выдаст правильные результаты.

Ответ №5:

вы можете использовать List l = Collections.synchronizedList(new ArrayList()); , если хотите потокобезопасную версию ArrayList.

Ответ №6:

Поведение, вероятно, не определено, так как ArrayList не является потокобезопасным. Если вы измените список, пока итератор взаимодействует с ним, вы получите исключение ConcurrentModificationException. Вы можете обернуть список массивов коллекцией.synchronizedList или используйте потокобезопасную коллекцию (их много), или просто поместите вызовы добавления в синхронизированный блок.

Ответ №7:

Вы могли бы использовать вместо ArrayList(); :

 Collections.synchronizedList( new ArrayList() );
 

или

 new Vector();
 

synchronizedList как по мне предпочтительнее, потому что это:

  • быстрее на 50-100%
  • может работать с уже существующими списками ArrayList

Ответ №8:

java.util.concurrent имеет потокобезопасный список массивов. Стандартный список массивов не является потокобезопасным, и поведение, когда несколько потоков обновляются одновременно, не определено. Также может наблюдаться странное поведение с несколькими читателями, когда один или несколько потоков пишут одновременно.

Ответ №9:

http://java.sun.com/j2se/1.4.2/docs/api/java/util/ArrayList.html

Обратите внимание, что эта реализация не синхронизирована. Если несколько потоков одновременно обращаются к экземпляру ArrayList и по крайней мере один из потоков изменяет структуру списка, он должен быть синхронизирован извне.

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

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

Комментарии:

1. Верно, видел это. Я думал, что ConcurrentModificationException был бы выброшен a, чего не происходит для нас.. возможно, мы сталкиваемся с другим побочным эффектом, связанным с тем, что мы не используем потокобезопасную коллекцию..

2. @Marcus это, по-видимому, имеет место только в том случае, если вы изменяете список во время его перебора. Кроме того, нет никакой гарантии, что это исключение будет выдано.