Parallelism and concurrency
Concurrency: In an operating system, the number of programs running on the same processor in a period of time between start and finish. It refers to multiple things happening at the same time in the same period of time. Concurrent tasks preempt each other’s resources.
Parallel: When the system has more than one CPU, when one CPU executes a process, the other CPU can execute another process. The two processes do not occupy CPU resources, but can run simultaneously. This method is called Parallel. It refers to multiple things happening at the same time. Parallel tasks do not grab resources from each other.
The start() and run() methods
-
Executing the start() method performs the appropriate preparation of the thread, and then automatically executes the contents of the run() method, which is true multithreading.
-
The run() method is executed as a normal method in the main thread, not in a thread, so it’s not multithreading.
Conclusion: The start method is called to start a thread and put it into a ready state, while the run method is just a normal method call on thread and is executed in the main thread.
ThreadLocal
ThreadLocal is a thread variable, and the ThreadLocal class is used to provide local variables within a thread. These variables can be accessed in a multithreaded environment (through get and SET methods) to ensure that the variables of each thread are relatively independent of the variables of other threads. ThreadLocal provides local variables within a thread that do not interfere with one another. These variables operate for the lifetime of the thread, reducing the complexity of passing common variables between functions or components within the same thread. Features: Data transfer: Save the data bound by each thread, can be directly obtained where needed, to avoid the code coupling problem caused by direct parameter transfer thread isolation: data between threads are isolated from each other but with concurrency, to avoid performance loss caused by synchronous mode internal structure:
- Each Thread has an internal instance of ThreadLocalMap, called threadLocals.
- ThreadLocals stores ThreadLocal objects (keys) and thread variables (values).
- A ThreadLocalMap is an internal class maintained by a ThreadLocal, which is responsible for getting and setting the values of thread variables from the Map.
For different threads, each time the copy value is obtained, other threads can not obtain the copy value of the current thread, forming the isolation of copies, mutual interference.
Memory leak:
While ThreadLocal prevents other threads from accessing thread variables in the current thread, it has its own memory leak problems because: In fact, no matter the entry inherits strong or weak references, memory leaks will occur if the entry is not manually deleted and the Current Thread is still running. Therefore, the real cause of memory leaks is as follows: Since ThreadLocalMap has the same lifetime as Thread, memory leaks can result if the corresponding key is not manually removed.
In fact, the set/getEntry method in ThreadLocalMap evaluates if the key is null (ThreadLocal is null), and if it is null, value is set to null. This means that when ThreadLocal is used and currentThreads are still running, even if you forget to call the remove method, weak references are more secure than strong ones: Weak references to ThreadLocal are reclaimed, and the corresponding value is cleared the next time ThreadLocalMap calls any of the set, GET, or remove methods to avoid memory leaks.
There are two ways to avoid memory leaks:
- After using ThreadLocal, call its remove method to remove the corresponding Entry.
- When ThreadLocal is used, the current Thread finishes running.
Set method of ThreadLocalMap:
- First, the index is computed based on the key, and then the Entry in the position is looked up.
- If the Entry already exists and the key is equal to the key passed in, then the Entry is assigned a new value.
- If an Entry exists but the key is null, replaceStaleEntry is called to replace the Entry with an empty key.
- If no return is returned during the loop, create an Entry at the null location and insert it, increasing the size by 1.
Differences between ThreadLocal and synchronized
Although the keyword ThreadLocal and synchronized are both used to deal with the problem of multi-threaded concurrent access to variables, they approach the problem from different perspectives and ideas.
Synchronized | ThreadLocal | |
---|---|---|
The principle of | Time for space, provide only one variable, let different threads queue access | In exchange for time, each thread provides a variable that is thread private |
focus | Synchronization of access to resources between multiple threads | In multithreading, data from each thread is isolated from each other |
While both ThreadLocal and synchronized can solve the problem, using ThreadLocal is more appropriate because it allows for higher concurrency.
Comparison between synchronized and ReentrantLock
Synchronized is a JVM-level lock performed by relying on the JVM. Synchronization was poor prior to 1.6, but after 1.6, the JDK optimized synchronized so that it performed much better. When the resources are not competitive, the performance of ReentrantLock is about the same as that of synchronized. When the resources are competitive, the performance of ReentrantLock is better than that of synchronized. Of course, in the use of time or according to the specific situation of choice.
synchronized VS ReentrantLock:
- Synchronized locking requires the JVM to call the underlying OS for locking, which incurs the overhead of switching from user -> kernel mode. Because ReentrantLock is API level, there is no need to switch resources, that is, from user to kernel mode.
- Synchronized does not require users to manually release the lock, when the synchronized code is finished, the system will automatically release the lock; ReentrantLock requires the user to manually release the lock. Otherwise, a deadlock may occur.
- ReentrantLock provides a mechanism to interrupt a thread waiting for a lock, using lock.lockInterruptibly(). That is, a thread that is waiting can choose to give up waiting and process something else instead. Synchronized cannot be interrupted unless an exception is thrown or a normal operation completes.
- Synchronized is an unfair lock; ReentrantLock can be specified as a fair or an unfair lock. The default is an unfair lock.
- ReentrantLock can be used in conjunction with Condition to implement wait()/notify().
Upgrade process of synchronized lock
Synchronized was optimized after JDK1.6:
- First of all, when the first thread accesses the synchronized code block, it will try to add a biased lock, that is, to the current thread biased function, that is, do not perform complex lock check, etc., in the object header, the biased thread ID is recorded. The next time the thread enters a block of synchronized code, see if the bias ID is its own ID, and if so, just enter it.
- However, if another thread accesses the synchronized resource while the thread is accessing it, that is, if there is a lock contention, the biased lock will be upgraded to a lightweight lock, which is called CAS spin lock, and will continuously acquire the lock through spin operation. Whoever grabs the lock accesses the code block.
- When there are more than two threads competing for the same lock, it is upgraded to a heavyweight lock. In another case, if a thread spins for a long time and spins beyond a certain number of times without successfully obtaining a lock, it will be upgraded to a heavyweight lock. JDK1.6, of course, introduces adaptive spin locks. The spin time is determined by the previous spin time on the same lock and the state of the lock owner. In short, when a thread does not acquire a resource for a long time, it will upgrade to a heavyweight lock, which will be suspended and blocked as long as other threads come in and cannot acquire the lock.
AQS
Abstract queue synchronizer, a framework for building locks and synchronizers, is at the heart of most of the other JUC components, such as ReentrantLock, CountdownLatch, Semaphore, and ReentrantReadWriteLock. Core ideas of AQS: If the requested free sharing of resources, will thread is set to the current request resources effective worker threads, and set the Shared resource is locked, if requested to share resources being used, you will need to a thread is blocked waiting for wake lock allocation mechanism, and the AQS are implemented with CLH lock queue, Queues threads that temporarily cannot acquire locks. (CLH queue is a virtual bidirectional queue. Virtual bidirectional queue means that there is no instance of queue and only the association relationship between nodes. AQS encapsulates each thread requesting shared resources into a Node (Node) of a CLH lock queue to realize lock allocation.
(In common terms, AQS is based on CLH lock queues, and uses volatile to modify the shared variable state. The thread changes the state value through CAS. If the modification succeeds, the thread acquires the lock successfully; if the modification fails, it enters the waiting queue and waits to be awakened.) State is a shared resource, which can be accessed in the following three ways:
- getState()
- setState()
- compareAndSetState()
AQS defines two ways of resource sharing:
- Exclusive: Exclusive. Only one thread can execute this lock, such as ReentrantLock
- Share: a Share that can be executed simultaneously by multiple threads, such as Semaphore, CountDownLatch, ReentrantReadWriteLock, and CycleBarrier
To customize a synchronizer, you need to override the following template methods provided by AQS:
isHeldExclusively()// Whether the thread is monopolizing resources. You only need to implement it if you use condition.
tryAcquire(int)// Exclusive mode. Attempts to obtain the resource return true on success and false on failure.
tryRelease(int)// Exclusive mode. Attempts to free resources return true on success and false on failure.
tryAcquireShared(int)// Share mode. Attempt to obtain resources. Negative numbers indicate failure; 0 indicates success, but no remaining resources are available. A positive number indicates success and free resources.
tryReleaseShared(int)// Share mode. Attempts to free resources return true on success and false on failure.
Copy the code
Java Memory Model (JMM)
In the Java Virtual Machine specification, a Java memory model is defined to shield memory access differences between different hardware and operating systems, so that Java programs running on various platforms can achieve consistent memory access effects. The main goal of the Java memory model, however, is to define the rules for accessing variables in a program, the low-level details of how the JVM stores and retrieves variables from memory. The JMM defines several rules or specifications:
- All shared variables are stored in main memory, which is a shared memory area accessible to all threads.
- Each thread is their working memory, working memory is the private data area each thread, the thread to the operation of the Shared variables must be conducted in the working memory, the first thing to copy the Shared variables from main memory into their working memory space, and then to Shared variables, operation to complete before you write Shared variables to main memory. Shared variables in main memory cannot be manipulated directly; copies of variables in main memory are stored in the working memory of each thread.
- Threads cannot directly access variables in each other’s working memory. Communication between threads must be done through main memory.
Here’s an example: If there are two threads A, B, A Shared memory variable C, if A and B have to be modified to C at the same time, then the process is such, have A look at your thread work space variable C, if you have, change directly, if not, then go to main memory copy of A copy of the C to their working memory, for B is the same operation. If A changes C first and writes it to main memory, and B doesn’t know that A has changed C and writes C to main memory after it changes C, then A’s change record is overwritten. This is where the visibility problem arises, and where volatile comes into play.
volatile
1. What is
Volatile is a lighter synchronization mechanism than synchronized, and in most cases, volatile is cheaper to implement than synchronized. Volatile has the following two characteristics:
- Ensure memory visibility of shared variables;
- Disallow instruction reordering;
2. Memory visibility
A shared variable modified by volatile that requires a copy of the shared variable to be re-read from main memory and re-operated in working memory each time the shared variable is manipulated. Specifically, when one thread makes a change to a volatile variable, when the change is written back to main memory, the other thread immediately sees the latest value.
The visibility of volatile is achieved by using the CPU lock instruction. By prefixing the machine instructions for volatile with the lock prefix, volatile writing has the following two principles:
- Shared variables that are volatile are immediately synchronized to main memory when they are modified.
- The shared variable it decorates is flushed from main memory before each read.
3. Disable command reordering
To improve performance, compilers and processors often reorder instructions from a given code execution order. Generally, reordering can be divided into the following three types:
- Compiler optimized reordering: the compiler can rearrange the execution order of statements without changing the semantics of a single-threaded program;
- Instruction-level parallelism reordering: Modern processors use instruction-level parallelism to overlap multiple instructions. If there is no data dependency, the processor can change the execution order of the corresponding machine instructions.
- Reordering of memory systems: Since the processor uses caching and read/write buffers, it can appear that load and store operations are performed out of order.
How to disallow instruction reordering: The Java compiler inserts a memory barrier instruction in place to disallow a particular type of handler reordering when generating instruction families.
Volatile writes insert memory barriers in front and after:
Volatile reads insert two memory barriers at the end:
synchronized
1. What is it:
Before Jdk 1.6, synchronized was a heavyweight lock and had to deal with the CPU kernel each time it was added, resulting in low performance. However, after Jdk 1.6, biased locking and lightweight locking were introduced in order to reduce performance consumption caused by lock acquisition and release. Synchronized has four types of lock: no lock > bias lock > lightweight lock > heavyweight lock. So, the object starts out unlocked, but as the thread contention escalates, so does the lock, up to the heavyweight lock. However, this lock upgrade strategy cannot be reversed, that is, there is no degraded strategy. Once upgraded to heavyweight lock, there is no way to go back to lightweight lock. The reason for this is also to improve the efficiency of lock acquisition and release.
2. Lock upgrade process (details)
Synchronized locks are stored in Java headers. Array headers are 12 bytes (3 bytes wide) and non-array headers are 8 bytes (2 bytes wide). Synchronized locks are stored in Java headers (3 bytes wide). Let’s take a look at the non-array object header storage structure in 32-bit virtual machines:
The length of the | content | instructions |
---|---|---|
32bit | Mark Word | Store the object’s Hashcode and some lock information |
32bit | Class Metadata Address | Pointer to object type data |
(Note: In the case of an array object, there is also 32bit to store the length of the array.)
We mainly focus on the information in Mark Word, which records different information for different types of locks:
Lock upgrade process:
When we create an object, the lock flag bit is: 01; Biased lock: 0; The current object is biased and is currently unlocked;
At this time, there is A thread A to access the synchronized code block, first look at the lock flag, flag bit is 01, which indicates that there may be no lock state, may also be biased lock state, first determine whether biased lock: The flag bit is 0 -> Thread A directly writes its thread ID into the Mark Word through CAS operation, and changes the flag bit biased to lock to 1 at the same time; If the flag bit is 1 -> check whether the Mark Word record is its own thread ID, if not, through CAS replace the thread ID, if yes, directly obtain bias lock, then execute synchronization block. If thread A accesses the synchronized code block next time, it can check whether it has its own thread ID in Mark Word. If so, it can directly access it, because at this time, the synchronized code block is biased to the thread and there is no other thread to disturb, so there is no lock competition.
One day, thread A is visiting the synchronized code block, then thread B also came to the synchronized code block, it also want to visit, then look at the Mark Word, found that is not his own thread id, then get biased locking by CAS, because threads are accessing A code block, so the thread B nature is failed to get the lock, the competition is present, When the biased lock of the object is invalid, the biased lock should be revoked. The specific process is as follows: After the thread A holding the biased lock reaches the safe point, thread A is suspended to check the thread state of thread A. If it is not in the active state, the biased lock of thread A is released and thread A is awakened. If active, the bias lock is upgraded to a lightweight lock.
Lock flag to 00, upgraded to lightweight locks, after the original hold biased locking thread A lock record in its own thread stack distribution, head of Mark Word into your own copy object is recorded in the lock, and then the object head Mark Word to lock the record pointer to thread A lock record, wake up the thread A, from A security point to continue, Release lightweight lock after executing; Thread B also allocates the lock record and copies the Mark Word to its own lock record. Through CAS operation, the pointer of the lock record in the Mark Word of the object head points to its own lock record. If it succeeds, the lightweight lock will be obtained and the synchronized code block can be executed. Try again.
Thread B, of course, also can’t have been so spin, spin if reach a certain number of times, the CAS operation is still not successful, in order to avoid useless spin, then began to escalate to heavyweight locks, change the lock symbol a 10, when in the heavyweight lock state, the other thread will be blocked when trying to get the lock, when only the thread lock will wake the threads after releasing the lock, The wakened thread then initiates a new lock contention.
3. Underlying principles:
- When synchronized modifies methods, the JVM implements synchronization by adding the ACC_Synchronized flag.
- The JVM uses monitorenter and Monitorexit directives to synchronize with synchronized code blocks, executing Monitorenter on entry and Monitorexit on exit.
The difference between volatile and synchronized
-
Volatile modifies only instance and class variables, while synchronized modifies only methods and code blocks.
-
Volatile ensures visibility of data, but not atomicity (multithreaded writes are not thread-safe). Synchronized is an exclusive (mutually exclusive) mechanism that keeps threads safe. Volatile is used to prohibit instruction reordering and to address out-of-order execution of singleton double-checked object initialization code.
-
Volatile is a lighter synchronization mechanism than synchronized. Volatile does not guarantee atomicity, but can be used instead of synchronized if multiple threads are assigned to a shared variable and nothing else is done. Because assignments are inherently atomic, and volatile guarantees visibility, it is thread-safe.
CountDownLatch, CyclicBarrier, Semaphore
CountDownLatch: Allows one or more threads to wait for other threads to complete an operation. There are the countDown() and await() methods, and CountDownLatch needs to specify a given integer as the counter when initialized. When the countDown() method is called, the counter is decrement by one; When the await() method is called, the thread will block if the counter is greater than 0 and will not continue until the counter is reduced to 0 by the countDown() method. The counter cannot be reset. (Qin destroyed six states and unified China. Do subtraction!
CyclicBarrier: Blocks a group of threads at a barrier (synchronization point) until the last thread reaches the barrier and the barrier opens, allowing the blocked thread to continue running. Threads enter the barrier through the await() method of the CyclicBarrier. Counters can be reset. Collect the seven dragon balls and summon the magic dragon. Do addition!)
Semaphore: Used to control the number of threads accessing a particular resource at the same time. It coordinates threads to ensure proper use of common resources. Two purposes: 1) mutually exclusive use of multiple shared resources and 2) control of the number of concurrent threads. (Fight for parking space!)
ThreadPoolExecutor
The core implementation class of thread pool is mainly composed of four components:
- CorePool: size of the core thread pool,
- MaximumPool: size of the maximum thread pool
- BlockingQueue: a BlockingQueue used to temporarily save tasks.
- ArrayBlockingQueue: An array-based bounded blocking queue.
- LinkedBlockingQueue: An unbounded blocking queue based on a linked list.
- PriorityBlockingQueue: An unbounded blocking queue with a priority.
- SynchronousQueue: A blocking queue that does not store elements.
- RejectedExecutionHandler: Saturation policy. When both the queue and the thread pool are full, the thread pool is saturated and a policy must be adopted to handle the submission of new tasks.
- AbortPolicy: Directly throws an exception
- DiscardPolicy: Does not process, discards.
- DiscardOldestPolicy: Discards the first task in the queue and executes the current task.
- CallerRunsPolicy: Main thread to perform tasks.
Several typical thread pools:
- FixedThreadPool: a thread pool with a fixed number of threads, corePool as large as maximumPool and LinkedBlockingQueue. It is suitable for processing CPU-intensive tasks. It ensures that the CPU is allocated as few threads as possible when it is used by workers for a long time, that is, it is suitable for performing long-term tasks.
- SingleThreadExecutor: A single thread pool for ensuring sequential execution of tasks. The blocking queue is also LinkedBlockingQueue. This method is applicable to serial task execution scenarios.
- CachedThreadPool: a thread pool with an unlimited number of threads that are idle for more than 60 seconds and are reclaimed, blocking as SynchronousQueue. This means that when the main thread is submitting tasks faster than the thread can process them, the thread pool will continue to create threads, and in extreme cases, deplete CPU and memory resources by creating too many threads. Suitable for a large number of short life cycle tasks.