[JCIP Notes] (5) JDK concurrent packages

This section will talk about some important thread safety related classes in the java.util.concurrent package.

synchronized容器

A synchronized container is a container that encapsulates its own internal state and controls access to shared variables by setting each public method to be synchronized. It mainly includes Vector, Hashtable, and wrappers provided by Collections.synchronizedxxx() methods.

Problems with synchronized containers - client locking

First of all, although the synchronzied container is thread-safe, the thread that wants to access the internal data of the container can only access the container's built-in lock first, which is actually equivalent to serial access, and the CPU utilization and efficiency are not high.
Another point worth noting is that when user code uses a synchronized container, if it needs to do some compound operations, such as put-if-absent, it still needs to be explicitly locked (called client locking), otherwise a race condition will be generated.
For example the following operations:

1 public Object getLast(Vector list){
2   int last = list.size() - 1; //1
3   return list.get(last); //2
4 }
5 public void removeLast(Vector list){
6   int last = list.size() - 1; //3
7   list.remove(last); //4
8 }

The above two methods both perform compound operations on Vector, which may result in a scenario where thread A calls getLast(), while thread B calls removeLast(). Thread A performs step 1 to get the last and thread B also gets the same last; at this time, due to thread scheduling reasons, thread B first executes step 4 to delete the last node, and thread A executes step 2 after this. , because the last node has been deleted, thread A will report ArrayIndexOutOfBoundsException here, and this error is not what the user wants to see.


  

So if you want to use the synchronized container in a similar way, you still need to lock it yourself. Since the thread safety policy inside these containers is to use their own built-in locks, the container itself needs to be used when user code locks.

 1 public Object getLast(Vector list){
 2   synchronized(list){
 3     int last = list.size() - 1; //1
 4     return list.get(last); //2
 5   }
 6 }
 7 public void removeLast(Vector list){
 8   synchronized(list){
 9     int last = list.size() - 1; //3
10     list.remove(last); //4
11   }
12 }

In addition to these user-defined composite operations, iteration is also a composite operation, so it should also be locked. Two points should be noted here:

  1.  The Iterator that comes with the container itself does not support concurrent modification, so it provides a so-called fail-fast concurrent modification error reporting mechanism, that is, the container itself maintains a modCount field, and the Iterator records the value of this modCount when it is created. If the user traverses the container The modCount value changes during the process, indicating that another thread has made modifications to the container, then the Iterator will immediately throw a ConcurrentModificationException.

    Strictly speaking, this mechanism cannot detect concurrent modifications 100%, because the modCount field is not volatile.    

    if(modCount == expectedModCount)

   It is also not locked. The authors describe this mechanism as a best-effort effort with performance in mind. In short, there should not be too much reliance on this mechanism.

       2. There are some methods that come with containers that look innocent, but iterators are used internally, so users still need to lock when they use these innocent methods. Such as our commonly used toString, for-each syntax, hashCode, equals, containsAll, removeAll, retainAll, constructors with other containers as parameters, and so on. And these methods are sometimes called implicitly, which is difficult to detect, such as:

    1 //...add some elements to the set
    2 System.out.println("DEBUG: added ten elements to " + set);

    Here the set.toString() method is implicitly called when printing.

The problem of client locking

Since the client code tries to use the thread safety mechanism inside the container, it is easy to cause starvation and deadlock. This is because any code can use the built-in lock of the container, and the thread safety mechanism scattered everywhere makes the program difficult to maintain and debug. If you want to solve this problem, you can clone the container into the thread for use, but you have to re-clone each time you use it, and you must consider the cost of cloning itself.

Concurrent container

Compared to synchronized containers, Concurrent containers can provide higher concurrency.
If concurrent Map is required, ConcurrentHashMap can be given priority over synchronized Map; similarly, CopyOnWriteArrayList/Set can be given priority over synchronized List/Set; ConcurrentSkipMap/Set can be given priority over synchronized SortedMap/Set.

ConcurrentHashMap

+ Uses a more fine-grained lock striping thread safety policy than Hashtable, and supports multiple (limited) threads to read and write at the same time.
+ The provided Iterator is weakly consistent, allowing concurrent modifications.
- methods like size/isEmpty only provide estimates.
- Since the lock object used is private, client-side locking is not supported. (but provides compound operations such as put-if-absent)

CopyOnWriteArrayList

+ Create and publish a new collection copy for every change.
+ The internal array is effectively immutable, so it can be safely accessed without locking after release.
+ works in the case of iteration >> modification, like listeners.

BlockingQueue and Producer-Consumer

The biggest advantage of BlockingQueue is that it is not only a simple container, it can also provide flow-control, which allows the program to remain robust in the case of too many messages.

Special BlockingQueue: SynchronousQueue

A very special kind of queue that actually has no intrinsic storage, but is only used for handover (rendezvous) between threads. It is suitable for situations where there are enough consumers. The biggest advantage over BlockingQueue is that there is no handover cost.

 1 Thread producer = new Thread("PRODUCER") {
 2   public void run() {
 3     String event = "MY_EVENT";
 4     try {
 5       queue.put(event); // thread will block here
 6       System.out.printf("[%s] published event : %s %n", Thread.currentThread().getName(), event);
 7     } catch (InterruptedException e) {
 8       e.printStackTrace();
 9     }
10   }
11 };
12 producer.start(); // starting publisher thread
13 
14 
15 Thread consumer = new Thread("CONSUMER") {
16   public void run() {
17     try {
18       String event = queue.take(); // thread will block here
19       System.out.printf("[%s] consumed event : %s %n", Thread.currentThread().getName(), event);
20     } catch (InterruptedException e) {
21       e.printStackTrace();
22     }
23   }
24 };
25 consumer.start(); // starting consumer thread
26 
27 [PRODUCER] published event : MY_EVENT
28 [CONSUMER] consumed event : MY_EVENT

 

Synchronizers

The so-called synchronizer is an object that can adjust the control flow of a thread according to its internal state.

CountDownLatch

Main methods:
  - countDown
  - await
CountDownLatch is like a valve that closes until it reaches its final state and the thread cannot pass. When the final state is reached, the valve opens and all threads pass. After opening the valve is always open and the state does not change.

Applicable scenarios:

  • Wait until all dependent resources are loaded before continuing. 
  • Mutual wait between services in the initialization sequence. 
  • Wait until all participating players are ready before starting the game.

FutureTask

Main method:
The get method will block before the get task actually ends, until the task execution ends/is canceled/throws an exception.

Semaphore

Main methods:
  - release
  - acquire

There are a limited number of permits. If the permit is 0 during acquire, it will block, but the release can be executed infinitely many times.

Good for: Controls the number of activities that can access a resource at the same time. Can be used to implement resource pools or to make a container a container that can store a limited number of elements.

CyclicBarrier

Main method: await

all threads can pass when all threads reach the Barrier.

Latch is used to wait for events; Barrier is used to wait for other threads.

Applicable scenarios: N etc. N

 1 public class CellularAutomata {
 2     private final Board mainBoard;
 3     private final CyclicBarrier barrier;
 4     private final Worker[] workers;
 5 
 6     public CellularAutomata(Board board) {
 7         this.mainBoard = board;
 8         int count = Runtime.getRuntime().availableProcessors();
 9         this.barrier = new CyclicBarrier(count,
10                 new Runnable() {
11                     public void run() {
12                         mainBoard.commitNewValues();
13                     }});
14         this.workers = new Worker[count];
15         for (int i = 0; i < count; i++)
16             workers[i] = new Worker(mainBoard.getSubBoard(count, i));
17     }
18 
19     private class Worker implements Runnable {
20         private final Board board;
21 
22         public Worker(Board board) { this.board = board; }
23         public void run() {
24             while (!board.hasConverged()) {
25                 for (int x = 0; x < board.getMaxX(); x++)
26                     for (int y = 0; y < board.getMaxY(); y++)
27                         board.setNewValue(x, y, computeValue(x, y));
28                 try {
29                     barrier.await();
30                 } catch (InterruptedException ex) {
31                     return;
32                 } catch (BrokenBarrierException ex) {
33                     return;
34                 }
35             }
36         }
37 
38         private int computeValue(int x, int y) {
39             // Compute the new value that goes in (x,y)
40             return 0;
41         }
42     }
43 
44     public void start() {
45         for (int i = 0; i < workers.length; i++)
46             new Thread(workers[i]).start();
47         mainBoard.waitForConvergence();
48     }
49 }

 

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325030626&siteId=291194637