In depth understanding of Java Concurrent locks
1. Introduction to concurrent lock
The most common way to ensure thread safety is to use the lock mechanism (Lock
、sychronized
) To synchronize shared data , So at the same time , Only one thread can execute a method or a block of code , So the operation must be atomic , Thread safe .
at work 、 During the interview , I often hear all kinds of locks , Listen to people in the clouds . There are many conceptual terms of lock , They are aimed at different problems , By simply combing , It's not hard to understand .
1.1. Reentrant lock
Reentrant lock , seeing the name of a thing one thinks of its function , It means that a thread can repeatedly acquire the same lock . That is, the same thread acquires the lock in the outer method , When entering the inner layer, the method will automatically acquire the lock .
Reentrant lock can avoid deadlock to some extent .
ReentrantLock
、ReentrantReadWriteLock
It's a reentrant lock . this , It's not hard to see from its name .synchronized
It's also a reentrant lock .
【 Example 】synchronized
A reentrant example of
synchronized void setA() throws Exception{
Thread.sleep(1000);
setB();
}
synchronized void setB() throws Exception{
Thread.sleep(1000);
}
The above code is a typical scenario : If the lock used is not a reentrant lock ,setB
May not be executed by the current thread , And cause deadlock .
【 Example 】ReentrantLock
A reentrant example of
class Task {
private int value;
private final Lock lock = new ReentrantLock();
public Task() {
this.value = 0;
}
public int get() {
// Get the lock
lock.lock();
try {
return value;
} finally {
// Make sure the lock can be released
lock.unlock();
}
}
public void addOne() {
// Get the lock
lock.lock();
try {
// Be careful : The lock has been successfully acquired here , Get into get After the method , Try to get the lock again ,
// If the lock is not reentrant , Can cause deadlock
value = 1 + get();
} finally {
// Make sure the lock can be released
lock.unlock();
}
}
}
1.2. Fair lock and unfair lock
- Fair lock - Fair lock means Multithreads acquire locks in the order in which they apply for locks .
- Not fair lock - Unfair lock means Multithreading does not acquire locks in the order in which they are requested . This may cause priority reversal ( Later generations live in ) Or hunger ( One thread can't compete with other threads , Resulting in never being able to execute ).
Fair lock to ensure thread application sequence , There must be a performance cost , Therefore, its throughput is generally lower than the unfair lock .
Fair lock and unfair lock stay Java Typical implementation of :
synchronized
Only unfair locks are supported .ReentrantLock
、ReentrantReadWriteLock
, Default is unfair lock , But support fair locks .
1.3. Exclusive lock and shared lock
Exclusive lock and shared lock are a kind of broad sense , In terms of practical use , It is also often called mutually exclusive lock and read-write lock .
- Exclusive lock - Exclusive lock means Locks can only be held by one thread at a time .
- Shared lock - Shared lock means Locks can be held by multiple threads .
Exclusive lock and sharing lock in Java Typical implementation of :
synchronized
、ReentrantLock
Only exclusive locks are supported .ReentrantReadWriteLock
Its writing lock is exclusive lock , The read lock is a shared lock . Read locks are shared locks that make concurrent reads very efficient , Reading and writing , Write to read , The process of writing is mutually exclusive .
1.4. Pessimistic lock and optimistic lock
Optimistic lock and pessimistic lock are not specific lock types , It's a strategy for dealing with concurrent synchronization .
- Pessimistic locking - Pessimistic lock takes a pessimistic attitude towards concurrency , Think : Concurrent operations without locks are bound to go wrong . Pessimistic lock is suitable for frequent write operations .
- Optimism lock - Optimistic lock is optimistic about concurrency , Think : There's no problem with concurrent operations without locks . For concurrent operations of the same data , It won't change . When updating data , The data will be updated in a continuous attempt to update . Optimistic lock is suitable for reading more and writing less .
Pessimistic lock and optimistic lock in Java Typical implementation of :
-
Pessimism is locked in Java The application in is through the use of
synchronized
andLock
Show lock for exclusive synchronization , This is a blocking synchronization . -
Optimism is locked in Java The application in is to use
CAS
Mechanism (CAS
The operation passesUnsafe
Class provides , But this class is not directly exposed as API, So it's all indirect use , Like all kinds of atoms ).
1.5. Biased locking 、 Lightweight lock 、 Heavyweight lock
So called lightweight lock and heavyweight lock , It refers to the thickness of lock control granularity . obviously , The finer the control granularity , The less blocking overhead , The more concurrent it is .
Java 1.6 before , A heavyweight lock generally means synchronized
, And lightweight locks mean volatile
.
Java 1.6 in the future , in the light of synchronized
A lot of optimization , introduce 4 Lock state : No lock state 、 Biased locking 、 Lightweight lock and heavyweight lock . Locks can be upgraded from biased locks to lightweight locks in one direction , Then upgrade from lightweight lock to heavyweight lock .
-
Biased locking - Biased lock means that a piece of synchronous code is always accessed by a thread , Then the thread will automatically acquire the lock . Reduce the cost of lock acquisition .
-
Lightweight lock - When a lock is biased toward a lock , Accessed by another thread , Biased locks will be upgraded to lightweight locks , Other threads try to acquire the lock by spinning , It won't block , Improve performance .
-
Heavyweight lock - When the lock is a lightweight lock , The other thread is spinning , But spin doesn't last , When you spin a certain number of times , We haven't got the lock yet , It's going to go into a block , The lock expands to a heavyweight lock . Heavyweight locks will block other threads applying for it , Performance degradation .
1.6. Section lock
The sectional lock is actually a kind of lock design , It's not a specific kind of lock . The so-called sectional lock , It is to divide the object of the lock into segments , Each segment is controlled independently , Make the lock finer , Reduce blocking overhead , To improve concurrency . It's actually quite understandable , It's like a toll booth on the highway , If there is only one toll gate , Then all the cars can only line up to pay ; If there are multiple toll gates , You can divert .
Hashtable
Use synchronized
Decorate methods to ensure thread safety , So the access to the thread ,Hashtable Will lock the whole object , All other threads can only wait , The throughput of this blocking mode is obviously very low .
Java 1.7 Former ConcurrentHashMap
It's a typical case of segmented lock .ConcurrentHashMap
One was maintained Segment
Array , It's generally called segmented barrel .
final Segment<K,V>[] segments;
When there is thread access ConcurrentHashMap
When the data is ,ConcurrentHashMap
According to hashCode Figure out which bucket the data is in ( Which one Segment), Then lock this Segment
.
1.7. Display locks and built-in locks
Java 1.5 Before , The only mechanism you can use to coordinate access to shared objects is synchronized
and volatile
. Both are built-in locks , That is to say, the application and release of locks are made by JVM Controlled by .
Java 1.5 after , New mechanisms have been added :ReentrantLock
、ReentrantReadWriteLock
, The application and release of such locks can be controlled by procedures , So it's often called a display lock .
Be careful : If you don't need to
ReentrantLock
、ReentrantReadWriteLock
Advanced synchronization features provided , Use should be a prioritysynchronized
. For the following reasons :
- Java 1.6 in the future ,
synchronized
A lot of optimization has been done , Its performance has been compared withReentrantLock
、ReentrantReadWriteLock
Basically flat .- In terms of trend ,Java The future is more likely to be optimized
synchronized
, instead ofReentrantLock
、ReentrantReadWriteLock
, becausesynchronized
yes JVM Built in properties , It can perform some optimizations .ReentrantLock
、ReentrantReadWriteLock
Application and release of lock are controlled by procedure , If not used properly , May cause deadlock , It's very dangerous .
The following comparison shows the difference between the lock and the built-in lock :
- Actively acquire lock and release lock
synchronized
Can't actively acquire lock and release lock . Get lock and release lock are JVM The control of the .ReentrantLock
Can actively acquire lock and release lock .( If you forget to release the lock , A deadlock may occur ).
- In response to interrupt
synchronized
Unresponsive interrupt .ReentrantLock
Can respond to interruptions .
- Timeout mechanism
synchronized
There is no timeout mechanism .ReentrantLock
There's a timeout mechanism .ReentrantLock
Timeout can be set , Automatically release the lock after timeout , Avoid waiting for .
- Support fair locks
synchronized
Only unfair locks are supported .ReentrantLock
Support unfair lock and fair lock .
- Is sharing supported
- By
synchronized
A decorated method or block of code , Can only be accessed by one thread ( exclusive ). If the thread is blocked , Other threads can only wait ReentrantLock
Can be based onCondition
Flexible control of synchronization conditions .
- By
- Is read-write separation supported
synchronized
Read / write lock separation is not supported ;ReentrantReadWriteLock
Read and write lock support , So as to separate the blocking read and write operations , Effectively improve concurrency .
2. Lock and Condition
2.1. Why introduce Lock and Condition
Concurrent programming , There are two core issues : One is mutual exclusion , That is, only one thread is allowed to access shared resources at the same time ; The other is synchronization , That is, how threads communicate with each other 、 Collaboration . These two problems , Management can be solved .Java SDK And the contract is approved Lock and Condition Two interfaces to implement the tube , among Lock Used to solve the problem of mutual exclusion ,Condition Used to solve synchronization problems .
synchronized It's an implementation of tube engineering , In that case , Why offer more Lock and Condition.
JDK 1.6 before ,synchronized It hasn't been optimized yet , Performance is much lower than Lock. however , Performance is not the introduction of Lock The most important factor of . The real key is :synchronized Improper use , There may be deadlock .
synchronized Deadlock cannot be avoided by breaking the non preemptive condition . as a result of synchronized When you apply for resources , If the application does not arrive , Thread is directly in blocking state , And the thread goes into a blocked state , You can't do anything , It can't release the resources already occupied by the thread .
With built-in locks synchronized
The difference is ,Lock
A set of unconditional 、 Pollable 、 Timed and interruptible lock operation , All access locks 、 Releasing locks is an explicit operation .
- Be able to respond to interruptions .synchronized The problem is , Hold lock A after , If you try to acquire a lock B Failure , Then the thread goes into a blocked state , Once a deadlock occurs , There's no chance to wake up a blocked thread . But if the thread in the blocked state can respond to interrupt signals , That is to say, when we send an interrupt signal to a blocked thread , Can wake it up , Then it has a chance to release the lock it once held A. This will destroy the conditions that cannot be seized .
- Support timeout . If the thread does not acquire the lock for a period of time , It's not blocking , It returns an error , Then this thread also has a chance to release the lock it once held . This can also destroy the conditions that cannot be seized .
- Non blocking access to locks . If the attempt to acquire the lock fails , It doesn't get stuck , I'm going straight back , Then this thread also has a chance to release the lock it once held . This can also destroy the conditions that cannot be seized .
2.2. Lock Interface
Lock
The interface of is defined as follows :
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
lock()
- Get the lock .unlock()
- Release the lock .tryLock()
- Attempt to acquire lock , Only if the lock is not held by another thread at the time of the call , To get the lock .tryLock(long time, TimeUnit unit)
- andtryLock()
similar , The difference is only in time , If the lock is not acquired within the limited time , See it as a failure .lockInterruptibly()
- The lock is not held by another thread , And the thread is not interrupted , To get the lock .newCondition()
- Returns a binding toLock
On the objectCondition
example .
2.3. Condition
Condition The conditional variables in the pipe process model are realized .
As mentioned above Lock
Interface There is one newCondition()
Method to return a binding to Lock
On the object Condition
example .Condition
What is it? ? What's the role ? This section will explain one by one .
In a single thread , The execution of a piece of code may depend on a certain state , If the state conditions are not met , The code will not be executed ( Typical scenario , Such as :if ... else ...
). In a concurrent environment , When a thread judges a state condition , Its state may be changed due to the operation of other threads , There needs to be a coordination mechanism to ensure that at the same time , Data can only be modified by one thread lock , And the modified data state is sensed by all threads .
Java 1.5 Before , Mainly to make use of Object
Class wait
、notify
、notifyAll
coordination synchronized
To communicate between threads .
wait
、notify
、notifyAll
Need to cooperate with synchronized
Use , Do not apply to Lock
. While using Lock
The thread of , Communication with each other should use Condition
. This can be understood as , What kind of lock matches what kind of key . Built in lock (synchronized
) With built-in conditional queues (wait
、notify
、notifyAll
), Explicit lock (Lock
) With explicit conditional queues (Condition
).
Condition Characteristics of
Condition
The interface is defined as follows :
public interface Condition {
void await() throws InterruptedException;
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
void signal();
void signalAll();
}
among ,await
、signal
、signalAll
And wait
、notify
、notifyAll
Corresponding , Functions are similar . in addition to ,Condition
Compared to the built-in conditional queue ( wait
、notify
、notifyAll
), It provides more functions :
- Each lock (
Lock
) There can be more than oneCondition
, This means that there can be more than one lock state condition . - Support fair or unfair queue operations .
- Supports interruptible conditional waiting , Related methods :
awaitUninterruptibly()
. - Support timed waiting , Related methods :
awaitNanos(long)
、await(long, TimeUnit)
、awaitUntil(Date)
.
Condition Usage of
Here we use Condition
To achieve a consumer 、 Producer mode .
Product class
class Message {
private final Lock lock = new ReentrantLock();
private final Condition producedMsg = lock.newCondition();
private final Condition consumedMsg = lock.newCondition();
private String message;
private boolean state;
private boolean end;
public void consume() {
//lock
lock.lock();
try {
// no new message wait for new message
while (!state) { producedMsg.await(); }
System.out.println("consume message : " + message);
state = false;
// message consumed, notify waiting thread
consumedMsg.signal();
} catch (InterruptedException ie) {
System.out.println("Thread interrupted - viewMessage");
} finally {
lock.unlock();
}
}
public void produce(String message) {
lock.lock();
try {
// last message not consumed, wait for it be consumed
while (state) { consumedMsg.await(); }
System.out.println("produce msg: " + message);
this.message = message;
state = true;
// new message added, notify waiting thread
producedMsg.signal();
} catch (InterruptedException ie) {
System.out.println("Thread interrupted - publishMessage");
} finally {
lock.unlock();
}
}
public boolean isEnd() {
return end;
}
public void setEnd(boolean end) {
this.end = end;
}
}
consumer
class MessageConsumer implements Runnable {
private Message message;
public MessageConsumer(Message msg) {
message = msg;
}
@Override
public void run() {
while (!message.isEnd()) { message.consume(); }
}
}
producer
class MessageProducer implements Runnable {
private Message message;
public MessageProducer(Message msg) {
message = msg;
}
@Override
public void run() {
produce();
}
public void produce() {
List<String> msgs = new ArrayList<>();
msgs.add("Begin");
msgs.add("Msg1");
msgs.add("Msg2");
for (String msg : msgs) {
message.produce(msg);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
message.produce("End");
message.setEnd(true);
}
}
test
public class LockConditionDemo {
public static void main(String[] args) {
Message msg = new Message();
Thread producer = new Thread(new MessageProducer(msg));
Thread consumer = new Thread(new MessageConsumer(msg));
producer.start();
consumer.start();
}
}
3. ReentrantLock
ReentrantLock
Class is Lock
The concrete implementation of the interface , With built-in locks synchronized
The same thing , It's a reentrant lock .
3.1. ReentrantLock Characteristics of
ReentrantLock
Its characteristics are as follows :
ReentrantLock
Provided withsynchronized
Same mutex 、 Memory visibility and reentrancy .ReentrantLock
Support fair locks and unfair locks ( Default ) Two modes .ReentrantLock
RealizedLock
Interface , Supportsynchronized
Flexibility that you don't have .synchronized
Unable to interrupt a thread waiting to acquire a locksynchronized
Can't wait endlessly while requesting a lock
3.2. ReentrantLock Usage of
I've learned about ReentrantLock
Characteristics of , Next , Let's talk about its specific usage .
ReentrantLock Construction method of
ReentrantLock
There are two constructors :
public ReentrantLock() {}
public ReentrantLock(boolean fair) {}
ReentrantLock()
- The default constructor initializes an unfair lock (NonfairSync);ReentrantLock(boolean)
-new ReentrantLock(true)
A fair lock will be initialized (FairSync).
lock and unlock Method
lock()
- Acquire lock unconditionally . If the current thread cannot acquire the lock , Then the current thread is not available for hibernation , Until the current thread gets the lock . If the lock is not held by another thread , Then acquire the lock and return immediately , Set the hold count of the lock to 1.unlock()
- Used to release locks .
:bell: Be careful : Please remember , Acquire lock operation
lock()
Must be intry catch
Out of the block , And will release the lock operationunlock()
Put it infinally
Carry out in block , To ensure that the lock must be released , Prevent deadlock .
Example :ReentrantLock
Basic operation
public class ReentrantLockDemo {
public static void main(String[] args) {
Task task = new Task();
MyThread tA = new MyThread("Thread-A", task);
MyThread tB = new MyThread("Thread-B", task);
MyThread tC = new MyThread("Thread-C", task);
tA.start();
tB.start();
tC.start();
}
static class MyThread extends Thread {
private Task task;
public MyThread(String name, Task task) {
super(name);
this.task = task;
}
@Override
public void run() {
task.execute();
}
}
static class Task {
private ReentrantLock lock = new ReentrantLock();
public void execute() {
lock.lock();
try {
for (int i = 0; i < 3; i++) {
System.out.println(lock.toString());
// Query the current thread hold The number of times to stay in this lock
System.out.println("\t holdCount: " + lock.getHoldCount());
// Query the number of threads waiting to acquire this lock
System.out.println("\t queuedLength: " + lock.getQueueLength());
// Is it a fair lock
System.out.println("\t isFair: " + lock.isFair());
// Is it locked
System.out.println("\t isLocked: " + lock.isLocked());
// Whether the lock is held by the current thread
System.out.println("\t isHeldByCurrentThread: " + lock.isHeldByCurrentThread());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
lock.unlock();
}
}
}
}
Output results :
[email protected][Locked by thread Thread-A]
holdCount: 1
queuedLength: 2
isFair: false
isLocked: true
isHeldByCurrentThread: true
[email protected][Locked by thread Thread-C]
holdCount: 1
queuedLength: 1
isFair: false
isLocked: true
isHeldByCurrentThread: true
// ...
tryLock Method
Compared with unconditional access lock ,tryLock There is a better fault tolerance mechanism .
tryLock()
- Can poll for lock . If it works , Then return to true; If you fail , Then return to false. in other words , Whether this method succeeds or fails, it will return immediately , Can't get lock ( The lock has been acquired by another thread ) Not always waiting for .tryLock(long, TimeUnit)
- The lock can be acquired regularly . andtryLock()
similar , The only difference is that this method will wait for a certain amount of time when the lock cannot be acquired , If the lock is not acquired within the time limit , Just go back to false. If you get the lock at the beginning or during the waiting period , Then return to true.
Example :ReentrantLock
Of tryLock()
operation
Modify... In the previous example execute()
Method
public void execute() {
if (lock.tryLock()) {
try {
for (int i = 0; i < 3; i++) {
// A little ...
}
} finally {
lock.unlock();
}
} else {
System.out.println(Thread.currentThread().getName() + " Lock acquisition failed ");
}
}
Example :ReentrantLock
Of tryLock(long, TimeUnit)
operation
Modify... In the previous example execute()
Method
public void execute() {
try {
if (lock.tryLock(2, TimeUnit.SECONDS)) {
try {
for (int i = 0; i < 3; i++) {
// A little ...
}
} finally {
lock.unlock();
}
} else {
System.out.println(Thread.currentThread().getName() + " Lock acquisition failed ");
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " Acquire lock timeout ");
e.printStackTrace();
}
}
lockInterruptibly Method
lockInterruptibly()
- Interruptible access lock . The interruptible acquisition lock can maintain the response to the interrupt while acquiring the lock . Interruptible access lock is a little more complicated than other ways of access lock , You need twotry-catch
block ( If it is thrown during the lock acquisition operationInterruptedException
, Then you can use standardtry-finally
Lock mode ).- for instance : Suppose that two threads pass through at the same time
lock.lockInterruptibly()
When acquiring a lock , If a thread A Got the lock , The thread B Can only wait for . If at this time on the thread B callthreadB.interrupt()
Method to interrupt threads B The waiting process . becauselockInterruptibly()
An exception was thrown in the declaration of , thereforelock.lockInterruptibly()
Must be ontry
Block or callinglockInterruptibly()
The method declaration ofInterruptedException
.
- for instance : Suppose that two threads pass through at the same time
:bell: Be careful : When a thread gets a lock , Will not be
interrupt()
Method interrupt . Call aloneinterrupt()
Method cannot interrupt a running thread , Only threads in blocked state can be interrupted . So when we passlockInterruptibly()
Method to acquire a lock , If the lock is not acquired , Only in the state of waiting , To respond to an interrupt .
Example :ReentrantLock
Of lockInterruptibly()
operation
Modify... In the previous example execute()
Method
public void execute() {
try {
lock.lockInterruptibly();
for (int i = 0; i < 3; i++) {
// A little ...
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " Interrupted ");
e.printStackTrace();
} finally {
lock.unlock();
}
}
newCondition Method
newCondition()
- Returns a binding to Lock
On the object Condition
example .
3.3. ReentrantLock Principle
ReentrantLock The visibility of
class X {
private final Lock rtl =
new ReentrantLock();
int value;
public void addOne() {
// Get the lock
rtl.lock();
try {
value+=1;
} finally {
// Make sure the lock can be released
rtl.unlock();
}
}
}
ReentrantLock, Hold one internally volatile Member variables of state, When getting the lock , Can read and write state Value ; When you unlock , I can read and write state Value ( The simplified code is shown below ). in other words , In execution value+=1 Before , The program reads and writes once volatile Variable state, In execution value+=1 after , Read and write again volatile Variable state. According to the relevant Happens-Before The rules :
- Sequential rules : For threads T1,value+=1 Happens-Before Release lock operation unlock();
- volatile Variable rule : because state = 1 Read first state, So threads T1 Of unlock() operation Happens-Before Threads T2 Of lock() operation ;
- Transitive rules : Threads T1 Of value+=1 Happens-Before Threads T2 Of lock() operation .
ReentrantLock Data structure of
read ReentrantLock
Source code , You can see that it has a core field :
private final Sync sync;
sync
- Internal abstract classReentrantLock.Sync
object ,Sync
Inherited from AQS. It has two subclasses :ReentrantLock.FairSync
- Fair lock .ReentrantLock.NonfairSync
- Not fair lock .
Check the source code to find out ,ReentrantLock
Realization Lock
An interface is actually a call ReentrantLock.FairSync
or ReentrantLock.NonfairSync
The realization of each , Here's not a list .
ReentrantLock Get lock and release lock
ReentrantLock Get lock and release lock interface , From the appearance , Is to call ReentrantLock.FairSync
or ReentrantLock.NonfairSync
The realization of each ; In essence , Is based on AQS The implementation of the .
It's easy to read the source code carefully :
-
void lock()
call Sync Of lock() Method . -
void lockInterruptibly()
Call directly AQS Of Get an exclusive lock that can be interrupted MethodlockInterruptibly()
. -
boolean tryLock()
call Sync OfnonfairTryAcquire()
. -
boolean tryLock(long time, TimeUnit unit)
Call directly AQS Of Get the exclusive lock of timeout wait type MethodtryAcquireNanos(int arg, long nanosTimeout)
. -
void unlock()
Call directly AQS Of Release exclusive lock Methodrelease(int arg)
.
Call directly AQS The interface method will not be repeated , The principle is [AQS Principle ](#AQS Principle ) I've explained it in a lot of space .
nonfairTryAcquire
The source code of the method is as follows :
// Both fair locks and unfair locks attempt to acquire locks using this method area
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
// If the synchronization state is 0, Set it to acquires, And set the current thread as exclusive thread
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
The process is simple :
- If the synchronization state is 0, Set the synchronization state to acquires, And set the current thread as exclusive thread , Then return true, Lock acquired successfully .
- If the synchronization state is not 0 And the current thread is exclusive , Set the synchronization state to the current state value +acquires value , Then return true, Lock acquired successfully .
- otherwise , return false, Lock acquisition failed .
Fair and non-fair locks
ReentrantLock This class has two constructors , One is the parameterless constructor , One is the introduction fair Constructor for parameter .fair The parameter represents the fair policy of the lock , If you pass in true It means that we need to construct a fair lock , Otherwise, it means to construct an unfair lock .
Locks all correspond to a waiting queue , If a thread does not get a lock , It's going to enter the waiting line , When a thread releases a lock , You need to wake up a waiting thread from the waiting queue . If it's a fair lock , The wake-up strategy is who waits long , Wake up who , Very fair ; If it's unfair lock , There is no guarantee of fairness , It is possible that the thread with a short waiting time will be awakened first .
lock Methods in fair lock and unfair lock :
The difference between them is only when applying for unfair lock , If the synchronization state is 0, Try setting it to 1, If it works , Directly set the current thread as the exclusive thread ; Otherwise, it's like a fair lock , call AQS Get exclusive lock method acquire
.
// Unfair lock implementation
final void lock() {
if (compareAndSetState(0, 1))
// If the synchronization state is 0, Set it to 1, And set the current thread as exclusive thread
setExclusiveOwnerThread(Thread.currentThread());
else
// call AQS Get exclusive lock method acquire
acquire(1);
}
// Fair lock implementation
final void lock() {
// call AQS Get exclusive lock method acquire
acquire(1);
}
4. ReentrantReadWriteLock
ReadWriteLock
It is suitable for reading more and writing less .
ReentrantReadWriteLock
Class is ReadWriteLock
The concrete implementation of the interface , It is a re - entry read - write lock .ReentrantReadWriteLock
Maintain a pair of read-write locks , Separate the read and write locks , It can improve the concurrent efficiency .
Read-write lock , Not at all Java Language specific , It's a general technology that is widely used , All read-write locks follow the following three basic principles :
- Allow multiple threads to read shared variables at the same time ;
- Only one thread is allowed to write shared variables ;
- If a write thread is performing a write operation , At this time, it is forbidden to read shared variables by the read thread .
An important difference between read-write lock and mutex lock is that read-write lock allows multiple threads to read shared variables simultaneously , Mutexes are not allowed , This is the key to the better performance of read-write locks than mutexes in read more write less scenarios . But the write operations of read-write locks are mutually exclusive , When a thread is writing shared variables , Other threads are not allowed to perform write and read operations .
4.1. ReentrantReadWriteLock Characteristics of
ReentrantReadWriteLock Its characteristics are as follows :
ReentrantReadWriteLock
It is suitable for reading more and writing less . If it's writing more and reading less , becauseReentrantReadWriteLock
Its internal implementation ratioReentrantLock
complex , The performance may be worse . If there is such a problem , Need specific analysis . becauseReentrantReadWriteLock
Read and write lock of (ReadLock
、WriteLock
) It's all doneLock
Interface , So replace it withReentrantLock
It's easier .ReentrantReadWriteLock
RealizedReadWriteLock
Interface , SupportReentrantLock
Do not have the read-write lock separation .ReentrantReadWriteLock
Maintain a pair of read-write locks (ReadLock
、WriteLock
). Separate the read and write locks , It can improve the concurrent efficiency .ReentrantReadWriteLock
The lock strategy of is : Allow multiple read operations to execute concurrently , But only one write operation is allowed at a time .ReentrantReadWriteLock
It provides reentrant lock semantics for both read and write locks .ReentrantReadWriteLock
Support fair locks and unfair locks ( Default ) Two modes .
ReadWriteLock
The interface is defined as follows :
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
readLock
- Returns the lock used for the read operation (ReadLock
).writeLock
- Returns the lock used for the write operation (WriteLock
).
The interaction between the read-write lock and the write lock can be realized in many ways ,ReadWriteLock
Some of the optional implementations of include :
- Release priority - When a write operation releases a write lock , And there are both read and write threads in the queue , Then read thread should be selected first 、 Write a thread , The first thread to make the request ?
- Read thread queue jumping - If the lock is held by a read thread , But there are write threads waiting , Then whether the newly arrived read thread can obtain access right immediately , Or should wait behind the write thread ? If the read thread is allowed to queue before the write thread , That will improve concurrency , But it may cause thread starvation .
- Reentrant - Whether the read lock and write lock are reentrant ?
- Downgrade - If a thread holds a write lock , Then whether it can obtain the read lock without releasing the lock ? This may degrade the write lock to read lock , At the same time, other write threads are not allowed to modify the protected resources .
- upgrade - Can a read lock be upgraded to a write lock prior to other waiting read and write threads ? Upgrade is not supported in most read-write lock implementations , Because if there is no explicit upgrade operation , It's easy to cause deadlock .
4.2. ReentrantReadWriteLock Usage of
I've learned about ReentrantReadWriteLock
Characteristics of , Next , Let's talk about its specific usage .
ReentrantReadWriteLock Construction method of
ReentrantReadWriteLock
and ReentrantLock
equally , There are also two construction methods , And the usage is similar .
public ReentrantReadWriteLock() {}
public ReentrantReadWriteLock(boolean fair) {}
ReentrantReadWriteLock()
- The default constructor initializes an unfair lock (NonfairSync). In an unfair lock , The order in which threads acquire locks is uncertain . It is possible to downgrade a write thread to a read thread , But it is not allowed to upgrade the read thread to the write thread ( This can lead to deadlock ).ReentrantReadWriteLock(boolean)
-new ReentrantLock(true)
A fair lock will be initialized (FairSync). For fair locks , The thread with the longest wait time will get the lock first . If the lock is held by the read thread , Then another thread requests a write lock , Then no other read thread can get a read lock , Until the write thread releases the write lock .
ReentrantReadWriteLock Use case of
stay ReentrantReadWriteLock
Characteristics of Has been introduced in ,ReentrantReadWriteLock
Read and write lock of (ReadLock
、WriteLock
) It's all done Lock
Interface , So they are independent of each other ReentrantLock
equally , No more details here .
ReentrantReadWriteLock
And ReentrantLock
Differences in usage , It mainly depends on the use of read-write lock . This article uses a typical scenario to explain .
【 Example 】 be based on ReadWriteLock
Implement a simple generic unbounded cache
/**
* Simple implementation of unbounded cache
* <p>
* Use WeakHashMap Store key value pairs .WeakHashMap Objects stored in are weak references ,JVM GC It will automatically clear the weak reference objects that are not referenced .
*/
static class UnboundedCache<K, V> {
private final Map<K, V> cacheMap = new WeakHashMap<>();
private final ReadWriteLock cacheLock = new ReentrantReadWriteLock();
public V get(K key) {
cacheLock.readLock().lock();
V value;
try {
value = cacheMap.get(key);
String log = String.format("%s Reading data %s:%s", Thread.currentThread().getName(), key, value);
System.out.println(log);
} finally {
cacheLock.readLock().unlock();
}
return value;
}
public V put(K key, V value) {
cacheLock.writeLock().lock();
try {
cacheMap.put(key, value);
String log = String.format("%s Write data %s:%s", Thread.currentThread().getName(), key, value);
System.out.println(log);
} finally {
cacheLock.writeLock().unlock();
}
return value;
}
public V remove(K key) {
cacheLock.writeLock().lock();
try {
return cacheMap.remove(key);
} finally {
cacheLock.writeLock().unlock();
}
}
public void clear() {
cacheLock.writeLock().lock();
try {
this.cacheMap.clear();
} finally {
cacheLock.writeLock().unlock();
}
}
}
explain :
- Use
WeakHashMap
instead ofHashMap
To store key value pairs .WeakHashMap
Objects stored in are weak references ,JVM GC It will automatically clear the weak reference objects that are not referenced . - towards
Map
Add write lock before writing data , After finish , Release the write lock . - towards
Map
Add read lock before reading data , After reading , Release read lock .
Test its thread security :
/**
* @author <a href="mailto:[email protected]">Zhang Peng</a>
* @since 2020-01-01
*/
public class ReentrantReadWriteLockDemo {
static UnboundedCache<Integer, Integer> cache = new UnboundedCache<>();
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 20; i++) {
executorService.execute(new MyThread());
cache.get(0);
}
executorService.shutdown();
}
/** The thread task writes... To the cache every time 3 A random value ,key Fix */
static class MyThread implements Runnable {
@Override
public void run() {
Random random = new Random();
for (int i = 0; i < 3; i++) {
cache.put(i, random.nextInt(100));
}
}
}
}
explain : Example , Start from thread pool 20 Concurrent tasks . The task writes... To the cache every time 3 A random value ,key Fix ; Then the main thread reads the first one in the cache every time key Value .
Output results :
main Reading data 0:null
pool-1-thread-1 Write data 0:16
pool-1-thread-1 Write data 1:58
pool-1-thread-1 Write data 2:50
main Reading data 0:16
pool-1-thread-1 Write data 0:85
pool-1-thread-1 Write data 1:76
pool-1-thread-1 Write data 2:46
pool-1-thread-2 Write data 0:21
pool-1-thread-2 Write data 1:41
pool-1-thread-2 Write data 2:63
main Reading data 0:21
main Reading data 0:21
// ...
4.3. ReentrantReadWriteLock Principle
I've learned about ReentrantLock
Principle , understand ReentrantReadWriteLock
It's much easier .
ReentrantReadWriteLock Data structure of
read ReentrantReadWriteLock Source code , You can see that it has three core fields :
/** Inner class providing readlock */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;
/** Performs all synchronization mechanics */
final Sync sync;
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
sync
- Inner classReentrantReadWriteLock.Sync
object . AndReentrantLock
similar , It has two subclasses :ReentrantReadWriteLock.FairSync
andReentrantReadWriteLock.NonfairSync
, It represents the realization of fair lock and unfair lock respectively .readerLock
- Inner classReentrantReadWriteLock.ReadLock
object , This is a reading lock .writerLock
- Inner classReentrantReadWriteLock.WriteLock
object , This is a writing lock .
ReentrantReadWriteLock Get lock and release lock
public static class ReadLock implements Lock, java.io.Serializable {
// call AQS Get shared lock method
public void lock() {
sync.acquireShared(1);
}
// call AQS Release shared lock method
public void unlock() {
sync.releaseShared(1);
}
}
public static class WriteLock implements Lock, java.io.Serializable {
// call AQS Get exclusive lock method
public void lock() {
sync.acquire(1);
}
// call AQS Release exclusive lock method
public void unlock() {
sync.release(1);
}
}
5. StampedLock
ReadWriteLock Two modes are supported : One is read lock , One is a write lock . and StampedLock Three modes are supported , Namely : Write lock 、 Pessimistic read lock and optimistic read . among , Write lock 、 The semantics of pessimistic read lock and ReadWriteLock Write lock of 、 The semantics of read locks are very similar , Allow multiple threads to acquire pessimistic read locks at the same time , But only one thread is allowed to get the write lock , Write lock and pessimistic read lock are mutually exclusive . The difference is :StampedLock After the write lock and pessimistic read lock are locked successfully , Will return to one stamp; And then when it's unlocked , Need to pass in this stamp.
Note that there , It's using “ Read optimistically ” The word , instead of “ Optimistic reading lock ”, To remind you of , Optimistic reading this operation is unlocked , So compared to ReadWriteLock Read lock , The performance of optimistic reading is better .
StampedLock The performance of is better than ReadWriteLock Still better , The key is StampedLock The way to support optimistic reading .
- ReadWriteLock Support multiple threads to read at the same time , But when multiple threads read at the same time , All writes will be blocked ;
- and StampedLock Provide optimistic reading , It allows a thread to acquire a write lock , In other words, not all write operations are blocked .
For reading more and writing less StampedLock Performance is very good , Simple application scenarios can basically replace ReadWriteLock, however StampedLock Its function is just ReadWriteLock Subset , In use , There are still a few places to pay attention to .
- StampedLock Reentry... Is not supported
- StampedLock Read the lock of pessimism 、 Write locks do not support conditional variables .
- If the thread is blocked in StampedLock Of readLock() perhaps writeLock() Upper time , At this point, call the blocking thread's interrupt() Method , It can lead to CPU soaring . Use StampedLock Do not call interrupt operation , If you need to support interrupt function , Be sure to use interruptible pessimistic read locks readLockInterruptibly() And write lock writeLockInterruptibly().
【 Example 】StampedLock Blocking time , call interrupt() Lead to CPU soaring
final StampedLock lock
= new StampedLock();
Thread T1 = new Thread(()->{
// Get write lock
lock.writeLock();
// It's stuck here forever , Do not release the write lock
LockSupport.park();
});
T1.start();
// Guarantee T1 Get write lock
Thread.sleep(100);
Thread T2 = new Thread(()->
// Blocking in pessimistic read lock
lock.readLock()
);
T2.start();
// Guarantee T2 Blocking in the read lock
Thread.sleep(100);
// Interrupt threads T2
// Will cause threads T2 Where CPU soaring
T2.interrupt();
T2.join();
【 Example 】StampedLock Read the template :
final StampedLock sl =
new StampedLock();
// Read optimistically
long stamp =
sl.tryOptimisticRead();
// Read in method local variables
......
// check stamp
if (!sl.validate(stamp)){
// Upgrade to pessimistic read lock
stamp = sl.readLock();
try {
// Read in method local variables
.....
} finally {
// Release pessimistic reading lock
sl.unlockRead(stamp);
}
}
// Use method local variables to perform business operations
......
【 Example 】StampedLock Write template :
long stamp = sl.writeLock();
try {
// Write shared variables
......
} finally {
sl.unlockWrite(stamp);
}
6. AQS
AbstractQueuedSynchronizer
( abbreviation AQS) Is the queue synchronizer , seeing the name of a thing one thinks of its function , Its main function is to deal with synchronization . It is the cornerstone of concurrent locks and many synchronization utility classes ( Such asReentrantLock
、ReentrantReadWriteLock
、CountDownLatch
、Semaphore
、FutureTask
etc. ).
6.1. AQS Main points
AQS Provides support for exclusive locks and shared locks .
stay java.util.concurrent.locks
The related lock in the bag ( Commonly used ReentrantLock
、 ReadWriteLock
) It's all based on AQS To achieve . None of these locks inherit directly AQS, It defines a Sync
Class to inherit AQS. Why do you do this ? Because the lock is for the user , And synchronizer is oriented to thread control , Then aggregate synchronizers instead of direct inheritance in lock implementation AQS Can be a good separation of the two concerns .
6.2. AQS Application
AQS Provides support for exclusive locks and shared locks .
Exclusive lock API
obtain 、 The main thing about releasing the exclusive lock API as follows :
public final void acquire(int arg)
public final void acquireInterruptibly(int arg)
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
public final boolean release(int arg)
acquire
- Acquire exclusive lock .acquireInterruptibly
- Get an exclusive lock that can be interrupted .tryAcquireNanos
- Try to get an interruptible exclusive lock within a specified time . Return to... In three cases :- Within the timeout period , The current thread successfully acquired the lock ;
- The current thread is interrupted during the timeout ;
- End of timeout , Still not get lock back false.
release
- Release exclusive lock .
Shared lock API
obtain 、 The main thing about releasing shared locks API as follows :
public final void acquireShared(int arg)
public final void acquireSharedInterruptibly(int arg)
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
public final boolean releaseShared(int arg)
acquireShared
- Get shared lock .acquireSharedInterruptibly
- Get an interruptible shared lock .tryAcquireSharedNanos
- Try to obtain an interruptible shared lock within a specified time .release
- Release the shared lock .
6.3. AQS Principle
ASQ Principle points :
- AQS Use an integer
volatile
Variable to Maintain synchronization status . The meaning of a state is given by subclasses .- AQS One was maintained FIFO Double linked list of , Thread used to store failed to get lock .
AQS Provides two basic operations around the synchronization state “ obtain ” and “ Release ”, And provide a series of judgment and treatment methods , A few simple points :
- state It's exclusive , It's still shared ;
- state After being captured , Other threads need to wait ;
- state After being released , Wake up waiting thread ;
- The thread is not in time , How to quit waiting .
As to whether the thread can get state, How to release state, It's not AQS Concerned about , To be implemented by subclasses .
AQS Data structure of
read AQS Source code , You can find :AQS Inherited from AbstractOwnableSynchronize
.
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
/** Waiting for the head of the line , Lazy loading . Only through setHead Methods to modify . */
private transient volatile Node head;
/** Wait for the end of the line , Lazy loading . Only through enq Method to add a new waiting node .*/
private transient volatile Node tail;
/** sync */
private volatile int state;
}
state
- AQS Use an integervolatile
Variable to Maintain synchronization status .- The meaning of this integer state is given by subclasses , Such as
ReentrantLock
The state value indicates the number of times the owner thread has repeatedly acquired the lock ,Semaphore
The status value in represents the remaining number of licenses .
- The meaning of this integer state is given by subclasses , Such as
head
andtail
- AQS One was maintainedNode
type (AQS The inner class of ) Double linked list to complete the synchronous state management . This double linked list is a two-way FIFO queue , adopthead
andtail
Pointer to access . When After a thread fails to acquire the lock , Is added to the end of the queue .
Let's take a look at Node
Source code
static final class Node {
/** The node waiting for synchronization is in shared mode */
static final Node SHARED = new Node();
/** The node waiting for synchronization is in exclusive mode */
static final Node EXCLUSIVE = null;
/** Thread wait state , The state values are : 0、1、-1、-2、-3 */
volatile int waitStatus;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
/** Precursor node */
volatile Node prev;
/** The subsequent nodes */
volatile Node next;
/** Thread waiting for lock */
volatile Thread thread;
/** It's related to whether the nodes share */
Node nextWaiter;
}
Obviously ,Node It's a double linked list structure .
waitStatus
-Node
Use an integervolatile
Variable to maintain AQS Synchronize the status of thread nodes in the queue .waitStatus
There are five states :CANCELLED(1)
- This state indicates : The thread of this node may be timeout or interrupted Being cancelled ( To void ) state , Once in this state , Indicates that this node should be removed from the waiting queue .SIGNAL(-1)
- This state indicates : Subsequent nodes will be suspended , So after the current node releases the lock or is cancelled , Must wake up (unparking
) Its successor node .CONDITION(-2)
- This state indicates : The thread of this node In a waiting condition , It will not be treated as a node on the synchronization queue , Until awakened (signal
), Set the value to 0, And then re-enter the blocking state .PROPAGATE(-3)
- This state indicates : nextacquireShared
It should spread unconditionally .- 0 - Not above .
Acquisition and release of exclusive lock
Acquire exclusive lock
AQS Use in acquire(int arg)
Method get exclusive lock , The general process is as follows :
- Try to get synchronization status first , If the synchronization status is obtained successfully , Then the end method , Go straight back to .
- If getting synchronization status is not successful ,AQS Will keep trying to make use of CAS Operation inserts the current thread into the end of the queue waiting for synchronization , Until we succeed .
- next , Keep trying to acquire exclusive lock for thread node in waiting queue .
The detailed process can be shown in the figure below , Please combine the source code to understand ( A picture is worth a thousand words ):
Release exclusive lock
AQS Use in release(int arg)
Method to release the exclusive lock , The general process is as follows :
- First try to get the synchronization status of the unlocked thread , If getting synchronization status is not successful , Then the end method , Go straight back to .
- If the synchronization status is obtained successfully ,AQS Will try to wake up the successor node of the current thread node .
Get an exclusive lock that can be interrupted
AQS Use in acquireInterruptibly(int arg)
Method to obtain an interruptible exclusive lock .
acquireInterruptibly(int arg)
Compared with the method of acquiring exclusive lock ( acquire
) Very similar , The only difference is that it will pass through Thread.interrupted
Detect whether the current thread is interrupted , If it is , Throw an interrupt exception immediately (InterruptedException
).
Get the exclusive lock of timeout wait type
AQS Use in tryAcquireNanos(int arg)
Method to obtain an exclusive lock for a timeout wait .
doAcquireNanos How to implement Compared to acquiring exclusive lock method ( acquire
) Very similar , The difference is that it calculates the deadline based on the timeout and the current time . In the process of acquiring lock , Will continue to determine whether the timeout , If the timeout , Go straight back to false; If it doesn't time out , Then use LockSupport.parkNanos
To block the current thread .
Acquisition and release of shared lock
Get shared lock
AQS Use in acquireShared(int arg)
Method to obtain the shared lock .
acquireShared
Methods and acquire
The logic of the method is very similar , The difference only lies in the spin conditions and the operation of node queuing .
The conditions for successfully obtaining the shared lock are as follows :
tryAcquireShared(arg)
The return value is greater than or equal to 0 ( This means sharing locks permit Not used up yet ).- The predecessor of the current node is the head node .
Release the shared lock
AQS Use in releaseShared(int arg)
Method to release the shared lock .
releaseShared
First try to release the synchronization state , If it works , Then unlock one or more successor thread nodes . The process of releasing shared lock is similar to that of releasing exclusive lock , The difference lies in :
For exclusive mode , if necessary SIGNAL, Release is only equivalent to calling the header node's unparkSuccessor
.
Get an interruptible shared lock
AQS Use in acquireSharedInterruptibly(int arg)
Method to obtain an interruptible shared lock .
acquireSharedInterruptibly
Methods and acquireInterruptibly
Almost unanimously , I won't repeat .
Get a timeout wait shared lock
AQS Use in tryAcquireSharedNanos(int arg)
Method to obtain a timeout waiting shared lock .
tryAcquireSharedNanos
Methods and tryAcquireNanos
Almost unanimously , I won't repeat .
7. Deadlock
7.1. What is a deadlock
Deadlock is a specific program state , Between entities , Due to circular dependence, each other has been waiting for each other , No one can move on . Deadlock is not just between threads , There is also a process where resources are exclusive There may be a deadlock . Generally speaking , Most of us focus on deadlocks in multithreading scenarios , Between two or more threads , Because each other holds the locks each other needs , And it's permanently blocked .
7.2. How to locate a deadlock
The most common way to locate deadlocks is to use jstack Wait for tools to get thread stack , And then position the interdependencies , And then find the deadlock . If it's a more obvious deadlock , Often jstack And then we can directly locate , similar JConsole Even limited deadlock detection can be done in the graphical interface .
If we are developing our own management tools , You need to scan the service process in a more procedural way 、 Positioning deadlocks , Consider using Java Standard management provided API,ThreadMXBean
, It directly provides findDeadlockedThreads()
Method is used to locate .
7.3. How to avoid deadlock
Basically, deadlock happens because :
- Mutually exclusive , similar Java in Monitor It's all exclusive .
- Keep mutually exclusive for a long time , Before the end of use , Don't release , It can't be preempted by other threads .
- Cyclic dependence , There is a cyclic dependence of locks between multiple individuals , Depend on each other to release the lock .
thus , We can analyze the ideas and methods to avoid deadlock .
(1) Avoid one thread acquiring multiple locks at the same time .
Avoid a thread taking up multiple resources in the lock at the same time , Try to make sure that each lock occupies only one resource .
Try using a time lock lock.tryLock(timeout)
, Avoid the lock never being released .
For database locks , Locking and unlocking must be in a database connection , Otherwise, the unlocking will fail .
There is a complete Java primary , Advanced corresponding learning routes and materials ! Focus on java Development . Share java Basics 、 Knowledge of principle 、JavaWeb actual combat 、spring Family bucket 、 Design patterns 、 Distributed and interview materials 、 Open source project , Help developers grow !
Welcome to WeChat official account. : Code state Lord