SoFunction
Updated on 2025-03-08

A brief discussion on what to do if ArrayList thread is not safe in Java

What should I do if the ArrayList thread is not safe?

There are three solutions:

Using the corresponding Vector class, all methods in this class have synchronized keywords

  • Just like HashMap and HashTable

Use the synchronizedList method provided by Collections to convert a collection class that is originally thread-unsafe to thread-safe. The usage method is as follows:

List<Integer> list = (new ArrayList<>());

In fact, HashMap can also use this trick:

Map<String, String> map = (new HashMap<>());

This seems to be something, but it actually adds a synchronized to each method, but it is not directly added to the method, but is added inside the method. Only when the thread acquires the lock of the mutex object can it enter the code block:

public E get(int index) {
    synchronized (mutex) {
        return (index);
    }
}

Use the CopyOnWriteArrayList class provided under the JUC package

  • In fact, ConcurrentHashMap is also packaged by JUC.

Here we will discuss the CopyOnWriteArrayList class. It adopts the "copy-on-write" technology. That is to say, whenever an element is added to this list, it does not add it directly, but will first copy a list, then add elements in this copy, and finally modify the pointer to see the source code of add:

public boolean add(E e) {
    synchronized (lock) {
        //Get the current array        Object[] es = getArray();
        int len = ;
        //Copy one and expand the capacity        es = (es, len + 1);
        //Add new elements into        es[len] = e;
        //Modify the pointer's pointer        setArray(es);
        return true;
    }
}

Some people may wonder, what's the point? Isn't this synchronized also added, and the array needs to be copied. Isn't this worse than Vector?

This is indeed the case. In scenarios where there are more write operations, CopyOnWriteArrayList is indeed slower than Vector, but it has two advantages:

Although the write operation is bad, the read operation is much faster, because in vector, the read operation also needs to be locked, and here, the read operation does not need to be locked. The get method is shorter and may not be convenient to understand. Let's take a look at the indexOf method:

public int indexOf(Object o) {
    Object[] es = getArray();
    return indexOfRange(o, es, 0, );
}
private static int indexOfRange(Object o, Object[] es, int from, int to) {
    if (o == null) {
        for (int i = from; i < to; i++)
            if (es[i] == null)
                return i;
    } else {
        //****here****
        for (int i = from; i < to; i++)
            if ((es[i]))
                return i;
    }
    return -1;
}

It can be found that this method first hand over the current array array to the es variable. All subsequent operations are based on es (at this time, array and es both point to the same array a1 in memory)

Since all write operations are performed on a1 copy (we call this copy in memory a2), it will not affect those read operations performed on a1, and even if the write operation is completed, the array points to a2, but will not affect the array, because es still points to a1

Just imagine, what would happen if the vector read operation is not locked? Since all read and write operations in vector are based on the same array, although the read operation is fine at the beginning, during the subsequent traversal (such as the above code marked here), other threads may modify the array. To exaggerately, if a thread clears the array, then the read operation will definitely report an error. For CopyOnWriteArrayList, even if there is an empty operation, it is still performed on a2, while the read operation is still performed on a1 and will not have any impact.

When traversing a vector by forEach, it is not allowed to modify the vector. The ConcurrentModificationException exception will be reported. The reason is very simple, because there is only one array. If half of the traversal is passed, wouldn’t there be a problem? Therefore, Java simply prohibits the behavior of modifying the array during traversal. However, for CopyOnWriteArrayList, its traversal is always carried out on a1, and other writing threads can only modify it to a2, which has no effect on a1. Let’s look at a piece of code to verify:

public class Test {
    public static void main(String[] args) {
        CopyOnWriteArrayList&lt;Integer&gt; list = new CopyOnWriteArrayList&lt;&gt;();
        for (int i = 0; i &lt; 1000; i++) {
            (i);
        }
        //Clear the array during traversal        for (Integer i : list) {
            (i);
            ();
        }
    }
}

As a result, there is no error, and all numbers 0~999 are output in full. It can be seen that the first array a1 is traversed here. No matter how many writes there are, it will not affect a1, because all writes are performed on a2 a3 a4
To sum up, there are two advantages of CopyOnWriteArrayList:

  • Reading operations do not require locks, so reading and reading can be concurrent, reading and writing can be concurrent, and performance is better
  • There is no need for locks when traversing forEach (in fact, traversal is also a read operation). The main reason is that the array can be modified during traversal and there will be no errors (because the traversal is a1, the traversal is a2, a3, which will not affect a1)

But its shortcomings are also obvious, with two main points:

  • First of all, the memory consumption of the write operation is very large. Each time the array is modified, it will be copied once. If the array is large or the number of modifications is large, a large amount of memory will be consumed soon and GC will be triggered. Therefore, in scenarios where there are too many writes, you must use this class with caution.
  • Secondly, all read operations and forEach traversals are based on the old array a1. Even if a very important data is added during the traversal, this data is still in a2. It is impossible to obtain this data by traversing a1. In short, once all read operations start, the latest data can no longer be sensed.

You can find an interesting thing, that is, the old array is success or failure, and the old array is failure. Because all reads are based on the old array a1, you can do it boldly without locking, and you don’t have to worry about threads changing the array, because the changes are all on a2 a3, and have nothing to do with a1, but because all reads are based on the old array a1, once the read operation starts, even if a thread adds a very important data to the array, this read operation cannot perceive the latest data, because the latest data will only be included in a2

This is the end of this article about what to do if ArrayList threads are not safe in Java. For more related content on ArrayList threads, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!