目录
目录
ReentrantLock源码解析
在分析ReentrantLock前先来大致了解一下AQS
AQS的基本概念
Java并发编程核心在于java.concurrent.util包,juc当中的大多数同步器实现都是围绕着共同的基础行为,比如等待队列、条件队列、独占获取、共享获取等,而这个行为的抽象就是基于AbstractQueuedSynchronizer简称AQS,AQS定义了一套多线程访问共享资源的同步器框架,是一个依赖状态(state)的同步器。
AQS保证线程安全的大致原理(伪代码)
//加锁
lock;
//为什么要死循环?因为只有加锁成功的线程才能跳出循环,其他的线程若是跳出循环了就会有线程问题了啊,因此需要一个死循环在这
for(;;){
//一次多个线程同时来进行加锁操作,如何能够保证只有一个线程能够加锁成功呢?
//通过CAS算法来加锁,他是一个原子操作。详情可见Unsafe魔法类
if(加锁成功){
//加锁成功的线程说明获取到了资源,跳出循环,执行真正的业务逻辑代码
break;
}
//其他未加锁成功的线程,不能一直占着CPU资源,所以要想个办法让出CPU资源
//将线程进行阻塞,让出资源。唤醒时调用LockSupport.unpark(thread)即可,唤醒线程需要在持有锁的线程释放锁才可以去唤醒,不然唤醒也没有意义。因此放在unlock后面
LockSupport.park(thread);
//向容器中放入阻塞的线程,为以后唤醒线程使用。
}
/**
*代码逻辑
*/
//解锁
unlock;
//唤醒之前阻塞的线程
//唤醒操作需要唤醒指定的线程,因此unlock方法需要传入需唤醒的线程。那么这个线程从哪搞呢?
//说明得在线程阻塞的时候就得有一个容器去放这个线程,可以是HashSet,LinkedList,数组等等,但是最后AQS用了一个双向链表。
LockSupport.unpark(thread);
这段伪代码大致就是AQS加锁解锁的基本原理了,后面会以ReentrantLock为例来详细解析。
由此可见,要实现一个AQS同步器,主要有以下核心的几点:
1.死循环(自旋)
2.CAS加锁算法,原子操作的一个算法 (CAS依赖的汇编指令:cmpxchg())
3.线程的阻塞与唤醒(LockSupport)
4.容器queue(用来存放被阻塞的线程)
AQS具备特性,共享模式等概念
1.阻塞队列 2.共享/独占 3.公平/非公平 4.可重入 5.允许中断
AQS内部一个重要属性:volatile int state,表示资源是否被加锁,加了几次锁
State三种访问方式:getState()、setState()、compareAndSetState()
AQS定义的两种共享的模式
- 独占(Exclusive):只有一个线程能够执行,如ReentrantLock
- 共享(Share):多个线程可以同时执行,如CountDownLatch/Semaphore
AQS定义的两种队列:
1.同步等待队列:也叫CLH队列,是一种基于双向链表数据结构的队列,是先入先出(FIFO)的线程等待队列
2.条件等待队列: Condition是一个多线程间协调通信的工具类,一个或多个线程等待某个条件(Condition)满足,才会被重新唤醒,从而开始竞争锁。如:cyclicBarrier.await();
AQS内部的大致数据结构图
ReentrantLock基本概念
ReentrantLock是一种基于AQS框架的应用实现,是以java程序来实现的一种线程并发访问的同步手段,它的功能类似于synchronized是一种互斥锁,可以保证线程安全。而且它具有比synchronized更多的特性,比如它支持手动加锁与解锁,支持加锁的公平性。
ReentrantLock如何实现synchronized不具备的公平与非公平性呢?
在ReentrantLock内部定义了一个Sync的内部类,该类继承AbstractQueuedSynchronized,对该抽象类的部分方法做了实现;并且还定义了两个子类:
1、FairSync 公平锁的实现
2、NonfairSync 非公平锁的实现
这两个类都继承自Sync,也就是间接继承了AbstractQueuedSynchronized,所以这一个ReentrantLock同时具备公平与非公平特性。
这里提一下,AQS用了模板方法的设计模式
ReentrantLock中Node节点的几个重要属性
在解析ReentranrLock源码之前,先了解一下AQS中等待队列中的重要角色Node。这里可以先混个眼熟,后面解析源码中会反复提到这个Node节点。
waitStatus:Node节点的状态,目前有5种状态,分别是:
0(初始状态);
1(CANCELLED 异常状态);
-1(SIGNAL 可被唤醒状态);
-2(CONDITION 条件等待状态);
-3(PROPAGATE 传播状态)
prev:节点的前置节点
next:节点的后置节点
thread:节点所对应的线程
nextWaiter:当前节点在Condition中等待队列上的下一个节点(给Condition等待队列使用),此次源码解析未用到
ReentrantLock代码案例
一个简单的demo,初步认识
public class ReentrantLockDemo {
private static volatile int count = 0;
private static ReentrantLock lock = new ReentrantLock(true);
/**
* count虽然被volatile关键字修饰,若没加锁结果并不是50000,而是小于等于50000
* 加锁后无论怎样执行,结果都是50000
**/
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
// count++;
lock.lock();
try{
count++; //先读,再加,不是一个原子操作
}finally {
lock.unlock();
}
}
});
thread.start();
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count==" + count);
}
}
ReentrantLock源码解析
此次解析的源码基于JDK1.8,以公平锁来解析
//实例化一个公平锁
private static ReentrantLock lock = new ReentrantLock(true);
lock.lock();
//进入锁,调用acquire方法,去获取锁,参数1的意思是给这个锁的计数器上加1
final void lock() {
acquire(1);
}
/**
* 三个核心方法:tryAcquire(尝试获取资源);
* addWaiter(将未获取到资源将线程添加至等待队列);
* acquireQueued(将等待队列中的线程取出来获取资源)
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//该方法在具体实现类中
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//获取当前资源的状态,也可以当作为计数器,因为是重入锁嘛,为0则说明资源未被其他线程所持有
int c = getState();
if (c == 0) {
//判断等待队列是否为空,若是第一次获取锁,队列必然是空的
//或者判断等待队列中的第一个线程是否为空,是否是当前线程
if (!hasQueuedPredecessors() &&
//通过CAS算法(原子操作)来将state状态修改为1,从而使当前线程获取资源,并给资源加锁
compareAndSetState(0, acquires)) {
//把当前线程设置为持有锁线程
setExclusiveOwnerThread(current);
return true;
}
}
//当state大于0时,且当前线程也是持有锁的线程,可以继续获取锁,可重入锁的体现就在这
else if (current == getExclusiveOwnerThread()) {
//将state状态再加1,这也是我为什么上面说可以当作计数器的原因,因为加了几次锁,state的值就为几
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//没有获取到资源,返回false
return false;
}
//将未获取到资源将线程添加至等待队列,数据结构是一个双向链表
private Node addWaiter(Node mode) {
//new一个节点
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
//获取tail(尾节点)
Node pred = tail;
//尾节点不为空时,说明这个队列已经有其他节点在排队了,需要往后面加节点
if (pred != null) {
//将新节点的前驱指针指向队列里的最后一个节点
node.prev = pred;
//通过cas算法来将tail指针指向新节点,因为有并发,所以需要cas算法来保证只有一个节点成为尾节点
if (compareAndSetTail(pred, node)) {
//将之前的尾节点的next指针指向新节点,那么此时新节点就成为了新的尾节点
pred.next = node;
//返回当前新节点
return node;
}
}
//说明还没有队列,需要建立一个队列,将新节点入队列
enq(node);
//返回当前新节点
return node;
}
//建立一个队列,将节点入队列
//注意:这个CLH队列的head节点的thread值是空的,head节点的下一个节点才是真正排队的线程节点,为什么要这样设计,在线程节点出队列的时候在细品
private Node enq(final Node node) {
//一个自旋操作,确保多个线程同时进来也能够都将所有节点加入至队列
for (;;) {
Node t = tail;
//当尾节点是空时,初始化一个CLH队列
if (t == null) { // Must initialize
//通过CAS算法将head头节点指向一个新new的节点(这个节点的thread为空)
if (compareAndSetHead(new Node()))
tail = head;
} else {
//第二次进入循环,将参数传进来的新节点的前驱指针指向尾节点
node.prev = t;
//通过CAS算法将tail指针指向新节点
if (compareAndSetTail(t, node)) {
//老的尾节点的next指针就指向新节点,此时的新节点也就成为了新的尾节点了
t.next = node;
return t;
}
}
}
}
//以独占不间断模式(自旋操作)获取队列中已存在的线程。由条件等待方法并且获取使用。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//一个死循环。自旋操作。确保并发的情况下,只能有一个线程能够获取锁资源。其他线程会在此进行阻塞,等待锁的释放并获取锁资源
for (;;) {
//获取此节点的前驱节点
final Node p = node.predecessor();
//如果当前节点的前驱节点是head头节点,那么就去尝试获取资源,调用tryAcquire
//注意:此时获取资源的是当前节点中的thread
if (p == head && tryAcquire(arg)) {
//当前节点获取到资源后,那么就将当前节点设置尾head头节点,将thread置空,前驱指针置空
//此时这个节点就成了head节点了,这里就对应上了之前head的thread为什么要为空了,这样的话每次都是将老的头结点扔掉,并不会对真正需要操作资源的thread做什么其他操作或处理,shouldParkAfterFailedAcquire方法里会详解
setHead(node);
//将老的头节点的next指针置空,那么这个老节点就被移除看这个CLH队列,会被下次的gc给清理掉
p.next = null; // help GC
failed = false;
return interrupted;
}
//检查并更新节点的状态
if (shouldParkAfterFailedAcquire(p, node) &&
//将线程阻塞,释放CPU资源
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
//当线程有中断处理时,将中断线程的Node的waitStatus标记成1(无效),然后给清理掉,队列中只保留正常状态的Node节点
cancelAcquire(node);
}
}
//注意:当前Node节点(队列中第二个节点)是否能够唤醒的标识量waitStatus是放在head节点上的,而不是在当前节点本身,当前节点的waitStatus一直都是0
//为什么这样呢?
//因为当Node中的thread唤醒时,只需将head指针指向队列中的第二个节点即可。自旋和线程获取到资源的原因会将第二个节点的thread置空,并且waitStatus会通过CAS算法将其变成-1,即说明第三个节点被认为是下一个可唤醒的节点,同时会将之前的第一个节点(原来的head节点)给剔除。结合FIFO队列原理和图来理解更好理解
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//第一次循环进来时,waitStatus的默认值是0
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.
*/
//第二次循环进入,由于第一次循环将waitStatus设为了SIGNAL状态
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
//若当前节点(第二个节点)的上一个节点(head节点)的waitStatus大于0,则认为该节点已经无效了,剔除队列
//并且遍历队列,将其余无效的节点也剔除掉
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.
*/
//第一次循环的时候,通过CAS算法将当前节点(队列中第二个节点)的前驱节点(head节点)的waitStatus状态设为SIGNAL(-1,可唤醒)
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
//能进入到这里,说明资源已经被其他线程所占有,当前线程得阻塞等待
private final boolean parkAndCheckInterrupt() {
//让当前线程park,释放CPU资源
LockSupport.park(this);
//重置线程中断状态
return Thread.interrupted();
}
自此,加锁的代码基本解析完了。
最后的LockSupport.park(this);让线程进行阻塞,但是不能一直阻塞在那啊,因此当资源被释放时,需要一个操作来将线程进行唤醒。
这个就是解锁并且唤醒新线程的操作。下面来分析解锁
lock.unlock();
//解锁,参数1的意思是给这个锁的计数器减1
public void unlock() {
//释放锁
sync.release(1);
}
/**
* 两个核心方法:tryRelease(尝试释放锁)
* unparkSuccessor(唤醒被阻塞的线程)
*/
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
//head节点不为空,说明有其他线程在等待中,且他的waitStatus状态是可被唤醒
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
//尝试释放锁
protected final boolean tryRelease(int releases) {
//将state状态减1,但不一定减完后就是0,因为可重入锁会加几次锁
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
//此时state状态为0,说明当前线程已经执行完所有的逻辑,释放了资源,已经可以由其他线程再来获取资源
free = true;
//将持有锁资源线程重新置空,让新的线程来填补
setExclusiveOwnerThread(null);
}
//写入状态值
setState(c);
return free;
}
//唤醒被阻塞的线程
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
//此时这个node是head节点
int ws = node.waitStatus;
if (ws < 0)
//这个head节点的waitStatus若是-1(SIGNAL),说明他的下一个节点(真实存放thread的节点)是要被唤醒的节点
//通过CAS算法将head节点的waitStatus从-1改为0
//这里是为了给非公平锁用的,若是非公平锁的话,这个时候第二个节点的线程和新的线程一起去抢锁,此时第二个节点中的线程并不是能够直接获取到锁的,因此先将状态改为0,然后再来唤醒第二个节点的thread,如果第二个节点的线程没能够抢到锁,那么将会继续阻塞。
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
//获取到需要唤醒thread的节点s
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
//若队列的第二个节点是空或者waitStatus是失效状态,那就从队列尾部往头部遍历,一直找到到最靠前的那个节点为可唤醒的节点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
//唤醒队列最靠前的那个节点中的thread
LockSupport.unpark(s.thread);
}
到了这里。ReentrantLock的加锁与解锁的主要代码都已经解析完毕了。
通过分析我们可以看到,代码中多次用到了CAS算法,CAS算法是一个原子操作,无论在多高的并发情况下,有且仅有一个线程能够完成一次CAS的操作。AQS同步器中很多地方都会用到CAS算法。
ReentrantLock中使用的队列是CLH队列,数据结构大致如下图:
也多次使用了死循环(自旋),其作用是能够保证每个线程都能够执行到,这也是AQS的一处精髓所在吧。不止是ReentrantLock,其他的锁也都会用上这个死循环。
等待的线程通过LockSupport的park和unpark方法来进行阻塞与唤醒。
ReentrantLock源码流程图
这个是我画的一个公平锁的一个源码的流程图。可以参考一下。
总结
本文详细的介绍了ReentrantLock,解析了主要的源码。在AQS的其他同步锁中,大致实现的思想与ReentrantLock的大同小异,可能细节方面有很多需要处理的。ReentrantLock也是大家比较常用的一种锁,解析起来也比较有意义。
对于其他的锁,也可以按照上面的AQS保证线程安全的大致原理的思路去分析其源码。
最后,很感谢你能够耐着性子一直看到最后,希望本篇文章也能够给你带来一定程度上的帮助,谢谢。