Golden nine silver ten is coming, sorted out 50 multi-threaded concurrent interview questions, everyone can like, collect, slowly taste! ~

The reason for choosing multithreading is because it is fast. For example:

If you want to move 1,000 bricks to the roof, assuming that there are several elevators to the roof, do you think it is faster to use one elevator to move, or to use several elevators at the same time to move fast? This elevator can be understood as a thread.

Therefore, we use multithreading because: in the right scenario, the right number of threads can be set to increase the running rate of the sequence. More professionally, it is to make full use of CPU and I/O utilization to improve the program running rate.

Of course, there are advantages and disadvantages, in multi-threading scenarios, we need to consider adding locks if we want to ensure thread safety. If the lock is not appropriate, it is very performance-intensive.

There are several main ways to create threads in Java:

If the thread you want to execute has a return, you can use Callable.

In daily development, we generally use the thread pool to perform asynchronous tasks.

In fact, the main differences between start and run are as follows:

You can combine the code example to see the ha~

For example:

You open QQ and open a process; Thunderbolt was turned on, and a process was also opened.

In this process of QQ, the transmission of text opens a thread, the transmission of speech opens a thread, and the pop-up dialog box opens another thread.

So running a certain software is equivalent to opening a process. In the process of running this software (in this process), multiple work supports the completion of QQ running, then this “multiple work” has a thread.

So a process manages multiple threads.

In layman’s terms: “The process is the father and mother, who manages many threads of the son”…

You can take a look at the APIs of the two of them:

In order to facilitate everyone’s understanding, I wrote a demo, and my friends can look at Ha:

The volatile keyword is the most lightweight synchronization mechanism offered by the Java Virtual Machine. It acts as a modifier to decorate variables. It guarantees the visibility of variables to all threads and prohibits instruction rearrangement, but does not guarantee atomicity.

Let’s first recall the Java memory model (jmm):

Volatile variables guarantee that new values can be immediately synchronized back to main memory, and immediately refreshed from main memory before each use, so we say that volatile guarantees the visibility of multi-threaded operation variables.

Volatile guarantees visibility and prohibits instruction reflow, both related to memory barriers. Let’s look at a piece of demo code used by volatile:

After compilation, comparing the assembly code generated when there is a volatile keyword and no volatile keyword, when the volatile keyword is found, there will be an extra lock addl $0x0, (%esp), that is, an extra lock prefix instruction, which is equivalent to a memory barrier

The lock directive acts as a memory barrier that guarantees the following:

Points 2 and 3 are the embodiment of ensuring the visibility of volatile, and point 1 is the embodiment of the prohibition of directive rearrangement.

Four categories of memory barriers: (Load represents read instructions, Store represents write instructions)

Some small partners, may still be a little confused about this, memory barrier this thing is too abstract. Let’s look at the code:

The memory barrier ensures that the previous instructions are executed first, so this ensures that the instruction reflow is prohibited, and the memory barrier guarantees that the cache is written to memory and other processor caches are invalidated, which also ensures visibility, haha ~ There is an underlying implementation of volatile, we will discuss this ha ~

Concurrency and parallelism were originally concepts in the operating system, representing the way the CPU performs multiple tasks.

(That is, if A and B execute sequentially, A must complete before B, while concurrent execution does not.) )

(That is, at any point in time, serial execution must have only one task executing, and parallel execution is not necessarily.) )

I know there is a very interesting answer, you can look at the following:

The key to concurrency is that you have the ability to handle multiple tasks, not necessarily at the same time. The key to parallelism is your ability to handle multiple tasks at the same time. So I think the most critical point about them is: whether they are simultaneous.

Source: Zhihu

Synchronized is a keyword in Java and is a type of synclock. The synchronized keyword can act on methods or blocks of code.

General interview. This can be answered like this:

If synchronized acts on a block of code, decompilation can see two instructions: monitorenter and monitorexit, and the JVM uses monitorenter and monitorexit instructions to achieve synchronization; If the effect synchronized acts on the method, decompilation can see the ACCSYNCHRONIZED markup, and the JVM implements the synchronization function by adding ACCSYNCHRONIZED to the method access identifier (flags).

What is monitor? The operating system’s monitors are conceptual principles, and ObjectMonitor is its implementation.

In the Java Virtual Machine (HotSpot), Monitor is implemented by ObjectMonitor with the following main data structures:

The meaning of several key fields in ObjectMonitor is shown in the figure:

Mark Word is used to store the object’s own run-time data, such as the hashCode, GC generational age, lock state flags, locks held by threads, bias thread IDs, bias timestamps, and so on.

A heavyweight lock that points to a mutex. In fact, synchronized is a heavyweight lock, that is, the object lock of Synchronized, the Mark Word lock ID bit is 10, where the pointer points to the start address of the Monitor object.

Threads have six states: New, Runnable, Blocked, Waiting, Timed_Waiting, and Terminated.

The conversion diagram is as follows:

Let’s take a look at a code demo:

suspend() is not recommended because after the suspend() method is called, the thread does not release the resources (such as locks) that have been occupied, but occupies the resources and goes to sleep, which can easily cause deadlock problems.

CAS, full name is Compare and Swap, which translates to compare and exchange;

CAS involves 3 operands, memory address value V, expected original value A, new value B; If the value V of the memory location matches the expected original A value, it is updated to the new value B, otherwise it is not updated

What are the defects of CAS?

In the concurrency environment, assuming that the initial condition is A, when the data is modified, the modification is performed when it is found to be A. However, although it is A that is seen, there may be a situation in the middle where A changes to B, and B changes back to A. At this point, A is no longer A, and even if the data is successfully modified, there may be problems.

The ABA problem can be solved with AtomicStampedReference, a marked atomic reference class that guarantees the correctness of the CAS by controlling the version of the variable value.

Spin CAS, if it has been cyclically executed and has not been successful, will bring very large execution overhead to the CPU. Many times, CAS thought is embodied in having a spin number of times, just to avoid this time-consuming problem

The CAS guarantees the atomicity of the operation on one variable, and if the operation is performed on multiple variables, the CAS cannot currently directly guarantee the atomicity of the operation. This problem can be solved in two ways: 1. Use mutexes to guarantee atomicity; 2. Encapsulate multiple variables into objects, guaranteed atomicity through AtomicReference.

Interested friends can take a look at my previous practical article Ha ~ CAS optimistic lock to solve the concurrency problem of a practice

Both CountDownLatech and CyclicBarrier are used to make threads wait and run when certain conditions are met. The main differences are:

Let me give you an example:

The CPU caches on a cache line, which affects each other’s performance when multiple threads modify variables that are in the same cache line. This is pseudo-sharing

Modern computer computing models:

It is precisely because of the existence of cache rows that the pseudo-sharing problem is caused, as shown in the figure:

Suppose data a and b are loaded into the same cache row.

Since pseudo-sharing is caused by the storage of independent variables to the same Cache line, the size of a cache line is 64 bytes. Then, we can use the space-for-time method, that is, the data filling method, to spread the independent variables into different Cache lines~

Let’s take a look at an example:

A long type is 8 bytes, we do not have 7 long type variables between variables a and b, what is the output result? As follows:

It can be found that by using the method of filling the data, the read and write variables are split into different cache lines, which can be very good and high performance~

The Fork/Join framework is a framework provided by Java 7 for parallel execution of tasks, which is a framework that divides large tasks into several small tasks, and finally summarizes the results of each small task to obtain the results of large tasks.

The Fork/Join framework needs to understand two points, “divide and conquer” and “work theft algorithms”.

divide and conquer

The above definition of the Fork/Join framework is the embodiment of the idea of divide and rule

Job theft algorithms

When a large task is split into small tasks, put into different queues for execution, and executed separately by different threads. Some threads give priority to the task they are responsible for, and other threads are still slowly processing their own tasks, at this time, in order to fully improve efficiency, it is necessary to work to steal the algorithm

A work theft algorithm is “the process by which a thread steals a task from another queue for execution.” Generally refers to the task of the fast thread (theft thread) to grab the slow thread, and in order to reduce lock competition, the double-ended queue is usually used, that is, the fast thread and the slow thread are each on one end.

ThreadLocal’s memory structure diagram

In order to have a macro understanding of ThreadLocal, let’s first look at the memory structure diagram of ThreadLocal

From the memory structure diagram, we can see:

Key source analysis

Compared with the key source code, it is easier to understand a little ha~

First look at the source code of the Thread class, you can see that the initial value of the member variable ThreadLocalMap is null

The key source code for the member variable ThreadLocalMap is as follows:

Key set() methods in the ThreadLocal class:

The key get() method in the ThreadLocal class

So how to answer the implementation principle of ThreadLocal? As follows, it is best to combine the above structure diagram to illustrate the ha~

You can take a look at my previous article Ha: ThreadLocal eight key knowledge points

Let’s first take a look at TreadLocal’s reference diagram Ha:

Regarding the ThreadLocal memory leak, the more popular statement on the Internet is this:

ThreadLocalMap uses a weak reference to ThreadLocal as the key, and when the ThreadLocal variable is manually set to null, i.e. a ThreadLocal does not have an external strong reference to reference it, ThreadLocal will definitely be recycled when the system GC. In this way, there will be a Entry with a key of null in the ThreadLocalMap, there is no way to access the value of these Entry with a key of null, and if the current thread is delayed (such as the core thread of the thread pool), the value of these key null Entry will always have a strong reference chain: Thread variable -> Thread object -> ThreaLocalMap -> Entry -> value -> Object can never be recycled, resulting in a memory leak.

When the ThreadLocal variable is manually set to null after the reference chain graph:

In fact, this has been taken into account in the design of ThreadLocalMap. So some precautions have also been added: that is, in the ThreadLocal get, set, and remove methods, all values with keys in the threadLocalMap will be cleared.

In the source code, there is a way to show, such as the set method of ThreadLocalMap:

Such as ThreadLocal’s get method:

Some small partners may have doubts since the key of ThreadLocal is a weak reference. Will GC rush to recycle the key, which will affect the normal use of ThreadLocal?

In fact, no, because there is a ThreadLocal variable that references it, it will not be recycled by the GC, unless the ThreadLocal variable is manually set to null, we can run a demo to verify it:

The conclusion is that the little partner put down this doubt, haha~

To show you the next example of a memory leak, it is actually to use the thread pool and put objects into it all the time

The result of the run appeared OOM, tianLuoThreadLocal.remove(); When added, it will not OOM.

We did not manually set the tianLuoThreadLocal variable to null here, but there will still be a memory leak. Because we use thread pools, which have a long lifetime, the thread pool will always hold the value value of the tianLuoClass object, even if tianLuoClass = null is set; References still exist. It’s like putting an object object into a list and then setting the object to null individually, and the list of objects still exists.

So the memory leak happened like this, and finally the memory was limited, and the OOM was thrown. If we add threadLocal.remove(); , there will be no memory leaks. Why? Because threadLocal.remove(); The Entry will be cleared from the following source code:

By reading ThreadLocal’s source code, we can see that Entry’s Key is designed for weak references (ThreadLocalMap uses ThreadLocal’s weak references as Key). Why design for weak references?

Let’s start by recalling four references:

Let’s first take a look at the official documentation and why it is designed as a weak reference:

I’ll take the ThreadLocal reference diagram again:

Let’s discuss them in different situations:

Therefore, it can be found that using a weak reference as the key of Entry can provide an additional layer of protection: the weak reference ThreadLocal will not easily leak memory, and the corresponding value will be cleared the next time ThreadLocalMap calls set, get, and remove.

In fact, the root cause of our memory leak is that Entry, which is no longer in use, is not removed from the thread’s ThreadLocalMap. There are two ways to generally delete an entry that is no longer used:

We know that ThreadLocal is thread-isolated, so how do we do it if we want parent-child threads to share data? InheritableThreadLocal can be used. Let’s take a look at the demo first:

It can be found that in a child thread, it is possible to get the value of the InheritableThreadLocal type variable of the parent thread, but not to the value of the ThreadLocal variable.

We can understand that we can’t get a value of type ThreadLocal because it’s thread-isolated. How does InheritableThreadLocal do it? What is the principle?

In the Thread class, in addition to the member variable threadLocals, there is another member variable: inheritableThreadLocals. They are the same for both types:

In the init method of the Thread class, there is an initial set of settings:

It can be found that when parent’s inheritableThreadLocals is not null, parent’s inheritableThreadLocals will be assigned to the previous thread’s inheritableThreadLocals. To put it bluntly, if the inheritableThreadLocals of the current thread is not null, one is copied from the parent thread, similar to another ThreadLocal, but the data comes from the parent thread. Interested partners can go to research the source code ~

To give a simple example, here’s it:

Running result:

A deadlock is a kind of deadlock in which multiple threads compete for resources and wait for each other. Feel it as shown in the picture:

Four necessary conditions for deadlock:

How to prevent deadlocks?

Using multithreading can improve program performance. But if too many threads are used, it is counterproductive.

Too many threads can affect the program’s system.

Therefore, we usually try to use the thread pool to manage threads. You also need to set the appropriate number of threads.

In Java, there is a happens-before. It includes eight major rules, as follows:

LockSupport is a tool class. Its main role is to suspend and wake up threads. This utility class is the basis for creating locks and other synchronization classes. Its main method is

Take a look at an example of the code:

Because there is a dormant operation inside the thread thread for 2 seconds, the operation of the unpark method must precede the call of the park method. Why thread threads can still end up eventually is because park and unpark maintain a license (Boolean) for each thread

Our server CPU core is 8 cores, a task thread CPU takes 20ms, thread waiting (network IO, disk IO) takes 80ms, the optimal number of threads: (80 + 20 )/20 * 8 = 40. That is, set the optimal number of 40 threads.

Interested partners, you can also see this article Ha: how many threads are more appropriate to set the thread pool?

Thread pool: A pool of administrative threads. The thread pool can:

You can take a look at my previous article, very classic, interview must-have: Java thread pool analysis

The thread pool executes as follows:

To visualize thread pool execution, use an analogy:

Let’s first look at the constructor of ThreadPoolExecutor

Four deny policies

Several types of work block queues

Let’s start with a piece of code:

Obviously, there will be an exception in this code, let’s look at the execution result

Although there is no result output, no exception is thrown, so we can’t perceive that the task has an exception, so we need to add try/catch. As shown in the following figure:

OK, thread exception handling, we can directly try… Catch capture.

Recently wrote a thread pool pit related, you can go to see Ha: 10 pits of the thread pool

AQS, or AbstractQueuedSynchronizer, is the underlying framework for building locks or other synchronization components that uses an int member variable to represent the synchronization state and queue the resource acquisition thread through the built-in FIFO queue. You can answer the following key points ha:

CLH Synchronization Queue, all English Craig, Landin, and Hagersten locks. is a FIFO bidirectional queue that internally records the first and last elements of the queue through the node head and tail, and the type of queue element is Node. AQS relies on it to complete the management of the synchronization state, if the current thread fails to obtain the synchronization state, AQS will construct the information that the current thread has waited for the state into a node (Node) and add it to the CLH synchronization queue, and at the same time block the current thread, when the synchronization state is released, the first node will wake up (fair lock), so that it tries to obtain the synchronization state again.

We all know that when synchronized controls synchronization, it can be combined with Object’s wait(), notify(), and notifyAll() series methods to implement wait/notification mode. And Lock? It provides a conditional Condition interface, and can also implement a wait/notification mechanism with methods such as await(), signal(), signalAll() and so on. ConditionObject implements the Condition interface and provides support for conditional variables for AQS

The love-hate feud between the ConditionObject queue and the CLH queue:

Template method pattern: Define the skeleton of an algorithm in a method while delaying some steps into subclasses. Template methods enable subclasses to redefine certain steps in the algorithm without changing the structure of the algorithm.

The typical design pattern of AQS is the template method design pattern. A derivative implementation of AQS Family Locks (Semaphore) reflects this design pattern. For example, AQS provides template methods such as tryAcquire, tryAcquireShared, etc., to implement custom synchronizers for subclasses.

If you want to implement a custom lock, you first need to determine whether you want to implement an exclusive lock or a shared lock, define the meaning of the atomic variable state, and then define an internal class to inherit the AQS, and override the corresponding template method

Semaphore, we also call it semaphores. It can be used to control the number of threads accessing a particular resource at the same time, by coordinating the individual threads to ensure the rational use of resources.

We can simply understand it as the display screen standing at the entrance of our parking lot, whenever a car enters the parking lot display will show the remaining parking space minus 1, every time a car goes out of the parking lot, the remaining vehicles displayed on the display screen will add 1, when the remaining parking space on the display is 0, the railing of the parking lot entrance will no longer be opened, and the vehicle will not be able to enter the parking lot until a car goes out of the parking lot.

Let’s use the parking lot example to implement the demo.

Assuming the parking lot can park up to 20 cars, there are now 100 cars to enter the parking lot.

We can easily write the following code;

Let’s see how the implementation works.

It creates an unfairly locked sync blocking queue and assigns the initial number of tokens (20) to the state of the sync queue, which is the AQS hash.

2. Number of available tokens

This availablePermits, get the state value. It’s 20 at first, so it’s certainly not 0.

Let’s look at the API for getting tokens

Get 1 token

An attempt was made to obtain a token, using the CAS algorithm.

If you can get a token, create a node and join the blocking queue; Heavy bidirectional linked list head, tail node relationship, empty invalid nodes; Suspends the current node thread

Prior to JDK 1.6, synchronized implementations called ObjectMonitor’s enter and exit directly, locks known as heavyweight locks. Starting with JDK6, the HotSpot virtual machine development team optimized locks in Java, such as adding optimization strategies such as adaptive spin, lock elimination, lock roughening, lightweight locks, and bias locks, improving synchronized performance.

What is CPU Context?

CPU registers are small but extremely fast memory built into the CPU. Program counters, on the other hand, are used to store the position of the instruction that the CPU is executing, or the position of the next instruction that is about to be executed. They are all dependent environments that the CPU must have before running any task, so they are called CPU contexts.

What is CPU context switching?

It means first saving the CPU context of the previous task (that is, CPU registers and program counters), then loading the context of the new task to these registers and program counters, and finally jumping to the new location indicated by the program counter to run the new task.

In general, when we talk about context switching, it means that the kernel (the core of the operating system) switches processes or threads on the CPU. The transition of a process from user-state to kernel-state needs to be done through system calls. The system calls the process and the CPU context switch occurs.

So you sometimes hear this statement, the context of the thread switching. It refers to the allocation of CPU resources using time slice rotation, that is, each thread is assigned a time slice, and the thread occupies the CPU to perform tasks within the time slice. When a thread runs out of time slices, it is in a ready state and lets the CPU drain other threads, which is the context switching of threads. Looking at a diagram, it may be a little easier to understand

The lock is just a marker that exists inside the object header.

The following is analyzed from the perspective of object-oriented and observer patterns.

The underlying layer of AtomicInteger is based on CAS. Let’s look at how AtomicInteger is added. as follows

Note: compareAndSwapInt is a native method that operates variables of type int based on CAS. Moreover, other atomic operations are basically the same.

We know that there are two scheduling models: time-sharing scheduling and preemptive scheduling.

Java’s default thread scheduling algorithm is preemptive. That is, after the thread runs out of CPU, the operating system will calculate a total priority based on the thread priority, thread hunger and other data and allocate the next time slice to a thread to execute.

Several common thread pools:

Working with scenarios

FixedThreadPool is suitable for processing CPU-intensive tasks, ensuring that the CPU is used by worker threads for a long time, with as few threads as possible, that is, suitable for long-term tasks.

Working with scenarios

When a task is submitted faster than it is processed, a thread is inevitably created each time a task is submitted. In extreme cases, too many threads are created and CPU and memory resources are exhausted. Because threads that are idle for 60 seconds are terminated, a CachedThreadPool that remains idle for a long time does not consume any resources.

Working with scenarios

Suitable for scenarios where tasks are executed serially, one task at a time.

Working with scenarios

Scenarios where tasks are executed periodically require a limit on the number of threads

A FutureTask is an asynchronous compute task that can be canceled. Its computation is implemented through Callable, which can be understood as a Runnable that can return a result.

Advantages of using FutureTask:

It implements the Runnable interface and the Future interface, and the underlying implementation is based on the producer-consumer pattern.

FutureTask is used in asynchronous operation scenarios, FutureTask acts as a bridge between the producer (the thread that executes the FutureTask) and the consumer (the thread that gets the FutureTask result), if the producer produces the data first, then the consumer can get the result directly; If the producer has not yet generated data, it will block or timeout blocking until the producer generates data to wake up the blocked consumer.

This problem can be solved using the join method. For example, in thread A, calling the join method of thread B means that A waits for the B thread to finish executing (frees up CPU execution rights) and continues to execute.

The code is as follows:

Concurrency is the number of segments, usually to the N power of 2. The default is 16

The Thread.sleep(long) method that causes the thread to go to a timeout waiting for a blocked (TIMED_WAITING) state. The long parameter sets the amount of sleep time, in milliseconds. When sleep ends, the thread automatically transitions to Runnable.

interrupt() indicates an interrupt thread. It should be noted that the InterruptedException is thrown internally by the thread itself, not by the interrupt() method. When interrupt() is called to a thread, if the thread is executing ordinary code, the thread does not throw an InterruptedException at all. However, as soon as the thread enters wait()/sleep()/join(), an InterruptedException is thrown immediately. You can use isInterrupted() to get the status.

The wait() method in the Object class causes the current thread to wait until another thread calls the object’s notify() method or the notifyAll() wake-up method.

The Thread.yield() method pauses the currently executing thread object, ceding execution to a thread of the same or higher priority.

The notify() method of an Object, which wakes up a single thread waiting on this object monitor. If all threads are waiting on this object, one of the threads is chosen to wake up. Choices are arbitrary and occur when decisions are made about implementations.

notifyAll(), which wakes up all threads waiting on this object monitor.

ReentrantLock, a reentrable lock, is a high-performance tool added to JDK5 under concurrent packets. It enables the same thread to repeatedly acquire a lock without releasing it.

Let’s first look at the template used by ReentrantLock:

The ReentrantLock parameterless constructor, which is created by default as an unfair lock, as follows:

Specify whether to use a fair lock (FairSync) or an unfair lock (NonfairSync) through the fair parameter.

What is a fair lock?

What is an unfair lock?

You can combine AQS + fair lock / unfair lock + CAS to talk about the principle of ReentrantLock.

The wait/notification mechanism means that one thread A calls the wait() method of object O to enter the waiting state, and another thread B calls the notify() or notifyAll() method of object O.

If a thread A executes the thread.join() statement, the meaning is that the current thread A waits for the thread thread to terminate before returning from thread.join(). In addition to providing the join() method, the thread also provides join(long millis) and join(long millis, int nanos) two methods with timeout characteristics. These two timeout methods indicate that if a thread does not terminate within a given timeout period, it will be returned from that timeout method.

ThreadLocal, that is, a thread-local variable (each thread has its own unique one), is a storage structure with the ThreadLocal object as the key and any object as the value. The bottom layer is a ThreadLocalMap to store information, key is a weak reference, value is a strong reference, so clean up in time after use (especially when using thread pools).

This is because the implementation classes provided by JDK developers for thread pools have pits, such as newFixedThreadPool and newCachedThreadPool have memory leak pits.

[1]

Semaphore Use and Principle: https://zhuanlan.zhihu.com/p/98593407

Java inter-thread communication methods explain: http://www.codebaoku.com/it-java/it-java-227064.html

– EOF –

Abandoning FastDFS, Spring Boot integrates MinIO to implement distributed file services, really fragrant! 10,000 words long text, SpringSecurity implements the permission system design

Interviewer: Does a single-core CPU support Java multithreading? What the?

Got a harvest after reading this article? Please forward and share it with more people

Follow “ImportNew” to improve your Java skills

Likes and looks are the biggest support ❤️