1、满二叉树、完全二叉树、平衡二叉树、红黑树、二叉搜索树的区别?
参考文章:树、二叉树(完全二叉树、满二叉树)概念图解
① 满二叉树
高度为 h
,由 2^h-1
个节点构成的二叉树称为满二叉树。
② 完全二叉树
完全二叉树是由满二叉树而引出来的,若二叉树的深度为 h
,除第 h
层外,其它各层 (1~h-1) 的结点数都达到最大个数(即1~h-1层为一个满二叉树),第 h
层所有的结点都连续集中在最左边,这就是完全二叉树。
堆一般都是用完全二叉树来实现的。
③ 二叉查找树
二叉查找树是二叉树中最常用的一种类型,是为了实现快速查找的,不仅仅支持快速查找一个数,还支持快速插入和删除数据。二叉查找树的这些性能都依赖于二叉查找树的特殊结构,二叉查找树的要求,在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都要大于这个节点的值。
④ 红黑树
红黑树的优势在于它是一棵平衡二叉查找树,对于普通的二叉查找树(非平衡二叉查找树)在极端情况下可能会退化为链表的结构,例如,当我们依次插入 3、4、5、6、7、8 这些数据时,二叉树会退化为如下链表结构:
红黑树是一种弱平衡二叉树(只有黑色节点完美平衡,红色节点不一定平衡),是特殊的二叉查找树(平衡二叉查找树)。
⑤ 平衡二叉树
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 就足用。
理论上,在内存不变的情况下,减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统。
总结
本篇文章就到这里了,希望可以给你带来一些帮助,也希望您能够多多关注脚本之家的更多内容!