大家好,我是小水珠。
今天这讲我们继续来聊聊锁优化。上一讲我重点介绍了在JVM层实现的Sychronized同步锁的优化方法,除此之外,在JDK1.5之后,Java还提供了Lock同步锁。那么它有什么优势呢?
相对于需要JVM隐式获取和释放锁的Sychronized同步锁,Lock同步锁(以下简称Lock锁)需要的是显示获取和释放锁,这就为获取和释放锁提供了更多的灵活性。Lock锁的基本操作是通过乐观锁来实现的,但由于Lock锁也会在阻塞时被挂起,因此它依然属于悲观锁。我们一张图来简单对比下两个同步锁,了解下各自的特点:
从性能方面来说,在并发量不高,竞争不激烈的情况下,Sychronized同步锁由于具有所分级的优势,性能上与Lock锁差不多;但在高负载,高并发的情况下,Sychronized同步锁由于竞争激烈会升级到重量级锁,性能则没有Lock锁稳定。
一 Lock锁的实现原理
Lock锁是基于Java实现的锁,Lock是一个接口类,常用的实现类有ReentrantLock,ReentrantReadWriteLock,他们都是依赖AbstractQueuedSychronier类实现的。
二 所分离优化Lock同步锁
1.读写锁ReentrantReadWriteLock
一个线程尝试获取写锁时,会先判断同步状态state是否为0,如果state是0,说明暂时没有其它线程获取锁;如果state不等于0,则说明其它线程获取了锁。
此时再判断同步状态state的低16位(w)是否为0,如果w为0,则说明其它线程获取了读锁,此时进入CLH队列进行阻塞等待;如果w不为0,则说明其它线程获取了写锁,此时要判断获取了写锁的是不是当前线程,如不是就进入CLH队列进行阻塞等待;若是,就应该判断当前线程获取写锁是否超过了最大次数,若超过,抛异常,反之更新同步状态。
一个线程获取读锁时,同样会先判断同步状态state是否为0.如果state等于0,说明暂时没有其它线程获取锁,此时判断是否需要阻塞,如果需要阻塞,则进入CLH队列进行阻塞等待;如果不需要阻塞,则CAS更新同步状态为读状态。
如果state不等于0,会判断同步状态低16位,如果存在写锁,则获取读锁失败,进入CLH阻塞队列;反之,判断当前线程是否应该被阻塞,如果不应该阻塞则尝试CAS同步状态,获取成功更新同步锁为读状态。
下面通过一个求平方的例子,来感受下RRW的实现,代码如下:
2.读写锁再优化之StampedLock
在JDK1.8中,Java提供了StampedLock类来解决这个问题。StampedLock不是基于AQS实现的,但实现的原理和AQS一样,都是基于队列和锁状态实现的。与RRW不一样的是,StampedLock控制锁有三种模式:写,悲观读以及乐观读,并且StampedLock在获取锁时会返回一个票据stamp,获取的stamp除了在释放锁时需要校验,在乐观读模式下,stamp还会作为读取共享资源后的二次校验,后面我们会讲解stamp的工作原理。
三 总结
不管使用Sychronized同步锁还是Lock同步锁,只要存在锁竞争就会产生线程阻塞,从而导致线程之间的频繁切换,最终增加性能消耗。因此,如何降低锁竞争,就成为了锁优化的关键。
在Sychronied同步锁中,我们了解了通过减小锁粒度,减少占用时间来降低锁的竞争。在这一讲中,我们知道可以利用Lock锁的灵活性,通过锁分离的方式来降低锁竞争。
Lock锁实现了读写锁分离来优化读大于写的场景,从普通的RRW实现到读锁和写锁,到StampedLock实现了乐观读锁,悲观读锁和写锁,都是为了降低锁的竞争,促使系统的并发性能达到最佳。