今天来探究一下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
compareAndSetState
方法是用过cas
的方式将state
的值从0改成1,如果这个修改成功,那么就算是获取到锁了,同时,用setExclusiveOwnerThread
来给exclusiveOwnerThread
设置为当前线程,也就是标记一下当前锁的持有线程。此时,主线程加锁成功,代码也就执行结束了。
当t1
线程也来加锁时,如果主线程还没有释放锁,那就要执行acquire
方法了,方法主体逻辑如下:
在执行acquire时,程序会先执行一下tryAcquire
方法,也就是再次尝试加锁,最后会调用到java.util.concurrent.locks.ReentrantLock.Sync#nonfairTryAcquire
中:
在这个代码中,如果当前的state
已经等于0,也就是之前持有锁的线程现在释放锁了,那么不用想,直接cas
抢锁,抢到就成功,抢不到就失败。如果当前的state
不等于0,那么需要判断一下是否是重入
,如果是重入
,那就将state
增加后写回去。
如果tryAcquire没有成功获取到锁,那么就需要执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
了。首先,是addWaiter
在这个方法中,先创建了一个Node
对象,如果此时tail
(也就是队尾)如果不为null,那么就将当前node.prev
指向队尾,同时通过cas
将当前node
设置为队尾,cas
成功后,当之前队尾的节点的next指向当前node
。如果tail
为null,那就需要执行enq的逻辑了。
在enq
这个段逻辑中,初始化了aqs
的队列。在这个死循环中,先判断tail
是否为空,如果为空,那就通过cas
的方式初始化一个head
,初始化成功后,将tail
指向head
。如果tail
不为空,那么就将当前node
拼接到tail
的后面同时变成新的tail。
至此,addWaiter的逻辑完毕,在这个方法中出初始化了队列,并将创建的node对象放入队列中,注意,只是放入队列中!
然后执行acquireQueued
方法:
在上面的代码中,先获得当前node
的前一个node
,如果前一个node
是head
,那就再尝试加锁一次(老子排第二嘛,万一第一已经搞完了呢)如果成功,那么当前node
就是head
了,同时将前一个node
的next
置位null(为了断开队列)。如果前一个不是head
或者tryAcquire
失败(非公平锁,可能被截胡了),那就会进入到shouldParkAfterFailedAcquire
中。
这个方法至少跑两次,因为第一次进来的时候pred.waitStatus
的值是0,需要通过cas将0改为-1,同时返回一个false。同时,这也会导致acquireQueued
中的死循环需要跑第二次,但是,这样造就了当前线程有第二次抢锁的可能。如果抢不到锁,那最后会跑到parkAndCheckInterrupt
中:
最后阻塞在park这里。当主线程释放锁时,t1会从park
中醒来,并返回Thread.interrupted()
,如果t1在等待的过程中没有被打断过,这个return的就是false,然后继续acquireQueued
的死循环,尝试获取锁,一旦获取到锁,这个死循环也就结束了,同时acquireQueued
这个方法会返回t1在等待获取锁的过程中有没有被打断过。
上面就是ReentrantLock
中lock
方法的源码解读了,在这个加锁的过程中体现了能不park
就不park
的思想。Doug Lea总是在各种临门一脚的地方尝试再获取一次锁,如果能获取到就结束,如果实在是获取不到才会去park
,毕竟park
涉及从用户态升级到内核态操作比较重型。
下面再简单的说下lockInterruptibly
,这是一个可打断的lock方式。
首先是acquireInterruptibly
,如果当前线程已经被打断,那就不用抢锁了,直接抛异常吧。接下来是尝试获取锁,如果获取失败,则进入doAcquireInterruptibly
中
在doAcquireInterruptibly
中如果parkAndCheckInterrupt
是被打断唤醒的,那么返回值是true,最后就会执行throw new InterruptedException();