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) 对不起哥,你写的太好了

相关推荐
NiNg_1_2344 分钟前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
Chrikk2 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*2 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue2 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man2 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
customer083 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
Yaml44 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
小码编匠5 小时前
一款 C# 编写的神经网络计算图框架
后端·神经网络·c#