01 – 异常相关
相关的类
Throwable
Exception
- 受检异常
IOException
SqlException
RuntimeException
Error
使用相关
Error
通常是硬件的错误,与代码无关,不应该捕获
Exception
-
RuntimeException
是由程序逻辑导致的
-
CheckedException
程序在运行前无法预料到,为了程序的健壮性,运行时必须捕获
处理方式
try .... catch
throws
日常使用方式
- 异常必须处理
- 异常是逻辑的补充
- 受检异常不足
- 类的变更会导致调用者破坏了类的封装性
- 降低了类的可读性
- 一般将异常转化为运行时异常去处理,全局异常的统一定义
02 – 强引用、软引用、弱引用、虚引用
目的
- 了解生命周期
- 不用的场景使用不同的引用类型,避免 OOM 异常
强引用-strongReference
- JVM 不会回收这个对象,内存不足,抛出 OOM 异常
- 只有在类被卸载的时候被销毁,进程结束的时候被卸载
- 两种回收情况
- 脱离了作用域之后,就会被回收
- 这个对象设置成 null,就会被回收
软引用-softReference
- 使用方式
- 套用
SoftReference
- 套用
- 回收情况
- 程序发生 OOM 异常
- 对内存 2G,生于 1024 * 1000 秒之内,没有调用,被回收
弱引用-weakReference
- 回收情况
- 设置成 null , GC
- 注意
- 字符串在常量池里边,不会被回收
虚引用-phantomReference
- 我们可以知道这个对象将要被回收了,去做一些事情
* 重点
- 内存重组的情况下,强引用、弱引用都不会被回收
- 内存不足的情况下,软引用才会被回收,避免 OOM 异常
03 – 一个方法交换两个 Integer 值
JAVA 传递
分类
- 值传递
- 引用传递
注意
- 传递基本数据类型,传递的是原始数据的一个副本,改变副本,不改变原始的值
- 传递引用类型,函数接收到的是原始引用类的内存地址,可以修改原来的值
我们引用都是存在堆里边,对象存储在堆中。
做题
-
需要修改值
-
private final int value
-
反射改变原来的对象的值
-
field.setAccessable(true)
可以绕过检查
-
-
-
Integer 有缓存
- -128 ~ 127 都在内存中
- 为了提高程序的侠侣,减少内存的分配
Integer a = 1;
- 调用
Integer.parseInt()
- 得到缓存中的地址
- 调用
- -128 ~ 127 都在内存中
总结
- 函数的值传递、引用传递
- 对象的引用传递传递的是一个内存地址
- 反射的可访问性
field.setAccessable(true)
- Java 编译器的自动装箱,自动拆箱
- Integer 的缓存
04 – 动态代理原理分析
考察点
- 动态代理本身机制的理解
- 了解 JVM 的运行原理
目的
- 提供一个代理
- 控制对真实对象的访问
- 做消息的预处理、过滤、日志 等,还有消息的后续处理
- 增强程序的灵活性
一种设计模式
总结
- 访问代理对象来访问真实对象
- 间接性,可以加入处理
- 可以协调调用者和被调用者的关系,减低系统的耦合度
- 作为客户端和目标对象的中介,可以起到保护真实对象的作用
实际场景
- 预处理
- 身份认证
- 日志
静态代理与动态代理
静态代理
- 缺点
- 一个代理只能代理一类对象,代理多个类,需要多个接口
- 类膨胀
- 代理对象中存的是目标对象的引用
- 一个代理只能代理一类对象,代理多个类,需要多个接口
动态代理
- JDK
- 是西安
InvocationHandler
接口,重写#invoke()
方法
- 是西安
- CGLB
- 对类代理,生成一个子类
- 利用父类——》》子类的关系,实现拓展
总结
- JDK 的动态代理是通过接口的方法名在动态生成的方法名里变更,去调用业务实现类的同名方法去实现拦截的。
- CBLG 使用继承我们具体的需要被代理的业务类,去动态的生成业务类的子类,去重写业务方法,进行代理
05 – 设计模式的优雅落地
体现
- 思想
- 代码的可拓展性和灵活度
设计原则
单一职责原则
- 优点
- 类的复杂度见底了
- 类的可读性提高了
- 类的可维护性提高了
- 变更引起的风险降低了
- 缺点
- 增加了一些类,导致后期的类的维护变得复杂
里氏替换原则
- 子类可以拓展父类的功能,但不能改变父类原有的功能
- 只要有父类出现的地方,子类就可以出现,替换以后没有错误和异常
- 如果子类不能完全的实现父类的方法,或者父类的方法改变了,建议不要用父子类关系,可以用依赖、组合、聚合 等方式。
依赖倒置原则
- 通过抽象使各个类实现彼此独立,不相互影响,实现松耦合
- 核心就是依赖倒置原则
接口隔离原则
- 接口最好细化,提供最小力度的方法
开闭原则
- 对扩展开放,对修改关闭
迪米特法则
- 一个类对于自己依赖的类,知道的越少越好。对于自己依赖的类,逻辑要封装到类的内部,对外提供一个 public 方法
- 应用
- 迪米特法则
设计模式
策略模式
- 应用场景
- 事项某一个功能需要算法、策略
- 根据环境或者策略选择不同的算法
- 总结
- 就是对算法的一个包装,把算法的责任和算法本身分割出来,委派给不同的对象做管理
- 例子
- 支付
- 收银台
- 网银支付
- 微信支付
- 信用卡支付
- 收银台
- 思想
- 定义一个工厂类
- 根据不同的策略生成不同的对象
- 调用同一个接口
- 支付
适配器模式
- 例子
- 上传文件
- 阿里云的上传
- 亚马逊的上传
- 代码实现
- 统一的service接口
- 阿里云的适配器
- 阿里云的 SDK
- 亚马逊的适配器
- 亚马逊的 SDK
- 阿里云的适配器
- 统一的service接口
- 上传文件
工厂模式
06 – 从底层分析 List 和 Set 的区别
- List
- 有序允许重复
- Set
- 不允许重复
arrayList特性
- Object[]
- 可变的动态数组
- 1.5 倍扩容
- 删除时,右边的元素全部左移一位
transient Object[] elementData
- 它的值不可以被序列化又重写了 readObject() 方法,大于 0 的时候,会对它进行序列化
- 三种构造方法
- 带参数的构造
- 不带参数的构造
Collection<? extends E>
构造
- 默认的初始化大小
DEFAULTCAPACITY_EMPTY_ELEMENTDATA = 10
- 只有在第一次添加的时候,才知道初始的大小
Set
HashSet
HashSet
就是对HashMap
的一个包装- 无序性
- Key 唯一性
TreeSet
TreeSet
就是对TreeMap
的一个包装
07 – 从底层分析ClassLoader加载机制
背景
- 不但要使用技术,还要知道原理
- 写好
Java
文件,编译成Class
,组成Java
程序,运行 - 一个
class
调用另一个Class
,找不到汇报ClassNotFoundException
- 通过程序的需要,通过类加载机制实现动态的加载
类加载器
- 职责
- 把 Class 加载到 JVM 内存里面
- 根据类的名称找到i字节码,然后从字节码中定义一个 Java 类
- 把 Class 加载到 JVM 内存里面
- 种类
- 启动类加载器
- 扩展类加载器
- 系统类装载器
- 自定义装载器
JVM
装载所有的 .class
文件到内存里边
-
类装载器
-
验证
验证我们的字节码是不是符合 JVM 规范,让它造成安全问题,验证失败会报
NoclassDefFoundError
- 文件格式验证
- 原数据验证
- 字节码验证
- 符号验证
-
准备
- 为变量去分配内存
- 设置类变量的初始化
- 静态变量的生命周期和类是绑定的,叫他类变量
-
解析
- 把常量池中的符号引用替换成直接引用
- 类接口的解析
- 字段解析
- 方法解析
- 接口方法解析
- 把常量池中的符号引用替换成直接引用
-
-
初始化
-
使用
-
卸载
划分职责
- 启动类加载器
- 扩展列加载器
- 系统类加载器
类装载器
- BootStrap Classloader
JRE\lib\rt.jar
、Xbootclasspath
指定的 jar 包
- extension Classloader
JRE\lib\ext\*.jar
、Djava.ext.dirs
指定的 jar 包
- application Classloader
Load CLASSPATH
、Djava.class.path
指定的 jar 包
- Custiom Classloader
加载过程
- 自底向上检查类是否已经加载
- 自顶向下尝试加载类
加载
- 双亲委派模型
- 加载一个类会已成已成的往上加载,看不看在这个职责范围内告诉下一级,不是我的。
- 通过分层的方式,并且划分职责,能够圈定哪些类应该在哪个职责去加载
- 意义
- 安全性
- 对于我们核心功能产生影响的类,如果随意定义一个类加载器都可以加载就会造成影响
- example
- 定义一个包
java.lang
定义一个类String
这个自定义String
中的方法是找不到的
- 定义一个包
- 分层性
- 分层次的加载能够让我们的加载变得很清晰,而不会变得很混乱
- 统一性
- Java 类的加载顺序随着它的加载机制存在一种优先级的层次感
- 安全性
ClassLoader
源码
- 如果父类不为空,就交给父类去加载
parent.loadClass(name, false)
- 如何判断两个类是不是相同的
- 加载器是不是一个
包名.类名
是一样的,一个类才是相同的
一个类的实例化顺序
- 没有父类
- 静态变量,静态初始化块
- 变量,初始化块
- 构造器
- 有父类
- 父类的 static 方法
- 子类的 Static 方法
- 父类的构造方法
- 子类的构造方法
08 – 关于深克隆和浅克隆
背景
- 克隆是在内存中,相比于 new 一个对象再去赋值,性能上好一点
- Java 的一些源码用到了克隆
类别
- 深克隆
- 浅克隆
定义
- 克隆是为了快速的构造一个和原有对象一摸一样的副本
使用
- 实现 Cloneable() 接口
- 重写 clone() 方法
源码
protected native Object clone() throws CloneNotSupportedException
native
表示是调用 C 语言的实现
注意
- 浅克隆
- 如果复制的对象都含有原来对象相同的值,而对所有其它对象的引用,仍然指向原来的对象
- 深克隆
- 父类子类都需要实现 Cloneable() 接口
09 – 如何将一个对象序列化到文件中
背景
- 序列化一般封装在底层
- 用途
- 加密
- 持久化
- 意义
- 序列化和反序列化的选型
- 提高通用性、健壮性、安全性
- 更加方便去扩展
- 序列化允许我们跨 JVM 传输
- 网络传输 Java 对象
- 序列化为 二进制文件
- 网络传输
- 反序列化为 java 对象
评价一个序列化好坏的标准
- 序列化以后数据的大小
- 序列化本身计算的速度
- CPU、内存的开销
* 当架构非常大,流量非常多的时候,资源的开销就会变得非常的珍贵
序列化技术
- Java 原生序列化
- 缺点
- 序列化以后数据比较大
- 不支持跨语言
- 缺点
- XML
- JSON
- Hessian
- protobuf
- avro
- kyro
使用
-
当有多个 transient 修饰的属性的时候,需要按照顺序,读取,按照顺序,写入。
-
serialVersionUID
- 最好设置这个,设置以后不对的话会报错
10 – 乐观锁和悲观锁的原理和作用
背景
-
锁
-
在 CPU 层面下引入的高速缓存,在多核心的 CPU 下,多线程并行执行的时候,会导致缓存一致性问题
-
引入总线索或者缓存锁去保证缓存的一致性。
锁的核心理念就是一种同步机制 在多线程并行执行的时候对共享资源访问的安全性。
-
-
分类
- 重入锁
- 独占锁
- 共享锁
- 读锁
- 写锁
数据库锁
乐观锁
-
它认为我们所做的事情是乐观的,每一次获取一些共享资源的数据或者去对数据做一些修改的时候,会认为这个时候不会有所得冲突,不会主动去对共享资源加锁
-
使用
- 一般加一个 version 字段
- 然后写入的时候,进行判断处理,
悲观锁
- 类似于 Java 中的 synchronized
- 悲观锁效率比较低
- 缺点
- 开销大
- 系统负载高
- 降低系统的并行
乐观锁和悲观锁怎么应用
区别
- 一个先加锁再访问
- 一个更新的时候,再去重试
在合适的场景下运用合适的技术
- 乐观锁适用于读多写少的情况下,提高系统的吞吐量
- 如果锁冲突比较多的话,直接采用雷管所,它会不断地去
#retry()
,会减低了系统的性能
11 – BIO-NIO-AIO
种类
-
BIO
NIO AIO
背景
- 传统的网络通信的代码
- server
- client
- JDk 1.4 之前
java.io
- 文件流
- 网络流
- 文件流
java.net
定义
- 同步
- 是否要亲自的去监听操作,不断地关注是否完成
- 阻塞
- IO 操作没有完成,在单线程情况下没有办法进行 IO 操作
过渡时期
- 每次来一个客户端的 socket 连接,都针对这个 socket 连接创建出一个线程
- 多线程解决了多个客户端同时连接的情况
BIO
- 过程
- 阻塞
- 来一个
new Thread()
Executors.newFixedThreadPool(60)
- 标记连接,设置 flag,表示是否创建线程
- 问题
- 多个客户端连接,服务端的线程数量肯定会增加
- 改良
- 客户端连接上来的受 Socket
- IO 操作的时候才去创建线程
NIO
Non-Blocking IO
同步非阻塞 IO- 引入 selector 的概念
- channel 消息通道
- selector 做一个等级,需要 IO 操作的时候,采取 new Thread()
阻塞,非阻塞
- 阻塞
- 单线程下,IO 没有完成,那么当前线程就会一直等待
- 非阻塞
- 不管你IO 操作有没有完成,我都会直接去返回,或者抛出一个异常了才取返回
优化
- 引入缓存
- 解决读操作,写操作的间断性!!!
buffer
是一个数组的形式selector
要和serverSocketChannel
进行绑定的
AIO
- 异步
- 我触发了 IO 操作以后,就不再关心 IO 操作委托给系统,系统完成以后,在告诉自己 OK
12 – Spring中对象注入的几种方式和区别
背景
- Java 中一个类需要依赖其它类的时候,需要通过
new()
来进行实例化,不好管理,各个类之间的耦合度非常的高
Sping
- 依赖注入
- 就像对象的配方一样,定义好以后,直接就可以用
- 注入方式
- Set
- 构造
- 静态工厂的方法注入
- 目前
- 用注解的方式比较多,可能就没又考虑到 构造注入 的方式了
- XML
- Set 注入
- ref 引用
- 构造
- 静态工厂的方法注入
- Set 注入
- 使用
- 生命周琪
- 定义的
Bean
类似于模板 bean
的名称可以通过ref
引用过来- 可以配置对相对应关系
- 模式
- 原型模式
Prototype
- 单例模式
Singleton
- 原型模式
13 – Mysql数据库的隔离级别及其区别
事务
-
定义
- 事务是应用程序中对数据库的一系列严谨的操作,所有操作必须成功过着失败。
-
特性 ACID
-
A
-
原子性
要不全部成功,要不全部失败
-
-
C
-
一致性
一致性状态变成另一个一致性状态,不会出现不一致的情况
-
-
I
-
隔离性
并行事务之间合理不能相互干扰
-
-
D
-
持续性
事务一旦提交,那么它对数据库的修改是永久性的
-
-
数据库并发
- 问题
- 脏读
- 事务A 读了 事务 B 未提交的数据
- 不可重复度
- A多次读取同一条数据,B 对这条数据进行了修改,提交了,A 独到的不一样
- 幻读
- A重复读取一条数据,B 中间对着条数据进行了新增或者删除
- 脏读
- 重点
- 不可重复度侧重的是修改
- 幻读侧重的是新增和删除
隔离级别
-
隔离我们事务之间的影响
-
种类
read uncommitted
- 读取未提交的数据(脏读,很少使用)
read committed
- 读取已提交的数据(解决了脏读,出现不可重复度)
repeateable read
(默认的)- 并发的时候,看到相同的数据行,就会出现幻读,一般通过 innoDB 里边的共享锁和独占锁,还可以通过多版本并发控制的 MVCC 的机制
serializable
- 严格按照顺序,执行,一个事务执行,另一个事务只能挂起
18 – HashMap源码面试题分析
数据结构
- key,value
- 数组 + 单项列表
HashMap
-
Hash 算法的作用
-
目的: 为了 Node 节点的一个下标的计算
-
要求
- 根据 hash() 算法得到一个整形数
- 控制在 0 ~ 15 之间
-
-
默认大小
DEFAULT_INITIAL_CAPACITY = 1 << 4
-
Hash 算法
- 右移 16 位
- 互相 异或 得到一个 hash 值
put 的过程
-
先 HASH 算法
-
初始化数组大小,
判断当前的
Node
节点的数组是否为null
空 -> 数组的默认大小 16 赋值给 newCap
-
根据 key, value 组装成 Node 节点,计算出下标的位置
- 和 &(n-1)取余
- 数组大小 16 , 对 15 &
-
得到下标位置以后
- key 值相同,只需要 value 值进行覆盖就行了
- 红黑树(JDK 1.8 开始引入红黑树)
- 链表的形式,循环遍历,next 为 null,把这个节点放进去
判断数组的容量大小的改变
-
扩容
-
扩容因子(负载因子)
-
两倍扩容
-
数组的最大长度
MAXIMUM_CAPACITY = 1 << 30;
- 2^30^
-
扩容以后
数据迁移
- 数组有元素,下面为 null
- 数组位置有元素,下面不为 null, 红黑树
- 重新拆分,重新打散
- 数组有元素,下面不为 null,链表
19 – 如何停止一个线程
如何停止
-
stop()
- 强制停止,不建议适用
-
正常
-
interrupt
-
打个招呼,你这个线程中断执行了
停止不停止,线程自己去判断
-
-
stop
- 标志
-
20 – Thread.join的实现原理
怎么启动
-
普通写法
-
1 2 3
thread1.start() thread2.start() thread3.start()
-
-
start() 方法,只是告诉 JVM 可以去驱动一个线程了,但是什么时候执行,取决于 OS 的调度线程。
join
-
1 2 3 4 5 6
thread1.start(); thread1.join(); thread2.start(); thread2.join(); thread3.start(); thread3.join();
-
join()
方式是去等待线程的执行结果。 -
join
方法就是主要用来阻塞一个线程,拿到一个线程的执行结果,或者等待 -
wait(0);
是让主线程去等待
21 – ThreadLocal的实现原理
场景
- 多线程对同一个变量做一个递增的时候
threadLocal
- 提供了线程的局部变量
- 访问
ThreadLocal
里的某一个变量的值的时候,都会拥有一个线程内局部变量的副本, - 利用
ThreadLocal
使得多线程之间不互相影响
Java 传递
- 值传递
- 引用传递
ThreadLocal 对每个线程来说都是一个独立的副本。对于每一个线程来说,拥有的副本都是独立的。
22 – 由浅入深掌握volatile关键字的原理
volatile
- 内存可见性问题
- 内存屏障
原理
- 加了 volatile 一个线程修改的值,会影响到子线程的流程 子线程就会拿到一个最新的值,去判断。
- 是通过内存屏障和编译器屏障完成这个编译器
- java 层面四种内存屏障
- loadload
- storestore
- loadstore
- storeload
- 内存屏障
- 就是两个操作之间,读操作或写操作之间,加入响应的屏障,定义了处理的同步点,同步点之前的指令必须全部执行完
- lock
- CPU 级别的 汇编指令
- 作用
- 使 CPU 的缓存行的数据写回到主内存里
- 使其它 CPU 核心里边该数据的内存无效
volatile
- 通过内存屏障达到我们可见性的效果
面试回答
- volatile 是去保证可见性的
- 可见性表现在不同线程直接按
- 这个值表更以后,对其它线程的可见性,
- 通过内存屏障实现
- 防止指令重排序
23 – 线程池的原理分析
背景
- 数据库连接池作用
- 避免了频繁的创建线程来造成极大的开销
- 通过控制线程池达到限流的目的
- 当线程池的数量超过我们限制的数量的时候,会抛出拒绝的异常。
线程池的种类
Executors.newSingleThreadExecutor()
- 构建只有一个线程的线程池
newFixedThreadPool(1)
- 返回一个固定线程数量的线程池
newcachedThreadPool()
- 返回一个不限制大小的线程池
- 但是创建的这些线程,都有一个回收机制,空闲 60 s后,会自动被回收
newSinglethreadScheduledExecutor()
- 是可以延时执行的,我们可以通过它做一些定时调度
线程池顶层接口 ThreadPoolExecutor
int corePoolSize
- 核心线程数
int maxiumPoolSize
- 最大线程数
long keepAliveTime
- 非核心线程数空闲时的活跃时间
TimeUnit unit
- 存活的单位
BlockingQueue<Runable> workQueue
- 阻塞队列
ThreadFacotry threadFactory
- 线程池中构建线程的工厂
RegectedExecutionHandler handler
- 一个拒绝策略
线程池执行的步骤
-
调用 execute() 去构建核心线程数、最大线程数
- 阻塞队列有阻塞策略,主线程调用 execute() 的时候,如果线程池中还不到和核心线程的个数,就会创建一个线程
-
如果核心线程数超过了定义的核心线程数 就会放到阻塞队列中,不断地去除数据去运行
- 队列添加失败,就是队列满了,然后就会创建非核心线程,如果线程满了,就会走拒绝策略
- 核心线程和非核心线程的区别
线程怎么运行的呢?
-
是由
worker
的方式去定义线程的。 - 不是直接运行这个线程,而是调用这个线程的
run()
方法。 - 在
ThreadLocalPool
里面构建一个worker
,调用我们的task
,调用里边的Thread
24 – 缓存穿透的原理以及解决方案
Redis 做缓存
- 单线程模型
- 如果保证 redis 和 mysql 的一致性
- 缓存雪崩
- 缓存击穿
场景
- 如果缓存数据很少的话,就会由很多请求都落到数据库层面
- 数据库 IO 和访问量就会很大,导致数据库宕机
- 查询数据库中都没有的数据,倒是不命中,叫做缓存穿透
解决方案
-
先查询数据库为空,然后,设置一些规律,传过来 value
value -> && __ 告诉程序这个数据不存在
过段时间在查询数据库
布隆过滤器
-
原理
- 概率性算法和数据结构
-
使用
- 判断一个元素在集合中是否存在
-
位图算法
-
位图j就是一个 int 型的数据,
-
一个整形可以存储 32 个数据,代表一个标志,
hash 以后,取模,然后落上去,
如果 这个数 hash 一下,落上去 1, 就可能存在, 0 就是不可能存在
-
25 – 什么是幂等-如何实现
什么是幂等,如何实现
- 系统之间相互调用三种状态
- 成功
- 失败
- 未知
问题
-
在未知的场景下,服务端已经收到了请求,但是客户端不知道,
他再次请求,导致了数据做了两次累加
幂等的实现
- 数据库唯一索引
- 收到请求,添加一条记录,
- 如果第二次过来,没办法入库
- 状态机幂等
- 状态流转有固定的流程
- 一定是一个状态才能改变成下一个状态
幂等的使用
- 不管你客户端调用多少次,我跟第一次的处理结果都是一样的,这就是幂等
- 消息中间件必须实现 幂等。 因为 消息中间件无法保消息只发送一次,怎么保证数据安全,需要幂等。
26 – 数据库和缓存双写如何保证数据一致性
场景
-
需要根据不同的场景,和对数据一致性的容忍度去做不同的权衡,
所以要提供相应的思路
-
转钱
选择
- 先更新缓存中的数据,再更新数据库
- 数据库更新失败怎么办?
- 修改数据库中的数据以后,在更新缓存中的数据
- 数据的脏读
两个要点
- 先更新缓存,还是让缓存失效
- 先操作数据库,还是先操作数据库
面试正确回答
- 更新缓存,表示数据不但写入到数据库,还写入到缓存,
- 缓存失效表示,更新数据库,删除缓存中的key
- 更新缓存代价小,先更新缓存
- 如果更新缓存代价大,就先让缓存失效
- 我们根据具体的业务,选择具体方案
最终一致性
- 缓存中间件保证最终一致性
两点
- 一个是更新缓存的代价大小
- 一个是对业务的容忍程度