java面试题
一、java面向对象有哪些特征?
面向对象三大特征:封装、继承、多态。封装说明一个类行为和属性于其他类的关系,低耦合、高内聚;继承是父类和子类的关系;多态说的是类与类的关系;
封装隐藏了类的内部实现机制,可以在不影响使用的情况下改变类的内部结构,同时也保护了数据。对于外界,他的内部细节是隐藏的,暴露的只是他的访问方法。属性的封装:使用者只能通过事先制定好的方法来访问数据,可以方便加入逻辑控制,限制对属性的不合理操作;方法的封装:使用者按照既定的方式调用方法,不必关心方法的内部实现,便于使用,增强代码的可维护性;
继承是从已有的类中派生出新的类,新的类的数据属性和行为,并能扩展新的能力。子类继承父类,表示子类是一种特殊的父类,并且具有父类所没有的一些方法和属性。从多种实现类中抽出一个基类,使其具备多种实现类的共同特性,当实现类用extends继承了基类(父类)后,实现类就具备了那些相同的属性。当然,父类中通过private定义的变量和方法不会被继承。
对于多态,我更倾向于分为静态多态和动态多态,静态多态是指在编译时就已经确定的多态,常见的就是重载,重载是指两个或者多个函数具有形同的函数名,但参数列表不同,这样就可以根据参数据的不同来区分不同的参数。动态多态是指在运行时才能确定的多态,常见的就是重写,重写就是子类重新定义父类的方法,从而使父类方法可以根据不同的对象而具有不同的表达。
二、java中有哪几种方式来创建线程执行任务?
继承thread类
重写run方法,new thread方法跑。但是这样占用了当前类的继承名额,使得当前类不能再继承其他类
实现runnable接口
实现run方法,使用依然用到thread。这种方法更常用,其实thread类底层也是实现了runnable接口。
也可以通过匿名内部类的方式去生成一个runnable对象
1
2
3
4
5Thread thread = new Thread(new Runnable(){
public void run(){
System.out.println("hello world")
}
})又因为runnable是一个函数式接口(只有一个方法),所以也可以用lamda表达式
1
Thread thread = new Thread(() -> System.out.println("hello world"))
实现callable接口
他去执行任务可以拿到任务结果,不过底层也是Runnable
实现Runnable生成对象 => 传给FutureTask => 把FutureTask传给Thread => 启动线程 =>FutureTask用get()方法阻塞式取得结果
顺带一提,FutureTask也是实现了Runnable
线程池
底层还是Runnable,从某种层面上来说,全是用Runnable来做的
三、为什么不建议使用Executors来创建线程池
Executors创建线程池底层采用的是无界阻塞队列,当线程数一定但是任务很多时,会将任务不断的塞入队列中,直到耗尽内存导致OOM(内存溢出)
而且Executors也不能自定义线程的名字,不利于监控线程和排查问题。更建议使用ThreadPoolExecutor,他可以灵活控制线程
四、线程池有哪几种状态?分别表示什么?
RUNNING
表示线程池正常运行,既能接受新任务,也可以处理队列中的任务
SHUTDOWN
当调用线程池的shutdown()方法时,线程池就会进入SHUTDOWN状态。此时线程池不会接受新的任务,但是会把队列中的任务处理完
STOP
当调用线程池中的shutdownnow()方法时,线程池就会进入STOP状态(停止状态),此状态下,线程池既不会接受新任务,也不会处理队列中的任务,并且正在运行的线程也会直接中断
TIDYING
线程池中没有线程运行后,线程池的状态会自动进入TIDYING,并调用terminated()方法,该方法为空方法,可以进行扩展
TERMINATED
terminated()方法执行完后,状态也成了TERMINATED
五、Sychronized和ReentrantLock有哪些不同?
| sychronized | ReentrantLock |
|---|---|
| java中的关键字 | JDK提供的一个类 |
| 自动加锁与释放锁 | 需手动加锁与释放锁 |
| JVM层面的锁 | API层面的锁 |
| 锁的是对象,信息保存在对象头中 | int类型的state标识标识锁的状态 |
| 底层有锁升级过程 | 无锁升级过程 |
| 非公平锁 | 非公平锁或公平锁 |
六、ThreadLocal有哪些场景?底层是如何实现的?
- ThreadLocal是java中提出的线程本地储存机制,可以利用该机制将数据缓存在某个线程内,该线程可随时采用任意方法获取缓存的线程
- ThreadLocal底层通过ThreadLocalMap来实现的,每个Thread对象(不是ThreadLocal对象)都存在一个ThreadLocalMap中,Map的key是ThreadLocal对象,value是需要缓存的值
- 特别注意,如果在线程池中使用ThreadLocal会导致内存泄漏,因为当ThreadLocal对象使用完后,应把设置的key、value即Entry对象回收,但线程池中的线程没有回收,且线程对象是通过强引用指向ThreadLocalMap,而ThreadLocalMap也是通过强引用指向Entry对象。线程没有回收就代表Entry对象没有回收,从而导致内存泄漏。解决方法:在使用了ThreadLocal对象之后,手动调用ThreadLocal的remove()方法手动消除Entry对象
- TreadLocal常用于连接管理,不太常用
七、ReentrantLock分为公平锁和非公平锁,底层是如何实现的?
首先,无论公平锁还是非公平锁,他们的底层都是使用AQS来进行排队,他们的区别在于线程使用lock()方法加锁时
- 如果是公平锁,会先检车AQS队列中是否存在线程在排队,如果有就也进行排队
- 如果是非公平锁,则不会检查是否有线程排队,而是直接去竞争
此外,无论是非公平锁还是公平锁,一旦没拿到锁,都会进行排队,当释放锁时,都是唤醒最前面的线程。所以非公平锁只是体现在线程加锁阶段,而没有体现在唤醒阶段。ReentrantLock也是可重入锁,不论是公平还是非公平,都是可重入锁(即同一个线程可连续重复加同一把锁)
八、Sychronized的锁升级过程
- 偏向锁:在锁对象的对象头中记录下当前获取到该锁的线程ID,该线程下次如果又来获取,该锁就可以直接被获取,也就是支持锁重入
- 轻量级锁:由偏向锁升级而来,当一个线程获取到锁后,此时这把锁就是偏向锁。但是此时如果有第二把锁来竞争,偏向锁就会升级为轻量级锁。之所以叫轻量级锁,就是为了和重量级锁区分开来。轻量级锁的底层是通过自旋来实现的,不会阻塞线程
- 如果自旋次数过多却仍然没获得锁,则会升级为重量级锁,重量级锁会导致线程阻塞
- 自旋锁:当一个线程尝试获取某个锁时,如果该锁被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或者睡眠状态。具体就是通过CAS获取预期的一个标记,如果没有获取到,则继续循环获取。如果获取到了,则表示获取到这个锁。自旋锁不会阻塞线程
九、字节流与字符流的区别
字节流和字符流是Java I/O中的两种基本的数据流,它们之间的主要区别在于处理数据的单位不同。
字节流以字节为单位读写数据,它可以读写任何类型的数据,包括二进制数据和文本数据。字节流的输入输出对象是InputStream和OutputStream,常用的子类有FileInputStream和FileOutputStream。
字符流以字符为单位读写数据,它只能读写文本数据,不能读写二进制数据。字符流的输入输出对象是Reader和Writer,常用的子类有FileReader和FileWriter。
在Java中,字符流和字节流是通过InputStreamReader和OutputStreamWriter相互转换的。InputStreamReader可以将字节流转换为字符流,而OutputStreamWriter可以将字符流转换为字节流。这样就可以在字符流和字节流之间进行转换,以满足不同的应用需求。
十、JDK、JRE、JVM之间的区别
- JDK是JAVA标准开发包,提供了编译运行java程序所需的各种工具(比如编译器javac)和资源
- JRE是java运行环境,用于运行java字节码文件(.class),JRE包括JVM和JVM工作所需类库。普通用户只需要安装JRE跑程序,开发者则需要JDK来编译调试
- JVM是java虚拟机,JRE的一部分,是整个java实现跨平台的关键,负责运行字节码文件
十一、String、StringBuffer、StringBuild的区别
string是常量,不可变,如果尝试修改string,会新生成一个字符串对象。stringbuffer和stringbuilder可修改,但是stringbuffer是线程安全的,stringbuilder线程不安全,底层没有锁机制,但是速度相对更快
十二、泛型中extends和super的区别
- <? extends T> 表示包括T在内的任何T的子类
- <? super T> 表示包括T在内的任何T的父类
十三、 ==和equals方法的区别
- ==:如果是基本数据类型,比较的是值;如果是引用类型,比较的是引用地址
- equals:一般情况下和==没区别,他的底层就是==。但是在一些类中被重写了,比如string,虽然是引用类,但是比较的却是每个字符是否相等
十四、重载和重写的区别
- 重载:发生在同一个类中,方法名必须相同,参数类型不同、参数个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生于编译时
- 重写:发生于父子类中,方法名和参数必须相同,返回值范围不可大于父类,抛出异常的范围不可大于父类,访问修饰符范围大于或等于父类。若父类中该方法被private修饰,则不可被重写
十五、Set和List的区别
- List:有序,按对象进入的顺序保存对象,可重复,允许多个null元素对象,可以用迭代器进行遍历,也可以用get(index)来获取指定下标的元素
- Set:无序,不可重复,最多允许一个null元素对象。只可以用迭代器(Iterator)遍历
十六、 ArrayList和LinkList的区别
- 首先底层结构不同,ArrayList采用数组实现,而linkedList采用链表来实现
- 因此,他们的应用场景不同,ArrayList更适合随机查找,linkedList更适合删除和添加
- 此外,虽然ArrayList和Linkedlist都实现了List接口,但是Linkedlist还额外实现了Deque接口,所以Linkedlist也可以当队列来使用
十七、谈谈ConcurrentHashMap的扩容机制
这个得分开来谈:
jdK1.7:
- 基于segment对象分段实现
- 每个segment对象相当于一个小型的HashMap
- 每个segment对象内部会进行扩容,与HashMap类似
- 先生成新的数组,然后转移元素去新的数组
jdk1.8:
- 不再基于segment
- 当某个线程put时,如果发现concurrentHashMap在扩容,则一起扩容
- 如果线程put而concurrentHashMap没有扩容。则将key-value添加到concurrentHashMap中,判断是否超出阈值,超出则扩容
- concurrentHashMap支持多线程同时扩容
- 扩容前也新生成一个数组,转移元素时,先将原数组分组,将每组分给不同线程来进行元素转移,每个线程负责一组或者多组元素的转移
十八、JDK1.7与JDK1.8的区别(详细描述HashMap的变化)
- 1.8的接口可以有static方法实现和default方法实现
- JVM元空间替换永久代
- lambda表达式
- 新增stream流
- 函数式接口
- 日期API和并发API
- Switch支持string类型
- HashMap底层变化:
- JDK1.7中底层为数组+链表,1.8中是数组+链表+红黑树,红黑树的加入提高了HashMap的插入和查询的整体效率
- JDK1.7中链表使用头插法,JDK1.8中使用尾插法。因为JDK1.8中插入key-value时需要判断链表元素个数,所以需要遍历链表元素个数,所以正好采用尾插法
- JDK1.7中哈希算法比较复杂,存在各种右移和异或运算,由于JDK1.8中引入了红黑树,所以对其做了适当简化,节省CPU资源
十九、HashMap的put方法
先说说HashMap的Put方法的大体流程:
- 根据key通过哈希算法运算得出数组下标
- 若数组下表位置为空,则将key-value封装为Entry对象(JDK1.7为Entry对象,JDK1.8为Node对象)并放入该位置
- 若不为空,则分开讨论:
- 若为JDK1.7,则判断是否需要扩容,若不用扩容则生成Entry对象,并用头插法添加到当前位置的链表中
- 若为JDK1.8,则先判断当前位置上的Node类型,看是链表Node还是红黑树Node:
- 如果是红黑树Node,则将key-value封装为一个红黑树节点并添加到红黑树中去,这个过程会判断红黑树中是否存在当前key,存在则更新value
- 如果是链表Node,则将key-value封装为Node插入链表最后,因为采用尾插法,所以需要遍历链表,在遍历链表时会判断key是否存在于当前链表中,存在则更新value,遍历完之后,将新链表Node插入到链表中,插入后会看当前链表的节点个数,大于等于8则将该链表转为红黑树
- 将key和value封装为Node插入红黑树或者链表中后,再判断是否扩容,需要就扩容。否则就结束Put方法
二十、深拷贝和浅拷贝
深拷贝和浅拷贝就是指对象的拷贝,一个对象中存在两种类型的属性,一种是基本数据类型,另一种是实例对象引用。
- 浅拷贝是指只会拷贝基本数据类型的值,以及实例对象的引用地址,并不会复制一份引用对象地址所以指向的对象,也就是浅拷贝出来的对象内部的类属性指向的是同一个对象
- 深拷贝是指既会拷贝基本数据类型的值,也会针对实例对象的引用地址所指向的对象进行复制,深拷贝出来的对象,内部的属性指向的不是同一个对象
二十一、HashMap的扩容机制原理
JDK1.7
- 先生成数组
- 遍历老数组中的每个位置上的链表上的每个元素
- 取每个元素key,并基于新的数组长度,计算出每个元素在新数组中的下标
- 将元素添加到数组中去
- 所有元素转移完后,将新数组赋值给HashMap对象的table属性
JDK1.8
- 先生成新数组
- 遍历老数组中的每个位置上的链表或红黑树
- 如果是链表,则将链表中每个元素重新计算下标,并添加到新数组中去
- 如果是红黑树,则先遍历红黑树,先计算红黑树中每个元素对应新数组中的下标:
- 统计每个下标位置元素个数
- 如果该下标的元素个数超过8,则生成新的红黑树,并将根节点添加到新数组对应位置
- 如果不超过8,则生成一个链表,并将其头节点添加到新数组的对应位置
- 所有元素转移完成后,将新数组赋值给HashMap对象table属性
二十二、CopyOnWriteArrayList的底层原理
- 首先CopyOnWriteArrayList内部也是通过数组来实现的,在向CopyOnWriteArrayList添加元素时,会复制一个新的数组,写操作在新数组上进行,读操作在原数组上进行
- 且写操作会加锁,防止出现并发写入丢失数据的问题
- 写操作结束之后会把原数组指定新数组
- CopyOnWriteArrayList允许在写操作时来读取数据,极大的提高了读的性能,因此适合读多写少的场景,但是其比较占内存,同时可额能读到的数据不是实时数据,所以不适合实时性要求高的场景
二十三、为什么要实现序列化?如何实现序列化?
- 序列化:指把堆内存中的java对象数据,通过某种方式把对象存储到磁盘文件中或者传递给其他网络节点(在网络上传输),这个过程就是序列化。通俗来说就是将数据结构或者对象转换成二进制数据流的过程
- 反序列化:把磁盘文件中的数据对象或者把网络节点上的对象数据恢复成java对象模型的过程。也就是将在序列化过程中产生的二进制数据流转换成对象
二十四、java的异常体系
- Java中所有异常都来自顶级父类Throwable
- Throwable下面有两个子类Exception和Error
- Error表示非常严重的错误,比如StackOverFlowError和OutOfMemoryError,通常这些错误出现时,仅仅想靠程序自己是解决不了的,可能是虚拟机、磁盘或者操作系统出了问题,这类异常不要去捕获。出现Exception则表示可以依靠程序解决,如NullPointException
- Exception子类又分为RuntimeException和非RuntimeException
- RuntimeException表示运行异常,是代码在运行中抛出的异常,一般由逻辑错误引起,应尽可能避免,常见如NullPointException
- 非RuntimeException表示非运行异常,常在编写代码时碰到,程序无法通过异常检测,如IOException
二十五、什么时候抛出异常?什么时候捕获异常?
异常相当于一种提示,如果抛出异常,相当于告诉上层方法,“我抛了一个异常,我处理不了这个异常,交给你处理”,但是上层方法未必能解决异常,他可能也需要抛给更上层。
所以我们在写一个方法时,我们需要考虑的是本方法怎么合理处理异常,如果无法处理就上抛。一般在项目中,我个人倾向于在service层抛出异常,在controller层捕获异常
二十六、Java中有哪些类加载器
JDK自带三个类加载器:bootstrap ClassLoader、ExtClassLoader、APPClassLoader
- bootstrap ClassLoader是ExtClassLoader的父加载器,默认负责加载%JAVA_HOME%lib下的jar包和class文件
- ExtClassLoader是APPClassLoader的父类加载器,负责加载%JAVA_HOME%lib/ext文件夹下的jar包和class类
- APPClassLoader是自定义类加载器的父类,负责加载classpath下的类文件
二十七、负载均衡的策略
- 轮询:最基本的配置方法也是默认方法,每个请求会按时间顺序逐一分配到不同的后端服务器。
- 在轮询中服务器down了,会自动剔除该服务器
- 缺省配置就是轮询策略
- 此策略适合服务器配置相当,无状态且短平快的服务使用
- 权重:在轮询策略的基础上指定轮询的几率
- 权重越高分配到需求处理的请求越多
- 此策略可以和least_conn和ip_hash结合使用
- 此策略比较适合服务器差别比较大的情况
- 根据ip分配:指定负载均衡器按照客户端ip的分配方式,这个方法确保了相同的客户端请求一直发送到相同的服务器,以保证session会话。这样每个访客都固定访问一个后端服务器,可以解决session不能跨服务器的问题
- 在nignx 1.3.1之前,不能在ip_hash中使用权重
- ip_hash不能与backup同时使用
- 此策略适合有状态的服务,比如session
- 当有服务器需要剔除,必须手动dowm掉
- 最少连接:把请求发给连接数最少的服务器。轮询算法是把请求平均发给各个后端,使他们的负载大致相同;但是有些请求占用的时间很长,会导致其所在的后端负载较高。
- 此负载均衡策略适合请求处理时间长短不一造服务器过载的情况。
- 响应时间(不需要掌握):这是第三方策略,需要安装插件。这个就是按服务器的响应时间来分配请求,响应时间短的优先分配
- 根据URL分配方式(不需要掌握):这是第三方策略,需要安装插件。按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,要配合缓存命中来使用。同一个资源多次请求,可能会到达不同的服务器上,导致不必要的多次下载,缓存命中率不高,以及一些资源时间的浪费。而使用url_hash,可以使得同一个url(也就是同一个资源请求)会到达同一台服务器,一旦缓存住了资源,再此收到请求,就可以从缓存中读取。
二十八、IO流的分类
- 按数据流的方向:输入流、输出流
- 按处理数据单位:字节流、字符流
- 按功能:节点流、处理流
二十九、Java IO流有什么特点
三十、resource和autowired的区别
- 都是用来实现自动装配
- @Resource只能用在属性上,默认按照名称匹配,如果没有找到匹配名称的bean,则按类型匹配。是java标准库自带注解
- @Autowired可以用在属性、构造器、方法上,默认按类型匹配,也可以通过@Qualifier指定具体bean,是Spring框架自带的注解
- 但是这两种方法都不被官方推荐,官方不推荐属性注入的方式。他们更倾向于构造器方式注入,可以有效提高性能,但是代码需要加final关键字。但是同一在相关服务类加上lombok中的@RequidArgsConstructor注解自动生成构造器,减少代码量
三十一、Spring MVC的执行流程
什么是MVC?MVC是Model、view、controller的缩写:
- Model(模型):数据模型吗,提供要展示的数据,因此包含数据和行为,可以认为是领域模型或JavaBean组件。也就是模型提供了模型数据查询和模型数据的状态更新等功能,包括数据和业务
- View(视图):负责进行模型的展示,一般是我们见到的用户界面,给客户看到的东西
- Controller(控制器):接受用户请求,委托给模型进行处理(状态改变),处理完毕后把返回的模型数据返回给视图,由视图负责展示。也就是说控制器起调度作用
spring MVC的执行流程:
- 用户发起HTTP请求,请求直接到前端控制器DispatcherServlet
- 前端控制器接收请求后调用处理器映射器HandlerMapping,处理器映射器根据请求的URL找到具体Handler,并返回给前端控制器
- 前端控制器会调用处理器适配器HandlerAdapter去适配调用Handler
- 处理器适配器会根据Handler调用真正的处理器去处理请求,并且处理对应的业务逻辑
- 当处理器处理完业务之后,会返回一个ModelAndView对象给处理器适配器,HandlerAdapter再将该对象返回给前端控制器;这里的Model是返回的数据对象,View是逻辑上的view
- 前端控制器DispatcherServlet将返回的ModelAndView对象传给视图解析器ViewResolver进行解析,解析完成后会返回一个具体的视图View给前端控制器(ViewResolver根据逻辑的View查找具体的View)
- 前端控制器DispatcherServlet将具体的视图进行渲染呈现给用户
三十二、JVM有哪些垃圾回收算法
- 标记清除算法:
- 标记阶段:把垃圾内存标记出来
- 清除阶段:直接将垃圾内存回收
- 此算法较为简单,但是会产生大量内存碎片
- 复制算法:为解决标记清除法的内存碎片问题而诞生。复制算法将内存分为大小相等的两半,每次只使用其中一般。垃圾回收时,将当前这一块的存活对象全部拷贝到另一半,然后当前这一班内存直接清除。这种算法没有内存碎片,但是很浪费空间,而且,他的效率跟存活对象个数有关
- 标记压缩算法:为了解决赋值算法的缺陷,就提出了标记压缩算法,此算法在标记阶段跟标记清除算法是一样的,但是在完成标记之后,不是直接清除垃圾内存,而是将存活对象往一段移动,然后将边界以外的全部清除
三十三、什么是STW
就是stop-the-world,是在垃圾回收算法执行过程中,需要将JVM内存冻结的一种状态。在STW状态下,java所有线程都是停止执行的(GC线程除外)。
三十四、常用JVM启动参数有哪些
大致可以分为三类:
- 标注指令: -开头,这些是所有的HotSpot都支持的参数,可以用java -help打印出来
- 非标准指令: -X开头,这些指令通常是跟特定的HotSpot版本对应的,可以用java -X打印出来
- 不稳定参数: -XX开头,这一类参数跟特定的HotSpot版本对应,并且变化非常大
1 | # 设置堆内存 |
三十五、说说对线程安全的理解
线程安全指,我们写的某段代码,在多个线程同时执行这段代码时,不会产生混乱,依然能得到正常结果。例如i++就是不安全的
三十六、并发、并行、串行之间的区别
- 串行:一个任务执行完才能执行下一个任务
- 并行:两个任务同时执行
- 并发:两个任务整体看上去是同时执行的,但是在底层,两个任务被拆分成很多分,然后一个个执行,但是宏观层面上两个任务是同时执行的
三十七、JAVA如何避免死锁
造成死锁的原因:
- 一个资源每次只能被一个线程使用
- 一个线程在阻塞等待某个资源是,不释放已占有资源
- 一个线程已经获得的资源,在未使用完之前,不能进行强行剥夺
- 若干线程形成了收尾相连循环等待资源的关系
这是造成死锁所必须的4个条件,如果想避免死锁,只要不满足其中之一即可。在研发过程中:
- 要注意加锁顺序,保证每个线程安同样顺序进行加锁
- 要注意加锁的时限,可以针对锁设置一个超时时间
- 注意死锁检查,确保第一时间发现死锁并解决
三十八、线程中wait和sleep的区别
wait()和sleep()都是Java中用于线程等待的方法,但它们之间有一些重要的区别。
wait()方法是Object类中的方法,而sleep()方法是Thread类中的方法。
wait()方法会释放线程占用的锁,而sleep()方法不会释放锁。
wait()方法必须在同步代码块中调用,而sleep()方法可以在任何地方调用。
wait()方法会使线程进入等待状态,直到被唤醒或者等待时间到期,而sleep()方法只会使线程进入休眠状态,等待一定的时间后自动醒来。
wait()方法必须由其他线程调用notify()或notifyAll()方法来唤醒,而sleep()方法则可以由线程自己醒来。
wait()方法可以避免忙等待,而sleep()方法不能。
总的来说,wait()方法和sleep()方法都可以用于线程等待,但它们的使用场景不同。wait()方法通常用于线程之间的协作,而sleep()方法通常用于线程的时间控制。
三十九、queue中poll()和remove()方法的区别
poll() 方法和 remove() 方法都是 Queue 类中的方法,但是它们的作用不同。
poll() 方法用于从队列中获取并移除队首元素,如果队列为空,则返回 null。这个方法不会抛出异常,因此在使用时需要注意判断返回值是否为 null。
remove() 方法用于从队列中移除指定元素。如果队列中存在多个指定元素,只会移除第一个匹配的元素。如果队列中不存在指定元素,则抛出 NoSuchElementException 异常。
因此,poll() 方法用于获取并移除队首元素,而 remove() 方法用于移除指定元素。如果只需要获取队首元素而不移除它,可以使用 peek() 方法。如果需要移除队列中的所有元素,可以使用 clear() 方法。
四十三、如果我在spring中装载两个类,怎么让他不重复
如果您在Spring中装载两个类,并且这两个类的功能或属性有重叠部分,您可以通过以下方式来避免它们的重复:
使用@Primary注解:在装载类的时候,使用@Primary注解来标记一个类,表示这个类是首选的实现。这样当Spring需要注入这个类时,它就会优先选择使用被@Primary注解标记的类。
使用@Qualifier注解:在装载类的时候,使用@Qualifier注解来指定需要注入的具体实现。通过指定不同的@Qualifier值,可以让Spring注入不同的实现。
使用条件注解:在装载类的时候,使用条件注解来控制是否需要装载这个类。通过使用不同的条件注解,可以让Spring根据不同的条件来装载不同的实现。
使用Spring Boot的自动配置功能:如果您使用的是Spring Boot,可以通过自动配置来避免重复。Spring Boot会根据类路径上的依赖关系自动装载相应的类,如果有多个实现,它会根据一定的规则来选择其中一个实现。
以上是几种常见的避免重复的方法,您可以根据具体情况选择合适的方法来解决问题。
四十三、Spring如何进行依赖注入
Spring 的依赖注入(Dependency Injection,简称 DI)是通过控制反转(Inversion of Control,简称 IOC)实现的。控制反转是指将对象的创建和依赖关系的管理交给容器来完成,而不是由对象自己来完成。
在 Spring 中,依赖注入有多种实现方式,包括构造函数注入、Setter 方法注入、字段注入等。其中,最常用的是 Setter 方法注入。
以 Setter 方法注入为例,下面是 Spring 进行依赖注入的步骤:
配置 Bean:在 Spring 的配置文件中,配置需要注入的 Bean 对象及其依赖关系。可以使用 XML、Java Config 或注解等方式进行配置。
创建容器:使用 Spring 的容器来管理 Bean 对象,容器会根据配置文件中的信息创建 Bean 对象,并将它们存储在容器中。
注入依赖:当容器创建 Bean 对象时,会自动调用 Bean 对象中的 Setter 方法,并将依赖对象作为参数传入。容器会根据配置文件中的信息,自动创建依赖对象,并将它们注入到 Bean 对象中。
例如,下面是一个简单的 Bean 类:
1 | public class UserServiceImpl implements UserService { |
在 Spring 的配置文件中,可以这样配置 Bean:
1 | <bean id="userService" class="com.example.UserService"> |
这样,当容器创建 UserService 对象时,会自动调用 setUserDao() 方法,并将 userDao 对象注入到 UserService 中。而容器会根据配置文件中的信息,自动创建 UserDaoImpl 对象,并将它注入到 UserService 中。这样,就完成了依赖注入的过程。
四十四、什么是DI
DI(Dependency Injection,依赖注入)是一种设计模式,它的主要思想是通过外部注入(或传递)依赖对象,来避免在程序内部创建和管理对象的过程。这样可以更好地实现代码的解耦和灵活性,使得代码更易于维护和测试。在 DI 中,依赖关系的管理通常由一个容器来完成,容器中保存了对象之间的依赖关系,程序通过容器来获取所需的依赖对象。常见的 DI 容器有 Spring、Guice 等。
四十五、谈谈对于IOC的理解(后期要修改)
在 Spring 中,控制反转指的是将对象的控制权转移给 Spring 框架进行管理,由 Spring 帮我们创建对象,管理对象之间的依赖关系
以前创建对象的主动权和时机都是由自己把控的,现在由 IOC 容器来做,在很大程度上简化了应用的开发
IOC 容器实际上就是一个 Map 的键值对,Map 里面存放的是各种对象。IOC 容量就像一个工厂一样,当我们需要创建对象的时候,只需要通过 xml 配置文件或者注解,把对象注册到组件中,而我们完全不用考虑对象是如何被创建出来的。其中,IOC 的最常见以及最合理的实现方式叫做依赖注入(DI)
四十六、谈谈对于AOP的理解
AOP,也就是面向切面编程,可以让我们将程序的特定功能从主业务逻辑中分离出来,从而让代码更清晰,更容易维护。有利于提高代码的可读性和重复利用性,并且可以帮助我们处理复杂应用程序中的共同功能,比如日志记录、安全性、事务处理和异常处理等。
四十七、aop有哪些注解
在Java中,AOP(面向切面编程)可以使用多种注解来实现。以下是一些常见的AOP注解:
@Aspect:用于定义切面类。
@Pointcut:用于定义切入点,即在哪些方法上应用切面。
@Before:用于在目标方法执行前执行切面逻辑。
@After:用于在目标方法执行后执行切面逻辑。
@AfterReturning:用于在目标方法返回后执行切面逻辑。
@AfterThrowing:用于在目标方法抛出异常时执行切面逻辑。
@Around:用于在目标方法执行前后都执行切面逻辑。
这些注解都是Spring框架中AOP模块所提供的,可以使用它们来实现切面编程。
四十八、Spring事务传播机制
- REQUIRED(Spring默认的事务传播类型):如果当前没有事务,则自己新建一个事务,如果当前存在事务,则加入这个事务
- SUPPORTS:当前存在事务,则加入当前事务,如果没有,则以非事务方法执行
- MANDATORY:当前存在事务,就加入当前事务;不存在就抛出异常
- REQUIRED_NEW:创建一个新事务,如果存在当前事务,则挂起该事务
- NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,则挂起当前事务
- NEVER:不使用事务,如果当前事务存在,则抛出异常
- NESTED:如果当前事务存在,则嵌套在事务中执行,否则开启一个事务
四十九、Spring事务什么时候会失效
Spring事务的原理是AOP,进行了切面增强,那么失效的原因就是AOP不起作用了,常见有以下几种:
- 发生自调用,类里面使用this调用本类的方法(this通常省略),此时这个this对象不是代理对象,而是当前类对象。解决方法就是让this变为当前对象的代理类
- 方法不是public的:@Transactiional只能用于public方法上,否则事务不会失效,如果用在非Public方法上,可以开启Aspectj代理模式
- 数据库不支持事务
- 没有被Spring管理
- 异常被吃掉,事务不会回滚(或者抛出的异常没有被定义,默认为RuntimeException)
五十、Spring中Bean是线程安全的吗
Spring本身没有针对Bean做线程安全处理,所以
- 如果Bean是无状态,那么Bean是线程安全的
- 如果Bean是有状态的,那么就是不安全的
另外,Bean是不是线程安全,跟Bean的作用域没有关系,Bean的作用域只是表示Bean的生命周期,对于任何生命周期Ban都是一个对象个,这个对象是不是线程安全的,还得看这个对象本身
五十一、Spring中Bean创建的生命周期有哪些步骤
Spring中一个Bean的创建大概为以下几步:
- 推断构造方法
- 实例化
- 填充属性,也就是依赖注入
- 处理Aware回调
- 初始化前,处理@PostConstruct注解
- 初始化,处理InitialZingBean接口
- 初始化后,进行AOP
五十二、ApplicationContext和BeanFactory有什么区别
五十三、Spring中事务是如何实现的
五十四、Spring中@Transational什么时候会失效
五十五、Spring容器的启动流程
五十六、Spring用到了那些设计模式
- 工厂模式:FactoryBean和BeanFactory
- 原型模式:原型Bean
- 单例模式:单例Bean
- 构造器模式:StringBuilder就是典型的
- 适配器模式:在Spring执行流程中有
- 代理模式:AOP就是典型的代理
五十七、Spring的优点
- 低侵入式设计,降低代码之间的耦合
- 独立于各种应用服务器,基于Sping框架的应用,可以真正实现一次编写多次使用
- 容器提供单例模式支持,开发者不用再自己编写代码实现
- 提供AOP技术,可以将一些通用任务,比如安全、事务、日志等进行集中式管理,更好的复用
- ORM和DAO提供了与第三方持久层框架的良好整合,并简化了底层的数据库访问
- Spring并不强制应用完全依赖于Spring,开发者可以自由的选择使用Spring框架的全部或者部分
五十八、Spring常用注解及其底层实现
五十九、SpringBoot如何启动Tomcat
六十、Mybatis的优缺点
优点:
- 基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成影响,SQL写在XML中,解除了Sql与程序代码的耦合,便于统一管理
- 与JDBC相比,减少大量代码量,不需要手动开关连接
- 只要JDBC支持的数据库,Mybatis都支持,所以支持各种数据库
- 和Spring集成方便
- 提供映射标签,支持对象与数据库的ORM字段关系映射
缺点:
- SQL语句编写工作量大,在连表查询的时候会有些复杂
- SQl语句依赖数据库,导致数据库移植性差,不能随意更换数据库
六十一、Mybatis中${}和#{}的区别是什么
- ·#{}·是预编译处理、是占位符,${}是字符串替换,是替换符
- Mybatis在处理#{}时,会将SQL中的#{}替换成?号,调用PreparedStatement来赋值
- Mybatis处理${}时,就是把其替换成变量的值,调用Statement来赋值
- ·#{}·可以有效预防SQL注入,提高系统安全。而${}不行
六十二、索引的基本原理
索引用来快速的寻找那些具有特定值的记录,如果没有索引,一般来说,执行查询时遍历整张表
把无序的数据变成有序的查询
- 把创建了索引的列的内容进行排序
- 对排序结果生成倒排表
- 在倒排表内容上拼上数据链
- 在查询内容时,先拿到倒排表内容,再取出数据地址链,从而拿到数据
六十三、kafka的特点
解耦合:
耦合的状态表示当你实现新功能时,是直接接入当前接口,可以将相应消息发送到消息队列,这样的话,如果接口出问题,将不会影响到当前的功能
异步处理:
异步处理替代了之前的同步处理,异步处理不需让流程走完就返回结果,可以将消息发送到消息队列中,然后返回结果,剩下让其他业务处理,接口从消息队列中拉去消息处理即可
流量削峰
高流量的时候,使用消息队列作为中间件可以将流量的高峰保存在消息队列中,从而防止系统的高请求
六十四、事务的基本特性和隔离级别
ACID:
- 原子性:不可再分割
- 一致性:要么全部成功要么全部失败
- 隔离性:一个事务的修改在最终提交前,对其他事务是不可见的
- 持久性:一旦提交,所做的修改将被永远保存到数据库中
四个隔离级别:
- read uncommit 读未提交:可能读到其他事务未提交数据,也叫脏读
- read commit 读已提交:两次读取结果不一致,叫不可重复读。解决了脏读的问题,他只会读已提交的事务
- repeatable read可重复读:这是mysql的默认级别,每次读取结果都一样,可能产生幻读
- serializable 串行:一般不会使用,她会给没一行读取的数据加锁,会消耗大量资源
六十五、拦截器和过滤器的区别
拦截器是基于java的反射机制的,而过滤器是基于函数回调
拦截器不依赖于servlet容器,过滤器依赖于servlet容器
拦截器只能对action请求起作用,过滤器则可以对所有请求起作用
拦截器可以访问action上下文、值栈里的对象,而过滤器不能
在action周期里,拦截器可以多次被调用,而过滤器只能在容器初始化时被调用一次
六十六、Innodb是如何实现事务的
六十七、B树和B+树的区别,为什么Mysql使用B+树
B数的特点:
- 节点排序
- 一个节点可以存多个元素,多个元素也排序了
B+树的特点:
- 拥有B树的特点
- 叶子结点之间有指针
- 非叶子结点上的元素在叶子节点都冗余了,也就是叶子节点中存储了所有的元素,并且排好顺序
Mysql索引使用的是B+树,因为索引是用来加快查询的,而B+树通过对数据进行排序所以是可以提高查询速度的,然后通过一个节点中可以存储多个元素,从而可以使得B+树的高度不会太高,在Mysql中一个Innodb页就是一个B+树节点,一个Innodb页默认16kb,所以一般情况下,一颗两层的B+树可以存200万行左右的数据,然后通过利用B+树叶子节点存储了所有的数据,并且叶子节点之间有指针,可以很好的支持全表扫描,范围查找等SQL
六十八、Mysql锁有哪些
按锁的粒度分类:
- 行锁:锁某行数据,锁粒度最小,并发度高
- 表锁:锁整张表,锁粒度最大,并发度低
- 间隙锁:锁的是一个区间
还可以分为:
- 共享锁:也就是读锁,一个事务给某行数据加了读锁,其他事务也可以读,但是不能写
- 排它锁:也就是写锁,一个事务给某行数据加了写锁,其他事务不能读,也不能写
还可以分为:
- 乐观锁:并不会真正的去锁某行记录,而是通过一个版本号实现
- 悲观锁:上面说的表锁、行锁都是悲观锁
六十九、什么是RDB和AOF
RDB: Redis DataBase,在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个字进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储
优点:
- 整个Redis数据库将只包含一个文件dump.rdb,方便持久化
- 容灾性好,方便备份
- 性能最大化,fork子进程来完成写操作,让主进程继续处理命令,所以是IO最大化,使用单独字进程来进行持久化,主进程不会进行任何IO操作,保证了redis的高性能
- 相当于数据集大时,比AOF的启动效率更高
缺点:
- 数据安全性低,RDB是间隔一段时间进行持久化,如果持久化之间redis故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候
- 由于RDB是通过fork子进程来协助完成数据持久化,所以在当数据集较大时,可能会导致整个服务器停止服务
AOF: Append Only File,以日志的形式记录服务器的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录
优点:
- 数据安全,Redis提供了三种同步策略,即每秒同步,每修改同步和不同步。事实上,每秒同步也是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒钟之内修改的数据将丢失。而每修同步,我们可以视其为同步持久化,即每次发生的数据变化都会被立即记录到磁盘中
- 通过append模式写文件,即便中途服务器宕机也不会破坏已经存在的内容,可以通过redis-check-of工具解决数据一致性问题
- AOF机制的rewrite模式,定期对AOF文件进行重写,以达到压缩目的
缺点:
- AOF文件比RDB文件大,且恢复速度慢
- 数据集大的时候,比RDB启动效率低
- 运行效率没有RDB高
AOF文件比RDB更新频率高,优先使用AOF还原数据,AOF比RDB更安全也更大,RDB性能比AOF好,如果两个都配了,优先加载AOF
七十、Redis的过期键删除策略
Redis是Key-value数据库,我们可以设置Redis中缓存的key的过期时间。Redis的过期策略就是指当Redis中缓存的Key过期了,Redis如何处理
- 惰性过期:只有当访问一个key时,才会判断该key是否过期,过期则清除。这样可以最大化节省CPU,但是对内存非常不友好。极端情况容易出现大量过期可以没有被访问而不会被清除,占用大量内存
- 定期过期:每隔一定时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是一个折中方案。通过调整扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最好的平衡效果
七十三、Redis有哪些数据结构?分别常用于什么什么场景?
- String:可以用来做最简单的数据,可以缓存某个简单的字符串,也可以缓存某个JSON字符串。Redis分布式锁的实现就利用了这种数据结构,还包括可以实现计数器、Session共享、分布式ID
- Hash:可以用来存储一些key-value对,更适合储存对象
- list:Redis的列表通过命令的组合,既可以当做栈,也可以做队列来使用,可以用来缓存类似微型公众号、微博等消息流数据
- set:和list类似,也可以存储多个元素,但是不能重复,集合可以进行并集、交集、差集等操作,从而实现我和某人共同关注的人,朋友圈点赞等功能
- zset:有序集合,可以用来作排行榜
七十四、Redis分布式锁底层是如何实现的
- 首先利用setnx来保证:如果key不存在才能获取到锁,如果key存在则获取不到锁
- 然后利用lua脚本来保证多个Redis操作的原子性
- 同时还要考虑到锁过期,所以需要额外的一个看门狗定时任务来监听锁是否需要续约
- 同时还要考虑redis节点挂掉后的情况,所以需要采用红锁的方式来同时向N/2+1个节点申请锁,都申请到才证明取锁成功,这样就算其中某个Redis节点挂掉,锁也不会被其他客户端获取到
七十五、Redis集群策略
- 主从模式:主库可以读写,并且和从库进行数据同步,从库负责读操作。客户端直接连主库或者某个从库,但是主库或者从库宕机后,客户端需要手动改IP
- 哨兵模式:在主从机制上增加了哨兵节点。当主库节点宕机后,哨兵会发现主库宕机了,然后从从库中选一个作为主库
- Cluster模式:用的最多,支持多主多从,这种模式会按照key进行槽位的分配,可以使得不同的key分散到不同的主节点上,利用这种模式可以使整个集群支持更大的数据容量,同时主节点可以拥有自己的多个从节点,如果该主节点宕机,他会从他的从节点中选举有个新的主节点
七十六、缓存穿透、缓存雪崩、缓存击穿分别是什么
- 缓存雪崩:如果缓存中某一时刻大批热点数据过期,那么就可能导致大量请求直接访问Mysql。解决办法就是在过期时间上增加一点随机值。另外,搭建一个高可用的Redis集群也可以防止
- 缓存击穿:和缓存雪崩类似,但是缓存击穿是某一个热点数据失效,导致大量请求直接访问Mysql数据库。解决方法是,考虑这个热点是否不设置过期时间
- 缓存穿透:假如某一时刻访问Redis的大量key都不在redis中(例如黑客故意伪造的key),那么也会给数据造成压力,这就是缓存穿透。解决方案是接口层增加校验,比如用户鉴权,ID做基础校验,比如id<=0直接拦截。如果数据库里也拿不到该数据,可以将该key-value写成key-null,设置有效时间,可以防止攻击者使用同一个id反复攻击
七十七、Redis如何和Mysql保持数据一致
- 先更新Mysql,再更新Redis,如果更新Redis失败,可能仍然不一致
- 先删除Redis缓存数据,再更新Mysql,再查询的时候,将数据添加到缓存中,但是在高并发下,性能低下,而且还是会出现数据不一致
- 延时双删:先删除Redis缓存数据,再更新Mysql,延迟几百毫秒再删除Redis缓存数据,这样就算在更新Mysql时,其他线程读了Mysql,把老数据读到Mysql中也会被删掉,从而保证数据一致
七十八、Redis的优缺点
优点:
- 读写性能优异
- 支持数据持久化
- 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离
缺点:
- 不具备自动容错和恢复功能,主机从机宕机都会导致前端部分读写请求失败。
- 主机宕机,可能导致部分数据没有同步到从机,切换IP后,还容易引起数据不一致
- Redis艰难支持在线扩容,在集群容量达到上限时,在线扩容会非常复杂
七十九、CAP理论,BASE理论
CAP:一致性,可用性和分区容错性,三者只能同时满足两个
八十五、Spring Cloud有哪些常用组件,作用是什么
- Eureka:注册中心
- Nacos:注册中心、配置中心(阿里巴巴的)
- Consul:注册中心、配置中心
- Spring cloud config: 配置中心
- Feign/OpenFeign:RPC调用(远程服务调用)
- zuul:服务网关
- Spring cloud gateway:服务网关
- Ribbon:负载均衡
- Spring cloud sleuth:链路追踪
- Zipkin:链路追踪
- seata:分布式事务
- Dubbo:RPC调用
- Sentinel:服务熔断
- Hystrix:服务熔断
八十六、RabbitMQ应用场景
- 应用解耦:当要调用远程系统时,当存在订单系统和库存系统时,订单系统下单,库存系统需要收到订单后库存减一,这时候如果系统宕机,会造成订单丢失。但是把订单消息发入mq,库存系统再去mq消费,就能解决这个问题
- 异步消费:传统的模式:用户下单===>邮件发送===>短信提醒,三个步骤全部完成侧能返回用户消费成功,因为后面两个步骤完全没有当前就去完成,可以用户下单成功够,直接发送给mq,返回给用户消费成功,之后邮件发送和短信提醒,可以在其他时间段来消费法给用户
- 流量削峰:并发量非常高的时候,这时候数据库不能承受那么大的数据冲击,而专门为高并发设计的mq可以承受海量的请求,发送给mq,存储成功后再消费
八十七、什么是服务雪崩?什么是服务限流?
- 当服务A调用服务B,服务B调用服务C,此时大量的请求去请求服务A,假如服务A能抗住,但是服务C扛不住,导致大量的请求堆积,从而导致服务B请求堆积,从而服务A不可用,这就是服务雪崩。解决方法是服务降级和服务熔断
- 服务限流是指在高并发请求下,为保护系统,可以对访问服务的请求进行数量上的限制,从而防止系统不被大量请求压垮,在秒杀中,限流非常重要
八十八、什么是服务熔断?什么是服务降级?区别是什么?
- 服务熔断是指,当服务A调用的某个服务B不可用时,上游服务A为了保证自己不受影响,从而不再调用服务B,直接返回一个结果,减轻服务A和服务B的压力,直到服务B恢复
- 服务降级指,当发现系统压力过载时,可以通过关闭某个服务,或者限流,某个服务来减轻系统压力
相同点:
- 都是为了防止系统崩溃
- 都是让用户体验到某些功能暂时不可用
不同点:熔断是下游服务触发的,降级是降低系统负载
八十九、kafka和RabbitMQ的区别
- 架构:RabbitMQ是一个消息代理,而Kafka是一个分布式流平台
- 实现:RabbitMQ基于队列模型,kafka基于发布/订阅模型
- 吞吐量:Kafka吞吐量远大于RabbitMQ,因为它具有分布式架构
- 持久性:Kafka消息储存更长
- 可靠性:RabbitMQ提供更高的可靠性,因为他支持事务和可靠性投递
- 用途:RabbitMQ用于异步通信和任务队列,Kafka适于大规模数据处理,和实时流数据
九十、项目中怎么保证微服务敏捷开发
九十一、如何进行消息队列选型
kafka:
- 优点:吞吐量大,性能非常好,集群高可用
- 缺点:会丢失数据,功能比较单一
- 使用场景:日志分析、大数据采集
RabbitMQ:
- 优点:消息可靠性高,功能全面
- 缺点:吞吐量低,消息积累会严重影响性能,erlang语言不好定制
- 使用场景:小规模场景
RocketMQ:
- 优点:高吞吐,高性能、高可用,功能全面
- 缺点:开源版本不如云上商业版,官方文档和周边生态不够成熟,客户端只支持java
- 使用场景:几乎是全场景
九十二、消息队列如何保证消息可靠性
消息可靠传输代表两层意思,既不能多,也不能少。
- 为了保证消息不多发,也就是消息不重复,也就是生产者不能重复生产消息,或者消费者不重复消费消息
- 首先要保证消息不多发,这个出现的比较少,也不好控制,因为出现了多发,很大原因是生产者自己的原因,如果要避免出现问题,就需要在消费端做控制
- 要避免不重复消费,最保险的机制就是消费者实现幂等性,保证就算重复消费,也不会有问题,通过幂等性,也能解决生产者重复发送消息的问题
- 消息不能少,意思就是消息不能丢,生产者发送的消息,消费者一定要消费到,对于这个问题,要考虑两个方面
- 生产者发送消息时,要确认broker确实收到并持久化了这条消息,比如RabbitMQ中的confirm机制,kafka的ack机制都可以保证生产者能正确的将消息发送给broker
- broker要等待消费者真正确认消费到了消息时才能删掉信息,这里通常就是消费端的ack机制,消费者接收到一条消息后,如果确认没问题了,就可以给broker发送一个ack,broker接收到后才会删除消息
九十三、RabbitMQ的五种模式
简单队列:
一个生产者对应一个消费者
work模式:
一个生产者对应多个消费者,但是一条消息只有一个消费者可以获取消息
发布/订阅模式:
一个生产者将消息首先发送到交换器,交换器绑定到多个队列,然后被监听该队列的消费者所接受并消费
路由模式:
生产者将消息发送到direct交换器,在绑定队列和交换器的时候有一个路由key,生产者发送的消息会指定一个路由Key,那么消息只会发送到相应Key相同的队列,接着监听该队列的消费者消费信息
也就是让消费者有选择性的接收消息
路由模式,是以路由规则为导向,引导消息存入符合规则的队列中。再由队列的消费者进行消费的。主题模式:
上面的路由模式是根据路由key进行完整的匹配(完全相等才发送消息),这里的通配符模式通俗的来讲就是模糊匹配。
符号“#”表示匹配一个或多个词,符号“*”表示匹配一个词。
与路由模式相似,但是,主题模式是一种模糊的匹配方式。
九十四、死信队列是什么?延时队列是什么?
- 死信队列也是一个消息队列,他是用来存那些没有成功消费的信息的,通常可以用作消息重试
- 延时队列就是用来存放需要在指定时间被处理的元素对队列,通常可以用来处理一些具有过期性操作的业务,比如十分钟内未支付取消订单
九十六、servlet的生命周期?
servlet有良好的生命周期定义,包括加载和实例化、初始化、处理请求以及服务结束四个阶段。WEB容器加载Servlet,生命周期开始,首先服务器调用Servlet的构造方法执行实例化操作,然后容器调用Servlet的init方法执行初始化操作,请求到达时运行Servlet的service方法,service方法自动调用与请求类型对应的doGet或者doPost方法来处理请求,当服务器决定将Servlet实例销毁前调用其destroy方法(释放servlet占用的资源,例如流、数据库连接等)
九十七、跨域请求是什么?有什么问题?怎么解决?
跨域请求是指浏览器发起网络请求时,会检查该请求所对应的协议、域名、端口和当前网页是否一致,如果不一致则导致浏览器进行限制,比如在www.baidu.com的某个网页中,如果使用ajax去访问www.jd.com是不行的,但是如果是img、iframe、script等标签的src属性去访问则是可以的。之所以做这层限制,是为了用户信息安全。
解决方法:
- response添加header
- jsonp的方式,该技术底层是基于script标签实现
- 后台自己控制
- 网关
九十八、乐观锁和悲观锁
- 悲观锁:每次去拿数据的时候都认为会进行修改,所以每次再拿数据的时候都会上锁。但是这样别人去拿数据就会被挡住,直到悲观锁释放。悲观锁中的共享资源每次只能给一个线程使用,其他线程阻塞,用完之后再把资源转让给其他线程。效率和并行性较低,还会增加死锁的风险。数据库中的行锁、表锁、读锁(共享锁)、写锁(排它锁),以及syncronized实现的锁均为悲观锁
- 乐观锁: 每次去拿数据都认为不会修改,所以不会上锁,但是如果想要更新数据,则会在更新前检查在读取至更新这段时间别人有没修改过这个数据。如果修改过,就重新读取,然后尝试更新,循环上述步骤,直至更新成功。适用于多读的应用类型,可以提高吞吐量
SQL面试题
一、什么是分库分表?以及他的应用场景
分库分表是一种数据库技术,它可以将数据库中的数据按照一定的规则进行分割,将数据存储到不同的数据库中,以提高数据库的性能和可用性。
应用场景:
- 当数据量较大时,可以将数据分散到多个数据库中,以提高查询性能;
- 当数据库访问量较大时,可以将数据分散到多个数据库中,以提高访问性能;
- 当数据库容量较大时,可以将数据分散到多个数据库中,以提高存储性能。
二、left join和right join的区别
LEFT JOIN和RIGHT JOIN的区别在于连接结果中左表和右表的位置不同。
LEFT JOIN会返回左表中的所有记录,即使右表中没有对应的匹配。如果右表中如果没有对应的匹配,右表的字段将被赋值为NULL。RIGHT JOIN则与之相反。
另外,在大部分数据库系统中,支持使用LEFT JOIN和RIGHT JOIN两种语句,并且两种操作是等价的,所以也可以使用 JOIN 替代LEFT JOIN 或 RIGHT JOIN
三、数据库优化
架构优化:
分布式缓存:性能不够,缓存来凑。我们可以在应用于数据库之间加一个缓存服务,比如Redis。当收到查询请求后,我们先查询缓存,判断缓存中是否有数据,有数据则直接返回给应用,如若没有再查数据库。大大减少了对数据库的访问次数,自然提高了性能。
但是需要注意引入Redis之后的缓存穿透、缓存击穿和缓存雪崩
读写分离:一主多从,读写分离,主动同步。一般来说,当你的应用是读多写少,数据库压力过大,采用读写分离,通过增加数据库量可以线性提升系统读性能**
主库,提供数据库写服务;从库,提供数据库读能力;主从之间,通过binlog同步数据
实施读写分离时,为了保证高可用,需要实现故障的自动转移,主从架构会有潜在主从不一致的问题
分库分表(水平切分):当你的应用业务数据量很大,单库容量成为性能瓶颈后,采用水平切分,可以降低数据库单库容量,提升数据库写性能。
当准备实施水平切分时,需要结合实际业务选取合理的分片键
硬件优化: 不管是读操作还是写操作,都是要访问磁盘,所以磁盘的性能决定了数据库的性能。用好的就行
DB优化: SQL执行慢有时候不一定完全是SQL问题,手动安装一台数据库而不做任何参数调整,再怎么优化SQL都无法让其性能最大化。
基本遵循以下三点:日志不能小、缓存足够大、连接要够用
SQL优化:
合理使用索引:
索引少了查询慢;索引多了占用空间大,执行增删改语句的时候需要动态维护索引,影响性能
使用UNION ALL替代UNION:
UNION ALL的执行效率比UNION高,因为UNION执行时需要排重;
避免使用select :
执行SQL时优化器需要将 * 转成具体的列;每次查询都要回表,不能走覆盖索引。
JOIN字段建立索引
避免复杂的SQL语句:
提升可阅读性;避免慢查询的概率;可以转换成多个短查询,用业务端处理
避免where 1= 1写法
避免order by rand()类似写法:
RAND()导致数据列被多次扫描
四、怎么进行去重查询?
distinct:效率较低。不适合用来展示去重后具体的值,一般用于计算
1
2
3
4
5
6
7-- 列出 task_id 的所有唯一值(去重后的记录)
-- select distinct task_id
-- from Task;
-- 任务总数
select count(distinct task_id) task_num
from Task;
group by :
1
2
3
4
5
6
7
8
9
10
11-- 列出 task_id 的所有唯一值(去重后的记录,null也是值)
-- select task_id
-- from Task
-- group by task_id;
-- 任务总数
select count(task_id) task_num
from (select task_id
from Task
group by task_id) tmp;
row_number:窗口函数,用的比较少,因为必须先支持窗口函数才行
1
2
3
4
5
6-- 在支持窗口函数的 sql 中使用
select count(case when rn=1 then task_id else null end) task_num
from (select task_id
, row_number() over (partition by task_id order by start_time) rn
from Task) tmp;
知识点
kafka相关知识点
kafka是一种消息队列,主要用来处理大量数据状态下的消息队列,一般用来做日志处理
优点:
解耦合:
耦合的状态表示当你实现新功能时,是直接接入当前接口,可以将相应消息发送到消息队列,这样的话,如果接口出问题,将不会影响到当前的功能
异步处理:
异步处理替代了之前的同步处理,异步处理不需让流程走完就返回结果,可以将消息发送到消息队列中,然后返回结果,剩下让其他业务处理,接口从消息队列中拉去消息处理即可
流量削峰
高流量的时候,使用消息队列作为中间件可以将流量的高峰保存在消息队列中,从而防止系统的高请求
消费模式:
- 一对一:消费者发布消息到Queue队列中,通知消费者从队列中拉取消息进行消费。消息消费后就删除,Queue支持多个消费者,但是一条消息只能被一个消费者消费
- 一对多:也叫发布/订阅模式,即利用Topic存储消息,消息生产者将消息发布到Topic中,同时有多个消费者订阅此Topic,消费者可以从中消费消息,注意,发布到Topic中的消息将会被多个消费者消费。消费者消费数据之后,数据不会被消除,kafka会默认保留一段时间,然后再删除
WebSocket是什么?应用场景
WebSocket是一种网络传输协议,位于OSI模型的应用层,可在单个TCP连接上进行全双工通信,能更好的节省服务器资源和带宽
客户端和服务器只需完成一次握手,两者就可以创建持久性的连接,并进行双向数据传输
优点:
- 较少的开销:数据包头部协议较小,不同于HTTP每次请求都需要携带完整的头部
- 更强的实时性:相对于HTTP,延时更少
- 保持创连接状态:创建通信后,可省略状态信息,不同于HTTP每次请求需要携带身份验证
- 支持扩展:用户可以扩展webSocket协议,实现部分自定义的子协议
缺点:
- 各浏览器的支持度不同
- 服务器维持长连接的成本高
- webSocket是长连接,受网络影响很大,需要处理好重连
应用场景:
- 弹幕
- 媒体聊天
- 协同编辑
- 基于位置的应用
- 体育实况
- 股票基金报价的实时更新
VUE面试题
一、nextTick
nextTick 是 Node.js 和浏览器中常用的一个异步方法,它可以将一个回调函数推迟到下一个事件循环中执行。
具体来说,nextTick 方法会将回调函数放到当前事件循环的末尾,等待当前事件循环完成后立即执行。这个过程是异步的,不会阻塞当前事件循环的执行。
在 Node.js 中,process.nextTick 方法是实现 nextTick 的一种方式。在浏览器中,setTimeout 方法也可以实现类似的功能,将延迟时间设置为 0 即可。
使用 nextTick 可以避免一些问题,比如在同步代码中修改了某个变量,但是在下一个事件循环中才需要使用这个变量的值,这时可以使用 nextTick 来确保变量已经被修改。
需要注意的是,由于 nextTick 是将回调函数放到当前事件循环的末尾执行,如果回调函数执行时间过长,会导致事件循环被阻塞,影响程序的性能。因此,在使用 nextTick 时,应该避免执行过长的回调函数。
二、vue中组件传值
在 Vue 中,组件之间传值可以通过props 和 $emit 实现。
props 是父组件向子组件传递数据的一种方式。在父组件中使用子组件时,可以通过 v-bind 指令将数据绑定到子组件的 props 上。例如:
1 | <template> |
在子组件中,可以通过 props 属性来接收父组件传递的数据。例如:
1 | <template> |
$emit 是子组件向父组件传递数据的一种方式。在子组件中,可以通过 $emit 方法触发一个自定义事件,将数据传递给父组件。例如:
1 | <template> |
在父组件中,可以通过 v-on 指令监听子组件触发的自定义事件,并在对应的方法中接收子组件传递的数据。例如:
1 | <template> |
以上是 Vue 中组件传值的基本用法,希望对你有所帮助。
三、v-if和v-show的区别
v-if 和 v-show 都是 Vue 中用于控制元素显示和隐藏的指令,但它们的实现方式不同。
v-if 指令是将元素从 DOM 树中移除或添加,当条件为 false 时,元素会被移除,当条件为 true 时,元素会被添加到 DOM 树中。这意味着当条件为 false 时,元素的所有事件监听器和子组件都会被销毁,当条件为 true 时,元素的事件监听器和子组件会被重新创建。
v-show 指令是通过 CSS 样式控制元素的显示和隐藏,当条件为 false 时,元素的 display 样式设置为 none,当条件为 true 时,元素的 display 样式设置为原来的值。这意味着元素始终存在于 DOM 树中,不会被销毁和重新创建,但是会影响页面的渲染性能。
因此,如果需要频繁切换元素的显示和隐藏,可以使用 v-show,如果元素的显示和隐藏较少变化,可以使用 v-if。
