• 打卡 Day 28,贵在坚持,要学的还有很多,有限的时间,尽可能多学一些总没坏处!
    【Java实习生】每日10道面试题打卡!_面试

1、满二叉树、完全二叉树、平衡二叉树、红黑树、二叉搜索树的区别?



① 满二叉树

高度为 ​​h​​​,由 ​​2^h-1​​个节点构成的二叉树称为满二叉树。

【Java实习生】每日10道面试题打卡!_原力计划_02

② 完全二叉树

完全二叉树是由满二叉树而引出来的,若二叉树的深度为 ​​h​​​,除第 ​​h​​​ 层外,其它各层 (1~h-1) 的结点数都达到最大个数(即1~h-1层为一个满二叉树),第 ​​h​​ 层所有的结点都连续集中在最左边,这就是完全二叉树。

堆一般都是用完全二叉树来实现的。

【Java实习生】每日10道面试题打卡!_面试_03

③ 二叉查找树

二叉查找树是二叉树中最常用的一种类型,是为了实现快速查找的,不仅仅支持快速查找一个数,还支持快速插入和删除数据。二叉查找树的这些性能都依赖于二叉查找树的特殊结构,二叉查找树的要求,在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都要大于这个节点的值。

④ 红黑树

红黑树的优势在于它是一棵平衡二叉查找树,对于普通的二叉查找树(非平衡二叉查找树)在极端情况下可能会退化为链表的结构,例如,当我们依次插入 3、4、5、6、7、8 这些数据时,二叉树会退化为如下链表结构:

【Java实习生】每日10道面试题打卡!_原力计划_04

红黑树是一种弱平衡二叉树(只有黑色节点完美平衡,红色节点不一定平衡),是特殊的二叉查找树(平衡二叉查找树)。

⑤ 平衡二叉树

AVL树是带有平衡条件的二叉查找树,一般是用平衡因子差值判断是否平衡并通过旋转来实现平衡,左右子树树高不超过 1,和红黑树相比,AVL树是严格的平衡二叉树,平衡条件必须满足(所有节点的左右子树高度差的绝对值不超过1)。

不管我们是执行插入还是删除操作,只要不满足上面的条件,就要通过旋转来保持平衡,而旋转是非常耗时的,由此我们可以知道 AVL 树适合用于插入与删除次数比较少,但查找多的情况。


2、IP地址分类

ABCDE五类,A、B、C为基本类,D、E作为多播和保留使用,为特殊地址。


  • A类地址:以0开头
  • B类地址:以10开头
  • C类地址:以110开头
  • D类地址:以1110开头
  • E类地址:以1111开头,保留地址


3、三次握手过程中可以携带数据吗?

第三次握手时可以携带数据,但是第一、二次不行。

原因:设想这样的场景,若第一次握手可以携带数据,有人要恶意攻击服务器,则他每次都可以在第一次握手中的SYN报文中放入大量数据,会让服务器花费很多时间、空间来处理报文。

也就是说:第一次握手无法放数据,保证了服务器的安全性。而第三次握手时,已经代表成功的建立了连接,从客户端携带数据到服务器也是被理解的。


4、SYN攻击是什么?

概念:Client在短时间伪造大量不存在的ip地址,向Server不断发送SYN包,Server则回复确认包,并等待Client确认,这些包将长时间占用未连接队列,导致其他正常的SYN请求因为队列满被丢弃,从而引起网络拥塞甚至系统瘫痪(Dos/DDoS攻击)

如何检测:当看到大量半连接状态,且源地址 IP 为随机时,即可断定为一次 SYN 攻击(Linux中的netstat命令)。

解决办法:缩短SYN包的过期时间,过滤网关防护、防火墙等。


5、线程调度策略?

分时调度模型、抢占式调度模型。


  • 抢占式调度 指的是每条线程执行的时间、线程的切换​​都由系统控制​​​,系统控制指的是在系统某种运行机制下,每条线程​​可能分同样的执行时间片​​​,也可能是某些线程执行的​​时间片较长​​​,甚至某些线程​​得不到执行的时间片​​​。在这种机制下,​​一个线程的堵塞不会导致整个进程堵塞​​。
  • **协同式调度 **指的是某一线程​​执行完后主动通知系统切换到另一线程上执行​​​,这种模式就​​像接力赛一样​​​,一个人跑完自己的路程就把接力棒交接给下一个人,下个人继续往下跑。线程的执行时间​​由线程本身控制​​​,​​线程切换可以预知​​​,​​不存在多线程同步问题​​​,但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直 ​堵塞​,那么可能导致整个系统崩溃。

JVM采用抢占式调度模型。


6、为什么wait、notify定义在Object中?


  • JAVA提供的锁是对象级的 (每个对象都有一把称之为monitor监控器的锁),而不是线程级的,每个对象都有锁,通过线程可以获取锁,一个线程可以获取多个锁Object 是所有对象的顶级父类,因此统一把对象锁设置为 Object 对象,这样 Jvm 就会很容易知道应该从哪个对象锁的等待池中唤醒线程。否则它根本不知道要操作的是哪一个。
  • ② ​​wait/notify/notifyAll​​​ 都是对象锁级别的操作,如果把 ​​wait/notify/notifyAll​​ 方法定义在 Thread 类中,会带来很大的局限性:

    • 比如一个线程可能持有多把锁,以便实现相互配合的复杂逻辑,假设此时 ​​wait()​​ 方法定义到 Thread 类中,这会遇到下面两个问题:
    • 如何实现让一个线程持有多把锁呢?
    • 又如何明确线程等待的是那把锁呢?

  • 简单的说,由于​​wait,notify和notifyAll​​都是锁级别的操作,所以把他们定义在 Object 类中因为锁属于对象。


7、为什么wait,notify必须在同步方法或同步块中被调用?

因为,​​wait()​​​ 方法调用时,会释放对象锁,那么一个线程调用 ​​wait()​​​的前提条件是,它必须拥有该对象锁,随后释放并等待,若达到了 ​​notify()​​后,再进入锁。


8、yield() 和 sleep() 区别?

(1) ​sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;​yield()​方法只会给相同优先级或更高优先级的线程以运行的机会

(2) 线程执行 ​​sleep()​​ 方法后转入阻塞(blocked)状态,而执行 ​​yield()​​ 方法后转入就绪(ready)状态;

(3)​​sleep()​​ 方法使用时,需要声明抛出 InterruptedException,而 ​​yield()​​ 方法不需要声明任何异常;


9、如果提交时,线程池队列满,会发生什么?

(1)如果使用的是无界队列LinkedBlockingQueue,那么,可以继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务。

(2)如果使用的是有界队列 比如 ArrayBlockingQueue,任务首先会被添加到 ArrayBlockingQueue 中,ArrayBlockingQueue 满了,会根据 ​​maximumPoolSize​​ 的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue 继续满,那么则会使用拒绝策略 RejectedExecutionHandler 处理满了的任务,默认是中止策略 AbortPolicy


10、JVM调优参数参考


注:这2点仅是简单的作为参考,之后会专门去研究一下这块知识区,写文章分享给大家


① 对堆的参数调整


  • 通过 ​​-Xms -Xmx​​ 限定堆的最小值、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,通常把最大、最小设置为相同的值
  • 年轻代和年老代将根据默认的比例(1:2)分配堆内存,考虑到年轻代需要频繁的进行垃圾回收(堆的空余空间比率不断变化,会导致堆内存大小取值的不断调整,触发阈值为 40%、70%)我们通常会把 ​​-XX:newSize -XX:MaxNewSize​​ 设置为同样大小。


年轻代和年老代设置多大才算合理?


1)更大的年轻代必然导致更小的年老代,大的年轻代会延长普通 GC 的周期,但会增加每次 GC 的时间;小的年老代会导致更频繁的Full GC

2)更小的年轻代必然导致更大年老代,小的年轻代会导致普通 GC 很频繁,但每次的 GC 时间会更短;大的年老代会减少Full GC的频率

如何选择应该依赖应用程序对象生命周期的分布情况: 如果应用存在大量的临时对象,应该选择更大的年轻代;如果存在相对较多的持久对象,年老代应该适当增大。但很多应用都没有这样明显的特性。

在抉择时应该根 据以下两点:


  • 本着 Full GC 尽量少的原则,让年老代尽量缓存常用对象,JVM 的默认比例 1:2 也是这个道理 。
  • 通过观察应用一段时间,看其他在峰值时年老代会占多少内存,在不影响 Full GC 的前提下,根据实际情况加大年轻代,比如可以把比例控制在 1:1。但应该给年老代至少预留 1/3 的增长空间。

② 对栈的参数调整

每个线程默认会开启 1M 的堆栈,用于存放栈帧、调用参数、局部变量等,对大多数应用而言这个默认值太大了,一般 256K 就足用。

理论上,在内存不变的情况下,减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统。