admin管理员组

文章数量:1530013

前情提要:下面的内容主要由网上的资料和个人的理解整理而成。由于时间仓促可能没有给出相应的链接,并不代表我不尊重他人的劳动成果,后续更新会补上相应的链接。其中内容可能有理解不到位的地方,大家可选择性采纳。

下面的内容主要是我自己一些可能比较疑惑的内容,并不能包含所有的点。希望大家可以将其作为一个大纲来看,对于每一个不太清除的知识点,继续深入的学习,而不是背诵。


文章目录

      • Java基础
        • 1. Java中使用字节码的好处?
        • 2. Java和C++的区别?
        • 3. 字符常量和字符串常量的区别?
        • 4. 为什么要hashcode和equals方法?
        • 5. Java中的八大基本类型有哪些?
        • 6. Java中为什么只有值传递?
        • 7. 重载和重写的区别?
        • 8. 深拷贝和浅拷贝有什么区别?
        • 9. 什么是多态?
        • 10. StringBuffer和StringBuilder有什么区别?
        • 11. BIO、NIO和AIO有什么区别?
        • 12. HashMap和HashTable的区别?
        • 13. 重写和重载的不同?
        • 14. 线程的创建方式有哪些?
        • 15. Java中的fail-fast和fast-safe机制什么意思?
        • 16. HashMap是如何得到key在哈希桶数组中的位置?
        • 17. HashMap扩容操作是怎么执行的?
        • 18. throw和throws有什么区别?
        • 19. finalize方法的执行过程是怎样的?
        • 20. ArrayList和LinkedList的区别有哪些?
        • 21. ArrayList和Vector的区别有哪些?
        • 22. PriorityQueue介绍一下?
        • 23. 为什么String、Integer适合作为HashMap的key?
        • 24. List、Set和Map的初始容量和加载因子?
        • 25. 动态代理是什么?它有哪些实现方式?
        • 26. LinkedHashMap实现LRU?
      • 高并发与多线程
        • 1. 什么是线程死锁?如何避免死锁?
        • 2. wait和sleep的区别?
        • 3. sychronized和volatile的区别?
        • 4. 什么是线程上下文切换?
        • 5. 简述一下Synchronized关键字?
        • 6. synchronized修饰方法和修饰同步代码块实现上有什么区别?
        • 7. synchronized和ReentrantLock有什么区别?
        • 8. 线程池有哪些饱和策略?
        • 9. 原子类如何保证线程安全?
        • 10. AQS的原理是什么?
        • 11. AQS的常用组件有哪些?
        • 12. 锁升级的过程是怎样的?
        • 13. 什么是乐观锁?
        • 14. 并发编程适合什么场景?创建多少个线程合适?
        • 15. CopyOnWriteArrayList介绍一下?
      • 计算机网络
        • 1. 网络协议的体系结构?
        • 2. ARP原理?
        • 3. TCP三次握手和四次挥手的全过程?
        • 4. TCP对应的协议和UDP对应的协议?
        • 5. NAT、DHCP、DNS的作用?
        • 6. HTTP和HTTPS的区别?
        • 7. 虚电路和数据包的区别?
        • 8. 分层次的路由选择协议?
        • 9. TCP拥塞控制机制?
        • 10. 为什么说UDP是面向报文的的,而TCP是面向字节流的?
        • 11. 流量控制?
        • 12. 什么是糊涂窗口综合症?
        • 13. TCP中的Nagle算法是什么?
        • 14. TCP中的粘包问题是如何出现的?发送方和接收方又是分别如何解决的?
        • 15. ipv4到ipv6的过渡手段?
        • 16. TIME_WAIT的意义?
        • 17. HTTP状态码?
        • 18. 浏览器中通过URL访问到显示页面的过程中使用了哪些协议?
        • 19. cookie和session有什么区别?
        • 20. HTTP1.0和HTTP1.1的主要区别有哪些?
        • 21. 重传机制有哪些?
        • 22. 什么是中间人攻击?
        • 23. 什么是跨域?跨域如何解决?
        • 24. GET和POST的区别?
        • 25.TCP如何保证可靠传输?
        • 26. 什么是DDOs攻击?如何预防?
      • 操作系统
        • 1. 线程间的同步方法有哪些?
        • 2. 内存管理机制有哪些?
        • 3. 进程间通信的方式有哪些?线程间通信方式有哪些?
        • 4. 什么是协程?
        • 5. 实时系统的特征是什么?
        • 6. 数据库以及线程发生死锁的必要条件是什么?
      • MySQL
        • 1. MyISAM和InnoDB的区别?
        • 2. 索引有哪些?聚簇索引是什么?什么是覆盖索引?索引有哪些优点?
        • 3. 事务的四大特性是什么?
        • 4. 事务的ACID靠什么保证?
        • 5. 并发事务会有哪些问题:
        • 6. 事务的隔离级别有哪些?
        • 7. 表锁和行锁有什么区别?
        • 8. InnoDB锁的算法有哪些?
        • 9. 大表的优化手段有哪些?
        • 10. 分表后的ID如何保证唯一性?
        • 11. MySQL中的主从复制流程是怎样的?
        • 12. 什么时候不要用索引?
        • 13. 哈希索引有什么不足之处?
        • 14. 联合索引是什么?为什么需要注意联合索引中的顺序?
        • 15. MySQL中binlog有哪几种格式?
        • 16. explain通常关注哪些信息?
        • 17. SQL优化的方向有哪些?
      • Redis
        • 1. redis的优势
        • 2. redis中的持久化机制有哪些?各有什么特点?
          • 2.1 RDB
          • 2.2 AOF
        • 3. redis 过期键的删除策略?
        • 4. redis中淘汰机制有哪些?
        • 5.为什么redis将数据放入内存中?
        • 6. Pipeline 有什么好处,为什么要用 pipeline?
        • 7. Redis 哈希槽的概念?
        • 8. 假如 Redis 里面有 1 亿个 key,其中有 10w 个 key 是以某个固定的已知的前缀开头的,如果将它们全部找出来?
        • 9. 常见的缓存问题有哪些?
        • 10.redis采用单线程为什么还很快?
        • 11.redis中哨兵的作用是什么?
        • 12. 了解redis中的事务吗?
        • 13. 为什么要用 redis 而不用 map/guava 做缓存?
        • 14. 如何解决 Redis 的并发竞争 Key 问题?
        • 15. 什么是布隆过滤器?
        • 16. Redis如何实现主从节点之间的同步?
        • 17. Redis的线程模型是什么?
        • 18. Redis和Memcached有什么区别?
      • JVM
        • 1. Java虚拟机内存区域的划分?
        • 2. 堆内存的分配机制有哪些?如何保证并发情况下的分配安全?
        • 3. 对象定位的方法有哪些?
        • 4. Jvm中系统级线程有哪些?
        • 5. 程序计数器有什么作用?
        • 6. 虚拟机栈是什么?它有什么作用?
        • 7. 类加载的过程是怎样的?什么是双亲委派机制?
        • 8. Minor GC、Major GC和Full GC有什么区别?
        • 9. 堆的内存分配的策略有哪些?
        • 10. 什么是栈上分配?什么是标量替换?
        • 11. 方法区包含哪些信息?
        • 12. 为什么要用元空间取代永久代(方法区)?
        • 13. 为什么将字符串常量池转移到堆?
        • 14. 方法区的GC主要针对于什么类型的数据?
        • 15. 为什么字符串常量池中不存在内容相同的字符串?
        • 16.GC Roots常包含哪些对象?
        • 17. JVM中的垃圾回收器有哪些?
        • 18. 什么是Remember Set?
        • 19. Shenandosh回收器和ZGC回收器的过程是怎么样的?
        • 20. 虚拟机栈什么时候会出现StackOverFlow和OOM?
        • 21. Java对象的创建过程是怎样的?
        • 22. 四种类型的引用有什么区别?
        • 23. 常用的JVM调优参数有哪些?
      • Spring
        • 1. Spring的特征有哪些?
        • 2. @Controller和@RestController的区别?
        • 3. 解释一下IoC和AOP?
        • 4. Sping中Bean的作用域有哪些?
        • 5. Bean的生命周期?
        • 6. Spring MVC的工作原理?
        • 7. Spring中用到了哪些设计模式?
        • 8. Spring中的事务有哪几种?事务隔离级别呢?事务传播行为呢?
        • 9. 详细说一下ioc?Important
        • 10. 详细说一下AOP?
        • 11. 什么是依赖注入?依赖注入的基本原则是什么?依赖注入有哪些实现方式?
        • 12. Spring如何解决线程安全问题?
        • 13. Spring Boot的自动配置原理是什么?
        • 14. @Transactional注解的实现机制 ?
        • 15. Spring中的循环依赖问题是什么?如何解决?
      • RocketMQ
        • 1. 消息队列的优缺点有哪些?
        • 2. 简要介绍一下RocketMQ的主要部分?
        • 3. 集群模式的特点?
        • 4. Producer和Consumer的工作流程是怎样的?
        • 5. 顺序消息的实现机制是怎样的?
        • 6. 事务型消息的实现机制?
        • 7. RoketMQ的持久化是什么?
        • 8. 消息的存储结构是什么样的?
        • 9. 消息的刷盘机制有哪两种?
        • 10. RocketMQ的高可用机制是如何实现的?
        • 11. Broker的主从复制是如何实现的?
        • 12. 负载均衡是如何实现的?
        • 13. 消息重试是什么?
        • 14. 死信队列是什么?
        • 15. 消息幂等是如何实现的?


Java基础


1. Java中使用字节码的好处?

Java中的字节码指令面向Java虚拟机,Java通过字节码的方式在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言的优点,所以它运行比较高效。而且字节码指令并不针对某一种特定的机器,故而Java程序无需重新编译便可在不同的操作系统上运行,做到了一次编译,到处运行


2. Java和C++的区别?
  • Java和C++都是面向对象的语言,所以都支持面向对象的四大特征:抽象、封装、继承、多态
  • Java中没有提供指针来直接操作内存空间,因此内存空间使用更安全
  • Java中的类是单继承的,C++中类是多继承的
  • Java提供了自动内存管理机制,内存管理由垃圾回收机制实现,C++中需要手动的申请和释放内存

3. 字符常量和字符串常量的区别?
  • 字符常量使用单引号'',字符串常量使用""

    char c = 'c';
    String name  = "Java"
  • 字符常量相当于一个整型的ASCII值,可以参与表达式运算;而字符串常量由于存放在字符串常量池中,所以实际上它表示的是字符串在堆内存中的地址

  • 字符常量占2个字节,字符串常量占若干个字节,和它包含的字符数有关


4. 为什么要hashcode和equals方法?

Java中自定义类为什么一定要重写HashCode和equals方法?


5. Java中的八大基本类型有哪些?
  • 6种数字类型:byte、short、int、long、float、double
  • 1种字符类型:char
  • 1种布尔类型:boolean

另外,每种基本类型都有对应的包装类,便于以面向对象的方式使用。


6. Java中为什么只有值传递?

Java中对对象采用的什么传递方式呢?

不管是基本数据类型还是引用类型,Java中的参数传递只有值传递一种!


7. 重载和重写的区别?
  • 重载发生了同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法的返回值和修饰符可以不同。总之,重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理
  • 重写发生在运行期,指的是子类对父类的允许访问的方法的实现过程进行重写编写:
    • 返回值类型、方法名、参数列表必须相同,抛出的异常范围小于等于父类,访问控制修饰符的范围大于等于父类
    • 如果父类中方法被private、final、static所修饰,则子类不能重写这些方法
    • 构造方法不能被重写

8. 深拷贝和浅拷贝有什么区别?

对于浅拷贝来说:

  • 对于基本数据类型的成员对象,因为基础数据类型是值传递的,所以是直接将属性值赋值给新的对象。基本类型数据的拷贝,其中一个对象修改该值,不会影响另外一个
  • 对于引用类型,比如数组或者类对象,因为引用类型是引用传递,所以浅拷贝只是把内存地址赋值给了成员变量,它们指向了同一内存空间。改变其中一个,会对另外一个也产生影响

而对于深拷贝来说:

  • 对于基本数据类型的成员对象,因为基础数据类型是值传递的,所以是直接将属性值赋值给新的对象。基本类型的拷贝,其中一个对象修改该值,不会影响另外一个(和浅拷贝一样)
  • 对于引用类型,比如数组或者类对象,深拷贝会新建一个对象空间,然后拷贝里面的内容,所以它们指向了不同的内存空间。改变其中一个,不会对另外一个也产生影响
    • 对于有多层对象的,每个对象都需要实现 Cloneable 并重写 clone() 方法,进而实现对象的串行层层拷贝
    • 深拷贝相比于浅拷贝速度较慢并且花销较大

图解Java中的浅拷贝和深拷贝


9. 什么是多态?

多态表示一个对象具有多种状态,表现为父类的引用指向子类的实例。多态具有如下特点:

  • 对象类型和引用类型之间具有继承或实现关系
  • 对象类型不可变,引用类型可变
  • 方法具有多态性,属性不具有多态性
  • 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在运行期才能确定
  • 多态不能调用只在子类存在但在父类中不存在的方法
  • 如果子类重写了父类的方法,真正执行的是子类覆盖的方法;如果子类没有覆盖父类的方法,执行的是父类的方法

10. StringBuffer和StringBuilder有什么区别?

StringBuffer和StringBuilder拥有相同的父类AbstractStringBuilder,它们的构造方法也是调用父类的构造方法。AbstractStringBuilder底层使用char型数组来保存字符串内容,但没有使用final关键字修饰。

AbstractStringBuilder中定义了操作字符串的一些公共方法,StringBuffer中方法都使用了sychronized来保证线程同步,所以它是线程安全的。StringBuilder并没有对方法加锁,所以它是线程不安全的,但是StringBuilder的性能更高一些。

public final class StringBuffer
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{

    @Override
    public synchronized int length() {
        return count;
    }
    
    @Override
    public synchronized int capacity() {
        return value.length;
    }


    @Override
    public synchronized void ensureCapacity(int minimumCapacity) {
        super.ensureCapacity(minimumCapacity);
    }
    ...

}

public final class StringBuilder
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{
    @Override
    public StringBuilder append(Object obj) {
        return append(String.valueOf(obj));
    }

    @Override
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }

    ...
}

对于String类型对象的操作本质上操作的是它的副本,StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象再改变对象的引用。

总结:

  • 操作少量的数据使用String
  • 单线程字符串缓冲区下操作大量数据使用StringBuilder
  • 多线程字符串缓冲区下操作大量数据使用StringBuffer

11. BIO、NIO和AIO有什么区别?
  • BIO,Blocking IO:同步阻塞I/O,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制来改善。BIO方式适用于连接数目比较小且固定的架构,这种方式对服务端资源要求比较高,并发局限于应用中
  • NIO,Non-Blocking/New IO:同步非阻塞I/O,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到IO多路复用器上,IO多路复用器轮询到连接有IO请求时才启动一个线程进行处理。NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂
  • AIO,Asynchronout IO:异步非阻塞I/O,服务器实现模式为一个有效请求一个线程,客户端的IO请求都是由操作系统先完成了再通知服务器启动线程进行处理。AIO方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂

BIO、NIO、AIO有什么区别


12. HashMap和HashTable的区别?
  • Hashtable继承自Dictionary类,而HashMap继承自AbstractMap类,但二者都实现了Map接口
  • HashTable方法被synchronized所修饰,它是线程安全的;而HashMap是线程不安全的
  • HashTable中key和value都不允许为null,put(null,null)编译可以通过,但是运行会抛空指针异常;HashMap中key允许为null,但是这样的key只有一个
  • HashTable和HashMap都可以使用iterator进行遍历;此外,HashTable还可以使用Enumeration方式遍历
  • HashTable默认容量为11,不要求底层数组容量为2的幂次;HashMap默认初始容量为16,要求底层数组容量必须是2的幂次
  • HashTable扩容时将容量变为原来的2倍加1;HashMap扩容时将容量变为原来的2倍

13. 重写和重载的不同?
  • 方法重写要求参数列表必须一致,而方法重载要求参数列表必须不一致。
  • 方法重写要求返回类型必须一致(或为其子类型),方法重载对此没有要求
  • 方法重写只能用于子类重写父类的方法,方法重载用于同一个类中的所有方法
  • 方法重写对方法的访问权限和抛出的异常有特殊的要求,而方法重载在这方面没有任何限制
  • 父类的一个方法只能被子类重写一次,而一个方法可以在所有的类中可以被重载多次
  • 重载是编译时多态,重写是运行时多态

14. 线程的创建方式有哪些?

线程的创建方式有四种:

  • 继承Thread类
  • 实现Runnable接口
  • 实现Callable接口
  • 使用Executor框架创建线程池

15. Java中的fail-fast和fast-safe机制什么意思?
  • fail-fast:快速失败,当遍历集合的过程中,如果集合的结构发生了改变,例如进行了put操作或是扩容操作,那么程序就会抛出Concurrent Modification Exception。java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)
  • fail-safe:任何对集合结构的修改都会在一个复制的集合上进行修改,因此不会抛出Concurren tModification Exception。java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改

Java中的fail-fast和fast-safe机制


16. HashMap是如何得到key在哈希桶数组中的位置?

哈希桶索引位置的计算需要如下三步:

  • 首先,调用hashcode()得到key的哈希值h
  • h的高16位和低16位进行异或运算
  • 将上一步异或的结果和length - 1做与运算得到数组中位置。这样的取模方式和传统的取模效果是一样的,但效率更高

HashMap的容量必须为2的幂次,是因为2的幂次减1的值每一位都是1,这样最后一步索引的位置只与key本身的哈希值有关。既保证了和传统的取模效果一致,又不会造成空间的浪费而且分布均匀。


17. HashMap扩容操作是怎么执行的?

当调用put()往HashMap插入元素之后,就会和扩容阈值进行比较,判断是否需要进行扩容。HashMap的扩容操作主要分为两步:

  • 首先,将HashMap的容量扩充为原来的两倍
  • 将原来HashMap中的元素放入到扩容后HashMap的相应位置。Jdk 1.8 之后位置判断的过程如下:
    • 首先计算key在新HashMap下的哈希值h
    • 接着只需要看h新增的一位是0还是1
      • 如果是0,则将其插入到和原来HashMap相同索引位置处
      • 如果是1,则新的索引位置等于原来的位置 + 源哈希数组的长度
    • 将元素插入新哈希表采用的是头插法

18. throw和throws有什么区别?
  • Throw用于方法内部,Throws用于方法声明上
  • Throw后跟异常对象,Throws后跟异常类型
  • Throw后只能跟一个异常对象,Throws后可以一次声明多种异常类型
// throw
public void test{
	try{
        ...
    } catch(Exception e){
    	throw new Exception("");
    }
}

// throws
public void test  throws IOException, xxxException{
	...
}

19. finalize方法的执行过程是怎样的?

finalize方法一个对象只能执行一次,只能在第一次进入被回收的队列,而且对象所属于的类重写了finalize方法才会被执行。第二次进入回收队列的时候,不会再执行其finalize方法,而是直接被二次标记,在下一次GC的时候被GC。



20. ArrayList和LinkedList的区别有哪些?
  • ArrayList依赖于数组实现,LinkedList依赖于链表实现
  • ArrayList便于随机访问,不利于删除和插入操作;LinkedList正好相反
  • LinkedList比ArrayList开销大,因为链表的节点需要同时存储数据和指针

21. ArrayList和Vector的区别有哪些?
  • Vector是线程安全的,ArrayList是线程不安全的
  • ArrayList在底层数据不够使用时,在原来的基础上扩容1.5倍;Vector扩容1倍
  • Vector在关键性操作的方法上使用了synchronized关键字,来保证线程安全

22. PriorityQueue介绍一下?

PriorityQueue是Queue接口的一个实现类,可以对队列中的元素进行排序。它默认是升序排列,当前也可以自定义排序规则。常用的方法有:

  • peek:返回队首元素
  • poll:返回队尾元素
  • add:添加元素
  • size:放回队列中元素的个数
  • isEmpty:判断队列是否为空

它的实现依赖于优先级堆,不允许有null,而且是线程不安全的。出入队操作的时间复杂度为O(longn),当调用remove方法时,返回的是堆的最小值。


23. 为什么String、Integer适合作为HashMap的key?
  • 它们都是被final修饰的类,不可变性保证了key的不可变,不会存在hash值不同的情况
  • 内存重写和hashCode和equals方法,遵从HashMap的规范
  • 能够有效减少哈希碰撞的概率

HashMap源码中这些常量的设计目的


24. List、Set和Map的初始容量和加载因子?
  • ArrayList的初始容量是10;加载因子为0.5; 扩容增量:原容量的 0.5倍+1;一次扩容后长度为15。
  • Vector初始容量为10,加载因子是1。扩容增量:原容量的 1倍,如 Vector的容量为10,一次扩容后是容量为20
  • HashSet,初始容量为16,加载因子为0.75; 扩容增量:原容量的 1 倍; 如 HashSet的容量为16,一次扩容后容量为32
  • HashMap,初始容量16,加载因子为0.75; 扩容增量:原容量的 1 倍; 如 HashMap的容量为16,一次扩容后容量为32

25. 动态代理是什么?它有哪些实现方式?

动态代理中的动态指的是代理类并不是在编译期就被确定,而是在运行期才被确定下来。常用的动态代理方式有JDK实现的动态代理和Cglib实现的动态代理,如果目标对象需要实现接口,那么使用JDK的动态代理;如果目标对象不需要实现接口,那么使用Cglib代理

JDK的代理类使用需要实现InvocationHandler接口,并重写其中的invoke方法,获取代理对象需要使用Proxy类的静态方法static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler invocationHandler)来获取代理对象。通过代理对象调用委托类中的方法时,最终都是通过invoke方法实现。

public class Agent implements InvocationHandler {
    Object obj;

    public Agent(Object obj) {
        this.obj = obj;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("before invoke...");
        System.out.println("RealClass is:" + this.obj.toString() + " and method is: " + method.getName());
        method.invoke(obj, args);
        System.out.println("after invoke...");
        return null;
    }


    public Object getProxyInstance(){
        return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), this);
    }
}

Cglib代理需要使用MethodInterceptor接口,并重写其中的intecept方法,方法中的method.invoke执行具体的逻辑。如果想要获取代理类对象,可以通过getProxyInstance方法,它主要包含如下几步:

  • 创建工具类
  • 设置委托类
  • 设置回调函数
  • 创建代理对象并返回
public class Agent implements MethodInterceptor {
    private Object obj;

    public Agent(Object obj) {
        this.obj = obj;
    }

    public Object getProxyInstance(){
        // 创建工具类
        Enhancer enhancer = new Enhancer();
        // 设置父类,即委托类
        enhancer.setSuperclass(obj.getClass());
        // 设置回调函数
        enhancer.setCallback(this);
        //创建子类对象,即代理对象
        return enhancer.create();
    }
	
    // 重写intercept方法,调用目标对象的方法
    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("before cglib proxy...");
        method.invoke(obj, args);
        System.out.println("after cglib proxy...");
        return null;
    }
}

总结:

  • JDK动态代理基于反射,利用反射机制生成实现代理接口的匿名类,再调用具体的方法前调用InvocationHandler处理
  • Cglib代理基于字节码,通过加载代理对象的类字节码,为代理对象创建一个子类,并在子类中拦截父类方法并织入方法的增强逻辑

26. LinkedHashMap实现LRU?
  • 设定最大缓存空间 MAX_ENTRIES 为 3;
  • 使用 LinkedHashMap 的构造函数将 accessOrder 设置为 true,开启 LRU 顺序;
  • 覆盖 removeEldestEntry() 方法实现,在节点多于 MAX_ENTRIES 就会将最近最久未使用的数据移除。
class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private static final int MAX_ENTRIES = 3;

    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > MAX_ENTRIES;
    }

    LRUCache() {
        super(MAX_ENTRIES, 0.75f, true);
    }
}

高并发与多线程


1. 什么是线程死锁?如何避免死锁?

线程死锁指多个线程同时被阻塞,它们中的一个或全部都在等待某个资源被释放,由于线程被无限期的阻塞,导致程序不能正常的终止。

死锁发生必须具备如下四个条件;

  • 互斥条件:临界资源任意时刻只由一个线程占用
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已经获得的资源保持不放
  • 不可剥夺条件:线程已获得的资源在未使用完之前不能被其他的线程所强行剥夺,只有自己使用完毕后才释放资源
  • 循环等待条件:若干进程之间形成一种首尾相接的循环等待资源关系

如果想避免死锁发生,可以选择破坏请求与保持条件、不可剥夺条件和循环等待条件,方法分别如下:

  • 一次性申请所有所需的资源
  • 占用部分资源的线程如果进一步申请其他资源时申请不到,主动使用自己已经获得的资源
  • 按序申请

2. wait和sleep的区别?
  • sleep方法调用时不会释放锁,wait方法会释放锁
  • wait常用于线程间的通信,如常见的生产者消费者模式;sleep常用于程序暂停执行
  • wait方法调用后不能自动唤醒,必须等待调用同一对象的其他线程使用notify或是notifyAll方法唤醒;而sleep方法在执行完成后会自动唤醒

3. sychronized和volatile的区别?
  • volatile是一种线程同步的轻量级实现,性能比sychronized关键字好一些,但volatile只能作用于变量,而sychronized可以作用于方法的同步代码块
  • 多线程访问volatile不会阻塞,而sychronized可能会发生阻塞
  • volatile能保证数据的可见性,但不能保证数据的原子性,sychronized关键字两者都可以保证
  • volatile主要用于解决变量在多个线程之间的可见性,而sychronized解决的是多线程之间访问共享资源的同步性

4. 什么是线程上下文切换?

多线程编程中一般线程数都会大于CPU核心数,但是一个CPU核心某一时刻只能执行一个线程。因此,CPU采用了时间片轮转调度的方式,当一个线程的时间片用完的时候,它会重新处于就绪状态,将CPU让给其他线程使用。但是切换之前需要保存自己的状态,以便下次再切换回该线程。线程从保存到再加载的过程就是一次上下文切换。


5. 简述一下Synchronized关键字?

synchronized关键字是一种最为常用的解决线程同步问题的手段,它可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。Jdk 6之前synchronized属于重量级锁,线程需要竞争的是锁对象的Monitor,而Monitor的实现依赖于操作系统的Mutex Lock实现。Java的线程是映射到操作系统上的原生线程上实现的,线程的挂起和唤醒都需要进行用户态和内核态的切换,性能开销交到,时间成本较高。

Jdk 6之后对于synchronized进行了优化,引入了自旋锁、偏向锁、轻量级锁、锁消除和锁粗化等技术来较少锁操作的开销。

synchronized主要有三种使用方式:

  • 修饰实例方法:作用于给当前对象实例加锁,进入同步代码块前需要获得当前对象实例的锁
  • 修饰静态方法:作用于给当前类加锁,作用于类的所有对象实例,因为静态成员是类成员,它不属于任何一个实例对象。
  • 修饰代码块:指定加锁独享,对给定的对象加锁,进入同步代码块之前需要获得给定的锁对象

synchronized加到static静态方法和synchronized(class)代码块上都是给Class类上锁;synchronized加到实例方法上是给对象实例上锁。


6. synchronized修饰方法和修饰同步代码块实现上有什么区别?
  • synchronized修饰同步代码块:底层使用monitorenter和monitorexit指令,用来表示同步代码块的开始和结束位置。当执行monitorenter指令时,线程试图获取对象有中的monitor的持有权。只有当计数器为0时才能成功获取,获取后将锁计数器设为1;执行monitorexit指令时再将计数器置为0,表示锁被释放。如果获取对象锁失败,那么当前线程就要阻塞等待,直到锁被另外一个线程释放
  • synchronized修饰方法:使用ACC_SYNCHRONIZED标识来指明当前的方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个 方法是否声明为同步方法,从而执行相应的同步调用。

7. synchronized和ReentrantLock有什么区别?
  • 两者都是可重入锁:同一个线程每次获取锁会使得锁的计数值加1,只有等到锁的计数器下降为0时才能释放锁
  • synchronized依赖于JVM,ReentrantLock依赖于API
  • ReentrantLock比synchronized多了一些高级功能:
    • ReentrantLock提供了一种能够中断等待锁的线程的机制:对应的方法是lock.lockInterruptibly()
    • ReentrantLock可以实现公平锁和非公平锁:对应的方法是ReentrantLock(Boolean fair)
    • synchronized配合wait()notify()notifyAll()可以实现等待通知机制,ReentrantLock借助Condition接口与newCondition()也可以实现。线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,被通知的线程由JVM选择,用ReentrantLock类结合Condition实例实现选择性通知
  • synchronized锁可以由JVM自动释放,ReentrantLock需要用户在finally子句中手动释放,否则会一直持有锁
  • 高并发场景下ReentrantLock的性能通常优于synchronized

8. 线程池有哪些饱和策略?

如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务,ThreadPoolTaskExecutor定义了如下的饱和策略:

  • ThreadPoolExecutor.AbortPolicy:抛出RejectedExecutionException异常拒绝新任务的处理
  • ThreadPoolExecutor.CalledRunsPolicy:调用执行自己的线程运行任务
  • ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃
  • ThreadPoolExecutor.DiscardOldestPolicy:丢弃最早的未处理的任务请求

9. 原子类如何保证线程安全?

原子类主要使用CAS+volatile和native方法来保证原子操作,从而避免synchronized的高开销,执行效率大为提升。


10. AQS的原理是什么?

AQS的核心思想是:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待和被唤醒时锁分配的机制,这个机制由CLH队列锁实现,即将暂时获取不到锁的线程加入到队列中。



11. AQS的常用组件有哪些?
  • Semaphore:信号量,控制线程访问的个数,来达到限制通用资源访问的目的。其原理是通过acquire()获取一个许可,如果没有则等待,而relaease()释放一个许可
  • CountDownLatch:倒计数,允许一个或多个线程等待某些操作完成。使用时在其构造方法中指明计数数量,被等待线程调用countDown将计数器减1,等待线程使用await()进行线程等待
  • CyclicBarrier:循环栅栏,可以实现线程的等待,让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,所有被屏障拦截的线程才会继续执行。它可以被重复使用,典型应用场景是用来等待并发线程结束。
    CyclicBarrier的主要方法是await(),方法每被调用一次计数便减少1,并阻塞住当前线程。当计数减至0时阻塞解除,所有在此CyclicBarrier上阻塞的线程开始运行。在此之后,如果再次调用await()会进行再一次轮相同的操作。

CyclicBarrier和CountDownLatch的区别在于:

  • CountDownLatch不可重用,CyclicBarrier是可以重用的
  • CountDownLatch只要调用await()的线程阻塞等待countDown足够的次数,不管是几个线程里countDown,只要次数够即可。CyclicBarrier需要所有的线程都调用了await()才能继续进行任务,并自动重置。

总而言之,CountDownLatch是让一个线程等待其他N个线程达到某个条件后,自己再去做某件事。CyclicBarrier是让N个线程互相等待直到所有线程都达到某种状态,这些线程再继续执行各自的任务


12. 锁升级的过程是怎样的?
  • 当没有竞争时,默认使用偏向锁:JVM使用CAS操作将锁对象的对象头的Mark Word部分设置为线程id,表示当前锁偏向与该线程,这里并没有使用操作系统底层的互斥锁。因为在很多场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏向锁可以减少无竞争的开销
  • 如果此时有另外的线程也来竞争偏向锁,JVM就会将偏向锁升级为轻量级锁。轻量级锁依赖于CAS操作Mark Word来尝试获取锁,如果成功就使用轻量级锁,否则升级为重量级锁

13. 什么是乐观锁?

乐观锁是相对于synchronized这样的悲观锁而言的,对于共享变量的操作并不是总是尝试去先加锁再操作,而是使用基于冲突监测的乐观并发策略。它会首先尝试操作共享变量,如果期间没有其他的线程来操作该变量,那么操作成功;如果期间也有其他的线程来操作变量,则会发生冲突,那就再进行其他的补偿措施,如加锁等。

乐观锁的核心是CAS(Compare And Swap),它涉及到三个操作数:内存值、预期值和新值,当且仅当预期值和内存值相等时才将内存值修改为新值。CAS具有原子性,它的原子性由CPU硬件指令实现保证,即使用JNI调用Native方法调用由C++ 编写的硬件指令,JDK中的Unsafe类可以执行这些操作。

乐观锁虽然可以避免悲观锁带来的线程切换、维护锁计数器和检查是否有阻塞线程等的开销,但是也有缺点:

  • 乐观锁只能保证对一个共享变量的操作具有原子性
  • 乐观锁会产生自旋操作,如果自旋时间太长会消耗CPU资源
  • 无法解决ABA问题,相应的可以引入版本号等方法解决,例如Jdk提供的AtomicStampedReference类,类中的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志。如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值

14. 并发编程适合什么场景?创建多少个线程合适?

一般来说,线程等待时间所占比例越高,需要越多线程;线程CPU时间所占比例越高,需要越少线程。因此,对于IO密集型任务采用并发编程更加合适。

创建多少了线程合适。可分为以下两种情况:

  • CPU密集型: C P U 核 心 数 + 1 CPU核心数 + 1 CPU+1
  • IO密集型: C P U 核 心 数 × 1 C P U 利 用 率 = C P U 核 心 数 × ( 1 + I O 耗 时 C P U 耗 时 ) CPU核心数 \times \frac{1}{CPU利用率} = CPU核心数 \times (1 + \frac{IO耗时}{CPU耗时}) CPU×CPU1=CPU×(1+CPUIO)

15. CopyOnWriteArrayList介绍一下?

它是JUC下的ArrayList线程安全的解决方法,通过ReentrantLock获取对象锁的方式来实现线程安全。

读操作和写操作源码如下:

@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
    return (E) a[index];
}

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

final void setArray(Object[] a) {
    array = a;
}

计算机网络

Java面试知识点解析(五)——网络协议篇


1. 网络协议的体系结构?

  • 应用层:通过应用进程间的交互来完成特定网络应用,它定义的是应用进程间的通信和交互规则。常用的应用层协议有:域名解析协议DNS、超文本传输协议HTTP、电子邮件协议SMTP和POP3等
  • 传输层:主要负责向进程间的通信提供通用的数据传输服务,该层主要协议有TCP和UDP
UDPTCP
是否连接无连接面向连接
是否可靠不可靠传输,不使用流量控制和拥塞控制可靠传输,使用流量控制和拥塞控制
传输方式支持一对一、一对多和多对多交互通信只能是一对一通信
面向对象面向报文面向字节流,TCP将数据看成是一连串无结构的字节流
信道不可靠信道全双工可靠信道
首部开销首部开销小,仅8字节首部最小20字节,最大60字节
场景适用于实时应用,如IP电话、实时视频会议、直播等适用于要求可靠传输的应用,如文件传输等
  • 网络层:选择合适的网间路由和交换结点,确保计算机通信的数据及时传送。
  • 数据链路层:负责网络寻址、错误侦测和改错。当表头和表尾被加至数据包时,会形成了帧。四大特点:
    • 封装成帧:把网络层数据报加头和尾,封装层帧,帧头中包括源MAC地址和目的MAC地址
    • 透明传输:在数据链路层将网络层数据封装成帧时,会在首部和尾部分别添加SOH以及EOT这两个特殊字符,接收方就是根据这两个字符来确定帧的帧首和帧尾的,如果上层协议发过来的数据(即链路层的数据部分)包含EOT,那么接收方解析这个帧的时候就会误以为数据已经结束,但是链路层通过对这个字符添加转义符ESC,那么当接收到连续的两个转义字符时,就删除前面的一个
    • 可靠传输: 在出错率很低的链路上很少用,但是无线链路WLAN会保证可靠传输
    • 差错检验(CRC):接收者检测错误,如果发现差错,丢弃该帧
  • 物理层: 实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异,使其上面的数据链路层不必考虑网络的具体传输介质是什么

另外在OSI参考模型中还有两层:

  • 表示层:数据格式转化、数据加密等
  • 会话层:建立、管理和维护会话

不同层工作的设备

  • 传输层:四层交换机、四层的路由器
  • 网络层:路由器、三层交换机
  • 数据链路层:网桥、以太网交换机、网卡
  • 物理层:中继器、集线器、双绞线

2. ARP原理?

根据目标Ip地址借助ARP请求ARP响应来确定目标的MAC地址。

  • 原理:通过广播发送ARP请求,Ip地址一致的主机接收该请求,然后将自己的MAC地址加入的ARP响应包,返回给源主机。要进行MAC地址缓存来避免占用网络流量
    • 每个主机都会在自己的ARP缓冲区建立一个ARP列表,表示Ip地址和MAC地址的对应关系
    • 当源主机要发送数据时,首先检查ARP列表中是否有对应Ip地址的目标主机的MAC地址,若有,则直接发送,若无,则向本网段的所有主机发送ARP数据包,该数据的内容有:源Ip地址,源MAC地址,目标主机Ip地址
    • 当本网络的所有主机收到该ARP数据包时,首先检查包中的Ip地址是否是自己的Ip地址,若不是则丢弃,若是,则首先从数据包中取出源主机的IP和MAC地址写入自己的ARP列表中,若已存在,则覆盖;然后将自己的MAC地址写入ARP响应包中,告诉源主机自己的MAC地址
    • 源主机收到ARP响应包后,将目标主机的IP和MAC地址写入ARP列表中。若源主机一直没有收到ARP响应,则ARP查询失败
    • 广播发送ARP请求,单播发送ARP响应

有了ip地址寻址为什么还需要MAC地址?

一. 整体与局部
信息传递时候,需要知道的其实是两个地址:

  • 终点地址(Final destination address)
  • 下一跳的地址(Next hop address)

IP地址本质上是终点地址,它在跳过路由器(hop)的时候不会改变,而MAC地址则是下一跳的地址,每跳过一次路由器都会改变。这就是为什么还要用MAC地址的原因之一,它起到了记录下一跳的信息的作用。注:一般来说IP地址经过路由器是不变的,不过NAT(Network address translation)例外,这也是有些人反对NAT而支持IPV6的原因之一。
二. 分层实现
如果在IP包头(header)中增加了”下一跳IP地址“这个字段,在逻辑上来说,如果IP地址够用,交换机也支持根据IP地址转发(现在的二层交换机不支持这样做),其实MAC地址并不是必要的。但用MAC地址和IP地址两个地址,用于分别表示物理地址和逻辑地址是有好处的。这样分层可以使网络层与链路层的协议更灵活地替换,网络层不一定非要用『IP』协议,链路层也不一定非用『以太网』协议。
三. 早期的『以太网』实现
早期的以太网只有集线器(hub),没有交换机(switch),所以发出去的包能被以太网内的所有机器监听到。因此要附带上MAC地址,每个机器只需要接受与自己MAC地址相匹配的包。


3. TCP三次握手和四次挥手的全过程?

三次握手:


  • 第一次握手:客户端发送syn包(syn=x)到服务器,并进入SYN_SEND状态,等待服务器确认;
  • 第二次握手:服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(syn=y),即SYN+ACK包,此时服务器进入SYN_RECV状态;
  • 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。

握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去。

四次挥手


与建立连接的“三次握手”类似,断开一个TCP连接则需要“四次挥手”。

  • 第一次挥手:主动关闭方发送一个FIN,用来关闭主动方到被动关闭方的数据传送,也就是主动关闭方告诉被动关闭方:我已经不会再给你发数据了(当然,在fin包之前发送出去的数据,如果没有收到对应的ack确认报文,主动关闭方依然会重发这些数据),但是,此时主动关闭方还可以接受数据
  • 第二次挥手:被动关闭方收到FIN包后,发送一个ACK给对方,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号)
  • 第三次挥手:被动关闭方发送一个FIN,用来关闭被动关闭方到主动关闭方的数据传送,也就是告诉主动关闭方,我的数据也发送完了,不会再给你发数据了
  • 第四次挥手:主动关闭方收到FIN后,发送一个ACK给被动关闭方,确认序号为收到序号+1,至此,完成四次挥手

TCP的三次握手过程?为什么会采用三次握手,若采用二次握手可以吗?

答:建立连接的过程是利用客户服务器模式,假设主机A为客户端,主机B为服务器端。

(1)TCP的三次握手过程:主机A向B发送连接请求;主机B对收到的主机A的报文段进行确认;主机A再次对主机B的确认进行确认。

(2)采用三次握手是为了防止失效的连接请求报文段突然又传送到主机B,因而产生错误。失效的连接请求报文段是指:主机A发出的连接请求没有收到主机B的确认,于是经过一段时间后,主机A又重新向主机B发送连接请求,且建立成功,顺序完成数据传输。考虑这样一种特殊情况,主机A第一次发送的连接请求并没有丢失,而是因为网络节点导致延迟达到主机B,主机B以为是主机A又发起的新连接,于是主机B同意连接,并向主机A发回确认,但是此时主机A根本不会理会,主机B就一直在等待主机A发送数据,导致主机B的资源浪费。或者如果只握手2次,第二次握手时如果服务端发给客户端的确认报文段丢失,此时服务端已经准备好了收发数据,而客户端一直没收到服务端的确认报文。此时,客户端就不知道服务端是否已经准备好了,客户端不会给服务端发数据,也会忽略服务端发过来的数据。


4. TCP对应的协议和UDP对应的协议?

TCP对应的协议:


  • FTP:定义了文件传输协议,使用21端口
  • Telnet:一种用于远程登陆的端口,使用23端口,用户可以以自己的身份远程连接到计算机上,可提供基于DOS模式下的通信服务
  • SMTP:邮件传送协议,用于发送邮件。服务器开放的是25号端口
  • POP3:它是和SMTP对应,POP3用于接收邮件。POP3协议所用的是110端口
  • HTTP:超文本传输协议,是从Web服务器传输超文本到本地浏览器的传送协议
  • HTTPS:HTTP的安全版本
  • SSH:用于加密安全登陆

UDP对应的协议:

  • DNS:用于域名解析服务,将域名地址转换为IP地址。DNS用的是53号端口

  • SNMP:简单网络管理协议,使用161号端口,是用来管理网络设备的。由于网络设备很多,无连接的服务就体现出其优势

  • TFTP:简单文件传输协议,该协议在熟知端口69上使用UDP服务

  • NTP:网络时间协议,用于网络同步

  • DHCP:动态主机配置协议,配置IP地址


5. NAT、DHCP、DNS的作用?
  • NAT:网络地址转换(NAT,Network Address Translation)属接入广域网(WAN)技术,是一种将私有(保留)地址转化为合法IP地址的转换技术,它被广泛应用于各种类型Internet接入方式和各种类型的网络中。NAT不仅完美地解决了lP地址不足的问题,而且还能够有效地避免来自网络外部的攻击,隐藏并保护网络内部的计算机
  • DHCP:动态主机设置协议(Dynamic Host ConfigurationProtocol, DHCP),是一个局域网的网络协议,使用UDP协议工作,主要有两个用途:给内部网络或网络服务供应商自动分配IP地址,给用户或者内部网络管理员作为对所有计算机作中央管理的手段
  • DNS:DNS 是域名系统 (Domain Name System)的缩写,是因特网的一项核心服务,它作为可以将域名和IP地址相互映射的一个分布式数据库,能够使人更方便的访问互联网,而不用去记住能够被机器直接读取的IP数串

6. HTTP和HTTPS的区别?

HTTP协议


HTTPS(HyperText Transfer Protocol over Secure Socket Layer)协议


两者的区别在于:

区别HTTPHTTPS
协议运行在TCP之上,明文传输,客户端与服务器段都无法验证对方的身份运行在SSL(Secure Socket Layer)协议之上,SSL协议运行在TCP之上,它是添加了加密和认证机制的HTTP
端口80443
资源消耗较少由于加密处理的存在,会消耗更多的CPU和内存资源,页面加载较慢;连接缓存较低效;申请CA证书需要花钱
加密机制共享密钥和公开密钥加密并用的混合加密机制
安全性由于加密机制,安全性强

区别:

  • HTTPS协议需要到CA申请证书,需要一定费用,针对HTTP不验证通信双方的身份
  • HTTP是超文本传输协议,信息是明文传输,HTTPS则是安全性的SSL加密传输协议
  • HTTPS采用共享密钥加密和公开密钥加密两者并用的混合加密机制。在交换密钥阶段使用公开密钥加密方式,之后的建立通信交换报文阶段则使用共享密钥加密方式

HTTPS建立连接的过程:

  1. 客户端发送请求到服务器端
  2. 服务器端返回证书和公开密钥,公开密钥作为证书的一部分而存在
  3. 客户端验证证书和公开密钥的有效性,如果有效,则生成共享密钥并使用公开密钥加密发送到服务器端
  4. 服务器端使用私有密钥解密数据,并使用收到的共享密钥加密数据,发送到客户端
  5. 客户端使用共享密钥解密数据
  6. SSL加密建立完成………

常见的加密算法:

  • 对称加密(加密和解密都使用相同密钥):DES,3DES
  • 非对称加密(加密和解密使用不同密钥): RSA,DSA(数字签名用)
  • Hash算法:MD5,SHA,SHA-1

7. 虚电路和数据包的区别?
虚电路数据包
思路可靠通信应当由网络来保证可靠通信应当由用户主机来保证
连接的建立必须有不要
目的站地址尽在连接建立阶段使用,每个分组使用较短的虚电路号每个分组都有目的站的全地址
分组的转发属于同一条虚电路的分组按照同一路由进行转发每个分组独立选择路由进行转发
当节点出故障时所有通过出故障节点的虚电路均不能工作故障节点可能丢失分组,一些路由可能会发生变化
分组的顺序总是按发送顺序到达目的地到达目的地时不一定按发送顺序
端到端的差错处理和流量控制可以由分组交换网负责也可以由用户主机负责由用户主机负责

8. 分层次的路由选择协议?

互联网采用分层次的路由选择协议,原因是:

  • 互联网的规模非常大。如果让所有的路由器知道所有的网络应怎样到达,则这种路由表将非常大,处理起来也太花时间
  • 许多单位不愿意外界了解自己单位网络的布局细节和本部门所采用的路由选择协议(这属于本部门内部的事情),但同时还希望连接到互联网上

路由选择协议可以分为两大类:

  • 内部网关协议IGP:在一个自治系统内部使用的路由选择协议,如RIP和OSPF等
  • 外部网关选择协议BGP:用于将路由选择信息传递给另一个自治系统中,如BGP-4

RIP:一种分布式的、基于距离度量的路由选择协议。这里的距离也称跳数,每经过一个路由器跳数加一。

RIP协议特点

  • 仅和相邻路由器交换信息
  • 交换的信息是当前本理由所知道的全部信息,即自己的路由表
  • 按固定的时间间隔交换路由信息
  • 好消息传的快、坏消息传的慢

OSPF:开放最短路径优先协议,特点如下:

  • 向本自治系统中所有路由器发送信息,这里使用的方法是洪泛法

  • 发送的信息就是与本路由器相邻的所有路由器的链路状态,但这只是路由器所知道的部分信息> 链路状态”就是说明本路由器都和哪些路由器相邻,以及该链路的“度量”(metric)

  • 只有当链路状态发生变化时,路由器才用洪泛法向所有路由器发送此信息

  • OSPF 的更新过程收敛得快

外部网关协议 BGP:外部网关协议,BGP 是不同自治系统的路由器之间交换路由信息的协议。互联网的规模太大,使得自治系统之间路由选择非常困难。对于自治系统之间的路由选择,要寻找最佳路由是很不现实的。因此,边界网关协议BGP只能是力求寻找一条能够到达目的网络且比较好的路由(不能兜圈子),而并非要寻找一条最佳路由。


9. TCP拥塞控制机制?

拥塞控制:防止过多的的数据注入到网络,不至于使网络中的路由器或链路过载。拥塞控制是一个全局性的行为,它涉及到所有的主机、所有的路由器,以及与降低网络传输有关的所有因素。常用的方法:

  • 慢启动:减少主机发送到网络中的分组数,使路由器有足够的时间把队列中积压的分组处理完毕
  • 快重传,快恢复:减少因为拥塞导致数据包丢失带来的重传时间,从而避免传递无用的数据到网络

慢启动算法:

慢启动为TCP发送发维护一个拥塞窗口(cwnd,以字节为单位),该窗口与接收窗口共同决定了发送者的发送窗口,swnd = min(cwnd, rwnd),也就是拥塞窗口和接收窗口中的最小值

拥塞窗口初始化为1字节的报文段,当收到确认时,发送2字节的报文段。若再收到2字节的报文,就发送4字节的报文,依次下去,当达到满开始门限时,改用拥塞避免算法。

拥塞窗口是发送方使用的流量控制,接收窗口是接收方使用的流量控制。慢启动的“慢”并不是指cwnd的增长速度慢,而是指在TCP开始发送报文段时先设置cwnd=1,使得发送方在开始时只发送一个报文段(目的是探测一下网络的拥塞情况),然后再逐渐增大cwnd。


拥塞避免算法

慢启动算法会设定一个门限值ssthresh(slow start threshold):

  • cwnd < ssthresh 时,使用慢启动算法
  • cwnd >= ssthresh 时,就会使用「拥塞避免算法」

当使用拥塞避免算法时,拥塞窗口的变化就以线性规律进行增长,即每次只增大一个窗口,知道网络发生拥塞。


拥塞发生算法

当网络发生拥塞就会出现丢包,此时的重传机制有超时重传和快重传两种。超时重传是一段时间后没有收到该数据对应ACK,就重新发送数据。如果使用超时重传,对应的拥塞发生算法操作为:

  • 将慢开始的门限值ssthresh设置为当前拥塞窗口的一半
  • 拥塞窗口立刻降为1
  • 接着执行慢开始算法

快重传就是发送方接收到3次以上的重复ACK,就重新发送数据,而不需要等到超时。此时,发送发执行快恢复算法。快恢复算法也会将将慢开始的门限值ssthresh设置为当前拥塞窗口的一半,拥塞窗口设置为当前的门限值大小。也有的实现将拥塞窗口设置为如下的形式:

  • 拥塞窗口设置为 cwnd = ssthresh + 3 ( 3 的意思是确认有 3 个数据包被收到了),然后重传丢失的数据包
  • 如果再收到重复的 ACK,那么 cwnd 增加 1
  • 如果收到新数据的 ACK 后,设置 cwnd 为 ssthresh,接着就进入了拥塞避免算法


10. 为什么说UDP是面向报文的的,而TCP是面向字节流的?

发送方的UDP对应用程序交下来的报文,在添加首部后就向下交付IP层。UDP对应用层交下来的报文既不合并也不拆分,而是保留这些报文的边界,即应用层交给UDP多长的报文,UDP都照样发送,一次发送一个报文。在接收方的UDP中对IP层交上来的UDP用户数据报,在去除首部后就原封不动的交付上层的应用进程,即UDP一次交付一个完整的报文。

TCP中的”流”指的是流入到进程和从进程流出的字节序列。 “面向字节流”的含义时:虽然应用程序和TCP的交换是一次一个数据块(大小不等),但TCP把应用程序交下来的数据看成仅仅是一连串的无结构的字节流。TCP并不知道所传送的字节流的含义。 TCP并不保证接收方应用程序所收到的数据块和发送方应用程序发送的数据块具有对应的大小关系,但是接收方应用程序收到的字节流必须和发送方应用程序发出的字节流完全一样。


11. 流量控制?

背景:发送方的速率与接收方的速率是不一定相等,如果发送方的发送速率太快,会导致接收方处理不过来,这时候接收方只能把处理不过来的数据存在缓存区里(失序的数据包也会被存放在缓存区里)。如果缓存区满了发送方还在疯狂着发送数据,接收方只能把收到的数据包丢掉,大量的丢包会极大着浪费网络资源。因此,我们需要控制发送方的发送速率,让接收方与发送方处于一种动态平衡才好。对发送方发送速率的控制,我们称之为流量控制。

做法: 接收方每次收到数据包,可以在发送确定报文的时候,同时告诉发送方自己的缓存区还剩余多少是空闲的,我们也把缓存区的剩余大小称之为接收窗口大小,用变量win来表示接收窗口的大小。发送方收到之后,便会调整自己的发送速率,也就是调整自己发送窗口的大小,当发送方收到接收窗口的大小为0时,发送方就会停止发送数据,防止出现大量丢包情况的发生。


12. 什么是糊涂窗口综合症?

TCP接收方的缓存已满,而交互式的应用进程一次只从接收缓存中读取1字节(这样就使接收缓存空间仅腾出1字节),然后向发送方发送确认,并把窗口设置为1个字节(但发送的数据报为40字节的的话)。接收,发送方又发来1个字节的数据(发送方的IP数据报是41字节)。接收方发回确认,仍然将窗口设置为1个字节。这样,网络的效率很低。

要解决这个问题,可让接收方等待一段时间,使得或者接收缓存已有足够空间容纳一个最长的报文段,或者等到接收方缓存已有一半空闲的空间。只要出现这两种情况,接收方就发回确认报文,并向发送方通知当前的窗口大小。此外,发送方也不要发送太小的报文段,而是把数据报积累成足够大的报文段,或达到接收方缓存的空间的一半大小。


13. TCP中的Nagle算法是什么?

若发送方应用进程把要发送的数据逐个字节地送到TCP的发送缓存,则发送方就把第一个字节先发送出去,把后面到达的数据字节都缓存起来。当发送方收到对第一个数据字符的确认后,再把发送缓存中的所有数据组装成一个报文段发送出去,同时继续对随后到达的数据进行缓存。只有在收到对前一个报文段的确认后才继续发送下一个报文段。当数据到达较快而网络速率缓慢时,用这种方法可明显减少所用的网络带宽。

Nagle算法还规定,当到达的数据已达到发送窗口大小的一半或者报文段的最大长度时,就立即发送一个报文段。这样做,可以有效提高网络的吞吐量。


14. TCP中的粘包问题是如何出现的?发送方和接收方又是分别如何解决的?

在流传输中出现,UDP不会出现粘包,因为它有消息边界。出现粘包的原因有:

  • 要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包
  • 接受数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包

解决办法:

对于短连接的TCP分包不是问题,对于长连接的TCP分包有如下四种方式:

  • 消息长度固定,比如muduo的roundtrip示例就采用了固定的16字节消息
  • 使用特殊的字符或字符串作为消息的边界,例如HTTP协议的headers以”\r\n”为字段的分隔符
  • 在每条消息的头部加一个长度字段,这恐怕是最常见的做法
  • 利用消息本身的格式来分包

15. ipv4到ipv6的过渡手段?
  1. 双栈策略:(最直接方式)在IPv6结点中加入IPv4协议栈。这种具有双协议栈的结点称作“IPv6/v4结点”,这些结点可以使用IPv4与IPv4结点互通,也可以直接使用IPv6与IPv6结点互通
  2. 隧道技术:(为解决局部纯IPv6网络与IPv4骨干隔离形成的孤岛问题,用隧道技术的方式解决)利用穿越现存IPv4互联网的隧道技术将孤岛连接起来,逐步扩大IPv6的实现范围。在隧道的入口处,路由器将IPv6的数组分组封装进入IPv4中,IPV4分组的源地址和目的地址分别是隧道入口和出口的IPV4地址。在隧道的出口处再将IPV6分组取出转发给目的节点。隧道技术在实践中有四种具体形式:构造隧道、自动配置隧道、组播隧道以及6to4
  3. 隧道代理TB,Tunnel Broker:(目的是简化隧道的配置,提供自动的配置手段),TB可以看作是一个虚拟的IPv6 ISP,它为已经连接到IPv4网络上的用户提供连接到IPv6网络的手段,而连接到IPv4网络上的用户就是TB的客户
  4. 协议转换技术:其主要思想是在V6节点与V4节点的通信时需借助于中间的协议转换服务器,此协议转换服务器的主要功能是把网络层协议头进行V6/V4间的转换,以适应对端的协议类型
  5. SOCKS64:在客户端里引入SOCKS库,它处于应用层和socket之间,对应用层的socket API和DNS域名解析API进行替换。另一种是SOCKS网关
  6. 传输层中继 :与SOCKS64的工作机理相似,只不过是在传输层中继器进行传输层的“协议翻译”
  7. 应用层代理网关(ALG):在应用层进行协议翻译

16. TIME_WAIT的意义?

TIME_WAIT存在于TCP连接释放过程,在服务器A收到服务器B发送的FIN+ACK后,会向B发送ACK,进入到TIME_WAIT阶段,等待2MSL(MSL:Max Segment Lifetime,最长报文段寿命,报文段在网络中能够存活的最长时间)。

意义:

  • 可靠地实现TCP全双工连接的终止:为了保证A发送的最后一个ACK报文段能够到达B。A给B发送的ACK可能会丢失,B收不到A发送的确认,B会超时重传FIN+ACK报文段,此时A处于2MSL时间内,就可以收到B重传的FIN+ACK报文段,接着A重传一次确认,重启2MSL计时器。最后A和B都能够正常进入到CLOSED状态。如果A在发完ACK后直接立即释放连接,而不等待一段时间,就无法收到B重传的FIN+ACK报文段,也就不会再次发送确认报文段,这样B就无法按照正常步骤进入CLOSED状态。
  • 允许旧的报文段在网络中消逝:A发送确认后,该确认报文段可能因为路由器异常在网络中发生“迷途”,并没有到达B,该确认报文段可以称为旧的报文段。A在超时后进行重传, 发送新的报文段,B在收到新的报文段后进入CLOSED状态。在这之后,发生迷途的旧报文段可能到达了B,通常情况下,该报文段会被丢弃,不会造成任何的影响。但是如果两个相同主机A和B之间又建立了一个具有相同端口号的新连接,那么旧的报文段可能会被看成是新连接的报文段,如果旧的报文段中数据的任何序列号恰恰在新连接的当前接收窗口中,数据就会被重新接收,对连接造成破坏。为了避免这种情况,TCP不允许处于TIME_WAIT状态的连接启动一个新的连接,因为TIME_WAIT状态持续2MSL,就可以保证当再次成功建立一个TCP连接的时,来自之前连接的旧的报文段已经在网络中消逝,不会再出现在新的连接中。

17. HTTP状态码?
  • 1XX Informational(信息性状态码) 接受的请求正在处理
  • 2XX Success(成功状态码) 请求正常处理完毕
    • 200 请求已正常处理
    • 204 请求处理成功,但是没有资源返回
    • 206 客户端进行了范围请求,而服务器成功执行了这部分的GET请求
  • 3XX Redirection(重定向状态码) 需要进行附加操作以完成请求
    • 301 永久性重定向
    • 302 临时性重定向,表示请求的资源已被分配了新的URI,希望用户能使用新的URI访问
    • 303 表示请求对应的资源存在着另外一个URI,应使用GET方法定向获取请求的资源
    • **304 **服务器资源未改变,可直接使用客户端未过期的缓存
  • 4XX client error(客户端错误状态码) 服务器无法处理请求
    • 400 表示请求报文中存在语法错误
    • 401 表示发送的请求需要有通过HTTP认证的认证信息
    • 404 服务器没有请求的资源
  • 5XX server error(服务器错误状态码)服务器处理请求
    • 500 表示服务器端在执行请求时发生了错误
    • 503 表示服务器处于超负荷或正在进行停机维护
18. 浏览器中通过URL访问到显示页面的过程中使用了哪些协议?

整个过程如下所示:

  • NDS解析:DNS协议,获取域名对应的ip地址
  • TCP连接:客户端可服务器之间需要建立TCP连接才能进行传输
  • 发送HTTP请求:使用HTTP协议访问网页
  • 服务器处理请求并返回HTTP报文
  • 浏览器解析渲染页面
  • 连接断开

建立TCP连接后,发送数据需要在网络层使用IP协议;IP数据包在路由间进行路由选择需使用OSPF协议;ip地址和MAC地址之间的转换需要使用ARP协议。


19. cookie和session有什么区别?

Cookie和Session都是客户端与服务器之间保持状态的解决方案,具体来说,cookie机制采用的是在客户端保持状态的方案,而session机制采用的是在服务器端保持状态的方案。

Cookie实际上是一小段的文本信息。客户端请求服务器,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie,而客户端浏览器会把Cookie保存起来。当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器,服务器检查该Cookie,以此来辨认用户状态。服务器还可以根据需要修改Cookie的内容。

客户端请求服务器,如果服务器记录该用户状态,就获取Session来保存状态,这时,如果服务器已经为此客户端创建过session,服务器就按照sessionid把这个session检索出来使用;如果客户端请求不包含sessionid,则为此客户端创建一个session并且生成一个与此session相关联的sessionid,并将这个sessionid在本次响应中返回给客户端保存。保存这个sessionid的方式可以采用 cookie机制 ,这样在交互过程中浏览器可以自动的按照规则把这个标识发挥给服务器;若浏览器禁用Cookie的话,可以通过 URL重写机制 将sessionid传回服务器。

Cookie机制是通过检查客户身上的“通行证”来确定客户身份的话,那么Session机制就是通过检查服务器上的“客户明细表”来确认客户身份。Session相当于程序在服务器上建立的一份客户档案,客户来访的时候只需要查询客户档案表就可以了。

总体来说,两者的区别在于:

  • 实现机制:session实现依赖于cookie,通过Cookie机制回传SessionID
  • 大小限制:Cookie有大小限制并且浏览器对每个站点也有cookie的个数限制,Session没有大小限制,理论上只与服务器的内存大小有关;
  • 安全性:Cookie存在安全隐患,通过拦截或本地文件找得到cookie后可以进行攻击,而Session由于保存在服务器端,相对更加安全;
  • 服务器资源消耗:Session是保存在服务器端上会存在一段时间才会消失,如果session过多会增加服务器的压力

cookie和session的详解和区别


20. HTTP1.0和HTTP1.1的主要区别有哪些?
  • 长连接:HTTP1.0中默认使用短链接,每次请求都需要重新建立一次连接,连接的建立和释放开销较大。HTTP1.1中默认使用长连接,表现为header中设置Connection:keep-live,长连接有如下两种形式:
    • 非流水线方式:客户端收到前一个响应后才能发送下一个请求
    • 流水线方式:客户端在收到HTTP的响应报文之前就能接着发送新的请求
  • 错误状态响应码:HTTP1.1中新增了24个错误状态响应码,如409表示请求的资源和当前状态发生冲突
  • 缓存处理:HTTP1.0中主要使用header中的If-Modified-Since来作为缓存判断的标准,HTTP1.1中引入了更多缓存控制策略,例如E-tag、If-Unmodified-Since、If-Match、If-None-Match等更多可供选择的缓存头来控制缓存策略
  • 带宽优化及网络连接的使用:HTTP1.0中存在一些浪费带宽的线程,HTTP1.1在请求头中引入了range头域,它允许只请求资源的某个部分,即返回码206(Partial Content)

21. 重传机制有哪些?

TCP/IP体系中常见的重传机制有:、

  • 超时重传
  • 快速重传
  • SACK
  • D-SACK

超时重传

发送方发送数据时,设定一个定时器,当超过指定的时间后仍然没有收到接收方的ACK确认报文后,就会重发该数据。

而导致发送方没有正常收到ACK确认有两种可能的情况 :

  • 接收方没有收到发送的数据
  • 接收方发送的确认报文丢失

超时重传时间(Retransmission Timeout ,RTO)应该略大于报文往返 RTT 的值。如果RTO设置的较大,重发太慢,但是网络的传输效率下降;如果RTO设置的较小,可能会导致不必要的重传,增加网络拥塞发生的可能性,反而会导致更多的超时重传发生。

由于网络的传输情况是动态变化的,使得往返时间RTT也是动态发生相应的变化。因此,RTO的值也应该是一个根据网络情况动态变化的值。

如果超时重发的数据,再次超时的时候,又需要重传的时候,TCP 的策略是超时间隔加倍。即每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。

快速重传

快速重传(Fast Retransmit)是一种以数据为驱动的重传机制,只要发送方连续三次收到同样的ACK确认报文,就立刻进行重传,而不必等到超时重传的时间是否到了。


如上所示,发送方收到3个连续的ACK 2,表示接收方没有正常收到报文2。因此,发送方会立即重传报文2。快重传可以避免超时时间设置不合理导致的一些问题,但是它仍然面临其他的问题。当发送方执行重传时,是重传之前的一个,还是重传所有。比如对于上面的例子,是重传 Seq2 呢?还是重传 Seq2、Seq3、Seq4、Seq5 呢?因为发送端并不清楚这连续的三个 Ack 2 是谁传回来的。

SACK

选择性确认( Selective Acknowledgment,SACK )需要在TCP头部加一个SACK字段,他可以将缓存的确认报文发送给发送方,这样发送方就知道了哪些数据已经收到,而哪些数据没有收到。知道了这些信息后,发送发只会重传接受方没有收到的数据。

如下图,发送方收到了三次同样的 ACK 确认报文,于是就会触发快速重发机制,通过 SACK 信息发现只有 200~299 这段数据丢失,则重发时,就只选择了这个 TCP 段进行重复。


Duplicate SACK

它主要使用了SACK告知发送发哪些数据被重复的接收了,如下所示:


  • 接收方发给发送方的两个 ACK 确认应答都丢失了,所以发送方超时后,重传第一个数据包(3000 ~ 3499)
  • 于是接收方发现数据是重复收到的,于是回了一个 SACK = 3000~3500,告诉发送方 3000~3500 的数据早已被接收了,因为 ACK 都到 4000 了,已经意味着 4000 之前的所有数据都已收到,所以这个 SACK 就代表着 D-SACK
  • 这样发送方就知道数据没有丢,而是接收方的 ACK 确认报文丢了

22. 什么是中间人攻击?

中间人攻击(Man-in-the-middle attack,缩写:MITM)是指攻击者与通讯的两端分别创建独立的联系,并交换其所收到的数据,使通讯的两端认为他们正在通过一个私密的连接与对方直接对话,但事实上整个会话都被攻击者完全控制。在中间人攻击中,攻击者可以拦截通讯双方的通话并插入新的内容,达到HTTPS攻击的目的。


23. 什么是跨域?跨域如何解决?

跨域指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器施加的安全限制。所谓同源,指的是域名、协议和端口均相同。

解决办法有:

  • JSONP:只支持GET请求
  • 代理服务器
  • 修改Header:
header('Access-Control-Allow-Origin:*');//允许所有来源访问
header('Access-Control-Allow-Method:POST,GET');//允许访问的方式

24. GET和POST的区别?

GET和POST是两个HTTP协议的使用方式,底层都是依赖于TCP。因此,从某种意义上来说,两者可以认为是相同的。但细分的话,两者的区别主要在于:

  • GET一般用于获取资源,PSOT一般用于更新资源
  • GET是幂等的,相同的GET请求多次获取到的是想用的数据;PSOT不是幂等的,因为每次请求对于资源的改变并不相同

GET不会改变服务器的资源,PSOT会改变服务器的资源

  • GET请求的参数会附在URL之后,以?分隔URL和传输数据,参数之间使用&相连;POST请求会将数据放置在请求报文的request body中
  • GET请求提交的数据会以明文的形式出现在URL上,PSOT请求的数据则会被包装到请求体中,相对更加安全
  • GET请求的长度会受限于浏览器或服务器对于URL长度的限制,允许发送的数据量较小,而PSOT请求则没有大小限制

25.TCP如何保证可靠传输?

TCP是一种面向连接、可靠的字节流的传输方式,两个应用在传输数据之前必须先建立一条TCP连接,传输结束后需要释放连接。TCP主要通过如下的方式来保证可靠性:

  • 数据包校验:如果接收包收到的数据包出错,那么它会丢失并不给出响应;接收方没有在规定时间内收到响应,就会重复出错的数据包
  • 失序数据包的重排序:TCP数据包作为IP报文进行传输就可能会失序,接收方会将失序的数据包重排序后交给应用层
  • 重复数据的丢弃:接收方如果收到重复的数据包就会丢弃
  • 确认机制:接收方在收到数据包时,会向发送方发送一个确认
  • 超时重发:发送方在定时器结束后,如果没有收到对应数据包的确认,那么就需要重发这些数据包
  • 流量控制:TCP使用可变大小的滑动窗口协议来保证发送方和接收方发送和接收数据包的能力相当

26. 什么是DDOs攻击?如何预防?

TCP连接的建立需要三次连接的建立,如果客户端不断的发送建立连接的请求,但是不响应服务端返回的确认报文,就会导致服务端一致等待连接的建立,消耗大量的内存和CPU资源。

DDos攻击只能预防,主要有如下的预防方式:

  • 限制同时打开SYN半连接的数目
  • 缩短SYN半连接的Time out时间
  • 关闭不必要的服务

操作系统


1. 线程间的同步方法有哪些?

线程同步是两个或多个共享关键资源的线程的并发执行,应该同步线程以避免关键的资源使用冲突。操作系统中有三种同步方式:

  • 互斥锁(mutex):采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限
  • 信号量(semphares):允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问资源的最大线程数数量。常用的信号量有两种:
    • 二进制信号量:它只有0和1两种取值。适用于临界区每次只能被一个执行线程运行,就要用到二进制信号量
    • 计数信号量:它可以有更大的取值范围,适用于临界区允许有限数目的线程执行,就需要用到计数信号量
  • 事件(event):wait/notify,通过通知操作的方式来保持多线程同步,还可以方便的实现多线程的比较操作
  • 条件变量:条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。适合多个线程等待某个条件的发生,通常条件变量和互斥锁同时使用
  • 读写锁:它有加读锁、加写锁和不加锁三种状态,适合于读请求远多于写请求的场景,并发能力能强。它对应的加锁规则是:线程加了读锁,其他的线程只能再申请加读锁,不能加写锁;线程加了写锁,其他的线程既不能加读锁,也不能加写锁
  • 自旋锁:当锁被其他线程占有时,获取锁的线程便会进入自旋,不断检测自旋锁的状态,直到得到锁。自旋操作会占用CPU资源,适合于临界区代码比较短、锁的持有时间比较短的场景
2. 内存管理机制有哪些?

操作系统中的内存管理机制可以分为连续分配管理和非连续分配管理,如下所示:

  • 连续分配管理:为一个用户程序分配一个连续的内存空间,如块式管理,将内存分为几个固定大小的块,每块中只包含一个进程,会有碎片问题
  • 非连续分配管理:允许一个程序使用的内存分布在离散的内存中,如:
    • 页式管理:将内存分为大小相等且固定的页,页较小,更能提高内存空间的利用率,较少了碎片产生的概率。页式管理通过页表来实现物理地址和逻辑地址之间的映射
    • 段式管理:将内存空间分为段的形式,段有实际意义,每段定义了一组逻辑信息。段式管理通过段表实现物理地址和逻辑地址之间的映射
    • 段页式管理:结合了段氏和页式管理的优点,将内存空间先分段,每个段又分成若干页,段与段之间以及段内部都是离散的

3. 进程间通信的方式有哪些?线程间通信方式有哪些?

由于不同的进程地址空间不同,因此,进程之间数据是不能共享的。常用的进程间通信(IPC,InterProcess Communication)方式有如下几种:

  • 管道:它是一个内核缓冲区,进程使用FIFO的方式半双工的对管道执行数据的存取操作,数据只能单向流动,而且只能在具有父子进程之间通信
  • FIFO有名管道:它提供了一个路径名与管道所关联,以文件形式存在于文件系统中,只要知道了路径的进程之间都可以进程通信。通信可以是单向的,也可以是双向的
  • 信号,Signal:主要用于Linux系统中,它可以在任意时刻发送给某一个进程,而无需知道该进程的状态,如果当前进程不是执行态,内核会暂时保存信号,当进程恢复执行后传递给它。信号在用户进程和内核之间进程之间直接交互,内核可以利用信息来通知用户空间的进程发生了哪些系统事件,信号事件主要有两个来源:
    • 硬件来源:用户按键输入Ctrl+C退出、硬件异常如无效的存储访问等
    • 软件终止:终止进程信号、其他进程调用 kill 函数、软件异常产生信号
  • 消息队列:消息队列是存放在内核中的消息链表,每个消息队列由消息队列标识符表示, 只有在内核重启或主动删除时,该消息队列才会被删除
  • 共享内存:共享内存是一个进程把地址空间的一段,映射到能被其他进程所访问的内存,一个进程创建、多个进程可访问,进程就可以直接读写这一块内存而不需要进行数据的拷贝,从而大大提高效率
  • 套接字,Socket:它可以在本机进程间通信,也可以通过网络接口将数据发送到本机的不同进程或远程计算机的进程实现跨网络的通信

一文讲懂进程间通信的几种方式

线程间的通信方式有:

  • volatile关键字
  • Object类的wait方法和notify方法
  • ReentrantLock结合Condition的await方法和signal方法
  • 信号量Semaphore的accquire方法和release方法
  • CountDownLatch、CyclicBarrier
  • LockSupport的park方法和unpark方法

4. 什么是协程?

协程(coroutines)是一种比线程更加轻量级的微线程,一个线程可以有多个协程。


协程在线程内实现,不存在多线程的抢占资源和资源同步的问题;协程创建和切换的开销比线程小的多。

一文讲透 “进程、线程、协程”


5. 实时系统的特征是什么?

提供及时响应和高可靠性是实时操作系统主要特点。实时操作系统有硬实时和软实时之分:

  • 硬实时要求在规定的时间内必须完成操作,由操作系统设计时保证
  • 软实时则只要按照任务的优先级,尽可能快地完成操作即可

6. 数据库以及线程发生死锁的必要条件是什么?
  • 互斥条件:一个资源每次只能被一个进程使用
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
  • 不可剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺,只能等待持有资源的进程自己释放
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系

MySQL


1. MyISAM和InnoDB的区别?
  • MyISAM只有表锁,而InnoDB支持行锁和表锁,默认为行锁
  • MyISAM强调性能,每次查询都具有原子性,其执行速度比InnoDB更快,但是不提供事务操作支持。InnoDB提供事务支持、外键等高级数据库功能,具有事务提交、事务回滚和崩溃恢复能力
  • MyISAM不支持外键,InnoDB支持外键
  • MVCC仅InnoDB支持,用来应对高并发事务,MVCC只在READ COMMITED和REPEATABLE READ两个隔离级别下工作,MVCC可以使用乐观锁和悲观锁实现
特性MyISAMInnodb
存储结构每张表被存放在三个文件:frm-表格定义、MYD(MYData)-数据文件、MYI(MYIndex)-索引文件所有的表都保存在同一个数据文件中(也可能是多个文件,或者是独立的表空间文件),InnoDB表的大小只受限于操作系统文件的大小,一般为2GB
存储空间MyISAM可被压缩,存储空间较小InnoDB的表需要更多的内存和存储,它会在主内存中建立其专用的缓冲池用于高速缓冲数据和索引
可移植性、备份及恢复由于MyISAM的数据是以文件的形式存储,所以在跨平台的数据转移中会很方便。在备份和恢复时可单独针对某个表进行操作免费的方案可以是拷贝数据文件、备份 binlog,或者用 mysqldump,在数据量达到几十G的时候就相对痛苦了
文件格式数据和索引是分别存储的,数据.MYD,索引.MYI数据和索引是集中存储的,.ibd
记录存储顺序按记录插入顺序保存按主键大小有序插入
外键不支持支持
事务不支持支持
锁支持(锁是避免资源争用的一个机制,MySQL锁对用户几乎是透明的)表级锁定行级锁定、表级锁定,锁定力度小并发能力高
SELECTMyISAM更优
INSERT、UPDATE、DELETEInnoDB更优
select count(*)myisam更快,因为myisam内部维护了一个计数器,可以直接调取。
索引的实现方式B+树索引,myisam 是堆表B+树索引,Innodb 是索引组织表
哈希索引不支持支持
全文索引支持不支持

2. 索引有哪些?聚簇索引是什么?什么是覆盖索引?索引有哪些优点?

MySQL中索引主要有BTree索引和哈希索引:

  • 哈希索引:底层实现就是哈希表。因此,当需求为单条记录查询时,哈希索引更适合,查询速度更快
  • BTree索引:MySQL中的BTree索引使用的是B+Tree,不同的引擎之间的区别在于:
    • MyISAM:BTree叶节点的data域中存储的是数据记录的地址,索引检索时,首先按照B+Tree搜索算法搜索索引,如果指定的key存在,则取出其data域的值,然后以data域的值为地址读取相应的数据记录,称为非聚簇索引。此时,索引文件和数据文件是分离的
    • InnoDB:数据文件本身就是索引文件,表数据文件本身就是主索引,称为聚簇索引。而其余的索引都作为辅助索引,辅助索引的data域中存储相应记录主键的值而不是地址。根据主索引搜索时,直接找到key所在的节点即可取出数据;而根据辅助索引查找时,则需要先取出主键的值,再走一遍主索引

对于一次查询而言,建立索引的字段正好是覆盖该查询语句与查询条件中所涉及的字段,那么就呈这一次查询使用了覆盖索引。聚簇索引和非聚簇索引中都可以使用覆盖索引。

索引的优点如下:

  • 大大减少数据库服务器需要扫描的数据行数
  • 帮助服务器避免进行排序和分组,以及避免创建临时表

B+Tree 索引是有序的,可以用于 ORDER BY 和 GROUP BY 操作。临时表主要是在排序和分组过程中创建,不需要排序和分组,也就不需要创建临时表

  • 将随机IO变成顺序IO

B+Tree是有序的,它会将相邻的数据都存储在一起


3. 事务的四大特性是什么?

数据库事务的ACID特性又称为强一致性,分别为:

  • 原子性(Atomicity):事务是最小的执行单位,不允许分隔,事务的原子性确保动作要么全部完成,要么完全不起作用
  • 一致性(Consistency):执行事务前后数据保持一致,多个事务对同一个数据读取的结果是相同的
  • 隔离性(Isolation):并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的
  • 持久性(Durability):一个事务被提交之后,它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响

4. 事务的ACID靠什么保证?
  • 原子性由undo log日志保证,它记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的sql
  • 一致性一般由代码层面保证
  • 隔离性由MVCC保证
  • 持久性靠redo log + 内存保证,MySQL修改数据同时在内存和redo log记录这次操作,事务提交的时候通过redo log刷盘,宕机的时候可以从redo log中恢复记录

undo log用于存放数据修改被修改前的值,对数据的变更操作,主要来自 INSERT、UPDATE和DELETE操作,而UNDO LOG中分为两种类型,一种是 INSERT_UNDO(INSERT操作),记录插入的唯一键值;一种是 UPDATE_UNDO(包含UPDATE及DELETE操作),记录修改的唯一键值以及old column记录。

当数据库对数据做修改的时候,需要把数据页(data page)从磁盘读到buffer pool中,然后在buffer pool中进行修改。修改期间buffer pool中的数据页就与磁盘上的数据页内容不一致,称buffer pool中的数据页为脏数据(dirty page)。如果此时数据库发生了非正常的服务重启,这些修改后的数据仍在内存当中,并没有同步到磁盘文件中(同步到磁盘文件是个随机IO),即会发生数据丢失**。redo log**用于记录buffer pool中对于数据页的修改,并且使用顺序记录。当数据库服务非正常的重启后,可以根据redo log中的记录来将内存中未来的及刷新的磁盘中的数据重新进行刷盘,从而保证数据的一致性。

详细分析MySQL事务日志(redo log和undo log)


5. 并发事务会有哪些问题:
  • 脏读:当一个事务正在访问数据并且对数据进行了修改,但修改还没有提交的数据库中。此时另一个事务也访问了该数据,然后进行了使用。但是该数据是一个还没有提交的数据,因此称该数据为脏数据,读取脏数据的过程称为脏读
  • 丢失修改:两个事务都访问同一个数据,并且各自对数据进行了修改,那么后提交的事务会将前一个事务修改的结果覆盖,称为丢失修改
  • 不可重复读:一个事务多次读同一个数据,在这个事务还没有提交时,另一个数据也访问了该数据。那么,在第一个事务的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的结果不一致,称为不可重复读
  • 幻读:一个事务读取了几行数据,接着另一个并发事务插入了一些数据。在随后的查询中,第一个事务就会发现多了一些原本不存在的记录,称为幻读

6. 事务的隔离级别有哪些?

MySQL中存在四个隔离级别:

  • 读未提交(READ_UNCOMMITTED):级别最低,允许读取尚未提交的数据变更,可能会导致脏读、幻读和不可重复读出现
  • 读已提交(READ-COMMITTED):允许读取并发事务已经提交的数据,可以避免脏读,不能避免幻读和不可重复读
  • 可重复读(REPEATABLE-READ):对同一字段的多次读取结果一致,除非数据是被本身事务自己所修改,可以避免脏读和不可重复读,不能避免幻读
  • 可串行化(SERIALIZABLE):级别最高,完全服从ACID隔离级别,所有的事务逐个执行,事务之间不可能产生干扰,脏读、幻读和不可重复读都可以避免

InnoDB默认支持的隔离级别是可重复读。不同的隔离级别可能会导致哪些事务操作问题的出现,如下所示:

隔离级别脏读不可重复读幻读
读未提交YesYesYes
读已提交NoYesYes
可重复读NoNoYes
可串行化NoNoNo

7. 表锁和行锁有什么区别?

MyISAM默认支持表锁,InnoDB默认支持行锁,当然也支持表锁,它们之间的区别在于:

  • 表锁:粒度最大,它会对当前操作的整张表进行加锁操作,实现简单、资源消耗少、加锁快,不会出现死锁。但是,由于加锁的粒度太大,会使得锁冲突的概率较高,并发能力差
  • 行锁:粒度最小,它只针对当前操作的行加锁,大大的减少了数据库操作的冲突。加锁粒度小,并发度高,但是加锁的开销最大,导致加锁慢,可能会出现死锁
8. InnoDB锁的算法有哪些?
  • Record Lock:单个记录上的锁
  • Gap Lock:间隙锁,锁定一个范围,不包括记录本身
  • Next-key Lock:锁定一个范围,包含记录本身

9. 大表的优化手段有哪些?
  • 限定数据的范围:查询时禁止不带任何限制数据范围条件的查询语句
  • 读写分离:MySQL实现主从复制架构,主库负责写,从库负责读
  • 垂直分区:根据数据库中数据表的相关性进行拆分,将一张列较多的表拆分为多张表。它可以使得列数据变小,查询时减少读取的block数,减少IO次数;但是主键会出现冗余,需要管理冗余列,并会引起join操作。另外,它会使得事务操作困难
  • 水平分区:分表,保持数据表结构不变,同通过某种策略存储数据分片,每一篇分散到不同的表或者库中,达到了分布式的目的。但分片事务难以解决,尽量不要使用分片操作

10. 分表后的ID如何保证唯一性?
  • 设定步长:例如1 ~ 1024张表可以分别设定步长为1 ~ 024的基础步长,这样主键落到不同的表上就不会有冲突
  • 分布式ID:自定义一套ID生成算法或者使用开源的算法
  • 分表后可以不再使用主键作为查询依据,而是每张表单独新增一个字段作为唯一主键使用,例如,订单表可以使用订单号,订单号是唯一的,因此由订单号查询的结果也是唯一的

11. MySQL中的主从复制流程是怎样的?

MySQL中主从同步的流程如下所示:

  • master提交完事务后,写入binlog
  • slave连接到master,获取binlog
  • master创建dump线程,推送binlog到slave
  • slave启动一个IO线程读取同步过来的master的binlog,记录到relay log中继日志中
  • slave再开启一个sql线程读取relay log事件并在slave执行,完成同步
  • 同步过程中,slave也会记录自己的binlog

对应的同步方式有全同步复制和半同步复制,区别在于master是否要等到slave全部同步完毕,返回确认信息后再进行下面的操作。


12. 什么时候不要用索引?
  • 经常增删改的列不要建立索引:索引的作用是为了加快查询的速度,如果主要的操作是增删改,那么在执行SQL的过程中还需要维护索引结构,所造成的的性能开销有点得不偿失
  • 有大量重复的列不要建立索引:可以构建索引的列的字段最好有区分度,这样索引的查询效率更高。如果列中的内容有大量重复的地方,导致区分度不高,使得索引反而会影响查询的效率
  • 表记录太少不要建立索引:当表中记录很少时,往往在执行第一条SQL语句时就将数据全局加载到了内存中,后续的查询只会操作内存中的数据,而不会再用到索引

13. 哈希索引有什么不足之处?

hash索引底层就是hash表,进行查找时,调用一次hash函数就可以获取到相应的键值,之后进行回表查询获得实际数据。它具有如下的特点:

  • 等值查询的速度快,但是无法进行范围查询:某一个范围内的数据经过哈希函数可能会落到哈希表的不同位置,它们的位置大概率不再相邻
  • 不支持使用索引进行排序:哈希表中的数据并没有大小关系,只和哈希函数得到的哈希值分布有关
  • 不支持模糊查询及多列索引的最左前缀法则
  • 哈希索引每一次查询都需要回表
  • 可能会发生哈希碰撞,此时的查询效率很差

14. 联合索引是什么?为什么需要注意联合索引中的顺序?

MySQL可以使用多个字段同时建立一个索引,叫做**联合索引。**在联合索引中,如果想要命中索引,需要按照建立索引时的字段顺序挨个使用,否则无法命中索引。

具体原因为:MySQL使用索引时需要索引有序,假设现在建立了"name,age,school"的联合索引,那么索引的排序为: 先按照name排序,如果name相同,则按照age排序,如果age的值也相等,则按照school进行排序。

当进行查询时,此时索引仅仅按照name严格有序,因此必须首先使用name字段进行等值查询,之后对于匹配到的列而言,其按照age字段严格有序,此时可以使用age字段用做索引查找,以此类推。因此,在建立联合索引的时候应该注意索引列的顺序,一般情况下,将查询需求频繁或者字段选择性高的列放在前面。此外,可以根据特例的查询或者表结构进行单独的调整。


15. MySQL中binlog有哪几种格式?

binlog一共有三种格式:

  • Statement:记录每一条修改数据的SQL语句,日志量较小
  • row:记录每一行的改动,日志量大
  • mixed:混合方案,普通操作使用Statement记录,无法使用Statement时使用row

16. explain通常关注哪些信息?

explain与SQL语句一起使用可以用来显示来自优化器关于SQL执行的信息,其中包括如下信息:

  • 表的加载顺序
  • SQL的查询类型
  • 可能用到的索引、实际使用的索引
  • 表与表之间的关系

17. SQL优化的方向有哪些?
  • 合理创建索引
  • 合理编写SQL语句
  • 防止索引失效
    • 保证最左前缀法则
    • 尽量不要使用前缀模糊查询%like
    • 索引列不要参与计算或使用函数
    • 避免在where子句中对字段进行null值判断
    • 联合索引中范围查询会使后面的索引字段失效
    • join查询要用小表驱动大表
  • 合理创建表字段:尽量对字段使用NOT NULL进行填充

Redis


1. redis的优势
  • 速度快
  • 支持丰富的数据类型
  • 支持事务
  • 丰富的特性

2. redis中的持久化机制有哪些?各有什么特点?

Redis提供两种持久化机制 RDB 和 AOF 机制:

2.1 RDB

RDB(Redis DataBase):用数据集快照的方式半持久化模式记录 redis 数据库的所有键值对,在某个时间点将数据写入一个临时文件,持久化结束后,用这个临时文件替换上次持久化的文件,达到数据恢复。

优点:

  • 只有一个文件 dump.rdb,方便持久化
  • 容灾性好,一个文件可以保存到安全的磁盘
  • 性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 redis的高性能
  • 相对于数据集大时,比 AOF 的启动效率更高

缺点:

  • 数据安全性低:RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。

RDB的原理:fork和cow。fork是指redis通过创建子进程来进行RDB操作,cow指的是copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。

2.2 AOF

AOF(Append-only file)指所有的命令行记录以 redis 命令请求协议的格式完全持久化存储保存为 aof 文件。

优点

  • 数据安全,aof 持久化可以配置 appendfsync 属性,有always、every second、no,其中always表示每进行一次命令操作就记录到 aof 文件中一次
  • 通过append模式写文件,即使中途服务器宕机,可以通过 redis-check-aof工具解决数据一致性问题
  • AOF 机制的 rewrite 模式。AOF 文件没被 rewrite 之前(文件过大时会对命令进行合并重写),可以删除其中的某些命令(比如误操作的 flushall))

缺点

  • AOF 文件比 RDB 文件大,且恢复速度慢
  • 数据集大的时候,比 rdb 启动效率低

3. redis 过期键的删除策略?
  • 定时删除:在设置键的过期时间的同时,创建一个定时器, 让定时器在键的过期时间来临时,立即执行对键的删除操作
  • 惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键
  • 定期删除:每隔一段时间程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定

4. redis中淘汰机制有哪些?

Redis支持的淘汰机制有如下几种:

  • volatile-lru:当内存不足时,Redis会在过了生存时间的key中淘汰掉一个最近最少使用的key
  • allkeys-lru:当内存不足时,Redis会在全部的key中淘汰掉一个最近最少使用的key
  • volatile-lfu:当内存不足时,Redis会在过了生存时间的key中淘汰掉一个最近最少频次使用的key
  • allkeys-lfu:当内存不足时,Redis会在全部的key中淘汰掉一个最近最少频次使用的key
  • volatile-random:当内存不足时,Redis会在过了生存时间的key中随机淘汰掉一个key
  • allkeys-random:当内存不足时,Redis会在全部的key中随机淘汰掉一个key
  • volatile-ttl:当内存不足时,Redis会在过了生存时间的key中淘汰掉一个剩余生存时间最少的key
  • noviction(默认):当内存不足时,直接报错

通过maxmemory-policy 策略的命令来执行具体使用的淘汰机制。同时,还可以通过maxmemory 字节大小来设置redis的最大内存。


5.为什么redis将数据放入内存中?

Redis 为了达到最快的读写速度将数据都读到内存中,并通过异步的方式将数据写入磁盘。所以, redis 具有快速读写和数据持久化的特征。如果不将数据放在内存中,磁盘 I/O 速度为严重影响 redis 的性能。


6. Pipeline 有什么好处,为什么要用 pipeline?

可以将多次 IO 往返的时间缩减为一次,前提是 pipeline 执行的指令之间没有因果相关性。使用 redis-benchmark 进行压测的时候可以发现影响 redis 的 QPS峰值的一个重要因素是 pipeline 批次指令的数目。


7. Redis 哈希槽的概念?

Redis 集群没有使用一致性 hash,而是引入了哈希槽的概念,Redis 集群有16384个哈希槽,每个key 通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分 hash 槽。


8. 假如 Redis 里面有 1 亿个 key,其中有 10w 个 key 是以某个固定的已知的前缀开头的,如果将它们全部找出来?

使用 keys指令可以扫出指定模式的 key 列表。但是redis有一个关键的一个特性:redis 的单线程的。keys 指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的 key 列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。


9. 常见的缓存问题有哪些?
  • 缓存雪崩:redis中所有的key同时失效,使得所有用户的请求都打到了数据库上,造成数据库服务器的宕机。解决办法:
    • 在批量往Redis存数据的时候,把每个Key的失效时间都加个随机值就好了,这样可以保证数据不会再同一时间大面积失效
    • 使用高可用的分布式缓存集群,确保缓存的高可用性,比如redis-cluster。将热key平均的分不到不同的缓存节点上,避免单一节点的key同时失效
    • 使用定时任务定时的刷新缓存
  • 缓存击穿热key失效导致对于热key的请求都打到数据库服务器上,造成服务器宕机。解决办法有:
    • 可以设置热key永不失效
    • 使用互斥锁,当缓存中没有热key时,只有一个线程可以查询数据库获取到数据,同时更新缓存
  • 缓存穿透:指缓存和数据库中都没有的数据,而用户(黑客)不断发起请求,所有的请求一直打到数据库上,造成数据库服务器的宕机。解决办法有:
    • 在接口层增加校验,比如用户鉴权,参数做校验,不合法的校验直接返回,比如id做基础校验,id<=0直接拦截
    • 使用布隆过滤器快速判断出你这个Key是否在数据库中存在,不存在直接返回;如果存在,则查询数据库刷新缓存后再返回

10.redis采用单线程为什么还很快?
  • Redis完全基于内存,绝大部分请求是纯粹的内存操作,非常迅速,数据存在内存中,减少了IO的开销,大大的提升了响应时间
  • 数据结构简单,对数据操作也简单
  • 采用单线程,避免了不必要的上下文切换和竞争条件,不存在多线程导致的CPU切换,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有死锁问题导致的性能消耗
  • **非阻塞多路IO复用机制:**使用一个线程来检查多个Socket的就绪状态,在单个线程中通过记录跟踪每一个socket(IO流)的状态来管理处理多个IO流。IO多路复用模型利用select、poll和epoll函数可以同时监测多个IO流,这些函数会轮询一遍IO流,依次处理返回的数据,减少了网络IO的开销

11.redis中哨兵的作用是什么?

Redis 的 Sentinel 系统用于管理多个 Redis 服务器(instance), 该系统执行以下三个任务:

  • 监控(Monitoring): Sentinel 会不断地检查你的主服务器和从服务器是否运作正常
  • 提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知
  • 自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作, 它会将失效主服务器的其中一个从服务器升级为新的主服务器, 并让失效主服务器的其他从服务器改为复制新的主服务器; 当客户端试图连接失效的主服务器时, 集群也会向客户端返回新主服务器的地址, 使得集群可以使用新主服务器代替失效服务器

Redis Sentinel 是一个分布式系统, 你可以在一个架构中运行多个 Sentinel 进程(progress), 这些进程使用gossip协议来接收关于主服务器是否下线的信息, 并使用投票协议(agreement protocols)来决定是否执行自动故障迁移, 以及选择哪个从服务器作为新的主服务器。


12. 了解redis中的事务吗?

MULTI 、 EXEC 、 DISCARD 和 WATCH 是 Redis 事务相关的命令。事务可以一次执行多个命令, 并且带有以下两个重要的保证:

  • 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断
  • 事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行

EXEC 命令负责触发并执行事务中的所有命令:

  • 如果客户端在使用 MULTI 开启了一个事务之后,却因为断线而没有成功执行 EXEC ,那么事务中的所有命令都不会被执行
  • 另一方面,如果客户端成功在开启事务之后执行 EXEC ,那么事务中的所有命令都会被执行

MULTI 命令用于开启一个事务,它总是返回 OK 。 MULTI 执行之后, 客户端可以继续向服务器发送任意多条命令, 这些命令不会立即被执行, 而是被放到一个队列中, 当 EXEC命令被调用时, 所有队列中的命令才会被执行。另一方面, 通过调用 DISCARD , 客户端可以清空事务队列, 并放弃执行事务。


13. 为什么要用 redis 而不用 map/guava 做缓存?

缓存分为本地缓存和分布式缓存。以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。

使用 redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持 redis 或 memcached服务的高可用,整个程序架构上较为复杂。


14. 如何解决 Redis 的并发竞争 Key 问题?

所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同!推荐使用分布式锁,但如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能。


15. 什么是布隆过滤器?

布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。

哈希表通过一个Hash函数将一个元素映射成一个位阵列(Bit array)中的一个点。这样一来,我们只要看看这个点是不是1就可以知道集合中有没有它了。这就是布隆过滤器的基本思想。

Hash面临的问题就是冲突。假设Hash函数是良好的,如果我们的位阵列长度为m个点,那么如果我们想将冲突率降低到例如 1%, 这个散列表就只能容纳m / 100个元素。显然这就不叫空间效率了(Space-efficient)了。解决方法也简单,就是使用多个Hash,如果它们有一个说元素不在集合中,那肯定就不在。如果它们都说在,虽然也有一定可能性它们在说谎,不过直觉上判断这种事情的概率是比较低的。

布隆过滤器


16. Redis如何实现主从节点之间的同步?

Redis可以使用主从同步,从从同步。第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录到内存buffer,待完成后将RDB文件全量同步到复制节点,复制节点接受完成后将RDB镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。后续的增量数据通过AOF日志同步即可,有点类似数据库的binlog。

Redis基础


17. Redis的线程模型是什么?

Redis 内部使用文件事件处理器 file event handler,这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 Socket,根据 Socket 上的事件来选择对应的事件处理器进行处理。

文件事件处理器的结构包含 4 个部分:

  • 多个 Socket
  • IO 多路复用程序
  • 文件事件分派器
  • 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)

多个 Socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 Socket,会将 Socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。


18. Redis和Memcached有什么区别?
比较点MemcachedRedis
线程模型多线程单线程
数据结构仅支持string、value最大1M、过期时间不能超过30天string、list、hash、set、zset、geo、hyperLogLog
淘汰策略LRULRU、LFU、随机等多种策略
管道与事物不支持支持
持久化不支持支持
高可用不支持主从复制+哨兵
集群化客户端一致性哈希算法主从复制+哨兵+固定哈希槽位
  • Memcache采用多线程模型,并且基于IO多路复用技术,主线程接收请求后分发给子线程处理。线程的切换会带来性能的开销,多线程访问共享资源需要加锁等同步操作。Redis使用的是单线程模型,同样采用了IO多路复用技术,从接收请求到处理数据都在同一个线程进行。当某个请求耗时较长时,整个Redis都会被阻塞,直到请求处理完成并返回。

Redis是基于内存的数据库,它的性能瓶颈并不在CPU,而在于内存和网络带宽。

  • Memcached只支持String类型的操作,并且对value的大小限制在1M以下,过期时间不能超过30天。它只能整存整取,即将序列化的数据存在Memcached中,获取后再按照反序列化的方式恢复数据。Redis支持多种多样的数据结构,针对于不同的数据结构又提供了很多的操作方法
  • Memcached只支持LRU的淘汰机制,优先淘汰不常使用的数据。Redis支持多种淘汰策略:volatile-lru、allkeys-lru、volatile-lfu、allkeys-lfu、volatile-random、allkeys-random和volatile-ttl(优先淘汰最近要过期的key)
  • Redis支持pipeline功能,客户端一次性打包发送多条命令到服务端,服务端依次处理客户端发过来的命令,减少网络IO的次数,提高访问性能。Redis提供事务支持,一般事务会配合pipeline一块使用,客户端一次性打包发送多条命令到服务端,并且标识这些命令必须严格按顺序执行,不能被其他客户端打断。同时执行事务之前,客户端可以告诉服务端某个key稍后会进行相关操作,如果这个客户端在操作这个key之前,有其他客户端对这个key进行更改,那么当前客户端在执行这些命令时会放弃整个事务操作,保证一致性
  • Memcached不支持持久化,Redis提供了rdb和aof两种持久化操作方式
  • Memcached智能单点部署,不支持高可用。Redis提供了主从复制和哨兵模式,保证高可用
  • Memcached和Redis都是由多个节点组成集群对外提供服务,Memcached的集群化是在客户端采用一致性哈希算法向指定节点发送数据,当一个节点宕机时,其他节点会分担这个节点的请求。Redis集群化采用的是每个节点维护一部分虚拟槽位,通过key的哈希计算,将key映射到具体的虚拟槽位上,这个槽位再映射到具体的Redis节点。

JVM


1. Java虚拟机内存区域的划分?

线程私有的有本地方法栈、虚拟机栈和程序计数器;线程共享的有元空间(方法区)和堆空间。


2. 堆内存的分配机制有哪些?如何保证并发情况下的分配安全?

堆内存的分配方式主要有两种,根据内存空间是否规整可分为:

  • 指针碰撞:如果可用和已用内存空间规整,那么使用指针来作为两类内存空间的分界点
  • 空闲列表:如果已用内存空间和可用内存空间相互交织,此时无法在使用单独的指针来作为分界点。需要使用一个空闲列表来维护当前空闲的内存块,创建对象申请空间时,系统从中选出一块足够大的空间进行分配

多线程场景中,如果不同的线程竞争分配同一块内存空间,必然会引发线程安全问题。为了避免问题的出现,可以采用如下选择:

  • 对分配内存空间的动作进行同步处理:即每块内存空间某个时刻只能由一个线程来分配,相当于加锁
  • 把内存分配的动作按照线程划分在不同的空间之中进行:每个线程在Java堆中预先分配的一小块称为本地线程分配缓冲(Thread Local Alloction Buffer,TLAB)的一小块内存中,每个线程分配的内存空间没有交叠,自然不存在竞争问题

3. 对象定位的方法有哪些?

Java中主流的对象定位方法有使用句柄间接访问和使用直接指针直接访问两种,如下所示:

  • 使用句柄:句柄中保存了对象实例数据与类型数据各自具体的地址信息,它是一种间接的访问方式,即需通过句柄中保存的地址来访问对象


  • 使用直接指针:指针直接存储的就是对象指针,因此它是一种直接访问方式,通过保存的对象地址直接访问


Java虚拟机内存模型


4. Jvm中系统级线程有哪些?
  • 虚拟机线程:它需要JVM到达安全点才会出现,这种此案成的执行类型包括"stop-the-world"的垃圾收集、线程栈收集、线程挂起和偏向锁撤销
  • 周期任务线程:它是时间周期事件的体现,一般用于周期性操作的调度执行
  • GC线程:JVM中不同的垃圾收集机制提供了不同的支持
  • 编译线程:它负责在运行时间字节码编译到本地代码
  • 信号调度线程:它用于接收信号并发送给JVM,在它内部通过调用适当的方法进行处理

5. 程序计数器有什么作用?

程序计数器是线程私有的,生命周期和所属的线程一致。它负责存放线程和指令的地址信息,即指向下一条指令的地址,执行引擎需要根据程序计数器中的内容取指令执行。


6. 虚拟机栈是什么?它有什么作用?

JVM中的虚拟机栈也是线程私有的,线程创建时会同时创建属于它的虚拟机栈,用于实现栈帧的入栈和出栈。栈帧随着方法的调用和执行结束入栈及出栈,而且只有虚拟机栈中当前栈帧所指向的方法才会被执行。当虚拟机栈无法为新创建的栈帧分配内存空间时,JVM就是抛出StackOverFlowError。


栈帧中的内容包括:

  • 局部变量表:存储方法参数和定义在方法体内部的局部变量,方法之间参数的传递通过局部变量表实现,只要被局部变量表中直接或是间接引用的对象都不会被回收,它的生命周期和当前栈帧或是当前执行的方法是一致的
  • 操作数栈:用于保存计算过程的中间结果,同时作为计算过程中变量的临时存储空间,它的生命周期和当前栈帧是一致的
  • 动态链接:用于将符号引用转换为调用方法的直接引用
  • 方法返回地址:存放调用该方法的程序计数器值

7. 类加载的过程是怎样的?什么是双亲委派机制?

类加载器ClassLoader整体的过程可以分为如下几个阶段:

  • 加载
  • 链接:该阶段又分为验证、准备和解析三个阶段
  • 初始化

JVM对.Class文件采用的是按需加载的方式,当使用该类时才将其字节码文件加载到内存中生成对应的Class类对象。当加载某个类的字节码文件时,JVM采用的是双亲委派机制.


它的核心思想为:

  • 自底向上检查类是否已经加载
  • 自顶向下尝试加载类

双亲委派机制的执行流程大致如下:

  • 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类加载器去执行
  • 如果父类加载器还存在父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的BoostStrapClassLoader
  • 如果父类加载器可以完成类加载任务,则成功返回;如果父类加载器无法完成此加载任务,子类加载器才会尝试自己去加载
  • 如果所有的类加载器都没法进行加载时,JVM会抛出ClassNotFoundException异常

双亲委派机制有如下的优点:

  • 避免重复加载同一个字节码文件,从而保证数据安全
  • 保证核心字节码文件不被篡改,不同的加载器加载同一个字节码文件得到的不是同一个对象,保证了字节码文件的安全

Java虚拟机 – 类加载子系统


8. Minor GC、Major GC和Full GC有什么区别?

JVM大部分时候执行GC主要是针对于新生代,按照回收区域又分为:

  • 部分收集:不是针对于完整的堆内存空间执行GC,其中又分为:

    • Young GC,Minor GC:只是新生代的垃圾收集,只有当Eden区满时才会触发Young GC
    • Old GC:针对于老年代的垃圾收集
  • 整堆收集:Full GC,Major GC,对整堆和方法区执行GC操作。当老年代空间不足时,首先会触发Minor GC;如果执行后空间仍不足,则会触发Major GC;如果Major GC执行后仍不足,则抛出OOM异常

Full GC的触发条件有:

  • 主动调用System.gc方法时,系统建议执行Full GC,但不是必然执行
  • 老年代空间不足
  • 方法区空间不足
  • 通过Minor GC后进入老年代的对象平均大小大于老年代的可用内存
  • 由Eden区、From区向To区复制时,对象大小大于可用的To区内存空间时,则把对象转存到老年代,且老年代的可用内存小于该对象大小

9. 堆的内存分配的策略有哪些?
  • 绝大多数的对象优先分配到Eden区
  • 大对象直接分配到老年代
  • 长期存活的对象分配到老年代
  • 如果Survivor区中相同年龄的所有对象的大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenturingThreshold中要求的数值

10. 什么是栈上分配?什么是标量替换?

栈上分配和标量替换是随着JIT和逃逸技术的发展而出现的,它们的含义分别为:

  • 栈上分配:如经过逃逸分析后发现一个对象并没有逃逸出方法,那么它就可能会被优化为栈上分配,分配完成后,继续在调用的栈内执行。最后线程结束,栈空间被回收,局部变量对象也被回收,这样就无需进行GC了
  • 标量替换:在JIT阶段,如果经过逃逸分析发现一个对象不会被外界所访问的话,那么经过JIT优化,就会把这个对象拆解为若干个成员变量替代

11. 方法区包含哪些信息?

方法区的结构如下所示:


主要包含如下几部分信息:

  • 类型信息:这里的类型包括class、interface、enum、annotation,信息有:全类名、直接父类的完整有效名、修饰符、直接接口的一个有序列表
  • 域信息:包括域名称、域类型和域修饰符
  • 方法信息:需要包含方法的信息,同时保存声明的顺序,信息包括:
    • 方法名称
    • 方法的返回类型
    • 方法参数的数量和类型
    • 方法的修饰符
    • 方法的字节码、操作数栈、局部变量表及大小(abstract和native方法除外)
    • 异常表(abstract和native方法除外):每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
  • non-final类常量

12. 为什么要用元空间取代永久代(方法区)?
  • 为永久代设置空间大小是很难确定:某些场景下,如果动态加载的类过多,容易产生永久代区的OOM。元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下元空间的大小仅受本地内存限制
  • 对永久代进行调优很困难

13. 为什么将字符串常量池转移到堆?

永久代基本上不进行GC,只有老年代空间不足时触发Full GC,才会对永久代进行垃圾回收。这就导致了永久代中字符串常量的回收效率不高,而在实际的使用中又会大量的使用到字符串。因此,如果回收效率低,将导致永久代内存不足,而将其放到堆中能做到及时回收。


14. 方法区的GC主要针对于什么类型的数据?

方法区的GC主要是回收常量池中废弃的常量和不再使用的类型,其中只要满足以下三个条件,就可以判定一个类不再被引用:

  • 该类的所有实例已经被回收
  • 加载该类的类加载器已经被回收
  • 该类的java.lang.Class对象没有在任何地方被引用,无法通过反射访问该类的方法

15. 为什么字符串常量池中不存在内容相同的字符串?

符创常量池的数据结构是HashTable,它是一个键值结构,而键值结构中键存储的内容是不能有重复的。如果放入常量池中的字符串非常多,大概率会出现哈希碰撞,从而使得保存相同哈希值的字符串的链表很长,而链表很长后直接会造成的影响就是当调用String.intern()时,性能大幅度下降。


16.GC Roots常包含哪些对象?
  • 虚拟机栈或是本地方法栈中引用的对象
  • 方法区中类静态属性或常量引用的对象
  • 所有被sychronized持有的对象
  • JVM内部的引用

17. JVM中的垃圾回收器有哪些?

JVM中的垃圾回收器,以及彼此之间的对应关系如下所示:


  • Serial GC:年轻代的串行垃圾回收器,使用复制算法
  • Serial Old:和Serial GC配套使用的老年代的串行垃圾回收器,使用标记-整理算法

Serial Old同时也是CMS的一个后备方案,当CMS导致的内存碎片过多而无法正常分配对象空间时,使用它进行一次GC,此时的性能会降低。

  • ParNew GC:年轻代的并行垃圾回收器

ParNew GC是一款为配合CMS使用的新生代垃圾回收器,原理和Parallel Scavenge GC类似。

  • Parallel Scavenge GC:年轻代的并行垃圾回收器,使用复制算法,目标是达到一个可控的吞吐量,而且采用了自调节策略

  • Parallel Old GC:和Parallel Scavenge GC配套使用的老年代的垃圾回收器,使用标记整理算法


  • CMS GC:工作在老年代,目标是尽可能的缩短垃圾回收和用户线程的暂停时间,追求低停顿


    使用标记清除算法,垃圾回收阶段分为如下四个阶段:

    • 初始标记阶段:仅标记和GC Roots直接关联的对象,速度很快
    • 并发标记阶段:从GC Roots直接关联的对象出发遍历整个对象图,标记所有可达的对象,耗时长,和用户线程并发执行
    • 重新标记阶段:重新标记因执行过程中发生变化的对象
    • 并发清除阶段:清除已经判断为死亡的对象,和用户线程并发执行

它优点有并发收集和低延迟;缺点有:

  • 有内存碎片:使用的是标记清除算法

  • 对CPU资源非常敏感:GC线程和用户线程同时运行,会占用一部分的CPU资源,使得吞吐量下降

  • 无法处理浮动垃圾:并发清除阶段无法处理新产生的垃圾

  • G1 GC:Garbage First GC,采用分代和分区的回收算法,堆内存划分为多个变化的Eden区、Survivor区和Old区,以及多个Humongous区。当执行垃圾回收时,虚拟机可有计划的避免在整个堆内存上进行垃圾回收。G1可以根据各个region的垃圾收集的价值大小,即回收所获得的空间大小以及回收所需时间的经验值,在后台维护一个相应的优先列表。在具体执行垃圾回收时,优先回收价值最大的region,即所谓的垃圾优先(Garbage First)。G1的特点有:并发和并行、分代收集、分区、空间整合、可预测的停顿时间模型。


    总结:

    GC分类作用位置算法特点使用场景
    Serial串行运行新生代复制算法响应速度优先单CPU的Client模式
    ParNew并行运行新生代复制算法响应速度优先多CPU的Server模式
    Parallel并行运行新生代复制算法吞吐量优先与后台运算不需太多交互的场景
    Serial Old串行运行老年代标记-整理算法响应速度优先单CPU的Client模式
    Parallel Old并行运行老年代标记-整理算法吞吐量优先与后台运算不需太多交互的场景
    CMS并发运行老年代标记-清除算法响应速度优先互联网或B/S业务
    G1并发、并行运行整堆标记-整理算法、复制算法响应速度优先面向服务端应用

18. 什么是Remember Set?

在广泛遵循的分代收集理论和局部回收的垃圾回收器(如G1 GC)中,处理判断存活对象时一个很重要的问题就是如何处理跨代引用问题。如果对新生代执行垃圾回收操作时,新生代中的对象被老年代中的某些对象所引用,那么在回收时就需要遍历老年代在找到引用了新生代对象的对象,从而最终决定回收新生代中的哪些对象。如果每次新生代的垃圾回收中都要遍历一次老年代,这显然是不太好的,而且导致的开销也很大。

Java使用了记忆集(Remember Set)来巧妙的避免对老年代的全局扫描。记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构,每个region都维护一个自己的记忆集,根据记忆集就知道自己是否被其他对象所引用,如果被引用,又是被哪个对象所引用。

记忆集(G1中实现方式是卡表)也会占用大量的内存空间,维护记忆集的成本较高,所以适用于大内存的应用上。


19. Shenandosh回收器和ZGC回收器的过程是怎么样的?

Shenandosh回收器的运行过程大致可以细分为九个阶段,具体为:

  • 初始标记:只标记和GC Roots直接可达的对象,此阶段会出现STW
  • 并发标记:从GC Roots出发遍历对象图,标记所有可达对象,此阶段和用户程序并发执行
  • 最终标记:修改并发标记阶段的标记,同时会统计出回收价值最高的region,将这些region构成回收集(Collection Set)
  • 并发清理:如果某个region中的所有对象都被标记为垃圾,则会执行此阶段的操作
  • 并发回收:这是为了进一步降低延迟所采取的改进操作,同时也是和G1相比的核心差异。Shenandosh会把最终标记阶段构建的回收集中的存活对象先复制一份到其他未被使用的region中,此阶段是和用户线程并发执行的。为了解决复制对象所导致出现的引用问题,Shenandosh采用了读屏障转发指针(Brooks Pointer)
  • 初始引用更新:建立一个线程集合点,确保上一阶段中进行垃圾回收的线程都已完成自己的对象移动任务
  • 并发引用更新:按照内存物理地址的顺序,线性搜索出引用类型,将引用的旧值更新为新值,此阶段同样是和用户线程并发执行
  • 最终引用更新:修正存在于GC Roots中的引用,确保移动对象后所用的引用都修改正确
  • 并发清理:经过并发回收和引用更新后,此时的回收集中将不再包含存活对象,最后调用并发清理操作回收回收集中的region区间

ZGC的执行过程大致可以分为如下的几个阶段:

  • 初始标记:同样是只标记GC Roots直接可达的对象,会有STW
  • 并发标记:同样是遍历对象图做可达性分析,此阶段会遍历所有和GC Roots可达的对象。不同之处在于,标记操作针对的是染色指针,而不是对象本身。此外,它之后同样还要经历最终标记,确保所有标记的正确性
  • 并发预备重分配:根据特定的查询条件统计出本次收集过程要清理的region区域,并将这些region组成重分配集(Relocation Set)。重分配集中的对象后续会被复制到其他的空闲region中,然后region空间就会被释放
  • 并发重分配:ZGC的核心阶段,将重分配集中的对象复制到新的region中,并为重分配集中的每个region维护一个转发表,用于记录旧对象到新对象的转向关系。根据染色指针中的Remapped标识位就可以知道对象是否处于重分配集,只要用户线程访问的对象位于重分配集,根据转发表将访问转发到新复制的对象上,同时修改更新该引用的值,使其直接指向新对象,ZGC称这个过程为指针的自愈(Self-Healing)。而且,自愈操作只会发生在第一次访问旧对象时,后续访问直接针对于新对象,大大提升了访问的效率
  • 并发重映射:修正整个堆中指向重分配集中旧对象的所有引用,而且它会延迟到下一个回收期的并发标记阶段执行,从而避免多一次的对象图遍历操作

20. 虚拟机栈什么时候会出现StackOverFlow和OOM?
  • 如果虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过了当前虚拟机栈的最大深度时,就会抛出StackOverflow
  • 如果虚拟机栈内存大小允许动态扩展,且当线程请求栈的内存用完了,再无法动态扩展时,就会抛出OOM

21. Java对象的创建过程是怎样的?

Java对象的创建过程大致是如下的流程:


  • 类加载检查:使用new关键字实例化类对象时,首先需要到方法区的运行时常量池中找相应类的符号引用,首先判断该类的符号引用是否已经执行了类加载、解析和初始化这一系列类加载的过程,如果已经完成,直接引用;否则必须先执行相应的类加载过程
  • 分配内存:通过上一步后,JVM需要为新对象分配内存,所需内存的大小在类加载完成后便固定了。所以,JVM只需要在堆中划分一块确定大小的内存空间给对象即可。按照内存空间是否规整,分配方式有碰撞指针和空闲列表两种方式;另外,为了解决多线程下的同步问题,首先会尝试在TLAB中分配内存,只有TLAB不能满足分配的要求后,才使用CAS尝试分配共享的堆内存空间
  • 初始化零值:JVM将对象分配到的内存空间都初始化为零值,保证了对象的实例字段在不赋初始值时可以直接使用默认值
  • 设置对象头:JVM设置对象头中哈希吗、分代年龄、偏向锁状态等相关的信息
  • 执行init方法:上述几步完成了JVM层面的对象创建,代码层面还需要执行init方法按照代码中的逻辑进行对象的初始化,执行结束后对象真正可用

22. 四种类型的引用有什么区别?
  • 强引用:永不回收
  • 软引用:内存不够时才回收
  • 弱引用:看到就回收
  • 虚引用:只起到跟踪垃圾回收的作用;回收直接内存

23. 常用的JVM调优参数有哪些?
  • -Xms2g:初始化推大小为 2g;
  • -Xmx2g:堆最大内存为 2g;
  • -XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
  • -XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
  • –XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
  • -XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
  • -XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
  • -XX:+PrintGC:开启打印 gc 信息;
  • -XX:+PrintGCDetails:打印 gc 详细信息

Spring


1. Spring的特征有哪些?
  • 核心技术:依赖注入、AOP、事件(events)、资源、iI8n、验证、数据绑定、类型转换、SpEL
  • 测试:模拟对象、TestContext框架、Spring MVC 框架、WebTestClient
  • 数据访问:事务、DAO支持、JDBC、ORM、编组XML
  • Web支持:Spring MVC、Spring WebFlux Web框架
  • 集成:远程处理、JMS、Email、调度、缓存
  • 语言:Kotlin、Groovy、动态语言

2. @Controller和@RestController的区别?

单独使用@Controller返回一个视图,它对应于前后端不分离的情况。

@RestController效果等同于@Controller+@ResponseBody,它只返回对象,对象数据直接以JSOM或XML形式写入到HTTP Response的body中,属于RESTful Web服务。


3. 解释一下IoC和AOP?
  • IoC,Inverse of Control:控制反转,一种设计思想,将原本在程序中手动创建对象的控制权交由Spring框架来管理。ioc容器是Spring实现IoC的载体,它是一个Map。这样ioc容器就像一个工厂一样,当我们需要创建一个对象时,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的
  • AOP,Aspect Oriented Programming:面向切面编程,基于动态代理,能够将那些与业务无关,但却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可扩展性和可维护性

4. Sping中Bean的作用域有哪些?
  • singleton:单例,唯一Bean实例,默认模式。当ioc容器被创建时,实例就被创建;当ioc容器销毁时,实例也就被销毁
  • prototype:每次请求都会创建一个新的Bean实例,多例。当使用实例时才会创建,当实例长时间不用时,就会被GC掉
  • request:每一次Http请求都会产生一个新的Bean,该Bean仅在当在当前HTTP Request中有效
  • session:每一次Http请求都会产生一个新的Bean,该Bean仅在当在当前HTTP session中有效
  • global-session:全局session作用域

5. Bean的生命周期?

Bean容器找到配置文件中Spring Bean的定义,利用Java Reflection API创建一个Bean的实例,如果涉及到一些属性值 利用set方法设置一些属性值。

  • 如果Bean实现了BeanNameAware接口,调用setBeanName()方法,传入Bean的名字
  • 如果Bean实现了BeanClassLoaderAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例
  • 如果Bean实现了BeanFactoryAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例

与上面的类似,如果实现了其他*Aware接口,就调用相应的方法,获取bean中相应的一些资源

  • 如果有和加载这个Bean的Spring容器相关的BeanPostProcessor对象,执行postProcessBeforeInitialization()方法
  • 如果Bean实现了InitializingBean接口,执行afterPropertiesSet()方法
  • 如果Bean在配置文件中的定义包含init-method属性,执行指定的方法
  • 如果有和加载这个Bean的Spring容器相关的BeanPostProcessor对象,执行postProcessAfterInitialization()方法
  • 当要销毁Bean的时候,如果Bean实现了DisposableBean接口,执行destroy()方法;如果Bean在配置文件中的定义包含destroy-method属性,执行指定的方法

【Spring】Bean的生命周期
请别再问Spring Bean的生命周期了!


6. Spring MVC的工作原理?

工作流程如下:

  • 客户端发送请求到DispatcherServlet,它根据请求信息调用HandlerMapping,解析请求对应的Handler
  • 解析到对应的Handler后,开始由HandlerAdapter适配器处理。HandlerAdapter会根据Handler调用真正的处理器开始处理请求,并处理相应的业务逻辑
  • 处理器处理完业务后,返回一个ModelView对象,Model是返回的数据对象,View是个逻辑上的View,ViewResolver会根据逻辑View查找实际的View
  • DispatcherServlet把返回的Model传给View进行渲染后返回给客户端

7. Spring中用到了哪些设计模式?
  • 工厂模式:BeanFactory、ApplicationContext创建Bean对象
  • 单例模式::Bean默认为单例
  • 代理模式:AOP依赖于动态代理
  • 包装器模式:根据不同的需求切换不同的数据源
  • 观察者模式:事件驱动模型
  • 适配器模式:AOP的增强或通知、SpringMVC中的HandlerAdapter
  • 模版方法模式:Spring中jdbcTemplate等各种xxxxTemplate

8. Spring中的事务有哪几种?事务隔离级别呢?事务传播行为呢?

Spring中事务管理有两种方式:

  • 编程式事务:代码中硬编写
  • 声明式事务:配置文件中配置,其中又分为两种:
    • 基于XML的声明式事务
    • 基于注解的声明式事务

Spring中的事务隔离级别相比于数据库的隔离级别只多了一个默认级别,具体如下:

  • TransactionDefinition.ISOLATION_DEFAULT:使用后端数据库默认的隔离级别
  • TransactionDefinition.ISOLATION_READ_UNCOMMITTED:最低级别,读未提交
  • TransactionDefinition.ISOLATION_READ_COMMITTED:读已提交
  • TransactionDefinition.ISOLATION_REPEATABLE_READ:可重复读
  • TransactionDefinition.ISOLATION_SERIALIZABLE:可序列化,最高级别,所有的事务逐个执行,事务之间完全不可能产生干扰

事务的传播行为分为:

  • 支持当前事务:

    • TransactionDefinition.PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务
    • TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行
    • TransactionDefinition.PROPAGATION_MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常
  • 不支持当前事务:

    • TransactionDefinition.PROPAGATION_REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起
    • TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起
    • TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常
  • 其他情况:

    • TransactionDefinition.PROPAGATION_NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED

9. 详细说一下ioc?Important

它是一种控制反转的理念,一种设计思想,将原本在程序中手动创建对象的控制权交由Spring框架来管理。ioc容器是Spring实现IoC的载体,它是一个Map。这样ioc容器就像一个工厂一样,当我们需要创建一个对象时,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。

sping ioc容器的设计主要依赖于BeanFactoryApplicationContext这两个接口,其中ApplicationContext是BeanFactory的子接口,则BeanFactory是容器定义的最底层接口。ApplicationContext作为其最高级的接口之一,并对BeanFactory做了其他的许多扩展,所以绝大多数情况下使用ApplicationContext作为ioc容器使用。

BeanFactory作为ioc容器的底层接口定义了许多操作Bean的方法。

package org.springframework.beans.factory;

import org.springframework.beans.BeansException;
import org.springframework.core.ResolvableType;
import org.springframework.lang.Nullable;

public interface BeanFactory {
    String FACTORY_BEAN_PREFIX = "&";

    Object getBean(String var1) throws BeansException;

    <T> T getBean(String var1, @Nullable Class<T> var2) throws BeansException;

    Object getBean(String var1, Object... var2) throws BeansException;

    <T> T getBean(Class<T> var1) throws BeansException;

    <T> T getBean(Class<T> var1, Object... var2) throws BeansException;

    boolean containsBean(String var1);

    boolean isSingleton(String var1) throws NoSuchBeanDefinitionException;

    boolean isPrototype(String var1) throws NoSuchBeanDefinitionException;

    boolean isTypeMatch(String var1, ResolvableType var2) throws NoSuchBeanDefinitionException;

    boolean isTypeMatch(String var1, @Nullable Class<?> var2) throws NoSuchBeanDefinitionException;

    @Nullable
    Class<?> getType(String var1) throws NoSuchBeanDefinitionException;

    String[] getAliases(String var1);
}

例如:

  • getBean:方法有许多的重载形式,可以根据不同的方式来获取指定的bean:
    • getBean(Classs<T>):按照bean的类型获取bean,此时对应的bean只能配置一个
    • getBean(String):按照bean的名字获取
    • getBean(String, Class<T>):推荐,根据bean的类型和名字获取bean
  • isSingleton:是否是单例模式生成bean,ioc容器中只会存在一个bean实例
  • isPrototype:是否是采用原型模式生成bean,每次当从ioc容器中获取bean时,容器都会生成一个新的bean实例
  • getAliases:获取别名

ApplicationContext除了继承Beanfactory接口之外,它还扩展了许多其他的接口,因此功能十分强大。通常可以使用xml文件或者注解的形式将bean注册到ioc容器中,例如,使用xml文件中的来定义bean,并将其注册到ioc容器中:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework/schema/beans"
       xmlns:xsi="http://www.w3/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework/schema/beans http://www.springframework/schema/beans/spring-beans.xsd">
    <!-- 通过 xml 方式装配 bean -->
    <bean name="person" class="domain.Person">
        <property name="name" value="Forlogen"/>
        <property name="age" value="18"/>
    </bean>
</beans>

定义好之后,ico容器初始化时就可以找到它。如果想要获取该bean,可以使用ClassPathXmlApplicationContext容器的getBean方法来通过名字或类型获取:

ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
Person person = (Person)context.getBean("person", Person.class)

如果使用注解来注册bean,可以使用@Component@Controller@Service@Respository等注解。例如:

@Controller
public class AccountController {
}

Spring 基于注解的Ioc

ApplicationContext的常用实现类有:

  • ClassPathXmlApplicationContext:读取classpath中的资源
  • FileSystemXmlApplicationContext:读取指定路径的资源
  • XmlWebApplicationContext:需要在Web环境下运行
  • AnnotationConfigApplicationContext:读取注解配置的资源

Bean在ioc容器中需要先定义,然后实现初始化和依赖注入。其中,Bean的定义分为如下三步:

  • Resource根据配置定位ioc容器,例如配置文件或者注解
  • BeanDefinition载入时将Resource定位到的消息保存在Bean定义中
  • BeanDefinition的注册,将其中的信息发布到ioc容器中

对于初始化和依赖注入,Bean其中的配置项lazy-init用于是否是懒初始化,默认为default,实际值为false。Spring默认会自动初始化Bean,如果设置为true,ioc容器只有在使用getBean获取bean时才会进行初始化,完成依赖注入。

依赖注入可以分为两个流程:

  • 收集和注册:通过xml文件或者注解的方式定义Bean,然后让ioc容器采用自动扫描的方式将定义好的Bean收集到ioc容器中
  • 分析和组装:如果ioc容器发现不同Bean之间存在依赖,它就会将bean注入到依赖它的那么bean中,直到所有bean的依赖都注入完成。当所有的bean都组装结束,整个ioc容器的工作就完成了

10. 详细说一下AOP?

AOP(Aspect Oriented Program)指面向切面编程。它能够将那些与业务无关,却为业务模块锁共同调用的逻辑或责任(如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于代码的可扩展性和可维护性。

面向切面编程的思想中,将核心业务功能和切面功能分别独立进行开发,然后把切面功能和核心业务功能编织在一起,这就是AOP。

AOP中一些核心的概念如下:

  • 切入点(PointCut):哪些类的哪些方法切入
  • 通知(Advice):在方法执行的什么时刻(方法前、方法后、方法前后)做什么操作(增强)
  • 切面(Aspect):切面 = 切入点 + 通知,在什么时机、什么地方做什么增强
  • 织入(WeaVing):把切面加入到独享,并创建代理对象的过程

可以使用配置文件和注解两种形式来配置AOP,其中使用配置文件配置AOP的相关标签有:

  • aop:config:声明AOP开始
  • aop:aspect:配置切面,其中id用于给切面提供一个唯一标识,ref引用配置好额bean的id
  • aop:pointcut:配置切入点表达式,expression用于定于切入点表达式,id提供唯一标识
  • aop:before:前置通知,指定增强的方法在切入点方法之前执行
  • aop:after-returning:后置通知,切入点方法正常执行之后
  • aop:after-throwing:异常通知,切入点方法在执行产生异常之后
  • aop:after:最终通知,无论切入点方法执行时是否有异常,它都会在其后面执行
  • aop:around:环绕通知,通常独立使用
<!--配置AOP-->
    <aop:config>
        <!--配置切面-->
        <aop:aspect id="log" ref="logger">
            <aop:pointcut id="pt" expression="execution(* dyliang.service.impl.*.*(..))"/>
            <!--
            <aop:before method="logging" pointcut="execution(* dyliang.service.impl.*.*(..))"></aop:before>-->

            <!-- 配置前置通知:在切入点方法执行之前执行
            <aop:before method="beforePrintLog" pointcut-ref="pt" ></aop:before>-->

            <!-- 配置后置通知:在切入点方法正常执行之后值。它和异常通知永远只能执行一个
            <aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt"></aop:after-returning>-->

            <!-- 配置异常通知:在切入点方法执行产生异常之后执行。它和后置通知永远只能执行一个
            <aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt"></aop:after-throwing>-->

            <!-- 配置最终通知:无论切入点方法是否正常执行它都会在其后面执行
            <aop:after method="afterPrintLog" pointcut-ref="pt"></aop:after>-->

            <!-- 配置环绕通知 详细的注释请看Logger类中-->
            <aop:around method="aroundPringLog" pointcut-ref="pt"></aop:around>
        </aop:aspect>
    </aop:config>

基于注解的AOP编程涉及到的注解有:

  • @Aspect:表明当前类是一个切面类
  • @Before : 把当前方法看成是前置通知。 value用于指定切入点表达式,还可以指定切入点表达式的引用
  • @AfterReturning: 把当前方法看成是后置通知。value用于指定切入点表达式,还可以指定切入点表达式的引用
  • @AfterThrowing : 把当前方法看成是异常通知。 value用于指定切入点表达式,还可以指定切入点表达式的引用
  • @After : 把当前方法看成是最终通知。 value用于指定切入点表达式,还可以指定切入点表达式的引用
  • @Around : 把当前方法看成是环绕通知。 value用于指定切入点表达式,还可以指定切入点表达式的引用
  • @Pointcut: 指定切入点表达式, value用于指定表达式的内容
@Component("logger")
@Aspect
public class Logger {
    
    @Pointcut("execution(* dyliang.service.impl.*.*(..))")
    public void pt(){}

    @Before("pt()")
    public void beforePrintLog(){
        System.out.println("beforePrintLog...");
    }

    @AfterReturning("pt()")
    public void afterReturningPrintLog(){
        System.out.println("afterReturningPrintLog...");
    }

    @AfterThrowing("pt()")
    public void afterThrowingPrintLog(){
        System.out.println("afterReturningPrintLog...");
    }

    @After("pt()")
    public void afterPrintLog(){
        System.out.println("afterPrintLog...");
    }

    @Around("pt()")
    public Object aroundPringLog(ProceedingJoinPoint pjp){
        Object rtValue = null;
        try{
            Object[] args = pjp.getArgs();
            System.out.println("before...");
            rtValue = pjp.proceed(args);  
            System.out.println("after return...");
            return rtValue;
        }catch (Throwable t){
            System.out.println("after throwing...");
            throw new RuntimeException(t);
        }finally {
            System.out.println("after...");
        }
    }
}


11. 什么是依赖注入?依赖注入的基本原则是什么?依赖注入有哪些实现方式?

依赖注入(Dependency Injection,DI)指的是由容器动态的将某种依赖关系的目标对象实例注入到应用系统的各个关联的组件之中。应用组件不应该负责查找资源或者其他依赖的协作对象,配置对象的工作由ioc容器完成。

依赖注入让ioc容器全权负责依赖查询,受管组件只需要暴露JavaBean的setter方法或者带参数的构造器或者接口,使容器可以在初始化时组装对象的依赖关系。注入的方式有:

  • 接口注入:已废弃
  • Setter方法注入:调用bean的setter方法实现注入
  • 构造方法注入:通过ioc容器触发一个类的构造器实现,该类有一系列参数,每个参数代表一个对其他类的依赖
构造函数注入setter 注入
没有部分注入有部分注入
不会覆盖 setter 属性会覆盖 setter 属性
任意修改都会创建一个新实例任意修改不会创建一个新实例
适用于设置很多属性适用于设置少量属性

12. Spring如何解决线程安全问题?

Spring中的bean默认是单例的,如果bean是无状态的,那么它在多线程环境下是线程安全的。否则,可以使用ThreadLocal为每个线程提供一个独立的变量副本,从而隔离多个线程对数据的访问冲突。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。


13. Spring Boot的自动配置原理是什么?

SpringBoot中主要使用@EnableAutoConfiguration注解来开启自动配置,它利用EnableAutoConfigurationImportSelector给容器中导入一些组件。它的父类中的selectImport方法会返回configurations:

List configurations = getCandidateConfigurations(annotationMetadata, attributes);

用于获取候选的配置。将类路径下 **META-INF/spring.factories**里面配置的所有EnableAutoConfiguration的值加入到了容器中。加载某个组件时,根据注解的条件判断每个加入的组件是否生效。如果生效,就把类的属性和配置文件绑定起来,这是需要读取配置文件的值加载组件。

面试这么答:
Spring Boot启动的时候会通过@EnableAutoConfiguration注解找到META-INF/spring.factories配置文件中的所有自动配置类,并对其进行加载,而这些自动配置类都是以AutoConfiguration结尾来命名的,它实际上就是一个JavaConfig形式的Spring容器配置类,它能通过以Properties结尾命名的类中取得在全局配置文件中配置的属性如:server.port,而XxxxProperties类是通过@ConfigurationProperties注解与全局配置文件中对应的属性进行绑定的。

SpringBoot中的自动配置很好的反映了习惯大于配置的理念,通过简单的配置就可以集成绝大部分常用的框架使用。我们知道SpringBoot项目需要一个启动类。启动类上需要标注@SpringBootApplication注解,如下所示:

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

@SpringBootApplication注解本身又是一个复合注解,如下所示:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration  // 当前类为一个配置类
@EnableAutoConfiguration  
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {}

其中比较重要的是@EnableAutoConfiguration注解,它用于开启SpringBoot的自动配置。而它也是一个复合注解,定义如下:

@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

	String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

	Class<?>[] exclude() default {};

	String[] excludeName() default {};
}

它的关键功能是由@Import注解提供,其导入的AutoConfigurationImportSelector中的selectImports方法会通过SpringFactoriesLoader.loadFactoryNames()扫描sping-boot-autoconfiguration.jar包下META-INF/spring.factories文件中以AutoConfiguration为后缀的配置类。

spring.factories文件中的内容以键值对的形式存储,键为EnableAutoConfiguration类的全类名,值为xxxxAutoConfiguration的类名列表,它们之间以逗号分隔,表示可以配置的组件类。

当调用SpringApplication.run方法启动项目时,内部就会执行selectImport方法,找到所有JavaConfig自动配置类的全限定类名对应的class,然后将所有的自动配置类加载到ioc容器中。

但是,并不是所有的自动配置类都会在项目启动时生效,它依赖于某些条件,这些条件在SpringBoot中以注解的形式体现。例如常用的JdbcTemplateAutoConfiguration的定义如下:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ DataSource.class, JdbcTemplate.class })
@ConditionalOnSingleCandidate(DataSource.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
@EnableConfigurationProperties(JdbcProperties.class)
@Import({ JdbcTemplateConfiguration.class, NamedParameterJdbcTemplateConfiguration.class })
public class JdbcTemplateAutoConfiguration {

}

其中:

  • @ConditionalOnClass:表示当类路径下有指定类时才加载

类似的注解还有:

  • @ConditionalOnBean:当容器中有指定的bean才加载
  • @ConditionalOnMissingBean:当容器中不存在指定的bean时才加载
  • @ConditionalOnMissingClass:当类路径下不存在指定的类时才加载
    -@ConditionalOnProperty:指定的属性是否有指定值

另外,JdbcTemplateAutoConfiguration定义中的@EnableConfigurationProperties用于开启注解属性,当我们在全局的配置文件application.properties或者application.yaml中配置了相关的参数后,@ConfigurationProperties注解会将配置的参数和绑定到对应的JdbcProperties配置类上,将其封装为一个bean后通过@EnableConfigurationProperties注解注入到ioc容器中。

@ConfigurationProperties(prefix = "spring.jdbc")
public class JdbcProperties {}

SpringBootz自动配置的整体过程如下所示:



14. @Transactional注解的实现机制 ?

在应用系统调用声明@Transactional的目标方法时,Spring 默认使用 AOP 代理,在代码运行时生成一个代理对象,根据@Transactional的属性配置信息,代理对象来决定该声明@Transactional的目标方法是否由拦截器 TransactionInterceptor 来使用拦截。在拦截时,会在目标方法开始执行之前创建并加入事务,并执行目标方法的逻辑, 最后根据执行情况是否出现异常,利用抽象事务管理器操作数据源 DataSource 提交或回滚事务。


15. Spring中的循环依赖问题是什么?如何解决?

如果Bean之间相互依赖,例如A依赖B,B依赖C,C依赖A,它们之间就构成了循环依赖关系。当Bean之间具有循环依赖时,Spring无法知道应该先创建哪个Bean。Spring可以解决以Setter方法构成的循环依赖,但无法解决以构造方法造成的循环依赖。

setter 注入和构造器注入的区别就在于创建bean的过程中,setter注入可以先用无参数构造方法返回bean实例,再注入依赖的属性,使用到了 Spring 的三级缓存。而constructor方式无法先返回bean的实例,必须先实例化它所依赖的属性,这样一来就会造成死循环所以会失败。

另外,常用的使用@Autowored注解的循环依赖问题也可以通过Spring的三级缓存解决。Bean创建的三个核心方法有:

  • createBeanInstance:实例化Bean,调用对象的构造方法实例化对象
  • populateBean:填充属性,对bean的依赖属性进行注入
  • initializaBean:回到一些形如initMethod、InitializingBean等方法

Spring的三级缓存分为如下:

  • singletonObjects:第一级,单例缓存池,用于存放完全初始化好的bean,取出即可使用
  • earlySingletonObjects:第二级,存放早期暴露的bean对象,即尚未填充属性的bean
  • singletonFactories:第三级,单例对象工厂缓存,存放bean工厂对象

Spring是如何解决的循环依赖?
答:Spring通过三级缓存解决了循环依赖,其中一级缓存为单例池(singletonObjects),二级缓存为早期曝光对象earlySingletonObjects,三级缓存为早期曝光对象工厂(singletonFactories)。当A、B两个类发生循环引用时,在A完成实例化后,就使用实例化后的对象去创建一个对象工厂,并添加到三级缓存中,如果A被AOP代理,那么通过这个工厂获取到的就是A代理后的对象,如果A没有被AOP代理,那么这个工厂获取到的就是A实例化的对象。当A进行属性注入时,会去创建B,同时B又依赖了A,所以创建B的同时又会去调用getBean(a)来获取需要的依赖,此时的getBean(a)会从缓存中获取,第一步,先获取到三级缓存中的工厂;第二步,调用对象工工厂的getObject方法来获取到对应的对象,得到这个对象后将其注入到B中。紧接着B会走完它的生命周期流程,包括初始化、后置处理器等。当B创建完后,会将B再注入到A中,此时A再完成它的整个生命周期。至此,循环依赖结束!

为什么要使用三级缓存呢?二级缓存能解决循环依赖吗?
答:如果要使用二级缓存解决循环依赖,意味着所有Bean在实例化后就要完成AOP代理,这样违背了Spring设计的原则,Spring在设计之初就是通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器来在Bean生命周期的最后一步来完成AOP代理,而不是在实例化后就立马进行AOP代理。


RocketMQ


1. 消息队列的优缺点有哪些?

消息队列的优点有:

  • 应用解耦
  • 流量削峰
  • 数据分发

但是它同样存在如下的不足之处:

  • 系统可用性降低
  • 系统的复杂度提高
  • 一致性问题

2. 简要介绍一下RocketMQ的主要部分?

RocketMQ的构成如下所示:


  • Producer:消息的发送者

  • Consumer:消息接收者

  • Broker:暂存和传输消息

  • NameServer:管理Broker;举例:各个邮局的管理机构

  • Topic:区分消息的种类;一个发送者可以发送消息给一个或者多个Topic;一个消息的接收者可以订阅一个或者多个Topic消息

  • Message Queue:相当于是Topic的分区;用于并行发送和接收消息


3. 集群模式的特点?
  • NameServer是一个几乎无状态节点,可集群部署,节点之间无任何信息同步
  • Broker部署相对复杂,Broker分为Master与Slave,一个Master可以对应多个Slave,但是一个Slave只能对应一个Master,Master与Slave的对应关系通过指定相同的BrokerName,不同的BrokerId来定义,BrokerId为0表示Master,非0表示Slave。Master也可以部署多个。每个Broker与NameServer集群中的所有节点建立长连接,定时注册Topic信息到所有NameServer
  • Producer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer取Topic路由信息,并向提供Topic服务的Master建立长连接,且定时向Master发送心跳。Producer完全无状态,可集群部署
  • Consumer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer取Topic路由信息,并向提供Topic服务的Master、Slave建立长连接,且定时向Master、Slave发送心跳。Consumer既可以从Master订阅消息,也可以从Slave订阅消息,订阅规则由Broker配置决定

4. Producer和Consumer的工作流程是怎样的?

Producer的工作流程如下:

  • 创建消息生产者producer,并制定Producer Group名
  • 指定Nameserver地址
  • 启动producer
  • 创建消息对象,指定主题Topic、Tag和消息体
  • 发送消息
  • 关闭生产者producer

Consumer的工作流程如下:

  • 创建消费者Consumer,制定Consumer Group名
  • 指定Nameserver地址
  • 订阅主题Topic和Tag
  • 设置回调函数,处理消息
  • 启动消费者consumer

5. 顺序消息的实现机制是怎样的?

控制发送的顺序消息只依次发送到同一个queue中,消费的时候只从这个queue上依次拉取,则就保证了顺序。

  • 当发送和消费参与的queue只有一个,则是全局有序
  • 如果多个queue参与,则为分区有序,即相对每个queue,消息都是有序的

6. 事务型消息的实现机制?

事务型消息大致涉及两个流程:

  • 正常事务消息的发送和提交
  • 事务消息的补偿机制

正常事务消息的发送和提交:

  • 发送消息(half消息)
  • 服务端响应消息写入结果
  • 根据发送结果执行本地事务(如果写入失败,此时half消息对业务不可见,本地逻辑不执行)
  • 根据本地事务状态执行Commit或者Rollback(Commit操作生成消息索引,消息对消费者可见)

事务补偿:

  • 对没有Commit/Rollback的事务消息(pending状态的消息),从服务端发起一次“回查”
  • Producer收到回查消息,检查回查消息对应的本地事务的状态
  • 根据本地事务状态,重新Commit或者Rollback

其中,补偿阶段用于解决消息Commit或者Rollback发生超时或者失败的情况

事务消息共有三种状态,提交状态、回滚状态、中间状态:

  • TransactionStatus.CommitTransaction: 提交事务,它允许消费者消费此消息
  • TransactionStatus.RollbackTransaction: 回滚事务,它代表该消息将被删除,不允许被消费
  • TransactionStatus.Unknown: 中间状态,它代表需要检查消息队列来确定状态

7. RoketMQ的持久化是什么?

消息队列的持久化存储是为了满足分布队列对于高可靠性的要求,每当Producer发送了一条消息,消息队列收到消息后需要将消息持久化到磁盘中,并返回确认ack给Producer。Consumer主动拉取消息,或者消息队列推送消息给Consumer,Consumer在指定的时间内成功消费消息后返回ack,消息队列在收到确认后删除磁盘中存储的消息。否则,尝试重新推送或者拉取消息,直到消息被成功消费。

RocketMQ采用的持久化方式是将消息刷到本地磁盘,它提供了一种高效率、高可靠性和高性能的方式,除非消息队列服务器本身或者磁盘挂了,否则不会出现持久化故障。

RocketMQ采用顺序写的方式保证了消息存储的速度,缓解了磁盘写所带来的性能开销。并且使用了零拷贝技术,将数据直接从内核态的内存中直接拷贝到网络驱动的内核态内存,避免了中间复制到用户态内存这一步,提高了消息刷盘和网络发送的速度。

采用MappedByteBuffer这种内存映射的方式有几个限制,其中之一是一次只能映射1.5~2G 的文件至用户态的虚拟内存,这也是为何RocketMQ默认设置单个CommitLog日志数据文件为1G的原因


8. 消息的存储结构是什么样的?

RocketMQ的消息存储由ConsumeQueue和CommitLog配合实现,消息真正的物理存储文件是CommitLog,ConsumeQueue是消息的逻辑队列,类似于数据库的索引文件,存储的是指向物理存储CommitLog的地址。每个Topic下的每个MessageQueue都有一个对应的ConsumeQueue文件。


其中涉及的主要部分有:

  • CommitLog:存储消息的物理文件
  • ConsumeQueue:存储消息在CommitLog的索引
  • IndexFile:提供了一种通过key或者时间区间来查询消息的方法,查询过程中不会影响发送和消费消息的主流程

9. 消息的刷盘机制有哪两种?

RocketMQ的消息是存储到磁盘上的,这样既能保证断电后恢复,又可以让存储的消息量不受内存大小的限制。RocketMQ为了提高性能,会尽可能地保证磁盘的顺序写。它提供了同步刷盘和异步刷盘两种机制:

  • 同步刷盘:当消息写入内存的页缓存中后,立刻通知刷盘线程执行刷盘操作,然后等待操作执行完成。刷盘成功后,返回消息写成功的状态
  • 异步刷盘:当消息写入到页缓存中后就返回写成功状态,而当内存里的消息量累积到一定程度时,统一触发刷盘机制,快速的执行顺序写操作

10. RocketMQ的高可用机制是如何实现的?

RocketMQ的高可用主要依赖于Producer、Consumer和NameServer的集群部署,以及Broker的主从复制实现。


Consumer和订阅的Topic对应Broker的master和slave建立长连接后,当它需要消费消息时,并不需要设置是从mater还是slave中读。当master不可用或者繁忙时,Consumer会自动的切换到slave进行消息读取。

创建Topic的时候,把Topic的多个Message Queue创建在多个Broker组上(相同Broker名称,不同 brokerId的机器组成一个Broker组),这样当一个Broker组的Master不可用后,其他组的Master仍然可用,Producer仍然可以发送消息。


11. Broker的主从复制是如何实现的?

Broker的主从复制有同步复制异步复制两种方式:

  • 同步复制:只有消息成功的写入到master和slave后,才向客户端返回写成功状态。这种方式可以避免消息的丢失,但是会造成较大的写入延迟和较低的吞吐量。
  • 异步复制:只要消息成功的写入到master即返回写成功状态,保证了较低的延迟和较高的吞吐量,但是可能会有消息的丢失

12. 负载均衡是如何实现的?

对于Producer来说,它发送消息时会轮询消息的topic对应的Broker的所有MessageQueue,以达到让消息平均的落在不同的Queue上。


对于Consumer来说,每条消息只需要投递到订阅了消息对应的Topic的Consumer Group中的一个Consumer实例即可。当Consumer主动拉取并消费消息,拉取时需要明确指定拉取哪一个MessageQueue。一旦Consumer实例的数量发生变化时,就会触发负载均衡机制,此时会按照Consumer和MessageQueue的数量,平均的将MessageQueue分配给每个Consumer。

对应的分配算法有:

  • 平均分配,AllocateMessageQueueAveragely


  • AllocateMessageQueueAveragelyByCircle:以环状轮流分配Queue


一个queue只分给一个consumer实例,一个consumer实例可以允许同时分到不同的queue。并且保证Consumer的数量小于等于Queue的数量,既保证了负载均衡机制,又避免了资源的浪费。

如果消费消息时选择的是广播模式,那么消息将被投递到一个ConsumerGroup下的所有Consumer。


13. 消息重试是什么?

对于顺序消息来说,Consumer需要按照顺序消费Queue中的消息。那么,当Consume消费消息失败时,RocketMQ会自动不断的进行消息重试,重试的过程中消息的消费会被阻塞。

对于无序消息(普通消息、定时消息、延时消息、事务消息)来说,可以通过设置返回状态达到消息重试的结果。例如,事务型消息中的回查机制,根据消息的不同状态判断是否需要回查。

RocketMQ默认允许每条消息最多重试16次,每次重试的间隔时间为:

第几次重试与上次重试的间隔时间第几次重试与上次重试的间隔时间
110 秒97 分钟
230 秒108 分钟
31 分钟119 分钟
42 分钟1210 分钟
53 分钟1320 分钟
64 分钟1430 分钟
75 分钟151 小时
86 分钟162 小时

如果重试16次消息仍不能正常投递,那么则不再投递。RocketMQ也支持自定义重试的最大次数,重试间隔时间按照如下的策略:

  • 最大重试次数小于等于 16 次,则重试时间间隔同上表描述
  • 最大重试次数大于 16 次,超过 16 次的重试时间间隔均为每次 2 小时

并且重试次数的设置,对于整个Consumer Group下的所有Consumer均生效。


14. 死信队列是什么?

如果一条消息在经过最大的重试次数后,仍然无法被成功的消息,那么它并不会立刻被丢弃,而是将其发送到消费者的死信队列(Dead-Queue)中,此时消息称为死信消息(Dead-Letter)。

死信消息不能再被Consumer正常消费,它经过特殊的处理,仍有机会被重新消费一次;有效期和正常消息一样都是3天,3天后自动删除。每个Consumer Group对应一个死信队列,如果一个Consumer Group没有产生死信消息,那么就不存在死信队列。另外,死信队列中包含了一个Consumer Group下所有Consumer实例订阅的所有Topic的消息。


15. 消息幂等是如何实现的?

当消息在Producer发送到Broker的过程中,Broker投递到Consumer的过程中,或者负载均衡机制执行的过程中,都可能造成消息的重复发送,或者重复投递。为了避免同一条消息被消息多次,需要进行消息幂等处理。此时,不再使用MessageID来作为消息区分的标志,而是使用业务唯一标识来作为幂等处理的关键依据,而业务的唯一标识可以通过消息Key进行设置,Consumer接收到消息后可以根据key进行幂等处理。

本文标签: 多线程虚拟机面试题操作系统常见