并发编程之JUC(一):ReentrantLock的源码分析一

一. JUC简介

JUC是jdk从1.5版本之后开始提供的一套用于并发编程的API包,全称为java.util.concurrent,这个包下面提供了我们非常熟悉的ThreadPoolExecutor,ConcurrentHashMap,ReentrantLock等工具,本节我们开始讲解该包下一个子包中关于锁的实现类:ReentrantLock

二. AQS

要说ReentrantLock就必须先说AQS,AQS全称AbstractQueuedSynchronizer,抽象的队列化的同步器,抽象的说明他当中可能有一些方法需要被重写,队列化说明在并发时多个线程是以排队的形式执行,同步器类似java的synchronized关键字,可以保证多线程执行同一段代码时有序进行。

AQS维护了一个FIFO的双向队列,什么是FIFO?就是先进先出的意思,双向队列就是上一个节点指向下一个节点的同时,下一个节点也指向上一个节点,也就是我们下面讲要讲到的Node。

AQS中有几个关键的属性:

  • head:是一个Node类型的实例,表示队列的头部
  • tail:也是一个Node类型的实例,表示队列的尾部
  • state:int类型,所有针对该属性的操作都是原子操作的,用来判断锁被持有的状态
  • unsafe:Unsafe类为我们提供了类似C++手动管理内存的能力,后续关于state,head,tail的原子操作都将借助于unsafe

aqs的关键属性
上面提到的head和tail都是一个Node类型的实例,我们来看下这个Node的结构:

  • next:当前节点的下一个节点
  • prev:当前节点的上一个节点
  • thread:当前节点的指向的线程对象
  • waitStatus:当前节点的等待状态,他有以下几个常量值
    • CANCELLED: 取消
    • CONDITION: 线程等待在某个条件上
    • SIGNAL: 表示线程被park起来,即锁住,等待解锁
    • PROPAGATE:表示下一次以共享模式获取锁时无条件传播,具体使用场景恕我孤陋寡闻,没有研究到

在这里插入图片描述
AQS的关键方法:

  • tryAcquire(int arg): 尝试获取锁,该方法是一个抽象方法,由具体类实现,如ReentrantLock中的Sync中被实现类FairSync和NonfairSync实现
// NonfairSync的tryAcquire方法
protected final boolean tryAcquire(int acquires) {
   return nonfairTryAcquire(acquires);
}
// FairSync的tryAcquire方法
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}
  • acquire(int arg): 获取锁,获取锁之前会先尝试获取锁,没有获取到则会往队列中添加节点,线程阻塞,等待前一个锁释放
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

当然这里还有其他的acquireXXX方法,用于获取共享锁,获取线程可被打断的独享锁等等,这里我们主要讲ReentrantLock会使用的到的上面两个方法。

三. ReentrantLock源码分析

这是一段最简单的ReentrantLock的使用,创建一个ReentrantLock对象,调用lock方法锁住当前线程,执行逻辑,最后释放锁。

public class Test {

    private static ReentrantLock reentrantLock = new ReentrantLock();
    public static void main(String[] args) {
        try {
            reentrantLock.lock();
            System.out.println("XXXXXXXXXXX");
        }finally {
            reentrantLock.unlock();
        }
    }
}

ReentrantLock中只有一个属性,Sync,这个类是ReentrantLock的一个内部抽象类,该类继承自AQS,并且有自己的lock方法。

/** Synchronizer providing all implementation mechanics */
private final Sync sync;

而Sync又有两个子类,FairSync和NonfairSync,正如我们上面提到的,他们最终实现了AQS的tryAcquire方法以及Sync类中的lock方法
FairSync及NonfairSync的类图
那么我们现在来看一下private static ReentrantLock reentrantLock = new ReentrantLock()这一行代码,实际就是为Sync属性赋值

// 默认为sync属性赋值为NonfairSync,顾名思义,他将默认实现非公平锁。
public ReentrantLock() {
    sync = new NonfairSync();
}
// 重载构造方法,可通过参数决定是公平锁还是非公平锁
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

我们先以默认的非公平锁来看,此时调用reentrantLock.lock() 方法,实际就是调用NonfairSync的lock方法。

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    // 加锁
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }
	//尝试加锁
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

首先执行compareAndSetState(0, 1) 方法,这个方法在AQS中实现,直接调用属性unsafe的方法,解释一下这个方法,从名字看就是比较然后设置state,

protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

Unsafe类中有几个方法比如下面的compareAndSwapInt的方法,它们都是native本地方法,它有四个参数

// var1:需要操作的对象
// var2:想要改变的字段在内存中的地址
// var4:预期的值,
// var5:需要更新进该属性的值
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

而上面这个stateOfferset会在类初始化时获取到。

stateOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("state"));

所以,总结一下,compareAndSetState(0, 1)的意思就是如果当前值不为expect,比较失败,返回false,如果为该值,则替换为update。

因此为什么要叫非公平锁,就是所有线程来加锁,都首先去执行这个compareAndSetState(0,1),哪个线程能抢到就直接把state变为了1,然后把exclusiveOwnerThread当前拥有独占锁的线程变更为自己,看谁刚好抢到,而不是谁先来谁就先获取到锁,因此是不公平的。

很明显,当并发执行时,后面没有抢到锁的线程就会执行acquire(1)这个逻辑

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

首先还是会尝试获取锁,调用到非公平锁的nonfairTryAcquire方法,首先获取到当前线程,及当前state的值

  • 如果为0,说明之前被抢走的锁已经被释放了,因此这里再次执行上面的compareAndSetState(0, acquires)方法去抢锁,抢到了按之前的逻辑走,直接返回,并把线程置为当前线程。
  • 如果不为0,则判断当前线程与之前保存在exclusiveOwnerThread中的线程是否一致,注意,这里就是ReentrantLock实现重入的关键,如果一致,说明当前线程和之前占用锁的线程是同一个,因此此处允许它执行,然后把state的值+1,表示当前线程的重入次数。
final boolean nonfairTryAcquire(int acquires) {
	  final Thread current = Thread.currentThread();
	  int c = getState();
	  if (c == 0) {
	      if (compareAndSetState(0, acquires)) {
	          setExclusiveOwnerThread(current);
	          return true;
	      }
	  }
	  else if (current == getExclusiveOwnerThread()) {
	      int nextc = c + acquires;
	      if (nextc < 0) // overflow
	          throw new Error("Maximum lock count exceeded");
	      setState(nextc);
	      return true;
	  }
	  return false;
}

因此,次数无论是获取到锁或者重入了锁,都将返回true,如果两者都不满足,则执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法

首先addWaiter(Node.EXCLUSIVE),添加独占模式的等待者。

private Node addWaiter(Node mode) {
	// 创建一个新的节点,节点对应的线程,nextWaiter指向一个独占模式的node
   Node node = new Node(Thread.currentThread(), mode);
    // 如果尾节点不为空,当第一次进这个方法的时候,tail肯定为null
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 尾节点为空,执行这里
    enq(node);
    return node;
}
  1. 第一次进addWaiter方法进enq方法,执行一个死循环,首先判断尾节点是否为空,如果为空则设置一个新的节点为head,并且也赋值给tail,此时tail和head都指向同一个Node对象,我们称作node1;
  2. 然后进入第二次循环,此时tail已经不为null,执行else逻辑,首先把方法传进来的这个node,我们称作node2,把它的prev上一个节点指向node1,然后把tail指向当前node2,此时head指向node1,再把node1的next指向node2,最后双向链表结构成型。
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;
            }
        }
    }
}

上面这段话非常的绕,大家可以多看几遍,最终会形成下面这样一个结构
enq方法后的
如果此时又来一个线程抢锁,那么就会再次调用到addWaiter方法,此时tail就不为null了,需要被加进来的这个Node节点我们称为node3
3. 首先就把node3的prev属性执行tail,然后把尾节点变更为node3
4. 把之前的尾节点,即上面的node2的next属性指向新的尾节点node。

Node pred = tail;
if (pred != null) {
    node.prev = pred;
    if (compareAndSetTail(pred, node)) {
        pred.next = node;
        return node;
    }
}

我们也用一幅图来加入这个节点,这样就轻松实现了双向链表的节点增加
在这里插入图片描述
OK,现在链表有,排队的这些节点需要调用上面说到的外层方法acquireQueued(addWaiter(Node.EXCLUSIVE), arg))来尝试获取锁,这里又有一个死循环,

  • 首先获取当前节点的上一个节点
  • 判断当前节点的上一个节点是否为head节点,这里判断的目的是什么呢?如果上一个节点是head节点,那么说明在这个节点之前已经没有排队的node了,已经轮到我自己去尝试获取锁了
  • 尝试获取锁,又去尝试获取锁,如果前一个占用锁的线程还没释放,那么则会获取失败,即使前一个占用锁的线程已经释放了,此处也不一定就能够获取到锁,因为我们说过,这里我们实现的是非公平锁,新来的线程很有可能在lock的时候就能抢到这把锁。
  • 如果抢锁成功,把当前节点设置为head节点,node的prev置为null,thread置为null,原head的next置为null,这样以前的head节点就没有引用了,后续就会被GC回收掉
    画个图表示一下
    获取到锁之后的状态
  • 如果抢锁失败,那么就需要判断是否需要阻塞,即调用shouldParkAfterFailedAcquire(p, node)方法
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;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

下面我们看一下shouldParkAfterFailedAcquire(p, node)这个方法,顾名思义,方法的作用是判断在获取锁失败之后是否应该线程park。这里我们就要用到上面提到的Node的waitStatus了。

  • 因为外层的死循环结构,这里第一次进来获取到上一个节点的waitStatus,如果为Node.SIGNAL,说明队列中还有在阻塞的线程,当前线程也应该park,如果上一个节点的waitStatus大于0,说明上一个节点取消了,那就循环知道找到最近的一个不是取消状态的node,将当前节点的prev指向它,并且把它的next指向当前节点,即跳过中间所有的取消状态的Node。
  • 当第一个进入该方法的线程进来时,它的上一个节点的waitStatus默认是0,于是进入else逻辑,将它的waitStatus置为Node.SIGNAL, 这里又使用compareAndSetXXX方法,可以看出,在JUC里面,大量使用了这种cas操作,来保证原子性。
  • 进入外层的第二次循环,执行上面的p == head && tryAcquire(arg)判断,此时如果获取到的锁,那么直接执行上面的逻辑,返回false,注意,这里返回的false不代表抢占锁是否成功
  • 如果p == head && tryAcquire(arg)还是判断失败,那么再次进入shouldParkAfterFailedAcquire方法,此时的上一个节点的waitStatus就等于Node.SIGNAL了,直接返回true
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

当上面shouldParkAfterFailedAcquire返回true之后,就进入parkAndCheckInterrupt,LockSupport.park(this)最终调用了UNSAFE.park(false, 0L); 调用到本地方法将当前线程挂起阻塞,等到unpark方法来解除阻塞。返回thread.interrupted()这个方法表示当前阻塞是不是被打断的,如果是unpark方法触发的解阻塞,返回false。

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

当阻塞被接触之后再次进入死循环内部,获取锁,重复之前的逻辑,直到获取到锁,返回true或者false,如果返回true,那么表示当前线程是被打断的,调用selfInterrupt()方法打断当前线程。

OK,本次就先讲完ReentrantLock的lock方法,后面再讲unlock解阻塞方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值