博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
深入理解ReentrantLock的实现原理
阅读量:6238 次
发布时间:2019-06-22

本文共 11712 字,大约阅读时间需要 39 分钟。

ReentrantLock简介

ReentrantLockJavaJDK1.5引入的显式锁,在实现原理和功能上都和内置锁(synchronized)上都有区别,在文章最后我们再比较这两个锁。

首先我们要知道ReentrantLock是基于AQS实现的,所以我们得对AQS有所了解才能更好的去学习掌握ReentrantLock,关于AQS的介绍可以参考我之前写的一篇文章,这里简单回顾下AQS

AQS回顾

AQSAbstractQueuedSynchronizer的缩写,这个是个内部实现了两个队列的抽象类,分别是同步队列条件队列。其中同步队列是一个双向链表,里面储存的是处于等待状态的线程,正在排队等待唤醒去获取锁,而条件队列是一个单向链表,里面储存的也是处于等待状态的线程,只不过这些线程唤醒的结果是加入到了同步队列的队尾,AQS所做的就是管理这两个队列里面线程之间的等待状态-唤醒的工作。

在同步队列中,还存在2中模式,分别是独占模式共享模式,这两种模式的区别就在于AQS在唤醒线程节点的时候是不是传递唤醒,这两种模式分别对应独占锁共享锁
AQS是一个抽象类,所以不能直接实例化,当我们需要实现一个自定义锁的时候可以去继承AQS然后重写获取锁的方式释放锁的方式还有管理state,而ReentrantLock就是通过重写了AQStryAcquiretryRelease方法实现的lockunlock

ReentrantLock原理

通过前面的回顾,是不是对ReentrantLock有了一定的了解了,ReentrantLock通过重写锁获取方式锁释放方式这两个方法实现了公平锁非公平锁,那么ReentrantLock是怎么重写的呢,这也就是本节需要探讨的问题。

ReentrantLock结构

首先
ReentrantLock继承自父类
Lock,然后有
3个内部类,其中
Sync内部类继承自
AQS,另外的两个内部类继承自
Sync,这两个类分别是用来
公平锁和非公平锁的。
通过
Sync重写的方法
tryAcquire
tryRelease可以知道,
ReentrantLock实现的是AQS的独占模式,也就是独占锁,这个锁是悲观锁

ReentrantLock有个重要的成员变量:

private final Sync sync;复制代码

这个变量是用来指向Sync的子类的,也就是FairSync或者NonfairSync,这个也就是多态的父类引用指向子类,具体Sycn指向哪个子类,看构造方法:

public ReentrantLock() {    sync = new NonfairSync();}public ReentrantLock(boolean fair) {    sync = fair ? new FairSync() : new NonfairSync();}复制代码

ReentrantLock有两个构造方法,无参构造方法默认是创建非公平锁,而传入true为参数的构造方法创建的是公平锁

非公平锁的实现原理

当我们使用无参构造方法构造的时候即ReentrantLock lock = new ReentrantLock(),创建的就是非公平锁。

public ReentrantLock() {    sync = new NonfairSync();}//或者传入false参数 创建的也是非公平锁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然后线程会进入到同步队列中

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,不唤醒同步队列。

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方法获取锁

  1. 获取状态的state的值,如果state=0即代表锁没有被其它线程占用(但是并不代表同步队列没有线程在等待),执行步骤2。如果state!=0则代表锁正在被其它线程占用,执行步骤3
  2. 判断同步队列是否存在线程(节点),如果不存在则直接将锁的所有者设置成当前线程,且更新状态state,然后返回true。
  3. 判断锁的所有者是不是当前线程,如果是则更新状态state的值,然后返回true,如果不是,那么返回false,即线程会被加入到同步队列中

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

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;}复制代码

tryRelease锁的释放

公平锁的释放和非公平锁的释放一样,这里就不重复。

公平锁和非公平锁的公平性是在获取锁的时候体现出来的,释放的时候都是一样释放的。

lockInterruptibly可中断方式获取锁

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

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);}复制代码

tryLock超时等待方式获取锁

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

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

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

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方法能实现等待/通知机制。

代码示例:

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();}复制代码

运行输出:

线程获取锁----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对象,例如:

//为了减少篇幅 仅给出伪代码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对比

关于Synchronized的介绍可以看、

ReentrantLock Synchronized
底层实现 通过AQS实现 通过JVM实现,其中synchronized又有多个类型的锁,除了重量级锁是通过monitor对象(操作系统mutex互斥原语)实现外,其它类型的通过对象头实现。
是否可重入
公平锁
非公平锁
锁的类型 悲观锁、显式锁 悲观锁、隐式锁(内置锁)
是否支持中断
是否支持超时等待
是否自动获取/释放锁

参考

《Java并发编程的艺术》

原文地址:

转载于:https://juejin.im/post/5c95df97e51d4551d06d8e8e

你可能感兴趣的文章
计算机网络与Internet应用
查看>>
oracle在线迁移同步数据,数据库报错
查看>>
linux性能剖析工具
查看>>
flutter中的异步
查看>>
计算机高手也不能编出俄罗斯方块——计算机达人成长之路(16)
查看>>
error LNK2001: 无法解析的外部符号 __CrtDbgReport
查看>>
# 2017-2018-1 20155224 《信息安全系统设计基础》第七周学习总结
查看>>
scikit-learn预处理实例之一:使用FunctionTransformer选择列
查看>>
邮件客户端导入邮件通讯录地址薄
查看>>
荆慕瑶
查看>>
EIGRP 查看邻居命令详解
查看>>
Linux启动的顺序说明
查看>>
Linux系统安装
查看>>
Oracle数据库的体系结构
查看>>
Cassandra监控 - OpsCenter手册
查看>>
rm: cannot remove `libtoolT': No such file or directory
查看>>
shell特殊符号cut命令,sort、wc、uniq命令,tee、tr、split命令
查看>>
Python第一天:Python擅长领域以及各种重点学习框架(包含Python在世界上的应用)...
查看>>
CentOS 7命令行安装GNOME、KDE图形界面
查看>>
如何用C++做游戏(3)
查看>>