又到一年求职季,持续更新高频java面试题
目录
- java 基础
- JDK JRE JVM
- 字节码
- final
- 异常
- 异常处理
- hashCode、equals、==
- JAVA SE 和 JAVA EE
- Java 访问修饰符 public、private、protected,default
- 接口和抽象类
- String、StringBuffer、StringBuilder
- 为什么String不可变
- String两种创建方式的区别
- 内部类
- 反射机制
- 值传递和引用传递
- 深拷贝和浅拷贝
- 序列化与反序列化
- transient
- volatile
- 集合
- Collection 和 Collections的区别
- 有哪些Collection对象
- 集合
- 迭代器 Iterator
- ArrayList 和 Vector 的区别
- ArrayList 和 LinkedList 的区别
- HashMap 和 HashTable 的区别
- 哪些集合是线程安全的
- HashMap是线程安全的吗
- 解决hash冲突的方法
- HashMap的扩容过程
- HashMap的扩容机制
- 红黑树
- JVM
- JVM内存模型 ✅
- 垃圾回收机制
- 并发
- 线程池的优点
- 线程池的参数
- 线程池执行原理
- java线程池种类
- 线程的生命周期
- start() 和 run()
- sleep() 和 wait()
- java创建线程的5种方式
- 如何保证线程安全
- 如何停止一个正在运行的线程
- 什么是CAS
- Web
- Cookie 和 Session
- 什么是跨域?如何解决
- Spring 框架
- Spring 框架是什么,有哪些好处
- Spring的两大核心
- AOP的实现原理
- 动态代理
- JDK动态代理 和 CGLib动态代理
- Spring容器是怎么去创建bean并获取bean
- 有哪些注解可以注入Bean
- 注解的实现原理
- Spring中的BeanFactory和ApplicationContext有什么区别和联系 ✅
- Spring支持哪几种事务管理类型
- 拦截器和过滤器 ✅
- Spring、SpringMVC、SpringBoot之间的关系
- 什么是java Bean
- 什么是依赖注入
- IOC容器的初始化过程
- Bean 的生命周期
- 设计模式
- 设计模式的六大原则
- 单例模式
- 工厂模式
- 模板模式
- 代理模式
- 观察者模式
- 建造者模式
- Spring中涉及到的设计模式
- 消息队列
- Git 版本控制
- git基本命令
java 基础
JDK JRE JVM
基本介绍:
- JDK:Java Development Kit java开发工具包
- JRE:Java Runtime Environment java运行时环境和其他基础构件
- JVM:Java Virtual Machine java虚拟机 : 不同的操作系统有不同的JVM,实现一次编译到处运行
三者之前的关系:
- JDK=JRE+开发工具
- JRE=JVM+核心类库
- 因此三者之间的关系是:JDK=JRE(JVM+核心类库)+开发工具
从程序运行的角度:
- 程序员编写源代码:JDK (比如使用调试工具)
- 源代码编译成字节码:JDK(使用编译器javac)
- 字节码转化为机器码:JVM(根据不同的OS转换为不同的机器码)
- 操作系统执行机器码:JRE (运行机器码当然要用到运行环境)
字节码
基本介绍:
- 字节码是java程序编译后的中间代码,是一种可移植性的二进制代码,可以在支持JVM的平台上运行
- 通过将java源代码编译为字节码指令序列,使得java程序可以跨平台运行,即使在不同的操作系统和硬件平台也可以运行
好处:
- 跨平台:由于字节码是中间代码,所以可以在任何支持JVM的平台上运行,使得java程序有更好的移植性,这也是java扩平台的重要特性之一
- 安全性:由于字节码在JVM中运行,因此可以对字节码进行安全检查,以确保程序不会对系统造成威胁
- 性能:由于字节码是一种紧凑的二进制格式,相比于直接编译为机器码,可以更快递加载和传输,同时也可以在运行时进行动态优化,提高程序的执行效率
- 可读性:相比于直接编译为机器码,字节码具有更好的可读性,可以方便地进行反汇编和调试
final
final关键字可以用来修饰类、方法和变量
- 修饰方法:表示该方法不能被子类重写
- 修饰类:表示该类不能被继承,final类中的方法默认都是final的
- 修饰变量:表示该变量只能被赋值一次,并且在声明时或者构造函数中初始化,初始化后不能被修改,常用语常量定义
final关键词的优点:
- 安全:将变量声明为final,可以防止它被改变,从而提高安全性
- 性能:使用final修饰的变量在编译时就已经确定了其值,并被优化为常量,因此在运行时访问比非final的变量更快
finally和finalize
- finally是异常处理语句结构的一部分,一般以try-catch-finally出现,finally代码块代表总是被执行
- finalize是Object类的一个方法,一般有垃圾回收器来调用,当我们调用System.gc()方法的时候,由垃圾回收器调用finalize()方法,回收垃圾。
异常
- 编译时异常:是编译器要求必须处置的异常
- FileNotFound:当操作一个不存在的文件时,发生异常
- ClassNotFound:加载类,而该类不存在时候,发生异常
- ILLegalArguement:参数异常
- EOF:操作文件,到达文件末尾,发生异常
- 运行时异常:编译器检查不出来,一般是指编程时的逻辑错误,是程序员应该尽量避免出现的异常
- NullPointerException:空指针异常
- ArithmeticException:数学运算异常
- ArrayIndexOutOfBoundException:数组下标越界异常
- ClassCastException:类型转换异常
- NumberFormatException:数字格式不正确异常
异常处理
try-catch-finally
- catch和finally都可以省略,但是不可以同时省略
- 如果没有发生异常catch不会被执行
- 无论有没有异常,finally中的内容都会被执行
- 当在try和catch中遇到return语句时,finally语句将在方法返回之前被执行
throws
- 将发生的异常抛出,交给调用者(方法)来处理,最顶级的调用者是JVM
- 如果一个方法可能生成某种异常,但是并不能确定如何处理这些异常,这个方法就可以显示地声明抛出异常,该方法不对这个异常进行处理,交给调用者来处理。
hashCode、equals、==
类型 | 返回类型 | 本质 | |
---|---|---|---|
hashCode | Object类中的方法 | int | 返回一个代表对象hash码的整数值,地址相同的两个对象有相同的哈希码 |
equals | Object类中的方法 | Boolean | 子类往往会重写该方法,如果没有重写,相当于==,比较两个对象的地址是否相同 |
== | 运算符 | Boolean | 对于基本类型:比较值是否相等;对于引用类型:比较内存地址是否相等 |
- equals()相等时,hashCode()不一定相等
- equals()没有被重写时,说明两个对象的引用相同,则hashCode()相等
- equals()被子类重写时,说明引用可能不相同(内容相同),则hashCode()不相等
- hashCode()相等, equals()不一定相等
- 当哈希冲突时,两个对象的hashCode()相等,但是equals()不相等
- 当哈希不冲突时,两个对象的hashCode()相等,equals()相等
- 在重写了equals()之后要重写hashcode():这是为了保证equals()为true的时候,hashcode()也相等。
- hashcode()是用来提升对象的比较效率的,先进行hashcode()的比较,如果不相等,就说明两个对象不一致,不必要再调用equals()方法
JAVA SE 和 JAVA EE
- JAVA SE:java platform standard edition (java 平台标准版)
- JAVA EE:java platform enterprise edition (java 平台企业版)
- 关系:
- JAVA SE 被包含在JAVA EE中
- JAVA EE 是java应用中最广泛的部分
Java 访问修饰符 public、private、protected,default
访问修饰符指的是控制类、接⼝、⽅法、属性等成员的访问范围
访问修饰符 | 同类 | 同包 | 子类 | 不同包 |
---|---|---|---|---|
公开的 public | ✅ | ✅ | ✅ | ✅ |
受保护的 protected | ✅ | ✅ | ✅ | ❌ |
默认的 default | ✅ | ✅ | ❌ | ❌ |
私有的 provide | ✅ | ❌ | ❌ | ❌ |
接口和抽象类
- 抽象类:当父类中的某些方法需要声明,但是又不知道如何实现时,可以将其声明为抽象方法(没有方法体的方法),那么这个类对应的就是抽象类
- 接口:只有方法的声明,没有方法的实现,可以理解为一种特殊的抽象类
以jdk7.0为例:
抽象类 | 方法 | |
---|---|---|
构造器/实例化 | ✅ | ❌ |
方法种类 | 可抽可不抽 | 全抽 |
方法修饰 | 抽象方法用abstract修饰,不能用private/final/static修饰 (和继承相违背) | 只能是public abstract,可省略abstract |
属性修饰 | 无要求 | 只能public static final |
多继承 | 抽象类只能继承一个类 | 接口可以实现多个接口 |
String、StringBuffer、StringBuilder
- String 和 StringBuffer/StringBuilder 是java中两种字符串的处理方式,主要区别在于String是不可变的,但是StringBuffer/StringBuilder是可变的
- String对象一旦被创建就不可以修改,任何的字符串操作都会返回一个新的String对象,这可能导致频繁的对象创建和销毁,影响性能。而StringBuffer/StringBuilder允许修改操作,更加高效
- StringBuffer 是线程安全的,而StringBuilder不是线程安全的。
为什么String不可变
- JDK8中,String的底层是一个char[]数组(两个字节),它由final修饰,是不可变的。并且是private的,没有对外提供修改内部状态的方法。
String两种创建方式的区别
-
方式一:直接赋值
- String s=“hsp”
- 先从常量池查看是否有“hsp”的数据空间,如果有,直接指向;如果没有则重新创建,然后指向。s最终指向的是常量池中的空间地址。
-
方式二:调用构造器
- String s=new String(“hsp”)
- 先在堆中创建空间,里面维护了value属性,指向常量池的hsp空间。如果常量池没有“hsp”,重新创建,如果有,直接通过value指向。最终指向的是堆中的空间地址。
- 内存分布
内部类
内部类是定义在另一个类中的类,它有四种:
- 定义在外部类的成员位置上
- 成员内部类: 没有被static修饰
- 静态内部类:被static修饰
- 定义在外部类的局部位置上(方法中)
- 局部内部类: 有类名
- 匿名内部类:没有类名
优点:
- 内部类可以访问外部类的私有成员变量和方法
- 解决了java的单继承问题
- 可以方便地对外部类进行扩展
反射机制
java的反射机制是指在运行时获取类的信息、加载类的对象、调用其中的方法和属性。
优点:
- 提高程序的灵活性和扩展性,降低耦合性
- 动态地创建对象,调用方法,不需要在编译时就知道对象的类型
- 反射机制是构建框架技术的基础所在,使用反射可以避免将代码写死在框架中
缺点:
- 性能问题:放射机制中包含了一些动态类型,所以虚拟机不能对动态代码进行优化,反射的操作会比正常的操作效率低
- 封装性:使用反射会破坏程序的封装性
应用场景
- 动态代理:动态代理可以使⽤反射机制在运⾏时动态地创建代理对象,⽽不需要在编译时就知道接⼝的实现类
- 单元测试:JUnit 等单元测试框架可以使⽤反射机制在运⾏时动态地获取类和⽅法的信息,实现⾃动化测试
- 配置文件加载:许多框架(如 Spring)使⽤反射机制来读取和解析配置⽂件,从⽽实现依赖注⼊和⾯向切⾯编程等功能
- 注解:基于反射来分析类,然后获取到类/属性/方法上的注解
- IDEA等开发工具利用反射动态解析对象的类型和结构,动态提示对象的属性和方法
反射的原理
- 反射是基于java虚拟机的类加载机制的
- 在java程序运行时,通过ClassLoader来加载类的信息,创建Class对象
- 每个类都有一个对应的class对象,这个对象包含了该类的所有信息,包括类的名称、父类、实现的方法等。
获取class对象的几种方式:
- 编译阶段:Class.forName:Class cls = Class.forName(“java.lang.Cat”)
- 加载阶段:类.class:Class cls = Cat.class 已经知道了类
- 运行阶段:对象.getClass() 已经有了对象实例
值传递和引用传递
函数形参和实参的概念:java只有值传递,没有引用传递
- 值传递:传递参数的一个副本
- 引用传递:传递参数引用的一个副本
// 对于基本数据类型
int a=1;
System.out.println("改变之前:"+a); // 1
testStandard(a);
System.out.println("改变之后:"+a); // 1
//对于String
String b=new String("1");
System.out.println("改变之前:"+b); // 1
testString(b);
System.out.println("改变之后:"+b); // 1
//对于对象
Person person = new Person();
person.setA(1);
System.out.println("改变之前:"+person.getA()); // 1
testPerson(person);
System.out.println("改变之后:"+person.getA()); // 999
- 对于基本数据类型:在函数内改变变量值,函数外的值不变,说明传递是值传递
- 对于对象:在函数内改变变量值,函数外的值也改变了,这是因为传递的参数本身就是引用,相当于传递了一个引用值,也是值传递
- 对于String:他是一个特殊的类,他的值本身就是不会改变的,所以也是值传递
深拷贝和浅拷贝
- 深拷贝:拷贝对象和原始对象的引用不相同
- 浅拷贝:拷贝对象和原始对象的引用相同
序列化与反序列化
- 序列化:将对象转换为字节序列的过程
- 反序列化:将字节序列恢复为对象的过程
应用场景:
- 当需要对内存中的对象进行持久化(持久存储在磁盘/数据库)和网络传输时(和浏览器进行交互/rpc),就需要用到序列化和反序列化
- 发送方将要发送的java对象序列化为字节序列,在网络上传输,接收方收到后从字节序列中恢复出java对象
如何实现序列化
- 只有实现了Serializable和Externalizable接口的类的对象才能被序列化
实现Serializable接口之后,为什么要显示地指定serialVersionUID的值
- 序列化时会根据属性自动生成一个serialVersionUID,反序列化时会自动生成一个新的serialVersionUID
- 反序列化时,会比较两个serialVersionUID是否相等
- 如果显示指定了serialVersionUID,那序列化和反序列化时生成的serialVersionUID的值就是我们指定的值,这两个serialVersionUID一定是相等的
- 如果不显示指定,如果在序列化之后修改了类的属性,那么反序列化生成的serialVersionUID和之前生成的serialVersionUID会不一致,反序列化失败
static属性为什么不会被序列化?
- 序列化是针对对象而言的,但是static属性优于对象存在,随着类的加载而加载,所以不会被序列化
- 显示指定的serialVersionUID是被static修饰的,它实际是不参与序列化的,JVM在序列化时会自动生成一个serialVersionUID将值指定为这个static修饰的serialVersionUID
transient
- adj.转瞬即逝的,短暂的;暂住的,(工作)临时的
- transient修饰的实例变量,在对象存储时,他的值不需要维持
- 序列化时,transient修饰的成员变量的值会被忽略
- 反序列化时,transient变量的值设置为默认值(int为0)
volatile
- volatile可以保证变量的可见性:如果我们将变量声明volatile,就指示JVM,这个变量是共享且不稳定的,每次使用都需要从主从中进行读取
- volatile可以防止JVM的指令重排序:
- volatile只能保证变量的可见性,但不能保证对变量的操作是原子性的
- synchronized既能保证可见性,也能保证原子性
集合
Collection 和 Collections的区别
- Collection是单列集合
- Collections是操作List Set和Map集合的工具类,它提供了一系列的方法对集合元素进行排序、查询和修改等操作。
有哪些Collection对象
Collection是单列集合:
- List: ArrayList/Vector/LinkedList
- Set:HashSet/LinkedHashSet/TreeSet
- Queue:LinkedList
集合
集合主要是单列集合Collection和双列集合Map
基本特点:
- List:有序存储、允许重复、允许多个null
- Set:无序存储、不允许重复、只允许一个null
- Map:保存键值对、key和value可以为null,key为null的键值对放在下标为0的头结点的链表中
实现方式:
- List有数组和链表两种实现方式
- Set基于Map实现,Set的元素值是Map的键
- Set和Map有基于哈希和红黑树两种实现方式
迭代器 Iterator
- Iterator模式用同一种逻辑遍历及耦合
- 不需要了解集合的内部实现就可以遍历集合元素
- 更加安全:在当前遍历的集合元素被更改时,会抛出CurrentModificationException
- 主要有三个方法:hasNext(), next(), remove()
ArrayList 和 Vector 的区别
- 两者都是可变数组
- Vector是线程安全的,但是效率比较低,一般不用
- ArrayList在内存不够时,扩充为原来的1.5倍,Vector扩充为2倍
ArrayList 和 LinkedList 的区别
- ArrayList 基于动态数组实现,LinkedList基于双向链表实现
- 对于get和set操作,ArrayList比LinkedList效率高,因为ArrayList可以直接通过下标访问,而LinkedList还需要移动指针
- 对于插入和删除操作,LinkedList比ArrayList效率高,因为ArrayList需要扩容和复制数组,但是LinkedList只需要修改指针即可
HashMap 和 HashTable 的区别
都实现了map接口,都能存放键值对,但是有以下区别:
HashMap | HashTable |
---|---|
KV可以为null | KV不能为null |
线程不安全 | 线程安全 |
单线程下速度较快 | 速度较慢(因为是同步方法) |
会重新计算hash值 | 直接使用对象的hashCode |
哪些集合是线程安全的
- Vector
- Hashtable:通过synchronized关键字保证Hashtable底层共享变量操作的UAN自省与内存可见性
- ConcurrentHashMap:通过cas+synchronized保证原子性
- Stack:继承与Vector,所以也是线程安全的
HashMap是线程安全的吗
不是线程安全的:
- 多线程情况下,在扩容时可能会导致环形链表,出现死循环
- 多线程情况下,可能会发生数据覆盖的情况
如何解决:
- 多线程的情况下,HashMap的put(引起扩容)操作可能会导致死循环,应该使用支持多线程的ConcurrentHashMap
- ConcurrentHashMap采用CAS和synchronized保证并发安全
解决hash冲突的方法
- 拉链法:所有的同义词存储在一个链表中 (HashMap使用的是链地址法)
- 开放地址法:
- 线性探测法
- 平方探测法
- 在散列法
- 伪随机序列法
HashMap的扩容过程
HashSet的扩容机制和HashMap一样,因为HashSet底层就是由HashMap实现的
- 第一次添加时,table扩容到16,临界值(threshold)是16*加载因子(loadFactor)0.75=12
- 也就是说,不是到达16才进行第二次扩容,而是在12时就开始扩容
- 如果table数组到临界值12,就会扩容到16 * 2=32,新的临界值是32 * 0.75=24,以此类推
HashMap的扩容机制
- HashMap的扩容机制和HashSet完全一致(因为HashSet的底层就是用HashMap实现的)
- jdk7.0的HashMap的底层实现是【数组+链表】,jdk8底层的实现是【数组+链表+红黑树】
- 链表长度大于8,并且table长度大于等于64时才会树化。
- 假如树化后,不停地删除这棵树上的结点,就会进行剪枝操作:将这棵树转为链表
为什么先用链表,再用红黑树?
- 红黑树需要进行左旋、右旋、变色等操作来保持平衡,而链表不用
- 当元素个数小于8时,使用链表结构可以保证查询性能
- 当元素个数大于8,数组长度大于等于64时,会采用红黑树结构
- 红黑树的搜索时间复杂度是o(logn),而链表是o(n),当n比较大的时候,红黑树的搜索速度更快
红黑树
红黑树和二叉平衡树 (AVL)
- AVL是严格的平衡二叉树,必须满足所有节点的左右子树高度差的不超过1,在执行了插入和删除之后,如果不平衡了就需要通过旋转进行调整。
- 维护这种高度平衡是很耗时的,在实际应用中,追求的是局部的而不是整体的平衡
- 因此在插入和删除较多的场景中,红黑树的效率更多,受到了广泛的应用
红黑树
- 红黑树是一种弱平衡的二叉查找树
- 在普通的二叉查找树的基础上增加了存储位表示节点的颜色
- 通过限制每个节点的着色,确保没有一条路径比其他路径长出两倍
红黑树的特点:
- 每个结点非红即黑
- 根节点是黑色
- 每个叶子节点都是黑色
- 如果某个节点是红色,那么它的两个子节点都是黑色
- 对于任意一个节点而言,到叶子结点的每条路径都包含相同数目的黑节点
JVM
JVM内存模型 ✅
-
1️⃣ 程序计数器:
- 当前线程所执行的字节码的行号指示器、多线程情况下用于记录当前线程的执行位置
- 唯一不会出现OutOfMemoryError 的内存区域
- 随着线程的创建而创建,随着线程的结束而死亡
-
2️⃣ 虚拟机栈
- 由一个个栈帧组成
- 每次函数调用都有一个对应的栈帧被压入虚拟机栈,调用结束后弹出
-
3️⃣ 本地方法栈
- 虚拟机栈为虚拟机执行java服务,本地方法栈为虚拟机使用到的native方法服务
- Native 方法一般是用其它语言(C、C++等)编写的。
-
4️⃣ 堆
- 堆用来管理对象实例,是垃圾收集器管理的主要区域
-
5️⃣ 方法区
- 和堆一样,是各个线程共享的内存区域
- 用于存储已经被虚拟机加载的类信息、常量、静态变量等
- 对方法区进行垃圾收回的主要目标是,对常量池的回收和堆类的卸载
- 运行时常量池:方法区的一部分,在类加载后,将编译器生成的各种字面量和符号引号放到运行时常量
垃圾回收机制
-
标记清除法
- 利用可达性遍历内存,把存活对象和垃圾对象进行标记,标记结束之后回收垃圾对象
- 效率低,会产生大量不连续的内存碎片
-
标记整理法
- 标记过程和标记清除法一样
- 但是不直接对垃圾对象进行清除,而是将所有的存活对象都向一端移动,然后清理掉边界以外的内存空间
-
复制算法
- 将内存划分为大小相等的两块,每次使用其中一块
- 当这一块的内存使用完之后,将存活的对象复制到另一块内存中,然后再把这一块使用的空间一次性清除掉
- 实现简单、高效,但是内存缩小为原来的一半,浪费空间
-
分类收集算法
- 将堆分为新生代和老年代
- 新生代采用复制算法:因为新生代的存活率较低,只有少量的存活对象,只需要付出少量的对象复制成本
- 老年代采用标记清理/标记整理:因为老年代的存活率较高,只有少量的垃圾对象
并发
线程池的优点
为什么不直接new一个线程,而要要用线程池?
- 降低资源消耗:重复利用已创建的线程降低线程创建和销毁产生的时间消耗
- 提高响应速度:当任务到达时,无需要等线程创建就能立即执行
- 提高线程的可管理性:统一管理线程,避免系统创建大量同类线程而消耗完内存
线程池的参数
ThreadPoolExecutor通用构造器
- corePoolSize:核心线程池大小
- maximumPoolSize:最大线程池大小
- BlockingQueue:存储等待运行的任务
- keepAliveTime:非核心线程保持存活的时间
- ThreadFactory:线程池创建线程是通过线程工厂来创建的
- RejectedExecutionHandler:当队列和线程池都满了的时候,根据拒绝策略处理新任务
- TimeUnit:时间单位
线程池执行原理
- 当线程池中存活的线程数<corePoolSize时,对于新提交的任务,会创建新的线程来处理任务
- 当线程池中存活的线程数=corePoolSize时,将任务放在任务队列中排队等待
- 当线程池中存活的线程数=corePoolSize,并且任务队列也满了,如果有新的任务到来,会创建进程处理,直到线程数=maximumPoolSize
- 当线程池中存活的线程数=maximumPoolSize,并且任务队列也满了,如果有新的任务带来,采用拒绝策略进行处理
java线程池种类
- 可缓存的线程池: newCachedThreadPool
- 固定大小的线程池:newFixedThreadPool
- 足够大小的线程池:newWorkStealingPool
- 可做任务调度的线程池:newScheduledThreadPool
- 单个线程的线程池:newSingleThreadPool
线程的生命周期
- 初始(NEW):线程被创建,还没有调用start()方法
- 运行(RUNNABLE):包括操作系统的就绪(Ready)和运行(Running)两种状态
- 阻塞(BLOCKED):在抢占资源时得不到资源,被动挂起在内存,等待资源释放释放将其唤醒。线程的阻塞会释放CPU,不会释放内存
- 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定的动作(通知/中断)
- 超时等待(TIMED_WAITING):该状态不等同于等待,它可以在指定的时间后自行返回
- 终止(TERMINATED):表示线程执行完毕
start() 和 run()
start()
- 用start()方法启动线程,是真正意义的启动线程,实现了多线程运行,会出现异步执行的效果
- 使用start()方法启动线程,此时线程处于可运行状态中的就绪状态,并没有直接到运行态,一旦等到cpu时间片,才开始运行
- start()方法只能调用一次,多次调用会抛出IllegalThreadStateException异常
run()
- 用run()方法启动线程,是同步执行线程,不是真正意义上的使用线程
- run方法只是一个普通的方法,调用run方法程序中只有主线程这一个线程,程序执行路径只有一条。
- 没有限制调用次数
举例1:run方法启动线程是顺序执行的,删除是pong ping
Thread thread=new Thread(){
public void run(){
System.out.println("pong");
}
};
thread.run();
System.out.println("ping");
举例2:start方法启动才是真的多线程执行,输出是ping pong
Thread thread=new Thread(){
public void run(){ // 重写run方法
System.out.println("pong");
}
};
thread.start();
System.out.println("ping");
sleep() 和 wait()
相同点:
- 都可以使线程暂停运行,把机会交给其他线程
- 任何线程在调用了这两个方法之后,在等待期间被中断都会抛出InterruptedException
不同点:
- wait()是Object超类中的方法,而sleep()是线程Thread类的方法
- 对锁的持有不同,wait会释放锁,sleep不释放锁
- 调用wait要先获取对象的锁,sleep不用
- 唤醒的方式不完全相同,wait依靠notify、中断、达到指定时间来唤醒,而sleep只能是到达指定时间被唤醒
java创建线程的5种方式
- 继承Thread类
class MyThread extends Thread{ // 继承Thread创建线程
@Override
public void run() {
for(int i =0 ;i <100; i++){
System.out.println(i);
}
}
}
- 实现Runnable接口
class HerThread implements Runnable{ // 实现Runnable创建线程
@Override
public void run() {
System.out.println("implements Runnable");
}
}
- 实现Callable接口
- 使用 Executor创建线程池
// 创建一个指定线程数量的线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
// 执行线程操作,需要提供实现Runnable和Callable的实现类对象
executorService.execute(new MyThread());
executorService.execute(new HerThread());
executorService.shutdown();
- 使用内部类
Thread hisThread=new Thread(){
@Override
public void run() {
System.out.println("内部类");;
}
};
hisThread.start();
如何保证线程安全
导致线程不安全的原因有三:
- 不满足原子性:一个或多个操作在执行时被中断
- 不满足可见性:一个线程对共享变量的修改,另一个进程不能立刻看到
- 不满足有序性:程序的执行顺序没有按照代码的先后顺序执行
保证线程安全的方法:
- JDK中提供了很多atomic类,比如AtomicInteger、AtomicLong,这些类使用CAS来保证原子性
- java提供volatile 关键字,可以保证修改对其他线程的可见性
- java中提供了很多的锁机制,比如synchronized,保证同步代码块或者同步方法的有序性
如何停止一个正在运行的线程
- stop():stop()方法可以强制终止线程,不过stop是一个被弃用的方法,不推荐使用
- interrupt():interrupt():方法可以中断一个线程,该方法只是告诉在当前线程中打了一个停止的标记,并不是真的停止线程,何止停止取决于计算机
- 设置标志位:使用共享变量的方式修改标志位,使线程正常退出
什么是CAS
- CAS:Compare and Swap 比较与交换
- CAS在不使用锁的情况下实现多线程之间的变量同步
- 它涉及到三个操作:
- 需要读写的内存值V
- 进行比较的值A
- 需要写入的新值B
- 只要当V=A时,才会使用原子方式来更新V的值为B,否则会继续重试直到成功更新
Web
Cookie 和 Session
session:
- session存放在服务器端,当浏览器第一次发送请求时,服务器会自动生成一个session和一个session ID,并将其通过响应发送到浏览器
- 浏览器第二次请求时,会将该session ID 放入请求中一并发送到服务器
- 服务器从请求中取出session ID,并和保存的session ID做对比,找到这个用户对应的session ID
cookie:
- cookie是一种session客户端的实现形式,即客户端保存session ID的方式之一
保存session ID的三种方式:
- cookie
- 使用URL附加信息
- 在页面表单中增加隐藏域
区别:
- cookie数据存储在浏览器 session把数据存储在服务器
- cookie的安全性较差,但是过多的session会占用服务器资源
- 可以将登陆等重要信息存在session中,其他信息如果需要保留可以放在cookie中
什么是跨域?如何解决
- 跨域:指的是由于同源策略的限制,一个域名的网页不允许去请求另一个域名的资源,是一种浏览器的安全策略
- 同源策略:指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。
- 解决办法:
- @CrossOrigin:在Controller类上添加一个 @CrossOrigin(origins =“*”) 注解就可以实现对当前controller 的跨域访问了,也可以在启动类上配置
- Ngix配置反向代理: 同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只使用HTTP协议,因此可以使用反向代理作为一个中转。
正向代理:配置在客户端
反向代理:配置在服务端,对客户端透明
Spring 框架
Spring 框架是什么,有哪些好处
- Spring是一个为了解决企业应用开发的复杂性而创建的开源框架
- Spring目前是JAVA EE的灵魂框架,可以简化JAVA EE开发,可以方便地做到整合其他框架,无侵入地进行功能增强
- Spring的核心是 控制反转(IOC inversion of control)和 面向切面编程(AOP aspect-oriented programming )
- 好处:
- 控制反转IOC:将对象创建和依赖注入由应用代码反转到Spring容器中,降低了代码的复杂性
- 面向切面编程AOP:将于业务逻辑无关的代码剥离出来,方便统一管理和维护
- 便于测试:可以方便地整合单元测试和集成测试,提高代码的可测试性和可靠性
- 支持声明式事务管理:可以通过配置来管理应用程序中的事务,简化事务管理过程
- 提供多种技术整合方法:Spring框架可以与其他Java企业应用程序和技术进行整合,如Mybatis、Hibernate等
Spring的两大核心
Spring的核心是 控制反转(IOC inversion of control)和 面向切面编程(AOP aspect-oriented programming )
- 🍓 IOC:
- 将对象的创建和依赖注入由应用代码反转到了 Spring 容器中进行,即由 Spring 容器负责创建对象和管理它们之间的依赖关系。
- 应用代码只需要关注业务逻辑的实现,无需要关注对象的创建和管理,降低了应用代码的复杂性,提高了可重用性和可维护性
- 简单来说,就是不在代码中new一个对象了,而是通过Spring容器读取配置文件来实现对象创建
- 🍓AOP:
- 将与业务逻辑无关的代码(如日志、安全等)从业务逻辑中剥离出来,以便统一管理和维护
- 简单来说,就是一种可以不修改原来的核心代码的情况下给程序动态统一进行增强的一种技术
- 举例:可以使用AOP来实现全局的统一登录校验,可以不在每一个方法中单独校验
AOP的实现原理
- 基于动态代理实现
- 如果要代理的对象实现了某个接口,那么Spring AOP 会使用JDK Proxy去创建代理对象
- 对于没有实现接口的对象,使用Cglib的生成一个被代理对象的子类来作为代理
动态代理
代理模式:
- 使用代理对象来代替对真实对象的访问,可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能
- 代理模式一般用于扩展目标对象的功能,比如说在目标对象的某个方法执行前后增加一些自定义的操作
静态代理:
- 定义一个接口和实现类
public interface SmsService {
String send(String message);
}
public class SmsServiceImpl implements SmsService {
public String send(String message) {
System.out.println("send message:" + message);
return message;
}
}
- 创建一个代理类并实现同样的接口
public class SmsProxy implements SmsService {
private final SmsService smsService;
public SmsProxy(SmsService smsService) {
this.smsService = smsService;
}
@Override
public String send(String message) {
//调用方法之前,我们可以添加自己的操作
System.out.println("before method send()");
smsService.send(message);
//调用方法之后,我们同样可以添加自己的操作
System.out.println("after method send()");
return null;
}
}
- 将目标对象注入代理类,在代理类的对应方法中调用目标类的对应方法,这样就可以通过代理类屏蔽目标对象的访问,并且可以在目标方法执行前后做一些操作
public class Main {
public static void main(String[] args) {
SmsService smsService = new SmsServiceImpl();
SmsProxy smsProxy = new SmsProxy(smsService);
smsProxy.send("java");
}
}
动态代理:
- 相比于静态代理,动态代理不需要为每个类都单独创建一个代理类,也不需要必须实现接口(CGLIb),可以直接代理实现类
JDK动态代理 和 CGLib动态代理
JDK:如果目标类实现了接口,Spring AOP会使用JDK来动态代理目标类
- 代理类根据目标类实现的接口动态生成,不需要自己编写
- 生成的动态代理类和目标类都实现相同的接口
CGLib:如果目标类没有实现接口,Spring AOP会使用CGLib来动态代理目标类
- 通过继承实现
- 在运行时动态生成类的字节码,动态创建目标类的子类对象,在子类中对象中增强目标类
- 因为是使用继承来实现动态代理的,如果某被标记为final,那么它是无法使用CGLib代理的
Spring容器是怎么去创建bean并获取bean
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml") // 这一句执行完后,bean就创建了
User user = (User) context.getBean(“user”); // 获取bean
创建bean:
- 非懒加载的bean在容器创建的时候创建
- 容器内部会创建一个beanFactory, 使用 beanFactory 的 doGetBean 方法 创建bean
- 并把创建后的bean放入一个单例bean的map集合中(singletonObjects)
- 这个集合的key就是我们配置的bean的名称
获取bean:
- 当我们调用getBean方法来获取容器时,也会调用beanFactory的doGetBean方法
- 从集合singletonObjects中按照名称取出对应的bean
有哪些注解可以注入Bean
以下5个注释都可以实现Bean的注入,区别在于注入方式和实现方式不一样
- @Autowired:按照类型自动装配
- @Resource:按照名称自动装配
- @inject
- @value
- @Component:用于声明一个bean,类似于xml中的bean标签
注意:注入类的时候最好使用接口类型作为注入对象,避免具体实现类变更导致注入失败的问题
注解的实现原理
注解的用处:
- 生成文档,最常见,最早的注释:@param @return
- 跟踪代码依赖性,实现代替配置问价的功能:@Dagger
- 依赖注入:@Autowire
- 在编译时进行格式检查:@override
注解的实现原理:
- 注解本质是继承了Annotation的特殊接口
- 具体的实现类是java运行时生成的动态代理类
自定义注解如何实现:
- 利用APO切面编程以及反射
- 反射是为了获取Java运行时的相关信息
- AOP切面编程是为了能够在Spring的AOP中直接判断某一个方法是否带有自定义的注解
@Target(ElementType.METHOD) // 指定该注解只能用在方法上
@Retention(RetentionPolicy.RUNTIME) // 指定该注解的生命周期是在运行时
public @interface AuthCheck {
/**
* 有任何一个角色
*
* @return
*/
String[] anyRole() default "";
/**
* 必须有某个角色
*
* @return
*/
String mustRole() default "";
}
元注解:在自定义注解中,专门用了注解其他注解
- @Documented:注解是否将包含在javaDoc中
- @Retention:什么时候使用该注解(定义注解的生命周期)
- @Target:注解用于什么地方
- @Inherited:是否允许子类继承该注解
Spring中的BeanFactory和ApplicationContext有什么区别和联系 ✅
- BeanFactory
- 是Spring框架的基础设施,提供了IOC和DI的功能
- 延迟注入:使用到某个bean的时候才会注入
- ApplicationContext
- 是BeanFactory的扩展,提供了更多的功能和扩展,可以通过多种方式来配置和管理Spring Bean
- 在容器启动的时候,不管有没有用到,一次性创建所有的bean
Spring支持哪几种事务管理类型
- 编程式事务管理
- 声明式事务管理
- 注解式事务管理
拦截器和过滤器 ✅
- 处理。而拦截器是对Handler(处理器)执行前后进行处理。
- 也就是说过滤器拦截的是servlet;拦截器是servlet内部的控件,拦截在具体的方法之前。
Spring、SpringMVC、SpringBoot之间的关系
- Spring是一个java轻量级应用框架,提供了基于IOC 和 AOP的支持
- SpringMVC是Spring框架中用于构建web应用程序的模块
- SpringBoot可以看做是Spring的基础上,通过自动配置和约定优于配置的方法,提供了更简单快速的开发体验。比如:它内置了Tomcat等服务器,不用像传统SSM一样自己去搭环境。
约定优于配置:简单来说,如果你所用工具的约定和你的期待相符,就可以省去配置;不符合的话,你就要通过相关的配置来达到你所期待的结果。
什么是java Bean
bean 可以是三个概念:
- Bean容器:或者叫做Spring IOC容器,用来管理对象和依赖,实现依赖的注入
- Bean对象:根据bean规范写出来的类,并且有bean容器生成的对象就是一个bean对象
- Bean规范:
- 提供一个默认的无参构造器
- 实现 Serializable 接口
- 所有属性都是private的
- 提供getter setter方法
什么是依赖注入
- 在Spring构造对象的时候,把对象依赖的属性注入到对象中
- 有两种方式:构造器注入和属性注入
IOC容器的初始化过程
- 从XML中读取配置文件
- 将bean标签解析为BeanDefinition,比如解析property元素,并注入到BeanDefinition实例中
- 将BeanDefinition注册到BeanDefinitionMap 中
- BeanFactory根据BeanDefinition的定义信息实例化和初始化bean
Bean 的生命周期
- 调用bean的构造方法创建Bean
- .通过反射调用setter方法进行属性的依赖注入
- 如果Bean实现了BeanNameAware接口,Spring将调用setBeanName(),设置 Bean的name(xml文件中bean标签的id)
- 如果Bean实现了BeanFactoryAware接口,Spring将调用setBeanFactory()把bean factory设置给Bean
- 如果存在BeanPostProcessor,Spring将调用它们的postProcessBeforeInitialization(预初始化)方法,在Bean初始化前对其进行处理
- 如果Bean实现了InitializingBean接口,Spring将调用它的afterPropertiesSet方法,然后调用xml定义的 init-method 方法,两个方法作用类似,都是在初始化 bean 的时候执行
- 如果存在BeanPostProcessor,Spring将调用它们的postProcessAfterInitialization(后初始化)方法,在Bean初始化后对其进行处理
- Bean初始化完成,供应用使用,这里分两种情况:
8.1 如果Bean为单例的话,那么容器会返回Bean给用户,并存入缓存池。如果Bean实现了DisposableBean接口,Spring将调用它的destory方法,然后调用在xml中定义的 destory-method方法,这两个方法作用类似,都是在Bean实例销毁前执行。
8.2 如果Bean是多例的话,容器将Bean返回给用户,剩下的生命周期由用户控制。
- 实例化
- 属性赋值
- 初始化
- 使用
- 销毁
设计模式
设计模式的六大原则
- 开闭原则:对扩展开放,对修改关闭,多使用抽象类和接口
- 里氏替换原则:基类可以被子类替换,使用抽象类继承,不使用具体类继承
- 依赖倒转原则:要依赖于抽象,不要依赖于具体,针对接口编程,不针对实现编程
- 接口隔离原则:使用多个隔离的接口,比使用单个接口好,建立最小的接口
- 迪米特法则:一个软件实体应当尽可能少地与其他实体发生相互作用,通过中间类建立联系
- 合成复用原则:尽量使用合成/聚合,而不是使用继承。
单例模式
- 保证在一个进程中,某个类有且仅有一个实例。
- 由于这个类只有一个实例,所以不能让调用方使用new Xxx()来创建实例。所以,单例的构造方法必须是private,这样就防止了调用方自己创建实例
- 饿汉模式:
- 只在类加载的时候创建一次实例,没有多线程同步的问题
- 线程安全,获取单例对象不需要加锁
- 懒汉模式:
- 将对象的创建延迟到了获取对象的时候,为了线程安全,在获取对象的时候需要加锁
- 线程安全,支持延迟加载
工厂模式
工厂用来封装对象的创建
- 简单工厂模式:只有一个工厂类,根据传入的参数不同返回不同的实例
- 工厂方法模式:针对每一个创建的对象都提供一个工厂
- 抽象工厂模式:创建对象家族,一起创建很多相关的对象
模板模式
- 一个抽象类公开定义了执行它的方法的方式/模板。
- 它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。
- 这种类型的设计模式属于行为型模式。
- 定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。
代理模式
代理模式使用代理对象完成用户请求,屏蔽用户对真实对象的访问。
观察者模式
- 观察者模式(Observer),又叫发布-订阅模式(Publish/Subscribe),定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并自动更新。
建造者模式
- 是将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
- 建造者模式是一步一步创建一个复杂的对象,它允许用户只通过指定复杂对象的类型和内容就可以构建它们,用户不需要知道内部的具体构建细节。
Spring中涉及到的设计模式
- 工厂模式:通过BeanFactory或ApplicationContext创建bean对象
- 代理模式:Spring AOP 动态代理
- 单例模式:Spring中bean的默认作用域就是singleton
消息队列
使用消息队列有三个原因:
- 解耦:(订单系统和库存系统)
- 异步:将消息写入消息队列,非必要的业务逻辑以异步的方式运行,不影响主流程业务
- 削峰:消费者慢慢地按照数据库能处理的并发量,从消息队列中慢慢拉取消息
Git 版本控制
git基本命令
避免冲突的合并代码流程:
- clone:将远程代码拉到本地进行修改
- add:将修改后的代码提交到暂存区
- commit:将暂存区中的代码提交到本地仓库
- pull:将远程仓库中的最新代码拉到本地进行合并,适应新代码可能带来的冲突。(pull=fetch+merge)
- push:将修改好的代码上传到远程仓库