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

1,432 阅读4分钟

当我们在使用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在并发方面提供了更完善的机制,能满足开发者更多的并发策略需求。