This is the 9th day of my participation in the August More Text Challenge

StampedLock is a new Java8 lock designed to enhance ReentrantReadWriteLock read and write locks. It adds an optimistic lock mode to ReentrantReadWriteLock read and write locks, and internally optimizes the access apis for read and write locks, enabling interconversion between read and write locks. This allows us to control concurrency from a more granular dimension and reduce the performance degradation associated with locking. ReentrantReadWriteLock locks and the use of the concrete introduction, here is not introduced, an interested friends can go to access to relevant data, this article mainly introduces StampedLock lock related characteristics, source, and how to use knowledge.

The use of StampedLock

Before analyzing the implementation principle of StampedLock, let’s take a look at how to use it. Here we use the official scheme provided by Oracle to see how to use it. The official case address: docs.oracle.com/javase/8/do…

public class StampedLockDemo {
    /**
     * 共享变量
     */
    private double x, y;
    /** * instantiate StampedLock */
    private final StampedLock sl = new StampedLock();

    /** * move coordinate: write operation **@param deltaX
     * @param deltaY
     */
    void move(double deltaX, double deltaY) {
        // Get the write lock
        long stamp = sl.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            // Release the lock when the operation is completesl.unlockWrite(stamp); }}/** * Use optimistic lock **@return* /
    double distanceFromOrigin(a) {
        // Get an optimistic lock
        long stamp = sl.tryOptimisticRead();
        // Store shared variables into local local variables
        double currentX = x, currentY = y;
        // Check the optimistic lock to see if any other write locks occur after the optimistic lock is acquired
        if(! sl.validate(stamp)) {// If there is: get a pessimistic lock
            stamp = sl.readLock();
            try {
                currentX = x;
                currentY = y;
            } finally {
                / / whether the locksl.unlockRead(stamp); }}return Math.sqrt(currentX * currentX + currentY * currentY);
    }

    /** * Upgrade case with pessimistic read lock *@param newX
     * @param newY
     */
    void moveIfAtOrigin(double newX, double newY) {
        // Get pessimistic read lock
        long stamp = sl.readLock();
        try {
            while (x == 0.0 && y == 0.0) {
                // If the conditions are met, the read lock is upgraded to a write lock
                long ws = sl.tryConvertToWriteLock(stamp);
                // Check whether the read lock is successfully converted to write lock
                if(ws ! =0L) {
                    // The ticket is replaced
                    stamp = ws;
                    x = newX;
                    y = newY;
                    break;
                } else {
                    // The read lock failed to convert the write lock, releasing the original read lock
                    sl.unlockRead(stamp);
                    // Get the write lock againstamp = sl.writeLock(); }}}finally {
            / / releases the locksl.unlock(stamp); }}}Copy the code

From the example on the official website, the method is used: WriteLock- get writeLock, readLock- get readLock, tryOptimisticRead- get optimistic lock, tryConvertToWriteLock- upgrade writeLock to read/write, unlockRead- release lock, Validate – Whether the ticket validation occurs to modify these methods. When using optimistic lock, we need to judge whether the bill has been modified during the period of data acquisition and lock acquisition. If it has been modified, we need to acquire the lock again. Pessimistic read-write lock can be directly acquired to prevent the bill from being modified after optimistic lock is adopted. Look at the case provided by the official, it is relatively clear to use, but we must release the lock in finally when using, otherwise the lock is not released, the subsequent thread can not get the lock has been blocked, let’s look at how to implement from the source point of view.

StampedLock related source code analysis

Create a lock

StampedLock is internally based on CLH(CLH is a high-performance, fair spinlock based on one-way linked lists). The thread applying for the lock spins through the variables of the precursor node. After the front node is unlocked, the current node finishes spinning and locks. CLH has more advantages in SMP architecture. In the NUMA architecture, if the current node and the precursor node are not in the same CPU module, there is additional overhead for cross-CPU module, whereas THE MCS lock is more suitable for the NUMA architecture. Each node in the queue represents a thread, and the node saves a marker bit to determine whether the current thread has released the lock. When a thread view when acquiring the lock, the first will get the tail end of the queue as its antecedent nodes (because it is a FIFO queue model, so you just need to judge the state of a node is in front of the line) whether to release the lock, if no lock is released, the tail in front of the node will join the wait queue tail, and then spin. If the current preordering tail thread has released the lock, the lock is acquired and the current thread executes. An internal node is maintained inside StampedLock, which encapsulates the thread we need to execute, and a number of constant values are defined. Before looking at other methods, let’s take a look at internally defined constants and queue node object information.

  • Internal queue node
static final class WNode {
    // The previous node pointer
    volatile WNode prev;
    // The last node pointer
    volatile WNode next;
    // Read the thread list
    volatile WNode cowait;    // list of linked reader
    // The thread object we need to execute
    volatile Thread thread;   // non-null while possibly parked
    // Node status: Waiting or cancelled
    volatile int status;      // 0, WAITING, or CANCELLED
    // Node mode: read or write
    final int mode;           // RMODE or WMODE
    WNode(intm, WNode p) { mode = m; prev = p; }}Copy the code
  • Internal constants
// Queue node status constant: wait
private static final int WAITING   = -1;
// Queue node status constant: cancel
private static final int CANCELLED =  1;

// Node mode: read
private static final int RMODE = 0
// Node mode: write
private static final int WMODE = 1;

// Queue head node
private transient volatile WNode whead;
// End of queue
private transient volatile WNode wtail;


// Read lock view
transient ReadLockView readLockView;
// Write lock view
transient WriteLockView writeLockView;
// Multi-lock view
transient ReadWriteLockView readWriteLockView;

// The current lock status
private transient volatile long state;
/ / record values
private transient int readerOverflow;
Copy the code

state:

  • Record the status of the current lock, whether it is occupied by a write lock or a read lock. The eighth digit from the bottom of long is 1, indicating that it is occupied by a write lock (0000 0001), and the first seven digits are occupied by a read lock (1-126).

  • The constructor

public StampedLock(a) {
    state = ORIGIN;
}
Copy the code

Instantiate a lock whose status is: _ORIGIN(0000, 0000). The lock has been initialized

Acquiring a lock

  • WriteLock – obtains a writeLock
public long writeLock(a) {
    long s, next;  // bypass acquireWrite in fully unlocked case only
    return ((((s = state) & ABITS) == 0L && U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?
            next : acquireWrite(false.0L));
}
Copy the code

Write lock acquireWrite () {return long Next (state); if this fails, write locks will be queued by acquireWrite. AcquireWrite joins the write lock queue in the source code quite long, is queue-related operations, here not to analyze interested in looking at their own detailed source code carefully

  • ReadLock – gets the readLock
public long readLock(a) {
    long s = state, next;  // bypass acquireRead on common uncontended case
    return ((whead == wtail && (s & ABITS) < RFULL &&
             U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ?
            next : acquireRead(false.0L));
}
Copy the code

The source code for a read lock is basically the same as the source code for a write lock, but the read lock can be shared multiple times.

  • UnlockWrite – releases the lock
 public void unlockWrite(long stamp) {
     WNode h;
     if(state ! = stamp || (stamp & WBIT) ==0L)
         throw new IllegalMonitorStateException();
     state = (stamp += WBIT) == 0L ? ORIGIN : stamp;
     if((h = whead) ! =null&& h.status ! =0)
         release(h);
 }
Copy the code

Before releasing the lock, internal check whether the incoming ticket is the same, if not, it will throw the same; The ticket changes the value of state and then calls release() to wake up the first node in the waiting queue

private void release(WNode h) {
    if(h ! =null) {
        WNode q; Thread w;
        U.compareAndSwapInt(h, WSTATUS, WAITING, 0);
        if ((q = h.next) == null || q.status == CANCELLED) {
            for(WNode t = wtail; t ! =null&& t ! = h; t = t.prev)if (t.status <= 0)
                    q = t;
        }
        if(q ! =null&& (w = q.thread) ! =null) U.unpark(w); }}Copy the code

To determine whether the first node of the queue obtained is empty and cancelled, we throw away the changed node and retrieve another node from the queue until the obtained node is not empty and the thread to execute is not empty, we call U.npark () to execute our thread task

  • TryOptimisticRead – Try to obtain an optimistic lock
public long tryOptimisticRead(a) {
    long s;
    return (((s = state) & WBIT) == 0L)? (s & SBITS) :0L;
}
Copy the code

If no thread obtains the write lock, the optimistic read lock is successfully acquired and ticket information is returned. The optimistic lock only needs to be acquired, but does not need to be released. When get as long as no thread to get to write much, then you can get optimistic read lock, at the same time to share data stored in the local variables, will not block other threads to share data is modified, it is because of such a mechanism, can cause using Shared data, will appear inconsistent problems, therefore, when using optimistic read lock, need to check the data again and again, When writing the value of the local variable to the shared memory, check whether the ticket information of the current optimistic lock is valid. If yes, perform subsequent operations. If not, upgrade the lock to pessimistic read lock, and then perform data operations.

  • Validate – Verifies the ticket
 public boolean validate(long stamp) {
     U.loadFence();
     return (stamp & SBITS) == (state & SBITS);
 }
Copy the code

Above, we just give a rough interpretation of several common methods. In general, we have a certain understanding of the implementation principle of StampedLock. As for the specific implementation of StampedLock, friends who are interested in their own research. Let’s summarize some of the features of StampedLock

StampedLock characteristics

  1. When the lock is obtained, stamp ticket information is returned. If the ticket is 0, it means that the lock has failed to be obtained and needs to be obtained again. If the ticket is not 0, it means that the lock has been obtained successfully
  2. When the lock is released, the obtained ticket information needs to be passed in, and the accuracy of the information will be verified first. If the information is inconsistent, an exception will be thrown. The main benefit is to control the use and release of the same lock
  3. Write locks are not reentrant, and if a thread has already acquired a write lock, subsequent attempts to acquire it can cause deadlock problems
  4. Three types of locks are provided
    • Write lock: The lock is not shared. A write lock can only be acquired by one thread. When a write lock is acquired by one thread, all other threads will block and wait
    • Pessimistic read lock: Multiple threads can share a read lock. When a read lock is acquired, the write lock is blocked. The read lock and the write lock are mutually exclusive
    • Optimistic read lock: When there is no write lock, the optimistic read lock can be obtained. The optimistic read lock does not need to be released. After the optimistic read lock is obtained, subsequent write operations will not be blocked
  5. The lock waiting queue adopts FIFO, first-in, first-out mode, and the node that waits first obtains the lock first
  6. Read and write locks can be interchangeable