文章目录
前言
ReentrantLock是一个可重入锁,其底层是基于AQS来实现的,AQS(AbstractQueuedSynchronizer),它是java几乎所有锁和同步器底层的一个基石框架,AQS是基于FIFO的队列实现,内部维护了一个状态变量state,通过CAS更新这个状态变量来实现枷锁解锁等操作。
1、主要内部类及属性分析
1.1、内部类
这里定义了三个内部类
- Sync作为一个抽象类,他继承自AbstractQueuedSynchronizer,实现了部分AQS方法
- NonfairSync继承自Sync,主要用于实现非公平锁
- FairSync实现了Sync,主要用于实现公平锁
1.2、属性
这里只有一个属性sync,他在构造方法中初始化,用于确定是NonfairSync还是FairSync,实现公平锁还是非公平锁。
2、构造方法解析
- 默认构造方法使用的是非公平锁
- 有参构造方法可以根据用户的传入来确定使用公平锁或非公平锁
3、常用方法解析
3.1、Lock()加锁方法
这里直接委托给sync,会调用Sync#lock方法,,这种就有两种实现NonfairSync#lock和FairSync#lock
3.1.1、FairSync#lock(公平锁加锁)
这里会调用AQS的acquire()方法获取锁,这里传的值是1
3.1.1.1、AbstractQueuedSynchronizer#acquire
- 首先调用tryAcquire尝试去获取锁
- 获取锁失败,把当先线程包装成一个Node节点,然后去队列排队
3.1.1.1、FairSync#tryAcquire(这里tryAcquire是抽象方法,因为我们这里是公平锁,所以到FairSync)
- 获取到当前线程和state变量
- 判断state变量是否为0,如果为0说明现在没有线程持有锁,那么调用hasQueuedPredecessors方法判断有没有其他线程在排队,如果没有那么当前线程尝试更新state的值为1,如果成功了,则说明当前线程获取到锁了,那么把当前线程设置到exclusiveOwnerThread变量中,标识当前占有锁的线程。
- 如果state变量不为0,则判断exclusiveOwnerThread变量的线程是否是当前线程,如果是当前线程则说明是可重入锁的情况,则让state+1,这里外部传进来的是1。
- 如果上面条件都没命中,则返回尝试加锁失败。
3.1.1.1.1 、hasQueuedPredecessors
判断当前是否没有排队的线程或者自己是当前队列的第一个,判断有没有抢锁的权利。
3.1.1.2、addWaiter(Node.EXCLUSIVE), arg)
如果上面尝试加锁失败,则会调用这个方法步骤如下:
- 把当前线程包装成一个Node节点
- 尝试把新节点加入到队列尾部,这里使用CAS更新防止并发冲突
- 如果尝试加入尾节点不成功,那么会调用enq方法(这里会循环直到加入队列)
3.1.1.2.1、enq(node)
这里会不断自旋,尝试把当前节点插入尾节点。
3.1.1.3、acquireQueued
上面已经调用了addWaiter()方法把新节点加入队列了,这个方法是尝试让当前节点去获取锁的,流程如下
- 这里会阻塞在一个死循环中,循环体中会先判断当前的前一个节点是否为head,如果是head节点可能是head节点释放锁唤醒后续节点的操作,那么进行抢占锁,这里有可能获取不到,因为后续进来的也会直接尝试加锁
- 如果获取到锁了,那么会把head设置为自己,将自己的next设置为null方便gc然后返回。
- 如果没有获取到锁,那么会调用shouldParkAfterFailedAcquire判断是否应该阻塞自己,如果可以,那么会调用parkAndCheckInterrupt阻塞自己
- 这里如果线程被阻塞了,因为已经加入队列了,那么前面的释放锁会唤醒后续节点,那么这个时候从parkAndCheckInterrupt出来,回到循环开头尝试去加锁,如果获取不到锁再进行阻塞。
3.1.1.3.1、shouldParkAfterFailedAcquire
这里判断节点是否可以阻塞,是根据前一个节点的状态,如果前驱节点的状态是SIGNAL,表示他在释放锁的时候会唤醒后续节点,那么当前节点可以被唤醒,那么可以进行阻塞,反之不应该阻塞。
3.1.1.3.2、parkAndCheckInterrupt
这里会调用LockSupport.park阻塞当前线程,这里就等待前驱节点释放锁把它唤醒,然后继续去抢占锁。
3.1.1、NonfairSync#lock(非公平锁加锁)
这里和公平锁的区别就是:
- 一开始就尝试使用CAS更新state状态的值,如果修改成功了,那说明加锁成功了,那么修改当前占有锁的线程
- 这里就是非公平锁,我不管队列中有没有等待的线程,我不直接去排队,我首先尝试去获取锁,获取不到锁再去队列排队。
- 如果获取不到锁那么会调用acquire(1)尝试去队列排队,这个是和上面公平锁加锁的流程一样。
3.2、tryLock()加锁方法
这里尝试获取一次锁,成功了就返回true,没成功就返回false,不会继续尝试。
3.2.1、Sync#nonfairTryAcquire
这里和非公平锁的tryAcquire一致,产生加锁。
3.3、tryLock(long timeout, TimeUnit unit)带超时时间加锁方法
尝试获取锁,并等待一段时间,如果在这段时间内都没有获取到锁,就返回false
3.3.1、AbstractQueuedSynchronizer#tryAcquireNanos
这里首先会调用tryAcquire方法尝试获取锁,之前我们已经剖析过了,如果尝试没有加到锁,这里会调用doAcquireNanos方法
3.3.2、AbstractQueuedSynchronizer#doAcquireNanos
这里流程和上面剖析的差不多,只不过在park的时候加了过期时间,过期时间到了并且也没获取到锁,那么返回失败。
3.3、unlock()释放锁方法
这里会直接委托给sync的release方法,会调用到AbstractQueuedSynchronizer#release方法
3.3.1、AbstractQueuedSynchronizer#release
- 首先会调用tryRelease方法,这里是模版,会调用到具体实现的sync类,因为这里只有一个线程持有锁,所以不涉及到并发
- 释放锁成功,那么会唤醒后继节点
3.3.1.1、Sync#tryRelease
这里释放锁就是对state状态做–,如果–后state变成0,则说明没有不是可重入锁,那么把记录的线程信息也给清除了。
3.3.1.2、AbstractQueuedSynchronizer#unparkSuccessor
这里就是找到下一个满足条件可以被唤醒的节点,然后调用unpark方法唤醒后继节点。
4、总结
为什么ReentrantLock默认采用的是非公平状态状态?
因为非公平模式效率比较高,它一开始就尝试去获取锁,如果当前并发度不高的话,那么直接就可以获取到锁,减少了排队导致线程的阻塞,用户态到内存态切换的开销,非公平锁也有可能会导致一开始排队的线程一直获取不到锁,导致线程饥饿,不过,这种情况不常发生。