ReentrantLock 解析 - 不公平锁 -Chapter1

6 篇文章 0 订阅
可重入锁
synchronized 关键字其实就是可重入锁。
那什么是可重入锁呢,简单举个栗子就清楚了:
假如一个对象A,有两个方法m1和m2,下面是代码

class A {
synchronized void m1(){
...
m2();
...
}
synchronized void m2(){
...
}
}

一个线程如果想执行m1或者m2,都要获取到对象A的锁才可以。
看的m1方法, 在m1里面会调用m2。如果一个线程已经获取锁并且正在执行m1方法,然后在m1方法里面调用m2方法,不需要重新获取锁,因为在执行m1方法的时候一斤获取到了对象A的锁。
这种不需要重新获取锁的锁就是可重入锁。
对应的,肯定有不可重入锁,那再m1中调用m2就必须要重新获取一个锁资源。

公平锁和非公平锁
Reentrant内部分别实现了公平锁和非公平锁。
公平锁获取锁资源的方式是先来后到的方式。也就是说,会实现一个等待队列,先尝试获取锁的放到队列的前面,后尝试获取所的放到队列的后面,这样的话当锁资源释放的时候,在队列前面的等待线程就可以优先得到锁。
非公平锁正好想法,每当一个锁资源释放的时候,所有的线程会一起竞争锁资源。我们也可以通过给一个线程设置高优先级的方式来提高获取锁的概率,但是并不能够保证一定会比其他的线程先获取到锁。所以也就没法控制线程获取到锁的顺序。


源码解析

类图
下图展示了ReentrantLock的类结构:
Sync类有两个子类 -- NonfairSync和FairSync,这两个类中分别实现了非公平锁和公平锁的具体细节。
Sync类有自己的一套继承关系,承载了ReentrantLock的底层实现细节。
ReentrantLock内部实现依赖sync, 因此sync和它的两个子类都是ReentrantLock的静态内部类

实例化一个ReentrantLock
当我们实例化ReentrantLock的时候, 默认会实现一个非公平锁,下面是默认构造方法:
public ReentrantLock() {
sync = new NonfairSync();
}
可以通过传递参数 给构造方法来指定创建公平锁还是非公平锁
public ReentrantLock( boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
如果传递true,会实例化一个公平锁,如果传递为false,会建立一个非公平锁。

获取一个NonFair锁

看代码,执行lock()方法获取锁是通过调用sync对象的lock方法来实现。
具体使用的是FairSync或NonFairSync其中的一个。这个取决于我们实例化的是公平锁还是非公平锁。

public void lock() {
sync.lock();
}
大多数的场景下,我们都会使用非公平锁,因此接下来分析下非公平锁的获取流程,有些代码显得有点怪怪的,因为是考虑到多线程的场景下,所以理解起来稍微有点难。总的来说,获取一个非公平锁主要的执行流程见下图:
从上面的类图我们知道,NonFairSync的继承关系是:NofairSync -> Sync -> AbstractQueuedSynchronizer ->....
AbstractQueuedSynchronizer 从名字可以看得出来,内部维护了一个queue,用来保存当前竞争锁资源的所有的线程。这个queue是一个链表,每个元素都是一个Node对象。每个Node对象中都存储一个线程Thread对象。关于这个Queue,暂时只需要了解这么多。
接下来,解释下具体的流程:
  • NofairSync类的lock方法
从上图可以看到,调用ReentrantLock的lock方法,会自动调用NonFairSync或者FairSync的lock方法。下面是NonFairSync的lock方法的代码:
final void lock() {
// 通过CVS的方式来修改state,成功则获取锁
if ( compareAndSetState ( 0 , 1 ))
// 设置当前线程为独占线程
setExclusiveOwnerThread(Thread.currentThread());
else
// 修改State失败,进入acquire方法
acquire( 1 );
}

  1. lock方法首先会调用的是compareAndSetState方法,通过CVS的方式更新系统中state的值,如果返回true表示成功获取到锁。
  2. 它的原理是查看系统中state的值是否为0,如果为0则更新为1。如果成功了,当前线程就可以获取到锁并且执行下面的代码,并且其它线程调用compareAndSetState方法的时候就会发现state值已经为1,就会执行acquire方法然后被放到等待队列中。
  3. 当前线程成功把state的值更新为1之后,接下来执行setExclusiveOwnerThread方法。这个方法是AbstractOwnableSynchronizer类提供的,作用是告诉其他所有的线程当前线程是正在执行的唯一的线程。
  4. 接下来当前线程退出lock方法,执行后面的业务逻辑。

  • 关于compareAndSetState方法详解
compareAndSetState方法是通过CVS的方式保证原子性的,通过对系统中一个唯一变量state的状态的获取和更新来表示对锁的占用。如果一个线程成功把state的值更新为1,那么当前线程则获得了执行的机会。如果一个线程通过CVS的方式更新state失败,那么则会被放到等待队列中,继续等待锁被释放后的下次机会。



这个方法的底层实现是通过Unsafe类的 compareAndSetState方式实现的。这个是一个native方法,是真正直接对state变量的操作的关键底层方法。
关于CVS的方式更新state变量,在多线程的情况下,如果有多个线程对state变量进行更新,统一时刻只有一个线程成功,其它的线程都会失败并返回false。因此更新了state变量的那个线程,就是此刻唯一允许执行的线程。所以其它的线程都会被放到等待队列中,继续尝试着通过CVS的方式尝试获取被重新释放的锁。

  • acquire方法
public final void acquire( int arg) {
//再次通过tryAcquire方法尝试获取锁,因为此刻有可能前面的线程已经释放了锁
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
上面是acquire方法的具体代码。可以看到首先会执行tryAcquire方法尝试再次更新更新state变量。如果失败了话,接下来会把当前线程放到一个Node中,并把这个Node放到等待队列中。
tryAcquire方法公平锁和非公平锁都有自己的实现,这里我们看下非公平锁的实现方式。首先会获取当前的state的值,如果是0的话,会尝试着再次使用CVS的方式更新state说去锁,成功后就退出。
如果没有获取锁成功,判断当前线程是否就是那个已经获取到锁的线程,如果是的话就把state+1。 这个逻辑是提供了ReentrantLock可重入锁的特性。只有重入锁的时候才会走到这块代码。线程每次重入了一个锁之后,都会给state值加1。我们知道,使用ReentrantLock的过程中, 每次执行lock方法都会对应着一个unlock的操作。因此每次lock操作后state值加1,然后每次unlock的时候,state值减去1,这样确保加锁和解锁的次数一致

如果tryAcquire获取锁再次失败,那么这次就会把当前线程放到一个Node中,并放到一个等待队列中。并且通过CVS的方式把当前Node放到队列的末尾( 防止多线程的情况下会出现覆盖)。然后再通过acquireQueued方法让当前的Thread通过自旋的方式去获取锁。所谓的自旋的方式是通过一个无限循环的方式去尝试获取锁,直到成功获取到锁。

  • addWaiter方法
把当前线程包装到一个Node对象中,然后放到等待队列中。如果当前没有等待队列,或者插入失败(插入是通过CVS的方式保证原子性),则使用无限循环的方式多次尝试重新添加,最终把线程插入到等待队列
private Node addWaiter(Node mode) {
// 建立一个新的Node节点,以当前的线程为基础
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null ) {
node.prev = pred;
//通过CVS的方式把该节点添加为新的尾巴节点,注意这个等待队列是一个双向链表
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 如果CVS的方式添加失败,那么则通过enq方法无限循环重新尝试添加
enq(node);
return node;
}


  • enq方法
循环尝试把当前Thread的Node节点插入到等待队列中。通过CVS的方式来保证线程安全

private Node enq( final Node node) {
for (;;) {
Node t = tail;
if (t == null ) { // Must initialize
if (compareAndSetHead( new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}

  • acquireQueued方法
在把线程放到了等待队列之后,仍然尝试再次获取锁。如果失败,则把当前线程的状态设置为等待状态,防止无限循环当前线程去获取锁,占用CPU资源。
final boolean acquireQueued( final Node node, int arg) {
boolean failed = true ;
try {
boolean interrupted = false ;
for (;;) {
final Node p = node.predecessor();
//当前节点是头结点,并且获取锁成功
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null ; // help GC
failed = false ;
return interrupted;
}
// 如果当前节点不是头结点,或者是头结点但是获取锁失败,判断当前线程是否需要被设置为等待状态,并且parkAndCheckInterrupt方法可以在底层把当前线程block,直到upark调用
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true ;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值