AQS的傀儡之Lock锁
Java锁的武林纷争中,存在两大阵营。分别是以Synchronized为首的Java内置关键字对象,自动维护锁的持有和释放,还有就是以Lock锁为代表的Java自定义锁。在之前的博文中,帝都的雁已为大家介绍了Synchronized派系的渊源,本文则重点聊聊Lock锁的那些花边新闻。
Lock锁,是Java并发包下的一个类,其API的使用功能在本文不做介绍,感兴趣的读者可自行阅读源码或查阅相关资料。本文重点介绍Lock锁底层实现原理(仅概括设计原理,源码级别的剖析会在后续文章做更新)。
一、AQS(AbstractQueuedSynchronizer)
直译为抽象同步队列。主要封装了Java并发包下很多公共的算法逻辑,而Lock锁本质上来说就是AQS的一个宿主,内部的任何操作都是靠AQS的运转。
AQS中有几大属性,而并发包下很多工具类都是基于AQS的加锁解锁的原理,以及属性的运用来巧妙实现自身业务功能。
1、int state
状态值,用于标识锁的一些信息,AQS底层在通过CAS自旋比较时,就是比较此属性是否发生预期变化;也会记录lock锁重入次数。
2、Node head
AQS阻塞队列中的头节点,也就是存放当前锁持有线程的节点,但这个头节点的很多信息都是空的,主要为了方便GC回收。
3、Node tail
AQS阻塞队列中的尾节点,未抢到锁的线程会放入阻塞队列的末尾。
二、读写锁
Lock锁的创建方式有多种,支持创建读锁ReentrantReadWriteLock.readLock和写锁ReentrantReadWriteLock.writeLock。对于熟悉锁概念的读者一定明白,读读可以共享,其他情况下资源是不会共享的。Lock的读写锁也很好地体现了这一点。
细心的读者在阅读Lock锁源码时发现,无论是哪一种Lock的实现类,内部都会维护一个sync的内部类去继承AbstractQueuedSynchronizer(简称AQS)类,而它加锁和解锁也都是在AQS中实现模板方法。
三、重入锁
Lock lock = new ReentrantLock();
创建的重入锁,支持锁的重入性,即当前线程拿到锁后,遇到这把锁的加锁操作,会进行重入,这也是基于AQS的属性state去做对应的标记。即锁重入一次,state会自增一次。
四、公平锁和非公平锁
Lock锁的实现类ReentrantLock内部在进行对锁支配时,通过内部类Sync操作,这个类有两个子类。
1、非公平锁NonfairSync
ReentrantLock默认创建的就是非公平锁。
当多个线程抢夺锁资源时,不管抢到与否,都会放入到AQS的阻塞队列中(类似于Synchronized的锁池)。只不过抢到锁的线程位于阻塞队列头部,且内容为空。设想,如果当这个线程执行完业务,释放锁时,按照队列原则,队列第二个位置的线程理论上应该获取这把锁,但这时非阻塞队列中的其他线程来了,进行锁资源的抢夺,且抢成功,那么对于排队的线程来说,是不是就不公平了?这就是非公平锁的设计思想。
2、公平锁FairSync
对比非公平锁,公平锁对抢到锁资源的线程做了判断,如果不是阻塞队列头部那个理论上应该获取到锁的线程的话,就会把这个线程阻塞,并放入阻塞队列的末尾,然后继续让头部进行锁资源的抢夺。
五、加锁原理
不管锁是否公平,其加锁和释放锁的操作都是一致的。加锁时,通过CAS比较state是否为预期状态,如果是则设置锁的持有线程,执行业务代码;若不是,则也会在CAS自旋一次,最后通过LockSupport.park去将当前线程阻塞,封装为Node节点,放入AQS的双向链表末尾中等待锁资源。
六、解锁原理
解锁时会判断解锁线程和锁持有线程是否为一个线程,然后将阻塞队列的第二个节点信息向后变更,同时改变锁持有线程的信息,通过LockSupport唤醒锁持有线程。
七、Condition
类似于Synchronized的等待池,是AQS的一个内部类。
Condition condition = lock.newCondition();
lock实现类似sync中的wait和notify方法的工具。
当condition.await()时,会将当前线程放入Condition的单向阻塞列表中,LockSupport阻塞,直到调用condition.signal()才会从阻塞列表中取出LockSupport唤醒。
八、AQS与Lock的关系
Lock锁本质上就是AQS的具体应用,AQS封装了并发包下常见的公共方法,通过对这些方法的重写以及对AQS的属性灵活运用,可以很好地扩展各种业务功能。
九、Lock与Synchronized区别
个人理解,Lock和Synchronized几乎设计的原理以及组件都是类似的。
1、相同点
都有锁的短暂自旋去减少线程阻塞(锁的升级膨胀)。
对于未获取锁的线程,都有一个双向列表存放。
对于主动调用锁对象阻塞方法的线程,都会存放在一个单向链表中。
都支持锁的重入。
2、不同点
Lock的开销是我们自己控制的,使用上更为灵活,而且支持对其功能进行扩展;但Synchronized是内置的关键字,所以其维护依赖于JDK。
锁升级膨胀的具体行为不同,lock没有偏向锁的概念,只会通过CAS自旋比较一次;Synchronized则是在对象头的MarkWord中记录锁的偏向信息,然后通过CAS比较,比较多次之后才会阻塞线程。
欢迎大家和帝都的雁积极互动,头脑交流会比个人埋头苦学更有效!共勉!
公众号:帝都的雁