面试题
- Java基础
- 1、HashMap是Java中常用的数据结构,它基于哈希表实现。具体的底层原理如下:
- 2、ConcurrentHashMap 的底层原理?
- 3、重写equals为什么也要重写hashcode方法
- 4、什么是序列化,什么是反序列化
- 5、 ==与equals
- 6、数据类型
- 7、说说强、软、弱、虚引用?
- 8、深拷贝与浅拷贝
- 9、volatile的使用及其原理
- 10、ThreadLocal 是什么?它的实现原理呢?
- 11、ThreadLocal 内存泄露问题
- 12、什么是反射
- 13、什么是Semaphore?
- 14、线程的5个状态
- 15、JDK、JRE、JVM的联系与区别是什么?
- 16、注解是什么原理?
- 17、说说List、Set、Map三者的区别?
- 18、装箱拆箱
- 19、Integer 和 int 的区别?Java 为什么要设计封装类?
- 20、String、StringBuffer、StringBuilder 的区别是什么?
- 21、string为什么要设计成不可变的?
- 22、关于final关键字的一些总结
- 23、ArrayList 和 LinkedList 的区别
- 24、Java中的集合类
- 25、集合的排序方式的实现方案
- 25、抽象类和接口的区别
- 26、super与this的区别
- 锁
- mybatis
- JVM
- redis
- MySQL
- 消息队列
- spring
- 网络编程
Java基础
1、HashMap是Java中常用的数据结构,它基于哈希表实现。具体的底层原理如下:
-
数据结构:HashMap底层使用数组和链表(或红黑树)来存储键值对。数组被划分为多个桶(bucket),每个桶可以存储一个或多个键值对。
-
哈希算法:当向HashMap中添加键值对时,系统会使用键的hashCode()方法计算出一个哈希码,然后通过哈希码找到对应的桶索引。这样可以保证键值对在数组中均匀分布,减少冲突。
-
解决冲突:由于不同的键可能产生相同的哈希码,可能会导致冲突。为了解决冲突,HashMap使用链表(JDK8以前版本)或红黑树(JDK8及以后版本)来管理具有相同哈希码的键值对。
-
添加元素:当需要向HashMap中添加键值对时,首先计算键的哈希码,并根据哈希码找到对应的桶。如果桶为空,则直接将键值对插入到桶中;如果桶非空,则通过比较键的equals()方法确定是否已存在该键,若存在则更新值,若不存在则将新键值对插入桶中(链表或红黑树的末尾)。如果链表过长,JDK8之后会将链表转化为红黑树。
-
获取元素:通过键获取元素时,首先计算键的哈希码,并找到对应的桶。如果桶为空,则表示不存在该键,返回null;如果桶非空,则遍历链表或红黑树,根据键的equals()方法找到对应的值并返回。
-
扩容:当HashMap中存储的键值对数量达到负载因子(0.75)乘以当前容量时,会进行扩容操作。扩容会创建一个更大的桶数组,并将原有的键值对重新分布到新的桶中,以降低冲突的概率。
-
性能优化:为了提高性能,可以通过调整负载因子和初始容量来平衡内存占用和查询效率。较小的负载因子会增加冲突的可能性,但在空间利用上更高效,而较大的负载因子则提高了查询的效率。
当数组长度大于 64 且链表长度超过阈值时,HashMap 将链表升级为红黑树
hashmap与treemap的区别
哈希映射(HashMap)和树映射(TreeMap)是两种常见的映射数据结构,它们在实现上有一些区别:
-
实现方式:
- HashMap 基于哈希表实现,使用哈希函数将键映射到存储桶(bucket)的位置,然后在该位置存储对应的值。
- TreeMap 基于红黑树实现,它是一种自平衡的二叉查找树,能够保持元素的有序性。
-
排序:
- HashMap 不保证元素的顺序,即插入顺序不会影响遍历顺序,它是无序的。
- TreeMap 会根据键的自然顺序或者通过 Comparator 进行排序,因此遍历时会按照键的顺序输出。
-
性能:
- 在大多数情况下,HashMap 的插入、查找和删除操作具有常数时间复杂度,即 O(1) 的时间复杂度。
- TreeMap 的插入、查找和删除操作的时间复杂度为 O(log n),其中 n 为元素个数,因为它要维护树的平衡性。
-
空间占用:
- HashMap 的空间利用率可能会高于 TreeMap,因为它使用了哈希表来存储数据。
- TreeMap 存储每个节点的额外信息(如颜色标记等),可能占用更多的内存。
总的来说,如果需要快速的插入、查找和删除操作,并且不关心元素的顺序,可以选择使用 HashMap。如果需要元素有序,并且对性能要求不是非常苛刻,可以选择使用 TreeMap。
hashmap与hashtable区别
两者最大的区别是tabel是线程同步的,而map不是。
从线程安全来看,HashMap不是线程安全的,在多线程情况下可能遇到并发问题,hashTable是线程安全的,但是通过内部方法添加syn来解决的,效率低下,而且在官方注释中hashTable是保留类,不建议使用。如需要线程安全可使用ConcurrentHashMap。
在结构上,hashTable一直是数组和链表结构,而hashMap在1.8时改为了数组和链表及红黑树。
在遍历上,HashTable使用Enumeration,HashMap使用Iterator。
在储存支持上,hashMap可以允许一个空健和多个空值,会把 null 转化为 0 进行存储,而hashTable由于需要保证线程安全的特性,不允许任何空。否则会造成歧义,即:这个key到底是存在还是不存在。
在容量上,hashTable默认为11,扩容为2n+1,而hashMap默认为16,扩容为2倍。如添加指定长度,table会直接使用,而map总会扩充为2的n次幂。
在散列算法上,上HashTable会使用key对hashCode对长度取模,hashMap会做一些扰动来达到更好的分布。
2、ConcurrentHashMap 的底层原理?
- JDK1.7 中的 ConcurrentHashMap 是由 Segment(分段锁) 数组结构和HashEntry 数组结构组成,把哈希桶数组切分成小数组(Segment ),每个小数组有 n 个 HashEntry ,将数据分段存储,每一段数据配一把锁,多线程访问容器里不同数据段的数据
Segment 继承了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。Segment 默认为 16,也就是并发度为 16。 - JDK1.8 中的ConcurrentHashMap 选择了与 HashMap 相同的Node数组+链表+红黑树结构
在锁的实现上,抛弃了原有的 Segment 分段锁,采用CAS + synchronized实现更加细粒度的锁。将锁的级别控制在了更细粒度的哈希桶数组元素级别,也就是说只需要锁住这个链表头节点(红黑树的根节点),就不会影响其他的哈希桶数组元素的读写,大大提高了并发度。
在JDK1.8 中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本
ConcurrentHashMap不支持key与value为null的原因
我们先来说value 为什么不能为 null。因为 ConcurrentHashMap 是用于多线程的,如果ConcurrentHashMap.get(key)得到了 null,这就无法判断,是映射的value是 null,还是没有找到对应的key而为 null,就有了二义性。
那 HashMap 允许插入 null(空) 值,难道它就不担心出现歧义吗?
这是因为HashMap 的设计是给单线程使用的,所以如果取到 null空值,我们可以通过HashMap 的 containsKey(key)方 法来区分这个 null空值到底是插入值是 null,还是本就没有才返回的 null(空) 值
3、重写equals为什么也要重写hashcode方法
重写equals()
方法时,是为了定义两个对象在逻辑上是否相等。根据Java规范,如果两个对象在equals()
方法比较下相等,那么它们的hashCode()
方法返回的哈希值应该相等。
hashCode()
方法用于获取对象的哈希码,它是一个整数值,通常用于提高散列表(如HashMap、HashSet等)的性能。散列表通过哈希码将对象映射到对应的桶中,然后根据哈希码和equals()
方法来确定对象的相等性。
如果不重写hashCode()
方法而仅重写了equals()
方法,就会导致违反上述规范,即两个相等的对象返回的哈希码可能不同。这会导致在使用散列表的数据结构时,无法正确地存储和查找对象。例如,将一个对象添加到HashSet中后,再尝试使用相同内容的另一个对象进行查找时,由于哈希码不同,无法正确地找到对象。
因此,为了保持一致性,当重写equals()
方法时,必须同时重写hashCode()
方法。重写hashCode()
方法需要满足以下原则:
- 如果两个对象调用
equals()
方法返回true,则它们的hashCode()
方法应该返回相同的哈希码。 - 如果两个对象调用
equals()
方法返回false,它们的hashCode()
方法不要求返回不同的哈希码,但是为了提高散列表的效率,尽量使得哈希码分布均匀,以减少哈希冲突。
在重写hashCode()
方法时,可以选择一些合适的属性进行哈希计算,通常会结合使用对象内部的字段,并使用一些算法(如乘法、位运算等)来计算出最终的哈希码。
综上所述,当重写equals()
方法时,为了遵守Java规范和保持散列表的正确性,也需要同时重写hashCode()
方法,以确保相等的对象返回相等的哈希码。
4、什么是序列化,什么是反序列化
Java序列化是指把Java对象转换为字节序列的过程,而Java反序列化是指把字节序列恢复为Java对象的过程
Java对象是保存在JVM的堆内存中的,如果JVM堆不存在了,那么对象也就跟着消失了。对堆内存里的数据进行持久化或网络传输,这个时候需要用到序列化和反序列化。
缓存机制:序列化和反序列化可以用于将对象存储在缓存中,以提高系统性能和响应速度。
需要注意的是,序列化和反序列化过程中应注意以下事项:
- 类的兼容性:在进行反序列化时,确保原始对象和目标对象的类定义相同或兼容,以免导致数据丢失或错误。
- 安全性:对于从外部接收的序列化数据,应进行安全检查和验证,防止恶意攻击或注入。
- 性能和大小:序列化和反序列化过程可能涉及大量的数据传输和处理,应注意性能和数据大小的影响。
对象序列化是一种将Java对象转换为字节流的过程,通常用于网络传输、对象持久化(保存到文件或数据库中)、跨进程通信等场景。在以下情况下,通常需要对对象进行序列化:
-
网络传输:当两个进程/应用程序之间需要进行网络传输时,可能需要将对象序列化为字节流,以便在网络上传输。
-
对象持久化:当需要将一个对象保存到文件或数据库中,并在需要时重新读取出来时,可以将对象进行序列化,然后写入到文件或数据库中。当需要重新读取对象时,再从文件或数据库中读取对象的序列化数据,反序列化成原对象。
-
跨进程通信:在使用分布式系统开发时,可能需要将一个Java对象在不同的进程或服务器之间进行传输,这时就需要将对象进行序列化。
5、 ==与equals
== :它的作用是判断两个对象的地址是不是相等。即判断两个对象是不是同一个对象(基本数据类型 == 比较的是值,引用数据类型== 比较的是内存地址)。equals):它的作用也是判断两个对象是否相等。但它一般有两种使用情况情况1:类没有覆盖equals()方法。则通过equals()比较该类的两个对象时,等价于通过“=="比较这两个对象。
·情况2:类覆盖了equals()方法。一般,我们都覆盖equals() 方法来比较两个对象的内容是否相等,若它们的内容相等,则返回true(即,认为这两个对象相等)。
6、数据类型
7、说说强、软、弱、虚引用?
Java 根据其生命周期的长短将引用类型又分为强引用、软引用、弱引用、虚引用。
- 强引用:就是我们平时new 一个对象的引用。当VM 的内存空间不足时,宁愿抛出0utOMemoryError使得程序异常终止,也不愿意回收具有强引用的存活着的对象
- 软引用:生命周期比强引用短,当JVM 认为内存空间不足时,会试图回收软引用指向的对象,也就是说在JVM 抛出 OutofMemoryError 之前,会去清理软引用对象,适合用在内存敏感的场景。
- 弱引用:比软引用还短,在GC 的时候,不管内存空间足不足都会回收这个对象,ThreadLocal中的 key 就用到了弱引用,适合用在内存敏感的场景。
- 虚引用:如果一个对象仅持有虚引用,它就和没有任何引用一样,在任何时候都可能被垃圾回收。唯一作用就是配合引用队列来监控引用的对象是否被加入到引用队列中,也就是可以准确的让我们知晓对象何时被回收。
8、深拷贝与浅拷贝
深拷贝和浅拷贝是用来描述对象或者对象数组这种引用数据类型的复制场景的。
- 浅拷贝,就是只复制某个对象的指针(引用),而不复制对象本身。这种复制方式意味着两个对象指向同一块内存地址。
- 深拷贝,会完全创建一个一模一样的新对象,新对象和老对象不共享内存,也就意味着对新对象的修改不会影响老对象的值。
9、volatile的使用及其原理
- 可以保证在多线程环境下共享变量的可见性
当一个线程修改了被volatile修饰的变量的值时,其他线程可以立即看到最新的值。这是因为在每次读取volatile变量时,JVM会强制从主内存中读取该变量的值,而不是使用线程工作内存中的副本。 - 通过增加内存屏障防止多个指令之间的重排序
相对于synchronized来说:1.轻量级:volatile是一种轻量级的同步机制,volatile 只保证单次读/写操作的原子性,对于多步操作,volatile 不能保证原子性
编译器和处理器在执行指令时为了提高性能,可能会对指令进行重排序,但这种重排序对单线程程序并不会产生影响。但在多线程环境下,指令重排序可能导致结果的不一致性。使用 volatile 关键字修饰的变量会禁止指令重排序,保证指令的执行顺序与代码中的顺序一致。
单例模式设计为什么需要 volatile 修饰实例对象
我所理解的 DCL 问题,是在基于双重检查锁设计下的单例模式中,存在不完整对象的问题。而这个不完整对象的本质,是因为指令重排序导致的
当我们使用instance=new DCLExample()构建一个实例对象的时候,因为 new 这个操作并不是原子的。所以这段代码最终会被编译成 3 条指令。
为对象分配内存空间
初始化对象
把实例对象赋值给 instance 引由于这是三个指令并不是原子的
按照重排序规则,在不影响单线程执行结果的情况下,两个不存在依赖关系的指令允许重排序,也就是不一定会按照代码编写顺序来执行。这样一来,就会导致其他线程可能拿到一个不完整的对象,也就是这个 instance已经分配了引用实例,但是这个实例的初始化指令还没执行
解决办法就是可以在 instance 这个变量上增加一个 volatile 关键字修饰,volatile 底层使用了内存屏障机制来避免指令重排序。
10、ThreadLocal 是什么?它的实现原理呢?
ThreadLocal 是一种线程隔离机制,它提供了多线程环境下对于共享变量访问的安全性
在多线程访问共享变量的场景中,一般的解决办法是对共享变量加锁
但是加锁会带来性能的下降,所以 ThreadLocal 用了一种空间换时间的设计思想,也就是说在每个线程里面,都有一个容器来存储共享变量的副本,然后每个线程只对自己的变量副本来做更新操作,这样既解决了线程安全问题,又避免了多线程竞争加锁的开销。
具体实现原理是,在 Thread 类里面有一个成员变量ThreadLocalMap,它专门来存储当前线程的共享变量副本,后续这个线程对于共享变量的操作,都是从这个 ThreadLocalMap 里面进行变更,不会影响全局共享变量的值
在使用 ThreadLocal 时,需要注意以下几点:
-
内存泄漏:使用 ThreadLocal 时,如果没有手动清理 ThreadLocal 中的数据,可能会导致线程池中的线程长期持有对象的引用而无法被垃圾回收,从而造成内存泄漏。因此,必须及时清理 ThreadLocal 中的数据,可以通过调用
remove()
方法或将 ThreadLocal 对象设置为 null 来进行清理。 -
共享变量问题:ThreadLocal 可以让每个线程拥有自己独立的变量副本,避免了共享变量的竞争和同步,但也可能造成数据不一致的问题。不同线程之间使用 ThreadLocal 存储的数据相互独立,不能直接共享数据。如果需要在多个线程之间共享数据,应该使用其他机制来实现线程间的数据传递和同步。
-
隐式传参:ThreadLocal 可以作为一种隐式传参的方式,避免了显式参数传递的复杂性。但过度依赖 ThreadLocal 可能会导致代码可读性下降,增加代码理解和维护的难度。因此,在使用 ThreadLocal 时需要权衡简洁性和代码可读性,合理选择使用场景。
-
线程池使用时需谨慎:在使用线程池时,由于线程池中的线程是可复用的,可能会出现线程切换或线程重用导致 ThreadLocal 数据没有被清理的情况。为了避免这种问题,应该在任务执行结束后及时清理 ThreadLocal 中的数据,以确保数据的正确性。
总结来说,使用 ThreadLocal 时需要注意内存泄漏问题、共享变量问题、代码可读性和线程池场景下的问题。合理使用 ThreadLocal 可以提高代码的简洁性和性能,但需谨慎使用并正确处理其中的注意事项,避免出现不可预料的问题。
11、ThreadLocal 内存泄露问题
弱引用:只要垃圾回收机制一运行,不管JVM的内存空间是否充足,都会回收该对象占用的内存。
因此,如果ThreadLocal不再被引用,当JVM GC时就会被垃圾回收器回收 (ThreadLocalMap的Key),但是因ThreadLocalMap生命周期和Thread是一样的,它这时候如果不被回收,就会出现这种情况: ThreadLocalMap的key为null,value还在,这就会造成了内存泄漏问题。为了解决threadLocal潜在的内存泄漏的问题,ThreadLocal在set、get、remove方法中都有相应的处理。若发现ThreadLocalMap某entry key为null,则将entry value和其entry本身都设置为null,下一次GC的时候就会被回收掉。
12、什么是反射
反射是在程序运行时动态的获取类的信息、成员变量、成员方法、属性等。
-
Java 反射的优点:
- 增加程序的灵活性,可以在运行的过程中动态对类进行修改和操作
- 提高代码的复用率,比如动态代理,就是用到了反射来实现
- 可以在运行时轻松获取任意一个类的方法、属性,并且还能通过反射进行动态调用
-
Java 反射的缺点:
- 反射会涉及到动态类型的解析,所以 JVM 无法对这些代码进行优化,导致性能要比非反射调用更低。
- 使用反射以后,代码的可读性会下降
- 反射可以绕过一些限制访问的属性或者方法,可能会导致破坏了代码本身的抽象性
13、什么是Semaphore?
Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它基于AQS共享锁实现。通过协调各个线程以保证合理的使用公共资源
每次使用资源前,先申请一个信号量,如果资源数不够,就会阻塞等待,
每次释放资源后,就释放一个信号量。
14、线程的5个状态
线程通常都有五种状态,创建、就绪、运行、阻塞和死亡
- 第一是创建状态。在生成线程对象,并没有调用该对象的start方法,这是线程处于创建状态,
- 第二是就绪状态。当调用了线程对象的stat方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态;
- 第三是运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码;
- 第四是阻塞状态。线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait等方法都可以导致线程阻塞;
- 第五是死亡状态。如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪
15、JDK、JRE、JVM的联系与区别是什么?
JDK ( Java Development Kit )
Java开发工具包,除了包含JRE以外,还包含了开发Java程序所必须的编译、调试、运行命令工具。
JRE (Java Runtime Environment)
Java运行环境,主要包含两个部分: JVM和Java核心类库。
所有的Java 程序都要在JRE下才能运行。普通用户只需要运行已开发好的Java程序,安装JRE即可。
JVM ( Java Virtual Mechinal )
Java虚拟机,负责加载、执行字节码文件(class),它是Java实现跨平台的核心(一次编写,多处运行)。JVM是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持与操作系统无关,实现跨平台。
16、注解是什么原理?
主解其实就是一个标记,可以标记在类上、方法上、属性上等,标记自身也可以设置一些值。有了标记之后,我门就可以在解析的时候得到这个标记,然后做一些特别的处理,这就是注解的用处。
%如我们可以定义一些切面,在执行一些方法的时候看下方法上是否有某个注解标记,如果是的话可以执行一些寺殊逻辑(RUNTIME类型的注解)。
主解生命周期有三大类,分别是
RetentionPolicy.SOURCE: 给编译器用的,不会写入 class 文件
RetentionPolicyCLASS:会写入 class 文件,在类加载阶段丢弃,也就是运行的时候就没这个信息了。
RetentionPolicyRUNTIME: 会写入 class 文件,永久保存,可以通过反射获取注解信息。
17、说说List、Set、Map三者的区别?
在Java中,List、Set和Map是三种常见的集合类型,它们有以下区别:
- List(列表):
- List是有序的集合,允许重复元素存在。
- List可以通过索引访问和操作集合中的元素。
- 常见的List实现类有ArrayList、LinkedList等。
- Set(集合):
- Set是无序的集合,不允许重复元素存在。
- Set没有提供直接的索引访问方式,通常使用迭代器(Iterator)遍历元素。
- 常见的Set实现类有HashSet、TreeSet等。
- Map(映射):
- Map是一种键值对(Key-Value)的数据结构,用于存储具有唯一键的元素。
- Map中的键是唯一的,但值可以重复。
- 可以通过键来访问和操作对应的值。
- 常见的Map实现类有HashMap、TreeMap等。
简要总结:
- List是有序的、允许重复元素的集合。
- Set是无序的、不允许重复元素的集合。
- Map是基于键值对的映射结构,键唯一且与值相关联。需要根据实际需求选择适当的集合类型。
如果需要保持元素的插入顺序或允许重复元素,则选择List。如果需要保证元素的唯一性且不关心顺序,则选择Set。如果需要按照键值对存储和访问元素,则选择Map。
18、装箱拆箱
装箱:将基本数据类型转换为包装类类型
拆箱: 将包装类类型转换为基本数据类型
19、Integer 和 int 的区别?Java 为什么要设计封装类?
Integer 和 int 的区别有很多,我简单说 3 个方面
Integer 的初始值是 null,int 的初始值是 0
Integer 存储在堆内存,int 类型是直接存储在栈空间
Integer 是对象类型,它封装了很多的方法和属性,我们在使用的时候更加灵活。
至于为什么要设计封装类型,最主要的原因是 Java 本身是面向对象的语言,一切操作都是以对象作为基础,基本类型并不具有对象的性质,为了让基本类型也具有对象的特征,就出现了包装类型(如我们在使用集合类型Collection时就一定要使用包装类型而非基本类型),它相当于将基本类型“包装起来”,使得它具有了对象的性质。
比如像集合里面存储的元素,也只支持存储 Object 类型,普通类型无法通过集合来存储
20、String、StringBuffer、StringBuilder 的区别是什么?
-
可变与不可变。
- String类中使用字符数组保存字符串,因为有“final”修饰符,所以String对象是不可变的。对于已经存在的String对象的修改都是重新创建一个新的对象然后把新的值保存进去;
- StrinqBuilder与StringBufer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,这两种对象都是可变的。
-
线程安全
- String中的对象是不可变的,也就可以理解为常量,显然线程安全
- StringBufer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的
- StringBuilder是非线程安全的
-
执行效率
- String最慢,因为每次对String 类型进行改变的时候其实都等同于在堆中生成了一个新的 String 对象,然后将指针指向新的 String 对象,这样不仅效率低下,而且大量浪费有限的内存空间。
- StringBuilder的效率会比StringBuffer更高些
21、string为什么要设计成不可变的?
(1)便于实现字符串池 (String pool)
在Java中,由于会大量的使用String常量,如果每一次声明一个String都创建一个String对象,那将会造成极大的空间资源的浪费。Java提出了String pool的概念,在堆中开辟一块存储空间String pool,当初始化一个String变量时,如果该字符串已经存在了,就不会去创建一个新的字符串变量,而是会返回已经存在了的字符串的引用。
String a =“Hello world!”.
String b =“Hello world!”.
如果字符串是可变的,某一个字符串变量改变了其值,那么其指向的变量的值也会改变,String pool将不能够实现!
(2)使多线程安全
在并发场景下,多个线程同时读一个资源,是安全的,不会引发竞争,但对资源进行写操作时是不安全的,不可变对象不能被写,所以保证了多线程的安全
(3)避免安全问题
在网络连接和数据库连接中字符串常常作为参数,例如,网络连接地址URL,文件路径pah,反射机制所需要的String参数。其不可变性可以保证连接的安全性。如果字符串是可变的,黑客就有可能改变字符串指向对象的值,那么会引起很严重的安全问题。
(4)加快字符串处理速度
由于String是不可变的,保证了hashcode的唯一性,于是在创建对象时其hashcode就可以放心的缓存了,不需要重新计算。这也就是Map喜欢将String作为Key的原因,处理速度要快过其它的键对象。所以HashMap中的键往往都使用String。
总体来说,String不可变的原因要包括 设计考虑,效率优化,以及安全性这三大方面。
22、关于final关键字的一些总结
final 关键字主要用在三个地方: 变量、方法、类。
- 对于一个final 变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改:如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象;
- 当用final修饰一个方法时,则意味着这个方法不能被重写
- 当用final 修饰一个类时,表明这个类不能被继承。final 类中的所有成员方法都会被隐式地指定为final方法;
23、ArrayList 和 LinkedList 的区别
ArrayList和LinkedList都是Java集合框架中的List接口的实现类,它们有以下几点不同:
-
内部实现:ArrayList底层使用数组实现,而LinkedList底层使用双向链表实现。
-
插入和删除操作:由于ArrayList是基于数组实现的,所以在插入和删除元素时需要移动元素。而LinkedList则可以通过改变节点之间的引用来实现插入和删除操作,因此平均复杂度更低。当需要频繁的执行插入和删除操作时,LinkedList通常比ArrayList表现更好。
-
随机访问:由于ArrayList是基于数组实现的,所以随机访问效率很高,时间复杂度为O(1)。而LinkedList没有索引,只能从头开始遍历找到所需元素,因此随机访问的效率比较低,时间复杂度为O(n)。
-
内存占用:在内存占用方面,ArrayList比LinkedList更优,因为ArrayList只需要存储数据元素本身,而LinkedList还需要存储指向前后节点的指针。
综上所述,如果需要对元素进行频繁插入、删除操作,建议使用LinkedList。而如果需要随机访问元素和对元素进行大量的读取操作,建议使用ArrayList。
24、Java中的集合类
Java中的集合类可以分为两大类:Collection和Map。
- Collection接口
Collection是Java中所有集合类的基类,它可以存储一组对象。Collection接口的常用实现类有:
- List(列表):按顺序存储元素,允许重复元素。常用实现类有ArrayList、LinkedList、Vector等。
- Set(集):不按顺序存储元素,不允许重复元素。常用实现类有HashSet、TreeSet等。
- Queue(队列):按先进先出(FIFO)原则存储元素。常用实现类有ArrayDeque、LinkedList、PriorityQueue等。
- Map接口
Map是一种键值对(key-value)映射表,可以通过键来查找对应的值。Map接口的常用实现类有:
- HashMap:基于散列表实现,键值对无序。
- TreeMap:基于红黑树实现,键值对有序。
- LinkedHashMap:维护插入顺序或最近访问顺序,键值对有序。
- Hashtable:与HashMap类似,但它是线程安全的。
- ConcurrentHashMap:与HashMap类似,但它是线程安全的。
此外,Java还提供了一些其他的集合类,如:
- Arrays:提供了一些静态方法,用于操作数组,如排序、查找等。
- Collections:提供了一些静态方法,用于操作集合,如排序、查找等。
- BitSet:一种可变的位向量,用于存储一组二进制位。
以上是Java中常用的集合类。开发者可以根据具体需求来选择适合的集合类。
25、集合的排序方式的实现方案
在Java中,实现集合的排序可以通过以下几种方式来实现:
-
实现Comparable接口:
- 对于自定义类,可以让该类实现Comparable接口,并重写compareTo()方法。在compareTo()方法中定义对象之间的比较规则。然后使用Collections.sort()方法或Arrays.sort()方法对集合进行排序。
-
实现Comparator接口:
- 对于已有的类,或者无法修改源代码的类,可以创建一个单独的比较器类,实现Comparator接口,并重写compare()方法。在compare()方法中定义对象之间的比较规则。然后使用Collections.sort()方法或Arrays.sort()方法时,传入该比较器对象进行排序。
-
使用Lambda表达式或方法引用:
- 在Java 8以后,可以使用Lambda表达式或方法引用来简化排序操作。通过传递一个Comparator对象给sort()方法,使用Lambda表达式或方法引用来定义比较规则。
例如,假设有一个Person类,包含姓名(name)和年龄(age)属性,我们要按照年龄对Person对象进行排序:
// 实现Comparable接口
public class Person implements Comparable<Person> {
private String name;
private int age;
// 构造函数和其他方法
// 重写compareTo方法,根据年龄进行比较
@Override
public int compareTo(Person other) {
return Integer.compare(this.age, other.age);
}
}
// 使用Comparator接口
public class AgeComparator implements Comparator<Person> {
@Override
public int compare(Person p1, Person p2) {
return Integer.compare(p1.getAge(), p2.getAge());
}
}
// 使用Lambda表达式
List<Person> personList = new ArrayList<>();
// 添加数据到personList
Collections.sort(personList, (p1, p2) -> Integer.compare(p1.getAge(), p2.getAge()));
// 使用方法引用
personList.sort(Comparator.comparingInt(Person::getAge));
以上是几种常见的集合排序实现方案。开发者可以根据实际需求和业务逻辑选择合适的方式来进行集合排序。
25、抽象类和接口的区别
Java中两种不同的抽象类型,它们都可以用于实现多态和定义一组相关的操作。以下是抽象类和接口的主要区别:
- 实现方式:抽象类使用
abstract
关键字声明,并且可以包含具体方法的实现,也可以有成员变量。接口使用interface
关键字声明,只能包含方法的签名,不能有成员变量和方法的实现。 - 继承关系:一个类只能继承一个抽象类,但可以实现多个接口。接口之间可以通过继承扩展,一个接口可以继承多个接口。
- 构造函数:抽象类可以有构造函数,而接口不能有构造函数。因为接口是一种纯粹的规范,不涉及具体的实例化过程。
- 默认实现:抽象类可以提供具体方法的默认实现,子类可以选择性地重写这些方法。接口不能提供方法的默认实现,实现接口的类必须对接口的所有方法进行实现。
- 访问修饰符:抽象类中的成员变量和方法可以有不同的访问修饰符,可以是
public
、protected
、private
或默认。接口中的成员变量默认为public static final
,方法默认为public
。 - 设计目的:抽象类旨在为子类提供通用的行为和状态,它是一种对于子类的模板设计。接口则用于定义一组相关的操作,并且可以被不相关的类实现,提供了更大程度的灵活性。因此,选择抽象类还是接口取决于具体需求。如果需要提供默认实现和共享代码,则使用抽象类;如果需要定义一组操作规范,并让不相关的类进行实现,则使用接口。通常情况下,接口更适合用于实现多继承和解耦的场景。
26、super与this的区别
在Java中,super和this是两个关键字,用于访问父类和当前类的成员。它们之间有以下区别:
-
super关键字:
- super关键字可以用来访问父类的属性、方法和构造函数。
- 可以使用super来调用父类被子类覆盖的方法,即在子类中使用super.method()来调用父类的同名方法。
- 在子类的构造函数中,可以使用super关键字调用父类的构造函数,以便完成对父类部分的初始化。
- super关键字不能用于访问同级别或子类的成员。
-
this关键字:
- this关键字表示当前对象的引用,可以用来访问当前类的属性、方法和构造函数。
- 可以使用this来调用当前类的其他构造函数,以便在构造函数内部重用代码。
- this关键字也可以用于区分实例变量和局部变量,如果二者同名,可以使用this.varName来表示实例变量。
- this关键字不能用于访问父类的成员。
总结:
super关键字用于访问父类的成员,而this关键字用于访问当前类的成员。super关键字主要用于在子类中访问父类的成员或调用父类的构造函数,而this关键字主要用于区分实例变量和局部变量、调用其他构造函数或访问当前类的成员。它们都是在类的内部使用的关键字,用于提供更精确的访问和调用方式。
锁
1、线程安全需要保证几个基本特性:
原子性,简单说就是相关操作不会中途被其他线程干扰,一般通过同步机制实现。
可见性,是一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为将线程本地状态反映到主内存上,volatile 就是负责保证可见性的。
有序性,是保证线程内串行语义,避免指令重排等。
2、ReentrantLock 和 synchronized的区别
ReentrantLock和synchronized是Java中用于实现线程同步的两种机制。
synchronized的实现是依赖于jvm,ReentrantLock的实现是依赖于AQS
-
锁的获取方式:使用synchronized关键字时,线程会自动获取锁,当线程退出同步块或同步方法时,会自动释放锁。而ReentrantLock需要显式地调用lock()方法来获取锁,并且在不再需要锁时,需要调用unlock()方法来手动释放锁。
-
可重入性:ReentrantLock是可重入的,即同一个线程可以多次获取同一个锁,synchronized是可重入的。
-
等待可中断,通过lock.lockInterruptibly0来实现这机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情;
-
可实现公平锁,ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock默认情况是非公平的,可以通过ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的;
-
可实现选择性通知(锁可以绑定多个条件),ReentrantLock类线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll0方法进行通知时,被通知的线程是由JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知”
当使用 synchronized 和 ReentrantLock 实现可重入锁时,有一些需要注意的点。下面是一些常见的注意事项:
-
锁的获取和释放:使用 synchronized 关键字时,锁的获取和释放是隐式的,由 JVM 自动管理。而使用 ReentrantLock 时,需要手动调用 lock() 方法获取锁,并在合适的位置调用 unlock() 方法释放锁。为了避免忘记释放锁,通常会将 unlock() 方法放在 finally 块中。
-
公平性:synchronized 是非公平锁,它不保证线程获取锁的顺序。而 ReentrantLock 默认是非公平锁,但可以通过构造函数参数设置为公平锁。公平锁会按照线程请求锁的顺序进行处理,可能会导致性能下降,但可以避免饥饿现象。
-
锁的可重入性:synchronized 和 ReentrantLock 都支持锁的可重入性。可重入锁意味着同一个线程可以多次获取同一个锁,而不会产生死锁。在使用可重入锁时,需要注意确保每次获取锁后都要相应地释放锁。
-
条件变量:ReentrantLock 提供了条件变量(Condition),可以通过它们实现更灵活的线程等待和唤醒机制。通过调用 Condition 的 await() 方法使线程等待,调用 signal() 或 signalAll() 方法唤醒等待的线程。这种机制在某些场景下可以更好地控制线程的执行顺序和通信。
-
性能:一般情况下,synchronized 的性能比 ReentrantLock 好,因为 synchronized 是 JVM 内置的特性,而 ReentrantLock 是通过 Java 代码实现的。但在高度竞争的情况下,ReentrantLock 可以提供更好的性能,并且它具有更多的高级特性,如可中断锁、超时锁和公平锁等。
-
锁粒度:无论是 synchronized 还是 ReentrantLock,都需要在代码中选择合适的锁粒度。锁的粒度过大可能导致并发性能下降,而锁的粒度过小可能会增加锁的竞争和线程切换的开销。需要根据具体情况进行权衡和优化。
这些是使用 synchronized 和 ReentrantLock 实现可重入锁时需要注意的一些点。根据具体的需求和场景,选择合适的锁机制以及正确地使用和释放锁非常重要。
根据具体的需求和场景,可以选择适合的机制。通常情况下,推荐优先使用synchronized,因为它更简单易用,能够满足大部分的同步需求。只有在需要更高级功能时,或者需要更加精细地控制锁的获取和释放时,才考虑使用ReentrantLock。
3、关于死锁
所谓死锁,是指多个线程(或进程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用它们都将无法推进下去。
死锁的四个必要条件
- 互斥条件:线程(或进程)对所分配到的资源不允许其他线程(或进程)进行访问,若其他线程(或进程)访问该资源只能等待,直至占有该资源的线程(或进程) 使用完成后释放该资源
- 请求和保持条件:线程(或进程)获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他线程(或进程)占有,此时请求阻塞,但又对自己获得的资源保持不放;
- 不可剥夺条件:是指线程或进程》已获得的资源,在未完成使用前不可被剥夺,只能在使用完后自己释放
- 环路等待条件:是指线程(或进程)发生死锁后,若干线程(或进程)之间形成一种头尾相接的循环等待资源关系。
预防死锁
- 资源一次性分配::一次性分配所有资源,这样就不会再有请求了,或是只要有一个资源得不到分配,也不给这个线程(或进程)分配其他的资源(破坏请求和保持条件)。
- 可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)。
- 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺请求资源,释放则相反(破坏环路等待条件)
4、AQS(AbstractQueuedSynchronizer)
AQS(抽象队列同步器)就是一个并发包的基础组件,用来实现各种锁,各种同步组件的。
是一个用来构建锁和同步器的抽象框架。
它包含了state变量、加锁线程、等待队列等并发中的核心组件。
5、sleep()和 wait()有什么区别?
sleep方法:是Thread类的静态方法,当前线程将睡眠n毫秒,线程进入阻塞状态。当睡眠时间到了,会解除阻塞,进入可运行状态,等待CPU的到来。睡眠不释放锁(如果有的话)
wait方法: 是Object的方法,必须与synchronized关键字一起使用,线程进入阻塞状态,当notify或者notifyall被调用后,会解除阻塞。但是,只有重新占用互斥锁之后才会进入可运行状态。睡眠时,会释放互斥锁,
- sleep方法没有释放锁,而wait方法释放了锁
- sleep方法通常被用于暂停执行,wait方法通常被用于线程间交互/通信
- sleep方法执行完成后,线程会自动苏醒,wait方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll()方法
6、synchronized 的使用方法,加在静态方法和普通方法上的区别
synchronized
关键字可以用来实现线程同步,它可以修饰方法或者代码块。下面是 synchronized
的使用方法和静态方法和普通方法的区别:
-
synchronized
修饰方法:- 在方法声明中直接使用
synchronized
关键字修饰方法,例如:public synchronized void methodName() { ... }
- 当
synchronized
关键字修饰普通方法时,锁定的是当前实例对象(即方法所属对象)。当一个线程进入被synchronized
修饰的方法时,它将获取该方法所属对象的锁,其他线程需要等待。 - 不同的实例对象之间的实例方法是相互独立的,互不影响。每个实例对象都有自己的锁。
- 在方法声明中直接使用
-
synchronized
修饰静态方法:- 使用
synchronized
关键字修饰静态方法,例如:public static synchronized void methodName() { ... }
- 当
synchronized
关键字修饰静态方法时,锁定的是该方法所在的类的 Class 对象。每个类只有一个 Class 对象,因此静态方法共享同一把锁。 - 当多个线程调用同一个类的不同对象的静态方法时,同一时间只能有一个线程执行其中的一个静态方法,其他线程需要等待。
- 使用
需要注意的是:
- 被
synchronized
修饰的方法或代码块在同一时间只能被一个线程访问,其他线程需要等待。 synchronized
只能保证同一个对象的同步,对于不同对象之间的实例方法或静态方法,它们之间的锁是独立的。- 使用
synchronized
可以确保线程安全,但可能会降低并发性能。因此,在使用时需要权衡考虑是否需要使用锁机制,并根据实际需求进行优化。
除了使用 synchronized
关键字,还可以使用显式锁(如 ReentrantLock
)等更灵活的锁机制来实现线程同步,根据具体情况选择适合的方式。
7、在 Java 中,创建线程的常见方式有以下几种:
- 继承
Thread
类:- 创建一个类,继承
Thread
类,并重写run()
方法,在run()
方法中定义线程的任务逻辑。 - 通过创建该类的实例,并调用
start()
方法来启动线程执行任务。
- 创建一个类,继承
public class MyThread extends Thread {
@Override
public void run() {
// 定义线程的任务逻辑
// ...
}
}
// 创建线程并启动
MyThread myThread = new MyThread();
myThread.start();
- 实现
Runnable
接口:- 创建一个类,实现
Runnable
接口,并实现run()
方法作为线程的任务逻辑。 - 创建该类的实例,并将其作为参数传递给
Thread
类的构造方法。 - 通过创建
Thread
实例并调用start()
方法来启动线程执行任务。
- 创建一个类,实现
public class MyRunnable implements Runnable {
@Override
public void run() {
// 定义线程的任务逻辑
// ...
}
}
// 创建线程并启动
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
- 使用匿名内部类创建线程:
- 可以使用匿名内部类来继承
Thread
类或实现Runnable
接口,并重写run()
方法来定义线程的任务逻辑。 - 创建该匿名内部类的实例,并调用
start()
方法来启动线程执行任务。
- 可以使用匿名内部类来继承
// 使用匿名内部类继承 Thread 类创建线程
Thread thread = new Thread() {
@Override
public void run() {
// 定义线程的任务逻辑
// ...
}
};
thread.start();
// 使用匿名内部类实现 Runnable 接口创建线程
Runnable runnable = new Runnable() {
@Override
public void run() {
// 定义线程的任务逻辑
// ...
}
};
Thread thread = new Thread(runnable);
thread.start();
- 使用
Callable
和Future
(返回结果):- 创建一个类,实现
Callable
接口,并实现call()
方法作为线程的任务逻辑。 - 创建
ExecutorService
线程池,将Callable
任务提交给线程池执行。 - 通过
Future
对象获取线程执行结果。
- 创建一个类,实现
import java.util.concurrent.*;
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
// 定义线程的任务逻辑
return "Task result";
}
}
// 创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(1);
// 提交 Callable 任务并获取 Future 对象
Future<String> future = executorService.submit(new MyCallable());
// 获取线程执行结果
String result = future.get();
// 关闭线程池
executorService.shutdown();
以上是几种常见的创建线程的方式,在选择使用时,可以根据具体需求和场景来决定使用哪种方式。
8、CAS
CAS(Compare and Swap)是一种并发编程中的原子操作,用于解决多线程环境下的数据竞争问题。CAS操作由三个参数组成:内存地址(或称为变量的引用)、期望值和新值。它的执行过程如下:
- 获取当前内存地址的值(旧值)。
- 比较旧值与期望值是否相等,如果相等,则将新值写入内存地址;否则不做任何操作。
- 返回修改前的旧值。
CAS操作是以硬件级别的原子指令支持的,具有以下特点:
-
原子性:CAS操作是原子的,能够确保在同一时刻只有一个线程可以修改共享的变量。
-
无锁:CAS操作并不需要使用锁机制,因此避免了传统锁机制中的死锁和性能开销。
-
自旋:如果比较失败(旧值与期望值不相等),CAS操作会反复尝试更新,直到成功或达到某个限定次数。
由于CAS操作的特性,它常被用于实现无锁的数据结构、并发算法和乐观锁策略。但也需要注意以下几点:
-
ABA问题:CAS操作解决不了ABA问题,即当一个值由A变为B再变回A时,CAS无法感知到中间的修改。为了解决ABA问题,可以使用版本号或标记等方式。
-
自旋开销:CAS操作在比较失败时会进行自旋,如果自旋次数过多,会消耗大量的CPU资源。可以通过设定自旋次数或使用其他机制来避免自旋过长。
-
并发限制:由于CAS操作在尝试更新时需要比较旧值,因此如果有多个线程同时修改同一个变量,可能会造成竞争和冲突。在高并发场景下,需合理设计并发策略。
总的来说,CAS操作是一种高效、无锁的并发编程技术,能够在并发环境中实现一些并发数据结构和算法的核心功能。
平常用过线程池吗,该注意那些问题
在使用线程池时,需要注意以下几个问题:
-
线程池大小的选择:线程池的大小应该根据任务的类型、系统资源和性能要求来进行选择。如果线程池过小,可能导致任务排队等待执行;而如果线程池过大,则会增加线程上下文切换的开销。需要根据具体场景进行合理的调整。
-
任务提交方式:线程池一般支持两种任务提交方式,即同步提交和异步提交。同步提交会阻塞等待任务执行完成并返回结果,而异步提交则会立即返回一个 Future 对象。根据任务的需求和对结果的处理方式,选择合适的提交方式。
-
任务异常处理:当任务执行过程中发生异常时,线程池默认会捕获异常并忽略,不会抛出到外层。因此,需要在提交任务时设置合适的异常处理机制,例如通过重写
Thread.UncaughtExceptionHandler
处理未捕获的异常。 -
控制任务的依赖和顺序:线程池一般是无序执行任务的,但某些场景下可能需要控制任务的依赖和顺序。可以使用线程池提供的一些特殊队列(如
LinkedBlockingQueue
)或者使用其他方式来实现任务之间的依赖和顺序控制。 -
任务执行超时处理:当任务执行时间过长时,可能会影响整个系统的性能。可以通过设置任务的超时时间,并使用
Future.get()
方法获取结果时设置超时时长,来进行任务的超时处理。 -
线程池的关闭:在程序结束或不再需要使用线程池时,需要正确地关闭线程池,释放资源。可以调用线程池的
shutdown()
或shutdownNow()
方法来安全地关闭线程池,并等待所有任务执行完成。
总的来说,合理使用线程池可以提高多线程程序的性能和资源利用率。在使用线程池时需要注意线程池大小选择、任务提交方式、任务异常处理、任务依赖和顺序控制、任务执行超时处理以及线程池的关闭等问题,以保证多线程程序的正确性和可靠性。
mybatis
1、Mybatis 是如何进行分页的
有三种方式来实现分页:
第一种,直接在 Select 语句上增加数据库提供的分页关键字,然后在应用程序里面传递当前页,以及每页展示条数即可。
第二种,使用 Mybatis 提供的 RowBounds 对象,实现内存级别分页。
第三种,基于 Mybatis 里面的 Interceptor 拦截器,在 select 语句执行之前动态拼接分页关键字。
2、Mybatis 里面的缓存机制
Mybatis 里面设计了二级缓存来提升数据的检索效率,避免每次数据的
访问都需要去查询数据库。
一级缓存,是 SqlSession 级别的缓存,也叫本地缓存,因为每个用户在执行查询的时候都需要使用 SqlSession 来执行,为了避免每次都去查数据库,Mybatis 把查询出来的数据保存到 SqlSession 的本地缓存中,后续的 SQL 如果命中缓存,就可以直接从本地缓存读取了。
如果想要实现跨 SqlSession 级别的缓存?那么一级缓存就无法实现了,因此在Mybatis 里面引入了二级缓存,就是当多个用户在查询数据的时候,只有有任何一个 SqlSession 拿到了数据就会放入到二级缓存里面,其他的 SqlSession 就可以从二级缓存加载数据。
3、#和$的区别是什么,什么情况下必须用$,有什么问题
MyBatis中#
和$
的区别如下:
-
#
是预编译处理,即在SQL语句中使用占位符`#{}",MyBatis会将该占位符替换为一个?号,再使用PreparedStatement进行预编译处理,通过设置参数来执行SQL语句。 -
$
是字符串拼接,即在SQL语句中使用${}
,MyBatis会将该占位符替换为实际的参数值,生成完整的SQL语句。因此,使用${}
可能存在SQL注入的安全问题,应谨慎使用。
因为${}
是直接进行字符串拼接,所以必须用在SQL语句的动态拼接中,
以下情况下,必须使用$
符号:
- 表名、列名动态拼接:如果需要动态指定表名或列名,因为这些部分不能通过
#{}
进行占位,只能使用${}
进行字符串拼接。
例如,需要根据用户输入的条件查询不同的表:
SELECT * FROM ${tableName}
- SQL函数、关键字等:对于一些SQL函数、数据库的关键字等,也无法使用
#{}
进行占位,需要使用${}
。
例如,在MySQL中使用LIMIT关键字限制查询结果的数量:
SELECT * FROM users LIMIT ${limit}
- Order By 子句:如果希望动态指定排序字段和排序方式,也需要使用
${}
。
例如,根据用户条件动态排序查询结果:
SELECT * FROM users ORDER BY ${columnName} ${sortDirection}
需要注意的是,在使用${}
时要特别注意防范SQL注入攻击,确保传入的参数值是经过验证和过滤的,以免造成安全风险。可以通过合理的输入校验和参数过滤来预防可能的攻击。
JVM
1、类加载的五个过程:加载、链接(验证、准备、解析)、初始化
-
加载阶段主要查找并加载类的二进制数据。在该阶段,虚拟机需要完成以下3件事情:
(1)通过一个类的全限定名来获取定义此类的二进制字节流。
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
(3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。 -
验证阶段主要确保被加载的类的正确性
(1)文件格式验证
(2)元数据验证
(3)字节码验证
(4)符号引用验证 -
准备阶段主要为类的静态变量分配内存并将其初始化为默认值
-
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
-
初始化是指为类的静态变量赋予正确的初始值
2、java的垃圾收集器有哪些,各有什么特点
-
Serial收集器:
- 在进行垃圾收集时,会暂停所有的用户线程。
- 单线程的垃圾收集器,主要用于客户端应用和小型服务器。
-
Parallel收集器:
- 目标是获取最高的吞吐量,最大限度减少垃圾回收总时间
- 与Serial收集器类似,也使用标记-复制算法。
- 收集过程会使用多个线程并行执行,提高垃圾收集效率。
-
CMS收集器(Concurrent Mark Sweep):
- 目标是获取最小垃圾回收的停顿时间
- 使用标记-清除算法,在标记阶段和用户线程同时运行。
- 由于不进行整理过程,可能会产生内存碎片导致效率下降。
-
G1收集器(Garbage-First):
- 一种面向服务器的垃圾收集器,应用于多核大内存的环境
- 具有可预测的停顿时间,可以避免长时间的垃圾收集暂停。
-
ZGC收集器:
- 一种低延迟的垃圾收集器,旨在减少全局垃圾收集造成的停顿时间。
- 使用了并发标记和整理算法,使垃圾收集与应用程序执行同时进行。
- 可以处理非常大的堆内存,适用于需要低延迟和高吞吐量的场景。
3、什么是双亲委派机制
当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。
保证了安全性(防止核心类库被篡改)、防止重复加载
Bootstrap ClassLoader 启动类加载器
Extention ClassLoader 扩展类加载器
Application ClassLoader 应用类加载器
User ClassLoader 用户自定义类加载器
4、tomcat为什么要打破双亲委派机制
为了实现两个项目之间的类隔离
对象创建流程
-
类加载检查
-
分配内存
内存分配的两种方式
① 指针碰撞
使用场合:堆内存规整(即没有内存碎片)的情况下。
实现原理:将用过的内存都整合到一边,没有用过的内存放到另一边,中间有一个分界指针,当需要为新对象分配内存空间时,只需要将分界指针向没有用过的内存一侧移动对象内存大小位置即可。
②空闲列表
使用场合:堆内存不规整的情况下。
实现原理:虚拟机会维护一个列表,该列表记录了哪些内存是可用的,当需要为新对象分配内存空间时,只需要在列表中找一块足够大小的内存分配给对象实例,然后更新列表记录列表 -
初始化
-
设置对象头
-
执行方法
动态年龄判断: 如果 survivor 区的相同年龄的所有对象的内存大小总和如果大于survivor 区内存区域的50%,那么年龄大于等于该年龄的对象会直接进入到老年代
jvm调优常见参数
-xss 设置线程栈(虚拟机栈)的最大内存空间
-xms 设置堆的最小内存 默认 电脑内存/64
-xmx 设置堆的最大内存 默认 电脑内存/4
-xmn 设置新生代大小
jvm怎么调优
加机器、加内存、调比例
redis
1、内存淘汰策略是指在Redis的内存使用达到上限时,选择哪些键来从内存中移除以腾出空间给新的数据。Redis使用了几种常见的内存淘汰策略,包括:
-
LRU(Least Recently Used,最近最少使用):LRU是Redis默认采用的内存淘汰策略。它会优先淘汰最近最少被访问的键,即最久未被使用的键。
-
LFU(Least Frequently Used,最不经常使用):LFU根据键被访问的频率来进行淘汰。频率较低的键会被优先淘汰,以便为更频繁访问的键腾出空间。
-
Random(随机):随机策略是简单而有效的一种策略。它会随机选择某个键进行淘汰,无论其最近访问频率如何。
-
TTL(Time To Live,生存时间):TTL策略基于键的过期时间。当键的过期时间到期时,它将被淘汰。
-
Maxmemory-policy:除了以上单一策略外,Redis还提供了多种组合策略,如volatile-lru、volatile-ttl、allkeys-lru等。这些组合策略结合了LRU和TTL,以及对特定类型的键进行淘汰。
需要注意的是,内存淘汰策略在Redis中是可配置的,可以根据具体的业务需求选择适合的策略。不同的策略会对性能、数据持久性和缓存命中率等产生影响。同时,Redis还提供了手动删除或通过设置最大内存限制来避免内存溢出的方法。
总之,内存淘汰策略用于在Redis内存使用达到上限时选择要淘汰的键。LRU是默认策略,但其他策略如LFU、随机、TTL等也可根据具体需求进行配置。选择合适的策略有助于平衡内存使用和系统性能。
2、跳表是基于链表的一种优化数据结构,通过在原始链表中插入冗余的索引节点来加速查找操作。这些冗余索引节点将链表中间的某些元素"跳过",形成了多级索引结构。
具体地说,跳表中的每个节点都包含了多个指针,这些指针分别指向相同层级的下一个节点或下一级索引的节点。通过这样的结构,可以在查找时跨越不必要的中间节点,从而大大减少了查找的时间复杂度。
通常情况下,跳表会有多层索引,底层索引是原始链表,每一层索引的节点数量逐渐减少(可以通过随机化算法决定节点插入的层级),最顶层索引只有一个节点,用于快速定位元素的起始位置。
跳表的优点是简单易懂且易于实现,它的插入、删除和查找操作的时间复杂度均为O(log n)。相比平衡树等其他数据结构,跳表的实现更简单,并且具有较高的查询效率。
总而言之,跳表通过在链表中添加冗余的索引节点,将部分元素跳过,从而加快了查找的速度。它是一种简单且有效的数据结构,在实际应用中得到了广泛的使用。
3、持久化
Redis 支持两种持久化方式,用于在服务器重启后将数据从内存恢复到磁盘。
-
快照(RDB)持久化:
优点: 使用单独子进程来进行持久化,主进程不会进行任何 IO操作 ,保证了 Redis 的高性能;而且RDB文件存储的是压缩的二进制文件,适用于备份、全量复制,可用于灾难备份,同时RDB文件的加载速度远超于AOF文件。
缺点:RDB是间隔一段时间进行持久化,如果持久化之间的时间内发生故障,会出现数据丢失 -
日志(AOF)持久化:日志持久化记录了 Redis 服务器接收到的所有写操作命令,以文本方式追加写入到磁盘中的 AOF 文件。Redis 会根据配置的不同策略将 AOF 文件进行重写,以减小文件大小。在服务器重启时,可以通过重新执行 AOF 文件的命令来还原数据。
日志持久化的优点是可以提供更高的数据安全性,因为它记录了每个写操作命令,具备更好的灾难恢复能力。但是相对于快照持久化,AOF 文件的体积可能会比较大,恢复时间可能会稍长。
可以通过配置文件中的 “save” 和 “appendonly” 选项来启用和调整持久化方式。同时,Redis 还支持同时开启快照和日志持久化,以提供更高的数据可靠性。
需要注意的是,持久化不适合用作实时的数据备份方案,而是作为故障恢复、数据迁移和系统升级的手段之一。因此,在使用持久化时,需要根据实际情况选择合适的持久化方式,并结合定期备份策略来保障数据的安全和可靠性。
4、redis与数据库内存一致性问题
缓存过期时间兜底 、更新数据库后删除缓存、订阅binlog方式(引入了消息队列和消息服务,成本较高)
5、四种缓存模式
旁路缓存、读穿透、写穿透、异步缓存写入
6、分布式锁实现要点
基于SET NX(不存在则添加)
加锁时候要设置owner和过期时间,前者是便于解锁时进行拥有者判断,后者是作为异常情况的兜底.解锁时候要先判断owner,是自己的再释放,这里还要注意这两步操作的原子性,可以用lua脚本来进行保证
怎么知道解锁来的人是谁?
两层hash map:
第一层key 业务key(订单id)
第二层key threadId , value 锁的重入次数
默认30s过期,每十秒通过watchDog检查一次,发现业务代码还在执行就主动进行续约(主动设置了过期时间watchdog就不会生效)
7、redis基本数据类型及其应用
String 类型的应用场景:缓存对象、常规计数、分布式锁、共享 session 信息等。
List 类型的应用场景:消息队列(但是有两个问题:1. 生产者需要自行实现全局唯一 ID;2. 不能以消费组形式消费数据)等。
Hash 类型:缓存对象、购物车等。
Set 类型:聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。
Zset 类型:排序场景,比如排行榜、电话和姓名排序等。
MySQL
一条更新sql的执行流程
先建立数据库链接,然后经过sql解析,sql优化器,开始执行sql
先更新undolog,再更新buffer pool,然后进行事务的两阶段提交
消息队列
mq消费模式
广播 订阅
spring
1、Spring Bean的线程安全问题(默认单例)
若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的。
1.如何解决ThreadLocal
2.单例模式,无论是静态成员变量还是普通成员变量,ThreadLocal都可以解决线程安全问题(把变量变成线程私有的)
Proptotype(原型模式)
成员变量线程是否安全。安全
静态变量线程是否安全。不安全
singleton(单例模式)有状态
普通成员变量线程是否安全。 不安全(通过get bean注入IOC容器,下一次发现有了就不注入了)
静态变量线程是否安全。不安全
2、Springboot的启动流程
初始化SpringApplication对象
- 推断应用类型,根据不同的应用类型,完成一些容器的初始化工作
- 实例化META-INF/spring.factories中已配置的ApplicationContextlnitializer初始化器类
- 实例化META-INF/spring.factories中已配置的ApplicationListener初始化器类
执行run方法
- 启动监听器
- 创建并配置应用程序上下文。
- 执行自动配置过程,根据classpath中的依赖和配置进行自动配置。
- 注册Bean,并进行依赖注入。
- 启动内嵌Servlet容器,并将应用程序部署到容器中。
- 运行应用程序,接收并处理外部请求
3、bean的生命周期
①实例化(Instantiation):在容器启动时,根据配置信息或注解创建Bean实例。这可以通过构造函数实例化、工厂方法或依赖注入来完成。
②属性赋值(Populating Properties):在实例化后,容器会注入Bean的属性值。这可以通过依赖注入、Setter方法等方式完成。
③初始化(Initialization):在属性赋值完成后,容器调用Bean的初始化回调方法,如InitializingBean接口的afterPropertiesSet()方法或自定义的init方法。开发者可以在这些方法中执行自定义的初始化逻辑。
④使用(In Use):Bean被放置在应用程序上下文中,可以被其他组件引用和使用。
⑤销毁(Destruction):当容器关闭或Bean不再需要时,会调用Bean的销毁回调方法,如DisposableBean接口的destroy()方法或自定义的destroy方法。开发者可以在这些方法中执行清理资源或释放连接的操作。
4、Spring中的IOC和AOP和DI的设计理念
-
IOC(控制反转):IOC的设计理念是将对象的创建和管理交给容器来完成,而不是由开发者手动创建和管理对象。在传统的编程模型中,对象之间的依赖关系通常通过直接实例化其他对象来实现,这导致对象之间紧密耦合,难以维护和扩展。而通过IOC,开发者将对象的依赖关系描述出来,容器根据这些描述来自动创建和管理对象,并在需要时将依赖注入到相应的对象中。这样可以降低对象之间的耦合度,提高代码的可维护性和可扩展性。
-
AOP(面向切面编程):将一些通用的逻辑集中实现,然后通过 AOP 进行逻辑的切入,减少了零散的碎片化代码,提高了系统的可维护性。
具体是含义可以理解为:通过代理的方式,在调用想要的对象方法时候,进行拦截处理,执行切入的逻辑,然后再调用真正的方法实现。
使用动态代理生成代理对象,代理对象持有原始对象的引用,然后对外暴露代理对象
spring生命周期初始化的时候生成代理对象
设计模式:代理模式、责任链模式(拦截器、filter)
场景:动态切换数据源、前置后置通知 -
DI(依赖注入):DI的设计理念是通过外部容器来注入对象所需的依赖,而不是由对象自己负责创建和管理依赖。传统的编程模型中,对象通常通过直接实例化其他对象来满足自身的依赖需求,这会导致对象之间的紧耦合。而通过DI,开发者可以将对象的依赖需求描述出来,容器根据这些描述来为对象提供所需的依赖。依赖注入可以通过构造函数注入、Setter方法注入或接口注入等方式实现。DI的好处是降低了对象之间的耦合度,使得代码更灵活、可测试和可维护。
5、动态代理与静态代理
静态代理在编译期确定代理对象的结构,需要手动创建代理类,用于实现对目标对象的增强(典型的Aspect就是静态代理)。
动态代理是在运行时生成代理对象的方式,通过Java的反射机制动态创建代理类和对象。动态代理相对灵活,可以对多个目标对象共享一个代理对象,但相对复杂一些。
6、Autowired和@Resource的区别
@Autowired和@Resource都是用于依赖注入(DI)的注解,但在使用方式和功能上存在一些区别。
@Autowired注解:
- 配置方式:@Autowired可以用于构造函数、字段、Setter方法和任意其他方法上。
- 自动装配规则:默认情况下,@Autowired注解根据类型进行依赖注入。如果容器中存在多个匹配类型的Bean,还可以结合@Qualifier注解指定具体的Bean名称。@Primary用于指定当存在多个同类型的Bean时,优先选择哪个Bean作为自动装配的依赖对象
- @Autowired的required属性默认为true,必须进行依赖注入。
@Resource注解:
- 配置方式:@Resource可以用于字段、Setter方法和任意其他方法上,但不能用于构造函数上。
- 自动装配规则:@Resource可以根据name或type进行依赖注入。当指定了name属性时,会按名称进行匹配;当未指定name属性时,会按类型进行匹配(
都未指定先按name
)。 - 必须存在的依赖关系:@Resource注解的依赖关系默认是必须的,即要求容器中必须存在对应的Bean。可以通过设置@Resource的required属性为false,允许依赖关系为可选。
在实际使用中,根据具体情况选择@Autowired还是@Resource。如果只考虑Spring框架,推荐使用@Autowired;如果需要与Java EE兼容或按名称进行明确匹配的场景,可以考虑@Resource。
7、JDK动态代理和Cglib动态代理的区别
JDK动态代理和Cglib动态代理是两种常见的Java动态代理技术,它们在实现原理和使用方式上存在一些区别。
-
JDK动态代理:
- 基于接口:JDK动态代理是基于接口的代理技术。它要求目标对象实现一个接口,代理对象通过实现同样的接口来进行代理。JDK动态代理使用
java.lang.reflect.Proxy
类和java.lang.reflect.InvocationHandler
接口来实现。 - 反射机制:JDK动态代理利用Java的反射机制,在运行时生成代理对象的字节码。代理对象的方法调用会被重定向到InvocationHandler接口的方法上,从而实现对目标对象的代理。
- 适用范围:JDK动态代理适用于那些实现了接口的目标对象。
- 基于接口:JDK动态代理是基于接口的代理技术。它要求目标对象实现一个接口,代理对象通过实现同样的接口来进行代理。JDK动态代理使用
-
Cglib动态代理:
- 基于继承:Cglib动态代理是基于继承的代理技术。它通过生成目标对象的子类来实现代理,无需目标对象实现接口。Cglib动态代理使用Cglib库来实现。
- 字节码操作:Cglib动态代理通过对目标对象的字节码进行操作,生成一个子类作为代理对象,并重写父类的方法。代理对象的方法调用会被重定向到MethodInterceptor接口的方法上,从而实现对目标对象的代理。
- 适用范围:Cglib动态代理适用于那些没有实现接口的目标对象。
-
使用场景:
- JDK动态代理通常应用于对接口进行代理的情况,例如对Spring Bean进行事务管理。
- Cglib动态代理通常应用于对类进行代理的情况,例如对普通的Java类进行增强。
需要注意的是,由于实现原理不同,JDK动态代理和Cglib动态代理在性能上会有所差异。通常情况下,JDK动态代理相对较快,但要求目标对象实现接口;而Cglib动态代理相对较慢,但可以代理没有实现接口的目标对象。因此,在选择使用哪种动态代理技术时,需要根据具体的需求和场景进行权衡。
8、Spring中的事务传播机制
多个事务方法相互调用时,事务如何进行传播的
REQUIRED:默认的传播特性,如果当前没有事务,则新建一个事务,如果当前存在事务,则加入这个事务
SUPPORTS:当前存在事务,则加入当前事务,如果当前没有事务,则以非事务的方式执行
MANDATORY:当前存在事务,则加入当前事务,如果当前事务不存在,则抛出异常
REQUIRED_NEW:创建一个新事务,如果存在当前事务,则挂起改事务
NOT_SUPPORTED:以非事务方式执行,如果存在当前事务,则挂起当前事务
NEVER: 不使用事务,如果当前事务存在,则抛出异常
NESTED: 如果当前事务存在,则在嵌套事务中执行,否则REQUIRED的操作一样
NESTED和REQUIRED NEW的区别:
REQUIRED_NEW是新建一个事务并且新开始的这个事务与原有事务无关,而NESTED则是当前存在事务时会开启一个嵌套事务,在NESTED情况下,父事务回滚时,子事务也会回滚,而REQUIRED NEW情况下,原有事务t回滚,不会影响新开启的事务
NESTED和REOUIRED的区别:
REQUIRED情况下,调用方存在事务时,则被调用方和调用方使用同一个事务,那么被调用方出现异常时,由于共用一个事务,所以无论是否catch异常,事务都会回滚,而在NESTED情况下,被调用方发生异常时,调用方可以catch其异常,这样只有子事务回滚,父事务不会回滚
9、Spring事务底层原理以及什么时候会失效
用@Transactional注解来实现
通过代理模式,Spring会为带有事务注解的类生成一个代理对象,在代理对象中对事务进行管理。代理对象会拦截被代理对象的方法调用,根据事务注解的配置来决定是否开启、提交或回滚事务。
10、spring框架中使用了哪些设计模式及应用场景
-
模板模式(Template Method Pattern):Spring提供了JdbcTemplate模板类,封装了数据库操作的公共逻辑,客户端只需关注自己的业务逻辑。应用场景包括:数据库操作、事务控制等。
-
单例模式(Singleton Pattern):Spring容器默认使用单例模式管理Bean对象,确保每个Bean在容器中只存在一个实例,从而提高性能和资源利用。应用场景包括:Service层、Repository层、工具类等。
-
工厂模式(Factory Pattern):Spring使用工厂模式来创建和管理Bean对象,通过依赖注入(DI)将Bean的创建过程交给工厂来完成。应用场景包括:ApplicationContext作为Bean工厂,根据配置文件创建Bean实例。
-
代理模式(Proxy Pattern):Spring AOP(面向切面编程)使用了代理模式,在运行时动态地生成代理对象,对目标对象的方法进行增强。应用场景包括:事务管理、日志记录、安全检查等。
-
观察者模式(Observer Pattern):Spring事件驱动模型基于观察者模式,通过定义事件和监听器,实现不同组件之间的解耦和通信。应用场景包括:处理用户请求、处理异步消息等。
-
策略模式,加载资源文件的方式,使用了不同的方法,比如: ClassPathResourece,FileSystemResource,ServletContextResource,UrlResource但他们都有共同的接口Resource; 在Aop的实现中,采用了两种不同的方式,JDK动态代理和CGLIB代理
11、springboot自动装配原理
自动装配,简单来说就是自动把第三方组件的 Bean 装载到 Spring IOC 器里面,不需要开发人员再去写 Bean 的装配配置。
在 Spring Boot 应用里面,只需要在启动类加上@SpringBootApplication 注解就可以实现自动装配。
@SpringBootApplication 是一个复合注解,真正实现自动装配的注解是@EnableAutoConfiguration。
自动配置原理
1、@EnableAutoConfiguration注解导入AutoConfigurationImportSelector类。
2、执行selectImports方法调用SpringFactoriesLoader.loadFactoryNames()扫描所有jar下面的对应的META-INF/spring.factories文件.
3、限定为@EnableAutoConfiguration对应的value,将这些装配条件的装配到IOC容器中。
自动装配简单来说就是自动将第三方的组件的bean装载到IOC容器内,不需要再去写bean相关的配置,符合约定大于配置理念。
Spring Boot基于约定大于配置的理念
,配置如果没有额外的配置的话,就给按照默认的配置使用约定的默认值,按照约定配置到IOC容器中,无需开发人员手动添加配置,加快开发效率。
12、Spring Boot的核心注解是哪个?它主要由哪几个注解组成的?
启动类上面的注解是@SpringBootApplication,它也是Spring Boot的核心注解,主要包含了以下3个注解
-
@SpringBootConfiguration: 继承自@Configuration,二者功能也一致,标注当前类是配置类。
可以理解为一个Confguration就是对应的一个Spring的xml版的容器个被@Configuration标注的类,相当于一个ApplicationContext.xml文件。 -
@EnableAutoConfiguration: 即把指定的类构造成对象,并放入Spring容器中,使其成为bean对象,作用类似@Bean注解。
-
@ComponentScan:主要作用是定义包扫描的规则,然后根据定义的规则找出哪些需类需要自动装配到Spring的bean容器中,然后交由Spring进行统一管理。标注了@Controller、@Service、@Repository、@Component 的类都可以被spring扫描到。
13、bean的注入方式
在 Spring 框架中,有三种主要的 bean 注入方式:
-
构造器注入(Constructor Injection):使用构造器注入时,Spring 容器会自动调用 bean 的构造方法,将所依赖的 bean 通过构造方法的参数传递进来,在 bean 初始化时进行依赖注入。构造器注入是一种安全可靠的注入方式,也是推荐的方式之一。
-
属性注入(Property Injection):属性注入是指使用 setter 方法对 bean 的属性进行注入。Spring 容器在初始化 bean 之后,会自动调用 bean 的 setter 方法,将所依赖的 bean 实例注入到对应的属性中。但是属性注入可能会引起空指针异常等问题,因此需要特别注意。
-
接口注入(Interface Injection):接口注入是指让 bean 实现相应的接口,通过接口的方法注入依赖的 bean。这种方式不太常用,因为同时实现很多接口会导致代码难以维护。
另外,还有自动装配(Autowired)注入方式,它是 Spring 框架提供的一种便捷的注入方式。自动装配可以根据注入目标的类型、名称或其他属性,自动匹配并注入所依赖的 bean 实例。自动装配方式简单方便,但需要避免出现重复或歧义的注入,提高代码的可读性与清晰度。
以上是 Spring 框架中常用的 bean 注入方式,选择何种方式,需要根据具体的情况进行选择和实施,以确保系统能够正确、安全地运行。在面试中,这个问题一般会被问到,需要回答清楚各种注入方式的区别和使用场景,以显示出你对 Spring 框架的熟练程度和实际运用能力。
cloud
单体的问题:合度:登录模块 调用 用户模块,可用性:某个模块失效导致整体业务不可用。开发成本.分布式/微服务:大的单体架构拆分成多个子模块或者子系统,逻辑上是一个整体,但是在物理层面是有多个子模块的。登录模块 服务器 A,用户模块 服务器 B
耦合降低了,减少开发成本,单个模块故障不影响整体。
服务注册与发现 Eureka / Nacos
整合所有模块的所有服务,统一存储。服务间调用的时候,直接去服务中心中查询。
负载均衡一一客户端与服务端 Ribbon 与 Nginx
负载均衡是一种在分布式系统中分配请求负担的技术,将请求均匀地分发给多个服务器,以提高系统的性能和可扩展性。
Nginx: 服务端负载均衡,请求交给负载均衡器,负载均衡器选取某种算法,从可用服务器中选取某个服务器,将请求转发过去。
Ribbon: 客户端负载均衡,客户端在发送请求前,从服务中心中选择一个服务端实例,进行访问。
远程调用 RPC-- Feign / OpenFeign
动态代理的机制,反射。登录模块 要调用 用户模块的 userList; @OpenFeign,实现目标接口,不用考虑方法实现;直接定义入参和返回值。
服务熔断与降级-Hystrix / Sentinal
限流:限流桶、漏通
配置中心 Config / Nacos
统一管理所有配置文件
有大量的模块,每个模块都有许多配置文件。yml,apl,conf。
按照 namespace,dev , test 。
网关 GateWay / Zuul
Nginx 做网关,偏前端,还没到服务器。
GateWay 做网关,偏后端,/user/id=1,网关把请求路由到具体的微服务,/order。统一鉴权,限流,缓存,
请求的统一处理(鉴权)
网络编程
1、HTTP被称为无状态协议是因为它在不同请求之间不会保存任何状态信息。每个HTTP请求都是相互独立的,服务器不会记住先前请求的任何信息。
无连接:HTTP默认是无连接的,即每个请求和响应之间都是独立的连接。当客户端发送请求后,服务器会根据请求进行处理并发送响应,然后立即关闭连接。这意味着服务器不会保留与该特定客户端的连接状态。
2、传输层的功能包括建立连接、差错控制、流量控制、断开连接
-
建立连接:传输层提供了建立连接的机制,通常使用的是面向连接的传输协议,如传输控制协议(TCP)。通过三次握手的过程,源主机和目标主机可以进行双向的通信准备工作,确保双方都已准备好进行数据传输。建立连接后,可以进行可靠的数据传输。
-
差错控制:传输层负责实现可靠的数据传输,确保数据在源主机和目标主机之间的正确传递。这包括错误检测和纠正等机制。传输层使用各种技术来检测数据在传输过程中可能出现的错误,并尽可能地纠正这些错误,以保证数据的完整性和准确性。
-
错误检测:传输层使用校验和等技术来检测数据在传输过程中是否发生了错误。发送方计算数据的校验和,并将其与接收方收到的校验和进行比较,以确定数据是否出现了错误。
-
错误纠正:某些传输层协议支持在数据传输过程中进行错误纠正。例如,TCP使用序列号和确认机制来确保数据的有序交付,并通过重传机制来纠正可能丢失或损坏的数据包。
-
-
流量控制:传输层使用流量控制机制来确保发送方和接收方之间的数据传输速度匹配。通过协商和调整发送速率、接收确认等方式,防止发送方发送过多的数据导致接收方无法及时处理。
-
断开连接:当应用程序完成数据传输或者不再需要连接时,传输层可以通过发送特定的控制信号来发起断开连接的过程。断开连接的过程通常包括四次挥手。
3、TCP(传输控制协议)和UDP(用户数据报协议)的区别以及应用场景-----------------IP(网络协议)
TCP(传输控制协议)和UDP(用户数据报协议)是两种常用的传输层协议,它们在数据传输的可靠性、连接性和适用场景等方面有所不同。
-
可靠性:
- TCP提供可靠的数据传输。它使用序列号、确认和重传等机制来确保数据的完整性、顺序性和可靠性。如果数据包丢失,TCP会自动重传,以确保数据的正确交付。
- UDP是无连接的,不提供可靠性保证。它不进行确认、重传等操作,因此在传输过程中可能丢失、重复或乱序。UDP更注重传输速度和效率。
-
连接性:
- TCP是面向连接的协议。在数据传输之前,需要进行三次握手建立连接,然后在通信结束后再进行四次挥手断开连接。
- UDP是无连接的协议,数据包可以直接发送,没有连接建立和断开的过程。每个数据包都是独立的,相互之间没有依赖关系。
-
适用场景:
- TCP适用于对数据传输的可靠性和顺序性要求较高的场景,如文件传输、网页浏览、电子邮件等。它能够确保数据的正确性,但相对于UDP会有一定的延迟和开销。
- UDP适用于对实时性要求较高、丢失少许数据也不会造成严重影响的场景,如音频、视频流媒体、在线游戏等。UDP具有较低的延迟和开销,但无法保证数据的可靠性。
4、HTTP和HTTPS的区别
-
安全性:
- HTTP是明文传输协议,数据在传输过程中不进行加密,容易被窃听和篡改。因此,HTTP通信存在安全风险,尤其是对于涉及敏感信息的网站。
- HTTPS使用SSL/TLS协议进行加密,通过对数据进行加密和认证来确保通信的安全性。加密后的HTTPS数据在传输过程中难以被窃听和篡改,提供了更高的安全性。
-
端口:
- HTTP使用TCP(通常是端口号80)作为传输层协议,数据传输不进行加密。
- HTTPS在HTTP上添加了SSL/TLS加密层,使用加密的SSL/TLS连接进行数据传输。默认情况下,HTTPS使用TCP的443端口。
-
证书验证:
- HTTPS使用数字证书对服务器进行身份验证。浏览器会检查服务器的证书是否由受信任的证书颁发机构(CA)签发,并验证证书的有效性和合法性,确保用户与正当服务器建立安全连接。
- HTTP没有证书验证机制,无法提供对服务器身份的验证,存在被伪装的风险。
-
使用场景:
- HTTP适用于对安全性要求不高的场景,如一般的网页浏览和数据传输。它的传输速度相对较快。
- HTTPS适用于对传输数据安全性要求较高的场景,尤其是涉及敏感信息(如个人账号、密码、支付信息等)的网站和应用。
5、http一次完成流程
-
浏览器查询DNS,获取域名对应的IP地址:具体过程包括浏览器搜索自身的缓存去网络上搜索映射
-
浏览器获得域名对应的IP地址以后,浏览器向服务器请求建立链接发起三次握手
-
TCP/IP链接建立起来后,浏览器向服务器发送HTTP请求
-
Server端服务器处理请求spring内部逻辑
6、什么是对称加密什么是非对称加密
- 对称密钥加密是指加密和解密使用同一个密钥的方式,这种方式存在的最大问题就是密钥发送问题,即如何安全地将密钥发给对方;
- 而非对称加密是指使用一对非对称密钥,即公钥和私钥,公钥可以随意发布,但私钥只有自己知道。发送密文的一方使用对方的公钥进行加密处理对方接收到加密信息后,使用自己的私钥进行解密。由于非对称加密的方式不需要发送用来解密的私钥,所以可以保证安全性;但是和对称加密比起来,非常的慢
7、Https协议流程
- 服务器端申请CA证书,获得自己的公钥私钥
- 客户端与服务端建立连接,客户端对服务器的公钥和证书进行可靠性校验,防止中间人攻击
- 然后客户端将自己的私钥传递给服务器端
- 之后的数据传输就直接使用客户端的私钥进行对称加密就可以了
HTTPS是对称加密还是非对称加密?
建立连接时是非对称加密交换客户端的私钥之后就用的对称加密
先用非对称密钥加密,来保证传输安全,再用对称保证通信的效率
8、cookie和session 干嘛的? 有什么区别
HTTP是无状态协议,一个服务端可以和很多个客户端建立连接,但是服务端无法识别客户端谁是谁
所以需要有一套机制让客户端存储自己的数据,服务器也能够通过客户端传递的信息匹配对应的信息。相当于去医院看病,cookie就是病人的病历,session就是医院的存档
什么是cookie
cookie是由Web服务器保存在用户浏览器上的文件(key-value格式)可以包含用户相关的信息。客户端向服务器发起请求,就提取浏览器中的用户信息由http发送给服务器
什么是session
session是浏览器和服务器会话过程中,服务器会分配的一块储存空间给session。服务器默认为客户浏览器的cookie中设置sessionid,这个sessionid就和cookie对应,浏览器在向服务器请求过程中传输的cookie包含sessionid,服务器根据传输cookie中的sessionid获取出会话中存储的信息然后确定会话的身份信息。
cookie数据存放在客户端上安全性较差session数据放在服务器上安全区别性相对更高
9、Token
用户认证:当用户成功登录后,服务器会生成一个唯一的Token,并将其返回给客户端。客户端在后续的请求中携带该Token,服务器通过验证Token来确认用户的身份。
授权访问:除了确认用户身份外,Token还可以包含用户的权限信息。服务器在验证Token时,可以根据Token中的权限信息来决定是否允许用户访问特定的资源或执行某些操作。