[2023Java Interview Eight-part Essay] Java Multithreading

Table of contents

1. Please talk about multithreading

2. Where does Java use CAS

3. Talk about your understanding of AQS (Abstract Queue Synchronizer)

4. Talk about how to ensure thread safety

5. Talk about the thread synchronization method you know

6. Talk about the usage and principle of synchronize

7. What is the difference between synchronized and Lock

8. Talk about the commonly used locks and principles in Java

9. Talk about the usage and principle of volatile

10. Talk about the six states of threads

11. Talk about your understanding of ThreadLocal

12. Talk about the thread communication method you know

13. Talk about how threads are created

14. Talk about your understanding of the thread pool

15. What thread-safe collections do you know?

16. Is HashMap thread-safe? If not how to solve it?

17. Please tell me about JUC

18. Please talk about ConcurrentHashMap


1. Please talk about multithreading

scoring points

The relationship between threads and processes, why use multithreading

Thread: Thread is the smallest unit of operating system scheduling, which allows a process to handle multiple tasks concurrently.

Relationship: Multiple threads can be created in a process, and these threads have their own counters, stacks, local variables, and can share resources in the process. Because of the shared resources, the processor can quickly switch between these threads, giving the user the impression that these threads are executing at the same time.

The benefits of multi-threading: When a thread enters the blocking or waiting state, other threads can obtain the execution right of the CPU, which improves the utilization rate of the CPU.

Disadvantages of multi-threading: Deadlock may occur; frequent context switching may cause waste of resources; in concurrent programming, if multi-threaded serial execution is performed due to resource constraints, the speed may be slower than single-threaded.

Deadlock: Refers to a deadlock in which multiple threads wait for each other due to competition for resources during execution. If there is no external force, they will not be able to advance.

detailed:

Therefore, multiple threads can be created in a process, and these threads have their own counters, stacks, local variables, and can share resources in the process. Because of the shared resources, the processor can quickly switch between these threads, giving the user the impression that these threads are executing at the same time.

In general, the operating system can execute multiple tasks at the same time, and each task is a process. A process can execute multiple tasks at the same time, and each task is a thread. After a program runs, there is at least one process, and a process can contain multiple threads, but at least one thread must be included.

Using multithreading will bring significant benefits to developers, and the reasons for using multithreading are mainly as follows:

  • 1. More CPU cores The way to improve the performance of modern computer processors has changed from pursuing higher main frequency to pursuing more cores, so the number of cores in processors will increase, making full use of the power of processors. The core will significantly improve the performance of the program. The program uses multi-threading technology, which can distribute the calculation logic to multiple processor cores, significantly reducing the processing time of the program, and thus becomes more efficient with the addition of more processor cores.
  • 2. Faster response time We often have to write complex codes for complex businesses. If we use multi-threading technology, we can dispatch operations with weak data consistency to other threads for processing (or message queues), such as Upload pictures, send emails, generate orders, etc. In this way, the thread responding to the user request can complete the processing as soon as possible, which greatly shortens the response time and improves the user experience.
  • 3. A better programming model Java provides a good and consistent programming model for multi-threaded programming, enabling developers to focus more on problem solving. Developers only need to build a suitable business model for this problem without racking their brains Consider how to implement multithreading. Once the developer has established the business model, it can be easily mapped to the multi-threaded programming model provided by Java with a little modification.

2. Where does Java use CAS

scoring points

Atomic classes, AQS, concurrent containers

standard answer

CAS, compare and swap, is a mechanism to solve the performance loss caused by the use of locks in the case of multi-threaded parallelism.

A CAS operation consists of three operands - a memory location (V), an expected old value (A), and a new value (B). If the value of a memory location matches the expected original value, the processor automatically updates the location's value with the new value. Otherwise, the processor does nothing.

CAS effectively says "I think location V should contain value A; if it does, put B at this location; otherwise, don't change that location, just tell me what this location is now.

 There are many places where CAS is used in the API provided by Java. Typical usage scenarios include atomic classes, AQS, and concurrent containers .

atomic class 

For the atomic class, it provides many atomic operation methods inside. Such as atomic replacement of integer value, increase of specified value, and addition of 1, the bottom layer of these methods is realized by the CAS atomic instruction provided by the operating system . It can be used to solve the thread safety problem of a single variable .

Taking AtomicInteger as an example, when a thread calls the incrementAndGet() method of the object, it uses CAS to try to modify its value. If no other thread operates the value at this time, the value is modified successfully; otherwise, the CAS operation is repeatedly executed until the modification is successful.

AQS (Abstract Queue Synchronizer)

It is the basic framework of a multi-threaded synchronizer. Many components use this framework to perform lock and unlock operations by changing the value of the state variable. When multiple threads compete for the lock, they use CAS to modify the status code. If the modification is successful, the lock will be obtained, and if the modification fails, it will enter the synchronization queue and wait.

For AQS, when adding a node to the tail of the synchronization queue, it will first try once in the form of CAS, and if it fails, it will enter the spin state and try repeatedly in the form of CAS. In addition, when the synchronization state is released in a shared manner, it also modifies the synchronization state in a CAS manner.

concurrent container 

Some containers with synchronization functions, such as ConcurrentHashmap, use Synchronize+CAS+Node to achieve synchronization. Whether it is the initialization of the array, the expansion of the array or the operation of the linked list nodes, CAS is used first. 

For concurrent containers , take ConcurrentHashMap as an example, which uses CAS operations multiple times inside.

When initializing the array, it will modify the initialization state in CAS mode, avoiding multiple threads to initialize at the same time.

When executing the put method to initialize the head node, it will set the initialized head node to the first position of the specified slot in CAS mode, so as to avoid multiple threads setting the head node at the same time.

When expanding the array, each thread will modify the task sequence number in CAS mode to compete for the expansion task to avoid conflicts with other threads.

When the get method is executed, it will obtain the head node of the slot specified by the head in the way of CAS, so as to prevent other threads from modifying the head node at the same time.

Bonus answer - atomic operation instructions

The implementation of CAS is inseparable from the support of atomic instructions in the operating system. The methods of encapsulating atomic instructions in Java are concentrated in the Unsafe class, including: atomic replacement of reference types, atomic replacement of int integers, and atomic replacement of long integers. These methods have four parameters: var1, var2, var4, var5, where var1 represents the object to be operated, var2 represents the member variable to be replaced, var4 represents the expected value, and var5 represents the updated value. public final native boolean compareAndSwapObject( Object var1, long var2, Object var4, Object var5); public final native boolean compareAndSwapInt( Object var1, long var2, int var4, int var5); public final native boolean compareAndSwapLong( Object var1, long var2, long var4, long var6);

3. Talk about your understanding of AQS ( Abstract Queue Synchronizer )

scoring points

template method, sync queue, sync state

standard answer

bottom layer 

AQS is the basic framework of a multi-threaded synchronizer. Many components use this framework to perform lock and unlock operations by changing the value of the state variable. When multiple threads compete for the lock, they use CAS to modify the status code. If the modification is successful, the lock will be obtained, and if the modification fails, it will enter the synchronization queue and wait. 

1. Concept: AQS is an abstract queue synchronizer, which is used to build the basic framework of locks. Lock implementation classes are all implemented based on AQS .

2. Template method: AQS is designed based on the template method pattern, so the implementation of the lock needs to inherit AQS and rewrite the method specified by it.

3. Synchronization queue and synchronization state: AQS internally defines a FIFO queue to realize thread synchronization, and also defines a synchronization state to record lock information.

The template method of AQS extracts the logic of managing the synchronization state to form a standard process. These methods mainly include: exclusive acquisition of synchronization state, exclusive release of synchronization state, shared acquisition of synchronization state, and shared release of synchronization state.

Template method: AQS is designed based on the template method pattern , so the implementation of the lock needs to inherit AQS and rewrite its specified method.

AQS internally defines a FIFO queue to achieve thread synchronization, and also defines a synchronization state to record lock information .

The template method of AQS extracts the logic of managing the synchronization state to form a standard process. These methods mainly include: exclusive acquisition of synchronization state, exclusive release of synchronization state, shared acquisition of synchronization state, and shared release of synchronization state.

Taking the exclusive acquisition of synchronization status as an example, its general flow is:

 1. Attempts to acquire synchronization state exclusively.

 2. If the status acquisition fails, add the current thread to the synchronization queue.

 3. Spin processing synchronization state, if the current thread is at the head of the queue, wake it up and let it out of the queue, otherwise make it into the blocking state. Among them, some steps cannot be determined in the parent class, so they are extracted into empty methods and left for the subclass to implement. For example, the trial operation in the first step is different for fair locks and unfair locks, so subclasses need to implement this method according to the scene when implementing it.

Synchronization queue: The synchronization queue of AQS is a two-way linked list, and AQS holds the head and tail nodes of the linked list. For the setting of the tail node, there is multi-thread competition, so the method of CAS is used to modify it. For the head node setting, it must be processed by the thread that has obtained the synchronization state, so the modification of the head node does not need to use the CAS method.

Synchronization state: The synchronization state of AQS is an integer of int type, which can represent the quantity while representing the state. Normally, when the status is 0, it means no lock, and when the status is greater than 0, it means the number of reentrant locks. In addition, in the scenario of a read-write lock, this status flag must record both the read lock and the write lock. Therefore, the implementer of the lock splits the state representation into high and low parts, the high bit stores the read lock, and the low bit stores the write lock. Extra points to answer that the synchronization state needs to be modified in a concurrent environment, so it needs to be thread-safe. Since AQS itself is a lock implementation tool, it is not suitable to use locks to ensure its thread safety, because if you use a lock to define another lock, then simply use synchronized. In fact, the synchronization state is modified by volatile, which can ensure the memory visibility of state variables, thus solving the problem of thread safety. Extra points to answer that the synchronization state needs to be modified in a concurrent environment, so it needs to be thread-safe. Since AQS itself is a lock implementation tool, it is not suitable to use locks to ensure its thread safety, because if you use a lock to define another lock, then simply use synchronized. In fact, the synchronization state is modified by volatile, which can ensure the memory visibility of state variables, thus solving the problem of thread safety.

4. Talk about how to ensure thread safety

scoring points

Atomic classes, volatile, locks

standard answer

Thread safety issue: In the context of multi-threading, the thread does not execute as expected, resulting in an exception in the operation of shared variables .

Arranged from light to heavy according to resource occupation , thread safety is guaranteed  through atomic classes, volatile, and locks .

Atomic classes and volatile can only guarantee the thread safety of a single shared variable , while locks can guarantee the thread safety of multiple shared variables in the critical section.

atomic class

Atomic class is a class with atomic operation characteristics (atom is the smallest unit in chemistry, indivisible), and atomic means that an operation is uninterruptible. Even when multiple threads are executing together, once an operation starts, it will not be disturbed by other threads. .

Under java.util.concurrent.atomicthe package, there are a series of classes beginning with "Atomic", collectively referred to as atomic classes. For example, instead of AtomicInteger int, the bottom layer is implemented by CAS atomic instructions , and the internal storage value is modified with volatile , so the modification between multiple threads is visible .

Taking AtomicInteger as an example, when a thread calls the incrementAndGet() method of the object, it uses CAS to try to modify its value. If no other thread operates the value at this time, the value is modified successfully; otherwise, the CAS operation is repeatedly executed until the modification is successful.

volatile

The volatile keyword is a lightweight synchronization mechanism used to ensure order and visibility . Orderliness: The code before the variable declared by volatile must be executed before it, and the code after it must be executed slower than it. Visibility: Once a variable is modified, it is immediately refreshed to the shared memory. When other threads want to read this variable, they will eventually read it from the memory instead of from their own workspace.

Lock

There are two ways to lock, namely the synchronized keyword and the Lock interface (under the JUC package).

A synchronized lock is a mutual exclusion lock, which can be applied to instance methods, static methods, and code blocks. It can ensure that only one thread executes the code at the same time, ensuring thread safety. The lock is automatically released when execution is complete or an exception occurs. Synchronized locks are based on object headers and Monitor objects. After 1.6, optimizations such as lightweight locks and biased locks are introduced.  

The lock interface can lock a piece of code through the lock and unlock methods , and the Lock implementation classes are all implemented based on AQS . Lock can make threads waiting for locks respond to interruptions , but synchronized cannot. When using synchronized, waiting threads will wait forever and cannot respond to interruptions.

Thread safety: In a program that is executed in parallel by multiple threads with shared data, the thread-safe code will ensure that each thread can execute normally and correctly through a synchronization mechanism, and there will be no accidents such as data pollution  .

There are many ways to ensure thread safety in Java, among which there are three commonly used ones, which are arranged from light to heavy according to resource occupation. These three ways to ensure thread safety are atomic classes, volatile, and locks.

JDK has provided the java.util.concurrent.atomic package since 1.5. The atomic operation class in this package provides a way to update a variable with simple usage, high performance, and thread safety.

A total of 17 classes are provided in the atomic package, which can be summarized into 4 types of atomic update methods according to their functions, namely atomic update basic type, atomic update reference type, atomic update attribute, and atomic update array. Regardless of the type of atomic update, the "compare and replace" rule must be followed, that is, compare whether the value to be updated is equal to the expected value, update if it is, and fail if not. Volatile is a lightweight synchronized, which ensures the "visibility" of shared variables in multiprocessor development, thereby ensuring thread safety when reading and writing a single variable. The visibility problem is caused by the caches of the processor cores, each core has its own cache, and these caches are synchronized with the memory. Volatile has the following memory semantics: when writing a volatile variable, the value of the shared variable in the thread local memory will be immediately refreshed to the main memory; when reading a volatile variable, the thread local memory will be invalidated, forcing Threads read shared variables directly from main memory. Atomic classes and volatile can only guarantee the thread safety of a single shared variable, while locks can guarantee the thread safety of multiple shared variables in the critical section. There are two ways to lock in Java, namely the synchronized keyword and the Lock interface. Synchronized is a relatively early API, which did not consider the timeout mechanism, non-blocking form, and multiple condition variables at the beginning of the design. If you want to upgrade it to support these relatively complex functions, you need to change its grammatical structure, which is not conducive to compatibility with old codes. Therefore, the JDK development team added a Lock interface in 1.5, and supported the above-mentioned functions through Lock, namely: support for responding to interrupts, support for timeout mechanisms, support for acquiring locks in a non-blocking manner, and support for multiple condition variables (blocking queues) ).

Bonus answer

There are many ways to achieve thread safety, in addition to the above three ways, there are the following ways:

  • 1. The thread safety problem of stateless design is caused by the concurrent modification of shared variables by multiple threads. If no shared variables are designed in a concurrent environment, thread safety problems will naturally not arise. This kind of code implementation can be called "stateless implementation", and the so-called state refers to shared variables.
  • 2. Immutable design If you have to design shared variables in a concurrent environment, you should give priority to whether the shared variables are read-only. If it is a read-only scenario, you can design the shared variables as immutable, so naturally it will not appear Thread safety issues. Specifically, add the final modifier before the variable to make it unmodified. If the variable is a reference type, design it as an immutable type (refer to the String class).
  • 3. Concurrent tools The java.util.concurrent package provides several useful concurrent tool classes, which can also ensure thread safety: - Semaphore: It is a semaphore, which can control the number of threads accessing a specific resource at the same time. - CountDownLatch: Allows one or more threads to wait for other threads to complete operations. - CyclicBarrier: Let a group of threads be blocked when they reach a barrier, and the barrier will not be opened until the last thread reaches the barrier, and all threads blocked by the barrier will continue to run.
  • 4. Local storage We can also consider using ThreadLocal to store variables. ThreadLocal can easily store a separate data for each thread, that is, copy resources that need to be accessed concurrently into multiple copies. In this way, multi-thread access to shared variables can be avoided, and they access their own exclusive resources, which fundamentally isolates data sharing between multiple threads. 

5. Talk about the thread synchronization method you know

scoring points

synchronized、Lock

standard answer

Thread synchronization: that is, when a thread is operating on memory, other threads cannot operate on this memory address. Until the thread completes the operation, other threads can operate on the memory address, while other threads are in a waiting state .

Java mainly implements thread synchronization through locking , and there are two types of locks, namely the synchronized keyword and the Lock interface (under the JUC package).

A synchronized lock is a mutual exclusion lock, which can be applied to instance methods, static methods, and code blocks. It can ensure that only one thread executes the code at the same time, ensuring thread safety. The lock is automatically released when execution is complete or an exception occurs. Synchronized locks are based on object headers and Monitor objects. After 1.6, optimizations such as lightweight locks and biased locks are introduced.  

The lock interface can lock a piece of code through the lock and unlock methods , and the Lock implementation classes are all implemented based on AQS. Lock can make threads waiting for locks respond to interruptions, but synchronized cannot . When using synchronized, waiting threads will wait forever and cannot respond to interruptions.

Synchronized can be added in three different positions, corresponding to three different usage methods. The difference between these three methods is that the lock objects are different:

1. If added to a common method, the lock is the current instance (this).

2. Added to the static method, the lock is the Class object of the current class.

3. Add it to the code block, you need to explicitly specify an object as the lock object in the parentheses after the keyword.

Different lock objects mean different lock granularity, so we should not add it before the method without thinking, although usually this can solve the problem. Instead, we should accurately select the lock object according to the scope to be locked, so as to accurately determine the granularity of the lock and reduce the performance overhead caused by the lock.

Synchronized is a relatively early API, which did not consider the timeout mechanism, non-blocking form, and multiple condition variables at the beginning of the design. If you want synchronized to support these relatively complex functions by upgrading, you need to change its grammatical structure, which is not conducive to compatibility with old codes. Therefore, the JDK development team introduced the Lock interface in 1.5, and supported the above functions through Lock.

The functions supported by Lock include: support for response interrupts, support for timeout mechanisms, support for acquiring locks in a non-blocking manner, and support for multiple condition variables (blocking queues).

Bonus answer

Synchronized is implemented using "CAS+Mark Word" . For performance considerations, the lock overhead is reduced through the lock upgrade mechanism. In a concurrent environment, synchronized will gradually upgrade according to the following steps as multi-thread competition intensifies: no lock, biased lock, lightweight lock, and heavyweight lock.

Lock is implemented by "CAS+volatile" , and the core of its implementation is AQS. AQS is a thread synchronizer, a basic framework for thread synchronization, which is based on the template method pattern. In a specific Lock instance, the implementation of the lock is realized by inheriting AQS, and specific implementations such as fair lock, unfair lock, read lock, and write lock can be derived according to the usage scenarios of the lock. 

6. Talk about the usage and principle of synchronize

scoring points

Acts on three positions, object head, lock upgrade

standard answer 

synchronized lock: 

A synchronized lock is a mutual exclusion lock, which can be applied to instance methods, static methods, and code blocks. It can ensure that only one thread executes the code at the same time, ensuring thread safety. The lock is automatically released when execution is complete or an exception occurs. Synchronized locks are based on object headers and Monitor objects. After 1.6, optimizations such as lightweight locks and biased locks are introduced.  

Acts in three places:

 1. When acting on a static method, the lock is the Class object of the current class.

 2. When acting on a common method, the lock is the current instance (this).

 3. To act on a code block, you need to explicitly specify an object as the lock object in parentheses after the keyword.

Object header:  The bottom layer of synchronized uses Java object headers to store lock information , and also supports lock upgrades.

Lock upgrade: When the competition is small, it only needs to be locked at a small cost, and the heavyweight lock is not used until the competition intensifies, thereby reducing the overhead caused by locking.

Synchronized can be used in three different positions, corresponding to three different usage methods. The difference between these three methods is that the lock objects are different. Different lock objects mean different lock granularity, so we should not add it before the method without thinking, although usually this can solve the problem. Instead, we should accurately select the lock object according to the scope to be locked, so as to accurately determine the granularity of the lock and reduce the performance overhead caused by the lock.

object header

The bottom layer of synchronized uses Java object headers to store lock information , and also supports lock upgrades .

The Java object header consists of three parts, namely Mark Word, Class Metadata Address, and Array length . Among them, Mark Word is used to store the hashCode and lock information of the object, Class Metadata Address is used to store the pointer of the object type, and Array length is used to store the length of the array object. If the object is not an array type, there is no Array length information.

The synchronized lock information includes the lock flag and lock status , which are all stored in the Mark Word part of the object header.

In order to reduce the performance consumption caused by acquiring and releasing locks , Java 6 introduces biased locks and lightweight locks .

Therefore, in Java 6, locks are divided into four states , and the levels from low to high are: no lock state, biased lock state, lightweight lock state, and heavyweight lock state. With the escalation of thread competition, the state of the lock will gradually upgrade from the lock-free state to the heavyweight lock state. Locks can be upgraded but not downgraded . This strategy of only upgrading but not downgrading is to improve efficiency.

The early design of synchronized did not include a lock upgrade mechanism, so the performance was poor. At that time, there were only lock-free and lock-able points. Biased locks and lightweight locks are introduced to improve performance, so we need to focus on the principles of these two states and their differences.

Bias lock:

Biased lock, as the name implies, means that the lock is biased towards a certain thread .

When a thread accesses the synchronization block and acquires the lock, it will store the lock-biased thread ID in the object header and the lock record in the stack frame, so that the thread does not need to do locking and unlocking operations when it enters and exits the synchronization block in the future , you only need to simply test whether your own thread ID is stored in Mark Word. Lightweight lock means that when locking, the JVM first creates a space for storing lock records in the current thread stack frame, and copies the Mark Word to the lock record, which is officially called Displaced Mark Word. Then the thread tries to replace the Mark Word with a pointer to the lock record in CAS mode. If it succeeds, the current thread acquires the lock. If it fails, it means that other threads compete for the lock. At this time, the current thread will try to acquire the lock by spinning.

Bonus answer - the process of lock upgrade

Next, let's start from the actual scene and talk about the lock upgrade process in detail: 

1. At the beginning, no thread accesses the synchronized block, and the synchronized block is in a lock-free state.

 2. Then, thread 1 first accesses the synchronization block, which modifies the Mark Word in the form of CAS (compare and exchange), and tries to add a biased lock . Since there is no competition at this time, the biased lock is locked successfully, and the ID of thread 1 is stored in the Mark Word at this time.

 3. Then, thread 2 starts to access the synchronization block, it modifies the Mark Word in CAS mode, and tries to add a biased lock. Due to the competition at this time, the locking of the biased lock fails, so thread 2 will initiate the process of canceling the biased lock (clearing the ID of thread 1), so the synchronization block recovers from the biased state of thread 1 to the state of fair competition.

 4. Then, thread 1 and thread 2 compete together, and they modify the Mark Word in CAS mode at the same time, trying to add a lightweight lock. Due to competition, only one thread will succeed, assuming thread 1 succeeds. But thread 2 will not give up easily. It thinks that thread 1 will finish executing soon, and the execution right will soon fall to itself, so thread 2 continues to spin and lock.

 5. Finally, if thread 1 finishes executing soon, thread 2 will succeed in adding a lightweight lock, and the lock will not be promoted to a heavyweight state. It may also be that the execution time of thread 1 is longer, then thread 2 will give up the spin after a certain number of spins, and initiate the process of lock expansion. At that time, the lock is changed to a heavyweight lock by thread 2, and then thread 2 enters the blocked state. When thread 1 repeatedly locks or unlocks, the CAS operation will fail, and at this time it will release the lock and wake up the waiting thread.

In short, under the lock upgrade mechanism, the lock will not become a heavyweight lock in one step, but will be gradually upgraded according to the competition situation. When the competition is small, it only needs to lock at a small cost, and the heavyweight lock is not used until the competition intensifies, thereby reducing the overhead caused by locking.

7. What is the difference between synchronized and Lock

scoring points

Usage, main features, implementation mechanism

standard answer

Lock and synchronized have the following differences:

  1) Interface and keywords. Lock is an interface, and synchronized is a keyword in Java, and synchronized is a built-in language implementation;

  2) Deadlock problem. Synchronized will automatically release the lock held by the thread when an exception occurs, so it will not cause deadlock; while Lock does not actively release the lock through unLock() when an exception occurs, it is likely to cause deadlock, so When using Lock, you need to release the lock in the finally block;

  3) Let the thread waiting for the lock respond to the interrupt. Lock can make threads waiting for locks respond to interruptions, but synchronized cannot. When using synchronized, waiting threads will wait forever and cannot respond to interruptions;

  4) Know whether the lock is successfully acquired. Through Lock, you can know whether the lock has been successfully acquired, but synchronized cannot.

  5) Efficiency comparison. Lock can improve the efficiency of multiple threads for read operations. In terms of performance, if the competition for resources is not intense, the performance of the two is similar, and when the competition for resources is very intense (that is, there are a large number of threads competing at the same time), the performance of Lock is far better than synchronized . Therefore, it should be selected according to the appropriate situation in specific use.

Both synchronized and Lock are locks, which are means of thread synchronization. Their differences are mainly reflected in the following three aspects:

1. Differences in usage The synchronized keyword can be used on static methods, instance methods, and code blocks. It is an implicit lock, that is, we do not need to explicitly acquire and release locks, so it is very convenient to use. In this synchronization method, we need to rely on Monitor (synchronization monitor) to achieve thread communication. If the keyword acts on a static method, then Monitor is the Class object of the current class; if the keyword acts on an instance method, then Monitor is the current instance (this); if the keyword acts on a code block, it needs to be in the keyword The following parentheses explicitly specify an object as a Monitor. The Lock interface is an explicit lock, that is, we need to call its internally defined methods to explicitly lock and unlock. Compared with synchronized, this is a bit cumbersome, but it provides greater flexibility. In this synchronization method, we need to rely on the Condition object to achieve thread communication, which is created by the Lock object and depends on the Lock. Each Condition represents a waiting queue, and a Lock can create multiple Condition objects. Relatively speaking, each Monitor also represents a waiting queue, but synchronized can only have one Monitor. Therefore, the Lock interface has greater flexibility in implementing thread communication.

2. Differences in functional features Synchronized is an early API, and Lock was introduced in JDK 1.5. In terms of design, Lock makes up for the shortcomings of synchronized. It adds some new features, which are not available in synchronized. These features include: - Interruptible lock acquisition: threads can be interrupted during the process of acquiring locks. - Acquire the lock non-blockingly: This method returns immediately after the call, and returns true if the lock can be obtained, otherwise returns false. - The lock can be acquired with a timeout: If the thread has not acquired the lock after the timeout period, and the thread has not been interrupted, return false.

3. Differences in Implementation MechanismsThe bottom layer of synchronized uses the Java object header to store lock information. The object header contains three parts, namely Mark Word, Class Metadata Address, and Array length. Among them, Mark Word is used to store the hashCode and lock information of the object, Class Metadata Address is used to store the pointer of the object type, and Array length is used to store the length of the array object. AQS is a queue synchronizer, which is the basic framework for building locks. Lock implementation classes are all implemented based on AQS. AQS is designed based on the template method pattern, so the implementation of the lock needs to inherit AQS and rewrite its specified method. AQS internally defines a FIFO queue to achieve thread synchronization, and also defines a synchronization state to record lock information. Extra points to answer the early synchronized performance is poor, not as good as Lock. Later, synchronized introduced a lock upgrade mechanism in its implementation, and its performance has not lost to Lock. Therefore, the difference between synchronized and Lock is not mainly in performance, because the performance of the two is almost the same. In order to reduce the performance consumption caused by acquiring and releasing locks, Java 6 introduces biased locks and lightweight locks. Therefore, starting from Java 6, locks are divided into four states, and the levels are from low to high: no lock, biased lock, lightweight lock, and heavyweight lock. With the escalation of thread competition, the state of the lock will gradually upgrade from the lock-free state to the heavyweight lock state. Locks can be upgraded but not downgraded. This strategy of only upgrading but not downgrading is to improve efficiency. The early design of synchronized did not include a lock upgrade mechanism. At that time, there were only locks and locks. Biased locks and lightweight locks are introduced to improve performance, so we need to focus on the principles of these two states and their differences. Biased lock, as the name implies, means that the lock is biased towards a certain thread. When a thread accesses the synchronization block and acquires the lock, it will store the lock-biased thread ID in the object header and the lock record in the stack frame, so that the thread does not need to do locking and unlocking operations when it enters and exits the synchronization block in the future , you only need to simply test whether your own thread ID is stored in Mark Word. Lightweight lock means that when locking, the JVM first creates a space for storing lock records in the current thread stack frame, and copies the Mark Word to the lock record. Officially called Displaced Mark Word. Then the thread tries to replace the Mark Word with a pointer to the lock record in CAS mode. If it succeeds, the current thread acquires the lock. If it fails, it means that other threads compete for the lock. At this time, the current thread will try to acquire the lock by spinning.

8. Talk about the commonly used locks and principles in Java

scoring points

Object header, AQS

standard answer

Commonly used locks in Java: the synchronized keyword and the lock lock interface.

The bottom layer of the synchronized keyword uses the java object header to store lock information.

The lock interface is implemented based on AQS (Abstract Queue Synchronizer). AQS internally defines a first-in-first-out queue to realize lock synchronization, and also defines a synchronization state to record lock information.

AQS performs lock and unlock operations by changing the value of the state variable. When multiple threads compete for the lock, they use CAS to modify the status code. If the modification is successful, the lock will be obtained, and if the modification fails, it will enter the synchronization queue and wait. 

There are two ways to lock in Java, namely the synchronized keyword and the Lock interface, and the classic implementation of the Lock interface is ReentrantLock. In addition, there is the ReadWriteLock interface, which internally designs two locks for reading and writing respectively, both of which are of the Lock type, and its classic implementation is ReentrantReadWriteLock. Among them, the implementation of synchronized depends on the object header, and the implementation of the Lock interface depends on AQS.

synchronized

The bottom layer of synchronized uses the Java object header to store lock information. The object header contains three parts, namely Mark Word, Class Metadata Address, and Array length. Among them, Mark Word is used to store the hashCode and lock information of the object, Class Metadata Address is used to store the pointer of the object type, and Array length is used to store the length of the array object.

Lock

AQS is a queue synchronizer, which is the basic framework for building locks. Lock implementation classes are all implemented based on AQS. AQS is designed based on the template method pattern, so the implementation of the lock needs to inherit AQS and rewrite its specified method. AQS internally defines a FIFO queue to achieve thread synchronization, and also defines a synchronization state to record lock information.

Bonus answer

ReentrantLock defines locks through the internal class Sync, and it also defines two subclasses of Sync, FrSync and NonfrSync, which represent fair locks and unfair locks respectively. Sync is inherited from AQS. It not only uses the synchronization state of AQS to record lock information, but also uses the synchronization state to record the number of reentries. The synchronization state is an integer. When it is 0, it means no lock. When it is N, it means that the thread holds the lock and re-enters N times. ReentrantReadWriteLock supports reentrant in the same way as ReentrantLock. It also defines the internal class Sync, and defines two subclasses FrSync and NonfrSync to implement fair locks and unfair locks. In addition, ReentrantReadWriteLock internally contains two locks for reading and writing, both of which are implemented by Sync. The difference is that read locks support sharing, that is, multiple threads can successfully add read locks at the same time, while write locks are mutually exclusive, that is, only one thread can successfully add locks.

9. Talk about the usage and principle of volatile

scoring points

Features, Memory Semantics, Implementation Mechanisms

characteristic:

Orderliness: The code before the variable declared by volatile must be executed before it, and the code after it must be executed slower than it.

Visibility: Once a variable is modified, it is immediately refreshed to the shared memory. When other threads want to read this variable, they will eventually read it from the memory instead of from their own workspace. 

Atomicity: The read and write of a single volatile variable is atomic, but the composite operation of "volatile variable ++" is not atomic.

Note: The atomicity of volatile is only for a single variable, and compound operations cannot guarantee atomicity, which is also the difference from synchronized. 

Memory semantics:

- Write memory semantics: When writing a volatile variable, JMM (Java Memory Model) will refresh the value of the shared variable in the thread's local memory to the main memory.

- Read memory semantics: When reading a volatile variable, JMM will invalidate the thread local memory, making it read the shared variable from the main memory.

The underlying implementation mechanism:

The bottom layer of volatile is implemented by using memory barriers , that is, when the compiler generates bytecode, memory barriers are inserted into the instruction sequence to prohibit specific types of processor reordering .

Volatile is lightweight synchronized, which guarantees the "visibility" of shared variables in multiprocessor development. Visibility means that when one thread modifies a shared variable, another thread can read the modified value. If volatile is used properly, it has a lower execution cost than synchronized because it does not cause thread context switching and scheduling. In short, volatile variables have the following properties:

- Visibility: When reading a volatile variable, you can always see (any thread) the last write to this volatile variable.

- Atomicity: The read and write of a single volatile variable is atomic, but the composite operation of "volatile variable ++" is not atomic.

Memory semantics:

Volatile implements the above characteristics by affecting the memory visibility of threads, and it has the following memory semantics. Among them, JMM refers to the Java memory model, and local memory is just an abstract concept of JMM, which covers caches, write buffers, registers, and other hardware and compiler optimizations. In this article, you can simply understand it as a cache.

- Write memory semantics: When writing a volatile variable, JMM will refresh the value of the shared variable in the thread's local memory to the main memory.

- Read memory semantics: When reading a volatile variable, JMM will invalidate the thread local memory, making it read the shared variable from the main memory.

The underlying implementation mechanism:

The bottom layer of volatile is implemented by using memory barriers, that is, when the compiler generates bytecode, memory barriers are inserted into the instruction sequence to prohibit specific types of processor reordering. A memory barrier is a piece of platform-related code. The memory barrier code in Java is defined in the Unsafe class, which contains three methods: LoadFence(), storeFence(), and fullFence(). Extra points answer From the perspective of memory semantics, volatile read/write has the same memory effect as lock acquisition/release. That is, volatile reads have the same memory semantics as lock acquisition, and volatile writes have the same memory semantics as lock releases. Volatile can only guarantee the atomicity of reading and writing of a single variable, while the lock can guarantee the atomicity of the code execution of the entire critical section. Therefore, functional locking is more powerful than volatile, and volatile has more advantages in terms of scalability and performance.

10. Talk about the six states of threads

scoring points

NEW、RUNNABLE、BLOCKED、WTING、TIMED_WTING、TERMINATED

standard answer

During the running life cycle of a Java thread, at any given moment, it can only be in one of the following six states:

NEW: In the initial state, the thread is created, but the start method has not been called yet.

RUNNABLE: Runnable state, ready or running. The thread is executing in the JVM, but it may be waiting for the scheduling of the operating system.

BLOCKED: Blocked state, the thread is waiting to acquire the monitor lock.

WTING: Waiting state, the thread is waiting for notification or interruption from other threads.

TIMED_WTING: timeout waiting state, the timeout period is added on the basis of WTING, that is, the timeout is automatically returned.

TERMINATED: Terminated state, the thread has been executed.

After the thread is created, it defaults to the initial state, and enters the runnable state after calling the start method. The runnable state does not mean that the thread is running, and it may be waiting for the scheduling of the operating system. The thread entering the waiting state needs notification from other threads to return to the runnable state , and the timeout waiting state is equivalent to adding a timeout limit on the basis of the waiting state. In addition to the wake-up of other threads, it will also return to the running state when the timeout time is reached. .

In addition, when the thread executes the synchronization method, it will enter the blocked state without acquiring the lock. After the thread executes the run method, it will enter the terminated state.

Bonus answer

Java combines the two states of ready and running in the operating system into a runnable state (RUNNABLE). When a thread is blocked by a synchronized monitor lock, it will enter a blocked state, but when a thread is blocked by a Lock lock, it will enter a waiting state. This is because the Lock interface implementation class uses the relevant methods in the LockSupport class for the implementation of blocking.

11. Talk about your understanding of ThreadLocal

scoring points

function, mechanism

standard answer

Function: ThreadLocal, which is a thread variable , copies multiple resources that need to be accessed concurrently, so that each thread has a resource. Since each thread has its own copy of the resource, there is no need to synchronize this variable. When writing multi-threaded code, you can encapsulate unsafe variables into ThreadLocal .

Memory leak problem: When using the thread pool, ThreadLocal needs to manually remove the thread variable in the thread after using it , because the thread is not destroyed in the thread pool, and the objects in ThreadLocal cannot be automatically garbage collected 

Implementation Mechanism:

In terms of implementation, the threadLocals variable is declared in the Thread class, which is used to store the resources exclusive to the current thread. The type of the variable (ThreadLocalMap) is defined in the ThreadLocal class, which is a Map-like structure for storing key-value pairs.

The set and get methods are also provided in the ThreadLocal class. The set method will initialize the ThreadLocalMap and bind it to Thread.threadLocals, thereby binding the incoming value to the current thread. On data storage, the incoming value will be used as the value of the key-value pair, and the key is the ThreadLocal object itself (this). The get method does not have any parameters, and it will use the current ThreadLocal object (this) as the key to obtain the data bound to the current thread from Thread.threadLocals.

Bonus Answer - Comparing ThreadLocal and Synchronization Mechanisms

Note that ThreadLocal cannot replace the synchronization mechanism , and the two face different problem areas. 

The synchronization mechanism is to synchronize the concurrent access of multiple threads to the same resource , and it is an effective way to communicate between multiple threads. When a thread is operating on memory, no other thread can operate on this memory address.

And ThreadLocal is to isolate the data sharing of multiple threads, and fundamentally avoid the competition for shared resources (variables) between multiple threads, so there is no need to synchronize multiple threads.

In general, if multiple threads need to share resources to achieve the communication function between threads, a synchronization mechanism is used. If you just need to isolate sharing conflicts between multiple threads, you can use ThreadLocal.

Example of thread isolation using ThreadLocal:

The two thread sub-tables obtain the variables stored in their own threads, and the variable acquisition between them will not be confused:

public class ThreadLocaDemo {
 
    private static ThreadLocal<String> localVar = new ThreadLocal<String>();
 
    static void print(String str) {
        //打印当前线程中本地内存中本地变量的值
        System.out.println(str + " :" + localVar.get());
        //清除本地内存中的本地变量
        localVar.remove();
    }
    public static void main(String[] args) throws InterruptedException {
 
        new Thread(new Runnable() {
            public void run() {
                ThreadLocaDemo.localVar.set("local_A");
                print("A");
                //打印本地变量
                System.out.println("after remove : " + localVar.get());
               
            }
        },"A").start();
 
        Thread.sleep(1000);
 
        new Thread(new Runnable() {
            public void run() {
                ThreadLocaDemo.localVar.set("local_B");
                print("B");
                System.out.println("after remove : " + localVar.get());
              
            }
        },"B").start();
    }
}
 
A :local_A
after remove : null
B :local_B
after remove : null
 

12. Talk about the thread communication method you know

scoring points

Monitor、Condition

standard answer

Thread communication: When multiple threads are executed concurrently, they are randomly switched and executed in the CPU. At this time, we want multiple threads to complete a task together. At this time, we need communication between threads. Threads work together to complete a task.

Thread communication methods:  Monitor (synchronization monitor) and Condition.

The thread communication method depends on the thread synchronization method.

When we use synchronize to synchronize, we will use monitor to realize thread communication. The monitor here is actually a lock object , which uses the wait, notify, notifyAll and other methods of object to realize thread communication.

When using Lock for synchronization, it is to use Condition to realize thread communication. The Condition object is created by Lock and depends on the Lock object, and its await, sign or signAll method is used to realize thread communication. 

In Java, there are two commonly used thread communication methods, namely using Monitor to realize thread communication and using Condition to realize thread communication.

Thread synchronization is the premise of thread communication, so which way to achieve communication depends on the way of thread synchronization.

Monitor

If you use the synchronized keyword for synchronization, you need to rely on Monitor (synchronization monitor)

To achieve thread communication, Monitor is the lock object. In the synchronized synchronization mode, the lock object can be of any type, so the communication methods are naturally defined in the Object class, these methods include: wt(), notify(), notifyAll().

When a thread calls wt() through Monitor, it releases the lock and waits here. When other threads call notify() through Monitor, a thread waiting here will be awakened. When other threads call notifyAll() through Monitor, all threads waiting here will be woken up.

Lock

JDK 1.5 added the Lock interface and its implementation class, providing a more flexible synchronization method.

If you use the Lock object for synchronization, you need to rely on the Condition to implement thread communication. The Condition object is created by the Lock object, and it depends on the Lock object.

The communication methods defined in the Condition object are similar to those in the Object class, including awt(), signal(), and signalAll(). You can see their meanings by their names. When calling awt() through Condition, the current thread releases the lock and waits. When calling signal() through Condition, it wakes up a waiting thread. When calling signalAll() through Condition, it wakes up. All waiting threads.

Bonus answer

Thread synchronization is realized based on synchronous queue, while thread communication is realized based on waiting queue. When the waiting method is called, the current thread is added to the waiting queue. When the notify method is called, one or more threads in the waiting queue are transferred back to the synchronization queue. Because synchronized has only one Monitor, it has only one waiting queue. The Lock object can create multiple Conditions, so it has multiple waiting queues. Multiple waiting queues bring great flexibility, so the communication method based on Condition is more recommended. For example, when implementing the production and consumption model, the producer should notify the consumer, and the consumer should notify the producer. On the contrary, there should not be a situation where the producer informs the producer and the consumer informs the consumer. If you use synchronized to implement this model, since it has only one waiting queue, you can only add producers and consumers to the same queue, which will lead to the situation where the producer notifies the producer and the consumer notifies the consumer. When using Lock to implement this model, because it has multiple waiting queues, it can effectively separate the two roles and avoid such problems.

13. Talk about how threads are created

scoring points

Thread、Runnable、Callable

standard answer

There are 3 ways to create threads:

1. Inherit the Thread class and rewrite the run() method;

2. Implement the Runnable interface (recommended) , and implement the run() method of the interface;

3. Implement the Callable interface and rewrite the call() method.

The first two methods have no return value after the thread is executed, and the last one has a return value.

It is recommended to implement the Runnable interface, because Java is a single inheritance, and the thread class can inherit from other classes while implementing the interface .

There are three ways to create a thread, which are inheriting the Thread class, implementing the Runnable interface, and implementing the Callable interface.

1. The steps to create a thread by inheriting the Thread class are as follows

  • - Define a subclass of the Thread class, and rewrite the run() method of this class, which will be used as the thread execution body.
  • - Create an instance of the Thread subclass, that is, create a thread object.
  • - Call the start() method of the thread object to start the thread.

2. The steps to create a thread by implementing the Runnable interface are as follows

  • - Define the implementation class of the Runnable interface, and implement the run() method of the interface, which will be used as the thread execution body.
  • - Create an instance of the Runnable implementation class and use it as a parameter to create a Thread object, which is a thread object.
  • - Call the start() method of the thread object to start the thread.

3. The steps to create a thread by implementing the Callable interface are as follows

  • - Define the implementation class of the Callable interface, and implement the call() method, which will be used as the thread execution body.
  • - Create an instance of the Callable implementation class, and use the instance as a parameter to create a FutureTask object.
  • - Use the FutureTask object as a parameter, create a Thread object, and then start the thread.
  • - Call the get() method of the FutureTask object to obtain the return value after the execution of the child thread is completed.

To sum up, there are actually only two ways to create threads: inheriting the parent class and implementing the interface.

The difference between using the Runnable interface and the Callable interface is that the former cannot obtain the return value of the thread execution end, and the latter can obtain the return value of the thread execution end.

The advantages and disadvantages of inheriting the parent class and implementing the interface are:

  • - Using the interface to create threads , the advantage is that the thread class can also inherit from other classes , and multiple threads can share a thread body, which is suitable for the situation where multiple threads process the same resource . The disadvantage is that programming is a little more cumbersome.
  • - Using inheritance to create threads, the advantage is that programming is a little bit simpler. The disadvantage is that because the thread class has inherited the Thread class, it cannot inherit other parent classes .

Therefore, under normal circumstances, it is more recommended to use the interface method to create threads. If you need to return a value, use the Callable interface, otherwise use the Runnable interface.

14. Talk about your understanding of the thread pool

scoring points

Core parameters, processing flow, rejection policy, life cycle

standard answer

The thread pool can effectively manage threads: manage the number of threads and allow threads to be reused.

The life cycle of the thread pool contains 5 states: RUNNING, SHUTDOWN, STOP, TIDING, TERMINATED. The state values ​​of these five states are: -1, 0, 1, 2, 3. In the life cycle of the thread pool, its state can only be migrated from small to large, which is irreversible.

Five states of the thread pool: running, shutdown, stop, tiding, terminated.

Six states of threads: new runnable blocked waiting timed_waiting terminated

The thread pool can efficiently manage threads:

It can manage the number of threads , and can avoid unrestrained creation of threads, which will cause excessive system load and even crash.

It also allows thread reuse , which can greatly reduce the overhead caused by creating and destroying threads.

The thread pool needs to rely on some parameters to control the execution process of the task. The most important parameters are: corePoolSize (number of core threads), workQueue (waiting queue), maximumPoolSize (maximum number of threads), handler (rejection strategy), keepAliveTime (idle thread survival time). When we submit a task to the thread pool, the thread pool processes the task as follows:

1. Determine whether the number of threads reaches corePoolSize, if not, create a new thread to execute the task, otherwise go to the next step.

2. Determine whether the waiting queue is full, if not, put the task into the waiting queue, otherwise go to the next step.

3. Determine whether the number of threads reaches the maximumPoolSize, if not, create a new thread to execute the task, otherwise go to the next step.

4. Use the rejection policy specified when initializing the thread pool to refuse to execute the task.

5. After the newly created thread finishes processing the current task, it will not be closed immediately, but will continue to process the tasks in the waiting queue.

If the idle time of the thread reaches keepAliveTime, the thread pool will destroy some threads and shrink the number of threads to corePoolSize. The queue in step 2 can be bounded or unbounded. If an unbounded queue is specified, the thread pool will never enter step 3, which is equivalent to discarding the maximumPoolSize parameter. This kind of usage is very dangerous. If a large number of tasks accumulate in the queue, it is easy to cause memory overflow. JDK provides us with a thread pool creation tool called Executors. This tool creates a thread pool with an unbounded queue (without limiting the number of tasks in the waiting queue), so we generally do not recommend it in work. Use this class to create a thread pool. There are mainly four rejection strategies in step 4: let the caller execute the task by itself, throw an exception directly, discard the task without any processing, delete the oldest task in the queue and add the current task to the queue. These four rejection strategies correspond to the four implementation classes of the RejectedExecutionHandler interface, and we can also implement our own rejection strategies based on this interface . In Java, the actual type of thread pool is ThreadPoolExecutor, which provides the general usage of thread pool. This class also has a subclass named ScheduledThreadPoolExecutor, which provides support for scheduled tasks. In subclasses, we can periodically repeat a task, or we can delay executing a task for a certain amount of time.

Bonus answer

The life cycle of the thread pool includes 5 states: RUNNING, SHUTDOWN, STOP, TIDING, TERMINATED. The state values ​​of these five states are: -1, 0, 1, 2, 3. In the life cycle of the thread pool, its state can only be migrated from small to large, which is irreversible.

1. RUNNING: Indicates that the thread pool is running.

2. SHUTDOWN: Enter this state when shutdown() is executed. At this time, the queue will not be cleared , and the thread pool will wait for the task to complete.

3. STOP: Enter this state when shutdownNow() is executed. At this time, the current thread pool will clear the queue and no longer wait for the execution of the task.

4. TIDING : Enter this state when the thread pool and queue are empty . At this time, the thread pool will execute the hook function . Currently, this function is an empty implementation.

5. TERMINATED: After the hook function is executed, the thread enters this state, indicating that the thread pool has died.

15. What thread-safe collections do you know?

scoring points

Collections tool class, java.util.concurrent (JUC)

standard answer

Thread-safe collections include:

  1. The synchronizedXxx() method of the Collections tool class wraps collection classes such as ArrayList into thread-safe collection classes.
  2. Old APIs  with poor performance under the java.util package , such as Vector and Hashtable
  3. Containers starting with Concurrent under the JUC package to reduce lock granularity to improve concurrency performance, such as ConcurrentHashMap.
  4. Concurrent containers starting with CopyOnWrite under the JUC package and implemented by copy-on-write technology, such as CopyOnWriteArrayList.
  5. The blocking queue implemented by Lock creates two Conditions internally for the waiting of producers and consumers respectively. These classes all implement the BlockingQueue interface, such as ArrayBlockingQueue.

 

Thread-safe collections: Collections tool classes and collection classes of the concurrent package.

Collections tool class: The synchronizedXxx() method provided by this tool class can wrap these collection classes into thread-safe collection classes. 

Collection classes of the concurrent package: After java5, you can use a large number of collection classes that support concurrent access provided by the concurrent package, such as ConcurrentHashMap/CopyOnWriteArrayList, etc. Concurrent is translated as simultaneous, concurrent, and concurrent .

Ancient APIs with poor performance: Vector, Hashtable.

Most of the collection classes under the java.util package are non-thread-safe, but there are also a few thread-safe collection classes, such as Vector and Hashtable, which are very old APIs. Although they are thread-safe, their performance is poor and their use has been deprecated. For non-thread-safe collections under this package, you can use the Collections tool class. The synchronizedXxx() method provided by the tool class can package these collection classes into thread-safe collection classes.

Starting from JDK 1.5, a large number of efficient concurrent containers have been added under the concurrent package, and these containers can be divided into three categories according to the implementation mechanism.

The first category is to reduce the lock granularity to improve the concurrent performance of the container, and their class names start with Concurrent, such as ConcurrentHashMap.

The second type is concurrent containers implemented by copy-on-write technology, whose class names start with CopyOnWrite, such as CopyOnWriteArrayList.

The third category is the blocking queue implemented by Lock. Two Conditions are created internally for the waiting of producers and consumers respectively. These classes all implement the BlockingQueue interface, such as ArrayBlockingQueue.

Bonus answer

Collections also provides the following three types of methods to return an immutable collection. The parameters of these three types of methods are the original collection objects, and the return value is the "read-only" version of the collection. Through the three types of methods provided by Collections, a "read-only" Collection or Map can be generated.

emptyXxx(): returns an empty immutable collection object

singletonXxx(): returns an immutable collection object containing only the specified object unmodifiableXxx(): returns an immutable view of the specified collection object 

16. Is HashMap thread-safe? If not how to solve it?

scoring points

Hashtable、Collections、ConcurrentHashMap

standard answer

HashMap is thread-safe, and the underlying implementation is "array, linked list, red-black tree". When multi-threaded put, data may be overwritten, and put will execute modCount++ operation. This step is divided into read, increase, and save. An atomic operation.

Solution:

  1. Using Hashtable (Ancient)
  2. Use the Collections tool class to wrap HashMap into a thread-safe HashMap
  3. Use the safer CurrentHashMap (recommended) . CurrentHashMap ensures thread safety with less performance by locking the bucket .

HashMap is not thread-safe. In a multi-threaded environment, when multiple threads trigger the change of HashMap at the same time, conflicts may occur. Therefore, it is not recommended to use HashMap in a multi-threaded environment.

There are three ways to use a thread-safe HashMap: use Hashtable, use Collections to package HashMap into a thread-safe HashMap, and use ConcurrentHashMap. The third method is the most efficient and is our most recommended method.

Hashtable

Both HashMap and Hashtable are typical Map implementations, and Hashtable is thread-safe. While this is an option, it is not recommended. Because Hashtable is an ancient API that has appeared since Java 1.0, its synchronization scheme is immature, its performance is not good, and even the official advice is not recommended.

Collections

The SynchronizedMap() method is provided in the Collections class , which can wrap the Map we pass in into a thread-synchronized Map. In addition, Collections also provides the following three types of methods to return an immutable collection. The parameters of these three types of methods are the original collection objects, and the return value is the "read-only" version of the collection. Through the three types of methods provided by Collections, a "read-only" Map can be generated. emptyMap(): Returns an empty immutable Map object. singletonMap(): Returns an immutable Map object that contains only the specified key-value pairs. unmodifiableMap() : Returns an immutable view of the specified Map object.

ConcurrentHashMap

ConcurrentHashMap is a thread-safe and efficient HashMap, and it has been upgraded in JDK 8, which further reduces the granularity of locks on the basis of JDK 7, thereby improving the ability of concurrency. In JDK 7, the underlying data structure of ConcurrentHashMap is "array + linked list", but in order to reduce the granularity of locks, JDK7 splits a Map into several sub-Maps, and each sub-Map is called a segment. The multiple segments are independent of each other, and each segment contains several slots. When the data in a segment collides, a linked list structure is used to solve it. When inserting data concurrently, ConcurrentHashMap locks segments, not the entire Map. Because the granularity of the lock is a segment, this mode is also called "segmented lock". In addition, the segment is determined when the container is initialized and cannot be changed later. Each segment can be expanded independently, and each segment does not affect each other, so there is no problem of concurrent expansion. In JDK8, the underlying data structure of ConcurrentHashMap is "array + linked list + red-black tree". However, in order to further reduce the granularity of locks, JDK8 cancels the setting of segments, and stores the linked list or red-black tree directly in the slot of Map. When inserting concurrently, it locks the head node. Compared with the number of segment head nodes, the number of nodes can increase with expansion, so the granularity is smaller. The red-black tree is introduced to improve the efficiency of finding elements in the slot when the conflict is severe.

17. Please tell me about JUC

scoring points

Atomic classes, locks, thread pools, concurrent containers, synchronization tools

standard answer

JUC is the abbreviation of java.util.concurrent. This package contains various tools that support concurrent operations.

1. Atomic class: Under the JUC.Atomic package, it provides many atomic operation methods, following the CAS (Comparison and Replacement) principle. It can be used to solve the thread safety problem of a single variable .

2.Lock lock: Similar to Synchronized, on the basis of including all functions of synchronized, it also supports timeout mechanism and response interrupt mechanism, which is mainly used to solve the thread safety problem of multiple variables .

3. Thread pool: It can manage threads more conveniently, and at the same time avoid the consumption caused by repeated thread opening and killing, with high efficiency.

4. Concurrent containers: such as ConcurrentHashMap, which supports concurrent collections of multi-threaded operations, and is more efficient.

JUC is the abbreviation of java.util.concurrent . This package is a concurrent package provided by JDK 1.5. The package mainly provides various tools that support concurrent operations. These tools are roughly divided into the following five categories: atomic classes, locks, thread pools, concurrent containers, and synchronization tools.

 1. Atomic class Starting from JDK 1.5, the atomic subpackage is provided under the concurrent package. The atomic operation class in this package provides a way to update a variable with simple usage, high performance, and thread safety. A total of 17 classes are provided in the atomic package, belonging to 4 types of atomic update methods, namely atomic update basic type, atomic update reference type, atomic update attribute, and atomic update array.

For the atomic class, it provides many atomic operation methods inside. Such as atomic replacement of integer value, increase of specified value, and addition of 1, the bottom layer of these methods is realized by the CAS atomic instruction provided by the operating system . It can be used to solve the thread safety problem of a single variable .

Taking AtomicInteger as an example, when a thread calls the incrementAndGet() method of the object, it uses CAS to try to modify its value. If no other thread operates the value at this time, the value is modified successfully; otherwise, the CAS operation is repeatedly executed until the modification is successful.

 2. Starting from JDK 1.5, the Lock interface and related implementation classes are added to the concurrent package to implement the lock function. It provides a synchronization function similar to the synchronized keyword, but it needs to be explicitly acquired and released when using it. . Although it lacks the convenience of implicitly acquiring and releasing locks, it has a variety of synchronization features that the synchronized keyword does not have, including: interruptible acquisition of locks, non-blocking acquisition of locks, and timeout acquisition of locks.

 3. Thread pool Starting from JDK 1.5, a built-in thread pool has been added under the concurrent package. Among them, the ThreadPoolExecutor class represents a conventional thread pool, and its subclass ScheduledThreadPoolExecutor provides support for timing tasks. In the subclass, we can periodically repeat a task, or delay a certain time before executing a task. In addition, Executors is a tool class for creating thread pools. Since this class creates thread pools with unbounded queues, be careful when using them.

 4. Concurrent containers Starting from JDK 1.5, a large number of efficient concurrent containers have been added under the concurrent package. These containers can be divided into three categories according to the implementation mechanism. The first category is to reduce the lock granularity to improve the concurrent performance of the container, and their class names start with Concurrent, such as ConcurrentHashMap. The second type is concurrent containers implemented by copy-on-write technology, whose class names start with CopyOnWrite, such as CopyOnWriteArrayList. The third category is the blocking queue implemented by Lock. Two Conditions are created internally for the waiting of producers and consumers respectively. These classes all implement the BlockingQueue interface, such as ArrayBlockingQueue.

 5. Synchronization tools Starting from JDK 1.5, several useful concurrency tool classes have been added under the concurrency package, which can also ensure thread safety. Among them, the Semaphore class represents a semaphore, which can control the number of threads that access a specific resource at the same time; the CountDownLatch class allows one or more threads to wait for other threads to complete operations; CyclicBarrier allows a group of threads to be blocked when they reach a barrier until the last thread When the barrier is reached, the barrier will be opened, and all threads blocked by the barrier will continue to run.

18. Please talk about ConcurrentHashMap

scoring points

Array + linked list + red-black tree, lock granularity

standard answer 

The collection classes of the concurrent package are thread-safe.

ConcurrentHashMap is a class under the JUC ( java.util.concurrent ) concurrency package , which is equivalent to a thread-safe HashMap. 

Array + linked list + red-black tree: The underlying data structure of ConcurrentHashMap is the same as that of HashMap, which also uses "array + linked list + red-black tree"

Lock granularity: The method of locking the head node reduces the lock granularity, and achieves thread safety at a lower performance cost.

Implementation Mechanism:

1. When initializing the array or head node , ConcurrentHashMap does not lock, but performs atomic replacement in CAS mode

2. Hash lookup will perform locking when inserting data , but what is locked is not the entire array, but the head node of the linked list in the slot . Therefore, the granularity of the lock in ConcurrentHashMap is the slot , not the entire array, and the concurrent performance is very good.

3. When the chain address method handles conflicts and expands capacity , it will perform lock processing, and the lock is still the head node . In addition, it supports multiple threads to expand the array at the same time to improve concurrency.

4. In the process of expansion, the search operation can still be supported.

The logic of the underlying data structure can refer to the implementation of HashMap. Below I will focus on its thread-safe implementation mechanism.

1. When initializing the array or head node, ConcurrentHashMap does not lock, but performs atomic replacement in CAS (compare and exchange) mode (atomic operation, atomic operation API based on Unsafe class).

CAS, compare and swap, is a mechanism to solve the performance loss caused by the use of locks in the case of multi-threaded parallelism.

A CAS operation consists of three operands - a memory location (V), an expected old value (A), and a new value (B). If the value of a memory location matches the expected original value, the processor automatically updates the location's value with the new value. Otherwise, the processor does nothing. In either case, it returns the value at that location before the CAS instruction. CAS effectively says "I think location V should contain value A; if it does, put B at this location; otherwise, don't change that location, just tell me what this location is now.

2. When inserting data, lock processing will be performed, but the lock is not the entire array, but the head node in the slot. Therefore, the granularity of the lock in ConcurrentHashMap is the slot, not the entire array, and the concurrent performance is very good.

3. Locking will be performed during expansion, and the head node is still locked. In addition, it supports multiple threads to expand the array at the same time to improve concurrency. Each thread needs to use the CAS operation to grab the task first, and compete for the data transfer right of a continuous slot. After grabbing the task, the thread will lock the head node in the slot, and then migrate the data in the linked list or tree to the new array.

4. There is no lock when searching for data, so the performance is very good. In addition, in the process of expansion, the search operation can still be supported. If a slot has not been migrated, the data can be found directly from the old array. If a slot has been migrated, but the entire expansion is not over, the expansion thread will create a forwarding node and store it in the old array, and then the search thread will find the target data from the new array according to the prompt of the forwarding node.

Bonus answer

The difficulty for ConcurrentHashMap to achieve thread safety lies in the concurrent expansion of multiple threads. That is, when a thread is inserting data, if it finds that the array is expanding, it will immediately participate in the expansion operation, and then insert the data into the new array after the expansion is completed. During expansion, multiple threads share data migration tasks, and the number of migrations each thread is responsible for is `(array length >>> 3) / number of CPU cores`. In other words, the migration tasks assigned to threads fully consider the processing capabilities of the hardware. According to the processing capability of the hardware, multiple threads evenly share a part of the slot migration work. In addition, if the calculated number of migrations is less than 16, it is forced to be changed to 16. This is because considering the current mainstream CPU operating speed in the server field, too few tasks are processed each time, which is also a waste of CPU computing power. 

Guess you like

Origin blog.csdn.net/qq_40991313/article/details/129446871