In-depth analysis: Java multithreading, thread safety, and concurrent packages

Posted May 27, 20207 min read

1:synchronized(to ensure atomicity and visibility)

  1. Synchronous lock. When multiple threads access at the same time, only one thread can access the code block or method modified by synchronized at the same time. The objects it modifies are as follows:

Decorate a code block, the modified code block is called a synchronous statement block, and its scope of action is the code enclosed in braces {}, and the object of the function is the object that calls this code block
Modify a method, the modified method is called a synchronous method, its scope of action is the entire method, and the object of the function is the object that calls this method
Modify a static method, the scope of its effect is the entire static method, the object of effect is all objects of this class
Decorate a class, its scope of action is the part enclosed in parentheses after synchronized, and the objects of the role are all objects of this class

2:volatile modified variables(only guarantees visibility, does not guarantee atomicity, and does not block threads, high execution efficiency)

The thread reads the latest modified value of the variable each time it is used. Therefore, only when the state is truly independent of other content in the program, variables can be modified with volatile.
The happen-before principle in the java memory model, when two threads modify and access volatile variables at the same time, write operations have a higher priority than read operations.
Implementation principle:by adding a memory barrier and prohibiting instruction reordering. A load barrier instruction is added before reading a volatile variable, and a store instruction is added after writing.

3:Ideas to ensure thread safety

Lock control of non-secure code
Use thread-safe classes
In the case of multi-thread concurrency, the variables shared by the threads are changed to method-level local variables

4:Comparison of synchronized and lock

Synchronization is implemented at the JVM level. You can monitor synchronized locks through some monitoring tools, and the JVM will automatically release the locks when an exception occurs during code execution.
Lock is implemented through code, and you must call unLock() in finally {} to release the lock.
Synchronized is read-write, read-read, write-write operations are all mutually exclusive(performance is not high ...), the use of lock can improve performance, such as:use a read lock to lock the read code block, write lock to lock the written code block.
The locks acquired by synchronized are all objects, not a piece of code or function as a lock.
There is only one lock associated with each object.
Avoid unnecessary synchronization control(to achieve synchronization requires a lot of system overhead and improper use will cause deadlock).

5:Deadlock

In the process of execution of two or more processes, a phenomenon of waiting for each other due to contention for resources. Without external force, they will not be able to continue.

Reasons for deadlock:

Because of insufficient system resources
The order in which the process runs is not appropriate
Improper resource allocation
Four necessary conditions for deadlock:

Mutually exclusive condition:The so-called mutual exclusion is that the process monopolizes resources within a certain period of time.

Request and hold conditions:When a process is blocked by requesting resources, it keeps the resources it has acquired.
Non-deprivation condition:The process has acquired resources and cannot be forcibly deprived before it is used up.
Cyclic waiting condition:A process of cyclically waiting for resources is formed between several processes.
Break one or more of the four necessary conditions for deadlock to ensure that the system will not enter a deadlock state.

Break the mutually exclusive condition. That is, processes are allowed to access certain resources at the same time. However, some resources are not allowed to be accessed at the same time, such as printers, etc., which is determined by the properties of the resources themselves. Therefore, this approach has no practical value.

Break the preemptive conditions. That is, the process is allowed to forcefully grab some resources from the occupant. That is to say, when a process has occupied certain resources and it has applied for new resources, but it cannot be satisfied immediately, it must release all the resources it holds and apply again later. The resources it releases can be allocated to other processes. This is equivalent to the resources occupied by the process being stealthily occupied. This method of preventing deadlock is difficult to implement and will reduce system performance.
Break the possession and application conditions. The resource pre-allocation strategy can be implemented. That is, the process requests all the resources it needs from the system at once before running. If all the resources required by a process are not satisfied, no resources will be allocated and the process will not run for now. Only when the system can meet all the resource requirements of the current process, all the requested resources are allocated to the process at once. Since the running process has occupied all the resources it needs, the phenomenon of occupying resources and applying for resources will not occur, so no deadlock will occur.
Break the circular waiting conditions and implement an orderly resource allocation strategy. Using this strategy, the resources are classified and numbered in advance, and allocated according to the number, so that the process will not form a loop when applying for and occupying resources. All process requests for resources must be made strictly in the order of increasing resource serial numbers. The process can only apply for large-scale resources if it occupies a small-sized resource, so there is no loop, which prevents deadlock.

6:Thread class & pool

Thread.sleep() is a static class method of Thread. Whoever calls it and goes to sleep, even if the sleep method of b is called in thread a, actually a goes to sleep. To make b thread sleep, you need to call sleep in the code of b .
The role of Thread.sleep(0) is to "trigger the operating system to re-complete CPU competition immediately".
The Thread.start() method just keeps the thread in a ready state. The thread will only execute the thread after it has grabbed the CPU time slice, that is, the code in the run() method is executed at this time.
The two threads are each other's parameters. You can create an object of the other thread and start() in a thread's constructor, and then just start one thread to start two threads.
Create thread method:

Inherit the Thread class and rewrite the run method
Implement the Runnable interface, rewrite the run method, and implement the instance object of the implementation class of the Runnable interface as the target of the Thread constructor
Create threads via Callable and FutureTask
Create a thread through the thread pool
Description:

The first two have no return value. By rewriting the run method, the return value of the run method is void. To access the current thread, use this directly to get the current thread.
The latter two have return values, and the call method must be implemented through the Callable interface. The return value is Object. To access the current thread, you must use the Thread.currentThread() method.
The methods of the Callable and Runable interfaces are basically the same, except that the methods defined by the Callable interface can have a return value, and can declare that an exception is thrown. Future is an interface for canceling the execution result of a specific Runnable or Callable task, whether the query is completed, and obtaining the result. If necessary, you can get the execution result through the get method, which will block until the task returns the result.
The Future interface is defined as follows:

public interface Future {
    boolean cancel(boolean mayInterruptIfRunning); //Interrupt task
    boolean isCancelled(); boolean isDone(); //Determine whether the task is completed
    V get() throws InterruptedException, ExecutionException; //Get the task execution result
    V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}

The cancel method is used to cancel the task. If the cancellation succeeds, it returns true. If the cancellation fails, it returns false. The parameter mayInterruptIfRunning indicates whether it is allowed to cancel the task that is being executed but not completed. If set to true, it means that the task in the process of being executed can be canceled.
If the task has been completed, no matter whether mayInterruptIfRunning is true or false, this method will definitely return false, that is, if the canceled task has been canceled, it will return false;
If the task has not been executed, then whether mayInterruptIfRunning is true or false, it will definitely return true
If the task is being executed, if mayInterruptIfRunning is set to true, then return true, if mayInterruptIfRunning is set to false, then return false.
The isCancelled method indicates whether the task was successfully cancelled, and returns true if the task was successfully cancelled before the task was completed normally.
The isDone method indicates whether the task has been completed, if the task is completed, it returns true;
The get() method is used to obtain the execution result. This method will cause blocking and will wait until the task is completed before returning;
get(long timeout, TimeUnit unit) is used to obtain the execution result. If the result is not obtained within the specified time, it returns null directly.

Synchronous & asynchronous, blocking & non-blocking

The focus of synchronous and asynchronous is whether the execution of a task will cause the entire process to temporarily wait during the execution of multiple tasks;
The main point of blocking and non-blocking is whether to issue a request operation, if the conditions for performing the operation are not met, will return a flag message to inform the condition is not met.

CountDownLatch

It is a synchronization tool class that allows one or more threads to wait until the operation of other threads is completed before executing.
CountDownLatch is implemented by a counter, the initial value of the counter is the number of threads. Every time a thread completes its task, the counter value will decrease by one. When the counter value reaches 0, it means that all threads have completed the task, and then the thread waiting on the lock can resume the execution of the task.
Usage:To achieve maximum parallelism, wait for n threads to complete their tasks before starting execution, and deadlock detection.
CountDownLatch A group of threads waits for another group of threads to finish execution before continuing. CyclicBarrier synchronizes a group of threads at a point in time. It can start all tasks or part of tasks together, and it can be recycled.
Semaphore only allows a certain number of threads to execute a task at the same time.
Scenario:In a main thread, a large number(many many) of sub-threads are required to be executed before the main thread is completed. Multiple ways to consider efficiency.

  1. Customize an ImportThread class inherited from java.lang.Thread, overload run() method:use a List property to save all generated threads. In this way, as long as the List is empty, we know whether there are any child threads that have not been executed.
    Problem:If thread 1 starts and ends, and other threads have not yet started, the size of runningThreads is also 0 at this time. The main thread will think that all threads have finished execution.
    Method:Use a non-simple type counter to replace the List type runningThreads, and the value of the counter should be set before the thread is created.
  2. Use CountDownLatch instead of MyCountDown and await() method instead of while(true) { }