J,tomcat服务器面试题

当然这里代码的作用我是提前研究过的,对于大家肯定不是很清楚,我们继续里面去看,最后大家可以回到这儿再论证。

tryAcquire(int)

再次尝试抢占锁

protected final boolean tryAcquire(int acquires) {

return nonfairTryAcquire(acquires);

}

final boolean nonfairTryAcquire(int acquires) {

final Thread current = Thread.currentThread();

int c = getState();

//再次尝试抢占锁

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”);

setState(nextc);

return true;

}

// false 表示抢占失败

return false;

}

addWaiter

将阻塞的线程添加到双向链表的结尾

private Node addWaiter(Node mode) {

//以给定模式构造结点。mode有两种:EXCLUSIVE(独占)和SHARED(共享)

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;

}

}

//上一步失败则通过enq入队。

enq(node);

return node;

}

enq(Node)

private Node enq(final Node node) {

//CAS"自旋",直到成功加入队尾

for (;😉 {

Node t = tail;

if (t == null) { // 队列为空,创建一个空的标志结点作为head结点,并将tail也指向它。

if (compareAndSetHead(new Node()))

tail = head;

} else {//正常流程,放入队尾

node.prev = t;

if (compareAndSetTail(t, node)) {

t.next = node;

return t;

}

}

}

}

第一个if语句

在这里插入图片描述

else语句

在这里插入图片描述

线程3进来会执行如下代码

在这里插入图片描述

那么效果图

在这里插入图片描述

acquireQueued(Node, int)

OK,通过tryAcquire()和addWaiter(),该线程获取资源失败,已经被放入等待队列尾部了。聪明的你立刻应该能想到该线程下一部该干什么了吧:进入等待状态休息,直到其他线程彻底释放资源后唤醒自己,自己再拿到资源,然后就可以去干自己想干的事了。没错,就是这样!是不是跟医院排队拿号有点相似~~acquireQueued()就是干这件事:在等待队列中排队拿号(中间没其它事干可以休息),直到拿到号后再返回。这个函数非常关键,还是上源码吧:

final boolean acquireQueued(final Node node, int arg) {

boolean failed = true;//标记是否成功拿到资源

try {

boolean interrupted = false;//标记等待过程中是否被中断过

//又是一个“自旋”!

for (;😉 {

final Node p = node.predecessor();//拿到前驱

//如果前驱是head,即该结点已成老二,那么便有资格去尝试获取资源(可能是老大释放完资源唤醒自己的,当然也可能被interrupt了)。

if (p == head && tryAcquire(arg)) {

setHead(node);//拿到资源后,将head指向该结点。所以head所指的标杆结点,就是当前获取到资源的那个结点或null。

p.next = null; // setHead中node.prev已置为null,此处再将head.next置为null,就是为了方便GC回收以前的head结点。也就意味着之前拿完资源的结点出队了!

failed = false; // 成功获取资源

return interrupted;//返回等待过程中是否被中断过

}

//如果自己可以休息了,就通过park()进入waiting状态,直到被unpark()。如果不可中断的情况下被中断了,那么会从park()中醒过来,发现拿不到资源,从而继续进入park()等待。

if (shouldParkAfterFailedAcquire(p, node) &&

parkAndCheckInterrupt())

interrupted = true;//如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true

}

} finally {

if (failed) // 如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了),那么取消结点在队列中的等待。

cancelAcquire(node);

}

}

到这里了,我们先不急着总结acquireQueued()的函数流程,先看看shouldParkAfterFailedAcquire()和parkAndCheckInterrupt()具体干些什么。

shouldParkAfterFailedAcquire(Node, Node)

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {

int ws = pred.waitStatus;//拿到前驱的状态

if (ws == Node.SIGNAL)

//如果已经告诉前驱拿完号后通知自己一下,那就可以安心休息了

return true;

if (ws > 0) {

/*

  • 如果前驱放弃了,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后边。

  • 注意:那些放弃的结点,由于被自己“加塞”到它们前边,它们相当于形成一个无引用链,稍后就会被保安大叔赶走了(GC回收)!

*/

do {

node.prev = pred = pred.prev;

} while (pred.waitStatus > 0);

pred.next = node;

} else {

//如果前驱正常,那就把前驱的状态设置成SIGNAL,告诉它拿完号后通知自己一下。有可能失败,人家说不定刚刚释放完呢!

compareAndSetWaitStatus(pred, ws, Node.SIGNAL);

}

return false;

}

整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心去休息,需要去找个安心的休息点,同时可以再尝试下看有没有机会轮到自己拿号。

parkAndCheckInterrupt()

如果线程找好安全休息点后,那就可以安心去休息了。此方法就是让线程去休息,真正进入等待状态。

private final boolean parkAndCheckInterrupt() {

LockSupport.park(this);//调用park()使线程进入waiting状态

return Thread.interrupted();//如果被唤醒,查看自己是不是被中断的。

}

好了,我们可以小结下了。

看了shouldParkAfterFailedAcquire()和parkAndCheckInterrupt(),现在让我们再回到acquireQueued(),总结下该函数的具体流程:

  1. 结点进入队尾后,检查状态,找到安全休息点;

  2. 调用park()进入waiting状态,等待unpark()或interrupt()唤醒自己;

  3. 被唤醒后,看自己是不是有资格能拿到号。如果拿到,head指向当前结点,并返回从入队到拿到号的整个过程中是否被中断过;如果没拿到,继续流程1。

最后我们再回到前面的acquire方法来总结下

public final void acquire(int arg) {

if (!tryAcquire(arg) &&

acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

selfInterrupt();

}

总结下它的流程吧

  1. 调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;

  2. 没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;

  3. acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。

  4. 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。

在这里插入图片描述

Lock.unlock()


好了,lock方法看完后,我们再来看下unlock方法

release(int)

它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。这也正是unlock()的语义,当然不仅仅只限于unlock()

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(int)

此方法尝试去释放指定量的资源。下面是tryRelease()的源码:

public final boolean release(int arg) {

if (tryRelease(arg)) {//这里是先尝试释放一下资源,一般都可以释放成功,除了多次重入但只释放一次的情况。

Node h = head;

//这里判断的是 阻塞队列是否还存在和head节点是否是tail节点,因为之前说过,队列的尾节点的waitStatus是为0的

if (h != null && h.waitStatus != 0)

//到这里就说明head节点已经释放成功啦,就先去叫醒后面的直接节点去抢资源吧

unparkSuccessor(h);

return true;

}

return false;

}

private void unparkSuccessor(Node node) {

//这里,node一般为当前线程所在的结点。

int ws = node.waitStatus;

if (ws < 0)//置零当前线程所在的结点状态,允许失败。

compareAndSetWaitStatus(node, ws, 0);

Node s = node.next;//找到下一个需要唤醒的结点s

if (s == null || s.waitStatus > 0) {//如果为空或已取消

s = null;

for (Node t = tail; t != null && t != node; t = t.prev) // 从后向前找。

if (t.waitStatus <= 0)//从这里可以看出,<=0的结点,都是还有效的结点。

s = t;

}

if (s != null)

LockSupport.unpark(s.thread);//唤醒

}

这个函数并不复杂。一句话概括:用unpark()唤醒等待队列中最前边的那个未放弃线程,这里我们也用s来表示吧。此时,再和acquireQueued()联系起来,s被唤醒后,进入if (p == head && tryAcquire(arg))的判断(即使p!=head也没关系,它会再进入shouldParkAfterFailedAcquire()寻找一个安全点。这里既然s已经是等待队列中最前边的那个未放弃线程了,那么通过shouldParkAfterFailedAcquire()的调整,s也必然会跑到head的next结点,下一次自旋p==head就成立啦),然后s把自己设置成head标杆结点,表示自己已经获取到资源了,acquire()也返回了

好了,到这我们就因为把源码看完了,再回头来看下这张图

在这里插入图片描述

是不是就清楚了AQS到底是怎么实现的我们上面的猜想的了吧。那么对应的下课后让你自己去看

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
img

笔者福利

以下是小编自己针对马上即将到来的金九银十准备的一套“面试宝典”,不管是技术还是HR的问题都有针对性的回答。

有了这个,面试踩雷?不存在的!

回馈粉丝,诚意满满!!!




]
[外链图片转存中…(img-BVTtVd50-1711759377298)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-5FMWUaaW-1711759377298)]

笔者福利

以下是小编自己针对马上即将到来的金九银十准备的一套“面试宝典”,不管是技术还是HR的问题都有针对性的回答。

有了这个,面试踩雷?不存在的!

回馈粉丝,诚意满满!!!

[外链图片转存中…(img-QyCPaCqV-1711759377298)]
[外链图片转存中…(img-3zCJ1SyG-1711759377299)]
[外链图片转存中…(img-WkMW80hD-1711759377299)]
[外链图片转存中…(img-eDiYWa9y-1711759377300)]

  • 16
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是Tomcat调优面试题的答案: 1. Tomcat的缺省端口是多少,怎么修改? Tomcat的缺省端口是8080,可以通过修改server.xml文件中的Connector节点来修改端口号。 2. tomcat 有哪几种Connector 运行模式(优化)? Tomcat有三种Connector运行模式:BIO、NIO和APR。其中,BIO是传统的阻塞式I/O模式,NIO是非阻塞式I/O模式,APR是使用本地库实现的高性能模式。 3. Tomcat有几种部署方式? Tomcat有两种部署方式:WAR包部署和目录部署。WAR包部署是将应用程序打包成WAR包,然后将WAR包放到Tomcat的webapps目录下;目录部署是将应用程序解压到Tomcat的webapps目录下。 4. tomcat容器是如何创建servlet类实例?用到了什么原理? Tomcat容器在启动时会扫描web.xml文件,将其中的servlet类加载到内存中,并创建servlet实例。Tomcat使用Java反射机制来创建servlet实例。 5. tomcat 如何优化? Tomcat的优化可以从多个方面入手,包括内存调优、垃圾回收策略调优、共享session处理、添加JMS远程监控、使用专业的分析工具等。 6. 内存调优 可以通过调整JVM的内存参数来进行内存调优,例如-Xms和-Xmx参数可以分别设置JVM的初始内存和最大内存。 7. 垃圾回收策略调优 可以通过调整JVM的垃圾回收策略来进行垃圾回收策略调优,例如使用CMS垃圾回收器或G1垃圾回收器。 8. 共享session处理 可以使用集中式session管理方案,例如使用Redis等缓存服务器来存储session数据,从而实现session共享。 9. 添加JMS远程监控 可以使用JMX来进行远程监控,例如使用JConsole等工具来监控Tomcat的运行状态。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值