📦 The article and sample source code are archived in Javacore

A brief introduction to concurrent locking

The most common way to ensure thread-safety is to use locking mechanisms (Lock, sychronized) to synchronize shared data mutually exclusive. In this way, only one thread can execute a method or a code block at a time, so the operation must be atomic and thread-safe.

In the job, interview, often hear a variety of multifarious locks, listen to the person in the fog. There are many terms for the concept of lock. They are proposed for different problems. It is not difficult to understand through simple combing.

Reentrant lock

A reentrant lock, also known as a recursive lock, means that the same thread acquires the lock in the outer method and automatically acquires the lock in the inner method.

Reentrant locks can prevent deadlocks to some extent.

  • ReentrantLock and ReentrantReadWriteLock are reentrant locks. This, too, can be seen from its name.
  • Synchronized is also a reentrant lock.
synchronized void setA() throws Exception{
    Thread.sleep(1000);
    setB();
}

synchronized void setB() throws Exception{
    Thread.sleep(1000);
}Copy the code

The code above is a typical scenario: if the lock used is not a reentrant lock, setB may not be executed by the current thread, resulting in a deadlock.

Fair and unfair locks

  • Fair lock – Fair lock means that multiple threads acquire locks in the order in which they are applied.
  • Unfair lock – An unfair lock is when multiple threads do not acquire locks in the order in which they were applied. This can lead to priority inversion (later-in-waiting) or starvation (one thread always outperforms another and never executes).

In order to ensure the order of thread application, fair lock must pay a certain performance price, so its throughput is generally lower than that of non-fair lock.

Typical implementation of fair and unfair locks in Java:

  • Synchronized only supports unfair locks.
  • ReentrantLock and ReentrantReadWriteLock are non-fair locks by default, but fair locks are supported.

Exclusive locks and shared locks

Exclusive lock and shared lock is a broad term. In practical use, they are also called mutex lock and read/write lock.

  • Exclusive locks – Exclusive locks are locks that can only be held by one thread at a time.
  • Shared locks – Shared locks are locks that can be held by multiple threads.

Typical implementations of exclusive and shared locks in Java:

  • Synchronized and ReentrantLock only support exclusive locks.
  • ReentrantReadWriteLock The write lock is an exclusive lock, and the read lock is a shared lock. Read locks are shared locks that make concurrent reads very efficient. Read, write, read, and write processes are mutually exclusive.

Pessimistic locks and optimistic locks

Optimistic and pessimistic locks do not refer to specific types of locks, but rather strategies for handling concurrent synchronization.

  • Pessimistic locks – Pessimistic locks take a pessimistic view of concurrency, believing that unlocked concurrent operations are bound to go wrong. Pessimistic locking is suitable for scenarios with frequent write operations.
  • Optimistic locking – Optimistic locking takes an optimistic view of concurrency, believing that concurrent operations without locking are fine. For concurrent operations on the same data, no changes are made. When data is updated, it is updated in a trial update mode. Optimistic locking works well in scenarios where you read too much and write too little.

Typical implementations of pessimistic and optimistic locks in Java:

  • Pessimistic locking is used in Java by usingsynchronizedLockDisplay locks for mutex synchronization, which is a blocking synchronization.
  • The application of optimistic lock in Java is to adopt CAS mechanism (CAS operation passUnsafeClass, but this class is not directly exposed to the API, so it is used indirectly, such as various atomic classes.

Lightweight, heavyweight, and biased locks

The so-called lightweight lock and heavyweight lock, refers to the lock control granularity. Obviously, the finer the control granularity, the lower the blocking overhead and the higher the concurrency.

Prior to Java 1.6, heavyweight locks were generally referred to as synchronized and lightweight locks were referred to as volatile.

After Java 1.6, a lot of optimization was made for synchronized, introducing four lock states: lock free, biased, lightweight, and heavyweight. Locks can be upgraded unidirectionally from bias locks to lightweight locks, and from lightweight locks to heavyweight locks.

  • Biased lock – A biased lock is a lock that is automatically acquired by a thread that has been accessing a piece of synchronized code. Reduce the cost of acquiring locks.
  • Lightweight lock – When a biased lock is accessed by another thread, the biased lock will be upgraded to a lightweight lock. Other threads will try to acquire the lock through spin without blocking, improving performance.
  • Heavyweight lock – When the lock is a lightweight lock, another thread spins, but the spin does not last forever. When the spin reaches a certain number of times, the lock is blocked, and the lock expands to heavyweight. Heavyweight locks can block other threads that apply for them and degrade performance.

Segmented lock

Sectionalized lock is actually a design of a lock, not a specific lock. The so-called segmented locking is to divide the locked object into multiple segments, and each segment is controlled independently, which makes the locking granularity finer, reduces the blocking overhead, and thus improves the concurrency. It makes sense, just like the toll booth on the freeway. If there is only one toll booth, all the cars can only pay in one line. If you have more than one Toll Gate, you can split it.

Hashtable uses synchronized to ensure thread-safety, so the Hashtable locks the entire object in the face of a thread’s access and all other threads have to wait. This blocking method has a very low throughput.

ConcurrentHashMap prior to Java 1.7 is a typical example of segmented locking. ConcurrentHashMap maintains an array of segments, commonly called a segmented bucket.

final Segment<K,V>[] segments;Copy the code

When a thread accesses ConcurrentHashMap’s data, ConcurrentHashMap calculates which bucket (that is, which Segment) the data is in based on hashCode and then locks that Segment.

Display locks and built-in locks

Prior to Java 1.5, the only mechanisms available to coordinate access to shared objects were synchronized and volatile. Both of these are built-in locks, meaning that lock application and release are controlled by the JVM.

After Java 1.5, new mechanisms such as ReentrantLock and ReentrantReadWriteLock were added. The application and release of these locks can be controlled by programs, so they are often referred to as display locks.

💡 Synchronized For details, see Java Concurrency mechanism – synchronized.

Note: If you do not need the advanced synchronization features provided by ReentrantLock and ReentrantReadWriteLock, use synchronized first. The reasons are as follows:

– After Java 1.6, synchronized has made a lot of optimization, and its performance is almost the same as ReentrantLock and ReentrantReadWriteLock.

– Based on the trend, Java is more likely to optimize synchronized in the future than ReentrantLock, ReentrantReadWriteLock, because synchronized is a built-in JVM property that can perform some optimizations.

– ReentrantLock and ReentrantReadWriteLock Lock application and lock release are controlled by programs. If used improperly, deadlock may occur.

Here’s a comparison between display locks and built-in locks:

  • Proactively acquire locks and release locks
    • synchronizedCannot proactively acquire and release locks. Lock acquisition and lock release are controlled by the JVM.
    • ReentrantLockLocks can be actively acquired and released. (Deadlocks can occur if you forget to release the lock).
  • In response to interrupt
    • synchronizedUnable to respond to interrupts.
    • ReentrantLockCan respond to interrupts.
  • Timeout mechanism
    • synchronizedThere is no timeout mechanism.
    • ReentrantLockThere is a timeout mechanism.ReentrantLockYou can set the timeout period to automatically release the lock, avoiding a long wait.
  • Support fair locking
    • synchronizedOnly unfair locks are supported.
    • ReentrantLockSupports unfair and fair locks.
  • Whether sharing is supported
    • besynchronizedA modified method or block of code that can only be accessed (exclusively) by one thread. If this thread is blocked, other threads can only wait
    • ReentrantLockCan be based onConditionFlexible control of synchronization conditions.
  • Whether read/write separation is supported
    • synchronizedRead-write lock separation is not supported.
    • ReentrantReadWriteLockRead/write locks are supported to separate operations that block read/write, effectively increasing concurrency.

Second, the AQS

AbstractQueuedSynchronizer (AQS) is the queue synchronizer, as the name implies, its main function is to process synchronization. It is the implementation cornerstone of concurrent locking and many synchronization tool classes (ReentrantLock, ReentrantReadWriteLock, Semaphore, etc.).

Therefore, in order to thoroughly understand concurrent locking and synchronization tools such as ReentrantLock and ReentrantReadWriteLock, you must first understand the key points and principles of AQS.

The main points of the AQS

In Java. Util. Concurrent. The locks in the lock (commonly used have already, ReadWriteLock) is achieved based on AQS. Instead of directly inheriting AQS, these locks define a Sync class to inherit AQS. Why is that? Because locks are user-oriented and synchronizers are thread oriented, aggregating synchronizers in lock implementations rather than directly inheriting AQS provides a good way to isolate both concerns.

AQS provides support for both exclusive and shared locks.

Exclusive lock API

The main APIS for acquiring and releasing exclusive locks are 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)Copy the code

  • acquire– Obtain an exclusive lock.
  • acquireInterruptibly– Gets an exclusive breakable lock.
  • tryAcquireNanos– Attempts to acquire an exclusive breakable lock for a specified period of time. Returns in the following three cases:
    • Within the timeout period, the current thread successfully acquired the lock;
    • The current thread was interrupted during the timeout period.
    • Return false if the lock has not been acquired after the timeout period.
  • release– Release the exclusive lock.

Shared lock API

The main apis for obtaining and releasing shared locks are 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)Copy the code

  • acquireShared– Obtain the shared lock.
  • acquireSharedInterruptibly– Gets the breakable shared lock.
  • tryAcquireSharedNanos– Attempts to obtain a breakable shared lock within the specified time.
  • release– Release the shared lock.

The principle of AQS

AQS data structure

Reading AQS source code, can be found: AQS inherited from AbstractOwnableSynchronize.

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { /** Wait for queue head, lazy load. This can only be modified using the setHead method. */ private transient volatile Node head; /** wait for the end of the queue, lazy load. New wait nodes can only be added through the ENQ method. */ private transient volatile Node tail; /** state */ private volatile int state; }Copy the code

  • state– AQS uses an integervolatileVariable toMaintaining synchronization Status.
    • The meaning of the integer state is given by subclasses such asReentrantLockIs the number of times the owner thread has repeatedly acquired the lock.SemaphoreThe status value represents the number of remaining licenses.
  • headtail – AQS Maintained aNodeType (inner class of AQS) to manage synchronization state. The double-linked list is a bi-directional FIFO queue that passes throughheadtailPointer to access. whenWhen a thread fails to acquire the lock, it is added to the end of the queue.

Take a look at the source code for Node

Static final class Node {/** The Node to be synchronized is in SHARED mode */ static final Node SHARED = new Node(); Static final Node EXCLUSIVE = null; static final Node EXCLUSIVE = null; /** thread waitStatus: 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; /** Precursors */ volatile Node prev; /** Next Node */ volatile Node next; /** The Thread waiting for the lock */ volatile Thread Thread; /** specifies whether the Node is shared. */ Node nextWaiter; }Copy the code

Obviously, Node is a double-linked list structure.

  • waitStatusNodeUsing an integervolatileVariable to maintain the state of thread nodes in the AQS synchronization queue.waitStatusThere are five status values:
    • CANCELLED(1)– This status indicates that the thread on this node may have timed out or been interruptedBe in the state of being cancelledOnce in this state, the node should be removed from the wait queue.
    • SIGNAL(-1)– This state means:Successor nodes will be suspended, so after the current node releases the lock or is cancelled, it must wake up (unparking) the next node.
    • CONDITION(-2)– This status indicates the thread of the nodeIn the wait condition stateIs not treated as a node on the synchronization queue until it is woken up (signal), set its value to 0, and re-enter the blocking state.
    • PROPAGATE(-3)– This status indicates: NextacquireSharedIt should be transmitted unconditionally.
    • 0 – Not in the above state.

Exclusive lock acquisition and release

Get an exclusive lock

AQS uses acquire(int arg) to acquire an exclusive lock, and its general process is as follows:

  1. Try to obtain the synchronization status first. If the synchronization status is obtained successfully, end the method and return directly.
  2. If the synchronization status is not obtained successfully, AQS will continuously try to insert the current thread into the end of the queue waiting for synchronization using CAS operations until it succeeds.
  3. Then, you keep trying to get an exclusive lock for the thread node in the wait queue.

The detailed process can be represented by the following figure, please understand with the source code (a picture is worth a thousand words) :

Release an exclusive lock

AQS uses the release(int arg) method to release the exclusive lock, and its general process is as follows:

  1. First, try to obtain the synchronization status of the unlocked thread. If the synchronization status fails to be obtained, end the method and return directly.
  2. If the synchronization status is obtained successfully, AQS attempts to wake up the successor nodes of the current thread node.
Gets an interruptible exclusive lock

The acquireInterruptibly(int ARg) method is used in AQS to obtain an exclusive breakable lock.

AcquireInterruptibly (int ARG) is implemented in a very similar way to acquire except that it checks whether the current Thread has been interrupted through Thread.interrupted. If so, InterruptedException is thrown immediately.

Get the exclusive lock of timeout wait type

AQS uses the tryAcquireNanos(int arg) method to obtain the exclusive lock for the timeout wait.

DoAcquireNanos is implemented in a very similar way to acquire except that it calculates the expiration time based on the timeout and the current time. During the process of acquiring the lock, it is constantly checked whether the lock has timed out. If so, it returns false. If there is no timeout, block the current thread with locksupport. parkNanos.

Obtaining and releasing shared locks

Obtaining a Shared lock

AcquireShared (int arg) is used to obtain the shared lock in AQS.

The acquireShared and Acquire methods have similar logic, the only differences are the spin conditions and node exit operations.

The conditions for obtaining a shared lock are as follows:

  • tryAcquireShared(arg)The return value is greater than or equal to 0 (which means that the permit for the shared lock has not run out).
  • The precursor node of the current node is the header node.
Releasing a Shared Lock

AQS uses releaseShared(int ARg) to release the shared lock.

ReleaseShared first attempts to release the synchronization state and, if successful, unlocks one or more successor thread nodes. Releasing a shared lock is similar to releasing an exclusive lock. The differences are as follows:

For the exclusive mode, if SIGNAL is required, the unparkprecursor that is equivalent only to the invocation of the head node is released.

Gets an interruptible shared lock

Used in AQS acquireSharedInterruptibly (int arg) method for interruptible Shared lock.

, acquireSharedInterruptibly method and acquireInterruptibly almost unanimously.

Get the timeout wait shared lock

AQS uses the tryAcquireSharedNanos(int arg) method to obtain a timeout wait shared lock.

The tryAcquireSharedNanos method is almost identical to the tryAcquireNanos method, so I won’t repeat it again.

Third, already

The ReentrantLock class is a concrete implementation of the Lock interface, which is a ReentrantLock. Unlike synchronized, ReentrantLock provides a set of unconditional, pollable, timed, and interruptible lock operations. All lock acquisition and lock release operations are explicit.

The characteristics of the already

ReentrantLock has the following features:

  • ReentrantLock provides the same mutual exclusion, memory visibility, and reentrancy as synchronized.
  • ReentrantLockFair and unfair lock (default) modes are supported.
  • ReentrantLockTo achieve theLockInterface, supportedsynchronizedWhat you don’t haveflexibility.
    • synchronizedUnable to interrupt a thread waiting to acquire a lock
    • synchronizedYou cannot wait endlessly while requesting a lock

The Lock interface 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();
}Copy the code

  • lock()Acquiring a lock.
  • unlock()Release the lock.
  • tryLock()Attempt to acquire a lockThe lock is acquired only if it is not held by another thread at the time of the call.
  • tryLock(long time, TimeUnit unit)– andtryLock()Similarly, the difference is only the limited time. If the lock is not obtained within the limited time, it is regarded as failure.
  • lockInterruptibly()– The lock is acquired only if the lock is not held by another thread and the thread is not interrupted.
  • newCondition()– Returns a binding toLockOn the objectConditionInstance.

The use of the already

Now that you’ve seen the features of ReentrantLock, let’s talk about how to use it.

Constructor of ReentrantLock

ReentrantLock has two constructors:

public ReentrantLock() {}
public ReentrantLock(boolean fair) {}Copy the code

  • ReentrantLock()– The default constructor initializes oneNon-fair Lock (NonfairSync);
  • ReentrantLock(boolean)new ReentrantLock(true)It initializes oneFair Lock (FairSync).

Lock and unlock methods

  • lock()Unconditional access lock. If the current thread cannot acquire the lock, the current thread goes to sleep and becomes unavailable until the current thread acquires the lock. If the lock is not held by another thread, the lock is acquired and returned immediately, setting the lock’s holding count to 1.
  • unlock()– used toRelease the lock.

Note: Remember that lock() must be acquired ina try catch block, and unlock() must be released ina finally block to ensure that the lock is released and to prevent deadlocks.

Example: Basic operations of ReentrantLock

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()); System.out.println("\t holdCount: "+ lock.getholdCount ()); System.out.println("\t queuedLength: "+ lock.getQueuelength ()); Println ("\t isFair: "+ lock.isFair()); System.out.println("\t isLocked: "+ lock.isLocked()); / / whether the current thread holds the lock System. Out. The println (" \ t isHeldByCurrentThread: "+ lock. IsHeldByCurrentThread ()); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } } finally { lock.unlock(); }}}}Copy the code

Output result:

java.util.concurrent.locks.ReentrantLock@64fcd88a[Locked by thread Thread-A]
     holdCount: 1
     queuedLength: 2
     isFair: false
     isLocked: true
     isHeldByCurrentThread: true
java.util.concurrent.locks.ReentrantLock@64fcd88a[Locked by thread Thread-C]
     holdCount: 1
     queuedLength: 1
     isFair: false
     isLocked: true
     isHeldByCurrentThread: true
// ...Copy the code

TryLock method

TryLock has better fault tolerance than unconditional lock acquisition.

  • tryLock()You can poll for locks. Returns true on success; On failure, return false. In other words, this methodWill return immediately regardless of success or failureDoes not wait until the lock has been acquired by another thread.
  • tryLock(long, TimeUnit)Locks can be obtained periodically. andtryLock()Similarly, the only difference is that this method is inA certain amount of time will be waited if the lock cannot be acquiredIf the lock has not been acquired by the time period, return false. Returns true if the lock was acquired initially or during the wait.

Example: The tryLock() operation of ReentrantLock

Modify the execute() method in the previous example

public void execute() { if (lock.tryLock()) { try { for (int i = 0; i < 3; I++) {//... } } finally { lock.unlock(); }} else {system.out.println (thread.currentThread ().getName() + "lock failed "); }}Copy the code

Example: tryLock(long, TimeUnit) operation of ReentrantLock

Modify the execute() method in the previous example

public void execute() { try { if (lock.tryLock(2, TimeUnit.SECONDS)) { try { for (int i = 0; i < 3; I++) {//... } } finally { lock.unlock(); }} else {system.out.println (thread.currentThread ().getName() + "lock failed "); }} catch (InterruptedException e) {system.out.println (thread.currentThread ().getName() + "Get lock timeout "); e.printStackTrace(); }}Copy the code

LockInterruptibly method

  • lockInterruptibly()Lock acquisition can be interrupted. Interruptible lock acquisition allows the lock to be acquired while maintaining the response to interrupts. Interruptible lock acquisition is a little more complicated than the other lock acquisition methods, requiring twotry-catchBlock (if thrown during the lock acquisition operationInterruptedException, then you can use the standardtry-finallyLock mode).
    • For example, suppose there are two threads passing at the same timelock.lockInterruptibly()When thread A acquires A lock, thread B must wait. If this is called on thread BthreadB.interrupt()Method to interrupt the waiting process for thread B. Due to thelockInterruptibly()An exception was thrown in the declaration, solock.lockInterruptibly()Must be ontryBlock or callinglockInterruptibly()Throws are declared outside the methodInterruptedException.

      Note that once a thread has acquired the lock, it is not interrupted by the interrupt() method. Calling the interrupt() method alone cannot interrupt a thread in a running state, only a thread in a blocking state. Therefore, when a lock is obtained using the lockInterruptibly() method, if the lock is not obtained, interrupts can only be responded to in a waiting state.

Example: The lockInterruptibly() operation of ReentrantLock

Modify the execute() method in the previous example

public void execute() { try { lock.lockInterruptibly(); for (int i = 0; i < 3; I++) {//... }} Catch (InterruptedException e) {system.out.println (thread.currentThread ().getName() + "interrupted "); e.printStackTrace(); } finally { lock.unlock(); }}Copy the code

NewCondition method

NewCondition () – Returns an instance of Condition bound to the Lock object. For the features and methods of Condition, see Condition below.

The principle of already

ReentrantLock’s data structure

Reading the source code for ReentrantLock, you can see that it has one core field:

private final Sync sync;Copy the code

  • sync– Inner abstract classReentrantLock.SyncObject,SyncInherited from AQS. It has two subclasses:
  • ReentrantLock.FairSync– Fair lock.
  • ReentrantLock.NonfairSync– Unfair lock.

View the source code can be found that already realize the Lock interface is called already. FairSync or already. NonfairSync in their respective implementation, here is differ a list.

ReentrantLock acquires and releases locks

Already interface, acquiring the lock and lock is released from the appearance, is to call already. FairSync or already. NonfairSync in their implementation; In essence, it is an implementation based on AQS.

A close reading of the source code is easy to find:

  • void lock()Call Sync’s lock() method.
  • void lockInterruptibly()Call AQS directlyGets an interruptible exclusive lockmethodslockInterruptibly().
  • boolean tryLock()Call the SyncnonfairTryAcquire()
  • boolean tryLock(long time, TimeUnit unit)Call AQS directlyGet the exclusive lock of timeout wait typemethodstryAcquireNanos(int arg, long nanosTimeout).
  • void unlock()Call AQS directlyRelease an exclusive lockmethodsrelease(int arg)

The method of directly calling the AQS interface will not be described, and its principle has been explained at length in the PRINCIPLE of AQS.

NonfairTryAcquire method source code:

Final Boolean nonfairTryAcquire(int acquires) {final Thread current = thread.currentThread (); int c = getState(); If (c == 0) {if (compareAndSetState(0, acquires)) {if (compareAndSetState(0, acquires)) { Set the current thread to the 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; }Copy the code

The process is simple:

  • If the synchronization status is 0, set the synchronization status to acquires, set the current thread to an exclusive thread, and return true to acquire the lock.
  • If the synchronization status is not 0 and the current thread is an exclusive thread, set the synchronization status to the current value +acquires and return true to acquire the lock.
  • Otherwise, false is returned and the lock failed to be acquired.

Implementation of lock method in fair lock and unfair lock:

The only difference is that when applying for an unfair lock, if the synchronization status is 0, an attempt is made to set it to 1. If successful, the current thread is directly set to an exclusive thread. Otherwise, as with fair lock, call AQS to acquire the exclusive lock method.

Final void lock() {if (compareAndSetState(0, 1)) // If (compareAndSetState(0, 1)) SetExclusiveOwnerThread (thread.currentThread ()); Else // call AQS acquire acquire(1); } // final void lock() {acquire acquire(1); }Copy the code

Four, ReentrantReadWriteLock

The ReentrantReadWriteLock class is a concrete implementation of the ReadWriteLock interface, which is a reentrant read-write lock. ReentrantReadWriteLock maintains a pair of read/write locks. It separates read/write locks to improve concurrency efficiency.

ReentrantLock implements a standard mutex: no more than one thread can hold a ReentrantLock at a time. But mutexes are often too strong a locking strategy to maintain data integrity, and thus unnecessarily limit concurrency. In most scenarios, read operations are more frequent than write operations, so as long as each thread is able to read the latest data and no other thread is modifying the data while reading, thread-safety issues will not occur. This strategy reduces mutually exclusive synchronization and improves concurrency performance. ReentrantReadWriteLock is an implementation of this strategy.

The characteristics of ReentrantReadWriteLock

ReentrantReadWriteLock has the following features:

  • ReentrantReadWriteLockThis applies to scenarios where you read a lot and write a little. If it is the scene of writing more and reading less, becauseReentrantReadWriteLockIts internal implementation ratioReentrantLockComplex, performance may be worse. If such a problem exists, it needs to be analyzed on a case-by-case basis. Due to theReentrantReadWriteLockRead/write lock (ReadLock,WriteLock) it all came trueLockInterface, so replace it withReentrantLockIt’s easier.
  • ReentrantReadWriteLockTo achieve theReadWriteLockInterface, supportedReentrantLockRead/write lock separation that is not available.ReentrantReadWriteLockMaintains a pair of read-write locks (ReadLock,WriteLock). Separate read and write locks to improve concurrency efficiency.ReentrantReadWriteLockThe locking strategy is:Multiple read operations can be executed concurrently, but only one write operation can be performed at a time.
  • ReentrantReadWriteLockReentrant locking semantics are provided for both read and write locks.
  • ReentrantReadWriteLockFair and unfair lock (default) modes are supported.

The ReadWriteLock interface is defined as follows:

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}Copy the code

  • readLock– Returns the lock used for the read operation (ReadLock).
  • writeLock– Returns the lock used for the write operation (WriteLock).

The interaction between a read/write lock and a write lock can be implemented in a number of ways. Some optional implementations of ReadWriteLock include:

  • Release first – When a write operation releases the write lock, and there are both reader and writer threads in the queue, should the reader, writer, or first request thread be selected first?
  • Reader thread queue jumping – If the lock is held by the reader thread, but a writer thread is waiting, can the newly arrived reader thread gain access immediately, or should it wait behind the writer thread? Allowing the reader thread to jump the queue before the writer thread improves concurrency, but can cause thread hunger.
  • Reentrancy – Are read and write locks reentrant?
  • Degrade – If a thread holds a write lock, can it acquire a read lock without releasing the lock? This can degrade a write lock to a read lock and prevent other writer threads from modifying the protected resource.
  • Upgrade – Can read locks be upgraded to a write lock before other waiting reader and writer threads? Escalation is not supported in most read-write lock implementations, because without explicit escalation, it is easy to cause deadlocks.

The use of the ReentrantReadWriteLock

Now that you’ve learned about ReentrantReadWriteLock, we’ll talk about how to use it.

ReentrantReadWriteLock constructor

Like ReentrantLock, ReentrantReadWriteLock has two constructors that are used similarly.

public ReentrantReadWriteLock() {}
public ReentrantReadWriteLock(boolean fair) {}Copy the code

  • ReentrantReadWriteLock()– The default constructor initializes oneNon-fair Lock (NonfairSync). In an unfair lock, the order in which the thread acquires the lock is uncertain. It is possible for a writer thread to downgrade to a reader thread, but not possible for a reader thread to upgrade to a writer thread (which would result in a deadlock).
  • ReentrantReadWriteLock(boolean)new ReentrantLock(true)It initializes oneFair Lock (FairSync). For fair locks, the thread that waits the longest gets the lock first. If the lock is held by the reader thread, another thread requests the write lock, and no other reader thread can acquire the read lock until the writer thread releases the write lock.

ReentrantReadWriteLock example

ReentrantReadWriteLock supports read and write locks such as ReadLock and WriteLock. So they are used separately in the same way as ReentrantLock, which I won’t repeat here.

The difference between ReentrantReadWriteLock and ReentrantLock is that read and write locks are used together. This article illustrates a typical usage scenario.

Example: Implementing a simple local cache based on ReentrantReadWriteLock

/** * Simple unbounded cache implementation * <p> * Uses WeakHashMap to store key-value pairs. Objects stored in WeakHashMap are weak references, and weak reference objects that are not referenced are automatically cleared during JVM GC. */ static class UnboundedCache<K, V> { private final Map<K, V> cacheMap = new WeakHashMap<>(); private final ReentrantReadWriteLock cacheLock = new ReentrantReadWriteLock(); public V get(K key) { cacheLock.readLock().lock(); V value; try { value = cacheMap.get(key); Format ("%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 :%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(); }}}Copy the code

Description:

  • useWeakHashMapRather thanHashMapTo store key-value pairs.WeakHashMapThe objects stored in the “weak reference” are weak references, which are automatically cleared by the JVM GC.
  • toMapWrite lock is added before data is written and released after data is written.
  • toMapAdd a read lock before reading data, and release the read lock after reading data.

Test its thread safety:

/** * @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(); } /** Thread tasks write three random values to the cache at a time, */ 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)); }}}}Copy the code

Note: In the example, 20 concurrent tasks are started from a thread pool. The task writes three random values to the cache each time, with fixed keys. The main thread then reads the value of the first key in the cache at a fixed time.

Output result:

Main Read 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 read data 0:16 Pool-1 thread-1 Writes data 0:85 5 Pool-1 thread-1 writes data 1:76 Pool-1 thread-1 writes data 2:46 Pool-1-thread-2 writes data 0:21 Pool-1-thread-2 Writing data 1:41 pool-1-thread-2 Writing data 2:63 main Reading data 0:21 main reading data 0:21 //...Copy the code

The principle of ReentrantReadWriteLock

Now that you know how ReentrantLock works, it’s easy to understand ReentrantReadWriteLock.

ReentrantReadWriteLock Data structure

ReentrantReadWriteLock 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; }Copy the code

  • sync– the inner classReentrantReadWriteLock.SyncObject. withReentrantLockSimilarly, it has two subclasses:ReentrantReadWriteLock.FairSyncReentrantReadWriteLock.NonfairSyncRepresents the implementation of fair lock and unfair lock respectively.
  • readerLock– the inner classReentrantReadWriteLock.ReadLockObject, which is a read lock.
  • writerLock– the inner classReentrantReadWriteLock.WriteLockObject, which is a write lock.

ReentrantReadWriteLock Obtains and releases locks

public static class ReadLock implements Lock, Public void lock() {sync.acquireshared (1); java.io.Serializable public void Lock () {sync.acquireshared (1); } public void unlock() {sync.releaseshared (1); } } public static class WriteLock implements Lock, Public void lock() {sync.acquire(1); java.io.Serializable public void lock() {sync.acquire(1); } public void unlock() {sync.release(1); }}Copy the code

Five, the Condition

As mentioned earlier, the Lock interface has a newCondition() method that returns an instance of Condition bound to the Lock object. What is Condition? What does it do? This section will explain them all.

In a single thread, the execution of a piece of code may depend on a state, and if the state condition is not met, the code will not be executed (typical scenario: if… The else…). . In a concurrent environment, when a thread determines a state condition, its state may be changed due to the actions of other threads, so there needs to be some coordination mechanism to ensure that data can only be modified by one thread lock at a time, and the modified data state is perceived by all threads.

Before Java 1.5, wait, notify, and notifyAll of Object class are mainly used for inter-thread communication with synchronized. Java Thread basics – wait/notify/notifyAll).

Wait, notify, and notifyAll must work with synchronized but not Lock. Threads using Lock should communicate with each other using Condition. It can be interpreted as, what kind of lock makes what kind of key. Synchronized works with wait, notify, and notifyAll, while explicit locks work with Condition.

The characteristics of the Condition

The Condition 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();
}Copy the code

Among them, await, signal, signalAll correspond to wait, notify, notifyAll and have similar functions. In addition, Condition provides more functions than the built-in conditional queue (wait, notify, and notifyAll) :

  • Each lock (LockThere can be more than oneCondition, which means that there can be multiple lock state conditions.
  • Support fair or unfair queue operations.
  • Support interruptible conditional wait, related methods:awaitUninterruptibly()
  • Supports timed wait. Related methods:awaitNanos(long)await(long, TimeUnit),awaitUntil(Date).

The use of the Condition

Condition is used here to implement a consumer/producer pattern.

In fact, it is easier and safer to solve these problems with tools like CountDownLatch and Semaphore. For more information, see the Java Concurrency utility class.

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;
    }

}Copy the code

consumers

class MessageConsumer implements Runnable { private Message message; public MessageConsumer(Message msg) { message = msg; } @Override public void run() { while (! message.isEnd()) { message.consume(); }}}Copy the code

producers

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); }}Copy the code

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(); }}Copy the code

The resources

  • Java Concurrent Programming
  • The Art of Concurrent Programming in Java
  • Java concurrent programming: Lock
  • Learn in depth about Java synchronizer AQS
  • AbstractQueuedSynchronizer framework
  • Lock classification in Java