java重入锁原理

来源:互联网 发布:java构造set get方法 编辑:程序博客网 时间:2024/06/05 08:37

1、Lock接口,是对控制并发的工具的抽象。同一时间内只有一个线程可以获取这个锁并占用资源,其他资源响应获取锁,必须等待这个线程是否锁。在java实现中的ReentrantLock就是这样的锁。另外一种锁,它可以允许多个线程读取资源,但是只能允许一个线程写入资源,ReadWirteLock就是这样一种特殊的锁(读写锁)。

2、ReentrantLock实现了Lock接口,内部有三个内部类,Sync、NonfairSync、FairSync,Sync是一个抽象类型,它继承AbstractQueuedSynchronizer,这个AbstractQueuedSynchronizer是一个模板类,它实现了许多和锁相关的功能,并提供了钩子方法供用户实现,比如tryAcquire,tryRelease等。Sync实现了AbstractQueuedSynchronizer的tryRelease方法。NonfairSync和FairSync两个类继承自Sync,实现了lock方法,然后分别公平抢占和非公平抢占针对tryAcquire有不同的实现。

3、AbstractQueuedSynchronizer继承自AbstractOwnableSynchronizer,AbstractOwnableSynchronizer的实现很简单,它表示独占的同步器,内部使用变量exclusiveOwnerThread表示独占的线程

其次,AbstractQueuedSynchronizer内部使用CLH锁队列来将并发执行变成串行执行。整个队列是一个双向链表。每个CLH锁队列的节点,会保存前一个节点和后一个节点的引用

,当前节点对应的线程,以及一个状态。这个状态用来表明该线程是否应该block。当节点的前一个节点被释放的时候,当前节点就被唤醒,成为头部。新加入的节点会放在队列尾部。

4、非公平锁的lock方法

ReentrantLock(调用默认构造方法)-》NonfairSync(构造一个NonfairSync)-》ReentrantLock(赋值内部变量sync)-》ReentrantLock(调用lock方法)

-》NonfairSync(调用lock方法)-》AbstractQueuedSynchronizer(尝试设置当前锁的状态为1)-》成功-》AbstractOwnableSynchronizer(将当前线程设置到锁中,表示抢占了该锁)-》结束(或)

-》失败-》AbstractQueuedSynchronzier(调用acquire(1))-》ReentrantLock(tryAcquire)-》Sync(nonfairTryAcquire)-》获取锁失败AbstractQueuedSynchronizer(addWaiter)-》AbstractQueuedSynchronizer(acquireQueued)-》结束(或-》需要中断-》AbstractQueuedSynchronizer(selfInterrupt)-》结束)

-》Sync(获取锁成功)

5、lock方法详细描述

在初始化ReentrantLock的时候,如果我们不传参数是否公平,那么默认使用非公平锁,也就是NonfairSync;

当我们调用ReentrantLock的lock方法的时候,实际上是调用了NonfairSync的lock方法,这个方法先用CAS操作,去尝试抢占该锁。如果成功,就把当前线程设置在这个锁上,表示抢占成功;

调用acquire(1)实际上使用的是AbstractQueuedSynchronizer的acquire方法,它是一套锁抢占的模板,总体原理是先去尝试获取锁,如果没有获取成功,就在CLH队列中增加一个当前线程的节点,表示等待抢占。然后进入CLH队列的抢占模式,进入的时候也会去执行一次获取锁的操作,如果还是获取不到,就调用LockSupport.park将当前线程挂起。当持有锁的那个线程调用unlock的时候,会将CLH队列的头节点的下一个节点上的线程唤醒,调用的是LockSupport.unpark方法;

acquire方法内部先使用tryAcquire这个钩子方法去尝试再次获取锁,这个方法在NofairSync这个类其实就是使用了nonfairTryAcquire,具体实现原理是先比较当前锁的状态是否为0,如果是0,则尝试去原子抢占这个锁(设置状态为1,然后把当前线程设置成独占线程),如果当前的状态不是0,就去比较当前线程和占用锁的线程是不是一个线程,如果是,会去增加状态变量的值,从这里看出可重入锁之所以可重入,就是同一个线程可以反复使用它占用的锁。如果以上两种情况都不通过,则返回失败,如果tryAcquire一旦返回false,就会进入acquireQueued流程,也就是进入基于CLH队列的抢占模式;

首先,在CLH锁队列尾部增加一个等待节点,这个节点保存了当前线程,通过调用addWaiter实现,这里需要考虑初始化的情况,在第一个等待节点进入的时候,需要初始化一个头节点然后把当前节点加入到尾部,后续则直接在尾部加入节点就行;

将节点增加到CLH队列后,进入acquireQueued方法,首先,外层是一个无限for循环,如果当前节点是头节点的下个节点,并且通过tryAcquire获取到了锁,说明头节点已经释放了锁,当前线程是被头结点那个线程唤醒的,这时候就可以将当前节点设置成头结点,并且将failed标记设置成false,然后返回。至于上一个节点,它的next变量被设置为null,在下次GC的时候会清理掉,如果本次循环没有获取到锁,就进入线程挂起阶段,也就是shouldParkAfterFailedAcquire;

如果尝试获取锁失败,就会进入shouldParkAfterFailedAcquire,会判断当前线程是否挂起,如果当前节点已经是SIGNAL状态,则当前线程需要挂起。如果前一个节点是取消状态,则需要将取消节点从队列移除。如果前一个节点状态是其他状态,则尝试设置成SIGNAL状态,并返回不需要挂起,从而进行第二次抢占

当进入挂起阶段,会进入parkAndCheckInterrupt方法,则会调用LockSupport.park将当前线程挂起

6、非公平锁的unlock方法

调用unlock方法,其实是直接调用AbstractQueuedSynchronizer的release操作;

进入release方法,内部先尝试tryRelease操作,主要是去除锁的独占线程,然后将状态减一,这里减一主要是考虑到可重入锁可能自身会多次占用锁,只有当状态变成0,才表示完全释放了锁。

一旦tryRelease成功,则将CHL队列的头节点的状态设置为0,然后唤醒下一个非取消的节点线程;

一旦下一个节点的线程被唤醒,被唤醒的线程就会进入acquireQueued代码流程中去获取锁

7、公平锁和非公平锁的区别

在CHL队列抢占模式上都是一致的,就是在进入acquireQueued这个方法之后都一样,它们的区别在初次抢占上有区别,也就是tryAcquire上的区别

真正的区别就是公平锁多了hasQueuePredecessors这个方法,这个方法用于判断CHL队列中是否有节点,对于公平锁,如果CHL队列有节点,则新进入竞争的线程一定要在CHL上排队,而非公平锁则是无视CHL队列中的节点,直接进行竞争抢占,这就有可能导致CHL队列上的节点永远获取不到锁。

8、总结

线程使用ReentrantLock获取锁分为两个阶段,第一个阶段是初次竞争,第二个阶段是基于CHL队列的竞争。在初次竞争的时候是否考虑队列节点直接区分出了公平锁和非公平锁。在基于CHL队列的竞争中,依靠CAS操作保证原子操作,依靠LockSupport来做线程的挂起和唤醒,使用队列来保证并发执行变成串行执行。

0 0
原创粉丝点击