您当前的位置:首页 > 电脑百科 > 程序开发 > 编程百科

还不知道ReentrantLock的实现流程,那你就out了

时间:2021-01-20 12:01:49  来源:  作者:

公平锁和非公平锁的区别

锁的公平是相对于获取锁的顺序而言的,如果是一个公平锁,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。在上面分析的例子来说,只要CAS设置同步状态成功,则表示当前线程获取了锁,而公平锁则不一样,差异点有两个

FairSync.tryAcquire

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

非公平锁在获取锁的时候,会先通过CAS进行抢占,则公平锁不会

FairSync.tryAcquire

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

这个方法与 nonfairTryAcquire(int acquires)比较,不同的地方在于判断条件多了 hasQueuedPredecessors()方法,也就是加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回 true,则表示有线程比当前线程更早地请求获取锁, 因此需要等待前驱线程获取并释放锁之后才能继续获取锁。

Condition

在synchronized中,有wait/notify可以实现线程的通信。那么J.U.C里面提供了锁的实现机制,肯定也提供了线程通信的机制

Condition是一个多线程协调通信的工具类,可以让某些线程一起等待某个条件(condition),只有满足条件时,线程才会被唤醒

Condition基本使用

public class ConditionDemoWait implements Runnable{
    private Lock lock;

    private Condition condition;
    public ConditionDemoWait(Lock lock , Condition condition){
        this.lock = lock;
        this.condition = condition;
    }

    public void run() {
        System.out.println("begin -ConditionDemoWait");

        try {
            lock.lock();
            condition.await();
            System.out.println("end - ConditionDemoWait");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }

    }
}
复制代码
public class ConditionDemoSignal implements Runnable{
    private Lock lock;
    private Condition condition;

    public ConditionDemoSignal(Lock lock , Condition condition){
        this.lock = lock;
        this.condition = condition;
    }
    public void run() {
        System.out.println("begin -ConditionDemoSignal");
        try {
            lock.lock();
            condition.signal();
            System.out.println("end - ConditionDemoSignal");
        }finally {
            lock.unlock();
        }

    }

    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
        new Thread(new ConditionDemoWait(lock , condition)).start();
        new Thread(new ConditionDemoSignal( lock , condition)).start();
    }
}
复制代码

结果:

begin -ConditionDemoWait
begin -ConditionDemoSignal
end - ConditionDemoSignal
end - ConditionDemoWait
复制代码

通过这个案例简单实现了wait和notify的功能,当调用await方法后,当前线程会释放锁并等待,而其他线程调用condition对象的signal或者signalall方法通知并被阻塞的线程,然后自己执行unlock释放锁,被唤醒的线程获得之前的锁继续执行,最后释放锁

所以,condition中两个最重要的方法,一个是await,一个是signal方法

  • await:把当前线程阻塞挂起
  • signal:唤醒阻塞的线程

Condition源码分析

每一个Condition对象上面,都阻塞了多个线程。因此,在ConditionObject内部也有一个双向链表 组成的队列,如下所示

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

        /**
         * Creates a new {@code ConditionObject} instance.
         */
        public ConditionObject() { }
}
  static final class Node {
    volatile Node prev;
    volatile Node next;
    volatile Thread thread;
    Node nextWaiter;
}
...
复制代码

调用Condition,需要获取lock锁,所以意味着会存在一个AQS同步队列,先来看Condition.await方法

condition.await

调用Condition的await方法(或者以await开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从await方法返回时,当前线程一定获取了Condition相关联的锁

public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            Node node = addConditionWaiter();//创建一个新的节点,节点状态为condition,采用数据结构是链表
            int savedState = fullyRelease(node);//释放当前的锁,得到锁的状态,并唤醒AQS队列中的一个线程
            int interruptMode = 0;
            //如果当前节点没有在同步队列中,即还没有被signal,则将当前线程阻塞
            while (!isOnSyncQueue(node)) {//判断这个节点是否在AQS队列上,第一次判断的是false,因为前面已经释放锁了
                LockSupport.park(this);//第一次总是park自己,开始阻塞等待
                //线程判断自己在等待过程中是否被中断了,如果没有中断,则再次循环,会在isOnSyncQueue中判断自己是否在队列上
                //isOnSyncQueue判断当前node状态,如果是CONDITION状态,或者不在队列上了,就继续阻塞
                //isOnSyncQueue判断当前node还在队列上且不是CONDITION状态了,就结束循环和阻塞
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            //当这个线程醒来,会尝试拿锁,当acquireQueued返回false就是拿到锁了
            //interruptMode != THROW_IE -> 表示这个线程没有成功将node入队,但signal执行了enq方法让其入队了
            //将这个变量设置成REINTERRUPT
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            //如果node的下一个等待者不是null,则进行清理,清理Condition队列上的节点
            //如果是null,就没有什么好清理的了
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            //如果线程被中断了,需要抛出异常,或者什么都不做
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }
复制代码

关于await,有几个关键点要说明:

  1. 线程调用await()的时候,肯定已经先拿到锁。所以,在addConditionWaiter()内部,对这个双向链表的操作不需要执行cas操作,线程天生是安全的,代码如下:
private Node addConditionWaiter() {
    // ...
    //拿到末端的node
    Node t = lastWaiter;
    //new一个node
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}
复制代码
  1. 在线程执行wait操作之前,必须先释放锁。也就是fullyRelease(node),否则会发生死锁。这个和wait/notify与synchronized的配合机制一样
  2. 线程从wait中被唤醒后,必须用acquireQueue(node,savedState)方法重新拿锁。
  3. checkInterruptWhileWaiting(node)代码在park(this)代码之后,是为了检测在park期间是否收到过中断信号。当线程从park中醒来时,有两种可能:一种是其他线程调用了unpark,另一种是收到中断信号。这里的await()方法是可以响应中断的,所以当发现自己是被中断唤醒的,而不是被unpark唤醒的时候,会直接退出while循环,await()方法也会返回。
  4. isOnSyncQueue(node)用于判断该Node是否在AQS的同步队列里面。初始的时候,Node只在Condition的队列里,而不在AQS的队列里。但执行notity操作的时候,会放进AQS的同步队列

awaitUninterruptibly实现分析

与await()不同,awaitUninterruptibly()不会响应中断,其方法的定义中不会有中断异常抛出,下面分析实现和await()的区别

public final void awaitUninterruptibly() {
            Node node = addConditionWaiter();
            int savedState = fullyRelease(node);
            boolean interrupted = false;
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if (Thread.interrupted())
                    interrupted = true;
            }
            if (acquireQueued(node, savedState) || interrupted)
                selfInterrupt();
        }
复制代码

可以看出,整体代码和await()类似,区别在于收到异常后,不会抛出异常,而是继续执行while循环

Condition.signal

调用Condition的signal方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中

        public final void signal() {
            if (!isHeldExclusively())//先判断当前线程是否获取了锁
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;//拿到Condition队列上第一个节点
            if (first != null)
                doSignal(first);
        }
复制代码

Condition.doSignal

        private void doSignal(Node first) {
            do {
                if ( (firstWaiter = first.nextWaiter) == null)//如果第一个节点的下一个节点是null,那么,最后一个节点也是null
                    lastWaiter = null;//将next节点设置成null
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }
复制代码
final boolean transferForSignal(Node node) {
        
        Node p = enq(node);
        int ws = p.waitStatus;
        //如果上一个节点的状态被取消了,或者尝试设置上一个节点的状态为SIGNAL失败了(SIGNAL 表示: 他的 next 节点需要停止阻塞)
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);//唤醒输入节点上的线程
        return true;
    }
复制代码

该方法先是CAS修改了节点状态,如果成功,就将这个节点放到AQS队列中,然后唤醒这个节点上的线程。此时,那个节点就会在await方法中苏醒

Condition总结

阻塞:await()方法中,在线程释放锁资源之后,如果节点不在 AQS 等待队 列,则阻塞当前线程,如果在等待队列,则自旋等待尝试获取锁

释放:signal()后,节点会从 condition 队列移动到 AQS 等待队列,则进入 正常锁的获取流程


作者:Five在努力
链接:https://juejin.cn/post/6918557057947795470



Tags:ReentrantLock   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
Java从版本5开始,在 java.util.concurrent.locks包内给我们提供了除了synchronized关键字以外的几个新的锁功能的实现,ReentrantLock就是其中的一个。但是这并不意味着我们可...【详细内容】
2021-12-17  Tags: ReentrantLock  点击:(10)  评论:(0)  加入收藏
公平锁和非公平锁的区别锁的公平是相对于获取锁的顺序而言的,如果是一个公平锁,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。在上面分析的例子来说,只要CAS设置...【详细内容】
2021-01-20  Tags: ReentrantLock  点击:(179)  评论:(0)  加入收藏
▌简易百科推荐
摘 要 (OF作品展示)OF之前介绍了用python实现数据可视化、数据分析及一些小项目,但基本都是后端的知识。想要做一个好看的可视化大屏,我们还要学一些前端的知识(vue),网上有很多比...【详细内容】
2021-12-27  项目与数据管理    Tags:Vue   点击:(1)  评论:(0)  加入收藏
程序是如何被执行的&emsp;&emsp;程序是如何被执行的?许多开发者可能也没法回答这个问题,大多数人更注重的是如何编写程序,却不会太注意编写好的程序是如何被运行,这并不是一个好...【详细内容】
2021-12-23  IT学习日记    Tags:程序   点击:(9)  评论:(0)  加入收藏
阅读收获✔️1. 了解单点登录实现原理✔️2. 掌握快速使用xxl-sso接入单点登录功能一、早期的多系统登录解决方案 单系统登录解决方案的核心是cookie,cookie携带会话id在浏览器...【详细内容】
2021-12-23  程序yuan    Tags:单点登录(   点击:(8)  评论:(0)  加入收藏
下载Eclipse RCP IDE如果你电脑上还没有安装Eclipse,那么请到这里下载对应版本的软件进行安装。具体的安装步骤就不在这赘述了。创建第一个标准Eclipse RCP应用(总共分为六步)1...【详细内容】
2021-12-22  阿福ChrisYuan    Tags:RCP应用   点击:(7)  评论:(0)  加入收藏
今天想简单聊一聊 Token 的 Value Capture,就是币的价值问题。首先说明啊,这个话题包含的内容非常之光,Token 的经济学设计也可以包含诸多问题,所以几乎不可能把这个问题说的清...【详细内容】
2021-12-21  唐少华TSH    Tags:Token   点击:(9)  评论:(0)  加入收藏
实现效果:假如有10条数据,分组展示,默认在当前页面展示4个,点击换一批,从第5个开始继续展示,到最后一组,再重新返回到第一组 data() { return { qList: [], //处理后...【详细内容】
2021-12-17  Mason程    Tags:VUE   点击:(14)  评论:(0)  加入收藏
什么是性能调优?(what) 为什么需要性能调优?(why) 什么时候需要性能调优?(when) 什么地方需要性能调优?(where) 什么时候来进行性能调优?(who) 怎么样进行性能调优?(How) 硬件配...【详细内容】
2021-12-16  软件测试小p    Tags:性能调优   点击:(19)  评论:(0)  加入收藏
Tasker 是一款适用于 Android 设备的高级自动化应用,它可以通过脚本让重复性的操作自动运行,提高效率。 不知道从哪里听说的抖音 app 会导致 OLED 屏幕烧屏。于是就现学现卖,自...【详细内容】
2021-12-15  ITBang    Tags:抖音防烧屏   点击:(23)  评论:(0)  加入收藏
11 月 23 日,Rust Moderation Team(审核团队)在 GitHub 上发布了辞职公告,即刻生效。根据公告,审核团队集体辞职是为了抗议 Rust 核心团队(Core team)在执行社区行为准则和标准上...【详细内容】
2021-12-15  InfoQ    Tags:Rust   点击:(24)  评论:(0)  加入收藏
一个项目的大部分API,测试用例在参数和参数值等信息会有很多相似的地方。我们可以复制API,复制用例来快速生成,然后做细微调整既可以满足我们的测试需求1.复制API:在菜单发布单...【详细内容】
2021-12-14  AutoMeter    Tags:AutoMeter   点击:(20)  评论:(0)  加入收藏
相关文章
    无相关信息
最新更新
栏目热门
栏目头条