java后端的面经

目录

  • 1. static关键字
  • 2. 多态是什么?
  • 3. ArrayList和LinkList的区别
    • 区别
    • ArrayList的扩容机制
  • 4. Java是编译型还是解释型?
  • 5. 什么是编译?什么是解释?
  • 6. String str=“abc” 和 String str = new String(“abc”)的区别?
  • 7. int的0和1可以转换成boolean类型吗?
  • 8. 使用new创建对象
  • 9. 对象的创建的过程和实例化过程
    • 1. 对象的创建的过程
    • 2. 对象的实例化过程
  • 10. 如何停止线程?
  • 11. 你如何理解OOP(面向对象编程)?
  • 12. Java中有哪些容器(集合类)?
  • 13. 什么是并发?什么是线程同步?
    • 如何实现线程同步?
  • 14. 对于锁的理解(自己总结)?
  • 15. synchronized怎么使用?
  • 16. lock和synchronized的对比?
  • 17. 什么是死锁?死锁的解决方法?
  • 18. 构造方法能不能重写?
  • 19. synchronized可以修饰静态方法和静态代码块吗?
  • 20. MySQL的三大范式
  • 21. @Autowired和@Resource注解有什么区别?
  • 22. Elasticsearch查询速度为什么这么快?
  • 23. Java和Python的区别?
  • 24. mysql数据库有什么锁?
  • 25. JVM包含哪几部分?
  • 26. Java中不能做switch参数的数据类型?
  • 27. 迭代、递归和动态规划的区别
  • 28. HashMap的扩容机制和ArrayList的扩容机制?
  • 29. 说说你对Spring Boot的理解(Spring Boot的优点)?
  • 30. mysql优化问题?
  • 31. Redis缓存问题?
    • 缓存穿透
    • 缓存击穿
    • 缓存雪崩
  • 32. Spring中默认的单例Bean如何保证线程安全?
  • 33. 哪些场景下Spring的事务会失效?
  • 34. 什么是CAS?
  • 35. MyIASM和InnoDB区别
  • 36. 为什么选择MySQL?
  • 37. 说一说重写与重载的区别?
  • 38. String、StringBuffer和StringBuilder的区别?
  • 39. 接口(Interface)和抽象类(abstract Class)有什么区别?
  • 40. Java中的异常体系是怎样的?
  • 41. 遇到过异常吗,如何处理?
  • 42. final关键字
  • 43. 泛型的理解
  • 44. 说一说你对Java反射机制的理解
  • 45. JUC的集合类
  • 46. HashMap和Hashtable有什么区别?
  • 47. HashMap为什么用红黑树而不用B树?
  • 48. HashMap线程不安全的体现
  • 49. HashMap如何实现线程安全?
  • 50. HashMap与ConcurrentHashMap有什么区别?
    • 安全性
    • 扩容机制
      • HashMap的扩容机制
      • ConcurrentHashMap的扩容机制
  • 51. 有哪些线程安全的List?
  • 52. 创建线程的方法及区别
  • 53. 线程生命周期
    • 阻塞线程的方式有哪些?
  • 54. 线程池
    • 1. 为什么有线程池?
    • 2. 线程池的特点及其优势
    • 3. 线程池的三大方法
    • 4. 线程池的七大参数
    • 5. 线程池的四大策略
  • 55. Java多线程之间的通信方式
  • 56. sleep()和wait()的区别
  • 57. synchronized的底层实现原理
  • 58. 如果不使用synchronized和Lock,如何保证线程安全?
  • 59. volatile关键字有什么用?
  • 60. 说说你对AQS的理解
  • 61. 公平锁和非公平锁的区别?
  • 62. ThreadLocal和它的应用场景
  • 63. Java程序是怎么运行的?
  • 64. Java的内存分布情况
    • 类存放在哪里?
    • 局部变量存放在哪里?
  • 65. 类加载的过程
  • 66. 谈谈JVM的类加载器
  • 67. 双亲委派模型
  • 68. JVM 参数总结
  • 69. Java的垃圾回收机制
    • 1. GC如何判断对象可以被回收?
    • 2. 垃圾收集算法
    • 3. 垃圾收集器
  • 70. java中堆和栈的区别
  • 71. 讲一下快排算法
  • 72. MySQL主从复制原理是什么?


Java与C/C++、Python的区别?
java后端的面经_第1张图片

解释型语言是可以一边编译一边执行,但是编译型语言必须是要先全部编译完成,才能执行。

  • Java是编译+解释型语言,会生成中间代码.class文件,一次编译到处运行。
  • C/C++是编译型语言,源代码编译成目标代码(机器指令),再由机器执行。
  • Python是解释型语言,一边转换成机器码一边执行,不会生成可执行程序。

为什么Java代码可以实现一次编写、到处运行?

JVM(Java虚拟机)是Java跨平台的关键。
java后端的面经_第2张图片

  • 在程序运行前,Java源代码(.java)需要经过编译器 (javac,jdk提供) 编译成字节码(.class)。
  • 在程序运行时,不同的平台上安装对应的JVM可以运行字节码文件,将字节码翻译成特定平台下的机器码并运行。
  • 同一份Java源代码在不同的平台上运行,它不需要做任何的改变,并且只需要编译一次。而编译好的字
    节码,是通过JVM这个中间的“桥梁”实现跨平台的,JVM是与平台相关的软件,它能将统一的字节码翻译
    成该平台的机器码。

C/C++为什么不行呢?(不能实现跨平台)

C语言编译出来的程序无法二进制跨平台, 每个操作系统带的C/C++编译器不同,
C/C++通过编译生成的目标代码,是直接由机器执行的,不同的机器识别的机器指令不同,因而不能跨平台。


1. static关键字

在java中表示“静态”的意思,主要用于内存管理。
static关键字可以用在变量、方法、代码块和嵌套类上。这些被static修饰之后,会随着类的加载而加载,具有公共的特性。 static关键字的作用是把类的成员变成类相关,而不是实例相关。

静态(static)修饰如下:

  • 变量:称为类变量→静态变量
  • 方法:称为类方法→静态方法
  • 代码块:称为静态代码块
  • 嵌套类:称为静态内部类

2. 多态是什么?

多态就是对象所属的类不同,外部对同一方法的调用执行的逻辑也不同。

特点:

  • 多态的前提1:是继承
  • 多态的前提2:要有方法的重写
  • 父类引用指向子类对象,如:Animal a = new Cat();
  • 多态中,编译看左边,运行看右边
    java后端的面经_第3张图片

(1)除了可以继承父类的方法外

//展示了继承
public class polymorphism {
    public static void main(String[] args) {
        Animal cat = new Cat();
        Animal dog = new Dog();
        cat.shower();
        dog.shower();
    }
}
//父类
class Animal{
    public void shower(){
        System.out.println("宠物洗澡了");
    }
}
//子类1
class Dog extends Animal {
}
//子类2
class Cat extends Animal {
}

(2)重写方法:实现外部对同一方法的调用,执行的逻辑不同

public class polymorphism {
    public static void main(String[] args) {
        Animal cat = new Cat();
        Animal dog = new Dog();
        cat.eat();
        dog.eat();
    }
}

//父类
class Animal{
    public void eat(){
        System.out.println("宠物吃饭了");
    }
}

//子类1
class Dog extends Animal {
    @Override
    public void eat(){
        System.out.println("狗狗吃饭");
    }

}
//子类2
class Cat extends Animal {
    @Override
    public void eat(){
        System.out.println("猫猫吃饭");
    }
}

注意static不能实现多态:

一,java中静态属性和静态方法可以被继承,但是没有被重写(overwrite)而是被隐藏。

二,原因:

  1. 静态方法和属性是属于类的,调用的时候直接通过类名;方法名完成对,不需要继承机制即可以调用;
     (1)如果子类里面定义了静态方法和属性,则这时候父类的静态方法或属性称之为"隐藏";
     (2)如果你想要调用父类的静态方法和属性,直接通过父类名.方法或变量名完成,至于是否继承,子类是有继承静态方法和属性,但是跟实例方法和属性不太一样,存在"隐藏"的这种情况。;
  2. 多态之所以能够实现依赖于继承、接口和重写、重载(继承和重写最为关键),有了继承和重写就可以实现父类的引用指向不同子类的对象;
  3. 重写的功能是:"重写"后子类的优先级要高于父类的优先级,但是“隐藏”是没有这个优先级之分的。
  4. 静态属性、静态方法和非静态的属性都可以被继承和隐藏而不能被重写,因此不能实现多态,不能实现父类的引用可以指向不同子类的对象。
  5. 非静态方法可以被继承和重写,因此可以实现多态。

3. ArrayList和LinkList的区别

区别

  1. ArrayList的实现是基于数组来实现的,LinkedList的基于双向链表来实现。这两个数据结构的逻辑关系是不一样,当然物理存储的方式也会是不一样。
  2. LinkedList比ArrayList更占内存,因为LinkedList的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
  3. 当随机访问List(get和set操作)时,ArrayList比LinkedList的效率更高,因为LinkedList是线性的数据存储方式,所以需要移动指针从前往后依次查找。
  4. 当对数据进行增加和删除的操作(add和remove操作)时,LinkedList比ArrayList的效率更高,因为ArrayList是数组,所以在其中进行增删操作时,会对操作点之后所有数据的下标索引造成影响,需要进行数据的移动。

ArrayList的扩容机制

ArrayList的扩容机制
ArrayList的底层是用数组来实现的,默认第一次插入元素时创建大小为10的数组,超出限制时会增加50%的容量 (1.5倍扩容),并且数据以 System.arraycopy() 复制到新的数组,因此最好能给出数组大小的预估值。

4. Java是编译型还是解释型?

java 即是编译型又是解释型,.java文件先通过编译器编译成 .class文件 ,再通过jvm的解释器,解释成机器语言让操作系统直接运行。
java后端的面经_第4张图片

5. 什么是编译?什么是解释?

  • 编译(Compile)的过程是把整个源程序代码翻译成另外一种代码,翻译后的代码等待被执行或者被优化等等,发生在运行之前,产物是另一份代码。
  • 解释(Interpret)的过程是把源程序代码一行一行的读懂,然后一行一行的执行,发生在运行时,产物是运行结果。
  • 编译和解释的过程上的区别:编译是将源程序翻译成可执行的目标代码,翻译与执行是分开的;而解释是对源程序的翻译与执行一次性完成,不生成可存储的目标代码。

C/C++/C#等都是编译型语言。 以C语言为例,源代码被编译之后生成中间文件(.o和.obj),然后用链接器和汇编器生成机器码,也就是一系列基本操作的序列,机器码最后被执行生成最终动作。
java后端的面经_第5张图片

Lisp/R/Python等都是解释型语言。

其实许多编程语言同时采用编译器与解释器来实现,这就包括Python,Java等,先将代码编译为字节码,在运行时再进行解释。所谓“解释型语言”(Python)并不是不用编译,而只是不需要用户显式去使用编译器得到可执行代码而已 。

java后端的面经_第6张图片

6. String str=“abc” 和 String str = new String(“abc”)的区别?

String类的内存分配

  1. String str1=“abc”
    • 当代码使用这种方式创建时,JVM首先检查该对象是否存在字符串常量池中,若在,就返回该对象的引用,否则新的字符串将在常量池中创建,这种方式可以减少同一值得字符串对象的重复创建,节约内存。
    • 然后在栈内存中开辟一个名字为str1的空间,来存储“abc”在常量池中的地址值。
  2. String str2 = new String(“abc”)
    • 首先在编译类文件时,“abc”常量字符串将会放在常量池中,在类加载的时候,“abc”将会在常量池中创建;
    • 其次在调用new时,JVM命令将会调用String的构造函数,同时引用常量池中的“abc”字符串,在堆内存中创建一个String对象;
    • 在栈中开辟名字为str2的空间,存放堆中new出来的这个String对象的地址值。

显然,采用new的方式会多创建一个对象出来,会占用更多的内存,所以 一般建议使用直接量的方式创建字符串。

7. int的0和1可以转换成boolean类型吗?

不能!不能对布尔值进行转换!(布尔值是按位的)

8. 使用new创建对象

使用new关键字创建对象的时候:

  1. 除了分配内存空间(栈、堆)
    (图的左边是栈,右边是堆)
    java后端的面经_第7张图片
  2. 还会给创建好的对象进行默认的初始化
  3. 对类中构造器的调用

9. 对象的创建的过程和实例化过程

1. 对象的创建的过程

  • Step1:类加载检查
    虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

  • Step2:分配内存
    在类加载检查通过后,接下来虚拟机将为新生对象分配内存。
    内存分配的两种方式 :
    指针碰撞 :适用场合 :堆内存规整(即没有内存碎片)的情况下。
    空闲列表 :适用场合 : 堆内存不规整的情况下。

  • Step3:初始化零值
    内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

  • Step4:设置对象头
    初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

  • Step5:执行 init 方法
    在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始, 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

2. 对象的实例化过程

对象实例化过程,就是执行类构造函数对应在字节码文件中的 < init>() 方法(实例构造器), < init>()方法由非静态变量、非静态代码块以及对应的构造器组成。

  • < init>() 方法可以重载多个,类有几个构造器就有几个 < init>() 方法;
  • < init>() 方法中的代码执行顺序:父类变量初始化、父类代码块、父类构造器、子类变量初始化、子类代码块、子类构造器。

静态变量、静态代码块、普通变量、普通代码块、构造器的执行顺序如下图:
java后端的面经_第8张图片

具有父类的子类的实例化顺序如下:
java后端的面经_第9张图片

10. 如何停止线程?

在java中有3种方法可以使正在运行的线程终止运行:

  1. 使用stop()方法强行终止线程,但是这个方法不推荐使用,因为stop()和suspend()、resume()一样,都是作废过期的方法,使用它们可能发生不可预料的结果。【已废弃】
  2. 使用退出标志位使线程正常退出。
  3. 使用interrupt()方法中断线程。

java后端的面经_第10张图片

11. 你如何理解OOP(面向对象编程)?

从两方面回答:

  • 一方面是面向对象和面向过程的区别;
  • 另一方面是面向对象的三大特性(封装、继承、多态)。

12. Java中有哪些容器(集合类)?

java容器

Java中的集合类主要由Collection和Map这两个接口派生而出,其中Collection接口又派生出三个子接
口,分别是Set、List、Queue。所有的Java集合类,都是Set、List、Queue、Map这四个接口的实现
类,这四个接口将集合分成了四大类,其中:

  • Collection:
    • Set 代表无序的,元素不可重复的集合;
    • List 代表有序的,元素可以重复的集合;
    • Queue 代表先进先出(FIFO)的队列;
  • Map:
    • Map 代表具有映射关系(key-value)的集合。

java后端的面经_第11张图片

13. 什么是并发?什么是线程同步?

并发: 多个程序访问同一个对象。

线程同步: 多个操作在同一时间内,只能有一个线程进行,其他的线程要等此线程执行完了之后才可以继续执行。(同步就是协同步调,按预定的先后次序进行运行。)

线程同步的形成条件:队列 + 锁

什么时候需要线程同步?
处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改该对象。这时候我们就需要线程同步。
线程同步其实时一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程执行完毕,下一个线程再使用。

如何实现线程同步?

简单回答:
由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突的问题,为了保证数据在方法中被访问时的正确性,在访问时加入了锁机制 synchronized / lock ,当一个线程获得对象的锁,独占资源,其他线程必须等待,使用后释放锁即可。

详细回答:

  1. 同步方法
    即有synchronized关键字修饰的方法,由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。需要注意, synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。
  2. 同步代码块
    即有synchronized关键字修饰的语句块,被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。需值得注意的是,同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
  3. ReentrantLock
    Java 5新增了一个java.util.concurrent包来支持同步,其中ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。需要注意的是,ReentrantLock还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,因此不推荐使用。
  4. volatile
    volatile关键字为域变量的访问提供了一种免锁机制,使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,因此每次使用该域就要重新计算,而不是使用寄存器中的值。需要注意的是,volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。
  5. 原子变量
    在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,使用该类可以简化线程同步。例如AtomicInteger 表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器),但不能用于替换Integer。可扩展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。

14. 对于锁的理解(自己总结)?

厕所举例:
同一资源相当于厕所位,一个线程拿锁进去,其他线程等待,使用完后释放锁。

15. synchronized怎么使用?

synchronized作为关键字,实现线程同步有两种用法:

  1. synchronized方法:锁的是this
  2. synchronized代码块:锁的是obj
synchronized (obj){

}

要实现线程同步我们应该锁变化的量,即需要增删改的对象

16. lock和synchronized的对比?

  1. synchronized是 Java关键字,在JVM层面实现加锁和解锁;
    Lock是一个 接口,在代码层面实现加锁和解锁。
  2. synchronized可以用在 代码块上、方法上;Lock只能写在代码块里。
  3. synchronized在代码执行完或出现异常时自动释放锁;
    Lock不会自动释放锁,需要在finally中显示释放锁。
  4. synchronized会导致线程拿不到锁一直等待;
    Lock可以设置获取锁失败的超时时间。
  5. synchronized无法得知是否获取锁成功;
    Lock则可以通过tryLock得知加锁是否成功。
  6. synchronized锁可重入、不可中断、非公平;
    Lock锁可重入、可中断、可公平/不公平,并可以细分读写锁以提高效率。

17. 什么是死锁?死锁的解决方法?

死锁的四个必要条件和解决办法

死锁概念及产生原理

  • 概念: 多个并发进程因争夺系统资源而产生相互等待的现象。
  • 原理: 当一组进程中的每个进程都在等待某个事件发生,而只有这组进程中的其他进程才能触发该事件,这就称这组进程发生了死锁。
  • 本质原因:
    • 1)系统资源有限。
    • 2)进程推进顺序不合理。

死锁产生的4个必要条件

  1. 互斥: 某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。
  2. 占有且等待: 一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。
  3. 不可抢占: 别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。
  4. 循环等待: 存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源。

当以上四个条件均满足,必然会造成死锁,发生死锁的进程无法进行下去,它们所持有的资源也无法释放。这样会导致CPU的吞吐量下降。所以死锁情况是会浪费系统资源和影响计算机的使用性能的。破坏其中一个或几个条件就可以解决死锁问题。

18. 构造方法能不能重写?

不能,因为构造方法要和类名相同,如果子类重写父类的构造方法,那么子类将会存在于类名不同的构造方法,这与构造方法的要求是矛盾的。

19. synchronized可以修饰静态方法和静态代码块吗?

synchronized可以修饰静态方法,但不能修饰静态代码块。

当修饰静态方法时,监视器锁(monitor)便是对象的Class实例,因为Class数据存在于永久代,因此静态方法锁相当于该类的一个全局锁。

20. MySQL的三大范式

MySQL之数据库三大范式
什么是范式?
范式就是一种规则约束,数据库范式就是通过约束来优化数据库数据的存储方式,没有规矩不成方圆,没有约束就没有性能卓越的。

最常用的三大范式:

  1. 第一范式(1NF): 属性不可分割,每个属性都是不可分割的原子项。(实体的属性即表中的列)
  2. 第二范式(2NF): 满足第一范式;且不存在部分依赖,即非主属性必须完全依赖于主属性。(主属性即主键;完全依赖是针对于联合主键的情况,非主键不能只依赖于主键的一部分)
  3. 第三范式(3NF): 满足第二范式;且不存在传递依赖,即非主属性不能与非主属性之间有依赖关系,非主属性必须直接依赖于主属性,不能间接依赖主属性。(A -> B, B ->C, A -> C)

21. @Autowired和@Resource注解有什么区别?

  1. @Autowired是Spring提供的注解,@Resource是JDK提供的注解。
  2. @Autowired是先按类型注入,默认情况下它要求依赖对象必须存在,如果允许null值,可以
    设置它required属性为false,如果我们想使用按名称装配,可以结合@Qualifier注解一起使用。
  3. @Resource是先按名称注入,@Resource有两个中重要的属性:name和type。@Resource如
    果没有指定name属性,并且按照默认的名称仍然找不到依赖对象时, @Resource注解会回退到
    按类型装配。
    但一旦指定了name属性,就只能按名称装配了。

22. Elasticsearch查询速度为什么这么快?

ElasticSearch为何查询速度快(秒懂)
ElasticSearch搜索引擎常见面试题总结

23. Java和Python的区别?

Java和Python区别

24. mysql数据库有什么锁?

MySQL中有哪些锁?

25. JVM包含哪几部分?

JVM 主要由四大部分组成:类加载器运行时数据区执行引擎本地库接口
java后端的面经_第12张图片

  • ClassLoader:负责加载字节码文件即 class 文件,class 文件在文件开头有特定的文件标示,并且ClassLoader 只负责class 文件的加载,至于它是否可以运行,则由 Execution Engine 决定。
  • Runtime Data Area:是存放数据的,分为五部分:Stack(虚拟机栈),Heap(堆),Method Area(方法区),PC Register(程序计数器),Native Method Stack(本地方法栈)。几乎所 有的关于 Java内存方面的问题,都是集中在这块。
  • Execution Engine:执行引擎,也叫 Interpreter(命令解释器)。Class文件被加载后,会把指令和数据信息放入内存中 ,Execution Engine 则负责把这些命令解释给操作系统,即将JVM指令集翻译为操作系统指令集
  • Native Interface:负责调用本地接口的。他的作用是调用不同语言的接口给 JAVA 用,他会在Native Method Stack 中记录对应的本地方法,然后调用该方法时就通过 Execution Engine 加载对应的本地lib。原本多用于一些专业领域,如JAVA驱动,地图制作引擎等,现在关于这种本地方法接口的调用已经被类似于Socket通信,WebService等方式取代。

26. Java中不能做switch参数的数据类型?

可以作为switch参数数据类型的有:int、bype、short、char、String、枚举(整数、枚举、字符、字符串)

不能作为switch参数的有:long、float、double、boolean、复杂的表达式

  • 测试1:String
 String a = "哈哈";
 switch(a) {
     case "嘻嘻":
         System.out.println("星期一");
         break;
     case "哈哈":
         System.out.println("星期二");
         break;
     case "拉拉":
         System.out.println("星期三");
         break;
     default:
         System.out.println("输入有误");
         break;
 }

输出:

星期二
  • 测试2:int
 int day = 6;
 switch(day) {
     case 1:
         System.out.println("星期一");
         break;
     case 2:
         System.out.println("星期二");
         break;
     case 3:
         System.out.println("星期三");
         break;
     case 4:
         System.out.println("星期四");
         break;
     case 5:
         System.out.println("星期五");
         break;
     case 6:
         System.out.println("星期六");
         break;
     case 7:
         System.out.println("星期日");
         break;
     default:
         System.out.println("输入有误");
         break;
 }

输出:

星期六
  • 测试3:包装类Integer
 Integer day = new Integer(6);
 switch(day) {
     case 1:
         System.out.println("星期一");
         break;
     case 2:
         System.out.println("星期二");
         break;
     case 3:
         System.out.println("星期三");
         break;
     case 4:
         System.out.println("星期四");
         break;
     case 5:
         System.out.println("星期五");
         break;
     case 6:
         System.out.println("星期六");
         break;
     case 7:
         System.out.println("星期日");
         break;
     default:
         System.out.println("输入有误");
         break;
 }

输出:

星期六
  • 测试4:long 爆红
    java后端的面经_第13张图片

27. 迭代、递归和动态规划的区别

  1. 递归:重复调用函数自身实现循环。将一个大问题拆解成一个小问题,先解决大问题到小问题。
    优点: a.用有限的循环语句实现无限集合;b.大问题转化成小问题,减少了代码量。
    缺点: 所需空间大,可能会出现子问题的重复计算(需要剪枝),递归太深时容易栈溢出。

斐波那契数列:1,1,2,3,5,8,11,13…
f(n)
=f(n-1)+f(n-2)
=f(n-2)+f(n-3)+f(n-3)+f(n-4)
=…

int fibonacci(int n)
{
	if (n <= 2)
		return 1;
	else
		return fibonacci(n - 1) + fibonacci(n - 2);
}

  1. 迭代:利用变量的原值推出新值,数内某段代码实现循环。
    优点: 计算效率高,无额外内存开销。
    缺点: 代码不如递归简洁,有时不容易理解。

斐波那契数列:1,1,2,3,5,8,11,13…
f(3)=f(2)+f(1)
f(4)=f(3)+f(2)
f(5)=f(4)+f(3)

int fibonacci(int n)
{
	if (n <= 2)
		return 1;
		
	int first = 1, second = 1, answer;
	for (int i = 3; i <= n; i++)
	{
		answer = first + second;
		first = second;
		second = answer;
	}
	
	return answer;
}

  1. 动态规划:思想类似于递归,动态规划储存每个小问题的结果,先解决小问题最后到大问题。
    优点: 比迭代还要更快一点
    缺点: 用空间换时间,所需空间大

斐波那契数列:1,1,2,3,5,8,11,13…

int fibonacci(int n)
{
	vector <int> dp;
	dp.push_back(0);
	dp.push_back(1);
 
	for (int i = 3; i <= n; i++)
		dp.push_back(dp[i-1] + dp[i-2]);
		
	return dp[n];
}

28. HashMap的扩容机制和ArrayList的扩容机制?

hashmap是2倍扩容,ArrayList是1.5倍扩容。
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。

29. 说说你对Spring Boot的理解(Spring Boot的优点)?

Spring Boot作为Spring的脚手架框架,其设计目的是用来简化Spring应用得初始搭建以及开发过程,以达到快速构建项目、预置三方配置、开箱即用的目的。Spring Boot有如下的优点:

  • 自动配置(简化常用工程相关配置)
    Spring Boot 提供 Spring 框架的最大自动化配置,大量使用自动配置,使得开发者对 Spring 的配置尽量减少;
  • 更快的构建能力:
    提供通过 Maven (或者 Grandle )依赖的starter ,starter可以直接获取开发所需的相关包,用于快速构建项目;
  • 起步依赖:
    创建springboot时可以直接勾选依赖模块,简化依赖配置;
  • 内嵌容器支持:
    springboot内嵌了servlet容器,应用不需要打包成WAR;
  • 提供运行时的应用监控;
  • parent(spring-boot-starter-parent)让我们写依赖的时候不需要写版本号,极大地避免了版本冲突问题(依赖管理,不是依赖)

java后端的面经_第14张图片

类 / 配置文件 Spring SpringBoot
pom文件中的坐标 手工添加 勾选添加(创建项目时)
web3.0配置类 手工制作
Spring/SpringMVC配置类 手工制作
控制器 手工制作 手工制作

30. mysql优化问题?

思路总结:主要考虑数据库优化与SQL语句优化。

  1. 数据库优化,包括存储引擎的优化,缓存的优化和内存的优化等。
  2. SQL优化,首先先判断什么样的SQL需要优化。
    • 可以在MySQL中开启慢查询,设置成例如SQL执行时长超过5秒就可以定为慢SQL,并记录到日志中。然后拿到慢SQL的执行记录和计划。
    • 通过explain关键字做分析。分析思路有例如SQL存在索引,判断是否执行了索引?
      ①. 有索引但索引失效了那就探究一下索引失效原因(比如在使用LIKE关键字进行查询的查询语句中,如果匹配字符串的第一个字符为“%”,索引不会起作用。只有“%”不在第一个位置,索引才会起作用。);
      ②. 若索引未失效则要考虑索引创建是否合理,以及是否遵循最左匹配原则等。

31. Redis缓存问题?

Redis缓存穿透、缓存击穿缓存雪崩1
Redis缓存穿透、缓存击穿缓存雪崩2
java后端的面经_第15张图片

缓存穿透

当有大量查询请求未命中缓存时,引起对后台数据库的频繁访问,导致数据库负载压力增大,这种现象就叫做缓存穿透。

引起的原因:
大量访问不存在的key,导致数据库处理大量请求。(访问缓存,缓存没有就会访问数据库)
java后端的面经_第16张图片
解决方法:

  1. 将无效的key存进Redis中,若果数据库查询某个key不存在时,同样将这个key缓存到Redis缓存中,并设置value为NULL,表示不存在。如果攻击请求的key每次都相同,该方法有效;如果攻击请求的key每次随机生成,则同样会产生缓存穿透问题。
  2. 使用布隆过滤器,过滤掉一些不存在的key。布隆过滤器判定为true时,key可能存在于数据库中,也可能不存在;判定为false时,key一定不存在于数据库。

缓存击穿

当Redis中存在某些极热点数据时,即有大量请求并发访问的key-value数据。当极热点key-value数据突然失效时,缓存未命中引起对后台数据库的频繁访问,这种现象叫缓存击穿。

引起的原因
缓存上极热点数据突然失效 (点)
java后端的面经_第17张图片
解决方法

  1. 对极热点key设置永不过期
  2. 使用互斥锁。如果缓存失效的情况,只有拿到锁才可以查询数据库,降低了在同一时刻访问数据库的请求量,防止数据库崩溃。缺点是会导致系统的性能变差。

缓存雪崩

当某⼀时刻发⽣⼤规模的缓存失效的情况,例如缓存服务宕机、大量key在同一时间过期,这样的后果就是⼤量的请求进来直接打到DB上,可能导致整个系统的崩溃,称为雪崩。如果运维重启宕机的数据库,马上又会有大量新的请求流量到来,再次引起数据库宕机。

可能的原因

  1. redis宕机、重启
  2. 大量数据使用了同一过期时间 (面)
    java后端的面经_第18张图片

解决方法
3. 引入随机性,在原有缓存失效时间上加上一个随机值,避免大量数据在同一时间失效。
4. 通过请求限流、熔断机制、服务降级等手段,降低服务器负载。
5. 实现缓存组件的高可用,防止单点故障、机器故障、机房宕机等一系列问题。
6. 提高数据后台数据库的容灾能力。

32. Spring中默认的单例Bean如何保证线程安全?

SpringBean默认是单例的,高并发情况下,如何保证并发安全
Spring官方提供的bean,一般提供了通过 ThreadLocal 去解决线程安全的方法。

33. 哪些场景下Spring的事务会失效?

spring事务什么时候会失效
java后端的面经_第19张图片

34. 什么是CAS?

Java 并发机制实现 原子操作 有两种: 一种是锁,一种是CAS。 (无锁的CAS操作在性能上要比同步锁高很多。)

CAS实际是普遍处理器都支持的一条指令,这条指令通过判断当前内存值V、旧的预期值A、即将更新的值B是否相等来对比并设置新值,从而实现变量的原子性。

CAS的原理: 通过三个参数,当前内存的变量值V、旧的预期值A、即将更新的值B。通过判断是V和A是否相等查看当前变量值是否被其他线程改变,如果相等则变量没有被其他线程更改,就把B值赋予V;如果不相等则做自旋操作(自旋就是不断尝试CAS操作,通常设置自旋次数防止死循环)。

悲观锁:synchronized
乐观锁:CAS (虽然叫锁,但它实际上并没有用到锁,而是一种无锁的同步机制)

CAS的缺点主要有3点:

  1. ABA问题: ABA的问题指的是在CAS更新的过程中,当读取到的值是A,然后准备赋值的时候仍然是A,但是实际上有可能A的值被改成了B,然后又被改回了A,这个CAS更新的漏洞就叫做ABA。只是ABA的问题大部分场景下都不影响并发的最终效果。
    Java中有AtomicStampedReference来解决这个问题,他加入了预期标志和更新后标志两个字段,更新时不光检查值,还要检查当前的标志是否等于预期标志,全部相等的话才会更新。
  2. 循环时间长开销大: 自旋CAS的方式如果长时间不成功,会给CPU带来很大的开销。
  3. 只能保证一个共享变量的原子操作: 只对一个共享变量操作可以保证原子性,但是多个则不行,多个可以通过AtomicReference来处理或者使用锁synchronized实现。

35. MyIASM和InnoDB区别

MYISAM INNODB
事务支持 不支持 支持
数据行锁定 不支持 支持
外键约束 不支持 支持
全文索引 支持 不支持
表空间的大小 较小 较大,约为MYISAM的2倍

常规使用操作:
MYISAM 节约空间,速度较快
INNODB 安全性高,事务的处理,多表多用户操作

36. 为什么选择MySQL?

  1. mysql是开源免费的,可以节省开发成本;
  2. “PHP+mysql”的组合是网站开发者的首选,得益于PHP语言,mysql也受到很大的追捧;
  3. 大多数服务器使用的是linux系统,而linux服务器使用最多的PHP环境架构,因此mysql在linux中得到广泛使用;
  4. MySQL易学易用;
  5. 具有灵活性和可扩展性,使MySQL可以根据用户当前系统的需要来进行调整。

37. 说一说重写与重载的区别?

重载: 发生了 同一类 中,若多个方法之间 方法名相同,参数列表不同,则他们构成重载关系。重载与方法的返回值以及访问修饰符无关,即 重载的方法不能根据返回类型进行区分。

重写: 发生在 父类子类 中,子类想重写父类的方法,那 它的方法名、参数列表必须与父类方法相同。另外,返回值要小于等于父类方法,抛出的异常要小于等于父类方法,访问修饰符则要大于等于父类方法。还有,若父类方法的访问修饰符为private,则子类不能对其重写。

38. String、StringBuffer和StringBuilder的区别?

  • String 字符串常量(final修饰,不可变,线程安全),StringBuffer 字符串变量(线程安全),StringBuilder 字符串变量(非线程安全)。
  • 性能对比:StringBuilder > StringBuffer > String

39. 接口(Interface)和抽象类(abstract Class)有什么区别?

Java中抽象类和接口的介绍及二者间的区别

相同:

  1. 接口和抽象类都不能被实例化,它们都位于继承树的顶端,用于被其他类实现和继承。
  2. 接口和抽象类都可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法。

区别:

  1. 接口里只能包含抽象方法、静态方法、默认方法和私有方法,不能为普通方法提供方法实现;
    抽象类则完全可以包含普通方法。
  2. 接口里只能定义静态常量,不能定义普通成员变量;
    抽象类里则既可以定义普通成员变量,也可以定义静态常量。
  3. 接口里不包含构造器;
    抽象类里可以包含构造器,抽象类里的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作。
  4. 一个类最多只能有一个直接父类,包括抽象类;
    但一个类可以直接实现多个接口,通过实现多个接口可以弥补Java单继承的不足。

注意: 由于接口定义的是一种规范,因此 接口里不能包含构造器和初始化块定义
接口里可以包含成员变量(只能是静态常量)、方法(只能是抽象实例方法、类方法、默认方法或私有方法)、内部类(包括内部接口、枚举)定义。

40. Java中的异常体系是怎样的?

java后端的面经_第20张图片

  1. Throwable是异常的顶层父类,代表所有的非正常情况。它有两个直接子类,分别是Error、Exception。
  2. Error是错误,一般是指与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态链接失败等,这种错误无法恢复或不可能捕获,将导致应用程序中断。通常应用程序无法处理这些错误,因此应用程序不应该试图使用catch块来捕获Error对象。在定义方法时,也无须在其throws子句中声明该方法可能抛出Error及其任何子类。
  3. Exception是异常,它被分为两大类,分别是Checked异常和Runtime异常。
    所有的RuntimeException类及其子类的实例被称为Runtime异常;不是RuntimeException类及其子类的异常实例则被称为Checked异常。Java认为Checked异常都是可以被处理(修复)的异常,所以Java程序必须显式处理Checked异常。如果程序没有处理Checked异常,该程序在编译时就会发生错误,无法通过编译。Runtime异常则更加灵活,Runtime异常无须显式声明抛出,如果程序需要捕获Runtime异常,也可以使用try…catch块来实现。

41. 遇到过异常吗,如何处理?

处理异常的语句由try、catch、finally三部分组成。
try块用于包裹业务代码,catch块用于捕获并处理某个类型的异常,finally块则用于回收资源。

在Java中,可以按照如下三个步骤处理异常:

  1. 捕获异常
    将业务代码包裹在try块内部,当业务代码中发生任何异常时,系统都会为此异常创建一个异常对象。创建异常对象之后,JVM会在try块之后寻找可以处理它的catch块,并将异常对象交给这个catch块处理。
  2. 处理异常
    在catch块中处理异常时,应该先记录日志,便于以后追溯这个异常。然后根据异常的类型、结合当前的业务情况,进行相应的处理。比如,给变量赋予一个默认值、直接返回空值、向外抛出一个新的业务异常交给调用者处理,等等。
  3. 回收资源
    如果业务代码打开了某个资源,比如数据库连接、网络连接、磁盘文件等,则需要在这段业务代码执行完毕后关闭这项资源。并且,无论是否发生异常,都要尝试关闭这项资源。将关闭资源的代码写在finally块内,可以满足这种需求,即 无论是否发生异常,finally块内的代码总会被执行。

42. final关键字

final关键字可以修饰类、方法、变量,以下是final修饰这3种目标时表现出的特征:

  • final类:final关键字修饰的类不可以被继承。(断子绝孙)
  • final方法:final关键字修饰的方法不可以被重写。(不可变)
  • final变量:final关键字修饰的变量,一旦获得了初始值,就不可以被修改。(不可变)

43. 泛型的理解

java 泛型全解 - 绝对最详细
以前集合里的对象都是Object类型,从Java 5开始,Java引入了“参数化类型”的概念,允许程序在创建集合时指定集合元素的类型,Java的参数化类型被称为泛型(Generic)。例如 List ,表明该List只能保存字符串类型的对象。
Java泛型的编译问题
public BoundedEcho echo(BoundedEcho value) { return value; } BoundedEcho numberEcho = new BoundedEcho(); 实例化的时候你把T声明成了Number,之后调用就必须是BoundedEcho。原因是BoundedEcho等类型和BoundedEcho是不同的类,并不存在继承关系。

44. 说一说你对Java反射机制的理解

Java反射详解

具体来说,通过反射机制,我们可以实现如下的操作:

  • 程序运行时,可以通过反射获得任意一个类的Class对象,并通过这个对象查看这个类的信息;
  • 程序运行时,可以通过反射创建任意一个类的实例,并访问该实例的成员;
  • 程序运行时,可以通过反射机制生成一个类的动态代理类或动态代理对象。

45. JUC的集合类

从Java5开始,Java在java.util.concurrent包下提供了大量支持高效并发访问的集合类,它们既能包装
良好的访问性能,有能包装线程安全。这些集合类可以分为两部分,它们的特征如下:

  • 以Concurrent开头的集合类:
    以Concurrent开头的集合类代表了支持并发访问的集合,它们可以支持多个线程并发写入访问,这些写入线程的所有操作都是线程安全的,但读取操作不必锁定。以Concurrent开头的集合类采用了更复杂的算法来保证永远不会锁住整个集合,因此在并发写入时有较好的性能。
  • 以CopyOnWrite开头的集合类:
    以CopyOnWrite开头的集合类采用复制底层数组的方式来实现写操作。当线程对此类集合执行读取操作时,线程将会直接读取集合本身,无须加锁与阻塞。当线程对此类集合执行写入操作时,集合会在底层复制一份新的数组,接下来对新的数组执行写入操作。由于对集合的写入操作都是对数组的副本执行操作,因此它是线程安全的。

java.util.concurrent包下线程安全的集合类的体系结构:
java后端的面经_第21张图片

46. HashMap和Hashtable有什么区别?

  1. HashMap⽅法没有synchronized修饰,线程⾮安全;Hashtable的实现方法里面都添加了synchronized关键字,线程安全;
  2. HashMap允许key和value为null,⽽HashTable不允许。

47. HashMap为什么用红黑树而不用B树?

红黑树是平衡二叉树,B树是平衡多叉树。
B树的节点可以存储多个数据,当数据量不够多时,数据都会”挤在“一个节点中,查询效率会退化为链表。

48. HashMap线程不安全的体现

  1. JDK1.7,当并发执行扩容操作时会造成死循环和数据丢失的情况。
  2. JDK1.8,在并发执行put操作时会发生数据覆盖的情况。

49. HashMap如何实现线程安全?

  1. 直接使用ConcurrentHashMap;
  2. 使用Collections将HashMap包装成线程安全的Map。
  3. 直接使用Hashtable类(性能差,尽量少用);

50. HashMap与ConcurrentHashMap有什么区别?

安全性

HashMap线程不安全,ConcurrentHashMap线程安全

扩容机制

HashMap的扩容机制

1.7版本

  1. 先⽣成新数组
  2. 遍历⽼数组中的每个位置上的链表上的每个元素
  3. 取每个元素的key,并基于新数组⻓度,计算出每个元素在新数组中的下标
  4. 将元素添加到新数组中去
  5. 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性

1.8版本

  1. 先⽣成新数组
  2. 遍历⽼数组中的每个位置上的链表或红⿊树
  3. 如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去
  4. 如果是红⿊树,则先遍历红⿊树,先计算出红⿊树中每个元素对应在新数组中的下标位置
    a. 统计每个下标位置的元素个数
    b. 如果该位置下的元素个数超过了8,则⽣成⼀个新的红⿊树,并将根节点的添加到新数组的对应位置
    c. 如果该位置下的元素个数没有超过8,那么则⽣成⼀个链表,并将链表的头节点添加到新数组的对应位置
  5. 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性

ConcurrentHashMap的扩容机制

1.7版本

  1. 1.7版本的ConcurrentHashMap是基于Segment分段实现的
  2. 每个Segment相对于⼀个⼩型的HashMap
  3. 每个Segment内部会进⾏扩容,和HashMap的扩容逻辑类似
  4. 先⽣成新的数组,然后转移元素到新数组中
  5. 扩容的判断也是每个Segment内部单独判断的,判断是否超过阈值

1.8版本

  1. 1.8版本的ConcurrentHashMap不再基于Segment实现
  2. 当某个线程进⾏put时,如果发现ConcurrentHashMap正在进⾏扩容那么该线程⼀起进⾏扩容
  3. 如果某个线程put时,发现没有正在进⾏扩容,则将key-value添加到ConcurrentHashMap中,然后判断是否超过阈值,超过了则进⾏扩容
  4. ConcurrentHashMap是⽀持多个线程同时扩容的
  5. 扩容之前也先⽣成⼀个新的数组
  6. 在转移元素时,先将原数组分组,将每组分给不同的线程来进⾏元素的转移,每个线程负责⼀组或多组的元素转移⼯作

51. 有哪些线程安全的List?

  1. Vector
    Vector是比较古老的API,虽然保证了线程安全,但是由于效率低一般不建议使用。
  2. Collections.SynchronizedList
    SynchronizedList是Collections的内部类,Collections提供了synchronizedList方法,可以将一个线程不安全的List包装成线程安全的List,即SynchronizedList。它比Vector有更好的扩展性和兼容性,但是它所有的方法都带有同步锁,也不是性能最优的List。
  3. CopyOnWriteArrayList
    CopyOnWriteArrayList是Java 1.5在java.util.concurrent包下增加的类,它采用复制底层数组的方式来实现写操作。当线程对此类集合执行读取操作时,线程将会直接读取集合本身,无须加锁与阻塞。当线程对此类集合执行写入操作时,集合会在底层复制一份新的数组,接下来对新的数组执行写入操作。由于对集合的写入操作都是对数组的副本执行操作,因此它是线程安全的。在所有线程安全的List中,它是性能最优的方案。

CopyOnWriteArrayList

CopyOnWriteArrayList是Java并发包里提供的并发类,简单来说它就是一个线程安全且读操作无锁的ArrayList。正如其名字一样,在写操作时会复制一份新的List,在新的List上完成写操作,然后再将原引用指向新的List。这样就保证了写操作的线程安全。(上锁的写操作不会影响到并发访问的读操作:写备份,加锁;读原份,不加锁)

优点:读操作性能很高,因为无需任何同步措施,比较适用于读多写少的并发场景。在遍历传统的List时,若中途有别的线程对其进行修改,则会抛出ConcurrentModificationException异常。而CopyOnWriteArrayList由于其"读写分离"的思想,遍历和修改操作分别作用在不同的List容器,所以在使用迭代器进行遍历时候,也就不会抛出ConcurrentModificationException异常了。
缺点:一是内存占用问题,毕竟每次执行写操作都要将原容器拷贝一份,数据量大时,对内存压力较大,可能会引起频繁GC。二是无法保证实时性,Vector对于读写操作均加锁同步,可以保证读和写的强一致性。而CopyOnWriteArrayList由于其实现策略的原因,写和读分别作用在新老不同容器上,在写操作执行过程中,读不会阻塞但读取到的却是老容器的数据。

52. 创建线程的方法及区别

Java创建线程的五种方法

  • 方式1:通过继承Thread类创建线程
  • 方式2:通过实现Runnable接口创建线程
  • 方式3:使用Callable和Future来创建线程
  • 方法4:通过线程池ThreadPoolExecutor来创建线程

--------------------------------------------------------四种创建线程方法对比-------------------------------------------------------

实现Runnable和实现Callable接口的方式基本相同,不过是后者执行call()方法有返回值,后者线程执行体run()方法无返回值,因此可以把这两种方式归为一种这种方式与继承Thread类的方法之间的差别如下:

  1. 线程只是实现Runnable或实现Callable接口,还可以继承其他类。
  2. Runnable和Callable情况下,多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。
  3. Runnable和Callable编程稍微复杂,如果需要访问当前线程,必须调用Thread.currentThread()方法。
  4. 继承Thread类的线程类不能再继承其他父类(Java单继承决定)。
  5. Thread、Runnable和Callable这三种线程如果创建关闭频繁会消耗系统资源影响性能,而使用线程池可以不用线程的时候放回线程池,用的时候再从线程池取,项目开发中主要使用线程池。

注:在前三种中一般推荐采用实现接口的方式来创建多线程

53. 线程生命周期

线程通常有五种状态,创建,就绪,运⾏、阻塞和死亡状态。
线程5种状态的转换关系,如下图所示:
java后端的面经_第22张图片

阻塞线程的方式有哪些?

当发生如下情况时,线程将会进入阻塞状态:

  • 线程调用sleep()方法主动放弃所占用的处理器资源。
  • 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。
  • 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。
  • 线程在等待 wait 某个通知(notify)。
  • 程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法。

针对上面几种情况,当发生如下特定的情况时可以解除上面的阻塞,让该线程重新进入就绪状态:

  • 调用sleep()方法的线程经过了指定时间。
  • 线程调用的阻塞式IO方法已经返回。
  • 线程成功地获得了试图取得的同步监视器。
  • 线程正在等待某个通知时,其他线程发出了一个通知。
  • 处于挂起状态的线程被调用了resume()恢复方法。

54. 线程池

1. 为什么有线程池?

为了提高资源的利用率。原来频繁地创建和释放线程对象会消耗系统资源,有了线程池就可以通过重复利用已创建的线程来降低线程创建和销毁造成的消耗。

2. 线程池的特点及其优势

  • 线程复用:通过重复利用已创建的线程降低线程创建和销毁造成的消耗
  • 控制最大并发数:当任务到达时,任务可以不需要等待线程创建就能立即执行
  • 管理线程:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控

3. 线程池的三大方法

创建多线程,我们要先了解 Executors 这是线程池的工具类,通过这个线程池工具类来创建线程池。创建出来的线程池,都是通过ThreadPoolExecutor类来实现的。
java后端的面经_第23张图片

  • newCachedThreadPool():创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中。
  • newFixedThreadPool(int nThreads):创建一个可重用的、具有固定线程数的线程池。
  • newSingleThreadExecutor():创建一个只有单线程的线程池,它相当于调用newFixedThreadPool()方法时传入参数为1。
//创建一个线程池,线程池根据需要创建线程,可扩容;任务多线程多,任务少线程少
ExecutorService threadPool = Executors.newCachedThreadPool();
//创建一个线程池,一池有固定线程数,适合于创建长期任务性能好
ExecutorService threadPool = Executors.newFixedThreadPool(5);
//创建一个线程池,里面只有一个线程
ExecutorService threadPool = Executors.newSingleThreadExecutor();

4. 线程池的七大参数

  1. corePoolSize(核心工作线程数): 当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时。
  2. maximumPoolSize(最大线程数): 线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。
  3. keepAliveTime(多余线程存活时间): 当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。
  4. workQueue(队列): 用于传输和保存等待执行任务的阻塞队列。
  5. threadFactory(线程创建工厂): 用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。
  6. handler(拒绝策略): 当线程池和队列都满了,再加入线程会执行此策略。
public ThreadPoolExecutor(int corePoolSize,        //核心线程数
                          int maximumPoolSize,     //最大线程数
                          long keepAliveTime,      //多余的空闲线程存活时间
                          TimeUnit unit,           //上面超时参数的单位
                          BlockingQueue<Runnable> workQueue, //阻塞队列
                          ThreadFactory threadFactory,       //线程创建工厂
                          RejectedExecutionHandler handler)  //拒绝策略

下面,我通过创建一个线程的过程来将这七个参数串起来讲解

  • 当进来一个任务后,如果正在运行的线程数量小于corePoolSize,则立即执行此任务。
    • 如果正在运行的线程数量大于或者等于corePoolSize,那么将这个任务放入workQueue
    • 如果这时候队列满了并且正在运行的线程数还小于maximumPoolSize,那么就会创建非核心线程来执行这个任务。
    • 如果队列满了且正在运行的线程数大于等于maximumPoolSize,那么线程池就启动拒绝策略RejectedExecutionHandler handler
  • 当一个线程无事可做一段时间keepAliveTime,线程就会判断。
    • 如果当前线程数大于corePoolSize,那么这个线程被停掉。
    • 所有线程池的任务完成后,就会收缩至corePoolSize大小。

5. 线程池的四大策略

java后端的面经_第24张图片
在进来的任务数量大于maximumPoolSize + workQueue时,就会触发拒绝策略,线程池默认使用的是AbortPolicy拒绝策略。

  1. AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常。
  2. DiscardPolicy: 也是丢弃任务,但是不抛出异常。
  3. DiscardOldestPolicy: 丢弃队列最前面的任务,然后重新尝试执行任务(重复该过程)。
  4. CallerRunsPolicy: 由调用线程处理该任务。

55. Java多线程之间的通信方式

在Java中线程通信主要有以下三种方式:

  1. wait()、notify()、notifyAll()
    • 如果线程之间采用synchronized来保证线程安全,则可以利用wait()、notify()、notifyAll()来实现线程通信。这三个方法是Object类中声明的方法。
    • wait()方法可以让当前线程释放对象锁并进入阻塞状态。
      notify()方法用于唤醒一个正在等待相应对象锁的线程,使其进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。
      notifyAll()用于唤醒所有正在等待相应对象锁的线程,使它们进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。
    • 每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列。就绪队列存储了已就绪(将要竞争锁)的线程,阻塞队列存储了被阻塞的线程。当一个阻塞线程被唤醒后,才会进入就绪队列,进而等待CPU的调度。反之,当一个线程被wait后,就会进入阻塞队列,等待被唤醒。
  2. await()、signal()、signalAll()
    • 如果线程之间采用Lock来保证线程安全,则可以利用await()、signal()、signalAll()来实现线程通信。这三个方法都是Condition接口中的方法,该接口是在Java 1.5中出现的,它用来替代传统的 wait+notify 实现线程间的协作,它的使用依赖于 Lock。相比使用wait+notify,使用Condition的 await+signal 这种方式能够更加安全和高效地实现线程间协作。
    • 注意,Condition 的 await()/signal()/signalAll() 使用都必须在lock保护之内,也就是说,必须在lock.lock()和lock.unlock之间才可以使用。await()/signal()/signalAll() 与 wait()/notify()/notifyAll() 有着天然的对应关系。
  3. BlockingQueue
    • BlockingQueue是Queue的子接口,它的主要用途并不是作为容器,而是作为线程通信的工具。BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞。
    • 程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,即可很好地控制线程的通信。线程之间需要通信,最经典的场景就是生产者与消费者模型,而BlockingQueue就是针对该模型提供的解决方案。

56. sleep()和wait()的区别

  1. sleep()是Thread类中的静态方法,而wait()是Object类中的成员方法;
  2. sleep()可以在任何地方使用,而wait()只能在同步方法或同步代码块中使用;
  3. sleep()不会释放锁,而wait()会释放锁,并需要通过notify()/notifyAll()重新获取锁。

57. synchronized的底层实现原理

  • 同步方法通过ACC_SYNCHRONIZED 关键字隐式的对方法进行加锁。当线程要执行的方法被标注上ACC_SYNCHRONIZED时,需要先获得锁才能执行该方法。
  • 同步代码块通过monitorenter和monitorexit执行来进行加锁。当线程执行到monitorenter的时候要先获得锁,才能执行后面的方法。当线程执行到monitorexit的时候则要释放锁。每个对象自身维护着一个被加锁次数的计数器,当计数器不为0时,只有获得锁的线程才能再次获得锁。

58. 如果不使用synchronized和Lock,如何保证线程安全?

  1. volatile
    volatile关键字为域变量的访问提供了一种免锁机制,使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,因此每次使用该域就要重新计算,而不是使用寄存器中的值。需要注意的是,volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。
  2. 原子变量
    在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,使用该类可以简化线程同步。例如AtomicInteger 表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器),但不能用于替换Integer。可扩展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。
  3. ThreadLocal类
    可以通过ThreadLocal类来实现线程本地存储的功能。Spring中单例bean就是用这个方法实现线程安全的。
  4. 不可变的
    只要一个不可变的对象被正确地构建出来,那其外部的可见状态永远都不会改变,永远都不会看到它在多个线程之中处于不一致的状态。(final修饰)

59. volatile关键字有什么用?

当一个变量被定义成volatile之后,它将具备两项特性:(在JVM底层volatile是采用“内存屏障”来实现的。)

  1. 保证可见性
    • 当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去,这个写会操作会导致其他线程中的volatile变量缓存无效。
  2. 禁止指令重排
    • 使用volatile关键字修饰共享变量可以禁止指令重排序,volatile禁止指令重排序有一些规则:
      ①. 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见,在其后面的操作肯定还没有进行;
      ②. 在进行指令优化时,不能将对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
    • 即执行到volatile变量时,其前面的所有语句都执行完,后面所有语句都未执行。且前面语句的结果对volatile变量及其后面语句可见。

注意,volatile不能保证原子性。 volatile变量在各个线程的工作内存中是存在一致性问题的,但是Java里面的运算操作符并非原子操作,这导致volatile变量的运算在并发下一样是不安全的。

60. 说说你对AQS的理解

聊聊你对 AQS 的理解?

抽象队列同步器 AbstractQueuedSynchronizer (AQS)原理概览
AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
java后端的面经_第25张图片

AQS是用来构建锁或者其他同步组件的骨架类,减少了各功能组件实现的代码量,也解决了在实现同步器时涉及的大量细节问题,例如等待线程采用FIFO队列操作的顺序。在不同的同步器中还可以定义一些灵活的标准来判断某个线程是应该通过还是等待。

AQS采用模板方法模式,在内部维护了n多的模板的方法的基础上,子类只需要实现特定的几个方法(不是抽象方法!),就可以实现子类自己的需求。

基于AQS实现的组件,诸如:

  • ReentrantLock 可重入锁(支持公平和非公平的方式获取锁);
  • Semaphore 计数信号量;
  • ReentrantReadWriteLock 读写锁。

61. 公平锁和非公平锁的区别?

说一下公平锁和非公平锁的区别?
公平锁:多个线程按照申请锁的顺序执行,先来后到。
非公平锁:多个线程不按照申请锁的顺序来,有可能先到后得。当非公平锁失败后才会采用公平锁。

62. ThreadLocal和它的应用场景

ThreadLocal对象可以提供线程局部变量,每个线程Thread拥有一份自己的副本变量,多个线程互不干扰。

ThreadLocal顾名思义是线程私有的局部变量存储容器,可以理解成每个线程都有自己专属的存储容器,它用来存储线程私有变量,其实它只是一个外壳,内部真正存取是一个Map。每个线程可以通过set() 和 get() 存取变量,多线程间无法访问各自的局部变量,相当于在每个线程间建立了一个隔板。只要线程处于活动状态,它所对应的ThreadLocal实例就是可访问的,线程被终止后,它的所有实例将被垃圾收集。总之记住一句话:ThreadLocal存储的变量属于当前线程。

应用场景
ThreadLocal经典的使用场景是为每个线程分配一个 JDBC 连接 Connection,这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的Connection。 另外ThreadLocal还经常用于管理Session会话,将Session保存在ThreadLocal中,使线程处理多次处理会话时始终是同一个Session。

63. Java程序是怎么运行的?

Java 源代码文件经过 Java 编译器编译成字节码文件后,通过类加载器加载到内存中,才能被实例化,然后到 Java 虚拟机中解释执行,最后通过操作系统操作 CPU 执行获取结果。 如下图:
java后端的面经_第26张图片

64. Java的内存分布情况

Java的内存分布情况
java后端的面经_第27张图片

类存放在哪里?

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作“非堆”(Non-Heap),目的是与Java堆区分开来。

局部变量存放在哪里?

Java虚拟机栈(Java Virtual Machine Stack): 每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

局部变量表 存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。

65. 类加载的过程

类加载过程详解
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历:
加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading) 七个阶段,其中验证、准备、解析三个部分统称为连接(Linking)
这七个阶段的发生顺序如下图所示:

java后端的面经_第28张图片

在上述七个阶段中,包括了类加载的全过程:加载、验证、准备、解析和初始化这五个阶段。

66. 谈谈JVM的类加载器

JVM 中内置了三个重要的类加载器,除了 BootstrapClassLoader (启动类加载器)其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader:

  1. BootstrapClassLoader(启动类加载器) : 最顶层的加载类,由 C++实现,负责加载 %JAVA_HOME%/lib目录下的 jar 包和类或者被 -Xbootclasspath参数指定的路径中的所有类。
  2. ExtensionClassLoader(扩展类加载器) : 主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类,或被 java.ext.dirs 系统变量所指定的路径下的 jar 包。
  3. AppClassLoader(应用程序类加载器) : 面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。

67. 双亲委派模型

  1. 双亲委派机制的过程

java后端的面经_第29张图片
在类加载的时候,系统会首先判断当前类是否被加载过。
已经被加载的类会直接返回,否则才会尝试加载。加载的时候,首先会把该请求委派给父类加载器的 loadClass() 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。当父类加载器无法处理时,才由自己来处理。当父类加载器为 null 时,会使用启动类加载器 BootstrapClassLoader 作为父类加载器。

  • 双亲委派模型实现源码分析
private final ClassLoader parent;
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先,检查请求的类是否已经被加载过
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {//父加载器不为空,调用父加载器loadClass()方法处理
                        c = parent.loadClass(name, false);
                    } else {//父加载器为空,使用启动类加载器 BootstrapClassLoader 加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                   //抛出异常说明父类加载器无法完成加载请求
                }

                if (c == null) {
                    long t1 = System.nanoTime();
                    //自己尝试加载
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
  • 双亲委派模型的好处
    避免重复加载 + 避免核心类篡改
    双亲委派模型保证了 Java 程序的稳定运行,可以 避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改 。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类。

68. JVM 参数总结

JVM 参数总结

  1. 堆内存相关
    • 显式指定堆内存 –Xms -Xmx
      最小堆大小:-Xms[unit]
      最大堆大小:-Xmx[unit]
    • 显式新生代内存(Young Generation)
      通过-XX:NewSize-XX:MaxNewSize指定
      通过-Xmn[unit] 指定
      通过-XX:NewRatio=来设置新生代和老年代内存的比值
    • 显式指定永久代/元空间的大小
      设置 Metaspace 的初始(和最小大小):-XX:MetaspaceSize=N
      设置 Metaspace 的最大大小:-XX:MaxMetaspaceSize=N
  2. 垃圾收集相关
    • 垃圾回收器
      串行垃圾收集器:-XX:+UseSerialGC
      并行垃圾收集器:-XX:+UseParallelGC
      CMS垃圾收集器:-XX:+UseParNewGC
      G1垃圾收集器:-XX:+UseG1GC
    • GC记录
      -XX:+UseGCLogFileRotation
      -XX:NumberOfGCLogFiles=< number of log files >
      -XX:GCLogFileSize=< file size >[ unit ]
      -Xloggc:/path/to/gc.log

69. Java的垃圾回收机制

1. GC如何判断对象可以被回收?

  1. 引用计数算法

    给对象中添加一个引用计数器:

    • 每当有一个地方引用它,计数器就加 1;
    • 当引用失效,计数器就减 1;
    • 任何时候计数器为 0 的对象就是不可能再被使用的。

    优点: 实现简单,效率高
    缺点: 目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。

  2. 可达性分析算法:
    是通过一系列称为 “GC Roots” 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
    java后端的面经_第30张图片

2. 垃圾收集算法

垃圾收集算法

标记:都是标记不需要回收的

  1. 标记-清除算法
    标记不需要回收的对象,回收掉没有被标记的对象。(下图,蓝色是标记的
    java后端的面经_第31张图片

  2. 标记-复制算法
    将内存分为大小相同的两块,当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。
    java后端的面经_第32张图片

  3. 标记-整理算法
    让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
    java后端的面经_第33张图片

  4. 分代收集算法
    将 java 堆分为新生代老年代

    • 新生代: 每次收集都会有大量对象死去,所以可以选择 "标记-复制" 算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。
    • 老年代: 对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择 “标记-清除”或“标记-整理” 算法进行垃圾收集。

3. 垃圾收集器

  1. Serial 收集器
    是单线程收集器,它只会使用一条垃圾收集线程去完成垃圾收集工作,且它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。
    java后端的面经_第34张图片

  2. ParNew 收集器
    除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。
    java后端的面经_第35张图片

  3. Parallel Scavenge 收集器
    使用标记-复制算法的多线程收集器,它看上去几乎和 ParNew 都一样。
    java后端的面经_第36张图片

  4. Serial Old 收集器

  5. Parallel Old 收集器

  6. CMS 收集器(Concurrent Mark Sweep)
    目标: 获取最短回收停顿时间。
    整个过程分为四个步骤:
    初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;
    并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
    重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
    并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。
    java后端的面经_第37张图片

  7. G1 收集器

  8. ZGC 收集器

70. java中堆和栈的区别

  1. 申请方式的不同。栈由系统自动分配,而堆是人为申请开辟;
  2. 申请大小的不同。栈获得的空间较小,而堆获得的空间较大;
  3. 申请效率的不同。栈由系统自动分配,速度较快,而堆一般速度比较慢;
  4. 存储内容的不同。栈在函数调用时,函数调用语句的下一条可执行语句的地址第一个进栈,然后函数的各个参数进栈,其中静态变量是不入栈的。而堆一般是在头部用一个字节存放堆的大小,堆中的具体内容是人为安排;
  5. 底层不同。栈是连续的空间,而堆是不连续的空间。

71. 讲一下快排算法

空间复杂度:O(log2n);
平均时间复杂度:O(nlogn)

快排是采用分治递归的思想去进行排序数据
待排序数组中随机找出一个数,可以随机取,也可以取固定位置,一般是取第一个或最后一个称为基准,然后就是比基准小的在左边,比基准大的放到右边。
步骤就是: 先从右往左找一个小于基准的数,再从左往右找一个大于基准的数,然后交换他们,直到碰头结束,碰头位置的数和基准进行交换,这样交换完左边都是比基准小的,右边都是比较基准大的,这样就将一个数组分成了两个子数组,然后再按照同样的方法把子数组再分成更小的子数组,直到不能分解为止。

72. MySQL主从复制原理是什么?

MySQL主从复制是一个异步的复制过程,主库发送更新事件到从库,从库读取更新记录,并执行更新记录,使得从库的内容与主库保持一致。

为什么要做主从复制?

  1. 在业务复杂的系统中,有这么一个情景,有一句sql语句需要锁表,导致暂时不能使用读的服务,那么就很影响运行中的业务,使用主从复制,让主库负责写,从库负责读,这样,即使主库出现了锁表的情景,通过读从库也可以保证业务的正常运行。
  2. 做数据的热备,主库宕机后能够及时替换主库,保证业务可用性。
  3. 架构的扩展。业务量越来越大,I/O访问频率过高,单机无法满足,此时做多库的存储,降低磁盘I/O访问的频率,提高单个机器的I/O性能。

你可能感兴趣的:(笔记,java)