Java interview must-test points--Lecture 04: Concurrency and multi-threading

The main content of this class is multi-threading and concurrency in Java. Key knowledge includes thread state transition, thread synchronization and mutual exclusion, detailed explanation of the operating mechanism of the thread pool, and commonly used tools in JUC.

Multithreading knowledge points

When multi-threads collaborate, deadlocks will occur due to locking and waiting for resources. Here you need to understand the four basic conditions for deadlocks, understand the concepts of competition conditions and critical sections, and know the four conditions that can cause deadlocks through destruction. to prevent deadlock.

We have talked about the communication methods between processes before. Here we also need to know the communication methods between threads. Communication mainly refers to the cooperation mechanism between threads, such as wait, notify, etc.

You also need to know some of the mechanisms provided by Java for multi-threading, such as ThreadLocal for saving thread-exclusive data, Fork/Join mechanism for dividing and summarizing large tasks, Volatile guarantees multi-thread data visibility, and thread interruption. mechanism.

Others include: ThreadLocal’s implementation mechanism. Fork/Join work-stealing algorithms and more.

Detailed explanation of thread state transition

Threads are the smallest unit for JVM to perform tasks. Understanding the state transitions of threads is the basis for understanding subsequent multi-threading issues. When the JVM is running, the thread has a total of six states: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, and TERMINATED. These states correspond to the states in the Thread.State enumeration class.

As shown in the figure below, when a thread is created, the thread is in the NEW state. After running the start method of Thread, the thread enters the RUNNABLE state.

At this time, all threads in the runnable state cannot run immediately, but need to enter the ready state first and wait for thread scheduling, as shown in the READY state in the middle of the figure. Only after obtaining the CPU can it enter the running state, as shown in the figure RUNNING. The running state can be converted to other states except NEW according to different conditions.

As shown on the left side of the figure, when a thread in the running state enters a synchronized synchronized block or synchronized method, if it fails to acquire the lock, it will enter the BLOCKED state. When the lock is acquired, it will be restored from the BLOCKED state to the ready state.

As shown on the right side of the figure, the running thread will also enter the waiting state. One of the two waits is a wait with a timeout, such as calling Object.wait, Thread.join, etc.; the other is a wait without a timeout, such as calling Thread.join or Locksupport.park etc. Both types of waiting can end the waiting state and return to the ready state through notify or unpark.

Finally, when the thread operation is completed, as shown on the lower side of the figure, the thread status changes to TERMINATED.

Detailed explanation of CAS and ABA issues

This part explains in detail thread synchronization and mutual exclusion. The main ways to solve thread synchronization and mutual exclusion are CAS, synchronized and lock.

CAS

CAS is an implementation method of optimistic locking and a lightweight lock. The implementation of many tool classes in JUC is based on CAS. The flow of CAS operation is shown in the figure below. The thread does not lock when reading data. When preparing to write back the data, it compares whether the original value has been modified. If it has not been modified by other threads, it is written back. If it has been modified, it is written again. Execute the reading process. This is an optimistic strategy that assumes that concurrent operations will not always occur.

The comparison and write-back operation is implemented through operating system primitives to ensure that the execution process will not be interrupted.

ABA

CAS is prone to ABA problems. As shown in the timing diagram below, if thread T1 reads value A, two writes occur. First, thread T2 writes back B, and then T3 writes back A. At this time, T1 is in When writing back for comparison, the value is still A, so it is impossible to determine whether it has been modified.

ABA problems may not necessarily affect the results, but they still need to be guarded against. The solution can be to add additional flags or timestamps. Such a class is provided in the JUC toolkit.

Detailed explanation of synchronized

synchronized is one of the most commonly used thread synchronization methods. How does it ensure that only one thread can enter the critical section at the same time?

synchronized locks the object. In the JVM, the object is divided into three areas in memory: object header, instance data and alignment filling. The lock flag and the starting address pointing to the monitor object are stored in the object header, as shown in the figure below. The right side is the Monitor object corresponding to the object. When the Monitor is held by a thread, it will be in a locked state. The Owner part in the figure will point to the thread holding the Monitor object. In addition, there are two queues in the Monitor, which are used to store threads entering and waiting to acquire locks.

When synchronized is applied to a method, it is implemented in the bytecode through the ACC_SYNCHRONIZED flag of the method. When synchronized is applied to a synchronized block, it is implemented in the bytecode through monitorenter and monitorexit.

For the synchronized lock acquisition method, the JVM uses an optimization method of lock upgrade, which is to first use a biased lock to prioritize the same thread and then acquire the lock again. If it fails, it will be upgraded to a CAS lightweight lock. If it fails, it will spin briefly to prevent The thread is suspended by the system. Finally, if all the above fails, upgrade to a heavyweight lock.

Detailed explanation of AQS and Lock

Before introducing Lock, let's first introduce AQS, which is the queue synchronizer, which is the basis for implementing Lock. The following figure is the structure diagram of AQS. As can be seen from the figure, AQS has a state flag bit. When the value is 1, it means that there is a thread occupying it, and other threads need to enter the synchronization queue to wait. The synchronization queue is a doubly linked list.

When the thread that acquires the lock needs to wait for a certain condition, it will enter the waiting queue of condition. There can be multiple waiting queues. When the condition condition is met, the thread will re-enter the synchronization queue from the waiting queue to compete for the lock. ReentrantLock is implemented based on AQS. As shown in the figure below, ReentrantLock has two internal implementations: fair lock and unfair lock. The difference lies in whether the new thread obtains the lock earlier than the waiting thread already in the synchronization queue.

Similar to the implementation of ReentrantLock, Semaphore is also based on AQS. The difference is that ReentrantLock is an exclusive lock and Semaphore is a shared lock.

Detailed explanation of thread pool

Thread pools avoid frequent creation and destruction of threads by reusing threads. Java's Executors tool class provides 5 types of thread pool creation methods, as shown in the figure below to see their characteristics and applicable scenarios.

  1. The fixed-size thread pool is characterized by a fixed number of threads and the use of unbounded queues. It is suitable for scenarios with uneven number of tasks and scenarios that are not sensitive to memory pressure but are sensitive to system load;

  2. Cached thread pool features no limit on the number of threads and is suitable for short-term task scenarios that require low latency;

  3. Single-threaded thread pool is a fixed thread pool of one thread, suitable for scenarios that require asynchronous execution but need to ensure the order of tasks;

  4. Scheduled thread pool is suitable for regular task execution scenarios and supports two methods: regular execution at a fixed frequency and regular execution at a fixed delay;

  5. The work-stealing thread pool uses ForkJoinPool, which is a multi-task queue with fixed parallelism and is suitable for scenarios where task execution times are uneven.

Detailed explanation of thread pool parameters

In addition to the work-stealing thread pool, thread pools are created through different initialization parameters of ThreadPoolExecutor.

Create a parameter list as shown below.

  • The first parameter sets the number of core threads. By default the core thread will always survive.

  • The second parameter sets the maximum number of threads. Determines the maximum number of threads that the thread pool can create.

  • The third and fourth parameters are used to set the idle time of the thread and the unit of idle time. When the thread is idle for more than the idle time, it will be destroyed. The core thread can be allowed to be recycled through the allowCoreThreadTimeOut method.

  • The fifth parameter sets the buffer queue. The three queues on the lower left in the above figure are buffer queues commonly used when setting up thread pools. Among them, ArrayBlockingQueue is a bounded queue, which means that the queue has a maximum capacity limit. LinkedBlockingQueue is an unbounded queue, that is, the queue does not limit its capacity. The last one is SynchronousQueue, which is a synchronous queue with no internal buffer.

  • The sixth parameter sets the thread pool factory method. The thread factory is used to create new threads and can be used to customize some attributes of the thread, such as the thread group, thread name, priority, etc. Generally, the default factory class can be used.

  • The seventh parameter sets the rejection policy when the thread pool is full. As shown in the lower right corner of the above figure, there are four strategies. The Abort strategy will throw a RejectedExecutionException when submitting a new task after the thread pool is full. This is also the default rejection strategy. The Discard strategy will directly discard the task when the submission fails. The CallerRuns strategy will directly execute the submitted task by the thread that submitted the task when the submission fails. The DiscardOldest policy discards the oldest submitted tasks.

Let's take a look at what parameters are used to create the previous thread pools.

  • When a fixed-size thread pool is created, both the core and the maximum number of threads are set to the specified number of threads, so that only a fixed-size number of threads will be used in the thread pool.

  • The queue uses an unbounded queue LinkedBlockingQueue.

  • A Single thread pool is a fixed thread pool with the number of threads set to 1.

  • The number of core threads in the Cached thread pool is set to 0, and the maximum number of threads is Integer.MAX_VALUE. This is mainly done by setting the buffer queue to SynchronousQueue, so that as long as there are no idle threads, new threads will be created.

  • The difference between the Scheduled thread pool and the previous ones is that it uses DelayedWorkQueue, which is a priority queue that obtains tasks according to the delay time.

Detailed explanation of thread pool execution process

You can use execute and submit when submitting tasks to a thread. The difference is that submit can return a future object. Through the future object, you can understand the task execution, cancel the execution of the task, and obtain execution results or execution exceptions. Submit is ultimately executed through execute.

The execution sequence when submitting tasks to the thread pool is shown in the figure below.

  1. When submitting a task to the thread pool, it will first determine whether the number of threads in the thread pool is greater than the set number of core threads. If not, a core thread will be created to execute the task.

  2. If it is greater than the number of core threads, it will be judged whether the buffer queue is full. If not, it will be put into the queue and wait for the thread to be idle to execute the task.

  3. If the queue is full, determine whether the maximum number of threads set by the thread pool has been reached. If not, create a new thread to perform the task.

  4. If the maximum number of threads has been reached, the specified deny policy is executed.

    这里需要注意队列的判断与最大线程数判断的顺序,不要搞反。
    
Detailed explanation of JUC tool class

JUC is a tool class library provided by Java for multi-thread processing. Let's look at the functions of common tool classes, as shown in the figure below.

As shown in the figure above, the classes in the first row are all atomic classes of basic data types, including AtomicBoolean, AtomicLong, and AtomicInteger classes.

  • AtomicLong is implemented through the unsafe class and is based on CAS. The unsafe class is a low-level tool class. Many classes in JUC use the functions in the unsafe package at the bottom level. The unsafe class provides C-like pointer operations and provides CAS and other functions. All methods in the unsafe class are native modified.

  • Four classes including LongAdder are more efficient operation classes provided in JDK1.8. LongAdder is implemented based on Cell and uses the idea of ​​segmented locks. It is a space-for-time strategy and is more suitable for high-concurrency scenarios. LongAccumulator provides more powerful functions than LongAdder and can specify operation rules for data. For example, it can The addition operation is changed to a multiplication operation.

The class in the second line provides atomic read and write functions for objects. The last two classes, AtomicStampedReference and AtomicMarkableReference, are used to solve the ABA problem mentioned earlier, based on timestamps and mark bits respectively.

Look at the picture below again.

The classes in the first line are mainly lock-related classes, such as the Reentrant reentrant lock introduced earlier.

  • Unlike ReentrantLock's exclusive lock, Semaphore is a shared lock that allows multiple threads to share resources. It is suitable for scenarios that limit the number of threads using shared resources. For example, if 100 vehicles use 20 parking spaces, then up to 20 vehicles are allowed to occupy the parking spaces. .

  • StampedLock is an improved read-write lock in JDK 1.8. It uses a CLH optimistic lock, which can effectively prevent write starvation. The so-called write starvation means that when multi-threads read and write, the reading thread accesses very frequently, causing the reading thread to always occupy resources, and it is difficult for the writing thread to add a write lock.

     第二行中主要是异步执行相关的类。
    
  • Focus on understanding the CompletableFuture provided in JDK 1.8, which can support streaming calls and can be easily used in combination with multiple futures. For example, two asynchronous tasks can be executed at the same time, and then the execution results can be merged and processed. You can also easily set the completion time.

  • The other one is ForkJoinPool provided in JDK 1.7, which uses the divide-and-conquer idea to decompose a large task into multiple small tasks for processing, and then merge the processing results. ForkJoinPool is characterized by the use of work-stealing algorithms, which can effectively balance multi-tasking scenarios with varying lengths of time.

Other commonly used JUC tools are shown in the figure below.

The first line is the commonly used blocking queue, which has been briefly introduced when explaining the thread pool, and I will add some more here.

  • LinkedBlockingDeque is a double-ended queue, that is, you can enter and leave the queue from the head and tail of the queue respectively.
  • ArrayBlockingQueue is a single-ended queue, which can only be entered from the end of the queue and dequeued from the head.

The second line is the class used to control multi-thread collaboration.

  • CountDownLatch implements the counter function and can be used to control waiting for multiple threads to perform tasks before summarizing.

  • CyclicBarrier allows a group of threads to wait for a certain state before executing them all at the same time. It is generally used during testing to allow better concurrent execution of multiple threads.

  • Semaphore is used to control the concurrency of access to shared resources.

The last line is two commonly used collection classes, ConcurrentHashMap, which we have introduced in detail in previous courses. Here you can learn about CopyOnWriteArrayList. COW eliminates the problems in parallel reading and writing by copying modifications when writing data and then updating references. The use of locks is more suitable for scenarios where there is more reading and less writing, the amount of data is relatively small, but the concurrency is very high.

Inspection points and bonus points
Inspection point

After explaining the knowledge points in this lesson, summarize the interview points.

  1. You must understand the principles of thread synchronization and mutual exclusion, including the concepts of critical resources and critical sections, and know the concepts of heavyweight locks, lightweight locks, spin locks, biased locks, reentrant locks, and read-write locks.

  2. To master thread safety-related mechanisms, such as the implementation principles of the three synchronization methods of CAS, synchronized, and Lock, you must understand that ThreadLocal is a local variable exclusive to each thread, and understand that ThreadLocal uses weak reference ThreadLocalMap to save different ThreadLocal variables.

  3. It is necessary to understand the usage scenarios of tool classes in JUC and the implementation principles of several main tool classes, such as Reentrantlock, ConcurrentHashMap, LongAdder and other implementation methods.

  4. You must be familiar with the principles, usage scenarios, and common configurations of thread pools. For example, Cached thread pools are suitable for scenarios with a large number of short-term tasks; when system resources are tight, you can choose fixed thread pools. Also, be careful when using unbounded queues, as there may be a risk of OOM.

  5. It is necessary to have a deep understanding of thread synchronization and asynchronous, blocking and non-blocking. The difference between synchronization and asynchronous lies in whether the task is executed by the same thread. The difference between blocking and non-blocking lies in whether the thread will block waiting for the result or continue when executing the task asynchronously. Execute subsequent logic.

bonus

After mastering the above content, if you can achieve these bonus points, you will definitely leave a better impression on the interviewer.

  1. You can introduce the principles based on actual project experience or actual cases. For example, when introducing thread pool settings, you can mention that there is a scenario in your project that requires high throughput and uses Cached's thread pool.

  2. If you have experience in solving multi-threading problems or have troubleshooting ideas, you will get bonus points in the interview.

  3. Be familiar with commonly used thread analysis tools and methods, such as using jstack to analyze the running status of threads and find lock object holding status, etc.

  4. Learn about the enhancements Java 8 has made to the JUC tool class. For example, LongAdder is provided to replace AtomicLong, which is more suitable for scenarios with high concurrency.

  5. Understand the idea of ​​Reactive asynchronous programming, and understand the concept and application scenarios of back pressure.

Summary of real questions

Summarize the relevant real interview questions, as shown in the figure below, and provide some ideas for key questions.

  • Question 1: How to implement a producer and consumer model? You can try to implement it through different methods such as locks, semaphores, thread communication, blocking queues, etc.

  • Question 4 What is the difference between wait and sleep? Four key points to answer:

    • wait belongs to the Object class, and sleep belongs to the Thread class;
    • wait will release the lock object, but sleep will not;
    • The locations used are different, wait needs to be used in a synchronized block, sleep can be used anywhere;
    • sleep needs to catch exceptions, but wait does not.
  • Question 6, what scenarios are read-write locks suitable for? It can be answered that read-write locks are suitable for scenarios with high read concurrency and low write concurrency. Another way to solve this scenario is copyonwrite.

The second part of the real test questions is as follows, providing ideas for solving the questions.

  • Question 7, how to communicate between threads? We can mainly introduce the wait/notify mechanism, synchronized or Lock synchronization mechanism of shared variables, etc.

  • Question 8, what are the methods to ensure thread safety? Mechanisms such as CAS, synchronized, Lock, and ThreadLocal can be mentioned.

  • Question 9: How to improve multi-thread concurrency performance as much as possible? The answer can be from the aspects of minimizing the scope of the critical section, using ThreadLocal, reducing thread switching, using read-write locks or copyonwrite and other mechanisms.

  • Question 10, what problem is ThreadLocal used to solve? How is ThreadLocal implemented? You can focus on answering that ThreadLocal is not used to solve the problem of multi-thread shared variables, but to solve the problem of thread data isolation.

The content of this lesson ends here. The next lesson will explain the data structure and algorithm in the basic knowledge module.

Guess you like

Origin blog.csdn.net/g_z_q_/article/details/129826243