写点什么

Java 并发底层知识,锁获取超时机制知多少?

用户头像
码农架构
关注
发布于: 2020 年 12 月 27 日
Java并发底层知识,锁获取超时机制知多少?

当我们在使用 Java 进行网络编程时经常会遇到很多超时的概念,比如一个浏览器请求过程就可能会产生很多超时的地方,当我们在浏览器发起一个请求后,网络 socket 读写可能会超时,web 服务器响应可能会超时,数据库查询可能会超时。而对于 Java 并发来说,与超时相关的内容主要是线程等待超时和获取锁超时,比如调用 Object.wait(long)就会使线程进入等待状并在指定时间后等待超时。



此篇主要讲解 Java 内置锁的获取操作的超时机制。当大量线程对某一锁竞争时可能导致某些线程在很长一段时间都获取不了锁,在某些场景下可能希望如果线程在一段时间内不能成功获取锁就取消对该锁的等待以提高性能,这时就需要用到超时机制。



 Synchronized 不支持超时

我们先看 Java 从语法层提供的并发锁——synchronized 关键词,synchronized 对我们来说是相当熟悉的了,它是 Java 内置的锁方案。在 Java 的世界,每个对象都关联着一个内置锁,当线程要访问被 synchronized 修饰的对象时都必须先获得其对应的锁才能继续访问,否则将一直等待直到该锁被其它线程所释放。普通对象和对象的方法都关联有对应的内置锁,所以它们都可以被 synchronized 修饰。



虽然 synchronized 使用很方便,但其存在一个缺点,那就是锁获取操作不支持超时机制。在并发的情况下,多个线程会去竞争被 synchronized 所修饰对应的锁对象,可能存在某个线程一直获取不到锁而一直处于阻塞等待状态。而这个处于阻塞状态的线程唯一能做的就是一直等待,我们没有办法设置一个等待超时时间。以下面的代码为例,线程一会先成功获取锁,在输出“Thread1 gets the lock”后进入睡眠,睡眠的时间很长。线程二较晚启动,它尝试获取锁,但该锁已被线程一所持有,所以线程一将永远获取不到锁而一直等待。


 AQS 同步器超时机制

在 JDK1.5 之前还没有 JUC 工具,当时的并发控制只能通过上述的 synchronized 关键词实现锁,但它对超时取消的控制力不从心。JDK1.5 开始引入的 JUC 工具则完美地解决了此问题,主要是因为 AQS 同步器提供了锁获取超时的支持。我们知道 AQS 同步器使用了队列的结构来处理等待的线程,AQS 获取锁的超时机制大致如下图所示。首先多个线程竞争锁,因为锁已被其它线程持有,所以通过自旋的 CAS 操作将各自线程添加到队尾。其次是在线程添加到队列后,每个线程节点都各自轮询前一节点看是否轮到自己获取锁。假如这里线程 2 设置了超时机制,且线程 2 在超时时间内都获取不到锁,则该线程对应的节点将被取消。最终线程 2 因为获取锁超时而被取消。

超时实现逻辑

为了更精确地保证时间间隔的准确性,实现时使用了更为精确的 System.nanoTime()方法,它能精确到纳秒级别。总体而言,超时机制的思想就是先计算 deadline 时间,然后在不断进行锁检查操作中计算是否已经到 deadline 时间,如果已到 deadline 时间则取消队列中的该节点并跳出循环。

AQS 的超时控制有两点必须要注意:

  • 一是超时时间包括了竞争入队的时间,如果竞  争入队就把超时时间消耗完的话则直接当作超时处理;

  • 另一个是关于 spinForTimeoutThreshold 变量阀值,它是决定使用自旋方式消耗时间还是使用系统阻塞方式消耗时间的分割线。

JUC 工具包作者通过测试将默认值设置为 1000ns,即如果在成功插入等待队列后剩余时间大于 1000ns 则调用系统底层阻塞。否则不调用系统底层阻塞,取而代之的是仅仅让其在 Java 层不断循环消耗时间,这属于性能优化的措施。


总结

Java 内置的 synchronized 关键词虽然提供了并发锁功能,但它却存在不支持超时的缺点。而 AQS 同步器则在获取锁的过程中提供了超时机制,同时我们深入分析了 AQS 获取锁超时的具体实现原理。获取锁超时的支持让 Java 在并发方面提供了更完善的机制,能满足开发者更多的并发策略需求。



发布于: 2020 年 12 月 27 日阅读数: 30
用户头像

码农架构

关注

公众号:码农架构 2018.03.22 加入

专注于系统架构、高可用、高性能、高并发类技术分享

评论

发布
暂无评论
Java并发底层知识,锁获取超时机制知多少?