Java Concurrent Programming-Thread Safety Issues of Collection Class

1. Thread safety of List collection

1.1. ArrayList thread safety issues.

Is ArrayList thread safe? Let's run the following program

public static void main(String[] args) {
    
    
		// TODO Auto-generated method stub
		List<String> list=new ArrayList<>();
		for(int i=1;i<=3;i++){
    
    
			new Thread(()->{
    
    
				//生成一个8位的随机不重复字符串,UUID版的。写操作
				list.add(UUID.randomUUID().toString().substring(0,8)); 
				System.out.println(list);	//默认复写了toString方法。读操作
			},String.valueOf(i)).start();;
		}
	}

Run the first time
Insert picture description here
Run the second time
Insert picture description here
Run the third time It
Insert picture description here
can be seen that the results of almost every run are different. No errors were reported. This is obviously thread-unsafe.
I don’t know which one of them will write first and who will read first, because it’s so fast, nanosecond level. Sometimes other threads rush to read it before it is written. It reads as null. In terms of number, it should be three. From the value above, it should be an 8-bit string. But the effect is different every time.

If you change to 30 threads, the program will report an error at this time

for(int i=1;i<=30;i++){
    
    
			new Thread(()->{
    
    
				//生成一个8位的随机不重复字符串,UUID版的
				list.add(UUID.randomUUID().toString().substring(0,8));
				System.out.println(list);	//默认复写了toString方法。
			},String.valueOf(i)).start();;
		}

Insert picture description here
Reported the exception directly. java.util.ConcurrentModificationException is abnormal.

analysis:

  1. Trouble phenomenon:
    java.util.ConcurrentModificationException (important)

  2. Cause:
    ArrayList thread is not safe, because the add method is not locked. Now 30 threads are operating at the same time, reading and writing again, and it crashed.

  3. Solution
    see next section

  4. Optimization suggestions (the same error will not occur the second time)

1.2. ArrayList thread safe solution.

1.2.1. Use Vector

public static void main(String[] args) {
    
    
		// TODO Auto-generated method stub
		List<String> list=new Vector<>();
		for(int i=1;i<=30;i++){
    
    
			new Thread(()->{
    
    
				//生成一个8位的随机不重复字符串,UUID版的
				list.add(UUID.randomUUID().toString().substring(0,8));
				System.out.println(list);	//默认复写了toString方法。
			},String.valueOf(i)).start();;
		}
	}

Insert picture description here
No error,
we can look at the source code of Vector

public synchronized boolean add(E e) {
    
    
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }

It can be seen that the synchronized keyword is added to the add method of Vector. Can ensure thread safety.
Let's take a look at the source code of ArrayList

public boolean add(E e) {
    
    
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

The synchronizad keyword is not added, so thread safety and
thread safety can ensure data consistency, but the read efficiency will decrease. Vector can only be operated by one person at the same time, which is not friendly. Data consistency can be guaranteed, but performance is reduced.

1.2.2.Collections.synchronizedList (collection parameter)

We can convert a thread-unsafe ArrayList into a thread-safe one. When the amount of data is small, this method can be used.

public static void main(String[] args) {
    
    
		// TODO Auto-generated method stub
		List<String> list=Collections.synchronizedList(new ArrayList<>());
		for(int i=1;i<=30;i++){
    
    
			new Thread(()->{
    
    
				//生成一个8位的随机不重复字符串,UUID版的
				list.add(UUID.randomUUID().toString().substring(0,8));
				System.out.println(list);	//默认复写了toString方法。
			},String.valueOf(i)).start();;
		}
	}

1.2.3. CopyOnWriteArrayList class (class in JUC)

public static void main(String[] args) {
    
    
		// TODO Auto-generated method stub
		List<String> list=new CopyOnWriteArrayList();
		
		for(int i=1;i<=30;i++){
    
    
			new Thread(()->{
    
    
				//生成一个8位的随机不重复字符串,UUID版的
				list.add(UUID.randomUUID().toString().substring(0,8));
				System.out.println(list);	//默认复写了toString方法。
			},String.valueOf(i)).start();;
		}
	}

Insert picture description here
How to explain this principle?
For example:
such as the sign-in required during class, the roster is a resource category.
First solve why ArrayList is wrong, ArrayList does not add synchronized. Multiple threads are allowed to grab.
Suppose the situation is ArrayList. There is only one list (resources) on the table at this time. When Zhang San was signing in, he had just finished writing Zhang and was about to write 3. Li Si came over at this time and drew a long one. This is equivalent to causing a concurrent modification exception.
And CopyOnWriteArrayList can control the contention of multiple people, which is related to the lock.
Locked with vector, only one person is allowed to write and one person to read in the same time period. There are more and more people using ArrayList to read, and those who write will compete. Can we solve a problem that can satisfy both writing and reading?
That is to ensure that there is no error when writing, but also to ensure that multiple people read when high concurrency.
At this time we have a third thought, commonly known as copy-on-write, which is also a kind of thought of separation of reading and writing .
Copy-on-write: The
CopyOnWrite container is a copy-on-write container. When adding elements to a container, do not directly add to the current container Object[], but copy the current container Object[] first. Copy out a new container Object[] newElements, and then add elements to the new container Object[] newElements. After adding the elements, point the reference of the original container to the new container setArray(newElements); the advantage of this is that The CopyOnWrite container performs concurrent reading without locking, because the current container does not add any elements, so the CopyOnWrite container is also a concept of separation of reading and writing, reading and writing in different containers.
Let's take a look at the source code
First, the CopyOnWriteArrayList class has an array of Object type and the corresponding get and set methods

private transient volatile Object[] array;
    final Object[] getArray() {
    
    
        return array;
    }
    final void setArray(Object[] a) {
    
    
        array = a;
    }

Find the add method

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();
        }
    }

Now you can explain the above problem, **for ConcurrentModificationException. This exception usually occurs when concurrently reading and writing to the container. **For example, when foreach traverses the List, add add elements to it.
After understanding the ConcurrentModificationException exception, we can think about it in conjunction with COW. If a container is not copied when writing, and it is still the previous container, then the concurrent read operation at this time is the operation performed on the previous container, and a container is being read. When the write operation is performed by another thread, the above error will be reported .
Therefore, the CopyOnWrite container is also an idea of ​​separation of reading and writing. When reading and writing different containers, ConcurrentModificationException will not occur.

The advantages and disadvantages of the CopyOnWrite container:
Advantages: The CopyOnWrite container can be read concurrently without locking, because the current container will not add any elements. The biggest advantage is that the CopyOnWrite container can still be read while being written. The Concurrent container cannot be read while it is being written.
Insufficiency 1: The CopyOnWrite container will copy the internal container when writing, so the internal implementation has an extra copy of the resources required for the core data copy competition, which can be understood as: The space is
not enough for time 2: The CopyOnWrite container only guarantees In order to ensure the final consistency of the data, the Concurrent container ensures the consistency of the data at any time.
Applicable scenarios
do not
require high data consistency in the operation process. According to the analysis of the above-mentioned deficiency 1, it can be concluded that it is more suitable for scenarios where read is greater than write. In other words, the data stored in the CopyOnWrite container should be as unchanged as possible.

2. Thread safety of Set collection

2.1. Thread safety of HashSet collection

public static void main(String[] args) {
    
    
		Set<String> set=new HashSet<>();
		for(int i=1;i<=30;i++){
    
    
			new Thread(()->{
    
    
				//生成一个8位的随机不重复字符串,UUID版的
				set.add(UUID.randomUUID().toString().substring(0,8));
				System.out.println(set);	//默认复写了toString方法。
			},String.valueOf(i)).start();;
		}
	}

Insert picture description hereThe same error was reported. The same exception.

2.2. Thread safe solution for HashSet collection

2.2.1. Collections.synchronizedSet (collection parameters)

Set<String> set=Collections.synchronizedSet(new HashSet<>());

2.2.2.CopyOnWriteArraySet class;

Set<String> set=new CopyOnWriteArraySet<>();

No problem.

2.3. Analysis of the underlying principle of HashSet

The underlying data structure of HashSet is HashMap. If it is a HashMap, to add elements to it, you need to add two key-value pairs. And HashSet only added one. What's the matter?
Let's take a look at the source code of HashSet

public HashSet() {
    
    
        map = new HashMap<>();
}

The bottom layer is HashMap.
Why do one add a key-value pair and one element?
Because the add method at the bottom of HashSet calls the put method of HashMap, an element added by HashSet is the key of HashMap, and the value is always a constant of Object, which is hard-coded.

private static final Object PRESENT = new Object();	//固定常量
public boolean add(E e) {
    
    
    return map.put(e, PRESENT)==null;
}

3. Thread safety of Map collection

3.1. Thread safety issues of HashMap collection

HashMap is not thread safe.

public static void main(String[] args) {
    
    
		Map<String,String> map=new HashMap<>();
		for(int i=1;i<=30;i++){
    
    
			new Thread(()->{
    
    
				//生成一个8位的随机不重复字符串,UUID版的
				map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0,8));
				System.out.println(map);	//默认复写了toString方法。
			},String.valueOf(i)).start();;
		}
	}

Insert picture description here

3.2. Solution to the thread safety problem of HashMap collection

3.2.1. Collections.synchronizedMap (collection parameters)

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

According to the above idea, there should be a CopyOnWriteMap class at this time, but it is not. JUC provides a class called ConcurrentHashMap.

3.2.2. ConcurrentHashMap class

Map<String,String> map=new ConcurrentHashMap<>();
		for(int i=1;i<=30;i++){
    
    
			new Thread(()->{
    
    
				//生成一个8位的随机不重复字符串,UUID版的
				map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0,8));
				System.out.println(map);	//默认复写了toString方法。
			},String.valueOf(i)).start();;
		}

3.3. Analysis of the underlying principle of HashMap

The bottom layer of HashMap is a Node type array + Node type linked list + red-black tree. That is, array + linked list + red-black tree. HashMap is a node of Node type. HashMap stores Node, and Node stores key-value pairs.

  final int hash;
  final K key;
  V value;
  Node<K,V> next;

The default initial capacity of HashMap is 16, and the load factor is 0.75. For
example, the new HashMap() we wrote is equivalent to new HashMap(16,0.75);
when we create an empty HashMap(), the initial capacity of the array is 16 and reaches 16* 0.75=12 will expand. We can give one capacity at a time according to the requirements of our project to avoid multiple expansions.
When ArrayList is expanded, the expansion is half of the original.
The capacity of HashMap is doubled. At the beginning, it was 16. After expansion, the capacity was expanded to 32. After each expansion, the capacity was 2 to the power of n. Therefore, optimizing HashMap is based on the needs of our project, and try to set the initial capacity of HashMap to be larger, and try to avoid expansion.

Guess you like

Origin blog.csdn.net/qq_39736597/article/details/112670327