ReentrantLock源码解析

ReentrantLock介绍

  • ReentrantLock是一个可重入的互斥锁,又被称为"独占锁"。
  • ReentrantLock锁在同一个时间点只能被一个线程锁持有;可重入表示,ReentrantLock锁可以被同一个线程多次获取。
  • ReentraantLock是通过一个FIFO的等待队列来管理获取该锁所有线程的。在"公平锁"的机制下,线程依次排队获取锁;而"非公平锁"在锁是可获取状态时,不管自己是不是在队列的开头都会获取锁。

锁的概念

  • 独占锁:是指该锁一次只能被一个线程所持有。

  • 共享锁:共享锁是指该锁可被多个线程所持有(只能再加共享锁)。

  • 可重入锁:可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。

  • 公平锁:先后顺序获取锁。

  • 非公平锁:随机的,并不会遵循先来先得的规则,所有线程会竞争获取锁。

  • 乐观锁:乐观锁在操作数据时非常乐观,认为别人不会同时修改数据。

    因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。

  • 悲观锁:悲观锁在操作数据时比较悲观,认为别人会同时修改数据。

    因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。

ReentrantLock源码

ReentrantLock结构:

ReentrantLock源码:

构造方法创建公平锁与非公锁(默认)。

arduino 复制代码
private final Sync sync;
//默认创建非公平锁
public ReentrantLock() {  
sync = new NonfairSync();  
}  
  
public ReentrantLock(boolean fair) {  
sync = fair ? new FairSync() : new NonfairSync();  
}

非公平锁的实现原理

lock方法获取锁

  1. lock方法调用CAS方法设置state的值,如果state等于期望值0(代表锁没有被占用),那么就将state更新为1(代表该线程获取锁成功),然后执行setExclusiveOwnerThread方法直接将该线程设置成锁的所有者。如果CAS设置state的值失败,即state不等于0,代表锁正在被占领着,则执行acquire(1),即下面的步骤。
  2. nonfairTryAcquire方法首先调用getState方法获取state的值,如果state的值为0(之前占领锁的线程刚好释放了锁),那么用CAS这是state的值,设置成功则将该线程设置成锁的所有者,并且返回true。如果state的值不为0,那就调用getExclusiveOwnerThread方法查看占用锁的线程是不是自己 ,如果是的话那就直接将state + 1,然后返回true。如果state不为0且锁的所有者又不是自己,那就返回false然后线程会进入到同步队列中
java 复制代码
final void lock() {
    //CAS操作设置state的值
    if (compareAndSetState(0, 1))
        //设置成功 直接将锁的所有者设置为当前线程 流程结束
        setExclusiveOwnerThread(Thread.currentThread());
    else
        //设置失败 则进行后续的加入同步队列准备
        acquire(1);
}

public final void acquire(int arg) {
    //调用子类重写的tryAcquire方法 如果tryAcquire方法返回false 那么线程就会进入同步队列
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

//子类重写的tryAcquire方法
protected final boolean tryAcquire(int acquires) {
    //调用nonfairTryAcquire方法
    return nonfairTryAcquire(acquires);
}

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    //如果状态state=0,即在这段时间内 锁的所有者把锁释放了 那么这里state就为0
    if (c == 0) {
        //使用CAS操作设置state的值
        if (compareAndSetState(0, acquires)) {
            //操作成功 则将锁的所有者设置成当前线程 且返回true,也就是当前线程不会进入同步
            //队列。
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //如果状态state不等于0,也就是有线程正在占用锁,那么先检查一下这个线程是不是自己
    else if (current == getExclusiveOwnerThread()) {
        //如果线程就是自己了,那么直接将state+1,返回true,不需要再获取锁 因为锁就在自己
        //身上了。
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    //如果state不等于0,且锁的所有者又不是自己,那么线程就会进入到同步队列。
    return false;
}

tryRelease锁的释放

  1. 判断当前线程是不是锁的所有者,如果是则进行步骤2,如果不是则抛出异常。

  2. 判断此次释放锁后state的值是否为0,如果是则代表锁有没有重入 ,然后将锁的所有者设置成null且返回true,然后执行步骤3,如果不是则代表锁发生了重入 执行步骤4

  3. 现在锁已经释放完,即state=0,唤醒同步队列中的后继节点进行锁的获取。

  4. 锁还没有释放完,即state!=0,不唤醒同步队列。 出处。

java 复制代码
public void unlock() {
    sync.release(1);
}

public final boolean release(int arg) {
    //子类重写的tryRelease方法,需要等锁的state=0,即tryRelease返回true的时候,才会去唤醒其
    //它线程进行尝试获取锁。
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
    
protected final boolean tryRelease(int releases) {
    //状态的state减去releases
    int c = getState() - releases;
    //判断锁的所有者是不是该线程
    if (Thread.currentThread() != getExclusiveOwnerThread())
        //如果所的所有者不是该线程 则抛出异常 也就是锁释放的前提是线程拥有这个锁,
        throw new IllegalMonitorStateException();
    boolean free = false;
    //如果该线程释放锁之后 状态state=0,即锁没有重入,那么直接将将锁的所有者设置成null
    //并且返回true,即代表可以唤醒其他线程去获取锁了。如果该线程释放锁之后state不等于0,
    //那么代表锁重入了,返回false,代表锁还未正在释放,不用去唤醒其他线程。
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

公平锁的实现原理

lock方法获取锁

  • 获取状态的state的值,如果state=0即代表锁没有被其它线程占用(但是并不代表同步队列没有线程在等待),执行步骤2。如果state!=0则代表锁正在被其它线程占用,执行步骤3

  • 判断同步队列是否存在线程(节点),如果不存在则直接将锁的所有者设置成当前线程,且更新状态state,然后返回true。

  • 判断锁的所有者是不是当前线程,如果是则更新状态state的值,然后返回true,如果不是,那么返回false,即线程会被加入到同步队列中

通过步骤2实现了锁获取的公平性,即锁的获取按照先来先得的顺序,后来的不能抢先获取锁,非公平锁和公平锁也正是通过这个区别来实现了锁的公平性。

scss 复制代码
final void lock() {
    acquire(1);
}

public final void acquire(int arg) {
    //同步队列中有线程 且 锁的所有者不是当前线程那么将线程加入到同步队列的尾部,
    //保证了公平性,也就是先来的线程先获得锁,后来的不能抢先获取。
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    //判断状态state是否等于0,等于0代表锁没有被占用,不等于0则代表锁被占用着。
    if (c == 0) {
        //调用hasQueuedPredecessors方法判断同步队列中是否有线程在等待,如果同步队列中没有
        //线程在等待 则当前线程成为锁的所有者,如果同步队列中有线程在等待,则继续往下执行
        //这个机制就是公平锁的机制,也就是先让先来的线程获取锁,后来的不能抢先获取。
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //判断当前线程是否为锁的所有者,如果是,那么直接更新状态state,然后返回true。
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    //如果同步队列中有线程存在 且 锁的所有者不是当前线程,则返回false。
    return false;
}

lockInterruptibly可中断方式获取锁

ReentrantLock相对于Synchronized拥有一些更方便的特性,比如可以中断的方式去获取锁。

java 复制代码
public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
}

public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    //如果当前线程已经中断了,那么抛出异常
    if (Thread.interrupted())
        throw new InterruptedException();
    //如果当前线程仍然未成功获取锁,则调用doAcquireInterruptibly方法,这个方法和
    //acquireQueued方法没什么区别,就是线程在等待状态的过程中,如果线程被中断,线程会
    //抛出异常。
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);
}

ryLock超时等待方式获取锁

ReentrantLock除了能以能中断的方式去获取锁,还可以以超时等待的方式去获取锁,所谓超时等待就是线程如果在超时时间内没有获取到锁,那么就会返回false,而不是一直"死循环"获取。

  1. 判断当前节点是否已经中断,已经被中断过则抛出异常,如果没有被中断过则尝试获取锁,获取失败则调用doAcquireNanos方法使用超时等待的方式获取锁。
  2. 将当前节点封装成独占模式的节点加入到同步队列的队尾中。
  3. 进入到"死循环"中,但是这个死循环是有个限制的,也就是当线程达到超时时间了仍未获得锁,那么就会返回false,结束循环 。这里调用的是LockSupport.parkNanos方法,在超时时间内没有被中断,那么线程会从超时等待状态转成了就绪状态 ,然后被CPU调度继续执行循环,而这时候线程已经达到超时等到的时间,返回false

LockSuport的方法能响应Thread.interrupt,但是不会抛出异常

java 复制代码
public boolean tryLock(long timeout, TimeUnit unit)
        throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    //如果当前线程已经中断了  则抛出异常
    if (Thread.interrupted())
        throw new InterruptedException();
    //再尝试获取一次 如果不成功则调用doAcquireNanos方法进行超时等待获取锁
    return tryAcquire(arg) ||
        doAcquireNanos(arg, nanosTimeout);
}

private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (nanosTimeout <= 0L)
        return false;
    //计算超时的时间 即当前虚拟机的时间+设置的超时时间
    final long deadline = System.nanoTime() + nanosTimeout;
    //调用addWaiter将当前线程封装成独占模式的节点 并且加入到同步队列尾部
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            //如果当前节点的前驱节点为头结点 则让当前节点去尝试获取锁。
            if (p == head && tryAcquire(arg)) {
                //当前节点获取锁成功 则将当前节点设置为头结点,然后返回true。
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return true;
            }
            //如果当前节点的前驱节点不是头结点 或者 当前节点获取锁失败,
            //则再次判断当前线程是否已经超时。
            nanosTimeout = deadline - System.nanoTime();
            if (nanosTimeout <= 0L)
                return false;
            //调用shouldParkAfterFailedAcquire方法,告诉当前节点的前驱节点 我要进入
            //等待状态了,到我了记得喊我,即做好进入等待状态前的准备。
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                //调用LockSupport.parkNanos方法,将当前线程设置成超时等待的状态。
                LockSupport.parkNanos(this, nanosTimeout);
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

ReentrantLock的等待/通知机制

我们知道关键字Synchronized + ObjectwaitnotifynotifyAll方法能实现等待/通知 机制,那么ReentrantLock是否也能实现这样的等待/通知机制,答案是:可以。
ReentrantLock通过Condition对象,也就是条件队列 实现了和waitnotifynotifyAll相同的语义。 线程执行condition.await()方法,将节点1从同步队列转移到条件队列中。

线程执行condition.signal()方法,将节点1从条件队列中转移到同步队列。

因为只有在同步队列中的线程才能去获取锁,所以通过Condition对象的waitsignal方法能实现等待/通知机制。

代码示例:

csharp 复制代码
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void await() {
    lock.lock();
    try {
        System.out.println("线程获取锁----" + Thread.currentThread().getName());
        condition.await(); //调用await()方法 会释放锁,和Object.wait()效果一样。
        System.out.println("线程被唤醒----" + Thread.currentThread().getName());
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        lock.unlock();
        System.out.println("线程释放锁----" + Thread.currentThread().getName());
    }
}

public void signal() {
    try {
        Thread.sleep(1000);  //休眠1秒钟 等等一个线程先执行
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    lock.lock();
    try {
        System.out.println("另外一个线程获取到锁----" + Thread.currentThread().getName());
        condition.signal();
        System.out.println("唤醒线程----" + Thread.currentThread().getName());
    } finally {
        lock.unlock();
        System.out.println("另外一个线程释放锁----" + Thread.currentThread().getName());
    }
}

public static void main(String[] args) {
    Test t = new Test();
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            t.await();
        }
    });

    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            t.signal();
        }
    });

    t1.start();
    t2.start();
}

运行输出:

css 复制代码
线程获取锁----Thread-0
另外一个线程获取到锁----Thread-1
唤醒线程----Thread-1
另外一个线程释放锁----Thread-1
线程被唤醒----Thread-0
线程释放锁----Thread-0

执行的流程大概是这样,线程t1先获取到锁,输出了"线程获取锁----Thread-0",然后线程t1调用await方法,调用这个方法的结果就是线程t1释放了锁进入等待状态,等待唤醒 ,接下来线程t2获取到锁,然输出了"另外一个线程获取到锁----Thread-1",同时线程t2调用signal方法,调用这个方法的结果就是唤醒一个在条件队列(Condition)的线程,然后线程t1被唤醒,而这个时候线程t2并没有释放锁,线程t1也就没法获得锁,等线程t2继续执行输出"唤醒线程----Thread-1"之后线程t2释放锁且输出"另外一个线程释放锁----Thread-1",这时候线程t1获得锁,继续往下执行输出了线程被唤醒----Thread-0,然后释放锁输出"线程释放锁----Thread-0"

如果想单独唤醒部分线程应该怎么做呢?这时就有必要使用多个Condition对象了,因为ReentrantLock支持创建多个Condition对象,例如:

ini 复制代码
//为了减少篇幅 仅给出伪代码
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
Condition condition1 = lock.newCondition();

//线程1 调用condition.await() 线程进入到条件队列
condition.await();

//线程2 调用condition1.await() 线程进入到条件队列
condition1.await();

//线程32 调用condition.signal() 仅唤醒调用condition中的线程,不会影响到调用condition1。
condition1.await();

ReentrantLock和Synchronized对比

参考资料

深入理解ReentrantLock的实现原理 - 掘金 (juejin.cn) 对不起哥,你写的太好了

相关推荐
洞窝技术6 分钟前
MYSQL:关于索引你想知道的
后端·mysql
MrWho不迷糊15 分钟前
企业级权限系统怎么设计四 —— ABAC模型统一功能权限与数据权限
后端·微服务
落尘29837 分钟前
Spring MVC——传递参数的方式
后端
ITCharge1 小时前
Docker 万字教程:从入门到掌握
后端·docker·容器
落尘2981 小时前
Bean 的作用域和生命周期
后端
是店小二呀1 小时前
处理Linux下磁盘空间不足问题的实用指南
后端
落尘2981 小时前
如何通过 JWT 来解决登录认证问题
后端
是店小二呀1 小时前
处理Linux下内存泄漏问题的诊断与解决方法
后端
倚栏听风雨1 小时前
IDEA 插件开发 对文件夹下的类进行 语法检查
后端
郝同学的测开笔记2 小时前
云原生探索系列(十七):Go 语言sync.Cond
后端·云原生·go