Java Review - 并发编程_独占锁ReentrantLock原理&源码剖析

Posted 小小工匠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java Review - 并发编程_独占锁ReentrantLock原理&源码剖析相关的知识,希望对你有一定的参考价值。

文章目录


Synchronized vs ReentrantLock


ReentrantLock概述

ReentrantLock是可重入的独占锁,同时只能有一个线程可以获取该锁,其他获取该锁的线程会被阻塞而被放入该锁的AQS阻塞队列里面。

类图结构如下

  • 底层基于AQS实现,ReentrantLock的lock等方法,委托给其依赖sync的lock方法

  • AQS 是典型的模板方法设计模式,父类(AQS)定义好骨架和内部操作细节,具体规则由子类去实现

从类图可以看出,ReentrantLock最终还是使用AQS来实现的,并且根据参数来决定其内部是一个公平还是非公平锁,默认是非公平锁。

    /**
     * Creates an instance of @code ReentrantLock.
     * This is equivalent to using @code ReentrantLock(false).
     */
    public ReentrantLock() 
        sync = new NonfairSync();
    

    /**
     * Creates an instance of @code ReentrantLock with the
     * given fairness policy.
     *
     * @param fair @code true if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) 
        sync = fair ? new FairSync() : new NonfairSync();
    

其中Sync类直接继承自AQS,它的子类NonfairSync和FairSync分别实现了获取锁的非公平与公平策略。

在这里,AQS的state状态值表示线程获取该锁的可重入次数

  • 在默认情况下,state的值为0表示当前锁没有被任何线程持有。

  • 当一个线程第一次获取该锁时会尝试使用CAS设置state的值为1,如果CAS成功则当前线程获取了该锁,然后记录该锁的持有者为当前线程。

  • 在该线程没有释放锁的情况下第二次获取该锁后,状态值被设置为2,这就是可重入次数。

  • 在该线程释放该锁时,会尝试使用CAS让状态值减1,如果减1后状态值为0,则当前线程释放该锁。


获取锁

void lock()

.

当一个线程调用该方法时,说明该线程希望获取该锁。

  • 如果锁当前没有被其他线程占用并且当前线程之前没有获取过该锁,则当前线程会获取到该锁,然后设置当前锁的拥有者为当前线程,并设置AQS的状态值为1,然后直接返回

  • 如果当前线程之前已经获取过该锁,则这次只是简单地把AQS的状态值加1后返回

  • 如果该锁已经被其他线程持有,则调用该方法的线程会被放入AQS队列后阻塞挂起

加锁过程如下

  /**
     * Acquires the lock.
     *
     * <p>Acquires the lock if it is not held by another thread and returns
     * immediately, setting the lock hold count to one.
     *
     * <p>If the current thread already holds the lock then the hold
     * count is incremented by one and the method returns immediately.
     *
     * <p>If the lock is held by another thread then the
     * current thread becomes disabled for thread scheduling
     * purposes and lies dormant until the lock has been acquired,
     * at which time the lock hold count is set to one.
     */
    public void lock() 
        sync.lock();
    

在如上代码中 ,ReentrantLock的lock()委托给了sync类,根据创建ReentrantLock构造函数选择sync的实现是NonfairSync还是FairSync,这个锁是一个非公平锁或者公平锁。

非公平锁的实现代码

先看sync的子类NonfairSync的情况,也就是非公平锁时。

   /**
     * Sync object for non-fair locks
     */
    static final class NonfairSync extends Sync 
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() 
        	// 1 cas 设置状态值
            if (compareAndSetState(0, 1))
               // 设置线程为独占锁线程
                setExclusiveOwnerThread(Thread.currentThread());
            else
               // 2 调用AQS的acquire方法
                acquire(1);
        

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

在代码(1)中,因为默认AQS的状态值为0,所以第一个调用Lock的线程会通过CAS设置状态值为1,CAS成功则表示当前线程获取到了锁,然后setExclusiveOwnerThread设置该锁持有者是当前线程.

如果这时候有其他线程调用lock方法企图获取该锁,CAS会失败,然后会调用AQS的acquire方法。注意,传递参数为1,这里再贴下AQS的acquire的核心代码。

    /**
     * Acquires in exclusive mode, ignoring interrupts.  Implemented
     * by invoking at least once @link #tryAcquire,
     * returning on success.  Otherwise the thread is queued, possibly
     * repeatedly blocking and unblocking, invoking @link
     * #tryAcquire until success.  This method can be used
     * to implement method @link Lock#lock.
     *
     * @param arg the acquire argument.  This value is conveyed to
     *        @link #tryAcquire but is otherwise uninterpreted and
     *        can represent anything you like.
     */
    public final void acquire(int arg) 
    	// 3 调用ReentrantLock重写的tryAcquire方法
        if (!tryAcquire(arg) &&
        	// tryAcquire返回false会把当前线程放入AQS阻塞队列
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    

AQS并没有提供可用的tryAcquire方法,tryAcquire方法需要子类自己定制化,所以这里代码(3)会调用ReentrantLock重写的tryAcquire方法。我们先看下非公平锁的代码。

     protected final boolean tryAcquire(int acquires) 
            return nonfairTryAcquire(acquires);
        
  /**
         * Performs non-fair tryLock.  tryAcquire is implemented in
         * subclasses, but both need nonfair try for trylock method.
         */
        final boolean nonfairTryAcquire(int acquires) 
            final Thread current = Thread.currentThread();
            int c = getState();
            // 4 当前AQS状态值state为0
            if (c == 0) 
                if (compareAndSetState(0, acquires)) 
                    setExclusiveOwnerThread(current);
                    return true;
                
            
            else if (current == getExclusiveOwnerThread()) // 5 当前线程是该锁的持有者
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            
            return false;
        
  • 代码(4)会查看当前锁的状态值是否为0,为0则说明当前该锁空闲,那么就尝试CAS获取该锁,将AQS的状态值从0设置为1,并设置当前锁的持有者为当前线程然后返回,true。

  • 如果当前状态值不为0则说明该锁已经被某个线程持有,所以代码(5)查看当前线程是否是该锁的持有者,如果当前线程是该锁的持有者,则状态值加1,然后返回true,这里需要注意,nextc<0说明可重入次数溢出了。

  • 如果当前线程不是锁的持有者则返回false,然后其会被放入AQS阻塞队列。


非公平锁是如何体现的?

上面看完了非公平锁的实现代码,回过头来看看非公平在这里是怎么体现的。

首先非公平是说先尝试获取锁的线程并不一定比后尝试获取锁的线程优先获取锁

这里假设线程A调用lock()方法时执行到nonfairTryAcquire的代码(4),发现当前状态值不为0,所以执行代码(5),发现当前线程不是线程持有者,则执行代码(6)返回false,然后当前线程被放入AQS阻塞队列。

这时候线程B也调用了lock()方法执行到nonfairTryAcquire的代码(4),发现当前状态值为0了(假设占有该锁的其他线程释放了该锁),所以通过CAS设置获取到了该锁。明明是线程A先请求获取该锁呀,这就是非公平的体现。

这里线程B在获取锁前并没有查看当前AQS队列里面是否有比自己更早请求该锁的线程,而是使用了抢夺策略。


公平锁是怎么实现公平的

那么下面看看公平锁是怎么实现公平的。公平锁的话只需要看FairSync重写的tryAcquire方法。

    /**
     * Sync object for fair locks
     */
    static final class FairSync extends Sync 
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() 
            acquire(1);
        

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) 
            final Thread current = Thread.currentThread();
            int c = getState();
            // 7 当前state的状态为0 
            if (c == 0) 
            	// 8 公平策略
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) 
                    setExclusiveOwnerThread(current);
                    return true;
                
            
            // 9 当前线程是锁的持有者 
            else if (current == getExclusiveOwnerThread()) 
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            
            return false;
        
    

如以上代码所示,公平的tryAcquire策略与非公平的类似,不同之处在于,代码(8)在设置CAS前添加了hasQueuedPredecessors方法,该方法是实现公平性的核心代码,

   public final boolean hasQueuedPredecessors() 
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    

在如上代码中,如果当前线程节点有前驱节点则返回true,否则如果当前AQS队列为空或者当前线程节点是AQS的第一个节点则返回false。

  • 其中如果h==t则说明当前队列为空,直接返回false;
  • 如果h!=t并且s==null则说明有一个元素将要作为AQS的第一个节点入队列( 回想一下 enq函数的第一个元素入队列是两步操作:首先创建一个哨兵头节点,然后将第一个元素插入哨兵节点后面),那么返回true
  • 如果h!=t并且s!=null和s.thread != Thread.currentThread()则说明队列里面的第一个元素不是当前线程,那么返回true。

void lockInterruptibly() 方法

该方法与lock()方法类似,它的不同在于,它对中断进行响应,就是当前线程在调用该方法时,如果其他线程调用了当前线程的interrupt()方法,则当前线程会抛出InterruptedException异常,然后返回

 /**
     * Acquires the lock unless the current thread is
     * @linkplain Thread#interrupt interrupted.
     *
     * <p>Acquires the lock if it is not held by another thread and returns
     * immediately, setting the lock hold count to one.
     *
     * <p>If the current thread already holds this lock then the hold count
     * is incremented by one and the method returns immediately.
     *
     * <p>If the lock is held by another thread then the
     * current thread becomes disabled for thread scheduling
     * purposes and lies dormant until one of two things happens:
     *
     * <ul>
     *
     * <li>The lock is acquired by the current thread; or
     *
     * <li>Some other thread @linkplain Thread#interrupt interrupts the
     * current thread.
     *
     * </ul>
     *
     * <p>If the lock is acquired by the current thread then the lock hold
     * count is set to one.
     *
     * <p>If the current thread:
     *
     * <ul>
     *
     * <li>has its interrupted status set on entry to this method; or
     *
     * <li>is @linkplain Thread#interrupt interrupted while acquiring
     * the lock,
     *
     * </ul>
     *
     * then @link InterruptedException is thrown and the current thread's
     * interrupted status is cleared.
     *
     * <p>In this implementation, as this method is an explicit
     * interruption point, preference is given to responding to the
     * interrupt over normal or reentrant acquisition of the lock.
     *
     * @throws InterruptedException if the current thread is interrupted
     */
    public void lockInterruptibly() throws InterruptedException 
        sync.acquireInterruptibly(1);
    
 /**
     * Acquires in exclusive mode, aborting if interrupted.
     * Implemented by first checking interrupt status, then invoking
     * at least once @link #tryAcquire, returning on
     * success.  Otherwise the thread is queued, possibly repeatedly
     * blocking and unblocking, invoking @link #tryAcquire
     * until success or the thread is interrupted.  This method can be
     * used to implement method @link Lock#lockInterruptibly.
     *
     * @param arg the acquire argument.  This value is conveyed to
     *        @link #tryAcquire but is otherwise uninterpreted and
     *        can represent anything you like.
     * @throws InterruptedException if the current thread is interrupted
     */
    public final void acquireInterruptibly(int arg)
            throws InterruptedException 
		// 若果当前线程被打断,直接抛出异常
        if (Thread.interrupted())
            throw new InterruptedException();
		// 尝试获取资源
        if (!tryAcquire(arg))
        	// 调用AQS可被中断的方法
            doAcquireInterruptibly(arg);
    


boolean tryLock() 方法

尝试获取锁,如果当前该锁没有被其他线程持有,则当前线程获取该锁并返回true,否则返回false。注意,该方法不会引起当前线程阻塞

  /**
     * Acquires the lock only if it is not held by another thread at the time
     * of invocation.
     *
     * <p>Acquires the lock if it is not held by another thread and
     * returns immediately with the value @code true, setting the
     * lock hold count to one. Even when this lock has been set to use a
     * fair ordering policy, a call to @code tryLock() <em>will</em>
     * immediately acquire the lock if it is available, whether or not
     * other threads are currently waiting for the lock.
     * This &quot;barging&quot; behavior can be useful in certain
     * circumstances, even though it breaks fairness. If you want to honor
     * the fairness setting for this lock, then use
     * @link #tryLock(long, TimeUnit) tryLock(0, TimeUnit.SECONDS) 
     * which is almost equivalent (it also detects interruption).
     *
     * <p>If the current thread already holds this lock then the hold
     * count is incremented by one and the method returns @code true.
     *
     * <p>If the lock is held by another thread then this method will return
     * immediately with the value @code false.
     *
     * @return @code true if the lock was free and was acquired by the
     *         current thread, or the lock was already held by the current
     *         thread; and @code false otherwise
     */
    public boolean tryLock() 
        return sync.nonfairTryAcquire(1);
    
    /**
         * Performs non-fair tryLock.  tryAcquire is implemented in
         * subclasses, but both need nonfair try for trylock method.
         */
        final boolean nonfairTryAcquire(int acquires) 
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) 
                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;
        

如上代码与非公平锁的tryAcquire()方法代码类似,所以tryLock()使用的是非公平策略。

boolean tryLock(long timeout, TimeUnit unit)

尝试获取锁,与tryLock()的不同之处在于,它设置了超时时间,如果超时时间到没有获取到该锁则返回false。

   public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException 
            // 调用AQS的tryAcquireNanos
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    

释放锁

void unlock() 方法

  • 尝试释放锁,如果当前线程持有该锁,则调用该方法会让该线程对该线程持有的AQS状态值减1,如果减去1后当前状态值为0,则当前线程会释放该锁,否则仅仅减1而已。

  • 如果当前线程没有持有该锁而调用了该方法则会抛出IllegalMonitorStateException异常

  /**
     * Attempts to release this lock.
     *
     * <p>If the current thread is the holder of this lock then the hold
     * count is decremented.  If the hold count is now zero then the lock
     * is released.  If the current thread is not the holder of this
     * lock then @link IllegalMonitorStateException is thrown.
     *
     * @throws IllegalMonitorStateException if the current thread does not
     *         hold this lock
     */
    public void unlock() 
        sync.release(1);
    
    /**
     * Releases in exclusive mode.  Implemented by unblocking one or
     * more threads if @link #tryRelease returns true.
     * This method can be used to implement method @link Lock#unlock.
     *
     * @param arg the release argument.  This value is conveyed to
     *        @link #tryRelease but is otherwise uninterpreted and
     *        can represent anything you like.
     * @return the value returned from @link #tryRelease
     */
    public final boolean release(int arg) 
        if (tryRelease(arg)) 
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        
        return false;
    

 protected final boolean tryRelease(int releases) 
            int c = getState() - releases;
            // 11 如果不是锁持有者调用 抛出IllegalMonitorStateException
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 12 若果可重入次数为0 ,则清空锁持有线程
            if (c == 0) 
                free = true;
                setExclusiveOwnerThread(null);
            
            // 13 设置可重入次数为原始值减1
            setState(c);
            return free;
        
  • 代码(11)所示,如果当前线程不是该锁持有者则直接抛出异常
  • 否则查看状态值是否为0,为0则说明当前线程要放弃对该锁的持有权,则执行代码(12)把当前锁持有者设置为null。
  • 如果状态值不为0,则仅仅让当前线程对该锁的可重入次数减1。


Demo : 使用ReentrantLock来实现一个简单的线程安全的list


import java.util.ArrayList;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author 小工匠
 * @version 1.0
 * @description: TODO
 * @date 2021/12/4 22:05
 * @mark: show me the code , change the world
 */
public class ReentrantLockList 

    //线程不安全的List
    private ArrayList<String> list = new ArrayList<String>();
    
    //独占锁,默认是非公平锁,传入true可以是公平锁
    private volatile ReentrantLock lock = new ReentrantLock();

    //往集合中添加元素
    public void add(String str) 
        lock.lock();
        try 
            list.add(str);
         finally 
            lock.unlock();
        
    

    //删除集合中的元素
    public void remove(String str) 
        lock.lock();
        try 
            list.remove(str);
         finally 
            lock.unlock();
        
    

    //根据索引获取集合中某个元素
    public String get(int index) 
        lock.lock();
        try 
            return list.get(index);
         finally 
            lock.unlock();
        
    

    

如上代码通过在操作array元素前进行加锁保证同一时间只有一个线程可以对array数组进行修改,但是也只能有一个线程对array元素进行访问。

同样最后使用图 来加深理解。

假如线程Thread1、Thread2和Thread3同时尝试获取独占锁ReentrantLock,假设Thread1获取到了,则Thread2和Thread3就会被转换为Node节点并被放入ReentrantLock对应的AQS阻塞队列,而后被阻塞挂起。 如上图。


假设Thread1获取锁后调用了对应的锁创建的条件变量1,那么Thread1就会释放获取到的锁,然后当前线程就会被转换为Node节点插入条件变量1的条件队列。

由于Thread1释放了锁,所以阻塞到AQS队列里面的Thread2和Thread3就有机会获取到该锁,假如使用的是公平策略,那么这时候Thread2会获取到该锁,从而从AQS队列里面移除Thread2对应的Node节点。 如下图


小结

我们梳理了ReentrantLock的实现原理,ReentrantLock的底层是使用AQS实现的可重入独占锁。

在这里AQS状态State值为0表示当前锁空闲,为大于等于1的值则说明该锁已经被占用。该锁内部有公平与非公平实现,默认情况下是非公平的实现。

另外,由于该锁是独占锁,所以某时只有一个线程可以获取该锁。

以上是关于Java Review - 并发编程_独占锁ReentrantLock原理&源码剖析的主要内容,如果未能解决你的问题,请参考以下文章

Java Review - 并发编程_锁的分类

Java Review - 并发编程_锁的分类

Java Review - 并发编程_LinkedBlockingQueue原理&源码剖析

Java Review - 并发编程_LinkedBlockingQueue原理&源码剖析

Java Review - 并发编程_抽象同步队列AQS

Java Review - 并发编程_抽象同步队列AQS