接着上一篇的
看一下ReentrantLock的源码,这里只是从AQS的角度出发,并不是从Lock的角度来看,那个以后再分析把
从AQS的状态角度,代码整体结构上是这样的:
//检查状态
while(!checkAndChangeState){
enque(currentthread) //将当前线程加入等待队列
park(currentThread) //挂起当前线程
}
//do sth
…….
//释放锁,恢复状态
changeState(){
Dequeue(current) //出对
Unpark(queue.thread)//唤醒其他等待中的线程
}
和真正的代码比起来主要是一些小地方的优化,比如一些自旋操作,特别是对于很小范围内的锁,另外,就是队列中的线程可能是已经取消的,这也要做相应的处理,下面就具体分析下
Lock的结构体系成对的主要有lock和unlock,其他方法到时候再看把:
我们看下lock方法:
public void lock() {
sync.lock();
}
//先暂且不管公平锁还是非公平锁,从独占锁的角度来说,就是请求一个状态
final void lock() {
acquire(1);
}
看下acquire的代码,完全就是上面伪代码的结构:
public final void acquire(int arg) {
//判断状态是否ok(自己实现),不ok的话就加入队列,并且阻塞(父类实现)
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
再来看下状态判断,这个是需要每个synchronizer自己做的:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//这里初看起来会有并发问题,但是下面的cas操作保证了不会有并发问题,并且state字段是volatile的
if (c == 0) {
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");
//重入锁,单个线程内,不会有并发性的,所以直接set就好
setState(nextc);
return true;
}
return false;
}
这个方法如果返回true的话,代表条件允许,这样线程也就获得到锁了,其他啥也不用做了,等着释放锁,但是如果返回false,表示其他线程占用着,就需要加入等待队列,并且阻塞线程:
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
先看addWaiter:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 这里假设队列非空,并且没有太多的并发性,尝试快速入队
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//队列为空,或者存在高并发,入队的时候失败了,需要用while尝试入队
enq(node);
return node;
}
// 一个完整的入队操作,需要在一个循环里面判断队列是否为空、高并发等情况
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // 队列为空,需要初始化
Node h = new Node(); // Dummy header
h.next = node;
node.prev = h;
if (compareAndSetHead(h)) {//设置队列头,原子操作
tail = node;
return h;
}
}
else {// 队列非空,直接插到最后面
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
//在入队之后,在看下真正阻塞线程的acquireQueued,这里是一个循环操作,唤醒的时候也是重新去竞争,不是立刻获得锁
final boolean acquireQueued(final Node node, int arg) {
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
//在真正阻塞线程之前,如果发现自己前面的节点是head,那还要再尝试下去获取锁,获取到以后就把head踢了,自己当head,因为head的意义表示正在占用锁的节点(某些同步范围很短的锁很有用)
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return interrupted;
}
//该方法判断是否需要阻塞当前线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) //park当前线程
interrupted = true; //默认不处理interrupted
}
} catch (RuntimeException ex) {
cancelAcquire(node); //发送异常的情况下,取消当前节点,并且如果当前节点是head,或者当前节点是等待唤醒状态,那么,还要尝试唤醒后面的节点
throw ex;
}
}
//决定是否需要阻塞当前线程
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int s = pred.waitStatus;
//如果当前节点的前一个节点的状态小于0(signal/condition),表示前面那个节点也在等待唤醒,果断把自己挂起
if (s < 0)
return true;
//如果前面节点的状态大于0,(cancelled),表示前面的线程已经取消,向前遍历,直到找到状态不大于0的节点
if (s > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else
/*
* 前面一个节点的状态为0,那么就是前一个节点认为他后面没有节点需要唤醒 * 啦,这时候要果断把他的状态改为SIGNAL,因为SIGNAL状态表示后面有阻塞
线程需要唤醒.
*/
compareAndSetWaitStatus(pred, 0, Node.SIGNAL);
/**
在state>=0的情况下,会走到这里,这里不能返回true把自己挂起,
因为这时候线程切换,占用锁的线程A已经结束(并且发出了unpark信号),如果这时候线程B直接返回true阻塞自己,那可能会因为错失信号B永远无法唤醒;返回false,当前线程会去再次尝试获取锁,如果还是不能获取,则阻塞;
*/
return false;
}
//挂起当前线程,因为park会相应中断,但是不是抛出异常,因此这里将是否中断作为boolean类型返回,交给外部代码处理
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
//并不是只有unpark和interrupt才能唤醒他,参考前面的关于LockSupport的讲解;唤醒的线程,重新去竞争锁,在高并发的情况下,有可能另一个节点正好也在请求lock,那么他刚好tryAcquire成功了,则当前线程又会重新阻塞!
}
释放锁
/**
* 释放锁主要做下面两件事情:
* 1.修改状态,改为可以获取锁
* 2.如果有等待的线程,则唤醒一个
*/
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
//如果waitStatus为0,则表示后面没阻塞线程了,没必要进行唤醒了
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
//先看修改状态的方法,这里只修改锁的状态,不修改队列的任何东西
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
//进行唤醒操作
private void unparkSuccessor(Node node) {
/*
* 把自己状态改为0,表示后面没有节点需要唤醒;如果后面有需要唤醒的节点,在请求锁的时候会把他的状态改为SIGNAL,不是很明白为什么这么做,难道是一种小小的优化?
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
//取队列里面状态不大于0(cancelled)的节点,如果大于0,则从队列移除,然后从后往前找,找到最前面的一个非cancelled节点,并且唤醒这个节点
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);
}
总结
1.通过AQS的state字段来表示锁是否被占用,特别需要注意的就是他的操作都是原子的
2.通过队列来保存阻塞的线程
3.通过LockSupport来进行挂起和唤醒操作(LockSupport的注意点可以看上一篇)
从挂起的时机上,AQS加了一些自旋操作,不是每次发现不能获取锁就挂起,而是后来会再次尝试获取锁,这样,对那些同步范围非常小,时间非常短的锁,应该是一种性能的提高。
另外,对于队列的操作,从我个人的角度来看,每次取当前正在跑的节点,跑完以后再取下一个节点,如果是取消状态,那么再去下一个,最后找到一个ok的唤醒,这样就可以了。不过AQS增加了一些状态判断,比如通过waitStatus来判断后面是否有节点需要唤醒,从角度来看,这完全增加了成本,不过性能提高多少就难说喽!
相关推荐
### 三、ReentrantLock源码分析 #### 3.1 ReentrantLock介绍 ReentrantLock是一种基于AQS框架实现的可重入锁。它可以显式地控制锁的获取和释放,并提供了公平性和非公平性两种获取策略。此外,ReentrantLock还支持...
### ReentrantLock源码分析 #### 一、ReentrantLock简介 ReentrantLock是一个基于`AbstractQueuedSynchronizer`(AQS)实现的高级锁工具类。与传统的synchronized关键字相比,ReentrantLock提供了更多控制手段,比如...
接下来,我们来具体分析一下AQS的源码。AQS中定义了一个名为state的volatile变量,用于表示同步状态。这个变量有三种操作方法:getstate()、setstate()和compareAndSetState(),分别用于获取、设置和原子性地更新...
Java并发系列之ReentrantLock源码分析 ReentrantLock是Java 5.0中引入的一种新的加锁机制,它实现了Lock接口,并提供了与synchronized相同的互斥性和内存可见性。ReentrantLock的底层实现是通过AQS来实现多线程同步...
**源码分析** ReentrantLock类实现了Lock接口,提供了lock()、unlock()等方法。Sync类是内部抽象类,继承自AQS,它有两个子类NonfairSync和FairSync。Sync类中有lock()抽象方法,以及nonfairTryAcquire()和...
7. **源码分析** 在深入源码之前,我们需要对获取锁的整体流程有一个大致的理解。ReentrantLock的`lock()`方法调用`Sync`的`lock()`,然后由`NonfairSync`或`FairSync`完成具体的锁获取逻辑。这个过程中涉及到的...
维护资源状态的可用性最后,文档提供了AQS源码的初步分析,突出了其设计和实现的关键部分,如等待队列节点类Node的定义综合来看,文章为Java开发者提供了对AQS及其在ReentrantLock中应用的详细理解,是探索Java并发...
本文将详细分析AQS的源码,探讨其工作机制,以及在Java中如何实现不同类型的锁。 首先,我们需要了解锁的基本类型。在Java中,锁主要分为两类:悲观锁和乐观锁。悲观锁认为并发操作会导致数据不一致,因此在操作...
Java并发包源码分析(JDK1.8):囊括了java.util.concurrent包中大部分类的源码分析,其中涉及automic包,locks包(AbstractQueuedSynchronizer、ReentrantLock、ReentrantReadWriteLock、LockSupport等),queue...
ReentrantLock Lock 加锁过程源码分析图,AQS 源码分析
09-深入理解AQS之独占锁ReentrantLock源码分析-fox 10-深入理解AQS之Semaphorer&CountDownLatch&CyclicBarrie详解-fox 11-深入理解AQS之CyclicBarrier&ReentrantReadWriteLock详解-fox 12-深入理解AQS之...
通过以上分析,我们可以看到Java并发包提供了丰富的并发锁机制,从简单的synchronized关键字到复杂的AQS、LockSupport、Condition等,这些都是为了解决并发编程中的互斥和同步问题。在实际应用中,开发者需要根据...
Java并发结合源码分析AQS原理 Java并发编程中,AQS(AbstractQueuedSynchronizer)是一个核心组件,它提供了一个基于FIFO队列和状态变量的基础框架,用于构建锁和其他同步装置。在这篇文章中,我们将深入探讨AQS的...
在本篇中,我们将深入分析AQS的条件队列,它是实现高级同步机制如`ReentrantLock`和`CountDownLatch`的关键部分。 条件队列是AQS中与`Condition`接口相关的部分,它允许线程在满足特定条件时等待,而不是简单地阻塞...
本文将通过图像解析和源码分析,深入探讨AQS的工作机制。 一、AQS基本结构与原理 AQS的核心是一个int类型的state字段,它表示资源的状态。当state为0时,表示资源可获取;非0则表示已被占用。AQS维护了一个FIFO的...
源码分析中,我们可以看到`Condition`的`await()`方法会使得当前线程释放锁、加入条件队列并进入等待状态;而`signal()`方法会唤醒条件队列的第一个线程,使其有机会重新竞争锁。这些操作都确保了线程安全性和正确性...
在本文中,我们将深入分析`ReentrantLock`的`lock()`方法,理解其内部机制,包括锁的获取、释放以及公平性和非公平性的实现。 首先,`ReentrantLock`的`lock()`方法很简单,它只是调用了内部类`Sync`的`lock()`方法...
通过对ReentrantLock的源码分析,我们可以更好地理解ReentrantLock的实现机制和使用场景,从而更好地应用于实际开发中。 知识点: 1. ReentrantLock是一个可重入锁,它和synchronized的方法和代码有着相同的行为和...
带你看看Javad的锁-ReentrantLock前言ReentrantLock简介Synchronized对比用法源码分析代码结构方法分析SyncNonfairSyncFairSync非公平锁VS公平锁什么是公平非公平ReentrantLockReentrantLock的构造函数lock加锁方法...