20

I need an ArrayList-like structure allowing just the following operations

  • get(int index)
  • add(E element)
  • set(int index, E element)
  • iterator()

Because of the iterator being used in many places, using Collections#synchronizedList would be too error-prone. The list can grow to a few thousand elements and gets used a lot, so I'm pretty sure, that CopyOnWriteArrayList will be too slow. I'll start with it to avoid premature optimizations, but I'd bet it won't work well.

Most accesses will be single-threaded reads. So I'm asking what's the proper data structure for this.


I though that wrapping the synchronizedList in something providing a synchronized iterator would do, but it won't because of the ConcurrentModificationException. Concenrning concurrent behavior, I obviously need that all changes will be visible by subsequent reads and iterators.

The iterator doesn't have to show a consistent snapshot, it may or may not see the updates via set(int index, E element) as this operation gets used only to replace an item with its updated version (containing some added information, which is irrelevant for the user of the iterator). The items are fully immutable.


I clearly stated why CopyOnWriteArrayList would not do. ConcurrentLinkedQueue is out of question as it lacks an indexed access. I need just a couple of operations rather than a fully fledged ArrayList. So unless any java concurrent list-related question is a duplicate of this question, this one is not.

Community
  • 1
  • 1
maaartinus
  • 44,714
  • 32
  • 161
  • 320
  • 2
    You may want to try out Clojure's [`PersistentVector`](https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/PersistentVector.java), it is copy-on-write as well, but not a naive monolithic array; rather a wide-and-shallow tree of arrays. At the end of the source code which I link to you'll find testing code. – Marko Topolnik Oct 17 '14 at 11:34
  • 1
    @skaffman Could you kindly read the question before closing it? See my update. – maaartinus Oct 17 '14 at 12:32
  • @MarkoTopolnik This would work, but the code looks very weird. It also adds some overhead needed for operations I don't care about. Btw., could you vote to reopen? IMHO the mod was a bit too fast with reading. – maaartinus Oct 17 '14 at 13:24
  • That was careless of him :) Yes, Rich Hickey has a peculiar coding style, but you may still want to reuse that code, deleting everything excessive. I think Rich's approach is really good. – Marko Topolnik Oct 17 '14 at 13:25
  • more information about usage cases would be helpful, as well as goals. Speed seems to be one, as does data integrity. – Marshall Tigerus Oct 17 '14 at 13:28
  • Could you not simply wrap an ArrayList in a class that reveals only the functions that you want to use? – FelixMarcus Oct 17 '14 at 13:30
  • @FelixMarcus No, as I need to iterate while the list may be changing (see my update). – maaartinus Oct 17 '14 at 13:45
  • Since you don't need the remove operation, have you considered using volatile array of thread safe objects? – Ivan Oct 17 '14 at 14:33
  • @Ivan Yes, it sounds simple and I'm actually about to give it a try now, but I'm curious about existing alternatives. – maaartinus Oct 17 '14 at 15:08
  • @Ivan It's harder than it looks as I need to keep track of the size as well and it's tricky to add an element so that 1. nobody sees an empty slot, 2. the update can't get lost. I guess, using `synchronized` would solve it easily, but I wanted to try without. – maaartinus Oct 17 '14 at 16:47
  • I think, you can use AtomicInteger shared counter. In order to add a new element, a thread would just getAndIncrement() a counter value, then put a new element at index, determined by this value. Actually, all your questions have their answers, you just need to think a little bit, weigh up pros and cons:) There is a very good book on this topic: "The art of multiprocessor programming" by Maurice Herlihy. – Ivan Oct 18 '14 at 06:36
  • So an iterator doesn't have to see any updates from `set`, but what about `add`? If a thread `add`s an element to the list after an iteration has started, does it matter if the iterator sees the (new) element or not? – cic Oct 29 '14 at 14:36
  • @cic Currently, I guess, both would be fine, but this may change. – maaartinus Oct 29 '14 at 23:15

1 Answers1

9

In your case you can use a ReadWriteLock to access a backed List, this allows multiple Threads to read your list. Only if one Thread needs write access all reader-Thread must wait for the operation to complete. The JavaDoc make's it clear:

A ReadWriteLock maintains a pair of associated locks, one for read-only operations and one for writing. The read lock may be held simultaneously by multiple reader threads, so long as there are no writers. The write lock is exclusive.

Here is a sample:

public class ConcurrentArrayList<T> {

    /** use this to lock for write operations like add/remove */
    private final Lock readLock;
    /** use this to lock for read operations like get/iterator/contains.. */
    private final Lock writeLock;
    /** the underlying list*/
    private final List<T> list = new ArrayList();

    {
        ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
        readLock = rwLock.readLock();
        writeLock = rwLock.writeLock();
    }

    public void add(T e){
        writeLock.lock();
        try{
            list.add(e);
        }finally{
            writeLock.unlock();
        }
    }

    public void get(int index){
        readLock.lock();
        try{
            list.get(index);
        }finally{
            readLock.unlock();
        }
    }

    public Iterator<T> iterator(){
        readLock.lock();
        try {
            return new ArrayList<T>( list ).iterator();
                   //^ we iterate over an snapshot of our list 
        } finally{
            readLock.unlock();
        }
    }
Chriss
  • 5,157
  • 7
  • 41
  • 75
  • That's fine, but I can't use the `ArrayList`'s iterator, as it'd throw a `ConcurrentModificationException`. I guess, there's a simple workaround. – maaartinus Oct 18 '14 at 20:15
  • You can't avoid the ´ConcurrentModificationException´ if you want to be ableto add elements while iterating. Imagin your next element while iterating is at index 5, but in the same time an other Thread adds an element at index 1, what should happen now? The only way out is to create a immutalbe copy of the List to get an independend iterator or to abuse the writeLock to lock the iteration loop block. – Chriss Oct 18 '14 at 20:38
  • "*what should happen now*" - Exactly this can't happen, as there'll be no such operation. But appending or replacing elements can, and then I just don't care. Getting the old value is fine and so it getting the new one. – maaartinus Oct 18 '14 at 21:55
  • This solution might be inappropriate for large lists are you are doing lots of copying while holding the lock (both in `add` and `iterator`). (Your iterator implementation also provides a stronger consistency guarantee that necessary.) – cic Oct 29 '14 at 14:45
  • Array copies always take less time than you think they will. (Well, unless you think they take no time at all). Returning an iterator over a copy of this list should be fine even if it's huge (especially as you create an iterator relatively seldom). – barneypitt Sep 27 '22 at 09:31