When synchronize is used for synchronization, the lock is acquired and released implicitly through compilation and different machine instructions.

And already is a generic class, it is based on the AQS (AbstractQueuedSynchronizer).

Is a reentrant lock: a thread that has acquired a lock can lock it again and again without blocking itself.

AQS is an important basic framework for implementing lock and synchronization in Java and sending packages.

The lock type

Reentrantlocks are classified into fair and unfair locks, and can be specified using constructors:

    // Default unfair lock
    public ReentrantLock(a) {
        sync = new NonfairSync();
    }
    
    / / fair lock
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
Copy the code

By default, non-fair locks are generally used, which are much more efficient and throughput than fair locks (more on why later).

Acquiring a lock

The usual usage is as follows:

    private ReentrantLock lock = new ReentrantLock();
    public void run(a) {
        lock.lock();
        try {
            //do bussiness
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally{ lock.unlock(); }}Copy the code

Fair lock acquires locks

Let’s first look at the process of acquiring a lock:

    public void lock(a) {
        sync.lock();
    }
Copy the code

As you can see, the sync method is used, which is an abstract method implemented by its subclass (FairSync). The following is a fair lock implementation:

        final void lock(a) {
            acquire(1);
        }
        
        / / AbstractQueuedSynchronizer the acquire ()
        public final void acquire(int arg) {
        if(! tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }Copy the code

The first step is to try to acquire a lock (tryAcquire(ARG)), which is also implemented by subclasses:

        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if(! hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true; }}else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false; }}Copy the code

The first step is to determine whether the state in AQS is equal to 0, which means that no other thread has acquired the lock, so the current thread can try to acquire the lock.

Note: The HasqueuedToraise () method is used to determine whether there are other threads in the AQS queue before the attempt and does not attempt to acquire the lock if it does (this is unique to fair locks).

If there are no threads in the queue, CAS is used to change the state in the AQS to 1, that is, to acquire the lock. If the lock is obtained successfully, the current thread is set to the exclusive thread that acquired the lock (setExclusiveOwnerThread(current)).

If state is greater than 0, it indicates that the lock has been acquired, and it is necessary to determine whether the thread that acquired the lock is the current thread (ReentrantLock supports reentrant). If yes, state + 1 is required and the value is updated.

Written to the queue

If tryAcquire(ARG) fails to acquire the lock, you need to write the current thread to the queue with addWaiter(Node.exclusive).

Before writing, wrap the current thread as a Node object (addWaiter(Node.exclusive)).

Queues in AQS are implemented by bidirectional linked lists of Node nodes.

Package code:

    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;
        if(pred ! =null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

Copy the code

First, determine whether the queue is empty. If it is not empty, the encapsulated Node will be written to the end of the queue using CAS. If concurrent writing fails, enq(Node) needs to be called. I’m going to write.

    private Node enq(final Node 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;
                    returnt; }}}}Copy the code

This processing logic is equivalent to the spin plus CAS guarantee that it will be written to the queue.

Suspend the wait thread

To suspend the current thread after writing to the queue (by acquireQueued(addWaiter(Node.exclusive), arg)) :

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                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); }}Copy the code

First, the node.predecessor() is used to obtain whether the previous node is the head node. If it is, a lock will be attempted, and everything will be fine if the lock is successfully obtained.

If not for the first node, or failed to get the lock, will be based on a node waitStatus state to handle (shouldParkAfterFailedAcquire (p, node).

WaitStatus Records the status of the current node, such as node cancellation or node waiting.

ShouldParkAfterFailedAcquire (p, node) whether you need to return the current thread hanging, if you need to call the parkAndCheckInterrupt () :

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

It uses the part method of LockSupport to suspend the current thread until it is woken up.

An unfair lock acquires a lock

The difference between fair and unfair locks is mainly in acquiring locks:

Fair lock is equivalent to buying a ticket, and the next person needs to queue up to buy a ticket, can not jump the queue.

Non-fair locks do not have these rules, is preemption mode, each person does not care about the queue, directly try to obtain the lock.

Unfair lock:

        final void lock(a) {
            // Try to get the lock directly
            if (compareAndSetState(0.1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
Copy the code

Fair lock:

        final void lock(a) {
            acquire(1);
        }
Copy the code

Another important difference is that tryAcquire(ARG) does not need to determine if there are other threads in the queue when trying to acquire the lock.

        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                / / not! HasQueuedPredecessors ()
                if (compareAndSetState(0, acquires)) {
                    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

Release the lock

The release process for fair and unfair locks is the same:

    public void unlock(a) {
        sync.release(1);
    }
    
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if(h ! =null&& h.waitStatus ! =0)
            	   // Wake up the suspended thread
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    
    // Try to release the lock
    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;
    }        
Copy the code

First, it will determine whether the current thread is the thread that acquired the lock. Since it is a reentrant lock, state needs to be reduced to 0 before the lock is considered fully released.

The unparksucceeded (h) needs to be called after the release to wake up the suspended threads.

conclusion

Because a fair lock needs to care about the condition of the queue, the lock must be acquired in the order in which it is in the queue (causing a lot of thread context switching), whereas a non-fair lock does not.

So that explains why unfair locking is more efficient than fair locking.

extra

Recently in the summary of some Java related knowledge points, interested friends can maintain together.

Address: github.com/crossoverJi…