JUC系列整体栏目
内容 | 链接地址 |
---|---|
【一】深入理解JMM内存模型的底层实现原理 | https://zhenghuisheng.blog.csdn.net/article/details/132400429 |
【二】深入理解CAS底层原理和基本使用 | https://blog.csdn.net/zhenghuishengq/article/details/132478786 |
【三】熟练掌握Atomic原子系列基本使用 | https://blog.csdn.net/zhenghuishengq/article/details/132543379 |
【四】精通Synchronized底层的实现原理 | https://blog.csdn.net/zhenghuishengq/article/details/132740980 |
【五】通过源码分析AQS和ReentrantLock的底层原理 | https://blog.csdn.net/zhenghuishengq/article/details/132857564 |
通过源码分析AQS和ReentrantLock的底层原理
- 一,深入理解AQS和ReentrantLock的底层原理
- 1,初识AQS
- 2,AQS抽象类的源码分析
- 2.1,同步等待队列
- 2.2,条件等待队列
- 3,Node结点分析
- 4,ReentrantLock底层源码分析
- 4.1,NonfairSync
- 4.1.1,线程尝试获取锁
- 4.1.2,双向链表的创建
- 4.1.3,Node结点入队
- 4.1.4,Node结点的阻塞
- 4.1.5,同步监视器修改状态,释放资源和释放锁
- 4.1.6,唤醒链表中的下一个结点
- 4.2,FairSync
- 4.3,ReentrantLock底层实现总结
- 5,ReentrantLock的特性
- 6,ReentrantLock和Synchronized联系
一,深入理解AQS和ReentrantLock的底层原理
在学习这个AQS之前,需要先了解synchronized设计思想和底层实现,因为AQS的底层是参考于synchronized,可以看本人的上一篇文章
1,初识AQS
在上一篇中了解了synchronized的底层原理,synchronized是jvm层面的一把隐式锁,在java层面很难的去控制这把锁,如某些场景需要手动加解锁等,并且在jdk6之前,synchronized还是一把重锁,并没有后来的锁升级过程的一大堆优化,其性能也比较慢,需要来回的从用户态到内核态之间切换,名副其实的一把重量级锁。
为了实现一把java层面的锁,并且提高原来的性能的情况下,在编程大师 Doug Lea 的努力下,通过java代码实现锁的AQS终于问世。AQS主要是参考了synchronized的底层实现,如AQS内部也使用了同步等待队列和条件等待队列等,也是通过cas的方式抢锁,但是在性能上,是超过这个synchronized的,并且支持手动的加锁和解锁,对java开发者相对而言更加友好。即使后面synchronized经过了一系列锁的升级和优化,其性能也不如AQS
AQS:AbstractQueuedSynchronizer ,顾名思义,可以叫做抽象队列同步器,是一个java语言层面的一个抽象类,java层面主要是通过这个AQS的这种方式实现管程的,主要是支持jdk5及以上版本
2,AQS抽象类的源码分析
由于这个AQS是通过java语言实现的,所以直接来分析他的底层源码即可
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizerimplements java.io.Serializable {...}
AQS是参考Synchronized的底层来实现的,因此可以直接根据Synchronized的底层思想来看这个AQS。首先Synchronized有两个重要的队列组成,一个是同步等待队列,一个是条件等待队列,那根据这两个队列,看看AQS底层是如何实现的
2.1,同步等待队列
同步等待队列在synchronized是通过这个cxq队列实现的,其内部采用的是栈结构,先进后出,所以并不能保证锁的公平性。当线程一进来抢锁时,抢到锁的线程继续往下执行,没抢到锁的线程进入这个同步等待队列
在这个AbstractQueuedSynchronizer
抽象类中,该类继承了一个父类AbstractOwnableSynchronizer
,在这个类中,有一个重要的属性exclusiveOwnerThread ,表示当前的独占锁的线程,因为这个队列是一个同步的队列,因此需要通过这个参数来记录是哪个线程抢到了锁。
如在ReentrantLock中,会进行一个尝试获取锁的状态,就会判断当前线程是否拿到了这把锁
if (Thread.currentThread() != getExclusiveOwnerThread())
在这个AbstractQueuedSynchronizer 抽象类中,里面有一个静态内部类Node,里面有如下参数
static final class Node {...
}
根据注释,很明显可以看出,AQS的同步等待队列的名称叫做CLH同步等待队列,其底层是通过一个双向链表的方式形成的,链表是通过各个Node结点组成,每个结点都有自己的生命周期,前驱指针和后继指针,以及一个结点所处的状态,如能否被唤醒状态等。和synchronized中的cxq队列不同的是,CLH采用的是先进先出的队列结构,cxq采用的是先进后出的栈结构
在该类中,也能看到内部的抢锁逻辑,是通过这个cas比较与交换来实现的,主要是判断当前结点的线程是否为同步监视器中 exclusiveOwnerThread 线程
CLH同步等待队列的大概图像如下,当然底层有着更加复杂的逻辑,下文会继续讲解。大概原理就是会有一个同步监视块(蓝色部分),里面会记录当前获取到锁的线程exclusive和一个state状态,state为0表示队列中的线程可以抢锁,队列是由各个node结点组成的双向链表,每个结点里面会有一个前驱指针和后继指针,以及一个waitStatus的状态,为-1时表示可唤醒状态。
2.2,条件等待队列
在synchronized中,当拿到锁的线程在调用wait方法之后,就会进入这个waitSet的条件等待队列中,在这个AQS中,也实现了这个条件等待队列的功能。在这个AbstractQueuedSynchronizer类中,有一个内部类ConditionObject类,如下,很明显这个条件等待队列是一个单向链表组成,并且里面也是由各个node结点组成,此时结点的waitState的值为-2.
public class ConditionObject implements Condition, java.io.Serializable {/** First node of condition queue. */private transient Node firstWaiter;/** Last node of condition queue. */private transient Node lastWaiter;...
}
synchronized主要是通过这个wait,notify和notyfyAll的方式来实现阻塞和唤醒功能,而在这个AQS中,是通过这个await,signal,signalAll 这三个方法实现
public final void await() throws InterruptedException {}
public final void signal() {}
public final void signalAll() {}
await方法的实现主要如下,内部会调用这个LockSupport.park进行阻塞,并且通过调用这个 addConditionWaiter 方法获取结点,可以发现这个结点时存放在链表的尾部的。在调用这个await方法之后,线程会释放资源,线程被sifnalAll唤醒的时候,会调用LockSupport.unpark的方式唤醒
public final void await() throws InterruptedException {...Node node = addConditionWaiter();while (!isOnSyncQueue(node)) {LockSupport.park(this); //阻塞if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)break;}
}
CLH同步等待队列和条件等待队列的关系如下,当拿到锁的线程调用await方法之后,此时结点会加入到条件等待队列的队尾里面,当某个结点被唤醒或者被全部唤醒的时候,会将条件等待队列的结点按顺序加入到同步等待队列的队尾,这样就实现了同步等待队列和条件等待队列之间的转换。(橙色部分表示CLH同步等待队列,绿色部分表示条件等待队列)
3,Node结点分析
上面了解了不管是条件等待队列还是同步等待队列,内部都是由node结点组成,接下来重点聊一下这个Node结点
public final class Node{static final Node SHARED = new Node(); //共享结点static final Node EXCLUSIVE = null; //独占结点static final int CANCELLED = 1; //异常状态static final int SIGNAL = -1; //可以被唤醒的状态static final int CONDITION = -2; //条件等待状态static final int PROPAGATE = -3; //传播volatile int waitStatus; //当前结点的生命状态,对应上面的1,-1,-2,-3volatile Node prev; //前驱指针volatile Node next; //后继指针volatile Thread thread; //当前线程Node nextWaiter; //下一个waiter
}
根据上面Node中的各个变量可知,Node可以分为共享结点和独占结点,每个结点有一个waitStatus的状态表示,主要有异常状态、可被唤醒状态、条件等待状态和可传播状态,每个结点可以有当前结点的生命状态,同时有一个前驱指针和后继指针,并有一个记录线程id的指针。
在阻塞队列中Node结点的waitStatus的值为-2,在信号量中Node结点的waitStatus值为-3
4,ReentrantLock底层源码分析
上面介绍了AQS的底层,接下来就可以分析一下这个耳熟能详的ReentrantLock了,依旧先分析一下他的底层源。这个类实现了Lock接口,在该接口中规范了如何加锁,解锁,尝试获取锁等
public class ReentrantLock implements Lock, java.io.Serializable {}
在这个ReentrantLock内部类中,有一个 Sync 的抽象的静态内部类,并且继承了这个AbstractQueuedSynchronizer 抽象队列同步器类,也就是说Sync已经具有该类的所有特性了。
abstract static class Sync extends AbstractQueuedSynchronizer{}
在这个Sync中类中,有两个静态的子类,分别是FairSync和NonfairSync类,分别代表着公平锁和非公平锁
static final class NonfairSync extends Sync {}
static final class FairSync extends Sync {}
在ReentrantLock类的构造方法中,默认的是一个非公平锁,减少阻塞,其效率相对较高
public ReentrantLock(boolean fair) sync = fair ? new FairSync() : new NonfairSync();
public ReentrantLock() sync = new NonfairSync();
4.1,NonfairSync
因为ReentrantLock默认采用的是非公平锁的,因此先研究这个 NonfairSync ,如下
ReentrantLock lock = new ReentrantLock();
lock.lock();
lock.unlock()
接下来研究这个lock方法,其内部实现如下,会先结果一个cas的比较与交换操作,随后将同步器中的Exclusive的值设置为当前线程的值。同步状态器为0时可以抢锁,为1时不能抢锁,在释放锁时又会设置成0。
final void lock() {//cas操作,比较与交换if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread());else acquire(1);
}
上面方法中的state状态和exclusive独占线程对应的就是下图中蓝色部分的同步监视器。
如果cas抢锁失败,那么就会调用这个acquire()方法,并传入参数1
public final void acquire(int arg) {if (!tryAcquire(arg) && //尝试加锁acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //加入队列中selfInterrupt(); //阻塞
}
4.1.1,线程尝试获取锁
尝试获取锁的代码如下,会判断同步状态器的state值以及判断是否为重入锁
//判断是否获取锁
final boolean nonfairTryAcquire(int acquires) {//获取当前获取锁的线程final Thread current = Thread.currentThread();//获取当前同步器状态,被volatile修饰的整型值,默认为0int c = getState();//如果同步状态器的值为0,说明外面的线程可以进行加锁操作if (c == 0) {//compareAndSetState:原子操作,比较与交换,进行加锁的操作,将state变量将0变为1//setExclusiveOwnerThread:设置获取锁的的线程拥有者if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}//如果状态同步器不为0,可能由自己持有,也可能由别的线程持有锁//重复加锁,如定义一个全局锁,出现了这个可重入锁的问题else if (current == getExclusiveOwnerThread()) {//重入一次就+1int nextc = c + acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");setState(nextc);return true;}//别的线程获有锁,直接返回return false;
}
4.1.2,双向链表的创建
如果尝试加锁失败,则会进入下一步操作,就是将这个Node加入队列中,即进入这个addWaiter方法中,其底层实现主要如下,首先会判断当前的队列是否为空,如果不为空,则将当前结点的前驱指针指向队列中的尾结点,随后通过cas的方式将当前结点作为尾结点,反之出现并发问题。如果队列中尾结点不存在,那么需要一个初始化队列和一个入队的操作,调用的是enq方法
//线程入队操作
private Node addWaiter(Node mode) {//获取当前线程的Node结点,此时的mode是一个独占的结点Node node = new Node(Thread.currentThread(), mode);// Try the fast path of enq; backup to full enq on failureNode pred = tail;if (pred != null) {//将当前结点的前驱指针指向队列中的尾结点node.prev = pred;//将当前结点做为队列的尾结点if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}//结点入队enq(node);return node;
}
4.1.3,Node结点入队
enq方法的底层实现如下,如果尾结点为空,就会通过自旋+cas的方式进行一个队列初始化的工作,然后队列不为空,此时大量线程依旧在自旋,则执行一个结点插入到队列的尾结点的操作。通过这个for循环,可以保证当前线程是一定可以入队成功的
private Node enq(final Node node) {for (;;) {Node t = tail;//如果尾结点为空if (t == null) { // Must initialize//给头结点定义一个新的结点,自旋+cas实现,实现队列的初始化if (compareAndSetHead(new Node()))//此时头结点和尾结点是同一个结点tail = head;} else {//当前结点的前驱指针指向尾结点node.prev = t;//通过比较与交换if (compareAndSetTail(t, node)) {t.next = node;return t;}}}
}
4.1.4,Node结点的阻塞
在线程入队之后,就需要堆线程进行阻塞操作,主要是通过acquireQueued 方法实现,其实现的底层逻辑如下,其逻辑和入队的逻辑一样,也是用了for自旋的方式来保证该结点一定会拿到锁。在拿到锁之前,就需要在队列中阻塞以及将当前结点修改成可被唤醒的状态
final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {//中断机制boolean interrupted = false;for (;;) { //for循环,保证可以一直拿到锁final Node p = node.predecessor(); //获取前驱结点,就是一直往前取,获取头结点if (p == head && tryAcquire(arg)) { //判断是不是头结点,尝试获取锁setHead(node); p.next = null; // help GCfailed = false;return interrupted;}if (shouldParkAfterFailedAcquire(p, node) && //如果获取锁失败,修改Node标志parkAndCheckInterrupt()) //阻塞interrupted = true;}} finally {if (failed) cancelAcquire(node);}
}
可以再研究一下以下这段代码,在这个shouldParkAfterFailedAcquire 方法中,将这个Node结点的waitStatus的状态修改成-1,即可被唤醒的状态
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {int ws = pred.waitStatus;if (ws == Node.SIGNAL)return true; //可被唤醒状态if (ws > 0) { //取消状态do {node.prev = pred = pred.prev; //将链表移除} while (pred.waitStatus > 0);pred.next = node;}else compareAndSetWaitStatus(pred, ws, Node.SIGNAL); //设置成可被唤醒状态return false;
}
在修改完状态之后,又会调用这个 parkAndCheckInterrupt 方法,接下来查看这个方法可以得知,在该方法中会调用 LockSupport.park()
这个方法实现阻塞,并且调用这个interupted实现线程中断
private final boolean parkAndCheckInterrupt() {LockSupport.park(this);return Thread.interrupted();
}
4.1.5,同步监视器修改状态,释放资源和释放锁
在线程被阻塞之后,需要通过调用unlock方法对线程进行解锁操作,接下来看看底层是如何实现的。
public void unlock() {sync.release(1);
}
接下来继续往下看这个release 方法,首先会进行一个tryRelease尝试释放锁的方法
public final boolean release(int arg) {if (tryRelease(arg)) { //尝试释放锁Node h = head; if (h != null && h.waitStatus != 0) //判断当前链表的头结点是否存在unparkSuccessor(h); return true;}return false;
}
接下来查看这个tryRelease 的底层实现,如下由于此时已经有线程拿到了锁,因此此时同步监视器中的state状态为1(没拿到为0),因此这个c的值为0,此时会将将同步监视器中的Exclusive的线程设置为null,如下图看图理解好一点,这样的话就是一步释放锁的操作,此时state状态为0,其他线程也可以来抢锁
protected final boolean tryRelease(int releases) {int c = getState() - releases; // 0 = 1-1if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;if (c == 0) {free = true; setExclusiveOwnerThread(null); //将exclusive的的线程清空}setState(c); //修改同步监视器状态,将状态0返回return free;
}
4.1.6,唤醒链表中的下一个结点
接下来进入release方法中的这个unparkSuccessor 方法中,查看其底层逻辑如下:首先会将当前结点的状态从-1可被唤醒状态改成0默认状态,由于队列中的线程都被阻塞,因此需要唤醒线程来抢锁,所以将当前结点的下一个结点给唤醒,通过调用:LockSupport.unpark 方法实现线程的唤醒
private void unparkSuccessor(Node node) {int ws = node.waitStatus; //获取当前释放锁的node结点状态if (ws < 0) compareAndSetWaitStatus(node, ws, 0); //将状态从可被唤醒状态修改成默认状态 Node s = node.next; //获取下一个要加锁的结点if (s == null || s.waitStatus > 0) { s = null;for (Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}if (s != null) LockSupport.unpark(s.thread); //唤醒下一个结点
}
在3.1.4的acquireQueued方法中,有一个 setHead() 方法,就是会将当前当前结点作为头结点,当前结点的前驱指针指向null,这样就可以实现上一个结点出队的逻辑
private void setHead(Node node) {head = node;node.thread = null;node.prev = null;
}
4.2,FairSync
fairSync表示的是公平锁,其底层逻辑实现和非公平锁的步骤几乎一模一样,就是在入队之前,非公平锁可以直接进行一次cas抢锁的机会,但是公平锁需要判断队列中是否有数据,有就需要排队的操作。因此在这不做过多的解释,理解了非公平锁自然就能理解公平锁的底层实现了。
4.3,ReentrantLock底层实现总结
ReentrantLock是Lock的一个实现类,在该类内部定义了一个继承AQS的Sync内部类,该类有两个子类,分别是实现非公平锁的NonfairSync和实现公平锁的FairSync,默认使用的是非公平锁。以非公平锁为例,如下:
调用lock方法进行加锁: 首先会进行一个cas的抢锁逻辑,如果抢锁失败,则会加入到CLH同步等待队列,加入队列之前,会判断该队列是否已经创建,如果没有创建则创建,该队列是由Node结点组成的双向链表,如果队列已经创建,则将该线程当做一个Node结点对象加入到队列的尾部,此时通过调用LockSupport.park的方法将Node结点阻塞,并修改状态为-1,即可被唤醒状态。
调用unlock方法进行解锁: 内部首先会先释放同步监视器资源,并修改state的状态为0,这样可以保证锁没有被抢占,其次是将当前结点的下一个结点通过调用LockSupport.unpark的方式唤醒,这样就会有线程去获取锁,最后就是将被释放锁的结点移除,下一个获取锁的结点作为头结点。
5,ReentrantLock的特性
通过上面的源码可知,ReentrantLock具有一下特性
- 阻塞等待队列:CLH同步等待队列,每个结点在队列中处于阻塞状态
- 共享/独占:每个Node结点分为独占模式和共享模式
- 公平/非公平:通过fair和unfair两种方式实现公平锁和非公平锁
- 可重入:在尝试获取锁时会对锁是否重复加进行判断和累加
- 允许中断 :在调用LockSupport.park方法之后,会对当前线程中断
6,ReentrantLock和Synchronized联系
- ReentrantLock是通过java语言层次实现的锁;Synchronized是通过jvm层次实现的
- ReentrantLock采用的是队列(FIFO)方式实现的同步队列,因此可以实现公平锁和非公平锁锁;Synchronized是通过栈(LIFO)的方式实现的同步队列,因此是一把非公平锁
- ReentrantLock可以在代码中获取锁的状态,Synchronized不行
- ReentrantLock可以在代码中实现中断,Synchronized不行
- ReentrantLock需要手动的加锁解锁,Synchronized不需要
- ReentrantLock可以保证先进来的线程先执行(队列),Synchronized可以保证后进来的线程先执行(栈)
- ReentrantLock是通过CLH队列(队列FIFO)的方式实现的同步等待队列;Synchronized是通过CXQ(栈LIFO)的方式实现的同步等待队列
- ReentrantLock和Synchronized都是支持可重入锁
如有转载,请标明出处:https://zhenghuisheng.blog.csdn.net/article/details/132857564