可重入锁ReentrantLock基础和原理

47 阅读14分钟

ReenterantLock基础

ReenterantLocksynchronized关键字的扩展。它是一个显示锁,意味着锁的获取和释放必须由程序员手动编写代码控制。

相对于synchronized,它具备以下的优势:

  1. 未获取锁后的状态可中断
  2. 可以设置超时时间
  3. 可以设置公平锁
  4. 支持多个条件变量

并且它与synchronized一样都支持可重入。

可重入

可重入是指同一个线程如果首次获得这把锁后,因为它是这把锁的主人,可以再次获得这把锁。

相对的,不可重入是指获得这把锁之后不能再次获得这把锁,自身也会被锁挡住。

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ReentrantLock reentrantLock=new ReentrantLock();
        for (int i=0;i<100;i++){
            testReentrantLock(reentrantLock,i);
        }
    }
    public static void testReentrantLock(ReentrantLock reentrantLock,int number){
        reentrantLock.lock();
        System.out.printf("第%d次获得锁\n",number);
    }

未获得锁后的状态可打断

先看synchronized的行为。

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Object lock=new Object();
        Thread thread1=new Thread(()->{
            synchronized (lock){
                while(true){

                }
            }
        });
        thread1.start();
        TimeUnit.SECONDS.sleep(1);
        Thread thread2=new Thread(()->{
            synchronized (lock){
                
            }
            System.out.println("running");
        });
        thread2.start();
        TimeUnit.SECONDS.sleep(1);
        System.out.println("线程1:"+thread1.getState());
        System.out.println("线程2:"+thread2.getState());
        thread2.interrupt();

        System.out.println("线程2:"+thread2.getState());
    }

程序运行结果如下:

image.png

再看ReentrantLocklock行为。

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ReentrantLock lock=new ReentrantLock();
        Thread thread1=new Thread(()->{
            lock.lock();
            while(true){

            }
        });
        thread1.start();
        TimeUnit.SECONDS.sleep(1);
        Thread thread2=new Thread(()->{
            try {
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                System.out.println("interrupted");
                return ;
            }
            System.out.println("running");
            lock.unlock();
        });
        thread2.start();
        TimeUnit.SECONDS.sleep(1);
        System.out.println("线程1:"+thread1.getState());
        System.out.println("线程2:"+thread2.getState());
        thread2.interrupt();

        System.out.println("线程2:"+thread2.getState());
    }

结果如下:

image.png

可以看出通过synchronized获取锁失败后是进入BLOCKED状态,而通过ReentrantLock获取锁失败后是进入WAITTING状态。

接下来继续观察lockInterruptibly

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ReentrantLock lock=new ReentrantLock();
        Thread thread1=new Thread(()->{
            lock.lock();
            while(true){

            }
        });
        thread1.start();
        TimeUnit.SECONDS.sleep(1);
        Thread thread2=new Thread(()->{
            try {
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                System.out.println("interrupted");
                return ;
            }
//            lock.lock();
            System.out.println("running");
            lock.unlock();
        });
        thread2.start();
        TimeUnit.SECONDS.sleep(1);
        System.out.println("线程1:"+thread1.getState());
        System.out.println("线程2:"+thread2.getState());
        thread2.interrupt();

        System.out.println("线程2:"+thread2.getState());
    }

结果是:

image.png

先探究一个问题:为什么BLOCKED不可以被打断,WAITING可以被打断?

我们知道,interrupt()Java线程中用于协作式地通知一个线程应该停止当前正在执行的任务的核心机制。它的核心作用是设置线程的中断标志位,而不是强制终止线程。

线程状态调用 t.interrupt()的影响
RUNNABLE仅设置中断标志位。线程的 isInterrupted()将返回 true,但线程会继续运行,直到它自己检查这个标志。
BLOCKED (等锁)仅设置中断标志位。线程依然会卡在 BLOCKED状态,继续等它的锁。不会抛出 InterruptedException。直到它终于拿到锁,进入 RUNNABLE后,才能通过检查标志位来响应中断。
WAITING TIMED_WAITING (等通知、睡眠、合并)立即抛出 InterruptedException,并且清除中断标志位(重置为false)。线程会从 wait(), join(), sleep()等方法调用中退出,进入 RUNNABLE状态。

那为什么ReentrantLocklock也是WAITING状态,为什么不会被打断呢?

因为导致WAITING状态的原因和底层机制不同,ReentrantLock的加锁底层就是LockSupport.park()

进入 WAITING的方法interrupt()的响应说明
Object.wait() Thread.sleep() Thread.join()立即响应,抛出 InterruptedException。并清除中断状态这些是Java标准库提供的协作式中断原语,设计目的就是快速响应中断请求。
LockSupport.park()立即解除阻塞,但不抛出异常,只是安静地返回。但是保留中断状态这是更底层的线程阻塞原语。中断会唤醒它,但行为的控制权完全交给了上层的调用代码。ReentrantLock就构建在此之上。

也就是说其实interrupt是唤醒了线程的,但是它被唤醒之后再次去尝试获得锁,导致又调用了LockSupport.lock

ReentrantLock.lockInterruptibly只是设定了唤醒之后抛出异常,这部分在后面的源码部分再深挖。

锁超时

通过synchronized获得锁时,如果未获得,则会一直BLOCKED,而ReentrantLock可以设置超时的时间。

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ReentrantLock lock=new ReentrantLock();
        Thread thread1=new Thread(()->{
            lock.lock();
            while(true){

            }
        });
        thread1.start();
        TimeUnit.SECONDS.sleep(1);
        Thread thread2=new Thread(()->{
//            try {
//                lock.lockInterruptibly();
//            } catch (InterruptedException e) {
//                System.out.println("interrupted");
//                return ;
//            }
            try {
                lock.tryLock(5,TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("running");
            //lock.unlock();
        });
        thread2.start();
        TimeUnit.SECONDS.sleep(1);
        System.out.println("线程1:"+thread1.getState());
        System.out.println("线程2:"+thread2.getState());
       //thread2.interrupt();

        System.out.println("线程2:"+thread2.getState());
    }

结果如下:

image.png

使用tryLock()再尝试在给定的时间内获取锁,如果没有获得,将直接进行剩余代码。

公平锁

synchronized是非公平锁,ReentrantLock默认也是非公平锁。 以下代码启用公平锁:

ReentrantLock lock=new ReentrantLock(true);

支持多个条件变量

synchronized中存在唯一的一个条件变量,也就是waitSet,任何调用wait的线程都将进入waitSet,通过notifyAll将会唤醒waitSet中的所有线程。

ReentrantLock支持多个条件变量。可以实现多个类似的waitSet,实现一种精细化的唤醒。

方法作用说明
await()使当前线程等待释放锁,进入等待状态
signal()唤醒一个等待线程从等待队列中唤醒一个
signalAll()唤醒所有等待线程唤醒该条件的所有线程
awaitNanos(long)限时等待超时自动唤醒

ReentrantLock非公平锁的实现原理

ReentrantLock使用无参构造的时候,默认使用非公平锁

    public ReentrantLock() {
        sync = new NonfairSync();
    }

加锁流程

加锁时,调用的是父类Synclock方法。

        final void lock() {
            if (!initialTryLock())
                acquire(1);
        }

lock之中,调用子类NofairSync覆写的initialTryLock

final boolean initialTryLock() {
    // 1. 获取当前请求锁的线程
    Thread current = Thread.currentThread();
    
    // 情况A: 锁目前完全空闲 (状态state为0)
    if (compareAndSetState(0, 1)) { // CAS操作: 如果当前状态是0,则原子性地设置为1
        setExclusiveOwnerThread(current); // 将当前线程设置为锁的独占所有者
        return true; // 获取锁成功
    }
    // 情况B: 锁已被当前线程持有 (可重入)
    else if (getExclusiveOwnerThread() == current) {
        int c = getState() + 1; // 状态值加1,记录重入次数
        if (c < 0) // 检查是否溢出(重入次数过多,超过int最大值)
            throw new Error("Maximum lock count exceeded");
        setState(c); // 更新状态值
        return true; // 重入成功
    }
    // 情况C: 锁被其他线程持有
    else
        return false; // 获取锁失败
}

根据上述代码,可知如果当前锁空闲,是可以直接去抢占锁的,实现了不公平锁,并且实现了可重入的功能。

如果加锁成功,那无后续了。

如果加锁失败,调用acquire(1)

    public final void acquire(int arg) {
        if (!tryAcquire(arg))
            acquire(null, arg, false, false, false, 0L);
    }

acquire方法中又会先调用子类NofairSync中的tryAcquire

        protected final boolean tryAcquire(int acquires) {
            if (getState() == 0 && compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

tryAcquire是再一次尝试获取锁,因为第一次获取锁失败了,因此使用getState()==0这种廉价的判断来确定是否可以竞争锁。并且这段代码没有处理可重入的情况。

如果第二次依然没有获取到锁,下面调用最后的acquire方法

final int acquire(Node node, int arg, boolean shared,
                      boolean interruptible, boolean timed, long time) {
        Thread current = Thread.currentThread();
        byte spins = 0, postSpins = 0;   // retries upon unpark of first thread
        boolean interrupted = false, first = false;
        Node pred = null;                // predecessor of node when enqueued

        for (;;) {
            /**
            * 第一个if的目的是:确保当前线程在阻塞等待前
            * 队列处于一个稳定、正确的状态,避免因前驱节点问题导致当前线程永远无法被唤醒。
            /
            if (!first  //条件1:当前节点不是第一个等待者
            && (pred = (node == null) ? null : node.prev) != null //条件2:存在前驱节点
            &&!(first = (head == pred))) //条件3:前驱节点不是头节点,并重新判断当前节点是不是第一个等待者
                {
                //当前节点有前驱节点,且前驱节点不是头节点
                if (pred.status < 0) { // 如果前驱节点已取消
                    cleanQueue();   //清理已取消的节点
                    continue;  //再次循环判断
                } else if (pred.prev == null) { //前驱节点的前驱节点为null,现在处于不完整状态
                    Thread.onSpinWait();    //提示CPU当前处于自旋等待状态
                    continue; //再次循环判断
                }
            }
            /**
            * 这个分支的目的是让第一个等待者自旋cas获取锁 或者 在阻塞之前给线程最后一次获得资源的机会
            */
            if (first || pred == null) { //条件:第一个等待者 或者 还未入队
                boolean acquired;
                try {
                    if (shared) // 共享模式获取锁
                        acquired = (tryAcquireShared(arg) >= 0);
                    else // 独占模式获取锁
                        acquired = tryAcquire(arg);
                } catch (Throwable ex) {
                    cancelAcquire(node, interrupted, false);
                    throw ex;
                }
                if (acquired) { 
                    if (first) {//如果获取锁成功,且该节点是第一个等待者
                        node.prev = null;
                        head = node;
                        pred.next = null;
                        node.waiter = null;
                        if (shared) //在共享模式
                            signalNextIfShared(node);
                        if (interrupted) //如果被打断了,需要恢复中断状态
                            current.interrupt();
                    }
                    return 1;
                }
            }
            if (node == null) { //第一次进入循环,创建一个新的节点
                if (shared) //共享模式创建共享节点
                    node = new SharedNode();
                else 
                    node = new ExclusiveNode();//独占模式创建独占节点
            } else if (pred == null) { // 已经创建节点,但是未加入队列
                node.waiter = current;
                Node t = tail;
                node.setPrevRelaxed(t);         // avoid unnecessary fence
                if (t == null) //队列为空,
                    tryInitializeHead(); //初始化头节点
                else if (!casTail(t, node)) //cas竞争尾节点
                    node.setPrevRelaxed(null);  // back out
                else
                    t.next = node; //cas竞争成功,完成入队
            } else if (first && spins != 0) {  // 如果是第一个等待者,自旋优化
                --spins;                        // reduce unfairness on rewaits
                Thread.onSpinWait();
            } else if (node.status == 0) { //节点已入队,自旋用完或不是第一个,且尚未设置等待状态。
                node.status = WAITING;          // enable signal and recheck
            } else { //最终阻塞
                long nanos;
                // 更新自旋次数,采用指数退避
                spins = postSpins = (byte)((postSpins << 1) | 1);
                // 根据是否超时选择阻塞方式
                if (!timed)
                    LockSupport.park(this);
                else if ((nanos = time - System.nanoTime()) > 0L)
                    LockSupport.parkNanos(this, nanos);
                else
                    break;
                // 被唤醒后清除状态
                node.clearStatus();
                //如果支持中断,且中断,退出
                if ((interrupted |= Thread.interrupted()) && interruptible)
                    break;
            }
        }
        return cancelAcquire(node, interrupted, interruptible);
    }

阶段1 队列健康检查和维护:确保当前节点有一个有效的、可唤醒自己的前驱节点

阶段2 获取资源的最后机会避免不必要的阻塞,在阻塞前抓住一切可能的机会直接获取资源。

阶段3 节点创建(延迟初始化):根据模式创建对应的节点类型

阶段4 节点入队(无锁并发插入):通过CAS竞争入队,入队分为两步,先设置前驱节点,再cas尾节点,如果队列为空,会调用tryInitializeHead()创建dummy头节点。

阶段5 自旋优化(减少上下文切换):重复流程1~4的流程几次

阶段6 设置等待状态:设置等待状态

阶段7 最终阻塞等待:调用 LockSupport.park阻塞

阶段8 取消获取:调用cancelAcquire,超时时间到或者被中断且支持中断。

节点在没有开始入队之前(没有pred)都可以去tryAcquire来获取锁。

锁重入原理

ReentrantLock加锁的时候,会调用以下方法:

        final boolean initialTryLock() {
            Thread current = Thread.currentThread();
            if (compareAndSetState(0, 1)) { // first attempt is unguarded
                setExclusiveOwnerThread(current);
                return true;
            } else if (getExclusiveOwnerThread() == current) {
                int c = getState() + 1;
                if (c < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(c);
                return true;
            } else
                return false;
        }

如果获取锁的时候,锁已经被获取了,判断持有者是否是当前线程,如果是当前线程的话,会让锁的state加1。

ReentrantLock解锁的时候,会调用以下方法:

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

会让锁的state减去releases,如果state为0才会去释放锁,否则仅仅只是更新锁的state

解锁流程

ReentrantLock调用Unlock进行解锁时, 会调用syncrelease方法。

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

release方法会调用tryRelease方法,并且在成功之后唤醒下一个节点。

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            signalNext(head);
            return true;
        }
        return false;
    }

tryRelease方法的逻辑很简单。

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

唤醒下一个节点

    private static void signalNext(Node h) {
        Node s;
        if (h != null && (s = h.next) != null && s.status != 0) {
            s.getAndUnsetStatus(WAITING);
            LockSupport.unpark(s.waiter);
        }
    }

更改后继节点的状态,并且唤醒。

不可打断的特性

以下是ReentrantLockacquire的部分源码

                long nanos;
                spins = postSpins = (byte)((postSpins << 1) | 1);
                if (!timed)
                    LockSupport.park(this);
                else if ((nanos = time - System.nanoTime()) > 0L)
                    LockSupport.parkNanos(this, nanos);
                else
                    break;
                node.clearStatus();
                if ((interrupted |= Thread.interrupted()) && interruptible)
                    break;

可以看到interruptible这个变量控制是否会跳出整个大循环。而在ReentrantLock中调用这个方法的参数为

   public final void acquire(int arg) {
        if (!tryAcquire(arg))
            acquire(null, arg, false, false, false, 0L);
    }

其中interruptible的参数是false,这就是不可打断的原理。

ReentrantLock公平锁的实现原理

ReentrantLock公平锁和非公平锁的主要区别就在于tryAcquire方法和initialTryLock的实现

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

可以看出不论是tryAcquire还是initialTryLock,在判断条件中都有hasQueuedThreads()来判断同步队列中是否有其他线程。

ReentrantLock的条件变量实现

每一个条件变量对应着一个等待队列,其实现类是ConditionObject。其在内部维护了一个单向的FIFO等待队列。

    public class ConditionObject implements Condition, java.io.Serializable {
        private static final long serialVersionUID = 1173984872572414699L;
        /** First node of condition queue. */
        private transient ConditionNode firstWaiter;
        /** Last node of condition queue. */
        private transient ConditionNode lastWaiter;
}

ConditionNode的结构如下:数据域中仅有一个nextWaiter

    static final class ConditionNode extends Node
        implements ForkJoinPool.ManagedBlocker {
        ConditionNode nextWaiter;            
        public final boolean isReleasable() {
            return status <= 1 || Thread.currentThread().isInterrupted();
        }

        public final boolean block() {
            while (!isReleasable()) LockSupport.park();
            return true;
        }
    }

await的原理

以下是await的源码

 public final void await() throws InterruptedException {
     		//检查当前线程的中断状态,如果已被中断,立即抛出InterruptedException。Thread.interrupted()会清除中断标志。
            if (Thread.interrupted())
                throw new InterruptedException();
     		//创建一个ConditionNode节点
            ConditionNode node = new ConditionNode();
            int savedState = enableWait(node);
     		//设置阻塞的原因,方便调试
            LockSupport.setCurrentBlocker(this); // for back-compatibility
            boolean interrupted = false, cancelled = false, rejected = false;
     		//只要节点未被signal转移到同步队列,就不断循环
            while (!canReacquire(node)) {
                //如果发生中断
                if (interrupted |= Thread.interrupted()) {
                    //尝试清除节点的COND状态位(将其移出条件队列)
                    //如果清除成功(cancelled = true),说明中断发生在被signal之前,则跳出循环,走向取消逻辑。		   
                    //如果清除失败(cancelled = false),说明中断发生在被signal之后,节点已不在条件队列,此时不跳出循环,继续走后续的重获取锁流程。
                    if (cancelled = (node.getAndUnsetStatus(COND) & COND) != 0)
                        break;              // else interrupted after signal
                    //没发生中断,仍然在条件队列中等待
                } else if ((node.status & COND) != 0) {
                    //首选:ForkJoinPool.managedBlock(node): 这是一个高效、可扩展的阻塞方法,适用于ForkJoin工作线程。
					//备选: node.block(): 如果被线程池拒绝执行,则回退到普通的LockSupport.park()进行阻塞。
                    //在阻塞期间若被中断,则捕获异常并设置interrupted = true,在下一轮循环中由状态A处理。
                    try {
                        if (rejected)
                            node.block();
                        else
                            ForkJoinPool.managedBlock(node);
                    } catch (RejectedExecutionException ex) {
                        rejected = true;
                    } catch (InterruptedException ie) {
                        interrupted = true;
                    }
                    //节点不满足canReacquire(锁还未就绪),但COND状态位已清除,说明它正处在被signal后,从条件队列转移到同步队列的中间状态。
                    //此时自旋一段时间
                } else
                    Thread.onSpinWait();    // awoke while enqueuing
            }
     		//循环结束,清理并且重新获取锁
            LockSupport.setCurrentBlocker(null);
            node.clearStatus();
            acquire(node, savedState, false, false, false, 0L);
     		//在循环中发生中断的后置处理
            if (interrupted) {
                //如果中断发生在被signal之前,清理取消的节点,并且抛出中断异常
                if (cancelled) {
                    unlinkCancelledWaiters(node);
                    throw new InterruptedException();
                }
                //中断发生在唤醒之后,将中断的响应权交给上层
                Thread.currentThread().interrupt();
            }
        }

流程1:进行第一个中断检查

流程2:创建条件节点,并且调用enableWait(node),将节点加入条件队列,并且释放锁。

        private int enableWait(ConditionNode node) {
            //判断当前线程是否持有错
            if (isHeldExclusively()) {
                //设置节点的等待线程
                node.waiter = Thread.currentThread();
                //设置节点的状态为条件+等待
                node.setStatusRelaxed(COND | WAITING);
                //将节点加入到条件队列中去
                ConditionNode last = lastWaiter;
                if (last == null)
                    firstWaiter = node;
                else
                    last.nextWaiter = node;
                lastWaiter = node;
                //获取当前线程持有锁的state,并且释放锁
                int savedState = getState();
                if (release(savedState))
                    return savedState;
            }
            //如果线程不持有锁,直接报错
            node.status = CANCELLED; // lock not held or inconsistent
            throw new IllegalMonitorStateException();
        }

流程3:通过canReacquire函数不断判断当前节点是否被移到同步队列中

        private boolean canReacquire(ConditionNode node) {
            return node != null && node.prev != null && isEnqueued(node);
        }
        //检查节点是否在同步队列中
        final boolean isEnqueued(Node node) {
            for (Node t = tail; t != null; t = t.prev)
                if (t == node)
                    return true;
            return false;
        }

流程3.1:判断线程是否被中断

流程3.2:判断线程是否仍然在条件队列,如果在就阻塞

流程3.3:既然线程不在条件队列,但是也没有检测到在同步队列,那么就在中间态,自旋等待一会

流程4:在同步队列中被前驱节点唤醒,清除node的状态,并调用acquire方法获得锁。

流程5:处理之前中断的情况

signal的原理

以下是signal的源码

        public final void signal() {
            ConditionNode first = firstWaiter;
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            if (first != null)
                doSignal(first, false);
        }

先获取条件队列中的第一个节点,如果队列中存在等待的节点,则调用doSignal来唤醒。

        private void doSignal(ConditionNode first, boolean all) {
            while (first != null) {
                ConditionNode next = first.nextWaiter;
                //更新队头节点
                if ((firstWaiter = next) == null)
                    lastWaiter = null;
                //检查节点状态是否包含COND标记
                if ((first.getAndUnsetStatus(COND) & COND) != 0) {	
                    //将节点加入同步队列
                    enqueue(first);
                    if (!all)
                        break;
                }
                first = next;
            }

enqueue源码如下:

    final void enqueue(Node node) {
        if (node != null) {
            //CAS自旋
            for (;;) {
                Node t = tail;
                //将尾节点设置为node的前驱
                node.setPrevRelaxed(t);        // avoid unnecessary fence
                if (t == null)                 // initialize
                    tryInitializeHead();
                //将node设置成尾节点
                else if (casTail(t, node)) {
                    t.next = node;
                    if (t.status < 0)          // wake up to clean link
                        LockSupport.unpark(node.waiter);
                    break;
                }
            }
        }
    }