Quick overview of java multithreading points (Java concurrent containers and frameworks, atomic operation classes, concurrent tool classes)

Implementation principle and use of ConcurrentHashMap

ConcurrentHashMap is a thread-safe and efficient HashMap. Using HashMap in concurrent programming may lead to an infinite loop of the program. The efficiency of using thread-safe HashTable is very low. Based on the above two reasons, ConcurrentHashMap has an opportunity to appear.
In a multi-threaded environment, using HashMap for put operations will cause an infinite loop, resulting in CPU utilization close to 100%, so HashMap cannot be used in concurrent situations. For example, executing the following code will cause an infinite loop.

final HashMap<String, String> map = new HashMap<String, String>(2);
Thread t = new Thread(new Runnable() {
    
    
	@Override
	public void run() {
    
    
		for (int i = 0; i < 10000; i++) {
    
    
			new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				map.put(UUID.randomUUID().toString(), "");
				}
			}, "ftf" + i).start();
		}
	}
}, "ftf");
t.start();
t.join();

HashMap will cause an infinite loop when the put operation is executed concurrently, because multi-threading will cause the HashMap Entry linked list to form a ring data structure. Once the ring data structure is formed, the next node of the Entry will never be empty, and an infinite loop will be generated to obtain the Entry.

The HashTable container uses synchronized to ensure thread safety, but the efficiency of HashTable is very low in the case of intense thread competition. Because when a thread accesses the synchronization method of HashTable and other threads also access the synchronization method of HashTable, it will enter the blocking or polling state. For example, thread 1 uses put to add elements, thread 2 not only cannot use the put method to add elements, but also cannot use the get method to obtain elements, so the more intense the competition, the lower the efficiency. Lock segmentation technology
used by ConcurrentHashMap . First, the data is divided into sections and stored one by one, and then a lock is assigned to each section of data. When a thread occupies a lock to access one section of data, other sections of data can also be accessed by other threads.

ConcurrentHashMap is composed of Segment array structure and HashEntry array structure. Segment is a reentrant lock (ReentrantLock), which plays the role of lock in ConcurrentHashMap; HashEntry is used to store key-value pair data. A ConcurrentHashMap contains a Segment array. The structure of Segment is similar to HashMap, which is an array and linked list structure. A Segment contains a HashEntry array. Each HashEntry is an element of a linked list structure. Each Segment guards an element in a HashEntry array. When modifying the data of the HashEntry array, you must first obtain the corresponding Segment lock.
insert image description here
The ConcurrentHashMap initialization method is implemented by initializing the segment array, segment offset segmentShift, segment mask segmentMask and HashEntry array in each segment through several parameters such as initialCapacity, loadFactor and concurrencyLevel.
The operation of ConcurrentHashMap
1. get operation
Segment's get operation is very simple and efficient. After one pass and then hash, then use this hash value to locate the Segment through the hash operation, and then locate the element through the hash algorithm, the code is as follows

public V get(Object key) {
    
    
	int hash = hash(key.hashCode());
	return segmentFor(hash).get(key, hash);
}

The efficiency of the get operation is that the entire get process does not need to be locked, unless the read value is empty, it will be locked and re-read. The shared variables to be used in the get method are all defined as volatile types. Variables defined as volatile can maintain visibility between threads, can be read by multiple threads at the same time, and are guaranteed not to read expired values, but can only be used by a single Thread writing (one case can be written by multiple threads, that is, the written value does not depend on the original value), in the get operation, only need to read and not write the shared variables count and value, so there is no need to lock. The reason why the expired value will not be read is because according to the happen before principle of the Java memory model, the write operation to the volatile field is prior to the read operation, even if two threads modify and obtain the volatile variable at the same time, the get operation can also get The latest value, which is a classic application scenario of replacing locks with volatile.
2. put operation
Since the put method needs to write to the shared variable, for thread safety, a lock must be added when operating the shared variable
. The put method first locates the Segment, and then performs an insert operation in the Segment. The insertion operation needs to go through two
steps. The first step is to determine whether the HashEntry array in the segment needs to be expanded. The second step is to locate the position of the added element
, and then place it in the HashEntry array.
3. Size operation
If you want to count the size of the elements in the entire ConcurrentHashMap, you must count the sizes of the elements in all segments and then sum them. In the process of accumulating the count, the probability of the previously accumulated count changing is very small, so the method of ConcurrentHashMap is to try twice to count the size of each segment by not locking the segment. If the count of the container is in the process of counting If there is a change, then use locking to count the size of all segments.
ConcurrentLinkedQueue
ConcurrentLinkedQueue is an unbounded thread-safe queue based on linked nodes. It uses the first-in-first-out rule to sort the nodes. When we add an element, it will be added to the tail of the queue; when we get an element, it will return The element at the head of the queue. It uses the "wait-free" algorithm (that is, the CAS algorithm) to implement, which has made some modifications on the Michael&Scott algorithm.

ConcurrentLinkedQueue is composed of a head node and a tail node. Each node (Node) is composed of a node element (item) and a reference to the next node (next). Nodes are connected through this next to form a linked list. A queue of structures. By default, the element stored in the head node is empty, and the tail node is equal to the head node.

Enqueue:
Enqueue is to add enqueue nodes to the end of the queue. Joining the team mainly does two things: the first is to set the joining node as the next node of the current queue tail node; the second is to update the tail node, if the next node of the tail node is not empty, set the joining node to tail node, if the next node of the tail node is empty, set the enqueue node to the next node of tail, so the tail node is not always the tail node

Out of the queue:
out of the queue is to return a node element from the queue, and clear the node's reference to the element. It is not necessary to update the head node every time the queue is released. When there are elements in the head node, the elements in the head node will be popped up directly without updating the head node. Only when there are no elements in the head node, the dequeue operation will update the
blocking queue in the head node Java.
The blocking queue (BlockingQueue) is a queue that supports two additional operations. These two additional operations support blocking insertion and removal methods.
1) Insertion method that supports blocking: it means that when the queue is full, the queue will block the thread that inserts elements until the queue is not full.
2) The removal method that supports blocking: means that when the queue is empty, the thread that gets the element will wait for the queue to become non-empty.
Blocking queues are often used in the scenario of producers and consumers. The producer is the thread that adds elements to the queue, and the consumer is the thread that takes elements from the queue. A blocking queue is a container used by producers to store elements and used by consumers to obtain elements.
insert image description here
JDK 7 provides seven blocking queues, as follows.
ArrayBlockingQueue: A bounded blocking queue composed of an array structure.
LinkedBlockingQueue: A bounded blocking queue composed of a linked list structure.
PriorityBlockingQueue: An unbounded blocking queue that supports priority sorting.
DelayQueue: An unbounded blocking queue implemented using a priority queue.
SynchronousQueue: A blocking queue that does not store elements.
LinkedTransferQueue: An unbounded blocking queue composed of a linked list structure.
LinkedBlockingDeque: A two-way blocking queue composed of a linked list structure.

Fork/Join framework
The Fork/Join framework is a framework provided by Java 7 for executing tasks in parallel. It is a framework for dividing a large task into several small tasks, and finally summarizing the results of each small task to obtain the result of the large task.
insert image description here
The work-stealing algorithm refers to a thread stealing tasks from other queues for execution. If we need to do a relatively large task, we can divide this task into several independent subtasks. In order to reduce the competition between threads, put these subtasks into different queues and create a queue for each queue. A separate thread executes the tasks in the queue, and there is a one-to-one correspondence between threads and queues. For example, thread A is responsible for processing tasks in queue A. However, some threads will finish the tasks in their own queues first, while there are still tasks waiting to be processed in the queues corresponding to other threads. Instead of waiting, the thread that has finished its work might as well help other threads to work, so it goes to the queue of other threads
to steal a task to execute. At this time, they will access the same queue, so in order to reduce the competition between the stealing task thread and the stolen task thread, a double-ended queue is usually used, and the stolen task thread always executes the task from the head of the double-ended queue. The thread that steals the task always executes the task from the tail of the double-ended queue.
Work-stealing operation process:
insert image description here
The advantages of the work-stealing algorithm: make full use of threads for parallel computing, reducing competition among threads.
Disadvantages of the work-stealing algorithm: In some cases there is still competition, such as when there is only one task in the double-ended queue. And the algorithm will consume more system resources, such as creating multiple threads and multiple double-ended queues.
Design of Fork/Join framework:
Step 1 Split tasks. First of all, we need a fork class to divide the large task into subtasks. It is possible that the subtasks are still very large, so we need to keep dividing until the subtasks are small enough.
Step 2 Execute the task and combine the results. The divided subtasks are placed in the double-ended queue, and then several startup threads obtain tasks from the double-ended queue for execution. The results of the subtasks are all placed in a queue, and a thread is started to get data from the queue, and then merge the data.
Fork/Join uses two classes to accomplish the above two things.
①ForkJoinTask: To use the ForkJoin framework, we must first create a ForkJoin task. It provides mechanisms to perform fork() and join() operations in tasks. Normally, we don't need to directly inherit the ForkJoinTask class, but only need to inherit its subclasses. The Fork/Join framework provides the following two subclasses.
· RecursiveAction: for tasks that do not return results.
· RecursiveTask: for tasks that return results.
②ForkJoinPool: ForkJoinTask needs to be executed through ForkJoinPool.
The subtasks split from the task will be added to the double-ended queue maintained by the current worker thread and enter the head of the queue. When there is no task in the queue of a worker thread, it will randomly get a task from the tail of the queue of other worker threads.

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveTask;
public class CountTask extends RecursiveTask<Integer> {
    
    
	private static final int THRESHOLD = 2; // 阈值
	private int start;
	private int end;
	public CountTask(int start, int end) {
    
    
		this.start = start;
		this.end = end;
	}
	@Override
	protected Integer compute() {
    
    
		int sum = 0;
		// 如果任务足够小就计算任务
		boolean canCompute = (end - start) <= THRESHOLD;
		if (canCompute) {
    
    
		for (int i = start; i <= end; i++) {
    
    
			sum += i;
			}
		} else {
    
    
		// 如果任务大于阈值,就分裂成两个子任务计算
	int middle = (start + end) / 2;
	CountTask leftTask = new CountTask(start, middle);
	CountTask rightTask = new CountTask(middle + 1, end);
	// 执行子任务
	leftTask.fork();
	rightTask.fork();
	// 等待子任务执行完,并得到其结果
	int leftResult=leftTask.join();
	int rightResult=rightTask.join();
	// 合并子任务
	sum = leftResult + rightResult;
	}
	return sum;
}
public static void main(String[] args) {
    
    
	ForkJoinPool forkJoinPool = new ForkJoinPool();
	// 生成一个计算任务,负责计算1+2+3+4
	CountTask task = new CountTask(1, 4);
	// 执行一个任务
	Future<Integer> result = forkJoinPool.submit(task);
	try {
    
    
		System.out.println(result.get());
	} catch (InterruptedException e) {
    
    
	} catch (ExecutionException e) {
    
    
	}
	}
}

The main difference between ForkJoinTask and general tasks is that it needs to implement the compute method. In this method, it is first necessary to determine whether the task is small enough, and if it is small enough, execute the task directly. If it is not small enough, it must be divided into two subtasks. When each subtask calls the fork method, it will enter the compute method to see if the current subtask needs to be divided into subtasks. If it does not need to continue to be divided, execute the current subtask. task and return the result. Using the join method will wait for the subtask to complete and get its result.

The implementation principle of the Fork/Join framework:
ForkJoinPool is composed of ForkJoinTask array and ForkJoinWorkerThread array. ForkJoinTask array is responsible for submitting the storage program to ForkJoinPool. ForkJoinWorkerThread array is responsible for executing these tasks.

13 atomic operation classes in Java

Java has provided the java.util.concurrent.atomic package (hereinafter referred to as the Atomic package) since JDK 1.5. The atomic operation class in this package provides a way to update a variable with simple usage, high performance, and thread safety. The Atomic package provides a total of 13 classes, which belong to 4 types of atomic update methods, namely atomic update basic type, atomic update array, atomic update reference and atomic update attribute (field). The classes in the Atomic package are basically wrapper classes implemented using Unsafe.
Atomic update basic type class:
AtomicBoolean: atomic update Boolean type.
·AtomicInteger: Atomic update integer.
·AtomicLong: Atomic update long integer.
Just take AtomicInteger as an example to explain, and the common methods of AtomicInteger are as follows.
· int addAndGet (int delta): Atomically adds the input value to the value in the instance (the value in AtomicInteger), and returns the result.
boolean compareAndSet(int expect, int update): Atomically sets the value to the value entered if the value entered is equal to the expected value.
·int getAndIncrement(): Increment the current value by 1 in an atomic way. Note that the value returned here is the value before the auto-increment.
· void lazySet (int newValue): will be set to newValue eventually, after using lazySet to set the value, other threads may still be able to read the old value for a short period of time
· int getAndSet (int newValue): atomically set is the value of newValue and returns the old value.

How does getAndIncrement implement atomic operations?

public final int getAndIncrement() {
    
    
	for (;;) {
    
    
		int current = get();
		int next = current + 1;
		if (compareAndSet(current, next))
			return current;
		}
}
public final boolean compareAndSet(int expect, int update) {
    
    
	return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

The first step of the for loop body in the source code is to obtain the value stored in the AtomicInteger, the second step is to add 1 to the current value of the AtomicInteger, and the key third step is to call the compareAndSet method to perform the atomic update operation. This method first checks the current value Whether it is equal to current, equal to means that the value of AtomicInteger has not been modified by other threads, then update the current value of AtomicInteger to the value of next, if the compareAndSet method returns false, the program will enter the for loop to perform the compareAndSet operation again.
Atomic update array:
Update an element in the array in an atomic way. The Atomic package provides the following:
·AtomicIntegerArray: Atomic update the element in the integer array.
AtomicLongArray: atomically update the elements in the long integer array.
AtomicReferenceArray: atomically update the elements in the reference type array

·The AtomicIntegerArray class mainly provides an atomic way to update the integer in the array, and its common methods are as follows.
· int addAndGet(int i, int delta): Atomically adds the input value to the element at index i in the array.
· boolean compareAndSet(int i, int expect, int update): If the current value is equal to the expected value, then atomically set the element at array position i to the update value.

public class AtomicIntegerArrayTest {
    
    
	static int[] value = new int[] {
    
     12 };
	static AtomicIntegerArray ai = new AtomicIntegerArray(value);
	public static void main(String[] args) {
    
    
		ai.getAndSet(03);
		System.out.println(ai.get(0));
		System.out.println(value[0]);
	}
}

Output result:

3
1

The array value is passed in through the construction method, and then AtomicIntegerArray will copy the current array, so when AtomicIntegerArray modifies the internal array elements, it will not affect the incoming array.
Atom update reference type
Atomic update of basic type AtomicInteger can only update one variable. If you want to update multiple variables atomically, you need to use the class provided by this atomic update reference type. The Atomic package provides the following three classes.
AtomicReference: atomic update reference type.
· AtomicReferenceFieldUpdater: atomically update the fields in the reference type.
AtomicMarkableReference: A reference type with a marked bit for atomic update. It is possible to atomically update a boolean tag bit and a reference type. The construction method is AtomicMarkableReference(V initialRef, boolean initialMark).
Take AtomicReference as an example:

public class AtomicReferenceTest {
    
    
	public static AtomicReference<user> atomicUserRef = new AtomicReference<user>();
	public static void main(String[] args) {
    
    
		User user = new User("conan"15);
		atomicUserRef.set(user);
		User updateUser = new User("Shinichi"17);
		atomicUserRef.compareAndSet(user, updateUser);
		System.out.println(atomicUserRef.get().getName());
		System.out.println(atomicUserRef.get().getOld());
	}
	static class User {
    
    
		private String name;
		private int old;
		public User(String name, int old) {
    
    
			this.name = name;
			this.old = old;
		}
		public String getName() {
    
    
			return name;
		}
		public int getOld() {
    
    
			return old;
		}
	}
}

In the code, first construct a user object, then set the user object into AtomicReferenc, and finally call the compareAndSet method to perform the atomic update operation. The implementation principle is the same as the compareAndSet method in AtomicInteger. After the code is executed, the output results are as follows.

Shinichi
17

Atomic update field class
If you need to atomically update a field in a certain class, you need to use the atomic update field class. The Atomic package provides the following three classes for atomic field update.
AtomicIntegerFieldUpdater: The updater for atomically updating integer fields.
AtomicLongFieldUpdater: An updater for atomically updating long integer fields.
· AtomicStampedReference: Atomic update reference type with a version number. This class associates integer values ​​with references, which can be used for atomic update data and data version numbers, and can solve the ABA problem that may occur when using CAS for atomic update.

Atomically updating a field class requires two steps. The first step, because the atomic update field class is an abstract class, you must use the static method newUpdater() to create an updater every time you use it, and you need to set the class and attributes you want to update. In the second step, the fields (properties) of the update class must use the public volatile modifier.

public class AtomicIntegerFieldUpdaterTest {
    
    
	// 创建原子更新器,并设置需要更新的对象类和对象的属性
	private static AtomicIntegerFieldUpdater<User> a = AtomicIntegerFieldUpdater.	newUpdater(User.class"old");
	public static void main(String[] args) {
    
    
		// 设置柯南的年龄是10岁
		User conan = new User("conan"10);
		// 柯南长了一岁,但是仍然会输出旧的年龄
		System.out.println(a.getAndIncrement(conan));
	// 输出柯南现在的年龄
	System.out.println(a.get(conan));
}
	public static class User {
    
    
		private String name;
		public volatile int old;
		public User(String name, int old) {
    
    
			this.name = name;
			this.old = old;
		}
		public String getName() {
    
    
			return name;
		}
		public int getOld() {
    
    
			return old;
		}
	}
}

Output result:

10
11

Concurrency tools in Java

The CountDownLatch, CyclicBarrier and Semaphore tool classes provide a means of concurrent process control, and the Exchanger tool class provides a means of exchanging data between threads.
CountDownLatch that waits for multiple threads to complete
CountDownLatch allows one or more threads to wait for other threads to complete operations. If there is such a requirement: we need to analyze the data of multiple sheets in an Excel, we can consider using multi-threading at this time, each thread parses the data in a sheet, and after all the sheets are parsed, the program needs to prompt that the parsing is complete . In this requirement, the easiest way to realize that the main thread waits for all threads to complete the sheet parsing operation is to use the join() method, as shown in the code list.

public class JoinCountDownLatchTest {
    
    
	public static void main(String[] args) throws InterruptedException {
    
    
		Thread parser1 = new Thread(new Runnable() {
    
    
		@Override
		public void run() {
    
    
		}
	});
		Thread parser2 = new Thread(new Runnable() {
    
    
		@Override
		public void run() {
    
    
			System.out.println("parser2 finish");
		}
	});
		parser1.start();
		parser2.start();
		parser1.join();
		parser2.join();
		System.out.println("all parser finish");
	}
}

join is used to let the current execution thread wait for the execution of the join thread to end. The implementation principle is to keep checking whether the join thread is alive, and if the join thread is alive, let the current thread wait forever. Until the join thread terminates, the thread's this.notifyAll() method will be called.

CountDownLatch can also implement the function of join, and has more functions than join:

public class CountDownLatchTest {
    
    
	staticCountDownLatch c = new CountDownLatch(2);
	public static void main(String[] args) throws InterruptedException {
    
    
		new Thread(new Runnable() {
    
    
		@Override
		public void run() {
    
    
				System.out.println(1);
				c.countDown();
				System.out.println(2);
				c.countDown();
			}
		}).start();
	c.await();
	System.out.println("3");
	}
}

The constructor of CountDownLatch receives a parameter of type int as a counter. If you want to wait for N points to complete, pass in N here.
When we call the countDown method of CountDownLatch, N will decrease by 1, and the await method of CountDownLatch will block the current thread until N becomes zero. Since the countDown method can be used anywhere, the N points mentioned here can be N threads, or N execution steps in one thread. When used in multiple threads, you only need to pass the CountDownLatch reference to the thread.
Synchronization barrier CyclicBarrier
CyclicBarrier literally means recyclable (Cyclic) barrier (Barrier). What it has to do is to block a group of threads when they reach a barrier (also called a synchronization point). The barrier will not open until the last thread reaches the barrier, and all threads blocked by the barrier will continue to run.
The default construction method is CyclicBarrier (int parties), whose parameter indicates the number of threads intercepted by the barrier. Each thread calls the await method to tell CyclicBarrier that I have reached the barrier, and then the current thread is blocked.

public class CyclicBarrierTest {
    
    
	staticCyclicBarrier c = new CyclicBarrier(2);
	public static void main(String[] args) {
    
    
		new Thread(new Runnable() {
    
    
		@Override
		public void run() {
    
    
			try {
    
    
				c.await();
			} catch (Exception e) {
    
    
		}
		System.out.println(1);
	}
	}).start();
	try {
    
    
		c.await();
	} catch (Exception e) {
    
    
	}
	System.out.println(2);
	}
}

Because the scheduling of the main thread and sub-threads is determined by the CPU, it is possible for both threads to execute first, so there will be two outputs, 1 2or 2 1. If new CyclicBarrier(2) is changed to new CyclicBarrier(3), the main thread and sub-threads will wait forever, because there is no third thread to execute the await method, that is, no third thread reaches the barrier, so the two before reaching the barrier Threads will not continue to execute
CyclicBarrier. It also provides a more advanced constructor CyclicBarrier (int parties, Runnable barrierAction), which is used to execute barrierAction first when a thread reaches the barrier, which is convenient for handling more complex business scenarios, as shown in the code list.

import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierTest2 {
    
    
	static CyclicBarrier c = new CyclicBarrier(2, new A());
	public static void main(String[] args) {
    
    
		new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				try {
    
    
					c.await();
				} catch (Exception e) {
    
    
			}
			System.out.println(1);
		}
	}).start();
		try {
    
    
			c.await();
		} catch (Exception e) {
    
    
	}
	System.out.println(2);
	}
	static class A implements Runnable {
    
    
		@Override
		public void run() {
    
    
		System.out.println(3);
		}
	}
}

Because CyclicBarrier sets the number of intercepted threads to be 2, it must wait until the first thread and thread A in the code are executed before continuing to execute the main thread, and then output 2, so the output after the code is executed is as follows: 3 1 2. The counter of CountDownLatch can only be used once, while the counter of CyclicBarrier can be reset using the reset() method. So CyclicBarrier can handle more complex business scenarios.
Semaphore that controls the number of concurrent threads
Semaphore (semaphore) is used to control the number of threads that access specific resources at the same time. It coordinates each thread to ensure the reasonable use of common resources.
For example, the traffic flow on the ×× road is limited. Only one hundred vehicles are allowed to drive on this road at the same time, and the others must wait at the intersection. Therefore, the first hundred vehicles will see the green light and can drive into this road. The car will see a red light and cannot enter the XX road, but if 5 of the first 100 vehicles have left the XX road, then 5 cars will be allowed to enter the road behind, the car mentioned in this example It is a thread. When you enter the road, it means that the thread is executing. When you leave the road, it means that the thread execution is completed. When you see a red light, it means that the thread is blocked and cannot be executed.

Semaphore can be used for flow control, especially in application scenarios with limited public resources, such as database connections. If there is a requirement to read the data of tens of thousands of files, because they are all IO-intensive tasks, we can start dozens of threads to read concurrently, but if they are read into the memory, they need to be stored in the database, and The number of database connections is only 10. At this time, we must control only 10 threads to obtain the database connection and save data at the same time, otherwise an error will be reported and the database connection cannot be obtained. At this time, you can use Semaphore for flow control, as shown in the code list.

public class SemaphoreTest {
    
    
	private static final int THREAD_COUNT = 30;
	private static ExecutorServicethreadPool = Executors.newFixedThreadPool(THREAD_COUNT);
	private static Semaphore s = new Semaphore(10);
	public static void main(String[] args) {
    
    
		for (inti = 0; i< THREAD_COUNT; i++) {
    
    
			threadPool.execute(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				try {
    
    
					s.acquire();
					System.out.println("save data");
					s.release();
			} catch (InterruptedException e) {
    
    
			}
			}
		});
	}
	threadPool.shutdown();
	}
}

Although there are 30 threads executing, only 10 are allowed to execute concurrently. Semaphore's constructor Semaphore(int permits) accepts an integer number indicating the number of licenses available. Semaphore (10) means that 10 threads are allowed to obtain a license, that is, the maximum number of concurrency is 10. The use of Semaphore is also very simple. First, the thread uses the acquire() method of Semaphore to obtain a license, and then calls the release() method to return the license after use. You can also try to acquire a license with the tryAcquire() method.

Exchanger for exchanging data between threads
Exchanger (exchanger) is a tool class for collaboration between threads. Exchanger is used for data exchange between threads. It provides a synchronization point where two threads can exchange data with each other. These two threads exchange data through the exchange method. If the first thread executes the exchange() method first, it will wait for the second thread to also execute the exchange method. When both threads reach the synchronization point, the two threads will be synchronized. You can exchange data and pass the data produced by this thread to the other party.

Exchanger can be used in genetic algorithm. In genetic algorithm, two people need to be selected as mating objects. At this time, the data of the two people will be exchanged, and two mating results will be obtained by using the crossover rule. Exchanger can also be used for proofreading. For example, we need to manually enter paper-based bank statements into electronic bank statements. In order to avoid mistakes, two people from AB and AB are used to enter. After entering into Excel, the system needs to load the two Excel , and proofread the two Excel data to see if they are entered consistently. The code is shown in the code list.

public class ExchangerTest {
    
    
	private static final Exchanger<String> exgr = new Exchanger<String>();
	private static ExecutorServicethreadPool = Executors.newFixedThreadPool(2);
	public static void main(String[] args) {
    
    
		threadPool.execute(new Runnable() {
    
    
		@Override
		public void run() {
    
    
			try {
    
    
				String A = "银行流水A"; // A录入银行流水数据
				exgr.exchange(A);
			} catch (InterruptedException e) {
    
    
		}
	}
});
	threadPool.execute(new Runnable() {
    
    
	@Override
	public void run() {
    
    
		try {
    
    
			String B = "银行流水B"; // B录入银行流水数据
			String A = exgr.exchange("B");
			System.out.println("A和B数据是否一致:" + A.equals(B) + ",A录入的是:"
			+ A + ",B录入是:" + B);
		} catch (InterruptedException e) {
    
    
		}
	}
	});
	threadPool.shutdown();
	}
}

If one of the two threads does not execute the exchange() method, it will wait forever. If you are worried about special circumstances, you can use exchange (V x, longtimeout, TimeUnit unit) to set the maximum waiting time.

Guess you like

Origin blog.csdn.net/jifashihan/article/details/129851108