Exploring the unlocking process of ReentrantLock unfair lock

lock 

ReentrantLock reentrantLock = new ReentrantLock();
        reentrantLock.lock();
        //...
        reentrantLock.unlock();

①java.util.concurrent.locks.ReentrantLock#lock

public void lock() {
    //具体的sync有公平和非公平(默认)两种
    sync.lock();
}

②java.util.concurrent.locks.ReentrantLock.NonfairSync#lock

final void lock() {
            if (compareAndSetState(0, 1))//这里体现的非公平
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

③java.util.concurrent.locks.AbstractQueuedSynchronizer#acquire

public final void acquire(int arg) {    
    if (!tryAcquire(arg) &&        
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))        
        selfInterrupt();
}

It has become complicated here, let’s analyze one by one:

④java.util.concurrent.locks.ReentrantLock.NonfairSync#tryAcquire

protected final boolean tryAcquire(int acquires) {   
    return nonfairTryAcquire(acquires);
}

⑤java.util.concurrent.locks.ReentrantLock.Sync#nonfairTryAcquire

final boolean nonfairTryAcquire(int acquires) {
	//获取当前线程
    final Thread current = Thread.currentThread();
    //获取状态
    int c = getState();
    //如果为0 ,说明没有线程竞争。通过cas+1,每次线程重入该锁都会+1,释放都会-1,为0时释放锁。如果cas设置成功,则可以预计其它线程cas都是失败的,也就认为当前线程得到了锁,作为Running线程
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
              setExclusiveOwnerThread(current);
              return true;
        }
    }
    //如果c!=0,但是自己已经拥有锁,则只是简单的执行+1,并修改status值,因为没有竞争,所以直接通过setStat修改,而非cas,也就是说这里实现了偏向锁的功能!
    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;
}

If fasle is returned , let's look at the acquireQueued( addWaiter(Node.EXCLUSIVE) , arg ) method in step ③ :

The addWaiter method is responsible for packaging the thread that cannot currently obtain the lock into a Node and adding it to the end of the CLH queue. The mode parameter indicates whether it is an exclusive lock or a shared lock:

static final class Node {    
    /** Marker to indicate a node is waiting in shared mode */    
    static final Node SHARED = new Node();    
    /** Marker to indicate a node is waiting in exclusive mode */   
    static final Node EXCLUSIVE = null;
}

private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
    	//如果当前队尾已经存在,则使用cas把当前线程更新为Tail
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
    //如果当前Tail为null或者使用cas把当前线程更新为Tail失败,则通过enq方法继续设置Tail
        enq(node);
        return node;
    }


private Node enq(final Node node) {    
    //循环调用CAS,将当前线程追加到队尾(或设置队头),并返回包装后的Node实例。
    for (;;) {        
        Node t = tail;        
        if (t == null) { 
            // Must initialize            
            if (compareAndSetHead(new Node()))                
                tail = head;        
        } else {            
            node.prev = t;            
            if (compareAndSetTail(t, node)) {               
                t.next = node;               
                return t;            
            }        
        }    
    }
}

The main reason for wrapping threads into Node objects is that in addition to using Node for virtual queues, Node is also used to wrap various thread states:

/** waitStatus value to indicate thread has cancelled */
        static final int CANCELLED =  1;
        /** waitStatus value to indicate successor's thread needs unparking */
        static final int SIGNAL    = -1;
        /** waitStatus value to indicate thread is waiting on condition */
        static final int CONDITION = -2;
        /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate
         */
        static final int PROPAGATE = -3;
  • SIGNAL(-1): The successor thread of the thread is/has been blocked. When the thread is released or canceled, the successor thread (unpark) should be called.

  • CANCELLED(1): The thread has been canceled due to timeout or interruption

  • CONDITION(-2): Indicates that the thread is in the condition queue, which is blocked because of calling Condition.wait

  • PROPAGATE(-3): Propagate shared locks

  • 0:0 means no state

Then look at acquireQueued. The main function of acquireQueued is to block the thread node that has been added to the queue (the return value of the addWaiter method), but before blocking, retry whether the lock can be obtained through tryAccauire. If the retry is successful, there is no need to block and return directly. :

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                //这里是个无限循环,但是不会出现死循环,原因在于parkAndCheckInterrupt会把当前线程挂起,从而阻塞住线程的调用栈
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
          	//规则1:如果前继的节点状态为SIGNAL,表明当前节点需要unpark,则返回成功,此时会执行parkAndCheckInterrupt方法,阻塞线程
            return true;
        if (ws > 0) {
            //规则2:如果前继节点状态大于0(CANCELLED状态),说明前置节点已经被放弃,则回溯到一个非取消的前继节点,返回false,acquireQueued方法的无限循环将递归调用该方法,直至规则1返回true,导致线程阻塞
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //规则3:如果前继节点状态为非SIGNAL、非CANCELLED,则设置前继的状态为SIGNAL,返回false后进入acquireQueued方法的无限循环,同规则2
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

In general, shouldParkAfterFailedAcquire is to judge whether the current thread should be blocked by the previous successor node. If the previous successor node is in the CANCELLED state, delete it by the way.

 

So far, the logic of locking the thread has been analyzed, and the unlocking process is discussed below.

unlock

The unsuccessful thread requesting the lock will be suspended in the parkAndCheckInterrupt method in the acquireQueued method, and the subsequent code must wait for the thread to be unlocked before execution. If the thread is unlocked now, execute interrupted = true;, and then enter the infinite loop of acquireQueued.

However, the unlocked thread may not be able to acquire the lock. It must re-compete by calling tryAcquire, because the lock is unfair and may be acquired by the newly joined thread, which will cause the newly awakened thread to be blocked again. This fully reflects the unfairness mechanism

①java.util.concurrent.locks.ReentrantLock#unlock

public void unlock() {
    sync.release(1);
}

②java.util.concurrent.locks.AbstractQueuedSynchronizer#release

public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

//第一个可以unpark的线程,一般来说head.next == head,head就是第一个线程,但是head.next可能被取消或置为null,因此最稳妥的办法就是从后往前找第一个可用线程
private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

If the lock is released successfully, wake up the first thread Head of the queue

protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

The function of tryRelease is very clear. If the thread locks multiple times, it will release multiple times until status==0, then the lock is actually released, that is, the status is set to 0, because there is no competition, so CAS is not used.

 Lock Vs Synchronized

     AbstractQueuedSynchronizer constructs a blocking-based CLH queue to accommodate all blocked threads, and operations on the queue are performed through Lock-Free (CAS) operations, but for threads that have acquired locks, ReentrantLock implements the function of biased locks. 
        The bottom layer of Synchronized is also a waiting queue based on CAS operations, but the JVM implementation is more refined, and the waiting queue is divided into ContentionList and EntryList. There is no essential difference between the two designs, but Synchronized also implements spin locks and is optimized for different systems and hardware systems, while Lock completely relies on system blocking to suspend waiting threads.

        Of course, Lock is more suitable for application layer expansion than Synchronized, and can inherit AbstractQueuedSynchronizer to define various implementations, such as implementing read-write lock (ReentrantReadWriteLock), fair or unfair lock; at the same time, the Condition corresponding to Lock is also more convenient and flexible than wait/notify many!

Guess you like

Origin blog.csdn.net/cj_eryue/article/details/125047543