Defeating Java Multithreading and High Concurrency-Synchronous Containers and Concurrent Containers

This article is a note made when learning Java multithreading and high concurrency knowledge.

This part has a lot of content and is divided into 5 parts according to the content:

  1. Multithreading basics
  2. JUC articles
  3. Synchronous container and concurrent container
  4. Thread Pool
  5. MQ articles

This is a synchronous container and concurrent container.

table of Contents

1 collection

1.1 List

1.2 Set

1.3 Map

2 blocking queue

2.1 Four sets of APIs for operating blocking queues

2.1.1 Throw an exception

2.1.2 Return value

2.1.3 Blocking wait

2.1.4 Timeout waiting

2.2 Synchronization queue

3 AQS


Most of the containers in the java.util package are not thread-safe.

If you want to use the container in multiple threads, you can use the wrapper function provided by java.util.Collections: synchronizedXXX to turn the ordinary container into a thread-safe synchronized container. But this method simply uses synchronization for the container, which is very inefficient.

The java.util.concurrent package provides an efficient concurrent container, and in order to maintain the interface consistency with ordinary containers, the interface of the util package is still used, which is easy to use and easy to understand.

1 collection

We usually say that the collection includes List, Set and Map.

List, Set, and Map are all interfaces provided by the java.util package.

1.1 List

All implementation classes of the List interface:

Among the more commonly used ones are:

  • ArrayList
  • LinkedList

The ArrayList container is a resizable array, which allows all elements, including null.

The LinkedList container is a doubly linked list, which also allows all elements including null.

Both ArrayList and LinkedList are not thread-safe:

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class Test {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        //List<String> list = new LinkedList<>();
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                list.add("element"); //向list中添加元素"element"
                System.out.println(list); //打印list,存在遍历集合的行为
            }).start();
        }
    }
}

No matter if you use ArrayList or LinkedList, you will get an error when you execute the above code:

Exception in thread "Thread-XX" java.util.ConcurrentModificationException

The reason for this exception is that a thread is usually not allowed to modify a collection that is being traversed by other threads. In this case, the result of the iteration is undefined.

For the thread insecurity of ArrayList and LinkedList, the java.util package and java.util.concurrent package provide solutions respectively.

The solution provided by the java.util package is:

Provides a Collenctions class, which provides a set of packaging methods that can make a container object thread-safe. [Decorator Mode]

public static <T> Collection<T> synchronizedCollection(Collection<T> c)
public static <T> List<T> synchronizedList(List<T> list)
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)
public static <T> Set<T> synchronizedSet(Set<T> s)
public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m)
public static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s)

When you see synchronized, you can understand that these packaging methods use synchronized locks to achieve thread synchronization.

Code demo:

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

public class Test {
    public static void main(String[] args) {
        List<String> list = Collections.synchronizedList(new ArrayList<>());
        //List<String> list = Collections.synchronizedList(new LinkedList<>());
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                list.add(Thread.currentThread().getName());
                System.out.println(list);
            }).start();
        }
    }
}

Execute the code and no longer report java.util.ConcurrentModificationException.

The solution provided by the java.util.concurrent package is:

A CopyOnWriteArrayList class is provided, and an instance of the CopyOnWriteArrayList class is created instead of an ArrayList instance.

The principle of CopyOnWriteArrayList:

When a thread writes data to the CopyOnWriteArrayList container, it will first copy out a copy container, then write data into this copy container, and finally assign the reference address of the copy container to the original container address. During the entire writing process, if other threads want to read data, it still reads the data in the original container.

Why is there no CopyOnWriteLinkedList?

The reason is that copy-on-write LinkedList does not have any performance advantages compared to traditional LinkedList.

Implementing CopyOnWriteLinkedList is a waste of time and time using it.

Code demo:

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

public class Test {
    public static void main(String[] args) {
        List<String> list = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                list.add(Thread.currentThread().getName());
                System.out.println(list);
            }).start();
        }
    }
}

No abnormal operation.

1.2 Set

All implementation classes of the Set interface:

Among the more commonly used ones are:

  • HashSet
  • TreeSet

The bottom layer of the HashSet container is a collection of keys of the HashMap container. Its elements are unordered and non-repeatable. Nulls are allowed but only one is allowed.

The bottom layer of the TreeSet container is a collection of the keys of the TreeMap container. Its elements are ordered and non-repeatable. Nulls are allowed but only one is allowed.

Both HashSet and TreeSet are thread-unsafe. Similarly, the java.util.Collections class provides a packaging method, and the java.util.concurrent package provides the CopyOnWriteArraySet class to solve the thread safety problem:

import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;

public class Test {
    public static void main(String[] args) {
        Set<String> set = new HashSet<>(); //HashSet是线程不安全的
        //Set<String> set = new TreeSet<>(); //TreeSet是线程不安全的
        //解决方案1:java.util包提供了Collections类中的包装方法
        //Set<String> set = Collections.synchronizedSet(new HashSet<>());
        //Set<String> set = Collections.synchronizedSet(new TreeSet<>());
        //解决方案2:java.util.concurrent包提供了CopyOnWriteArraySet类
        //Set<String> set = new CopyOnWriteArraySet<>();
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                set.add(Thread.currentThread().getName());
                System.out.println(set);
            }).start();
        }
    }
}

1.3 Map

All implementation classes of the Map interface:

Among the more commonly used ones are:

  • HashMap
  • TreeMap

The HashMap container is an array + linked list structure. Its elements are key-value key-value pairs. The key is unordered and non-repeatable. It is allowed to have null but only one; the value is repeatable and null is allowed.

The TreeMap container is a red-black tree structure, and its elements are also key-value key-value pairs. The key is ordered and non-repeatable, and null is allowed but only one; value is repeatable, and null is allowed.

Both HashMap and TreeMap are thread-unsafe. Similarly, the java.util.Collections class provides packaging methods, and the java.util.concurrent package provides the ConcurrentHashMap class to solve thread safety issues:

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class Test {
    public static void main(String[] args) {
        Map<String, String> map = new HashMap<>(); //HashMap是线程不安全的
        //Map<String, String> map = new TreeMap<>(); //TreeMap是线程不安全的
        //解决方案1:java.util包提供了Collections类中的包装方法
        //Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
        //Map<String, String> map = Collections.synchronizedMap(new TreeMap<>());
        //解决方案2:java.util.concurrent包提供了ConcurrentHashMap类
        //Map<String, String> map = new ConcurrentHashMap<>();
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                map.put(Thread.currentThread().getName(), Thread.currentThread().getName());
                System.out.println(map);
            }).start();
        }
    }
}

 

2 blocking queue

Queue is a kind of linear table. It only allows inserts from the back end of the table and values ​​(deletes) from the front end of the table. Namely FIFO (first in first out), first in first out.

In the java.util.concurrent package, a blocking queue is used as a synchronization container to solve the problem of multi-threaded efficient and safe data transmission.

Under what circumstances will the queue block?

  • The queue is full and blocked
  • The queue is empty, blocked, waiting for production

Blocking queue: BlockingQueue

BlockingQueue is an interface in the java.util.concurrent package, and it also implements the Collection interface.

All implementation classes of the BlockingQueue interface:

Among the more commonly used ones are:

  • ArrayBlockingQueue, an array-based blocking queue
  • LinkedBlockingQueue, a blocking queue based on a linked list
  • SynchronousQueue, synchronous queue

2.1 Four sets of APIs for operating blocking queues

The java.util.concurrent package provides four sets of APIs for operating blocking queues:

  • Throw an exception
  • return value
  • Blocking wait
  • Timeout waiting

They have their own purposes and can be used flexibly.

Operation method Throw an exception return value Blocking wait Timeout waiting
Add to add offer put offer(,,)
Remove remove poll take poll(,)
Determine the top of the queue element peek - -

2.1.1 Throw an exception

  • add(), insert an element to the end of the blocking queue, the operation is successful and return true, if the queue is full and blocking occurs, an exception java.lang.IllegalStateException is reported.
  • remove(), get the value from the head of the blocking queue (delete), the operation is successful and return the value, if the queue is empty, an exception java.util.NoSuchElementException is reported.
  • element(), view the first element of the blocking queue, the operation returns the value of the first element of the queue successfully, if the queue is empty, an exception java.util.NoSuchElementException is reported.

Test code:

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        test1();
    }
    
    public static void test1() {
        BlockingQueue blockingQueue = new ArrayBlockingQueue(2); //设置阻塞队列的大小为2
        System.out.println(blockingQueue.add("a"));
        System.out.println(blockingQueue.add("b"));
        //System.out.println(blockingQueue.add("c")); //队列已满,发生阻塞,发生异常
        System.out.println(blockingQueue.element());
        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());
        //System.out.println(blockingQueue.remove()); //队列为空,发生异常
        //System.out.println(blockingQueue.element()); //队列为空,发生异常
    }
}

operation result:

true
true
a
a
b

2.1.2 Return value

  • offer(), insert an element to the end of the blocking queue, return true if the operation succeeds, and return false if the queue is full and blocked.
  • poll(), get the value from the head of the blocking queue (delete), return the value if the operation is successful, and return null if the queue is empty.
  • peek(), check the first element of the blocking queue, return the value of the first element of the queue if the operation is successful, and return null if the queue is empty.

Test code:

​import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        test2();
    }

    public static void test2() {
        BlockingQueue blockingQueue = new ArrayBlockingQueue(2); //设置阻塞队列的大小为2
        System.out.println(blockingQueue.offer("a"));
        System.out.println(blockingQueue.offer("b"));
        System.out.println(blockingQueue.offer("c"));
        System.out.println(blockingQueue.peek());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.peek());
    }
}

operation result:

true
true
false
a
a
b
null
null

2.1.3 Blocking wait

  • put(), insert an element to the end of the blocking queue, no return value, if the queue is full and blocking occurs, the current thread waits.
  • take(), take the value from the head of the blocking queue (delete), the operation is successful and return the value, if the queue is empty, the current thread waits.

Test code:

​import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        test3();
    }

    public static void test3() throws InterruptedException {
        BlockingQueue blockingQueue = new ArrayBlockingQueue(2); //设置阻塞队列的大小为2
        blockingQueue.put("a");
        blockingQueue.put("b");
        System.out.println("point1");
        //blockingQueue.put("c"); //队列已满,发生阻塞,线程等待
        System.out.println("point2");
        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
        System.out.println("point3");
        //System.out.println(blockingQueue.take()); //队列为空,线程等待
        System.out.println("point4");
    }
}

operation result:

point1
point2
a
b
point3
point4

2.1.4 Timeout waiting

  • offer(,,): Insert an element to the end of the blocking queue, and the operation returns true if the operation succeeds. If the queue is full and blocked, the current thread waits. After waiting for a certain period of time, it returns false and continues to execute downward.
  • poll(,): Get the value from the head of the blocking queue (delete), and the operation returns the value successfully. If the queue is empty, the current thread waits. After waiting for a certain time, it returns null and continues to execute downward.
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        test4();
    }

    public static void test4() throws InterruptedException {
        BlockingQueue blockingQueue = new ArrayBlockingQueue(2); //设置阻塞队列的大小为2
        System.out.println(blockingQueue.offer("a", 2, TimeUnit.SECONDS));
        System.out.println(blockingQueue.offer("b", 2, TimeUnit.SECONDS));
        System.out.println(blockingQueue.offer("c", 2, TimeUnit.SECONDS)); //超时等待时间:2秒
        System.out.println(blockingQueue.poll(2, TimeUnit.SECONDS));
        System.out.println(blockingQueue.poll(2, TimeUnit.SECONDS));
        System.out.println(blockingQueue.poll(2, TimeUnit.SECONDS)); //超时等待时间:2秒
        System.out.println("ok");
    }
}

operation result:

true
true
false
a
b
null
ok

2.2 Synchronization queue

Synchronous queue: SynchronousQueue

The SynchronousQueue class is an implementation class of the BlockingQueue interface.

The characteristics of the SynchronousQueue container: the capacity is 1. After putting in an element, you must wait for the element to be taken out before you can put the element inside.

Test code:

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;

public class Test {
    public static void main(String[] args) {
        BlockingQueue<String> blockingQueue = new SynchronousQueue<>();
        new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + " put 1");
                blockingQueue.put("1");
                System.out.println(Thread.currentThread().getName() + " put 2");
                blockingQueue.put("2");
                System.out.println(Thread.currentThread().getName() + " put 3");
                blockingQueue.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "T1").start();
        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
                System.out.println(Thread.currentThread().getName() + " take " + blockingQueue.take());
                TimeUnit.SECONDS.sleep(1);
                System.out.println(Thread.currentThread().getName() + " take " + blockingQueue.take());
                TimeUnit.SECONDS.sleep(1);
                System.out.println(Thread.currentThread().getName() + " take " + blockingQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "T2").start();
    }
}

operation result:

T1 put 1
T2 take 1
T1 put 2
T2 take 2
T1 put 3
T2 take 3

 

3 AQS

AQS: AbstractQueuedSynchronizer, abstract queue synchronizer.

AQS maintains a variable state of type volatile int and a queue implemented by a double-linked list. The elements in the queue are threads (Thread objects).

The initial value of state is 0.

When a thread A (using the ReentrantLock object) calls the lock() method, it will call a tryAcquire() method to try to acquire the lock resource [CAS]:

If state is 0, thread A successfully acquires the lock resource, state+1, and thread A is set as an exclusive thread;

If the lock resource has already been monopolized by another thread B, that is, the state is not 0, thread A fails to obtain the lock resource and spins to wait; thread A spins for a certain number of times, and enters the CLH queue.

When thread B calls the lock() method again while holding the lock resource, the lock reentrance occurs, state+1; each time thread B calls the unlock() method, state-1, until state is reduced to 0, thread B Release lock resources.

After thread B releases the lock resource, the threads in the CLH queue continue to grab the lock resource.

tryAcquire() source code:

 

Learning video link:

https://www.bilibili.com/video/BV1B7411L7tE

加油! (d • _ •) d

Guess you like

Origin blog.csdn.net/qq_42082161/article/details/114002333