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

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

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

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

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


person Herman Lintvelt    schedule 16.10.2008    source источник


Ответы (5)


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

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

person Chris Jester-Young    schedule 16.10.2008
comment
Мне нравится идея ConcurrentSkipListMap. В 90% случаев список сортируется в соответствии с некоторой временной меткой (частью идентификатора каждого объекта домена), поэтому, вероятно, стоит оптимизировать для этого. Еще буду думать об остальных 10%. - person Herman Lintvelt; 17.10.2008

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

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

person Jon Skeet    schedule 16.10.2008
comment
Я взглянул на CopyOnWriteArrayList, но использовать его было бы слишком дорого. Список потенциально может содержать 100000 элементов и постоянно обновляется. - person Herman Lintvelt; 16.10.2008
comment
Хорошо, в этом случае вам нужно действительно тщательно проработать свою семантику. Эти обновления выполняют вставку / удаление или просто заменяют элементы? Если они добавляют / удаляют, делают ли они это в начале / конце списка? (В этом случае связанный список действительно может вам помочь.) - person Jon Skeet; 16.10.2008

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

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

Я второй предложение 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 ();
}

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

person Marcus Downing    schedule 16.10.2008
comment
Спасибо за ваш ответ. До 100000 элементов - это страница данных, которую мы получаем клиенту. БД может содержать миллиарды записей. Что я бы серьезно рассмотрел, так это ваш совет о доступе к списку только с помощью одной ветки. Это определенно упростило бы ситуацию. - person Herman Lintvelt; 17.10.2008

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

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... 

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

person Community    schedule 23.03.2009