Thread safe List for collection

Thread safe collection class

Thread safe collection classes can be divided into three categories:
Legacy thread safe collections such as Hashtable and Vector

Thread safe Collections decorated with Collections, such as:
Collections.synchronizedCollection
Collections.synchronizedList
Collections.synchronizedMap
Collections.synchronizedSet
Collections.synchronizedNavigableMap
Collections.synchronizedNavigableSet
Collections.synchronizedSortedMap
Collections.synchronizedSortedSet

java.util.concurrent.*

characteristic

Thread safe collection classes under java.util.concurrent. * can be found to be regular, which contains three types of keywords:
Blocking,CopyOnWrite,Concurrent
Most implementations of Blocking are lock based and provide methods for Blocking
The modification cost of containers such as CopyOnWrite is relatively heavy
Container of type Concurrent
Many internal operations use cas optimization, which can generally provide high throughput

Weak consistency
Weak consistency during traversal. For example, when traversing with an iterator, if the container is modified, the iterator can still continue to traverse
At this time, the content is old. Fail safe mechanism

If the traversal is modified, for non secure containers, use the fail fast mechanism, that is, make the traversal fail immediately and throw
ConcurrentModificationException, no more traversal

Fail fast and fail safe

When it comes to thread safety of collections, we have to understand fail fast and fail safe first

1: fail fast

When traversing a collection object, if the contents of the collection object are modified (added, deleted, modified) during traversal, a Concurrent Modification Exception will be thrown. Concurrent Modification Exception

Principle: the iterator directly accesses the contents of the collection during traversal, and uses a modCount variable during traversal. If the contents of the collection change during traversal, the value of modCount will change. Whenever the iterator uses hashNext()/next() to traverse the next element, it will detect whether the modCount variable is the expectedmodCount value. If so, it will return the traversal; Otherwise, an exception is thrown and the traversal is terminated.

Scenario: the collection classes under the java.util package fail quickly and cannot be modified concurrently under multithreading (modified during iteration).

2: fail safe

When traversing, it is not directly accessed on the collection content, but first copy the original collection content and traverse on the copied collection.

Disadvantages: the advantage of copying content is to avoid the Concurrent Modification Exception, but similarly, the iterator cannot access the modified content, that is, the iterator traverses the set copy obtained at the moment when it starts traversing, and the iterator does not know the modification of the original set during traversal.

Scenario: the containers under the java.util.concurrent (juc) package are all security failures, and can be used and modified concurrently under multithreading.

Summary:

The security failure of Iterator is based on copying the underlying collection, so it is not affected by modifications on the source collection.

All collection classes under the java.util package fail quickly, while all classes under the java.util.concurrent package fail safely.

Synchronization container and concurrency container

Synchronization container

Vector,Hashtable,Collections

Concurrent container

ConcurrentHashMap,CopyOnWriteArrayList,CopyOnWriteArraySet,bolckingQueue ...

List

ArrayList

package com.dongguo.concurrent.collection;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * @author Dongguo
 * @date 2021/9/3 0003-15:25
 * @description:  List Collection thread unsafe demo
 */
public class UnSafeListDemo {
    public static void main(String[] args) {
        List list = new ArrayList();
        for (int i = 1; i <= 3; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0,8));//Perform write operation
                System.out.println(list);//Perform read operation
            },"t"+i).start();
        }
    }
}

First run result:

[edb445d9]
[edb445d9]
[edb445d9]

Second run result

[03bd74e5]
[03bd74e5, 9def0238]
[03bd74e5, 9def0238, f3d85190]

Third run result

[77ae1026, 962660f8]
[77ae1026, 962660f8, b2248fc2]
[77ae1026, 962660f8]

Each execution is different and no error is reported.

Because the three threads are uncertain who writes first and who reads first

Increase the number of cycles

package com.dongguo.concurrent.collection;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * @author Dongguo
 * @date 2021/9/3 0003-15:25
 * @description:  List Collection thread unsafe demo
 */
public class UnSafeListDemo {
    public static void main(String[] args) {
        List list = new ArrayList();
        for (int i = 1; i <= 30; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0,8));//Perform write operation
                System.out.println(list);//Perform read operation
            },"t"+i).start();
        }
    }
}

Operation results:

Question: why do concurrent modification exceptions occur?

View the source code of the add method of ArrayList

/**
 * Appends the specified element to the end of this list.
 *
 * @param e element to be appended to this list
 * @return <tt>true</tt> (as specified by {@link Collection#add})
 */
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

Obviously, ArrayList does not do any synchronization. ArrayList is thread unsafe

So how do we solve the thread safety problem of List type?

Vector

Vector is a vector queue, which is a class added in JDK1.0. It inherits from AbstractList and implements List, randomaccess and clonable interfaces. Vector inherits AbstractList and implements List; Therefore, it is a queue that supports related functions such as addition, deletion, modification and traversal. Vector implements RandmoAccess interface, which provides random access function. RandmoAccess is implemented by List in java to provide fast access for List. In vector, we can quickly obtain the element object through the element serial number; This is fast random access. Vector implements the clonable interface, that is, the clone() function. It can be cloned.

Unlike ArrayList, operations in Vector are thread safe.

package com.dongguo.concurrent.collection;

import java.util.List;
import java.util.UUID;
import java.util.Vector;

/**
 * @author Dongguo
 * @date 2021/9/3 0003-15:25
 * @description: Vector
 */
public class UnSafeListDemo {
    public static void main(String[] args) {
        List list = new Vector();
        for (int i = 1; i <= 30; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0,8));//Perform write operation
                System.out.println(list);//Perform read operation
            },"t"+i).start();
        }
    }
}

Operation results:

As a result, no java.util.ConcurrentModificationException occurred

View the add method of Vector

/**
 * Appends the specified element to the end of this Vector.
 *
 * @param e element to be appended to this Vector
 * @return {@code true} (as specified by {@link Collection#add})
 * @since 1.2
 */
public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

The add method is modified by synchronized synchronization, which is thread safe! Therefore, there are no concurrent exceptions

Collections

Collections provides the method synchronizedList to ensure that the list is thread safe

package com.dongguo.concurrent.collection;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;

/**
 * @author Dongguo
 * @date 2021/9/3 0003-17:16
 * @description: Collections The method synchronizedList is provided to ensure that the list is thread safe
 */
public class SafeListCollectionsDemo {
    public static void main(String[] args) {
        List<String> list = Collections.synchronizedList(new ArrayList<>());
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list);
            }, "t" + i).start();
        }
    }
}

Operation results

As a result, java.util.ConcurrentModificationException does not occur

View method source code

@Override
public boolean add(T e) {
    synchronized(mutex) {
        return backingList.add(e);
    }
}

CopyOnWriteArrayList

CopyOnWriteArrayList, which is equivalent to a thread safe ArrayList. Like ArrayList, it is a variable array; However, unlike ArrayList, it has the following features:

  1. It is most suitable for applications with the following characteristics: the List size is usually kept small, read-only operations are far more than variable operations, and inter thread conflicts need to be prevented during traversal.

  2. It is thread safe.

  3. Because you usually need to copy the entire underlying array, variable operations (add(), set(), remove(), and so on) are expensive.

  4. Iterators support immutable operations such as hasNext(), next(), but do not support immutable operations such as remove().

  5. Traversal using iterators is fast and does not conflict with other threads. When constructing iterators, iterators rely on invariant array snapshots.

Design idea:

  1. Low efficiency of exclusive lock: it is solved by the idea of separation of read and write

  2. The write thread obtains the lock, and other write threads are blocked

  3. Copy ideas:

When we add elements to a container, instead of directly adding them to the current container, we first Copy the current container, Copy a new container, and then add elements to the new container. After adding elements, we point the reference of the original container to the new container.

CopyOnWriteArrayList can only guarantee final consistency, not real-time consistency.

At this time, a new problem will be thrown out, that is, the problem of inconsistent data. If the write thread has not had time to write back to memory, other threads will read the old data.

package com.dongguo.concurrent.collection;


import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * @author Dongguo
 * @date 2021/9/3 0003-17:16
 * @description: CopyOnWriteArrayList
 */
public class SafeListCollectionsDemo {
    public static void main(String[] args) {
        List list = new CopyOnWriteArrayList();
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list);
            }, "t" + i).start();
        }
    }
}

Operation results:

No thread safety issues

add method of CopyOnWriteArrayList

    
 	 private transient volatile Object[] array;
...
	final Object[] getArray() {
        return array;
    }
...
	public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }
...

Principle analysis

Next, the principle of CopyOnWriteArrayList is further explained from the two aspects of "dynamic array" and "thread safety".

Dynamic array mechanism

It has a "volatile array" inside to hold data. When "adding / modifying / deleting" data, a new array will be created, and the updated data will be copied to the new array. Finally, the array will be assigned to "volatile array", which is why it is called CopyOnWriteArrayList

Because it creates new arrays when "adding / modifying / deleting" data, CopyOnWriteArrayList is inefficient when it involves modifying data; However, it is more efficient to perform only traversal search.

  private transient volatile Object[] array;

"Thread safety" mechanism

Implemented through volatile and mutex.

Use the "volatile array" to save the data. When a thread reads the volatile array, it can always see the last write of the volatile variable by other threads; In this way, volatile provides the guarantee of the mechanism that "the data read is always up-to-date".

Protect data through the mutex ReentrantLock. When "adding / modifying / deleting" data, you will first "obtain the mutex", and then update the data to the "volatile array" after modification, and then "release the mutex", so as to achieve the purpose of protecting data

Because it creates new arrays when "adding / modifying / deleting" data, CopyOnWriteArrayList is inefficient when it involves modifying data; However, it is more efficient to perform only traversal search.

Tags: Java

Posted on Sun, 19 Sep 2021 05:38:49 -0400 by dreglo