性能方面,当并发量不高、竞争不激烈时,Synchronized同步锁具有层次锁的优点,性能与Lock类似;但在高负载、高并发的情况下,Synchronized同步锁具有层次锁的优点。如果竞争激烈,就会升级为重量级锁,性能不会像Lock锁那么稳定。
我们可以通过一组简单的性能测试来直观地比较两种锁的性能。结果如下所示。
通过上面的数据我们可以发现Lock锁的性能相对来说更加稳定。那么它与上一讲的Synchronized同步锁相比,它的实现原理是怎样的呢?
Lock 锁的实现原理
Lock是用Java实现的锁。 Lock是一个接口类。常用的实现类有ReentrantLock和ReentrantReadWriteLock(RRW),它们都是依赖于AbstractQueuedSynchronizer(AQS)类实现的。
AQS类结构包含一个基于链表的等待队列(CLH队列),用于存储所有被阻塞的线程。 AQS中还有一个状态变量,代表ReentrantLock的锁状态。
这个队列的操作都是通过CAS操作来实现的。我们可以通过一张图来看看整个锁的获取过程。
锁分离优化 Lock 同步锁
虽然Lock的性能稳定,但并不是所有场景都默认使用ReentrantLock独占锁来实现线程同步。
我们知道,读写同一份数据时,如果一个线程在读数据,另一个线程在写数据,那么读到的数据会和最终的数据不一致;如果一个线程在写数据,另一个线程也在写数据,那么读取到的数据会和最终的数据不一致。写入数据时,线程前后看到的数据会不一致。这时,我们可以在读写方法中添加互斥锁,以保证任一时刻只有一个线程可以执行读或写操作。
在大多数业务场景中,读业务操作远大于写业务操作。在多线程编程中,读操作不会修改共享资源的数据。如果多个线程只读取共享资源,这种情况下实际上不需要锁定资源。如果使用互斥锁,会影响业务的并发性能。那么在这种场景下,有没有什么办法可以优化加锁的实现呢?
1. 读写锁 ReentrantReadWriteLock
针对这种多读少写的场景,Java提供了另一个实现Lock接口的读写锁RRW。我们知道ReentrantLock是一种排它锁,只允许一个线程同时访问,而RRW允许多个读线程同时访问,但不允许写线程和读线程、写线程和写线程同时访问。读写锁内部维护两个锁,一个是读操作的ReadLock,一个是写操作的WriteLock。
那么读写锁如何实现锁分离来保证共享资源的原子性呢?
RRW也是基于AQS实现的。它的自定义同步器(继承自AQS)需要在同步状态上维护多个读线程和一个写线程的状态。这个状态的设计成为实现读写锁的关键。 RRW很好地利用了高低位,实现了用整数控制两种状态的功能。读写锁将变量分为两部分。高16位代表读,低16位代表写。
当一个线程尝试获取写锁时,会首先判断同步状态state是否为0,如果state等于0,则表示暂时没有其他线程获取到该锁;如果state不等于0,则说明其他线程已经获取了锁。
此时判断同步状态state的低16位(w)是否为0,如果w为0,则说明其他线程已经获取了读锁,则进入CLH队列阻塞等待;如果w不为0,则说明其他线程已经获取了读锁。线程获取写锁。这时候就需要判断当前线程是否获得了写锁。如果没有,则进入CLH队列阻塞等待;如果是,则判断当前线程获取写锁的次数是否超过最大次数。如果超过则抛出Exception,否则更新同步状态。
当一个线程尝试获取读锁时,它也会首先判断同步状态state是否为0。如果state等于0,则表示暂时没有其他线程获取到该锁。此时判断是否需要阻塞。如果需要阻塞,则进入CLH队列等待阻塞;如果不需要阻塞,CAS将同步状态更新为读状态。
如果state不等于0,则判断同步状态低于16位。如果有写锁,则读锁获取失败,进入CLH阻塞队列。否则,会判断当前线程是否应该被阻塞。如果不应该被阻塞,则会尝试CAS同步状态。同步锁成功更新为读状态。
我们通过一个找正方形的例子来体验一下RRW的实现。代码如下:
公共类TestRTTLock { 私有双x, y; private ReentrantReadWriteLock lock=new ReentrantReadWriteLock();//读锁private Lock readLock=lock.readLock();//写锁private Lock writeLock=lock.writeLock(); public double read () {//获取读锁readLock.lock(); try {return Math.sqrt(x * x + y * y);} finally {//释放读锁readLock.unlock();}} public void move( double deltaX, double deltaY) {//获取写锁writeLock.lock();try {x +=deltaX;y +=deltaY;} finally {//释放写锁writeLock.unlock();}} }
2. 读写锁再优化之 StampedLock
RRW很好用在读较大的并发场景比写作。不过,RRW在性能上仍有提升空间。当读多写少时,RRW会导致写线程遇到饥饿问题,这意味着写线程会因为无法竞争锁而处于等待状态。
在JDK1.8中,Java提供了StampedLock类来解决这个问题。 StampedLock并不是基于AQS实现的,但是实现原理和AQS是一样的,都是基于队列和锁状态实现的。与RRW不同的是,StampedLock控制锁有:写、悲观读和乐观读三种模式,并且StampedLock在获取锁时会返回一个票据戳记。释放锁时需要验证获取到的戳记。在乐观读取模式下,读取共享资源后,还会使用stamp作为二次验证。稍后我将解释邮票的工作原理。
我们先通过一个官方的例子来了解一下StampedLock是如何使用的。代码如下:
公共类点{私人双x,y;私有最终StampedLock s1=new StampedLock(); void move(double deltaX, double deltaY) { //获取写锁long stamp=s1.writeLock();尝试{ x +=deltaX ; y +=deltaY; } finally { //释放写锁s1.unlockWrite(stamp); } } double distanceFormOrigin() { //乐观读取操作long stamp=s1.tryOptimisticRead(); //复制变量double currentX=x , currentY=y; //判断读时是否有写操作if (!s1.validate(stamp)) { //升级为悲观读stamp=s1.readLock();尝试{当前X=x;当前Y=y; } 最后{ s1.unlockRead(stamp); return Math.sqrt(currentX * currentX + currentY * currentY);我们可以发现,写线程在获取写锁的过程中,首先通过WriteLock获取了票证。 WriteLock是排他锁。同一时间只有一个线程可以获取锁。当一个线程获取锁时,其他请求线程必须等待。只有当没有线程持有读锁或写锁时才能获取锁。成功请求锁后,将返回一个戳记票变量来表示锁的版本。释放锁时,需要传递unlockWrite,并传递参数stamp。
接下来就是读线程获取锁的过程。首先,线程会通过乐观锁tryOptimisticRead操作获取票据戳记。如果当前没有线程持有写锁,则会返回非0戳版本信息。线程获得标记后,会将共享资源复制到方法堆栈中。在此之前,具体操作都是根据方法栈的复制数据来进行的。
然后该方法需要调用validate 来验证调用tryOptimisticRead 返回的戳记当前是否被其他线程持有。如果是,validate将返回0并升级为悲观锁;否则,可以使用印章版本的锁对。数据被操作。
与RRW相比,StampedLock仅使用AND或运算进行验证,不涉及CAS运算。即使第一次乐观锁获取失败,也会立即升级为悲观锁,这样就可以避免连续CAS操作带来的问题。 CPU使用率是一个性能问题,所以StampedLock效率更高。
总结
无论使用Synchronized同步锁还是Lock同步锁,只要存在锁竞争,就会出现线程阻塞,导致线程之间频繁切换,最终增加性能消耗。因此,如何减少锁竞争就成为优化锁的关键。
在Synchronized同步锁中,我们了解到可以通过减小锁粒度、减少锁占用时间来减少锁竞争。到这里,我们知道,我们可以利用Lock锁的灵活性,通过锁分离来减少锁争用。
用户评论
感觉被直接问到“Lock同步锁优化”还真的挺突兀的!我心里想的是京东的面试水平也太高了吧!难道说现在连初级工程师都需要懂这方面的知识吗?
有6位网友表示赞同!
面试官确实犀利啊,直接点明要讲 Lock 同步锁优化。看来想要在京东获得Offer,刷题是远远不够的,还得深入了解一些底层原理和优化策略吧!
有12位网友表示赞同!
感觉这个题目挺难的,我平时学习的时候只是知道 Lock 的作用,但对于具体的优化方案还真没什么经验。这面估计要凉了...
有11位网友表示赞同!
京东面试问lock同步锁优化有点超出预期了,不过这也体现了京东对工程师技术能力的要求高啊!
有11位网友表示赞同!
Lock 同步锁优化这个概念确实挺重要的,尤其是在开发高并发系统的时候更显得尤为重要;不过感觉这个问题还是稍微考察得比较深入,对我的准备时间要求有点高...
有13位网友表示赞同!
面试官问 Lock 同步锁优化这个题目的目的是想看你的问题诊断能力和解决问题的思路,而不是让你死记硬背某一套方案。建议大家在备考时多思考一些常见场景下的并发问题,并尝试找出不同的解决方案。
有9位网友表示赞同!
如果说要准备一个很通俗易懂的 Lock 同步锁优化讲解,可以从一些简单的数据结构说起:比如使用HashMap存储数据,可能会有线程竞争的问题;这时候就需要引入 Lock 帮助解决冲突。再结合一些案例,更容易让面试官理解你的想法!
有13位网友表示赞同!
我的目标是进京东,所以要认真准备相关内容啦,虽然有点紧张,但也要保持冷静思路!
有18位网友表示赞同!
这种类型的笔试问题感觉很难在短时间内回答好。也许可以尝试先从一些基础知识讲起,比如什么是 Lock 同步锁,它有哪些实现方式等等,然后再逐渐深入到优化方案的探讨中去?
有17位网友表示赞同!
京东的面试题难度确实比较高,不过挑战也挺刺激的!我准备好好回顾一下相关资料,争取在下次面试时能够表现得更出色!
有16位网友表示赞同!
Lock 同步锁优化这个话题的确需要花费大量时间去研究和理解。这次面试经验很宝贵,让我更加明确自己需要提升的部分了。
有13位网友表示赞同!
京东面试题的难度确实挺高啊!感觉我平时刷题只关注基础知识,忽略了很多细节方面,看来以后要更加注重实践应用和系统设计,才能胜任大厂考验!
有15位网友表示赞同!
面试官这种直接切入深度的提问方式让我意识到,在大公司工作不仅仅是编码能力,更需要具备全局视野和解决实际问题的能力!
有20位网友表示赞同!
虽然面试题比较难,但我还是会努力去学习并掌握相关的知识!毕竟京东作为一家优秀的互联网企业,吸引了很多优秀人才,我也想努力成为其中的一个部分!
有5位网友表示赞同!
京东的面试考察范围蛮宽的,需要全方位准备!不过这也能让我更全面地提升自己的技术能力吧!
有7位网友表示赞同!
Lock 同步锁优化感觉是一门博大精深的学问啊,我还需要好好研究一下!下次面试希望能表现得更好!
有10位网友表示赞同!
京东的面试题确实挺有挑战性的,不过我还是很期待能进入这样的公司工作,学习和成长!
有12位网友表示赞同!