10

I've got an ArrayList which is being instantiated and populated on the background thread (I use it to store the Cursor data). At the same time it can be accessed on the main thread and iterated through using foreach. So this obviously may result in throwing an exception.

My question is what's the best practice to make this class field thread-safe without copying it every time or using flags?

class SomeClass {

    private final Context mContext;
    private List<String> mList = null;

    SomeClass(Context context) {
        mContext = context;
    }

    public void populateList() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                mList = new ArrayList<>();

                Cursor cursor = mContext.getContentResolver().query(
                        DataProvider.CONTENT_URI, null, null, null, null);
                try {
                    while (cursor.moveToNext()) {
                        mList.add(cursor.getString(cursor.getColumnIndex(DataProvider.NAME)));
                    }
                } catch (Exception e) {
                    Log.e("Error", e.getMessage(), e);
                } finally {
                    if (cursor != null) {
                        cursor.close();
                    }
                }
            }
        }).start();
    }

    public boolean searchList(String query) { // Invoked on the main thread
        if (mList != null) {
            for (String name : mList) {
                if (name.equals(query) {
                    return true;
                }
            }
        }

        return false;
    }
}
Nikolai
  • 821
  • 3
  • 17
  • 22

4 Answers4

6

In general it is a very bad idea to operate concurrently on a datastructure that is not thread-safe. You have no guarantee that the implementation will not change in the future, which may severly impact the runtime behavior of the application, i.e. java.util.HashMap causes infinite loops when being concurrently modified.

For accessing a List concurrently, Java provides the java.util.concurrent.CopyOnWriteArrayList. Using this implementation will solve your problem in various ways:

  • it is thread safe, allowing concurrent modifications
  • iterating over snapshots of the list is not affected by concurrent add operations, allowing concurrently adding and iterating
  • it's faster than synchronization

Alternatively, if not using a copy of the internal array is a strict requirement (which I can't imagine in your case, the array is rather small as it only contains object references, which can be copied in memory quite efficiently), you may synchronize the access on the map. But that would require the Map to be initialized properly, otherwise your code may throw a NullPointerException because the order of thread-execution is not guaranteed (you assume the populateList() is started before, so the list gets initialized. When using a synchronized block, choose the protected block wisely. In case you have the entire content of the run() method in a synchronized block, the reader thread has to wait until the results from the cursor are processed - which may take a while - so you actually loose all concurrency.

If you decide to go for the synchronized block, I'd make the following changes (and I don't claim, they are perfectly correct):

Initialize the list field so we can synchronize access on it:

private List<String> mList = new ArrayList<>(); //initialize the field

Synchronize the modification operation (add). Do not read the data from the cursor inside the synchronization block because if its a low latency operation, the mList could not be read during that operation, blocking all other threads for quite a while.

//mList = new ArrayList<>(); remove that line in your code
String data = cursor.getString(cursor.getColumnIndex(DataProvider.NAME)); //do this before synchronized block!
synchronized(mList){
  mList.add(data);
}

The read iteration must be inside the synchronization block, so no element gets added, while being iterated over:

synchronized(mList){ 
  for (String name : mList) {
    if (name.equals(query) {
      return true;
    }
  }
}

So when two threads operate on the list, one thread can either add a single element or iterate over the entire list at a time. You have no parallel execution on these parts of the code.

Regarding the synchronized versions of a List (i.e. Vector, Collections.synchronizedList()). Those might be less performant because with synchronization you actually lose prallel execution as only one thread may run the protected blocks at a time. Further, they might still be prone to ConcurrentModificationException, which may even occur in a single thread. It is thrown, if the datastructure is modified between iterator creation and iterator should proceed. So those datastructures won't solve your problem.

I do not recommend manualy synchronization as well, because the risk of simply doing it wrong is too high (synchronizing on the wrong or different monitory, too large synchronization blocks, ...)

TL;DR

use a java.util.concurrent.CopyOnWriteArrayList

Gerald Mücke
  • 10,724
  • 2
  • 50
  • 67
  • I'd prefer `CopyOnWriteArrayList` as well, but the OP said "without copying". Maybe he wants to guarantee "only one thread may run the protected blocks at a time". – beatngu13 Aug 09 '16 at 08:56
  • 2
    the "not copying"-requirement is sort of pointless. Sure, the CopyOnWriteArrayList *does* copy the content, but that's part of the implementation details. The only alternative would be to have a synchronized block, synchronizing on the map itself, which is prone to error as it is initialized with `null`. Using a `Vector` does not prevent a `ConcurrentModificationException`, because it has acutally nothing to do with concurrency (it is thrown when an iterator tries to proceed but the list has been modified after the creation of the iterator) – Gerald Mücke Aug 09 '16 at 09:01
  • Thank you for the explanation. I guess `CopyOnWriteArrayList` is better for my case (since I access the field on the UI thread). – Nikolai Aug 09 '16 at 09:06
  • I disagree. Simply using a CopyOnWriteArrayList will not be enough. It may lead to traversal of incomplete lists. OP should populate a "shadow" list and - if using CopyOnWriteArrayList - use the atomic `addAll` method. – Fildor Aug 09 '16 at 10:03
1

You could use a Vector which is the thread-safe equivalent of ArrayList.

EDIT: Thanks to Fildor's comment, I now know this doesn't avoid ConcurrentModificationException from being thrown using multiple threads:

Only single calls will be synchronized. So one add cannot be called while another thread is calling add, for example. But altering the list will cause the CME be thrown while iterating on another thread. You can read the docs of iterator on that topic.

Also interesting:

Long story short: DO NOT use Vector.

Community
  • 1
  • 1
beatngu13
  • 7,201
  • 6
  • 37
  • 66
  • Simply using a Vector won't avoid getting a ConcurrentModificationException. – Fildor Aug 09 '16 at 09:03
  • @Fildor But only if the same thread tries to iterate and modify, or am I wrong here? I thought the synchronization prevents multiple threads to access the data structure at the same time. – beatngu13 Aug 09 '16 at 09:32
  • 3
    Yes, but this has nothing to do with CME. Only single calls will be synchronized. So one add cannot be called while another thread is calling add, for example. But altering the list will cause the CME be thrown while iterating on another thread. You can read the docs of iterator on that topic. I stepped into that trap myself - I learned by pain ;) – Fildor Aug 09 '16 at 09:56
  • @Fildor Thanks for the clarification. I was thinking the whole vector is locked, not just a particular method. I'm not going to delete this answer, just to make sure people read this. ;) – beatngu13 Aug 09 '16 at 10:02
  • Well actually the whole instance is locked. So every method is "atomic". But CME is about state. When adding or even worse: removing elements you change state. And that is what makes the iterator throw the exception and what is not handled simply by making a list implementation synchronized. – Fildor Aug 09 '16 at 10:08
1

Use Collections.synchronizedList(new ArrayList<T>());

Ex:

Collections.synchronizedList(mList);
Shridutt Kothari
  • 7,326
  • 3
  • 41
  • 61
1

java synchronized block http://www.tutorialspoint.com/java/java_thread_synchronization.htm

class SomeClass {

    private final Context mContext;
    private List<String> mList = null;

    SomeClass(Context context) {
        mContext = context;
    }

    public void populateList() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized(SomeClass.this){
                    mList = new ArrayList<>();

                    Cursor cursor = mContext.getContentResolver().query(
                            DataProvider.CONTENT_URI, null, null, null, null);
                    try {
                        while (cursor.moveToNext()) {
                            mList.add(cursor.getString(cursor.getColumnIndex(DataProvider.NAME)));
                        }
                    } catch (Exception e) {
                        Log.e("Error", e.getMessage(), e);
                    } finally {
                        if (cursor != null) {
                            cursor.close();
                        }
                    }
                }
            }
        }).start();
    }

    public boolean searchList(String query) { // Invoked on the main thread
    synchronized(SomeClass.this){
            if (mList != null) {
                for (String name : mList) {
                    if (name.equals(query) {
                        return true;
                    }
                }
            }

            return false;
        }
    }
}
Karol Żygłowicz
  • 2,442
  • 2
  • 26
  • 35
  • are you sure, the synchronized blocks synchronize on the same monitor? – Gerald Mücke Aug 09 '16 at 08:52
  • you are right on runnable there should be exact point to the class.this object now (after update) I'm quite sure it should work – Karol Żygłowicz Aug 09 '16 at 08:56
  • 1
    and putting the entire run() method into a synchronized block will block access for quite a while... assume you iterate over 1000 elements, with each element requiring 1 sec to retrieve, the application is unresponsive for ~16min – Gerald Mücke Aug 09 '16 at 09:27