ReentrantLock加锁原理

今天来探究一下ReentrantLock中非公平锁的加锁原理,首先看一段代码来了解一下ReentrantLock是如何使用的

ReentrantLock lock = new ReentrantLock();
Logger logger = LoggerFactory.getLogger(this.getClass());
Thread t1 = new Thread(() -> {
	logger.info("线程[{}]在[{}]开始尝试获取锁", Thread.currentThread().getName(), LocalDateTime.now());
	lock.lock();
	try {
		logger.info("线程[{}]在[{}]获取到锁了", Thread.currentThread().getName(), LocalDateTime.now());
		try {
			TimeUnit.SECONDS.sleep(5);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	} finally {
		lock.unlock();
		logger.info("线程[{}]在[{}]释放锁了", Thread.currentThread().getName(), LocalDateTime.now());
	}
}, "t1");
t1.start();
lock.lock();
logger.info("线程[{}]在[{}]获取到锁了",Thread.currentThread().getName(), LocalDateTime.now());
try {
	try {
		TimeUnit.SECONDS.sleep(1);
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
}finally {
	lock.unlock();
	logger.info("线程[{}]在[{}]释放锁了",Thread.currentThread().getName(), LocalDateTime.now());
}

看下执行结果:
在这里插入图片描述
接下来我们看下ReentrantLock是如何加锁的,首先是主线程的加锁,这时候由于没有人跟他抢锁,锁代码的逻辑也相对简单:
主要执行的是下面的这段代码:java.util.concurrent.locks.ReentrantLock.NonfairSync#lock
lock
compareAndSetState方法是用过cas的方式将state的值从0改成1,如果这个修改成功,那么就算是获取到锁了,同时,用setExclusiveOwnerThread来给exclusiveOwnerThread设置为当前线程,也就是标记一下当前锁的持有线程。此时,主线程加锁成功,代码也就执行结束了。
t1线程也来加锁时,如果主线程还没有释放锁,那就要执行acquire方法了,方法主体逻辑如下:
acquire
在执行acquire时,程序会先执行一下tryAcquire方法,也就是再次尝试加锁,最后会调用到java.util.concurrent.locks.ReentrantLock.Sync#nonfairTryAcquire中:
nonfairTryAcquire
在这个代码中,如果当前的state已经等于0,也就是之前持有锁的线程现在释放锁了,那么不用想,直接cas抢锁,抢到就成功,抢不到就失败。如果当前的state不等于0,那么需要判断一下是否是重入,如果是重入,那就将state增加后写回去。
如果tryAcquire没有成功获取到锁,那么就需要执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)了。首先,是addWaiter
addWaiter
在这个方法中,先创建了一个Node对象,如果此时tail(也就是队尾)如果不为null,那么就将当前node.prev指向队尾,同时通过cas当前node设置为队尾,cas成功后,当之前队尾的节点的next指向当前node。如果tail为null,那就需要执行enq的逻辑了。
enq
enq这个段逻辑中,初始化了aqs的队列。在这个死循环中,先判断tail是否为空,如果为空,那就通过cas的方式初始化一个head,初始化成功后,将tail指向head。如果tail不为空,那么就将当前node拼接到tail的后面同时变成新的tail
至此,addWaiter的逻辑完毕,在这个方法中出初始化了队列,并将创建的node对象放入队列中,注意,只是放入队列中!
然后执行acquireQueued方法:
acquireQueued
在上面的代码中,先获得当前node前一个node,如果前一个nodehead,那就再尝试加锁一次(老子排第二嘛,万一第一已经搞完了呢)如果成功,那么当前node就是head了,同时将前一个nodenext置位null(为了断开队列)。如果前一个不是head或者tryAcquire失败(非公平锁,可能被截胡了),那就会进入到shouldParkAfterFailedAcquire中。
在这里插入图片描述
在这里插入图片描述
这个方法至少跑两次,因为第一次进来的时候pred.waitStatus的值是0,需要通过cas将0改为-1,同时返回一个false。同时,这也会导致acquireQueued中的死循环需要跑第二次,但是,这样造就了当前线程有第二次抢锁的可能。如果抢不到锁,那最后会跑到parkAndCheckInterrupt中:
在这里插入图片描述

最后阻塞在park这里。当主线程释放锁时,t1会从park中醒来,并返回Thread.interrupted(),如果t1在等待的过程中没有被打断过,这个return的就是false,然后继续acquireQueued的死循环,尝试获取锁,一旦获取到锁,这个死循环也就结束了,同时acquireQueued这个方法会返回t1在等待获取锁的过程中有没有被打断过。

上面就是ReentrantLocklock方法的源码解读了,在这个加锁的过程中体现了能不park就不park的思想。Doug Lea总是在各种临门一脚的地方尝试再获取一次锁,如果能获取到就结束,如果实在是获取不到才会去park,毕竟park涉及从用户态升级到内核态操作比较重型。

下面再简单的说下lockInterruptibly,这是一个可打断的lock方式。
首先是acquireInterruptibly,如果当前线程已经被打断,那就不用抢锁了,直接抛异常吧。接下来是尝试获取锁,如果获取失败,则进入doAcquireInterruptibly
acquireInterruptibly
doAcquireInterruptibly中如果parkAndCheckInterrupt是被打断唤醒的,那么返回值是true,最后就会执行throw new InterruptedException();
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值