Лучший подход к использованию в Java 6 для одновременного доступа к списку

У меня есть доступ к объекту List из нескольких потоков. Обычно список обновляется одним потоком, а в некоторых случаях - двумя потоками. Есть от одного до пяти потоков, которые могут читать из этого списка, в зависимости от количества обрабатываемых пользовательских запросов. Список - это не очередь задач, которые нужно выполнить, это список объектов домена, которые извлекаются и обновляются одновременно.

Теперь есть несколько способов сделать доступ к этому списку поточно-ориентированным:
-использовать синхронизированный блок
-использовать обычный Замок (т.е. операции чтения и записи имеют одну и ту же блокировку)
-используем ReadWriteLock
-использовать один из новых классов коллекции ConcurrentBLABLBA

Мой вопрос:
Каков оптимальный подход к использованию, учитывая, что критические разделы обычно не содержат большого количества операций (в основном просто добавления / удаления / вставки или получения элементов из списка)?
Можете ли вы порекомендовать другой подход, не указанный выше?

Некоторые сдерживает
-оптимальная производительность критична, использование памяти не так много
-это должен быть упорядоченный список (в настоящее время синхронизирующийся с ArrayList), но не отсортированный список (т.е. не отсортированный с помощью Comparable или Comparator, но в соответствии с порядком вставки)
-список будет большой, содержащий до 100000 объектов домена, поэтому использование чего-то вроде CopyOnWriteArrayList нецелесообразно
- циклические разделы записи / обновления обычно выполняются очень быстро, выполняются простые операции добавления / удаления / вставки или замены (установки)
- операции чтения будут в основном выполнять вызов elementAt (index) большую часть времени, хотя некоторые операции чтения могут выполнять двоичный поиск или indexOf (element)
-не выполняется прямая итерация по списку, хотя такая операция, как indexOf (..), будет проходить по списку

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
7
0
2 500
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

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

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

Я взглянул на CopyOnWriteArrayList, но использовать его было бы слишком дорого. Список потенциально может содержать 100000 элементов и постоянно обновляется.

Herman Lintvelt 16.10.2008 13:15

Хорошо, в этом случае вам нужно действительно тщательно проработать свою семантику. Эти обновления выполняют вставку / удаление или просто заменяют элементы? Если они добавляют / удаляют, делают ли они это в начале / конце списка? (В этом случае вам действительно может помочь связанный список.)

Jon Skeet 16.10.2008 13:40
Ответ принят как подходящий

Вам нужно использовать последовательный список? Если структура типа карты более подходящая, вы можете использовать ConcurrentHashMap. Со списком ReadWriteLock, вероятно, является наиболее эффективным способом.

Изменить, чтобы отразить редактирование OP: Двоичный поиск по порядку вставки? Вы храните метку времени и используете ее для сравнения в бинарном поиске? Если это так, вы можете использовать метку времени в качестве ключа и ConcurrentSkipListMap в качестве контейнера (который поддерживает порядок ключей).

Мне нравится идея ConcurrentSkipListMap. В 90% случаев список сортируется в соответствии с некоторой временной меткой (частью идентификатора каждого объекта домена), поэтому, вероятно, стоит оптимизировать для этого. Еще буду думать об остальных 10%.

Herman Lintvelt 17.10.2008 12:10

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

Данные управляются на стороне сервера менеджером БД и уровнем сервера приложений, но нам нужно как-то отобразить их конечному пользователю. Управление этим списком происходит на стороне клиента, откуда данные извлекаются.

Herman Lintvelt 16.10.2008 14:27

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

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

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

Да, это довольно сложное решение. Его составляющие:

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

При первом ударе кости этого кеша могут выглядеть примерно так:

class ServerCacheViewThingy {
    private static final int ACCEPTABLE_SIZE = 500;
    private int viewStart, viewLength;
    final Map<Integer, Record> items
            = new HashMap<Integer, Record>(1000);
    final ConcurrentLinkedQueue<Callback> callbackQueue
            = new ConcurrentLinkedQueue<Callback>();

    public void getRecords (int start, int length, ViewReciever reciever) {
        // remember the current view, to prevent records within
        // this view from being accidentally pruned.
        viewStart = start;
        viewLenght = length;

        // if the selected area is not already loaded, send a request
        // to load that area
        if (!rangeLoaded(start, length))
            addLoadRequest(start, length);

        // add the reciever to the queue, so it will be processed
        // when the data has arrived
        if (reciever != null)
            callbackQueue.add(new Callback(start, length, reciever));
    }

    class Callback {
        int start;
        int length;
        ViewReciever reciever;
        ...
    }

    class EditorThread extends Thread {

        private void prune () {
            if (items.size() <= ACCEPTABLE_SIZE)
                return;
            for (Map.Entry<Integer, Record> entry : items.entrySet()) {
                int position = entry.key();
                // if the position is outside the current view,
                // remove that item from the cache
                ...
            }
        }

        private void markDirty (int from) { ... }

        ....
    }

    class CallbackThread extends Thread {
        public void notifyCallback (Callback callback);
        private void processCallback (Callback) {
            readRecords
        }
    }
}

interface ViewReciever {
    void recieveData (int viewStart, Record[] records);
    void recieveTimeout ();
}

Очевидно, вам придется заполнить много деталей для себя.

Спасибо за ваш ответ. До 100000 элементов - это «страница» данных, которую мы получаем клиенту. БД может содержать миллиарды записей. Что я бы серьезно рассмотрел, так это ваш совет о доступе к списку только с помощью одной ветки. Это определенно упростило бы ситуацию.

Herman Lintvelt 17.10.2008 12:16

Вы можете использовать обертку, реализующую синхронизацию:

import java.util.Collections;
import java.util.ArrayList;

ArrayList list = new ArrayList();
List syncList = Collections.synchronizedList(list);

// make sure you only use syncList for your future calls... 

Это простое решение. Я бы попробовал это, прежде чем прибегать к более сложным решениям.

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