题外话
不管怎么样,不论是什么样的大小面试,要想不被面试官虐的不要不要的,只有刷爆面试题题做好全面的准备,当然除了这个还需要在平时把自己的基础打扎实,这样不论面试官怎么样一个知识点里往死里凿,你也能应付如流啊
这里我为大家准备了一些我工作以来以及参与过的大大小小的面试收集总结出来的一套进阶学习的视频及面试专题资料包,主要还是希望大家在如今大环境不好的情况下面试能够顺利一点,希望可以帮助到大家~
欢迎评论区讨论。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
- sync queue相关的属性
//thread属性为null
private transient volatile Node head;
private transient volatile Node tail; // 队尾,新入队的节点
- Node相关属性
// 节点所代表的线程
volatile Thread thread;
// 双向链表,每个节点需要保存自己的前驱节点和后继节点的引用
volatile Node prev;
volatile Node next;
// 线程所处的等待锁的状态,初始化时,该值为0
volatile int waitStatus;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
ReentrantLock
有公平锁和非公平锁两种实现,默认实现非公平锁。但是可配置为公平锁:
ReentrantLock lock=new ReentrantLock(true);
调用公平锁加锁逻辑:
final void lock() {
//开始加锁,将state修改为1
acquire(1);
}
真正的加锁方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
2.1 加锁的逻辑方法
只执行上述方法便可完成整个的加锁逻辑。而该方法中又包含下列四个方法的调用:
1. tryAcquire(arg)
该方法由继承AQS的子类实现,为获取锁的具体逻辑;
2. addWaiter(Node.EXCLUSIVE)
该方法由AQS实现,负责在获取锁失败后调用,将当前请求锁的线程包装成Node并且放到等待队列中
,并返回该Node。
3. acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
该方法由AQS实现。针对上面加入到队列的Node不断尝试两种操作之一:
-
若前驱节点是head节点的时候,尝试获取锁;
-
调用
park
将当前线程挂起,线程阻塞。
4. selfInterrupt
该方法由AQS实现。恢复用户行为。
-
用户在外界调用
t1.interrupt()
进行中断。 -
线程在
parkAndCheckInterrupt
方法被唤醒之后。会调用Thread.interrupted();
判断线程的中断标识,而该方法调用完毕会清除中断标识位。 -
而AQS为了不改变用户标识。再次调用
selfInterrupt
恢复用户行为。
2.2 如何构建等待队列——addWaiter
我们使用ReentrantLock
独占锁时,等待队列是延迟加载
的。也就是说若是线程交替执行,那么借助信号量(状态)
来保证。若是线程并发执行,就需要将阻塞线程放入到队列中。
//注意这个方法可能存在并发问题,mode为null(独占锁)。
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;
}
}
//队列不存在&&上面CAS失败的线程会进入enq方法自旋
enq(node);
return node;
}
队列不存在的情况
注意,该方法处理CAS
操作是原子性的,其他操作都存在并发冲突问题。
private Node enq(final Node node) {
for (;😉 {
Node t = tail;
//初始化阻塞队列
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
//自旋处理addWaiter中CAS加锁失败的线程
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
该方法采用自旋+CAS。CAS是保证同一时刻只有一个线程能成功改变引用的指向。
根据上面的流程图,sync queue的创建过程。head节点是new Node()
产生的,即其中的属性为默认值。也就是thread属性为null。也就是说正在执行的线程也会在sync queue
中占据头节点,但是节点中不会保存线程信息。
尾分叉问题:
上面已经说了,该方法是线程不安全的。
//步骤1:可能多个节点的prev指针都指向尾结点,导致尾分叉
node.prev = t;
//步骤2:但同一时刻,tail引用只会执行一个node。
if (compareAndSetTail(t, node)) {
//步骤3:现在环境是线程安全,旧尾结点的后继指针指向新尾结点。
t.next = node;
return t;
}
执行完步骤2
,但步骤3
还未执行时,恰好有线程从头节点开始往后遍历。**此时(旧)尾结点中的next域还为null。**它是遍历不到新加进来的尾结点的。这显然是不合理的。
但此时步骤1
是执行成功的,所以若是tail节点往前遍历,实际上是可以遍历到所有节点的,这也是为什么在AQS源码中,有时候常常会出现从尾结点开始逆向遍历链表的情况。
那些“分叉”的节点,肯定会入队失败。那么继续自旋,等待所有的线程节点全部入队成功。
2.3 尝试获取锁——tryAcquire
根据标志位state,来判断锁是否被占用。此时可能锁未被占用,由于是公平锁,于是会去判断sync queue
中是否有人在排队。
protected final boolean tryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取Lock对象的上锁情况,0-表示无线程持有;1-表示被线程持有;大于1-表示锁被重入
int c = getState();
//若此刻无人占有锁
if (c == 0) {
if (!hasQueuedPredecessors() && //判断队列中是否有前辈。若返回false代表没有,开始尝试加锁
compareAndSetState(0, acquires)) { //此刻队列中没有存在前辈,尝试加锁
setExclusiveOwnerThread(current); //将当前线程修改为持有锁的线程(后续判断可重入)
return true;
}
}
//若是当前线程是持有锁的线程
else if (current == getExclusiveOwnerThread()) {
//当前状态+1
int nextc = c + acquires;
if (nextc < 0)
throw new Error(“Maximum lock count exceeded”);
setState(nextc);
return true;
}
//否则,代表加锁失败
return false;
}
下面的方法返回false才会尝试加锁(该方法不具有原子性,可能会放行多个线程)。
//该方法不具有原子性,可能多个线程都觉得自己不需要排队,最终还是依靠外面
//条件上的CAS来保持其原子性。
public final boolean hasQueuedPredecessors() {
Node t = tail; //尾节点
Node h = head; //头节点
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
上述方法是判断队列中是否存在元素
。可能存在以下几种情况:
-
此时未维护队列【h和t指向null】,
h!=t
返回false,即无人排队; -
此时队列只有头节点(哑结点)【h和t都指向哑结点】,
h!=t
返回false,即无人排队; -
此时队列中存在2个以上的节点。若线程是头结点的后继节点线程(即处理正在办理业务的线程,进来的线程是第一个排队的线程)。那么
s.thread != Thread.currentThread()
返回false,即可是尝试加锁。 -
队列存在2个以上节点,且进来的线程不是第一个排队的线程,那么该线程需要乖乖的排队。
当然该方法不是并发安全的方法,即可能存在多个线程觉得自己无需排队,最终还是依靠CAS
来争夺锁。
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
//线程安全
setExclusiveOwnerThread(current);
return true;
}
同一时刻,只有一个线程可以成功改变state的状态。记录该线程为独占锁线程,一般后续可以重入。
没成功获取锁那么会调用2.2 中的方法,将该线程加入到阻塞队列中
。
2.3. 阻塞线程——acquireQueued
-
若执行到该方法,说明
addWaiter
方法已经成功将该线程包装为Node节点放到了队尾。 -
在该方法中依旧尝试获取锁;
-
再次获取锁失败后,会将其阻塞;
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;😉 {
//获取node的前驱节点
final Node p = node.predecessor();
//若前驱节点在办理业务,那么它将再次获取一次锁。
if (p == head && tryAcquire(arg)) {
//获取锁成功,此处便是线程安全。
//将自己设置为头节点,并将自己设置为哑节点
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//获取锁失败,将自己挂起。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
最后我还整理了很多Android中高级的PDF技术文档。以及一些大厂面试真题解析文档。
Android高级架构师之路很漫长,一起共勉吧!
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
turn interrupted;
}
//获取锁失败,将自己挂起。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
最后我还整理了很多Android中高级的PDF技术文档。以及一些大厂面试真题解析文档。
[外链图片转存中…(img-Bm0Z1cIu-1715828475338)]
Android高级架构师之路很漫长,一起共勉吧!
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!