ReentrantLock的源码阅读

        ReentrantLock是JUC包下的可重入的独占锁,可以在多线程的情况下保证线程安全,通过调用lock()和unlock()方法来进行加锁和解锁操作,使用示例如下

class X {
	private final ReentrantLock lock = new ReentrantLock();
	
	//其他属性

	public void method() {
		//加锁
		lock.lock();
		try {
			//业务逻辑
		} finally {
			//解锁
			lock.unlock();
		}
	}
}

那么这个加锁和解锁的源码是怎样的呢?需要从lock()方法的源码开始查看具体是实现。ReentrantLock中只有一个属性,内部类Sync,ReentrantLock的加锁内部实际上都是通过Sync来实现,ReentrantLock的构造方法有两个。源码如下

// 1. 无参构造函数
public ReentrantLock() {
	sync = new NonfairSync();
}

// 2.有参构造函数
public ReentrantLock(boolean fair) {
	sync = fair ? new FairSync() : new NonfairSync();
}

通过这两个构造方法可以得到一下信息,ReentrantLock可以实现公平锁FairSync和非公平锁NonfairSync,默认是实现非公平锁NonfairSync。分析源码分两种场景分析,一种是公平锁,一种是非公平锁,我们首先阅读公平锁的加锁源码。

1. 公平锁FairSync的加锁源码

        公平锁的加锁lock()源码详情如下,源码的注释在代码中注明了,话不多少,源码如下

//加锁
final void lock() {
	acquire(1);
}

acquire()的源码如下

public final void acquire(int arg) {
	//tryAcquire(arg)加锁逻辑,返回boolean,true加锁成功 false表示加锁失败
	//如果加锁成功,取反,后面的&&逻辑不触发
	//如果加锁失败,取反,执行 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)的相关逻辑,先执行addWaiter(Node.EXCLUSIVE)方法,Node.EXCLUSIVE=null。然后执行acquireQueued()方法
	if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
		//内部调用线程Thread的interrupt()方法
		selfInterrupt();
}

tryAcquire方法的加锁的源码

//tryAcquire方法的加锁的源码
protected final boolean tryAcquire(int acquires) {
	//获取当前线程
	final Thread current = Thread.currentThread();
	//获取state的值,该属性由volatile,可以保证线程可见性
	int c = getState();
	//如果state的值为0
	if (c == 0) {
		//hasQueuedPredecessors用来判断队列中是不是轮到当前线程加锁
		//compareAndSetState():CAS操作,将AQS中的state属性的值由0改为1,0--->1
		if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
			//设置exclusiveOwnerThread属性设置为当前线程,exclusiveOwnerThread用来标识哪个线程拿到了锁
			setExclusiveOwnerThread(current);
			//返回true
			return true;
		}
	}
	//state的值不为0的情况
	//先校验拿到锁的线程是否是当前线程
	else if (current == getExclusiveOwnerThread()) {
		//如果是当前线程,state的值加1
		int nextc = c + acquires;
		//健壮性校验
		if (nextc < 0)
			throw new Error("Maximum lock count exceeded");
		//设置state的值
		setState(nextc);
		//返回true
		return true;
	}
	//否则返回false,表示加锁失败
	return false;
}

hasQueuedPredecessors()的源码如下

/**
 * 该方法是AQS中的方法
 * public final boolean hasQueuedPredecessors() {
 *  尾节点
 *  Node t = tail;
 *  头节点
 *  Node h = head;
 *  Node s;
 *  h != t: 如果这个判断为false,表示 h == t,头节点等于尾节点,表明现在是一个空队列,如果为true,表示队列中存在Node
 *
 *  ((s = h.next) == null || s.thread != Thread.currentThread()):
 *      1. (s = h.next) == null:头节点赋值给s,s==null如果为true,表示头节点为空,反之头节点不为空,这种是考虑到了多线程并发下,修改值还未修改完的情况
 *      2. 尾节点不为空,(s = h.next) == null为false,再校验尾节点线程是否是当前线程,如果是,返回false
 *  return h != t &&
 *      ((s = h.next) == null || s.thread != Thread.currentThread());
 * }
 * 方法返回false的情况:
 * 1. 头节点等于尾节点,空队列的时候
 * 2. 头节点存在下一个节点,并且下一个节点就是当前线程
 */
public final boolean hasQueuedPredecessors() {
	//获取尾结点
	Node t = tail;
	//获取头节点
	Node h = head;
	Node s;
	
	return h != t &&
		((s = h.next) == null || s.thread != Thread.currentThread());
}

校验到如果还没轮到当前线程加锁或者加锁失败,就会将线程放入队列,放入队列通过addWaiter()方法来实现,其源码如下

//将Node加入队列中
private Node addWaiter(Node mode) {
	//创建一个Node对象,该对象中的nextWaiter为null
	Node node = new Node(Thread.currentThread(), mode);
	//获取队列中的尾节点
	Node pred = tail;
	//尾节点是否为空
	if (pred != null) { //if里面的逻辑是一个入队列的处理逻辑,将新创建的节点添加到队列的尾部
		//尾节点不为空
		//将新建的node对象的上一节点指向尾节点
		node.prev = pred;
		//CAS操作,将尾节点由原值pred修改为node节点
		if (compareAndSetTail(pred, node)) { //如果这里的CAS操作失败,就是compareAndSetTail返回false,那么线程将进入enq()方法中
			//将原来的尾节点的next节点指向新创建的node节点
			pred.next = node;
			//返回新创建的节点node
			return node;
		}
	}
	//enq方法
	enq(node);
	//返回创建的node对象
	return node;
}

//Node加入队列中
private Node enq(final Node node) {
	//等价于while(true)操作,如果不触发中断或者跳出循环的话,会一直循环调用,起到一个线程阻塞的效果==>自旋锁
	for (;;) {
		//获取尾节点
		Node t = tail;
		if (t == null) {
			//尾节点如果为空,创建一个空的Node对象,设置为头节点
			//这里一开始有点难以理解
			//我的理解是:AQS是一个阻塞队列,符合FIFO的顺序,如果尾节点不存在。说明还没有任务节点入队列。队列中是空的,这里创建的空Node节点,即使尾节点,也是头节点
			if (compareAndSetHead(new Node()))
				//将头节点赋值为尾节点,这里并没有跳出循环,那么执行完后
				//队列中存在一个空的Node对象,然后执行下一次循环,下一次循环的话,尾节点就不在进入这里而是执行else的逻辑
				tail = head;
		} else {
			//这里的逻辑和addWaiter()方法中if (pred != null)里面的逻辑一模一样
			//就是将新node放入队列中
			node.prev = t;
			if (compareAndSetTail(t, node)) {
				t.next = node;
				//执行成功后,返回尾节点
				return t;
			}
		}
	}
}

从源码上来看,该方法通过自旋的方案,可以保证线程一定可以进入队列。执行完addWorker方法后,执行acquireQueued的方法,该方法的源码如下

//Node:刚加入队列中的Node,arg=1
//该方法用来加锁或者入队列并修改waitStatus的值
final boolean acquireQueued(final Node node, int arg) {
	//失败flag
	boolean failed = true;
	try {
		//线程中断flag
		boolean interrupted = false;
		//自旋了
		for (;;) {
			//获取Node的上一个节点prev
			final Node p = node.predecessor();
			//如果上一个节点是头节点,那么就去获取锁tryAcquire
			if (p == head && tryAcquire(arg)) {
				//如果获得锁成功,那么上一个节点需要出队列,出队列后,当前节点就是头节点了,所以需要设置当前节点为头节点
				setHead(node);
				//next设置为null,便于GC回收
				p.next = null;
				failed = false;
				return interrupted;
			}
			//shouldParkAfterFailedAcquire(p, node):该方法用来查询node节点的有效的上一个节点
			if (shouldParkAfterFailedAcquire(p, node) &&
				//使用park线程阻塞当前线程
				parkAndCheckInterrupt())
				//线程中断标记改为true
				interrupted = true;
		}
	} finally {
		if (failed)
			//清除队列中失效或者不需要唤醒的Node节点
			cancelAcquire(node);
	}
}


//该方法用来查询node节点的有效的上一个节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	//获取节点的等待状态
	int ws = pred.waitStatus;
	if (ws == Node.SIGNAL)
		//Node.SIGNAL:值为-1,为该状态时直接返回true
		return true;
	if (ws > 0) {
		//循环
		//node节点循环查询并链接上一个状态值为-1的节点
		//找到后,将node节点的prev指向该节点
		do {
			node.prev = pred = pred.prev;
		} while (pred.waitStatus > 0);
		//上一个节点的next连接到node
		pred.next = node;
	} else {
		//将node的上一个节点状态改为Node.SIGNAL,-1。CAS操作
		compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
	}
	return false;
}

到这里,公平锁的加锁逻辑就看完了,进行加锁逻辑流程总结

  • 调用ReentrantLock的lock()方法
  • ReentrantLock的lock()方法会调用FairSync的lock进行加锁
  • FairSync的lock()方法会调用acquire(1)方法
  • acquire(1)调用tryAcquire(arg)加锁
  • tryAcquire(arg)方法中通过hasQueuedPredecessors()方法进行校验,只有当队列为空或者已经排队到当前队列加锁的情况下,线程才可以进行加锁操作,这也是公平锁的公平所在,或者进行重入操作
  • 如果tryAcquire(arg)方法判断当前线程还不能加锁,那么通过addWaiter方法将线程加入阻塞队列中进行排队
  • 线程进入队列后,再通过shouldParkAfterFailedAcquire()方法将前面的节点等待状态改为-1,并阻塞当前线程

以上是公平锁的完整流程源码分析。接下来进行非公平锁的源码分析

2. 非公平锁NonfairSync的加锁源码

        由于前面较为详细的介绍了公平锁的加锁源码,非公平锁的加锁原理上大同小异,这里我们只对差异的地方源码进行分析,第一个差异是在lock方法中,公平锁的lock方法中直接调用acquire()方法,但是非公平锁的实现原理不是的,其实现源码如下

final void lock() {
	//先加锁操作【将state的值由0改为1】,如果加锁成功,将锁的线程修改为当前线程
	//如果修改失败,执行acquire(1)的逻辑
	if (compareAndSetState(0, 1))
		setExclusiveOwnerThread(Thread.currentThread());
	else
		acquire(1);
}

 从源码上可以看到,调用加锁的lock方法后直接先进行加锁操作。加锁失败的话,才调用acquire()方法。调用acquire()方法中在调用tryAcquire()方法。tryAcquire()方法在公平锁和非公平锁的实现上也存在不一样的逻辑,非公平锁的实现源码如下

protected final boolean tryAcquire(int acquires) {
	//执行nonfairTryAcquire方法
	return nonfairTryAcquire(acquires);
}



final boolean nonfairTryAcquire(int acquires) {
	//获取当前线程
	final Thread current = Thread.currentThread();
	//获取锁的状态
	int c = getState();
	//如果state=0,表示锁未被其他线程占用,执行加锁逻辑
	if (c == 0) {
		//加锁操作,CAS操作,这里和公平锁的区别在于,公平锁会判断一下,是不是轮到当前线程加锁了。而非公平是不进行校验,直接进行加锁操作,修改state的值,其他逻辑
		//和公平锁是一样的
		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;
}

这里并不会进行是否轮到该线程加锁了,而是直接加锁或者重入操作。以上lock()和tryAcquire()两个方法就是公平锁和非公平锁的差异所在,其他地方的处理逻辑,例如入队列等一系列逻辑都是一样的。这些源码阅读的话,还需要有一个知识点,Unsafe类,源码中出现了很多的compareAndSetxxx的方法,这类方法就是通过CAS实现来保证原子操作的,这类方法的源码可以顺便也看看,源码如下

//得到Unsafe类
private static final Unsafe unsafe = Unsafe.getUnsafe();
//操作state
private static final long stateOffset;
//操作头节点
private static final long headOffset;
//操作尾节点
private static final long tailOffset;
//操作Node节点的等待状态waitStatus
private static final long waitStatusOffset;
//操作Node节点的next
private static final long nextOffset;

static {
	try {
		stateOffset = unsafe.objectFieldOffset
			(AbstractQueuedSynchronizer.class.getDeclaredField("state"));
		headOffset = unsafe.objectFieldOffset
			(AbstractQueuedSynchronizer.class.getDeclaredField("head"));
		tailOffset = unsafe.objectFieldOffset
			(AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
		waitStatusOffset = unsafe.objectFieldOffset
			(Node.class.getDeclaredField("waitStatus"));
		nextOffset = unsafe.objectFieldOffset
			(Node.class.getDeclaredField("next"));

	} catch (Exception ex) { throw new Error(ex); }
}

3. ReentrantLock的解锁源码

        有加锁,肯定就有解锁,那么ReentrantLock的解锁是如何做的呢?其实相对来说更简单一些了,解锁的逻辑核心就是将state的值修改为0的过程,每次unlock()实际就是一次state的是减1。源码如下

public void unlock() {
	sync.release(1);
}


public final boolean release(int arg) {
	if (tryRelease(arg)) {
		Node h = head;
		if (h != null && h.waitStatus != 0)
			unparkSuccessor(h);
		return true;
	}
	return false;
}


protected final boolean tryRelease(int releases) {
	//获取state的值,并减1
	int c = getState() - releases;
	//获取当前线程是否是拿到锁的线程,如果不是,抛出异常
	if (Thread.currentThread() != getExclusiveOwnerThread())
		throw new IllegalMonitorStateException();
	//标记,用来标记是否完全解锁,true表示解锁成功,false表示解锁未完成,存在重入操作
	boolean free = false;
	//state=0表示解锁成功
	if (c == 0) {
		free = true;
		setExclusiveOwnerThread(null);
	}
	setState(c);
	return free;
}

到此ReentrantLock的源码解读已经结束了。我们简单的进行一下总结,我个人的总结是如下几点

  • ReentrantLock的加锁过程,实际上是对state的值修改来实现
  • 在多线程场景下,使用CAS来实现state值修改的原子性,确保每次只能有一个线程修改成功
  • 在多线程场景下,由于state是一个需要所有线程都可见的变量,所以必须确保线程间的可见性,所以state属性通过volatile修饰来达到线程间的可见性
  • 另外,对于没有获得锁的线程,进入队列中进行等待再次加锁

以上是我根据阅读ReentrantLock的源码得到的一些总结。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值