Need Fool Proof Synchronization of ArrayList in A Multi-threaded Environment

前端 未结 4 902
闹比i
闹比i 2021-02-15 15:33

I\'ve been at this for a week now doing my research on how to properly synchronize an ArrayList.

My main issue in a nutshell is I have a \"master\" ArrayList of objects.

相关标签:
4条回答
  • 2021-02-15 16:13

    Use CopyOnWriteArrayList, and synchronize on write operations only

    CopyOnWriteArrayList<TestObject> list = ...
    
    final Object writeLock = new Object();
    
    void writeOpA()
    {
        synchronized(writeLock)
        {
            read/write list
        }
    }
    void writeOpB()
    {
        synchronized(writeLock)
        {
            read/write list
        }
    }
    

    Therefore no two write sessions will overlap with each other.

    Reads require no lock. But a read session may see a changing list. If we want a read session to see a snapshot of the list, either use iterator(), or take a snapshot by toArray().


    It's probably even better if you do the copy-on-write yourselves

    volatile Foo data = new Foo(); // ArrayList in your case
    
    final Object writeLock = new Object();
    
    void writeOpA()
    {
        synchronized(writeLock)
        {
            Foo clone = data.clone();
            // read/write clone
            data = clone;
        }
    }
    void writeOpB()
    {
        // similar...
    }
    
    void readSession()
    {
        Foo snapshot = data;
        // read snapshot
    }
    
    0 讨论(0)
  • 2021-02-15 16:19

    As you found out yourself, CopyOnWriteArrayList is NOT ABLE to make completely secure changes when someone is processing the data, especially not while iterating over the list. Because: Whenever you are working on the data, there is no context to make sure your complete block of statements accessing the list is executed before someone else changed the list data.

    Therefore you MUST have any context (like synchronization) for all your access operations (also for reading!) that execute your whole data accessing block. For example:

    ArrayList<String> list = getList();
    synchronized (list) {
        int index = list.indexOf("test");
        // if the whole block would not be synchronized,
        // the index could be invalid after an external change
        list.remove(index);
    }
    

    Or for iterators:

    synchronized (list) {
        for (String s : list) {
            System.out.println(s);
        }
    }
    

    But now comes the big problem with this type of synchronization: It is slow and doesn't allow multiple reading access.
    Therefore it would be useful to build your own context for data access. I am going to use the ReentrantReadWriteLock to allow multiple reading access and improve the performance.
    I'm very interested in this topic and will make such a context for the ArrayList and attach it here after I finished it.

    20.10.2012 | 18:30 - EDIT: I created an own access context using the ReentrantReadWriteLock for a secure ArrayList.
    Firstly I will insert the whole SecureArrayList class (the most of the first operations is just overriding and protecting), then I insert my Tester class with the explanation of the usage.
    I just tested the access with one thread, not with many at the same time, but I'm pretty sure it works! If not, please tell me.

    SecureArrayList:

    package mydatastore.collections.concurrent;
    
    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.ConcurrentModificationException;
    import java.util.Iterator;
    import java.util.List;
    import java.util.ListIterator;
    import java.util.NoSuchElementException;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
    import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
    
    /**
     * @date 19.10.2012
     * @author Thomas Jahoda
     *
     * uses ReentrantReadWriteLock
     */
    public class SecureArrayList<E> extends ArrayList<E> {
    
        protected final ReentrantReadWriteLock rwLock;
        protected final ReadLock readLock;
        protected final WriteLock writeLock;
    
        public SecureArrayList() {
            super();
            this.rwLock = new ReentrantReadWriteLock();
            readLock = rwLock.readLock();
            writeLock = rwLock.writeLock();
        }
    
        // write operations
        @Override
        public boolean add(E e) {
            try {
                writeLock.lock();
                return super.add(e);
            } finally {
                writeLock.unlock();
            }
        }
    
        @Override
        public void add(int index, E element) {
            try {
                writeLock.lock();
                super.add(index, element);
            } finally {
                writeLock.unlock();
            }
        }
    
        @Override
        public boolean addAll(Collection<? extends E> c) {
            try {
                writeLock.lock();
                return super.addAll(c);
            } finally {
                writeLock.unlock();
            }
        }
    
        @Override
        public boolean addAll(int index, Collection<? extends E> c) {
            try {
                writeLock.lock();
                return super.addAll(index, c);
            } finally {
                writeLock.unlock();
            }
        }
    
        @Override
        public boolean remove(Object o) {
            try {
                writeLock.lock();
                return super.remove(o);
            } finally {
                writeLock.unlock();
            }
        }
    
        @Override
        public E remove(int index) {
            try {
                writeLock.lock();
                return super.remove(index);
            } finally {
                writeLock.unlock();
            }
        }
    
        @Override
        public boolean removeAll(Collection<?> c) {
            try {
                writeLock.lock();
                return super.removeAll(c);
            } finally {
                writeLock.unlock();
            }
        }
    
        @Override
        protected void removeRange(int fromIndex, int toIndex) {
            try {
                writeLock.lock();
                super.removeRange(fromIndex, toIndex);
            } finally {
                writeLock.unlock();
            }
        }
    
        @Override
        public E set(int index, E element) {
            try {
                writeLock.lock();
                return super.set(index, element);
            } finally {
                writeLock.unlock();
            }
        }
    
        @Override
        public void clear() {
            try {
                writeLock.lock();
                super.clear();
            } finally {
                writeLock.unlock();
            }
        }
    
        @Override
        public boolean retainAll(Collection<?> c) {
            try {
                writeLock.lock();
                return super.retainAll(c);
            } finally {
                writeLock.unlock();
            }
        }
    
        @Override
        public void ensureCapacity(int minCapacity) {
            try {
                writeLock.lock();
                super.ensureCapacity(minCapacity);
            } finally {
                writeLock.unlock();
            }
        }
    
        @Override
        public void trimToSize() {
            try {
                writeLock.lock();
                super.trimToSize();
            } finally {
                writeLock.unlock();
            }
        }
    
        //// now the read operations
        @Override
        public E get(int index) {
            try {
                readLock.lock();
                return super.get(index);
            } finally {
                readLock.unlock();
            }
        }
    
        @Override
        public boolean contains(Object o) {
            try {
                readLock.lock();
                return super.contains(o);
            } finally {
                readLock.unlock();
            }
        }
    
        @Override
        public boolean containsAll(Collection<?> c) {
            try {
                readLock.lock();
                return super.containsAll(c);
            } finally {
                readLock.unlock();
            }
        }
    
        @Override
        public Object clone() {
            try {
                readLock.lock();
                return super.clone();
            } finally {
                readLock.unlock();
            }
        }
    
        @Override
        public boolean equals(Object o) {
            try {
                readLock.lock();
                return super.equals(o);
            } finally {
                readLock.unlock();
            }
        }
    
        @Override
        public int hashCode() {
            try {
                readLock.lock();
                return super.hashCode();
            } finally {
                readLock.unlock();
            }
        }
    
        @Override
        public int indexOf(Object o) {
            try {
                readLock.lock();
                return super.indexOf(o);
            } finally {
                readLock.unlock();
            }
        }
    
        @Override
        public Object[] toArray() {
            try {
                readLock.lock();
                return super.toArray();
            } finally {
                readLock.unlock();
            }
        }
    
        @Override
        public boolean isEmpty() { // not sure if have to override because the size is temporarly stored in every case...
            // it could happen that the size is accessed when it just gets assigned a new value, 
            // and the thread is switched after assigning 16 bits or smth... i dunno
            try {
                readLock.lock();
                return super.isEmpty();
            } finally {
                readLock.unlock();
            }
        }
    
        @Override
        public int size() {
            try {
                readLock.lock();
                return super.size();
            } finally {
                readLock.unlock();
            }
        }
    
        @Override
        public int lastIndexOf(Object o) {
            try {
                readLock.lock();
                return super.lastIndexOf(o);
            } finally {
                readLock.unlock();
            }
        }
    
        @Override
        public List<E> subList(int fromIndex, int toIndex) {
            try {
                readLock.lock();
                return super.subList(fromIndex, toIndex);
            } finally {
                readLock.unlock();
            }
        }
    
        @Override
        public <T> T[] toArray(T[] a) {
            try {
                readLock.lock();
                return super.toArray(a);
            } finally {
                readLock.unlock();
            }
        }
    
        @Override
        public String toString() {
            try {
                readLock.lock();
                return super.toString();
            } finally {
                readLock.unlock();
            }
        }
    
        ////// iterators
        @Override
        public Iterator<E> iterator() {
            return new SecureArrayListIterator();
        }
    
        @Override
        public ListIterator<E> listIterator() {
            return new SecureArrayListListIterator(0);
        }
    
        @Override
        public ListIterator<E> listIterator(int index) {
            return new SecureArrayListListIterator(index);
        }
        // deligated lock mechanisms
    
        public void lockRead() {
            readLock.lock();
        }
    
        public void unlockRead() {
            readLock.unlock();
        }
    
        public void lockWrite() {
            writeLock.lock();
        }
    
        public void unlockWrite() {
            writeLock.unlock();
        }
    
        // getters
        public ReadLock getReadLock() {
            return readLock;
        }
    
        /**
         * The writeLock also has access to reading, so when holding write, the
         * thread can also obtain the readLock. But while holding the readLock and
         * attempting to lock write, it will result in a deadlock.
         *
         * @return
         */
        public WriteLock getWriteLock() {
            return writeLock;
        }
    
        protected class SecureArrayListIterator implements Iterator<E> {
    
            int cursor;       // index of next element to return
            int lastRet = -1; // index of last element returned; -1 if no such
    
            @Override
            public boolean hasNext() {
                return cursor != size();
            }
    
            @Override
            public E next() {
                //  checkForComodification();
                int i = cursor;
                if (i >= SecureArrayList.super.size()) {
                    throw new NoSuchElementException();
                }
                cursor = i + 1;
                lastRet = i;
                return SecureArrayList.super.get(lastRet);
            }
    
            @Override
            public void remove() {
                if (!writeLock.isHeldByCurrentThread()) {
                    throw new IllegalMonitorStateException("when the iteration uses write operations,"
                            + "the complete iteration loop must hold a monitor for the writeLock");
                }
                if (lastRet < 0) {
                    throw new IllegalStateException("No element iterated over");
                }
                try {
                    SecureArrayList.super.remove(lastRet);
                    cursor = lastRet;
                    lastRet = -1;
                } catch (IndexOutOfBoundsException ex) {
                    throw new ConcurrentModificationException(); // impossibru, except for bugged child classes
                }
            }
            //  protected final void checkForComodification() {
            //      if (modCount != expectedModCount) {
            //          throw new IllegalMonitorStateException("The complete iteration must hold the read or write lock!");
            //      }
            //  }
        }
    
        /**
         * An optimized version of AbstractList.ListItr
         */
        protected class SecureArrayListListIterator extends SecureArrayListIterator implements ListIterator<E> {
    
            protected SecureArrayListListIterator(int index) {
                super();
                cursor = index;
            }
    
            @Override
            public boolean hasPrevious() {
                return cursor != 0;
            }
    
            @Override
            public int nextIndex() {
                return cursor;
            }
    
            @Override
            public int previousIndex() {
                return cursor - 1;
            }
    
            @Override
            public E previous() {
                //  checkForComodification();
                int i = cursor - 1;
                if (i < 0) {
                    throw new NoSuchElementException("No element iterated over");
                }
                cursor = i;
                lastRet = i;
                return SecureArrayList.super.get(lastRet);
            }
    
            @Override
            public void set(E e) {
                if (!writeLock.isHeldByCurrentThread()) {
                    throw new IllegalMonitorStateException("when the iteration uses write operations,"
                            + "the complete iteration loop must hold a monitor for the writeLock");
                }
                if (lastRet < 0) {
                    throw new IllegalStateException("No element iterated over");
                }
                //  try {
                SecureArrayList.super.set(lastRet, e);
                //  } catch (IndexOutOfBoundsException ex) {
                //      throw new ConcurrentModificationException(); // impossibru, except for bugged child classes
                //          EDIT: or any failed direct editing while iterating over the list
                //  }
            }
    
            @Override
            public void add(E e) {
                if (!writeLock.isHeldByCurrentThread()) {
                    throw new IllegalMonitorStateException("when the iteration uses write operations,"
                            + "the complete iteration loop must hold a monitor for the writeLock");
                }
                //  try {
                int i = cursor;
                SecureArrayList.super.add(i, e);
                cursor = i + 1;
                lastRet = -1;
                //  } catch (IndexOutOfBoundsException ex) {
                //      throw new ConcurrentModificationException(); // impossibru, except for bugged child classes
                //          // EDIT: or any failed direct editing while iterating over the list
                //  }
            }
        }
    }
    

    SecureArrayList_Test:

    package mydatastore.collections.concurrent;
    
    import java.util.Iterator;
    import java.util.ListIterator;
    
    /**
     * @date 19.10.2012
     * @author Thomas Jahoda
     */
    public class SecureArrayList_Test {
    
        private static SecureArrayList<String> statList = new SecureArrayList<>();
    
        public static void main(String[] args) {
            accessExamples();
    //        mechanismTest_1();
    //        mechanismTest_2();
        }
    
        private static void accessExamples() {
            final SecureArrayList<String> list = getList();
            //
            try {
                list.lockWrite();
                //
                list.add("banana");
                list.add("test");
            } finally {
                list.unlockWrite();
            }
            ////// independent single statement reading or writing access
            String val = list.get(0);
            //// ---
    
            ////// reading only block (just some senseless unoptimized 'whatever' example)
            int lastIndex = -1;
            try {
                list.lockRead();
                //
                String search = "test";
                if (list.contains(search)) {
                    lastIndex = list.lastIndexOf(search);
                }
                // !!! MIND !!!
                // inserting writing operations here results in a DEADLOCK!!!
                // ... which is just really, really awkward...
            } finally {
                list.unlockRead();
            }
            //// ---
    
            ////// writing block (can also contain reading operations!!)
            try {
                list.lockWrite();
                //
                int index = list.indexOf("test");
                if (index != -1) {
                    String newVal = "banana";
                    list.add(index + 1, newVal);
                }
            } finally {
                list.unlockWrite();
            }
            //// ---
    
            ////// iteration for reading only
            System.out.println("First output: ");
            try {
                list.lockRead();
                //
                for (Iterator<String> it = list.iterator(); it.hasNext();) {
                    String string = it.next();
                    System.out.println(string);
                    // !!! MIND !!!
                    // inserting writing operations called directly on the list will result in a deadlock!
                    // inserting writing operations called on the iterator will result in an IllegalMonitorStateException!
                }
            } finally {
                list.unlockRead();
            }
            System.out.println("------");
            //// ---
    
            ////// iteration for writing and reading
            try {
                list.lockWrite();
                //
                boolean firstAdd = true;
                for (ListIterator<String> it = list.listIterator(); it.hasNext();) {
                    int index = it.nextIndex();
                    String string = it.next();
                    switch (string) {
                        case "banana":
                            it.remove();
                            break;
                        case "test":
                            if (firstAdd) {
                                it.add("whatever");
                                firstAdd = false;
                            }
                            break;
                    }
                    if (index == 2) {
                        list.set(index - 1, "pretty senseless data and operations but just to show "
                                + "what's possible");
                    }
                    // !!! MIND !!!
                    // Only I implemented the iterators to enable direct list editing,
                    // other implementations normally throw a ConcurrentModificationException
                }
            } finally {
                list.unlockWrite();
            }
            //// ---
    
            System.out.println("Complete last output: ");
            try {
                list.lockRead();
                //
                for (String string : list) {
                    System.out.println(string);
                }
            } finally {
                list.unlockRead();
            }
            System.out.println("------");
    
    
            ////// getting the last element
            String lastElement = null;
            try {
                list.lockRead();
                int size = list.size();
                lastElement = list.get(size - 1);
            } finally {
                list.unlockRead();
            }
            System.out.println("Last element: " + lastElement);
            //// ---
        }
    
        private static void mechanismTest_1() { // fus, roh
            SecureArrayList<String> list = getList();
            try {
                System.out.print("fus, ");
                list.lockRead();
                System.out.print("roh, ");
                list.lockWrite();
                System.out.println("dah!"); // never happens cos of deadlock
            } finally {
                // also never happens
                System.out.println("dah?");
                list.unlockRead();
                list.unlockWrite();
            }
        }
    
        private static void mechanismTest_2() { // fus, roh, dah!
            SecureArrayList<String> list = getList();
            try {
                System.out.print("fus, ");
                list.lockWrite();
                System.out.print("roh, ");
                list.lockRead();
                System.out.println("dah!");
            } finally {
                list.unlockRead();
                list.unlockWrite();
            }
            // successful execution
        }
    
        private static SecureArrayList<String> getList() {
            return statList;
        }
    }
    

    Edit: I've added a couple test cases to demonstrate the functionality in threads. The above class works perfectly and I'm now using it in my main project (Liam):

    private static void threadedWriteLock(){
        final ThreadSafeArrayList<String> list = getList();
    
        Thread threadOne;
        Thread threadTwo;
        final long lStartMS = System.currentTimeMillis();
    
        list.add("String 1");
        list.add("String 2");
    
        System.out.println("******* basic write lock test *******");
    
        threadOne = new Thread(new Runnable(){
            public void run(){
                try {
                    list.lockWrite();
    
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } finally {
                    list.unlockWrite();
                }
            }
        });
    
        threadTwo = new Thread(new Runnable(){
            public void run(){
                //give threadOne time to lock (just in case)
                try {
                    Thread.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                System.out.println("Expect a wait....");
    
                //if this "add" line is commented out, even the iterator read will be locked. 
                //So its not only locking on the add, but also the read which is correct.
                list.add("String 3"); 
    
                for (ListIterator<String> it = list.listIterator(); it.hasNext();) {
                     System.out.println("String at index " + it.nextIndex() + ": " + it.next());
                }
    
                System.out.println("ThreadTwo completed in " + (System.currentTimeMillis() - lStartMS) + "ms");
    
            }
        });
    
        threadOne.start();
        threadTwo.start();
    }
    
    private static void threadedReadLock(){
        final ThreadSafeArrayList<String> list = getList();
    
        Thread threadOne;
        Thread threadTwo;
        final long lStartMS = System.currentTimeMillis();
    
        list.add("String 1");
        list.add("String 2");
    
        System.out.println("******* basic read lock test *******");
    
        threadOne = new Thread(new Runnable(){
            public void run(){
                try {
                    list.lockRead();
    
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } finally {
                    list.unlockRead();
                }
            }
        });
    
        threadTwo = new Thread(new Runnable(){
            public void run(){
                //give threadOne time to lock (just in case)
                try {
                    Thread.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                System.out.println("Expect a wait if adding, but not reading....");
    
                //if this "add" line is commented out, the read will continue without holding up the thread
                list.add("String 3"); 
    
                for (ListIterator<String> it = list.listIterator(); it.hasNext();) {
                     System.out.println("String at index " + it.nextIndex() + ": " + it.next());
                }
    
                System.out.println("ThreadTwo completed in " + (System.currentTimeMillis() - lStartMS) + "ms");
    
            }
        });
    
        threadOne.start();
        threadTwo.start();
    }
    
    0 讨论(0)
  • 2021-02-15 16:19

    If you're modifying during an iteration, yeah, you have to use option 3. None of the others will actually do what you want.

    More specifically: given what you want to do, you have to lock the entire list for the length of the iteration, because you might modify it in the middle, which would corrupt any other iterators working on the list at the same time. That means option 3, since the Java language can't just have a "synchronized iterator" -- the iterator itself can only synchronize individual calls to hasNext() or next(), but it can't synchronize across the entire length of the iteration.

    0 讨论(0)
  • 2021-02-15 16:31

    Another approach is to protect all access to the list, but with a ReadWriteLock instead of synchronized blocks.

    This allows simultaneous reads in a safe manner, and could improve performance a lot in a scenario with many reads and few writes.

    0 讨论(0)
提交回复
热议问题