当前位置:首页 » 《随便一记》 » 正文

2024年 Java 面试八股文(20w字)

16 人参与  2024年09月13日 09:22  分类 : 《随便一记》  评论

点击全文阅读


第一章-Java基础篇
1、你是怎样理解OOP面向对象    难度系数:⭐

面向对象是利于语言对现实事物进行抽象。面向对象具有以下特征:

继承继承是从已有类得到继承信息创建新类的过程封装:封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口多态性:多态性是指允许不同子类型的对象对同一消息作出不同的响应
2、重载与重写区别    难度系数:⭐
重载发生在本类,重写发生在父类与子类之间重载的方法名必须相同,重写的方法名相同且返回值类型必须相同重载的参数列表不同,重写的参数列表必须相同重写的访问权限不能比父类中被重写的方法的访问权限更低构造方法不能被重写
3、接口与抽象类的区别    难度系数:⭐
抽象类要被子类继承,接口要被类实现接口可多继承接口,但类只能单继承抽象类可以有构造器、接口不能有构造器抽象类:除了不能实例化抽象类之外,它和普通Java类没有任何区别抽象类:抽象方法可以有public、protected和default这些修饰符、接口:只能是public抽象类:可以有成员变量;接口:只能声明常量
4、深拷贝与浅拷贝的理解    难度系数:⭐

深拷贝和浅拷贝就是指对象的拷贝,一个对象中存在两种类型的属性,一种是基本数据类型,一种是实例对象的引用。

浅拷贝是指,只会拷贝基本数据类型的值,以及实例对象的引用地址,并不会复制一份引用地址所指向的对象,也就是浅拷贝出来的对象,内部的类属性指向的是同一个对象深拷贝是指,既会拷贝基本数据类型的值,也会针对实例对象的引用地址所指向的对象进行复制,深拷贝出来的对象,内部的类执行指向的不是同一个对象
5、sleep和wait区别    难度系数:⭐
sleep方法

属于Thread类中的方法

释放cpu给其它线程 不释放锁资源

sleep(1000) 等待超过1s被唤醒

wait方法

属于Object类中的方法

释放cpu给其它线程,同时释放锁资源

wait(1000) 等待超过1s被唤醒

wait() 一直等待需要通过notify或者notifyAll进行唤醒

wait 方法必须配合 synchronized 一起使用,不然在运行时就会抛出IllegalMonitorStateException异常

篇幅限制下面就只能给大家展示小册部分内容了。这份面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处】即可免费获取

#### 锁释放时机代码演示

public static void main(String[] args) {

    Object o = new Object();

    Thread thread = new Thread(() -> {

        synchronized (o) {

            System.out.println("新线程获取锁时间:" + LocalDateTime.now() + " 新线程名称:" + Thread.currentThread().getName());

            try {

                //wait 释放cpu同时释放锁

                o.wait(2000);

                //sleep 释放cpu不释放锁

                //Thread.sleep(2000);

                System.out.println("新线程获取释放锁锁时间:" + LocalDateTime.now() + " 新线程名称:" + Thread.currentThread().getName());

            } catch (InterruptedException e) {

                throw new RuntimeException(e);

            }

        }

    });

    thread.start();

    try {

        Thread.sleep(100);

    } catch (InterruptedException e) {

        throw new RuntimeException(e);

    }

    System.out.println("主线程获取锁时间:" + LocalDateTime.now() + " 主线程名称:" + Thread.currentThread().getName());

    synchronized (o){

        System.out.println("主线程获取释放锁锁时间:" + LocalDateTime.now() + " 主线程名称:" + Thread.currentThread().getName());

    }

}

Java

6、什么是自动拆装箱  int和Integer有什么区别    难度系数:⭐

基本数据类型,如int,float,double,boolean,char,byte,不具备对象的特征,不能调用方法。

装箱:将基本类型转换成包装类对象拆箱:将包装类对象转换成基本类型的值

java为什么要引入自动装箱和拆箱的功能?主要是用于java集合中,List<Inteter> list=new ArrayList<Integer>();

list集合如果要放整数的话,只能放对象,不能放基本类型,因此需要将整数自动装箱成对象。

实现原理:javac编译器的语法糖,底层是通过Integer.valueOf()和Integer.intValue()方法实现。

区别:

Integer是int的包装类,int则是java的一种基本数据类型Integer变量必须实例化后才能使用,而int变量不需要Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值Integer的默认值是null,int的默认值是0
7、==和equals区别    难度系数:⭐
==

如果比较的是基本数据类型,那么比较的是变量的值

如果比较的是引用数据类型,那么比较的是地址值(两个对象是否指向同一块内存)

equals

如果没重写equals方法比较的是两个对象的地址值

如果重写了equals方法后我们往往比较的是对象中的属性的内容

equals方法是从Object类中继承的,默认的实现就是使用==

8、String能被继承吗 为什么用final修饰    难度系数:⭐
不能被继承,因为String类有final修饰符,而final修饰的类是不能被继承的。String 类是最常用的类之一,为了效率,禁止被继承和重写。为了安全。String 类中有native关键字修饰的调用系统级别的本地方法,调用了操作系统的 API,如果方法可以重写,可能被植入恶意代码,破坏程序。Java 的安全性也体现在这里。
9、String buffer和String builder区别    难度系数:⭐
StringBuffer 与 StringBuilder 中的方法和功能完全是等价的,只是StringBuffer 中的方法大都采用了 synchronized 关键字进行修饰,因此是线程安全的,而 StringBuilder 没有这个修饰,可以被认为是线程不安全的。在单线程程序下,StringBuilder效率更快,因为它不需要加锁,不具备多线程安全而StringBuffer则每次都需要判断锁,效率相对更低

 篇幅限制下面就只能给大家展示小册部分内容了。这份面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处】即可免费获取

10、final、finally、finalize    难度系数:⭐
final:修饰符(关键字)有三种用法:修饰类、变量和方法。修饰类时,意味着它不能再派生出新的子类,即不能被继承,因此它和abstract是反义词。修饰变量时,该变量使用中不被改变,必须在声明时给定初值,在引用中只能读取不可修改,即为常量。修饰方法时,也同样只能使用,不能在子类中被重写。finally:通常放在try…catch的后面构造最终执行代码块,这就意味着程序无论正常执行还是发生异常,这里的代码只要JVM不关闭都能执行,可以将释放外部资源的代码写在finally块中。finalize:Object类中定义的方法,Java中允许使用finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在销毁对象时调用的,通过重写finalize() 方法可以整理系统资源或者执行其他清理工作。
11、Object中有哪些方法    难度系数:⭐
protected Object clone()--->创建并返回此对象的一个副本。boolean equals(Object obj)--->指示某个其他对象是否与此对象“相等protected void finalize()--->当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。Class<? extendsObject> getClass()--->返回一个对象的运行时类。int hashCode()--->返回该对象的哈希码值。void notify()--->唤醒在此对象监视器上等待的单个线程。void notifyAll()--->唤醒在此对象监视器上等待的所有线程。String toString()--->返回该对象的字符串表示。void wait()--->导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法。
      void wait(long timeout)--->导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll()方法,或者超过指定的时间量。
      void wait(long timeout, int nanos)--->导致当前的线程等待,直到其他线程调用此对象的 notify()
12、说一下集合体系    难度系数:⭐

13、ArrarList和LinkedList区别    难度系数:⭐              
ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。对于随机访问get和set,ArrayList效率优于LinkedList,因为LinkedList要移动指针。对于新增和删除操作add和remove,LinkedList比较占优势,因为ArrayList要移动数据。 这一点要看实际情况的。若只对单条数据插入或删除,ArrayList的速度反而优于LinkedList。但若是批量随机的插入删除数据,LinkedList的速度大大优于ArrayList. 因为ArrayList每插入一条数据,要移动插入点及之后的所有数据。
14、HashMap底层是 数组+链表+红黑树,为什么要用这几类结构    难度系数:⭐⭐
数组 Node<K,V>[] table ,哈希表,根据对象的key的hash值进行在数组里面是哪个节点链表的作用是解决hash冲突,将hash值取模之后的对象存在一个链表放在hash值对应的槽位红黑树 JDK8使用红黑树来替代超过8个节点的链表,主要是查询性能的提升,从原来的O(n)到O(logn),通过hash碰撞,让HashMap不断产生碰撞,那么相同的key的位置的链表就会不断增长,当对这个Hashmap的相应位置进行查询的时候,就会循环遍历这个超级大的链表,性能就会下降,所以改用红黑树
15、HashMap和HashTable区别    难度系数:⭐
线程安全性不同

HashMap是线程不安全的,HashTable是线程安全的,其中的方法是Synchronized,在多线程并发的情况下,可以直接使用HashTable,但是使用HashMap时必须自己增加同步处理。

是否提供contains方法

HashMap只有containsValue和containsKey方法;HashTable有contains、containsKey和containsValue三个方法,其中contains和containsValue方法功能相同。

key和value是否允许null值

Hashtable中,key和value都不允许出现null值。HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。

数组初始化和扩容机制

HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。

Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。

16、线程的创建方式    难度系数:⭐
继承Thread类创建线程实现Runnable接口创建线程使用Callable和Future创建线程   有返回值使用线程池创建线程
  

#### 代码演示

import java.util.concurrent.*;

public class threadTest{

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        //继承thread

        ThreadClass thread = new ThreadClass();

        thread.start();

        Thread.sleep(100);

        System.out.println("#####################");

        //实现runnable

        RunnableClass runnable = new RunnableClass();

        new Thread(runnable).start();

        Thread.sleep(100);

        System.out.println("#####################");

        //实现callable

        FutureTask futureTask = new FutureTask(new CallableClass());

        futureTask.run();

        System.out.println("callable返回值:" + futureTask.get());

        Thread.sleep(100);

        System.out.println("#####################");

        //线程池

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 2, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10));

        threadPoolExecutor.execute(thread);

        threadPoolExecutor.shutdown();

        Thread.sleep(100);

        System.out.println("#####################");

        //使用并发包Executors

        ExecutorService executorService = Executors.newFixedThreadPool(5);

        executorService.execute(thread);

        executorService.shutdown();

    }

}

class ThreadClass extends Thread{

    @Override

    public void run() {

        System.out.println("我是继承thread形式:" + Thread.currentThread().getName());

    }

}

class RunnableClass implements Runnable{

    @Override

    public void run(){

        System.out.println("我是实现runnable接口:" + Thread.currentThread().getName());

    }

}

class CallableClass  implements Callable<String> {

    @Override

    public String call(){

        System.out.println("我是实现callable接口:");

        return "我是返回值,可以通过get方法获取";

    }

}

Java

17、线程的状态转换有什么(生命周期)   难度系数:⭐

新建状态(New) :线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。运行状态(Running):线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种: 等待阻塞 -- 通过调用线程的wait()方法,让线程等待某工作的完成。同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
18、Java中有几种类型的流    难度系数:⭐

 篇幅限制下面就只能给大家展示小册部分内容了。这份面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处】即可免费获取

19、请写出你最常见的5个RuntimeException    难度系数:⭐
java.lang.NullPointerException

空指针异常;出现原因:调用了未经初始化的对象或者是不存在的对象。

java.lang.ClassNotFoundException

指定的类找不到;出现原因:类的名称和路径加载错误;通常都是程序试图通过字符串来加载某个类时可能引发异常。

java.lang.NumberFormatException

字符串转换为数字异常;出现原因:字符型数据中包含非数字型字符。

java.lang.IndexOutOfBoundsException

数组角标越界异常,常见于操作数组对象时发生。

java.lang.IllegalArgumentException

方法传递参数错误。

java.lang.ClassCastException

数据类型转换异常。

20、谈谈你对反射的理解    难度系数:⭐
反射机制

所谓的反射机制就是java语言在运行时拥有一项自观的能力。通过这种能力可以彻底了解自身的情况为下一步的动作做准备。

Java的反射机制的实现要借助于4个类:class,Constructor,Field,Method;其中class代表的时类对 象,Constructor-类的构造器对象,Field-类的属性对象,Method-类的方法对象。通过这四个对象我们可以粗略的看到一个类的各个组成部分。

Java反射的作用

在Java运行时环境中,对于任意一个类,可以知道这个类有哪些属性和方法。对于任意一个对象,可以调用它的任意一个方法。这种动态获取类的信息以及动态调用对象的方法的功能来自于Java 语言的反射(Reflection)机制。

Java 反射机制提供功能

在运行时判断任意一个对象所属的类。

在运行时构造任意一个类的对象。

在运行时判断任意一个类所具有的成员变量和方法。

在运行时调用任意一个对象的方法

21、什么是 java 序列化,如何实现 java 序列化    难度系数:⭐
序列化是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。序列化是为了解决在对对象流进行读写操作时所引发的问题。序 列 化 的 实 现 : 将 需 要 被 序 列 化 的 类 实 现 Serializable 接 口 , 该 接 口 没 有 需 要 实 现 的 方 法 , implements Serializable 只是为了标注该对象是可被序列化的,然后使用一个输出流(如:FileOutputStream)来构造一个ObjectOutputStream(对象流)对象,接着,使用 ObjectOutputStream 对象的 writeObject(Object obj)方法就可以将参数为 obj 的对象写出(即保存其状态),要恢复的话则用输入流。
22、Http 常见的状态码    难度系数:⭐
200 OK      //客户端请求成功301      Permanently Moved (永久移除),请求的 URL 已移走。Response 中应该包含一个 Location URL, 说明资源现在所处的位置302      Temporarily Moved  临时重定向400      Bad Request //客户端请求有语法错误,不能被服务器所理解401      Unauthorized //请求未经授权,这个状态代码必须和 WWW-Authenticate 报头域一起使用403      Forbidden //服务器收到请求,但是拒绝提供服务404      Not Found //请求资源不存在,eg:输入了错误的 URL500      Internal Server Error //服务器发生不可预期的错误503      Server Unavailable //服务器当前不能处理客户端的请求,一段时间后可能恢复正常
23、GET 和POST 的区别    难度系数:⭐
GET 请求的数据会附在URL 之后(就是把数据放置在 HTTP 协议头中),以?分割URL 和传输数据,参数之间以&相连,如:login.action?name=zhagnsan&password=123456。POST 把提交的数据则放置在是 HTTP 包的包体中。GET 方式提交的数据最多只能是 1024 字节,理论上POST 没有限制,可传较大量的数据。其实这样说是错误的,不准确的:“GET 方式提交的数据最多只能是 1024 字节",因为 GET 是通过 URL 提交数据,那么 GET 可提交的数据量就跟URL 的长度有直接关系了。而实际上,URL 不存在参数上限的问题,HTTP 协议规范没有对 URL 长度进行限制。这个限制是特定的浏览器及服务器对它的限制。IE 对URL 长度的限制是2083 字节(2K+35)。对于其他浏览器,如Netscape、FireFox 等,理论上没有长度限制,其限制取决于操作系统的支持。POST 的安全性要比GET 的安全性高。注意:这里所说的安全性和上面 GET 提到的“安全”不是同个概念。上面“安全”的含义仅仅是不作数据修改,而这里安全的含义是真正的 Security 的含义,比如:通过 GET 提交数据,用户名和密码将明文出现在 URL 上,因为(1)登录页面有可能被浏览器缓存,(2)其他人查看浏览器的历史纪录,那么别人就可以拿到你的账号和密码了,除此之外,使用 GET 提交数据还可能会造成 Cross-site request forgery 攻击。Get 是向服务器发索取数据的一种请求,而 Post 是向服务器提交数据的一种请求,在 FORM(表单)中,Method默认为"GET",实质上,GET 和 POST 只是发送机制不同,并不是一个取一个发!
24、Cookie 和Session 的区别    难度系数:⭐
Cookie 是 web 服务器发送给浏览器的一块信息,浏览器会在本地一个文件中给每个 web 服务器存储 cookie。以后浏览器再给特定的 web 服务器发送请求时,同时会发送所有为该服务器存储的 cookieSession 是存储在 web 服务器端的一块信息。session 对象存储特定用户会话所需的属性及配置信息。当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去Cookie 和session 的不同点

无论客户端做怎样的设置,session 都能够正常工作。当客户端禁用 cookie 时将无法使用 cookie

在存储的数据量方面:session 能够存储任意的java 对象,cookie 只能存储 String 类型的对象

第二章-Java高级篇
1、HashMap底层源码    难度系数:⭐⭐⭐

HashMap的底层结构在jdk1.7中由数组+链表实现,在jdk1.8中由数组+链表+红黑树实现,以数组+链表的结构为例。

JDK1.8之前Put方法:

JDK1.8之后Put方法:

 篇幅限制下面就只能给大家展示小册部分内容了。这份面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处】即可免费获取

HashMap基于哈希表的Map接口实现,是以key-value存储形式存在,即主要用来存放键值对。HashMap 的实现不是同步的,这意味着它不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射不是有序的。

JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突(两个对象调用的hashCode方法计算的哈希码值一致导致计算的数组索引值相同)而存在的(“拉链法”解决冲突).JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(或者红黑树的边界值,默认为 8)并且当前数组的长度大于64时,此时此索引位置上的所有数据改为使用红黑树存储。

补充:将链表转换成红黑树前会判断,即使阈值大于8,但是数组长度小于64,此时并不会将链表变为红黑树。而是选择进行数组扩容。

这样做的目的是因为数组比较小,尽量避开红黑树结构,这种情况下变为红黑树结构,反而会降低效率,因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡 。同时数组长度小于64时,搜索时间相对要快些。所以综上所述为了提高性能和减少搜索时间,底层在阈值大于8并且数组长度大于64时,链表才转换为红黑树。具体可以参考 treeifyBin方法。

当然虽然增了红黑树作为底层数据结构,结构变得复杂了,但是阈值大于8并且数组长度大于64时,链表转换为红黑树时,效率也变的更高效。

注意:可以结合百度hashmap源码解析进行更深入的了解。

史上最详细的 JDK 1.8 HashMap 源码解析_程序员囧辉的博客-CSDN博客

2、JVM内存分哪几个区,每个区的作用是什么    难度系数:⭐⭐

java虚拟机主要分为以下几个区

方法区 有时候也成为永久代,在该区内很少发生垃圾回收,但是并不代表不发生GC,在这里进行的GC主要是对方法区里的常量池和对类型的卸载方法区主要用来存储已被虚拟机加载的类的信息、常量、静态变量和即时编译器编译后的代码等数据。该区域是被线程共享的。方法区里有一个运行时常量池,用于存放静态编译产生的字面量和符号引用。该常量池具有动态性,也就是说常量并不一定是编译时确定,运行时生成的常量也会存在这个常量池中。虚拟机栈 虚拟机栈也就是我们平常所称的栈内存,它为java方法服务,每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接和方法出口等信息。虚拟机栈是线程私有的,它的生命周期与线程相同。局部变量表里存储的是基本数据类型、returnAddress类型(指向一条字节码指令的地址)和对象引用,这个对象引用有可能是指向对象起始地址的一个指针,也有可能是代表对象的句柄或者与对象相关联的位置。局部变量所需的内存空间在编译器间确定操作数栈的作用主要用来存储运算结果以及运算的操作数,它不同于局部变量表通过索引来访问,而是压栈和出栈的方式每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接.动态链接就是将常量池中的符号引用在运行期转化为直接引用。本地方法栈

本地方法栈和虚拟机栈类似,只不过本地方法栈为Native方法服务。

java堆是所有线程所共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都在这里创建,因此该区域经常发生垃圾回收操作。

程序计数器:

内存空间小,字节码解释器工作时通过改变这个计数值可以选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理和线程恢复等功能都需要依赖这个计数器完成。该内存区域是唯一一个java虚拟机规范没有规定任何OOM情况的区域。

JEP 122: Remove the Permanent Generation  介绍 静态变量、字符串常量从永久代移动到堆中

3、Java中垃圾收集的方法有哪些    难度系数:⭐

采用分区分代回收思想:

复制算法  年轻代中使用的是Minor GC,这种GC算法采用的是复制算法(Copying)

a) 效率高,缺点:需要内存容量大,比较耗内存

b) 使用在占空间比较小、刷新次数多的新生区

标记-清除  老年代一般是由标记清除或者是标记清除与标记整理的混合实现

a) 效率比较低,会差生碎片。

标记-整理  老年代一般是由标记清除或者是标记清除与标记整理的混合实现

a) 效率低速度慢,需要移动对象,但不会产生碎片。

4、如何判断一个对象是否存活(或者GC对象的判定方法)    难度系数:⭐

 篇幅限制下面就只能给大家展示小册部分内容了。这份面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处】即可免费获取

引用计数法

所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一。当一个对象的引用计数器为零时,说明此对象没有被引用,也就是“死对象”,将会被垃圾回收.

引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象A引用对象B,对象B又引用者对象A,那么此时A,B对象的引用计数器都不为零,也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法。

可达性算法(引用链法) 该算法的基本思路就是通过一些被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(即从GC Roots节点到该节点不可达),则证明该对象是不可用的。在java中可以作为GC Roots的对象有以下几种:虚拟机栈中引用的对象、方法区类静态属性引用的对象、方法区常量池引用的对象、本地方法栈JNI引用的对象。
5、什么情况下会产生StackOverflowError(栈溢出)和OutOfMemoryError(堆溢出)怎么排查    难度系数:⭐⭐
引发 StackOverFlowError 的常见原因有以下几种 无限递归循环调用(最常见)执行了大量方法,导致线程栈空间耗尽方法内声明了海量的局部变量native 代码有栈上分配的逻辑,并且要求的内存还不小,比如 java.net.SocketInputStream.read0 会在栈上要求分配一个 64KB 的缓存(64位 Linux)。引发 OutOfMemoryError的常见原因有以下几种 内存中加载的数据量过于庞大,如一次从数据库取出过多数据集合类中有对对象的引用,使用完后未清空,使得JVM不能回收代码中存在死循环或循环产生过多重复的对象实体启动参数内存值设定的过小排查:可以通过jvisualvm进行内存快照分析

参考https://www.cnblogs.com/boboooo/p/13164071.html

栈溢出、堆溢出案例演示

 public class StackOverFlowTest {
  private static int count = 1;
  public static void main(String[] args) {
      //模拟栈溢出
      //getDieCircle();
     
      //模拟堆溢出
      getOutOfMem();
  }

  public static void getDieCircle(){
      System.out.println(count++);
      getDieCircle();
  }

  public static void getOutOfMem(){
      while (true) {
          Object o = new Object();
          System.out.println(o);
      }
  }
}

Java

6、什么是线程池,线程池有哪些(创建)    难度系数:⭐

线程池就是事先将多个线程对象放到一个容器中,当使用的时候就不用 new 线程而是直接去池中拿线程即可,节省了开辟子线程的时间,提高的代码执行效率

在 JDK 的 java.util.concurrent.Executors 中提供了生成多种线程池的静态方法。

ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();

ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(4);

ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(4);

ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();

然后调用他们的 execute 方法即可。

这4种线程池底层 全部是ThreadPoolExecutor对象的实现,阿里规范手册中规定线程池采用ThreadPoolExecutor自定义的,实际开发也是。

newCachedThreadPool

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。这种类型的线程池特点是:

工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。

如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。

在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。

newFixedThreadPool

创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。

newSingleThreadExecutor

创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。

newScheduleThreadPool

创建一个定长的线程池,而且支持定时的以及周期性的任务执行。例如延迟3秒执行。

7、为什么要使用线程池    难度系数:⭐
线程池做的工作主要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最 大数量,超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。主要特点:线程复用;控制最大并发数:管理线程。

第一:降低资源消耗。通过重复利用己创建的线程降低线程创建和销毁造成的消耗。

第二:提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。

第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进 行统一的分配,调优和监控

8、线程池底层工作原理    难度系数:⭐

第一步:线程池刚创建的时候,里面没有任何线程,等到有任务过来的时候才会创建线程。当然也可以调用 prestartAllCoreThreads() 或者 prestartCoreThread() 方法预创建corePoolSize个线程第二步:调用execute()提交一个任务时,如果当前的工作线程数<corePoolSize,直接创建新的线程执行这个任务第三步:如果当时工作线程数量>=corePoolSize,会将任务放入任务队列中缓存第四步:如果队列已满,并且线程池中工作线程的数量<maximumPoolSize,还是会创建线程执行这个任务第五步:如果队列已满,并且线程池中的线程已达到maximumPoolSize,这个时候会执行拒绝策略,JAVA线程池默认的策略是AbortPolicy,即抛出RejectedExecutionException异常
9、ThreadPoolExecutor对象有哪些参数 怎么设定核心线程数和最大线程数 拒绝策略有哪些    难度系数:⭐

参数与作用:共7个参数

corePoolSize:核心线程数,

在ThreadPoolExecutor中有一个与它相关的配置:allowCoreThreadTimeOut(默认为false),当allowCoreThreadTimeOut为false时,核心线程会一直存活,哪怕是一直空闲着。而当allowCoreThreadTimeOut为true时核心线程空闲时间超过keepAliveTime时会被回收。

maximumPoolSize:最大线程数

线程池能容纳的最大线程数,当线程池中的线程达到最大时,此时添加任务将会采用拒绝策略,默认的拒绝策略是抛出一个运行时错误(RejectedExecutionException)。值得一提的是,当初始化时用的工作队列为LinkedBlockingDeque时,这个值将无效。

keepAliveTime:存活时间,

当非核心空闲超过这个时间将被回收,同时空闲核心线程是否回收受allowCoreThreadTimeOut影响。

unit:keepAliveTime的单位。workQueue:任务队列

常用有三种队列,即SynchronousQueue,LinkedBlockingDeque(无界队列),ArrayBlockingQueue(有界队列)。

threadFactory:线程工厂,

ThreadFactory是一个接口,用来创建worker。通过线程工厂可以对线程的一些属性进行定制。默认直接新建线程。

RejectedExecutionHandler:拒绝策略

也是一个接口,只有一个方法,当线程池中的资源已经全部使用,添加新线程被拒绝时,会调用RejectedExecutionHandler的rejectedExecution法。默认是抛出一个运行时异常。

线程池大小设置:

需要分析线程池执行的任务的特性: CPU 密集型还是 IO 密集型每个任务执行的平均时长大概是多少,这个任务的执行时长可能还跟任务处理逻辑是否涉及到网络传输以及底层系统资源依赖有关系

如果是 CPU 密集型,主要是执行计算任务,响应时间很快,cpu 一直在运行,这种任务 cpu的利用率很高,那么线程数的配置应该根据 CPU 核心数来决定,CPU 核心数=最大同时执行线程数,加入 CPU 核心数为 4,那么服务器最多能同时执行 4 个线程。过多的线程会导致上下文切换反而使得效率降低。那线程池的最大线程数可以配置为 cpu 核心数+1 如果是 IO 密集型,主要是进行 IO 操作,执行 IO 操作的时间较长,这是 cpu 出于空闲状态,导致 cpu 的利用率不高,这种情况下可以增加线程池的大小。这种情况下可以结合线程的等待时长来做判断,等待时间越高,那么线程数也相对越多。一般可以配置 cpu 核心数的 2 倍。

一个公式:线程池设定最佳线程数目 = ((线程池设定的线程等待时间+线程 CPU 时间)/
线程 CPU 时间 )* CPU 数目

这个公式的线程 cpu 时间是预估的程序单个线程在 cpu 上运行的时间(通常使用 loadrunner测试大量运行次数求出平均值)

拒绝策略:

AbortPolicy:直接抛出异常,默认策略;CallerRunsPolicy:用调用者所在的线程来执行任务;DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;DiscardPolicy:直接丢弃任务;当然也可以根据应用场景实现 RejectedExecutionHandler 接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务
10、常见线程安全的并发容器有哪些    难度系数:⭐
CopyOnWriteArrayList、CopyOnWriteArraySet、ConcurrentHashMapCopyOnWriteArrayList、CopyOnWriteArraySet采用写时复制实现线程安全ConcurrentHashMap采用分段锁的方式实现线程安全
11、Atomic原子类了解多少 原理是什么    难度系数:⭐

Java 的原子类都存放在并发包 java.util.concurrent.atomic下,如下图:

基本类型

使用原子的方式更新基本类型AtomicInteger:整型原子类AtomicLong:长整型原子类AtomicBoolean:布尔型原子类

数组类型

使用原子的方式更新数组里的某个元素AtomicIntegerArray:整形数组原子类AtomicLongArray:长整形数组原子类AtomicReferenceArray:引用类型数组原子类

引用类型

AtomicReference:引用类型原子类AtomicStampedReference:原子更新引用类型里的字段原子类AtomicMarkableReference :原子更新带有标记位的引用类型AtomicIntegerFieldUpdater:原子更新整形字段的更新器AtomicLongFieldUpdater:原子更新长整形字段的更新器AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,以及解决使用 CAS 进行原子更新时可能出现的 ABA 问题 AtomicInteger 类利用 CAS (Compare and Swap) + volatile + native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。CAS 的原理,是拿期望值和原本的值作比较,如果相同,则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是个本地方法,这个方法是用来拿“原值”的内存地址,返回值是 valueOffset;另外,value 是一个 volatile 变量,因此 JVM 总是可以保证任意时刻的任何线程总能拿到该变量的最新值。
12、synchronized底层实现是什么 lock底层是什么 有什么区

 篇幅限制下面就只能给大家展示小册部分内容了。这份面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处】即可免费获取

别    难度系数:⭐⭐⭐

Synchronized原理:

方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词),然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。

代码块的同步是利用monitorenter和monitorexit这两个字节码指令。它们分别位于同步代码块的开始和结束位置。当jvm执行到monitorenter指令时,当前线程试图获取monitor对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器+1;当执行monitorexit指令时,锁计数器-1;当锁计数器为0时,该锁就被释放了。如果获取monitor对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。

参考:一篇文章讲透synchronized底层实现原理_忘了带罗盘的船夫的博客-CSDN博客

Lock原理:

Lock的存储结构:一个int类型状态值(用于锁的状态变更),一个双向链表(用于存储等待中的线程)Lock获取锁的过程:本质上是通过CAS来获取状态值修改,如果当场没获取到,会将该线程放在线程等待链表中。Lock释放锁的过程:修改状态值,调整等待链表。Lock大量使用CAS+自旋。因此根据CAS特性,lock建议使用在低锁冲突的情况下。

Lock与synchronized的区别:

Lock的加锁和解锁都是由java代码配合native方法(调用操作系统的相关方法)实现的,而synchronize的加锁和解锁的过程是由JVM管理的当一个线程使用synchronize获取锁时,若锁被其他线程占用着,那么当前只能被阻塞,直到成功获取锁。而Lock则提供超时锁和可中断等更加灵活的方式,在未能获取锁的     条件下提供一种退出的机制。一个锁内部可以有多个Condition实例,即有多路条件队列,而synchronize只有一路条件队列;同样Condition也提供灵活的阻塞方式,在未获得通知之前可以通过中断线程以    及设置等待时限等方式退出条件队列。synchronize对线程的同步仅提供独占模式,而Lock即可以提供独占模式,也可以提供共享模式

synchronized
 

Lock
 

关键字
 


 

自动加锁和释放锁
 

需要手动调用unlock方法释放锁
 

jvm层面的锁
 

API层面的锁
 

非公平锁
 

可以选择公平或者非公平锁
 

锁是一个对象,并且锁的信息保存在了对象中
 

代码中通过int类型的state标识
 

有一个锁升级的过程
 


 

13、了解ConcurrentHashMap吗 为什么性能比HashTable高,说下原理    难度系数:⭐⭐

ConcurrentHashMap是线程安全的Map容器,JDK8之前,ConcurrentHashMap使用锁分段技术,将数据分成一段段存储,每个数据段配置一把锁,即segment类,这个类继承ReentrantLock来保证线程安全,JKD8的版本取消Segment这个分段锁数据结构,底层也是使用Node数组+链表+红黑树,从而实现对每一段数据就行加锁,也减少了并发冲突的概率。

hashtable类基本上所有的方法都是采用synchronized进行线程安全控制,高并发情况下效率就降低 ,ConcurrentHashMap是采用了分段锁的思想提高性能,锁粒度更细化

14、ConcurrentHashMap底层原理    难度系数:⭐⭐⭐
Java7 中 ConcurrentHashMap 使用的分段锁,也就是每一个 Segment 上同时只有一个线程可以操作,每一个 Segment 都是一个类似 HashMap 数组的结构,它可以扩容,它的冲突会转化为链表。但是 Segment 的个数一但初始化就不能改变。

 
  

public V put(K key, V value) {

    Segment<K,V> s;

    if (value == null)

        throw new NullPointerException();

    int hash = hash(key);

    // hash 值无符号右移 28位(初始化时获得),然后与 segmentMask=15 做与运算

    // 其实也就是把高4位与segmentMask(1111)做与运算

  // this.segmentMask = ssize - 1;

   //对hash值进行右移segmentShift位,计算元素对应segment中数组下表的位置

   //把hash右移segmentShift,相当于只要hash值的高32-segmentShift位,右移的目的是保留了hash值的高位。然后和segmentMask与操作计算元素在segment数组中的下表

    int j = (hash >>> segmentShift) & segmentMask;

   //使用unsafe对象获取数组中第j个位置的值,后面加上的是偏移量

    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck

         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment

        // 如果查找到的 Segment 为空,初始化

        s = ensureSegment(j);

   //插入segment对象

    return s.put(key, hash, value, false);

}

/**

 * Returns the segment for the given index, creating it and

 * recording in segment table (via CAS) if not already present.

 *

 * @param k the index

 * @return the segment

 */

@SuppressWarnings("unchecked")

private Segment<K,V> ensureSegment(int k) {

    final Segment<K,V>[] ss = this.segments;

    long u = (k << SSHIFT) + SBASE; // raw offset

    Segment<K,V> seg;

    // 判断 u 位置的 Segment 是否为null

    if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {

        Segment<K,V> proto = ss[0]; // use segment 0 as prototype

        // 获取0号 segment 里的 HashEntry<K,V> 初始化长度

        int cap = proto.table.length;

        // 获取0号 segment 里的 hash 表里的扩容负载因子,所有的 segment 的 loadFactor 是相同的

        float lf = proto.loadFactor;

        // 计算扩容阀值

        int threshold = (int)(cap * lf);

        // 创建一个 cap 容量的 HashEntry 数组

        HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];

        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) { // recheck

            // 再次检查 u 位置的 Segment 是否为null,因为这时可能有其他线程进行了操作

            Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);

            // 自旋检查 u 位置的 Segment 是否为null

            while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))

                   == null) {

                // 使用CAS 赋值,只会成功一次

                if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))

                    break;

            }

        }

    }

    return seg;

}

final V put(K key, int hash, V value, boolean onlyIfAbsent) {

    // 获取 ReentrantLock 独占锁,获取不到,scanAndLockForPut 获取。

    HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);

    V oldValue;

    try {

        HashEntry<K,V>[] tab = table;

        // 计算要put的数据位置

        int index = (tab.length - 1) & hash;

        // CAS 获取 index 坐标的值

        HashEntry<K,V> first = entryAt(tab, index);

        for (HashEntry<K,V> e = first;;) {

            if (e != null) {

                // 检查是否 key 已经存在,如果存在,则遍历链表寻找位置,找到后替换 value

                K k;

                if ((k = e.key) == key ||

                    (e.hash == hash && key.equals(k))) {

                    oldValue = e.value;

                    if (!onlyIfAbsent) {

                        e.value = value;

                        ++modCount;

                    }

                    break;

                }

                e = e.next;

            }

            else {

                // first 有值没说明 index 位置已经有值了,有冲突,链表头插法。

                if (node != null)

                    node.setNext(first);

                else

                    node = new HashEntry<K,V>(hash, key, value, first);

                int c = count + 1;

                // 容量大于扩容阀值,小于最大容量,进行扩容

                if (c > threshold && tab.length < MAXIMUM_CAPACITY)

                    rehash(node);

                else

                    // index 位置赋值 node,node 可能是一个元素,也可能是一个链表的表头

                    setEntryAt(tab, index, node);

                ++modCount;

                count = c;

                oldValue = null;

                break;

            }

        }

    } finally {

        unlock();

    }

    return oldValue;

}

Java

Java8 中的 ConcurrentHashMap 使用的 Synchronized 锁加 CAS 的机制。结构Node 数组 + 链表 / 红黑树,Node 是类似于一个 HashEntry 的结构。它的冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又退回链表。

  

public V put(K key, V value) {

    return putVal(key, value, false);

}

/** Implementation for put and putIfAbsent */

final V putVal(K key, V value, boolean onlyIfAbsent) {

    // key 和 value 不能为空

    if (key == null || value == null) throw new NullPointerException();

    int hash = spread(key.hashCode());

    int binCount = 0;

    for (Node<K,V>[] tab = table;;) {

        // f = 目标位置元素

        Node<K,V> f; int n, i, fh;// fh 后面存放目标位置的元素 hash 值

        if (tab == null || (n = tab.length) == 0)

            // 数组桶为空,初始化数组桶(自旋+CAS)

            tab = initTable();

        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {

            // 桶内为空,CAS 放入,不加锁,成功了就直接 break 跳出

            if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))

                break;  // no lock when adding to empty bin

        }

        else if ((fh = f.hash) == MOVED)

            tab = helpTransfer(tab, f);

        else {

            V oldVal = null;

            // 使用 synchronized 加锁加入节点

            synchronized (f) {

                if (tabAt(tab, i) == f) {

                    // 说明是链表

                    if (fh >= 0) {

                        binCount = 1;

                        // 循环加入新的或者覆盖节点

                        for (Node<K,V> e = f;; ++binCount) {

                            K ek;

                            if (e.hash == hash &&

                                ((ek = e.key) == key ||

                                 (ek != null && key.equals(ek)))) {

                                oldVal = e.val;

                                if (!onlyIfAbsent)

                                    e.val = value;

                                break;

                            }

                            Node<K,V> pred = e;

                            if ((e = e.next) == null) {

                                pred.next = new Node<K,V>(hash, key,

                                                          value, null);

                                break;

                            }

                        }

                    }

                    else if (f instanceof TreeBin) {

                        // 红黑树

                        Node<K,V> p;

                        binCount = 2;

                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,

                                                       value)) != null) {

                            oldVal = p.val;

                            if (!onlyIfAbsent)

                                p.val = value;

                        }

                    }

                }

            }

            if (binCount != 0) {

                if (binCount >= TREEIFY_THRESHOLD)

                    treeifyBin(tab, i);

                if (oldVal != null)

                    return oldVal;

                break;

            }

        }

    }

    addCount(1L, binCount);

    return null;

}

Java

15、了解volatile关键字不    难度系数:⭐
volatile是Java提供的最轻量级的同步机制,保证了共享变量的可见性,被volatile关键字修饰的变量,如果值发生了变化,其他线程立刻可见,避免出现脏读现象。volatile禁止了指令重排,可以保证程序执行的有序性,但是由于禁止了指令重排,所以JVM相关的优化没了,效率会偏弱
16、synchronized和volatile有什么区别    难度系数:⭐⭐
volatile本质是告诉JVM当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。volatile仅能用在变量级别,而synchronized可以使用在变量、方法、类级别。volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。volatile不会造成线程阻塞,synchronized可能会造成线程阻塞。volatile标记的变量不会被编译器优化,synchronized标记的变量可以被编译器优化。
17、Java类加载过程    难度系数:⭐
加载 加载时类加载的第一个过程,在这个阶段,将完成一下三件事情:

通过一个类的全限定名获取该类的二进制流。

将该二进制流中的静态存储结构转化为方法去运行时数据结构。 

在内存中生成该类的Class对象,作为该类的数据访问入口。

验证 验证的目的是为了确保Class文件的字节流中的信息不回危害到虚拟机.在该阶段主要完成以下四钟验证:

文件格式验证:验证字节流是否符合Class文件的规范,如主次版本号是否在当前虚拟机范围内,常量池中的常量是否有不被支持的类型.

元数据验证:对字节码描述的信息进行语义分析,如这个类是否有父类,是否集成了不被继承的类等。

字节码验证:是整个验证过程中最复杂的一个阶段,通过验证数据流和控制流的分析,确定程序语义是否正确,主要针对方法体的验证。如:方法中的类型转换是否正确,跳转指令是否正确等。

符号引用验证:这个动作在后面的解析过程中发生,主要是为了确保解析动作能正确执行。

准备

准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在Java堆中。

解析

该阶段主要完成符号引用到直接引用的转换动作。解析动作并不一定在初始化动作完成之前,也有可能在初始化之后。

初始化

初始化时类加载的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。

18、什么是类加载器,类加载器有哪些  难度系数:⭐

类加载器就是把类文件加载到虚拟机中,也就是说通过一个类的全限定名来获取描述该类的二进制字节流。

主要有以下四种类加载器

启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用

扩展类加载器(extension class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类

系统类加载器(system class loader)也叫应用类加载器:它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它

用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现

什么时候会使用到加载器?java中的加载器是按需加载,什么时候用到,什么时候加载 new对象的时候访问某个类或者接口的静态变量,或者对该静态变量赋值时调用类的静态方法时反射初始化一个类的子类时,其父类首先会被加载JVM启动时标明的启动类,也就是文件名和类名相同的那个类
19、简述java内存分配与回收策略以及Minor GC和Major GC(full GC)     难度系数:⭐⭐
内存分配

栈区:栈分为java虚拟机栈和本地方法栈

堆区:堆被所有线程共享区域,在虚拟机启动时创建,唯一目的存放对象实例。堆区是gc的主要区域,通常情况下分为两个区块年轻代和年老代。更细一点年轻代又分为Eden区,主要放新创建对象,From survivor 和 To survivor 保存gc后幸存下的对象,默认情况下各自占比 8:1:1。

方法区:被所有线程共享区域,用于存放已被虚拟机加载的类信息,常量,静态变量等数据。被Java虚拟机描述为堆的一个逻辑部分。习惯是也叫它永久代(permanment generation)

程序计数器:当前线程所执行的行号指示器。通过改变计数器的值来确定下一条指令,比如循环,分支,跳转,异常处理,线程恢复等都是依赖计数器来完成。线程私有的。

回收策略以及Minor GC和Major GC 对象优先在堆的Eden区分配大对象直接进入老年代长期存活的对象将直接进入老年代

当Eden区没有足够的空间进行分配时,虚拟机会执行一次Minor GC.Minor GC通常发生在新生代的Eden区,在这个区的对象生存期短,往往发生GC的频率较高,回收速度比较快;Full Gc/Major GC 发生在老年代,一般情况下,触发老年代GC的时候不会触发Minor GC,但是通过配置,可以在Full GC之前进行一次Minor GC这样可以加快老年代的回收速度。

20、如何查看java死锁     难度系数:⭐

 篇幅限制下面就只能给大家展示小册部分内容了。这份面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处】即可免费获取

####演示死锁

package com.ssg.mst;

public class 死锁 {

    private static final String lock1 = "lock1";

    private static final String lock2 = "lock2";

    public static void main(String[] args) {

        Thread thread1 = new Thread(() -> {

            while (true) {

                synchronized (lock1) {

                    try {

                        System.out.println(Thread.currentThread().getName() + lock1);

                        Thread.sleep(1000);

                        synchronized (lock2){

                            System.out.println(Thread.currentThread().getName() + lock2);

                        }

                    } catch (InterruptedException e) {

                        throw new RuntimeException(e);

                    }

                }

            }

        });

        Thread thread2 = new Thread(() -> {

            while (true) {

                synchronized (lock2) {

                    try {

                        System.out.println(Thread.currentThread().getName() + lock2);

                        Thread.sleep(1000);

                        synchronized (lock1){

                            System.out.println(Thread.currentThread().getName() + lock1);

                        }

                    } catch (InterruptedException e) {

                        throw new RuntimeException(e);

                    }

                }

            }

        });

        thread1.start();

        thread2.start();

    }

}

Java

死锁代码演示

程序运行,进程没有停止。

通过jps查看java进程,找到没有停止的进程

通过jstack 9060 查看进程具体执行信息

21、Java死锁如何避免     难度系数:⭐

造成死锁的几个原因

1.一个资源每次只能被一个线程使用

2.一个线程在阻塞等待某个资源时,不释放已占有资源

3.一个线程已经获得的资源,在未使用完之前,不能被强行剥夺

4.若干线程形成头尾相接的循环等待资源关系

这是造成死锁必须要达到的4个条件,如果要避免死锁,只需要不满足其中某一个条件即可。而其中前3个条件是作为锁要符合的条件,所以要避免死锁就需要打破第4个条件,不出现循环等待锁的关系。

在开发过程中

1.要注意加锁顺序,保证每个线程按同样的顺序进行加锁

2.要注意加锁时限,可以针对锁设置一个超时时间

3.要注意死锁检查,这是一种预防机制,确保在第一时间发现死锁并进行解决

第三章-java框架篇
1、简单的谈一下SpringMVC的工作流程    难度系数:⭐
用户发送请求至前端控制器DispatcherServletDispatcherServlet收到请求调用HandlerMapping处理器映射器。处理器映射器找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。DispatcherServlet调用HandlerAdapter处理器适配器HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。Controller执行完成返回ModelAndViewHandlerAdapter将controller执行结果ModelAndView返回给DispatcherServletDispatcherServlet将ModelAndView传给ViewReslover视图解析器ViewReslover解析后返回具体ViewDispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。DispatcherServlet响应用户
2、说出Spring或者SpringMVC中常用的5个注解    难度系数:⭐
@Component  基本注解,标识一个受Spring管理的组件@Controller    标识为一个表示层的组件@Service       标识为一个业务层的组件@Repository    标识为一个持久层的组件@Autowired     自动装配@Qualifier("")    具体指定要装配的组件的id值@RequestMapping()  完成请求映射@PathVariable    映射请求URL中占位符到请求处理方法的形参

只要说出几个注解并解释含义即可,如上答案只做参考

3、简述SpringMVC中如何返回JSON数据    难度系数:⭐

Step1:在项目中加入json转换的依赖,例如jackson,fastjson,gson等

Step2:在请求处理方法中将返回值改为具体返回的数据的类型, 例如数据的集合类List<Employee>等

Step3:在请求处理方法上使用@ResponseBody注解

4、谈谈你对Spring的理解    难度系数:⭐

Spring 是一个开源框架,为简化企业级应用开发而生。Spring 可以是使简单的JavaBean 实现以前只有EJB 才能实现的功能。Spring 是一个 IOC 和 AOP 容器框架。

Spring 容器的主要核心是:

控制反转(IOC),传统的 java 开发模式中,当需要一个对象时,我们会自己使用 new 或者 getInstance 等直接或者间接调用构造方法创建一个对象。而在 spring 开发模式中,spring 容器使用了工厂模式为我们创建了所需要的对象,不需要我们自己创建了,直接调用spring 提供的对象就可以了,这是控制反转的思想。

依赖注入(DI),spring 使用 javaBean 对象的 set 方法或者带参数的构造方法为我们在创建所需对象时将其属性自动设置所需要的值的过程,就是依赖注入的思想。

面向切面编程(AOP),在面向对象编程(oop)思想中,我们将事物纵向抽成一个个的对象。而在面向切面编程中,我们将一个个的对象某些类似的方面横向抽成一个切面,对这个切面进行一些如权限控制、事物管理,记录日志等公用操作处理的过程就是面向切面编程的思想。AOP 底层是动态代理,如果是接口采用 JDK 动态代理,如果是类采用CGLIB 方式实现动态代理。

5、Spring中常用的设计模式    难度系数:⭐
代理模式——spring 中两种代理方式,若目标对象实现了若干接口,spring 使用jdk 的java.lang.reflect.Proxy类代理。若目标兑现没有实现任何接口,spring 使用 CGLIB 库生成目标类的子类。单例模式——在 spring 的配置文件中设置 bean 默认为单例模式。模板方式模式——用来解决代码重复的问题。

比如:RestTemplate、JmsTemplate、JpaTemplate

工厂模式——在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用同一个接口来指向新创建的对象。Spring 中使用 beanFactory 来创建对象的实例。
6、Spring循环依赖问题    难度系数:⭐⭐
常见问法

请解释一下spring中的三级缓存

三级缓存分别是什么?三个Map有什么异同?

什么是循环依赖?请你谈谈?看过spring源码吗?

如何检测是否存在循环依赖?实际开发中见过循环依赖的异常吗?

多例的情况下,循环依赖问题为什么无法解决?

什么是循环依赖?

两种注入方式对循环依赖的影响?

官方解释

https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-dependency-resolution

相关概念

实例化:堆内存中申请空间

初始化:对象属性赋值

三级缓存

名称

对象名

含义

一级缓存

singletonObjects

存放已经经历了完整生命周期的Bean对象

二级缓存
 

earlySingletonObjects
 

存放早期暴露出来的Bean对象,Bean的生命周期未结束(属性还未填充完)

三级缓存

singletonFactories

存放可以生成Bean的工厂

四个关键方法

package org.springframework.beans.factory.support;

public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {

    /**

    单例对象的缓存:bean名称—bean实例,即:所谓的单例池。

    表示已经经历了完整生命周期的Bean对象

    第一级缓存

    */

    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

    /**

    早期的单例对象的高速缓存: bean名称—bean实例。

    表示 Bean的生命周期还没走完(Bean的属性还未填充)就把这个 Bean存入该缓存中也就是实例化但未初始化的 bean放入该缓存里

    第二级缓存

    */

    private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

    /**

    单例工厂的高速缓存:bean名称—ObjectFactory

    表示存放生成 bean的工厂

    第三级缓存

    */

    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

}

debug源代码过程

需要22个断点(可选)

1,A创建过程中需要B,于是A将自己放到三级缓里面,去实例化B

2,B实例化的时候发现需要A,于是B先查一级缓存,没有,再查二级缓存,还是没有,再查三级缓存,找到了A然后把三级缓存里面的这个A放到二级缓存里面,并删除三级缓存里面的A

3,B顺利初始化完毕,将自己放到一级缓存里面(此时B里面的A依然是创建中状态)

然后回来接着创建A,此时B已经创建结束,直接从一级缓存里面拿到B,然后完成创建,并将A自己放到一级缓存里面。

总结

1,Spring创建 bean主要分为两个步骤,创建原始bean对象,接着去填充对象属性和初始化。

2,每次创建 bean之前,我们都会从缓存中查下有没有该bean,因为是单例,只能有一个。

3,当创建 A的原始对象后,并把它放到三级缓存中,接下来就该填充对象属性了,这时候发现依赖了B,接着就又去创建B,同样的流程,创建完B填充属性时又发现它依赖了A又是同样的流程,不同的是:这时候可以在三级缓存中查到刚放进去的原始对象A。

所以不需要继续创建,用它注入 B,完成 B的创建既然 B创建好了,所以 A就可以完成填充属性的步骤了,接着执行剩下的逻辑,闭环完成

Spring解决循环依赖依靠的是Bean的"中间态"这个概念,而这个中间态指的是已经实例化但还没初始化的状态—>半成品。实例化的过程又是通过构造器创建的,如果A还没创建好出来怎么可能提前曝光,所以构造器的循环依赖无法解决

其他衍生问题

问题1:为什么构造器注入属性无法解决循环依赖问题?

       由于spring中的bean的创建过程为先实例化 再初始化(在进行对象实例化的过程中不必赋值)将实例化好的对象暴露出去,供其他对象调用,然而使用构造器注入,必须要使用构造器完成对象的初始化的操作,就会陷入死循环的状态

问题2:一级缓存能不能解决循环依赖问题? 不能

       在三个级别的缓存中存储的对象是有区别的 一级缓存为完全实例化且初始化的对象 二级缓存实例化但未初始化对象 如果只有一级缓存,如果是并发操作下,就有可能取到实例化但未初始化的对象,就会出现问题

问题3:二级缓存能不能解决循环依赖问题?

      理论上二级缓存可以解决循环依赖问题,但是需要注意,为什么需要在三级缓存中存储匿名内部类(ObjectFactory),原因在于 需要创建代理对象  eg:现有A类,需要生成代理对象 A是否需要进行实例化(需要) 在三级缓存中存放的是生成具体对象的一个匿名内部类,该类可能是代理类也可能是普通的对象,而使用三级缓存可以保证无论是否需要是代理对象,都可以保证使用的是同一个对象,而不会出现,一会儿使用普通bean 一会儿使用代理类

7、介绍一下Spring bean 的生命周期、注入方式和作用域    难度系数:⭐

Bean的生命周期

(1)默认情况下,IOC容器中bean的生命周期分为五个阶段:

调用构造器 或者是通过工厂的方式创建Bean对象给bean对象的属性注入值调用初始化方法,进行初始化, 初始化方法是通过init-method来指定的.使用IOC容器关闭时, 销毁Bean对象.

(2)当加入了Bean的后置处理器后,IOC容器中bean的生命周期分为七个阶段:

调用构造器 或者是通过工厂的方式创建Bean对象给bean对象的属性注入值执行Bean后置处理器中的 postProcessBeforeInitialization调用初始化方法,进行初始化, 初始化方法是通过init-method来指定的.x执行Bean的后置处理器中 postProcessAfterInitialization  使用IOC容器关闭时, 销毁Bean对象

只需要回答出第一点即可,第二点也回答可适当 加分。

注入方式:

通过 setter 方法注入

通过构造方法注入

Bean的作用域

总共有四种作用域:

Singleton  单例的Prototype  原型的RequestSession
8、请描述一下Spring 的事务管理    难度系数:⭐

(1)声明式事务管理的定义:用在 Spring 配置文件中声明式的处理事务来代替代码式的处理事务。这样的好处是,事务管理不侵入开发的组件,具体来说,业务逻辑对象就不会意识到正在事务管理之中,事实上也应该如此,因为事务管理是属于系统层面的服务,而不是业务逻辑的一部分,如果想要改变事务管理策划的话,也只需要在定义文件中重新配置即可,这样维护起来极其方便。

基于 TransactionInterceptor  的声明式事务管理:两个次要的属性: transactionManager,用来指定一个事务治理器, 并将具体事务相关的操作请托给它; 其他一个是 Properties 类型的transactionAttributes 属性,该属性的每一个键值对中,键指定的是方法名,方法名可以行使通配符, 而值就是表现呼应方法的所运用的事务属性。

(2)基于 @Transactional 的声明式事务管理:Spring 2.x 还引入了基于 Annotation 的体式格式,具体次要触及@Transactional 标注。@Transactional 可以浸染于接口、接口方法、类和类方法上。算作用于类上时,该类的一切public 方法将都具有该类型的事务属性。

(3)编程式事物管理的定义:在代码中显式挪用 beginTransaction()、commit()、rollback()等事务治理相关的方法, 这就是编程式事务管理。Spring 对事物的编程式管理有基于底层 API 的编程式管理和基于 TransactionTemplate 的编程式事务管理两种方式。

9、MyBatis中 #{}和${}的区别是什么    难度系数:⭐

#{}是预编译处理,${}是字符串替换;

Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值;

Mybatis在处理${}时,就是把${}替换成变量的值;

使用#{}可以有效的防止SQL注入,提高系统安全性。

10、Mybatis 中一级缓存与二级缓存    难度系数:⭐
MyBatis的缓存分为一级缓存和 二级缓存。

一级缓存是SqlSession级别的缓存,默认开启。

二级缓存是NameSpace级别(Mapper)的缓存,多个SqlSession可以共享,使用时需要进行配置开启。

缓存的查找顺序:二级缓存 => 一级缓存 => 数据库
11、MyBatis如何获取自动生成的(主)键值    难度系数:⭐

在<insert>标签中使用 useGeneratedKeys和keyProperty 两个属性来获取自动生成的主键值。

示例:

<insert id=”insertname” usegeneratedkeys=”true” keyproperty=”id”>
    insert into names (name) values (#{name}) 
</insert>

Java

12、简述Mybatis的动态SQL,列出常用的6个标签及作用    难度系数:⭐

动态SQL是MyBatis的强大特性之一 基于功能强大的OGNL表达式。

动态SQL主要是来解决查询条件不确定的情况,在程序运行期间,根据提交的条件动态的完成查询

常用的标签:

<if> : 进行条件的判断

<where>:在<if>判断后的SQL语句前面添加WHERE关键字,并处理SQL语句开始位置的AND 或者OR的问题

<trim>:可以在SQL语句前后进行添加指定字符 或者去掉指定字符.

<set>:  主要用于修改操作时出现的逗号问题

<choose> <when> <otherwise>:类似于java中的switch语句.在所有的条件中选择其一

<foreach>:迭代操作

13、Mybatis 如何完成MySQL的批量操作    难度系数:⭐

MyBatis完成MySQL的批量操作主要是通过<foreach>标签来拼装相应的SQL语句

例如:

<insert** id="insertBatch" >
    insert into tbl_employee(last_name,email,gender,d_id) values 
   <foreach** collection="emps" item="curr_emp" separator=","**>
      (#{curr_emp.lastName},#{curr_emp.email},#{curr_emp.gender},#{curr_emp.dept.id}) 
   </foreach>
</insert>

Java

14、谈谈怎么理解SpringBoot框架    难度系数:⭐⭐

Spring Boot 是 Spring 开源组织下的子项目,是 Spring 组件一站式解决方案,主要是简化了使用 Spring 的难度,简省了繁重的配置,提供了各种启动器,开发者能快速上手。

Spring Boot的优点

独立运行

Spring Boot而且内嵌了各种servlet容器,Tomcat、Jetty等,现在不再需要打成war包部署到容器中,Spring Boot只要打成一个可执行的jar包就能独立运行,所有的依赖包都在一个jar包内。

简化配置

spring-boot-starter-web启动器自动依赖其他组件,简少了maven的配置。除此之外,还提供了各种启动器,开发者能快速上手。

自动配置

Spring Boot能根据当前类路径下的类、jar包来自动配置bean,如添加一个spring-boot-starter-web启动器就能拥有web的功能,无需其他配置。

无代码生成和XML配置

Spring Boot配置过程中无代码生成,也无需XML配置文件就能完成所有配置工作,这一切都是借助于条件注解完成的,这也是Spring4.x的核心功能之一。

应用监控

Spring Boot提供一系列端点可以监控服务及应用,做健康检测。

Spring Boot缺点:

Spring Boot虽然上手很容易,但如果你不了解其核心技术及流程,所以一旦遇到问题就很棘手,而且现在的解决方案也不是很多,需要一个完善的过程。

15、Spring Boot 的核心注解是哪个 它主要由哪几个注解组成的    难度系数:⭐

启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解:

@SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。@EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项, 如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。@ComponentScan:Spring组件扫描。
16、Spring Boot自动配置原理是什么    难度系数:⭐

注解 @EnableAutoConfiguration, @Configuration, @ConditionalOnClass 就是自动配置的核心,

首先它得是一个配置文件,其次根据类路径下是否有这个类去自动配置。

@EnableAutoConfiguration是实现自动配置的注解

@Configuration表示这是一个配置文件

具体参考文档:

Spring Boot自动配置原理、实战

17、SpringBoot配置文件有哪些 怎么实现多环境配置    难度系数:⭐

Spring Boot 的核心配置文件是 application 和 bootstrap 配置文件。

application 配置文件这个容易理解,主要用于 Spring Boot 项目的自动化配置。

bootstrap配置文件的特性:

bootstrap 由父 ApplicationContext 加载,比 applicaton 优先加载bootstrap 里面的属性不能被覆盖

bootstrap 配置文件有以下几个应用场景:

使用 Spring Cloud Config 配置中心时,这时需要在 bootstrap 配置文件中添加连接到配置中心的配置属性来加载外部配置中心的配置信息;一些固定的不能被覆盖的属性;一些加密/解密的场景;

提供多套配置文件,如:

applcation.properties
application-dev.properties
application-test.properties
application-prod.properties

运行时指定具体的配置文件,具体请看这篇文章《Spring Boot Profile 不同环境配置》。

18、SpringBoot和SpringCloud是什么关系    难度系数:⭐

Spring Boot 是 Spring 的一套快速配置脚手架,可以基于Spring Boot 快速开发单个微服务,Spring Cloud是一个基于Spring Boot实现的开发工具;Spring Boot专注于快速、方便集成的单个微服务个体,Spring Cloud关注全局的服务治理框架; Spring Boot使用了默认大于配置的理念,很多集成方案已经帮你选择好了,能不配置就不配置,Spring Cloud很大的一部分是基于Spring Boot来实现,必须基于Spring Boot开发。

 可以单独使用Spring Boot开发项目,但是Spring Cloud离不开 Spring Boot。

19、SpringCloud都用过哪些组件 介绍一下作用    难度系数:⭐
Nacos--作为注册中心和配置中心,实现服务注册发现和服务健康监测及配置信息统一管理Gateway--作为网关,作为分布式系统统一的出入口,进行服务路由,统一鉴权等OpenFeign--作为远程调用的客户端,实现服务之间的远程调用Sentinel--实现系统的熔断限流Sleuth--实现服务的链路追踪
20、Nacos作用以及注册中心的原理    难度系数:⭐⭐

Nacos英文全称Dynamic Naming and Configuration Service,Na为naming/nameServer即注册中心,co为configuration即注册中心,service是指该注册/配置中心都是以服务为核心。

Nacos注册中心分为server与client,server采用Java编写,为client提供注册发现服务与配置服务。而client可以用多语言实现,client与微服务嵌套在一起,nacos提供sdk和openApi,如果没有sdk也可以根据openApi手动写服务注册与发现和配置拉取的逻辑。

服务注册原理

服务注册方法:以Java nacos client v1.0.1 为例子,服务注册的策略的是每5秒向nacos server发送一次心跳,心跳带上了服务名,服务ip,服务端口等信息。同时 nacos server也会向client 主动发起健康检查,支持tcp/http检查。如果15秒内无心跳且健康检查失败则认为实例不健康,如果30秒内健康检查失败则剔除实例。

21、Feign工作原理    难度系数:⭐⭐

主程序入口添加了@EnableFeignClients注解开启对FeignClient扫描加载处理。根据Feign Client的开发规范,定义接口并加@FeignClient注解。当程序启动时,会进行包扫描,扫描所有@FeignClient的注解的类,并且讲这些信息注入Spring IOC容器中,当定义的的Feign接口中的方法被调用时,通过JDK的代理方式,来生成具体的RequestTemplate.当生成代理时,Feign会为每个接口方法创建一个RequestTemplate。当生成代理时,Feign会为每个接口方法创建一个RequestTemplate对象,该对象封装HTTP请求需要的全部信息,如请求参数名,请求方法等信息都是在这个过程中确定的。然后RequestTemplate生成Request,然后把Request交给Client去处理,这里指的时Client可以时JDK原生的URLConnection,Apache的HttpClient,也可以时OKhttp,最后Client被封装到LoadBalanceClient类,这个类结合Ribbon负载均衡发器服务之间的调用。

第四章-MySQL
1、Select 语句完整的执行顺序    难度系数:⭐

SQL Select 语句完整的执行顺序:

(1)from 子句组装来自不同数据源的数据;

(2)where 子句基于指定的条件对记录行进行筛选;

(3)group by 子句将数据划分为多个分组;

(4)使用聚集函数进行计算;

(5)使用 having 子句筛选分组;

(6)计算所有的表达式;

(7)select 的字段;

(8)使用order by 对结果集进行排序。

2、MySQL事务    难度系数:⭐⭐

事务的基本要素(ACID)

原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能A扣了钱,B却没收到。隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。

MySQL事务隔离级别:

事务隔离级别

脏读                      

不可重复读

幻读

读未提交(read-uncommitted)

读提交(read-committed)

可重复读(repeatable-read)

串行化(serializable)

否                         

事务的并发问题

脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

如何解决脏读、幻读、不可重复读

脏读: 隔离级别为 读提交、可重复读、串行化可以解决脏读不可重复读:隔离级别为可重复读、串行化可以解决不可重复读幻读:隔离级别为串行化可以解决幻读、通过MVCC + 区间锁可以解决幻读

小结:

不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

3、MyISAM和InnoDB的区别    难度系数:⭐

MyISAM

InnoDB

事务

不支持

支持

表锁

表锁、行锁

文件存储

3个

1个

外键

不支持

支持

4、悲观锁和乐观锁的怎么实现     难度系数:⭐⭐

悲观锁:select...for update是MySQL提供的实现悲观锁的方式。

            例如:select price from item where id=100 for update

此时在items表中,id为100的那条数据就被我们锁定了,其它的要执行select price from items where id=100 for update的事务必须等本次事务提交之后才能执行。这样我们可以保证当前的数据不会被其它事务修改。MySQL有个问题是select...for update语句执行中所有扫描过的行都会被锁上,因此在MySQL中用悲观锁务必须确定走了索引,而不是全表扫描,否则将会将整个数据表锁住。

乐观锁:乐观锁相对悲观锁而言,它认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回错误信息,让用户决定如何去做。

利用数据版本号(version)机制是乐观锁最常用的一种实现方式。一般通过为数据库表增加一个数字类型的 “version” 字段,当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值+1。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据,返回更新失败。

举例:

//1: 查询出商品信息

select (quantity,version) from items where id=100;

//2: 根据商品信息生成订单

insert into orders(id,item_id) values(null,100);

//3: 修改商品的库存

update items set quantity=quantity-1,version=version+1 where id=100 and version=#{version};

5、聚簇索引与非聚簇索引区别     难度系数:⭐⭐

都是B+树的数据结构

聚簇索引:将数据存储与索引放到了一块、并且是按照一定的顺序组织的,找到索引也就找到了数据,数据的物理存放顺序与索引顺序是一致的,即:只要索引是相邻的,那么对应的数据一定也是相邻地存放在磁盘上的

非聚簇索引叶子节点不存储数据、存储的是数据行地址,也就是说根据索引查找到数据行的位置再取磁盘查找数据,这个就有点类似一本书的目录,比如我们要找第三章第一节,那我们先在这个目录里面找,找到对应的页码后再去对应的页码看文章。

优势:

1、查询通过聚簇索引可以直接获取数据,相比非聚簇索引需要第二次查询(非覆盖索引的情况下)效率要高

2、聚簇索引对于范围查询的效率很高,因为其数据是按照大小排列的

3、聚簇索引适合用在排序的场合,非聚簇索引不适合

劣势;

1、维护索引很昂贵,特别是插入新行或者主键被更新导至要分页(pagesplit)的时候。建议在大量插入新行后,选在负载较低的时间段,通过OPTIMIZETABLE优化表,因为必须被移动的行数据可能造成碎片。使用独享表空间可以弱化碎片

2、表因为使用uuId(随机ID)作为主键,使数据存储稀疏,这就会出现聚簇索引有可能有比全表扫面更慢,所以建议使用int的auto_increment作为主键

3、如果主键比较大的话,那辅助索引将会变的更大,因为辅助索引的叶子存储的是主键值,过长的主键值,会导致非叶子节点占用占用更多的物理空间

6、什么情况下mysql会索引失效    难度系数:⭐

失效条件:

where 后面使用函数使用or条件模糊查询 %放在前边类型转换组合索引 (最佳左前缀匹配原则)

#查询条件用到了计算或者函数
explain SELECT *  from test_slow_query where age = 20
explain SELECT *  from test_slow_query where age +10 = 30

#模糊查询
EXPLAIN SELECT * from test_slow_query where NAME like '%吕布'
EXPLAIN SELECT * from test_slow_query where NAME like '%吕布%'
EXPLAIN SELECT * from test_slow_query where NAME like '吕布&'

#用到了or条件
EXPLAIN SELECT * from test_slow_query where NAME = '吕布' or name = "aaa"

#类型不匹配查询
explain SELECT * from test_slow_query where NAME = 11
explain SELECT * from test_slow_query where NAME = '11'

SQL

7、B+tree 与 B-tree区别    难度系数:⭐⭐

原理:分批次的将磁盘块加载进内存中进行检索,若查到数据,则直接返回,若查不到,则释放内存,并重新加载同等数据量的索引进内存,重新遍历

结构: 数据  向下的指针 指向数据的指针

特点:

        1,节点排序

        2 .一个节点了可以存多个元索,多个元索也排序了

结构: 数据  向下的指针

特点:

    1.拥有B树的特点

    2.叶子节点之间有指针

    3.非叶子节点上的元素在叶子节点上都冗余了,也就是叶子节点中存储了所有的元素,并且排好顺序

从结构上看,B+Tree 相较于 B-Tree 而言  缺少了指向数据的指针 也就红色小方块;

Mysq|索引使用的是B+树,因为索引是用来加快查询的,而B+树通过对数据进行排序所以是可以提高查询速度的,然后通过一个节点中可以存储多个元素,从而可以使得B+树的高度不会太高,在Mysql中一个Innodb页就是一个B+树节点,一个Innodb页默认16kb,所以一般情况下一颗两层的B+树可以存2000万行左右的数据,然后通过利用B+树叶子节点存储了所有数据并且进行了排序,并且叶子节点之间有指针,可以很好的支持全表扫描,范围查找等SQL语句

文章推荐:B-Tree和B+Tree的区别 - 简书

8、以MySQL为例Linux下如何排查问题    难度系数:⭐⭐

类似提问方式:如果线上环境出现问题比如网站卡顿重则瘫痪 如何是好?

--->linux--->mysql/redis/nacos/sentinel/sluth--->可以从以上提到的技术点中选择一个自己熟悉单技术点进行分析

以mysql为例

1,架构层面 是否使用主从

2,表结构层面 是否满足常规的表设计规范(大量冗余字段会导致查询会变得很复杂)

3,sql语句层面(⭐)

前提:由于慢查询日志记录默认是关闭的,所以开启数据库mysql的慢查询记录 的功能 从慢查询日志中去获取哪些sql语句时慢查询  默认10S ,从中获取到sql语句进行分析

3.1 explain 分析一条sql

Id:执行顺序 如果单表的话,无参考价值 如果是关联查询,会据此判断主表 从表

Select_type:simple

Table:表

Type:  ALL 未创建索引 、const、 常量ref其他索引 、eq_ref 主键索引、

Possible_keys

Key  实际是到到索引到字段

Key_len  索引字段数据结构所使用长度 与是否有默认值null 以及对应字段到数据类型有关,有一个理论值 有一个实际使用值也即key_len的值

Rows  检索的行数 与查询返回的行数无关

Extra  常见的值:usingfilesort 使用磁盘排序算法进行排序,事关排序 分组 的字段是否使用索引的核心参考值

还可能这样去提问:sql语句中哪些位置适合建索引/索引建立在哪个位置

Select id,name,age from user where id=1 and name=”xxx” order by age

总结:  查询字段  查询条件(最常用)   排序/分组字段

补充:如何判断是数据库的问题?可以借助于top命令

9、如何处理慢查询    难度系数:⭐⭐

在业务系统中,除了使用主键进行的查询,其他的都会在测试库上测试其耗时,慢查询的统计主要由运维在做,会定期将业务中的慢查询反馈给我们。

慢查询的优化首先要搞明白慢的原因是什么?是查询条件没有命中索引?是加载了不需要的数据列?还是数据量太大?

所以优化也是针对这三个方向来的

首先分析语句,看看是否加载了额外的数据,可能是查询了多余的行并且抛弃掉了,可能是加载了许多结果中并不需要的列,对语句进行分析以及重写。

分析语句的执行计划,然后获得其使用索引的情况,之后修改语句或者修改索引,使得语句可以尽可能的命中索引。

如果对语句的优化已经无法进行,可以考虑表中的数据量是否太大,如果是的话可以进行横向或者纵向的分表。

具体处理流程 (阿里云RDS为例)

1.开启慢查询设置

日志管理导出慢查询文件

测试环境通过explain执行sql,主要关心以下字段

type:连接类型

key: MYSQL使用的索引

rows:显示MYSQL执行查询的行数,简单且重要,数值越大越不好,说明没有用好索引

extra:该列包含MySQL解决查询的详细信息。

10、数据库分表操作    难度系数:⭐

水平分表

步长法:1000万一张表拆分

取模法:举例:根据用户id取模落入不能的表

垂直分表:大表拆小表。商品信息 spu_info spu_image ...

可以说使用Mycat或者ShardingSphere等中间件来做,具体怎么做就要结合具体的场景进行分析了。可以参考:MySQL分库分表,写得太好了!-mysql分库分表

11、MySQL优化    难度系数:⭐

(1)尽量选择较小的列

(2)将where中用的比较频繁的字段建立索引

(3)select子句中避免使用‘*’

(4)避免在索引列上使用计算、not in 和<>等操作

(5)当只需要一行数据的时候使用limit 1

(6)保证单表数据不超过200W,适时分割表。针对查询较慢的语句,可以使用explain 来分析该语句具体的执行情况。

(7)避免改变索引列的类型。

(8)选择最有效的表名顺序,from字句中写在最后的表是基础表,将被最先处理,在from子句中包含多个表的情况下,你必须选择记录条数最少的表作为基础表。

(9)避免在索引列上面进行计算。

(10)尽量缩小子查询的结果

12、SQL语句优化案例    难度系数:⭐

例1:where 子句中可以对字段进行 null 值判断吗?

可以,比如 select id from t where num is null 这样的 sql 也是可以的。但是最好不要给数据库留NULL,尽可能的使用 NOT NULL 填充数据库。不要以为 NULL 不需要空间,比如:char(100) 型,在字段建立时,空间就固定了, 不管是否插入值(NULL 也包含在内),都是占用 100 个字符的空间的,如果是 varchar 这样的变长字段,null 不占用空间。可以在 num 上设置默认值 0,确保表中 num 列没有 null 值,然后这样查询:select id from t where num= 0。

例2:如何优化?下面的语句?

select * from admin left    join      log on admin.admin_id    = log.admin_id where log.admin_id>10

优化为:select * from (select * from admin where admin_id>10) T1 lef join log on T1.admin_id = log.admin_id。

使用 JOIN 时候,应该用小的结果驱动大的结果(left join 左边表结果尽量小如果有条件应该放到左边先处理, right join 同理反向),同时尽量把牵涉到多表联合的查询拆分多个 query(多个连表查询效率低,容易到之后锁表和阻塞)。

 篇幅限制下面就只能给大家展示小册部分内容了。这份面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处】即可免费获取

例3:limit 的基数比较大时使用 between

例如:select * from admin order by admin_id limit 100000,10

优化为:select * from admin where admin_id between 100000 and 100010 order by admin_id。

例4:尽量避免在列上做运算,这样导致索引失效

例如:select * from admin where year(admin_time)>2014

优化为: select * from admin where admin_time> '2014-01-01′

13、你们公司有哪些数据库设计规范    难度系数:⭐

(一)基础规范

1、表存储引擎必须使用InnoD,表字符集默认使用utf8,必要时候使用utf8mb4

解读:

(1)通用,无乱码风险,汉字3字节,英文1字节

(2)utf8mb4是utf8的超集,有存储4字节例如表情符号时,使用它

2、禁止使用存储过程,视图,触发器,Event

解读:

(1)对数据库性能影响较大,互联网业务,能让站点层和服务层干的事情,不要交到数据库层

(2)调试,排错,迁移都比较困难,扩展性较差

3、禁止在数据库中存储大文件,例如照片,可以将大文件存储在对象存储系统,数据库中存储路径

4、禁止在线上环境做数据库压力测试

5、测试,开发,线上数据库环境必须隔离

(二)命名规范

1、库名,表名,列名必须用小写,采用下划线分隔

解读:abc,Abc,ABC都是给自己埋坑

2、库名,表名,列名必须见名知义,长度不要超过32字符

解读:tmp,wushan谁知道这些库是干嘛的

3、库备份必须以bak为前缀,以日期为后缀

4、从库必须以-s为后缀

5、备库必须以-ss为后缀

(三)表设计规范

1、单实例表个数必须控制在2000个以内

2、单表分表个数必须控制在1024个以内

3、表必须有主键,推荐使用UNSIGNED整数为主键

潜在坑:删除无主键的表,如果是row模式的主从架构,从库会挂住

4、禁止使用外键,如果要保证完整性,应由应用程式实现

解读:外键使得表之间相互耦合,影响update/delete等SQL性能,有可能造成死锁,高并发情况下容易成为数据库瓶颈

5、建议将大字段,访问频度低的字段拆分到单独的表中存储,分离冷热数据

(四)列设计规范

1、根据业务区分使用tinyint/int/bigint,分别会占用1/4/8字节

2、根据业务区分使用char/varchar

解读:

(1)字段长度固定,或者长度近似的业务场景,适合使用char,能够减少碎片,查询性能高

(2)字段长度相差较大,或者更新较少的业务场景,适合使用varchar,能够减少空间

3、根据业务区分使用datetime/timestamp

解读:前者占用5个字节,后者占用4个字节,存储年使用YEAR,存储日期使用DATE,存储时间使用datetime

4、必须把字段定义为NOT NULL并设默认值

解读:

(1)NULL的列使用索引,索引统计,值都更加复杂,MySQL更难优化

(2)NULL需要更多的存储空间

(3)NULL只能采用IS NULL或者IS NOT NULL,而在=/!=/in/not in时有大坑

5、使用INT UNSIGNED存储IPv4,不要用char(15)

6、使用varchar(20)存储手机号,不要使用整数

解读:

(1)牵扯到国家代号,可能出现+/-/()等字符,例如+86

(2)手机号不会用来做数学运算

(3)varchar可以模糊查询,例如like ‘138%’

7、使用TINYINT来代替ENUM

解读:ENUM增加新值要进行DDL操作

(五)索引规范

1、唯一索引使用uniq_[字段名]来命名

2、非唯一索引使用idx_[字段名]来命名

3、单张表索引数量建议控制在5个以内

解读:

(1)互联网高并发业务,太多索引会影响写性能

(2)生成执行计划时,如果索引太多,会降低性能,并可能导致MySQL选择不到最优索引

(3)异常复杂的查询需求,可以选择ES等更为适合的方式存储

4、组合索引字段数不建议超过5个

解读:如果5个字段还不能极大缩小row范围,八成是设计有问题

5、不建议在频繁更新的字段上建立索引

6、非必要不要进行JOIN查询,如果要进行JOIN查询,被JOIN的字段必须类型相同,并建立索引

解读:踩过因为JOIN字段类型不一致,而导致全表扫描的坑么?

7、理解组合索引最左前缀原则,避免重复建设索引,如果建立了(a,b,c),相当于建立了(a), (a,b), (a,b,c)

(六)SQL规范

1、禁止使用select *,只获取必要字段

解读:

(1)select *会增加cpu/io/内存/带宽的消耗

(2)指定字段能有效利用索引覆盖

(3)指定字段查询,在表结构变更时,能保证对应用程序无影响

2、insert必须指定字段,禁止使用insert into T values()

解读:指定字段插入,在表结构变更时,能保证对应用程序无影响

3、隐式类型转换会使索引失效,导致全表扫描

4、禁止在where条件列使用函数或者表达式

解读:导致不能命中索引,全表扫描

5、禁止负向查询以及%开头的模糊查询

解读:导致不能命中索引,全表扫描

6、禁止大表JOIN和子查询

7、同一个字段上的OR必须改写问IN,IN的值必须少于50个

8、应用程序必须捕获SQL异常

解读:方便定位线上问题

说明:本规范适用于并发量大,数据量大的典型互联网业务,可直接参考。

14、有没有设计过数据表?你是如何设计的    难度系数:⭐

第一范式
 

每一列属性(字段)不可分割的,字段必须保证原子性
两列的属性值相近或者一样的,尽量合并到一列或者分表,确保数据不冗余
 

第二范式
 

每一行的数据只能与其中一行有关 即 主键  一行数据只能做一件事情或者表达一个意思,
只要数据出现重复,就要进行表的拆分
 

第三范式
 

数据不能存在传递关系,每个属性都跟主键有直接关联而不是间接关联

 

15、常见面试SQL    难度系数:⭐

例1:

用一条SQL语句查询出每门课都大于80分的学生姓名

name   kecheng    fenshu
张三     语文     81
张三     数学     75
李四     语文     76
李四     数学          90
王五     语文     81
王五     数学     100
王五     英语     90
答1:

select distinct name from table where name not in (select distinct name from table where fenshu<=80)  
答2:

select name from table group by name having min(fenshu)>80  


例2:

学生表 如下:
自动编号    学号     姓名     课程编号    课程名称    分数
1                      2005001    张三   0001          数学          69
2                      2005002    李四   0001          数学          89
3                      2005001    张三    0001          数学          69
删除除了自动编号不同,其他都相同的学生冗余信息
答:

delete tablename where 自动编号 not in(select min(自动编号) from tablename group by学号, 姓名, 课程编号, 课程名称, 分数)  

例3:

一个叫team的表,里面只有一个字段name,一共有4条纪录,分别是a,b,c,d,对应四个球队,现在四个球队进行比赛,用一条sql语句显示所有可能的比赛组合.

答:

select a.name, b.name  from team a, team b   where a.name < b.name  

例4:

怎么把这样一个表
year           month amount
1991    1         1.1
1991   2         1.2
1991   3         1.3
1991   4         1.4
1992   1         2.1
1992   2         2.2
1992   3         2.3
1992   4         2.4
查成这样一个结果
year           m1       m2       m3       m4
1991   1.1            1.2            1.3            1.4
1992   2.1            2.2            2.3            2.4 
答:

select year,   (select amount from aaa m where month=1 and m.year=aaa.year) as m1,  (select amount from aaa m where month=2 and m.year=aaa.year) as m2,  (select amount from aaa m where month=3 and m.year=aaa.year) as m3,  (select amount from  aaa m where month=4 and m.year=aaa.year) as m4  from aaa group by year  


例5:

说明:复制表(只复制结构,源表名:a新表名:b) 

答:
SQL:

select * into b from a where 1<>1 (where1=1,拷贝表结构和数据内容)  

ORACLE:

 create table b  As  Select * from a where 1=2  

[<>(不等于)(SQL Server Compact)

比较两个表达式。 当使用此运算符比较非空表达式时,如果左操作数不等于右操作数,则结果为 TRUE。 否则,结果为 FALSE。]

例6:

原表:
courseid    coursename    score
1         java                70
2         oracle       90
3         xml                 40
4         jsp                  30
5         servlet            80

为了便于阅读,查询此表后的结果显式如下(及格分数为60):
courseid    coursename    score   mark
1         java                70       pass
2         oracle       90       pass
3         xml                 40       fail
4         jsp                  30       fail
5         servlet            80       pass
写出此查询语句

答:

select courseid, coursename ,score ,if(score>=60, "pass","fail")  as mark from course  


例7:

表名:购物信息

购物人  商品名称    数量

A         甲              2

B         乙          4

C         丙          1

A         丁          2

B         丙          5

给出所有购入商品为两种或两种以上的购物人记录

答:

select * from 购物信息 where 购物人 in (select 购物人 from 购物信息 group by 购物人 having count(*) >= 2);  

例8:

info 表

date                result

2005-05-09    win

2005-05-09    lose

2005-05-09    lose

2005-05-09    lose

2005-05-10    win

2005-05-10    lose

2005-05-10    lose

如果要生成下列结果, 该如何写sql语句?

date                win      lose

2005-05-09         2        2

2005-05-10         1        2

答1:

select date, sum(case when result = "win" then 1 else 0 endas "win", sum(case when result = "lose" then 1 else 0 endas "lose" from info group by date;   

答2:

select a.date, a.result as win, b.result as lose   from   (select date, count(result) as result from info where result = "win" group by dateas a   join   (select date, count(result) as result from info where result = "lose" group by dateas b   on a.date = b.date;  

例9 mysql 创建了一个联合索引(a,b,c) 以下 索引生效 的是(1,2,4)

1、where a = 1 and b = 1 and c =1

2、where a = 1 and c = 1

3、where b = 1 and c = 1,

4、where b = 1 and a =1 and c = 1

第五章-Redis
1、介绍下Redis Redis有哪些数据类型    难度系数:⭐   

Redis全称(Remote Dictionary Server)本质上是一个Key-Value类型的内存数据库,整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据flush到硬盘上进行保存。因为是纯内存操作,Redis的性能非常出色,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB。 Redis的出色之处不仅仅是性能,Redis最大的魅力是支持保存多种数据结构,此外单个value的最大限制是1GB,不像 memcached只能保存1MB的数据,因此Redis可以用来实现很多有用的功能,比方说用他的List来做FIFO双向链表,实现一个轻量级的高性 能消息队列服务,用他的Set可以做高性能的tag系统等等。另外Redis也可以对存入的Key-Value设置expire时间,因此也可以被当作一 个功能加强版的memcached来用。 Redis的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。

常用基本数据类型如下:

string
 

字符串(一个字符串类型最大存储容量为512M)
 

list
 

可以重复的集合
 

set
 

不可以重复的集合
 

hash
 

类似于Map<String,String>
 

zset(sorted set)
 

带分数的set
 

2、Redis提供了哪几种持久化方式    难度系数:⭐  

RDB持久化方式能够在指定的时间间隔能对你的数据进行快照存储。

AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件末尾.Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大。

如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式。

你也可以同时开启两种持久化方式,在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。

(1)RDB持久化:

每隔一段时间,将内存中的数据集写到磁盘

Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。

保存策略:

save 9   00 1    900 秒内如果至少有 1 个 key 的值变化,则保存

save 300   10   300 秒内如果至少有 10 个 key 的值变化,则保存

save 60 1 0000  60 秒内如果至10000 个 key 的值变化,则保存

(2)AOF 持久化:  以日志形式记录每个更新((总结、改)操作

Redis重新启动时读取这个文件,重新执行新建、修改数据的命令恢复数据。

保存策略:

appendfsync always:每次产生一条新的修改数据的命令都执行保存操作;效率低,但是安全!

appendfsync everysec:每秒执行一次保存操作。如果在未保存当前秒内操作时发生了断电,仍然会导致一部分数据丢失(即1秒钟的数据)。

appendfsync no:从不保存,将数据交给操作系统来处理。更快,也更不安全的选择。

推荐(并且也是默认)的措施为每秒 fsync 一次, 这种 fsync 策略可以兼顾速度和安全性。

缺点:

1  比起RDB占用更多的磁盘空间

2 恢复备份速度要慢

3 每次读写都同步的话,有一定的性能压力

4 存在个别Bug,造成恢复不能

(3)选择策略:

可读的日志文本,通过操作AOF

官方推荐:

如果对数据不敏感,可以选单独用RDB;不建议单独用AOF,因为可能出现Bug;如果只是做纯内存缓存,可以都不用

3、Redis为什么快    难度系数:⭐  

1)完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)

2)数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的

3)采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗

4)使用I/O多路复用模型,非阻塞IO

IO:输入/输出

多路:多个输入/输出通道(socket)

复用:通过一种机制同时管理多个I/O操作

I/O多路复用是一种操作IO的技术,我们可以理解采用单线程同时管理多个Socket

多种I/O多路复用技术:select、poll、epoll

Redis在linux使用epoll机制

I/O 多路复用体现在计算机通信的操作系统内核层,具体来说,是在处理网络通信的过程中。操作系统内核层使用 I/O 多路复用技术来同时监视和管理多个套接字(Socket)的状态,以实现高效的并发通信。以下是一些关键环节:

接收数据和分发:当有多个客户端连接到服务器时,每个连接都需要进行数据的接收。操作系统内核使用 I/O 多路复用来同时监听多个套接字的读取事件,一旦某个套接字有数据可读,内核会通知应用程序,并将数据从内核缓冲区复制到应用程序的内存中。

发送数据和缓冲:类似地,内核使用 I/O 多路复用来监视套接字的写入事件,以确保可以高效地将数据从应用程序发送到套接字。当套接字可写时,内核会将应用程序提供的数据从内存缓冲区复制到内核缓冲区,然后通过网络传输。

连接管理:在服务器端,当有新的客户端连接请求时,操作系统内核可以使用 I/O 多路复用来监听连接事件,以便及时接受新的连接并进行处理。

多客户端并发处理:I/O 多路复用还可以帮助服务器同时处理多个客户端连接的读写操作,而不需要为每个连接创建独立的线程或进程。这有助于提高服务器的性能和并发处理能力。

5)使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求

4、Redis为什么是单线程的    难度系数:⭐  

官方FAQ表示,因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了Redis利用队列技术将并发访问变为串行访问

1)绝大部分请求是纯粹的内存操作

2)采用单线程,避免了不必要的上下文切换和竞争条件

5、Redis服务器的的内存是多大    难度系数:⭐  

配置文件中设置redis内存的参数:。

该参数如果不设置或者设置为0,则redis默认的内存大小为:

32位下默认是3G

64位下不受限制

一般推荐Redis设置内存为最大物理内存的四分之三,也就是0.75

命令行设置config set maxmemory <内存大小,单位字节>,服务器重启失效

config get maxmemory获取当前内存大小

永久则需要设置maxmemory参数,maxmemory是bytes字节类型,注意转换

6、为什么Redis的操作是原子性的,怎么保证原子性的    难度系数:⭐  

对于Redis而言,命令的原子性指的是:一个操作的不可以再分,操作要么执行,要么不执行。

Redis的操作之所以是原子性的,是因为Redis是单线程的。

Redis本身提供的所有API都是原子操作,Redis中的事务其实是要保证批量操作的原子性。

多个命令在并发中也是原子性的吗?

不一定, 将get和set改成单命令操作,incr 。使用Redis的事务,或者使用Redis+Lua==的方式实现.

7、Redis有事务吗    难度系数:⭐  

Redis是有事务的,redis中的事务是一组命令的集合,这组命令要么都执行,要不都不执行,

redis事务的实现,需要用到MULTI(事务的开始)和EXEC(事务的结束)命令 ;

当输入MULTI命令后,服务器返回OK表示事务开始成功,然后依次输入需要在本次事务中执行的所有命令,每次输入一个命令服务器并不会马上执行,而是返回”QUEUED”,这表示命令已经被服务器接受并且暂时保存起来,最后输入EXEC命令后,本次事务中的所有命令才会被依次执行,可以看到最后服务器一次性返回了两个OK,这里返回的结果与发送的命令是按顺序一一对应的,这说明这次事务中的命令全都执行成功了。

Redis的事务除了保证所有命令要不全部执行,要不全部不执行外,还能保证一个事务中的命令依次执行而不被其他命令插入。同时,redis的事务是不支持回滚操作的。

8、Redis数据和MySQL数据库的一致性如何实现    难度系数:⭐⭐

一、 延时双删策略

        在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。具体步骤是:

        1)先删除缓存

        2)再写数据库

        3)休眠500毫秒(根据具体的业务时间来定)

        4)再次删除缓存。

        那么,这个500毫秒怎么确定的,具体该休眠多久呢?

        需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

        当然,这种策略还要考虑 redis 和数据库主从同步的耗时。最后的写数据的休眠时间:则在读数据业务逻辑的耗时的基础上,加上几百ms即可。比如:休眠1秒。

二、设置缓存的过期时间

        从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存

        结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时。

三、如何写完数据库后,再次删除缓存成功?

        上述的方案有一个缺点,那就是操作完数据库后,由于种种原因删除缓存失败,这时,可能就会出现数据不一致的情况。这里,我们需要提供一个保障重试的方案。

1、方案一具体流程

        (1)更新数据库数据; 

        (2)缓存因为种种问题删除失败;

        (3)将需要删除的key发送至消息队列;

        (4)自己消费消息,获得需要删除的key;

        (5)继续重试删除操作,直到成功。

        然而,该方案有一个缺点,对业务线代码造成大量的侵入。于是有了方案二,在方案二中,启动一个订阅程序去订阅数据库的binlog,获得需要操作的数据。在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作。 

2、方案二具体流程

        (1)更新数据库数据;

        (2)数据库会将操作信息写入binlog日志当中;

        (3)订阅程序提取出所需要的数据以及key; 

        (4)另起一段非业务代码,获得该信息;

        (5)尝试删除缓存操作,发现删除失败; 

        (6)将这些信息发送至消息队列;

        (7)重新从消息队列中获得该数据,重试操作。

9、缓存击穿,缓存穿透,缓存雪崩的原因和解决方案(或者说使用缓存的过程中有没有遇到什么问题,怎么解决的)    难度系数:⭐ 

1. 缓存穿透:

是指查询一个不存在的数据,由于缓存无法命中,将去查询数据库,但是数据库也无此记录,并且出于容错考虑,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。

解决方案:空结果也进行缓存,可以设置一个空对象,但它的过期时间会很短,最长不超过五分钟。 或者用布隆过滤器也可以解决,Redisson框架中有布隆过滤器。

2. 缓存雪崩:

是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。

解决方案:原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

3. 缓存击穿

是指对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:如果这个key在大量请求同时进来之前正好失效,那么所有对这个key的数据查询都落到DB,我们称为缓存击穿。

解决方案:在分布式的环境下,应使用分布式锁来解决,分布式锁的实现方案有多种,比如使用Redis的setnx、使用Zookeeper的临时顺序节点等来实现

 篇幅限制下面就只能给大家展示小册部分内容了。这份面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处】即可免费获取


点击全文阅读


本文链接:http://zhangshiyu.com/post/159106.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1