Java面试
2020-12-10 09:49:21 49 举报
AI智能生成
登录查看完整内容
java面试
作者其他创作
大纲/内容
算法
排序算法
比较排序
交换排序
冒泡排序
快速排序
插入排序
简单插入排序
希尔排序
选择排序
简单选择排序
堆排序
归并排序
二路归并排序
多路归并排序
非比较排序
计数排序
桶排序
基数排序
查找算法
二分查找
顺序查找
插值查找
斐波那契查找
树表查找
分块查找
哈希查找
Spring
spring创建出来的对象作用域
singleton
spring容器初始化时创建对象,设置lazy-init为true时,调用getBean时才会创建对象
prototype
request
为每个请求创建一个对象
session
为每个session创建一个对象
优点
IOC
控制反转,别名依赖注入
由 Spring IOC 容器来负责对象的生命周期和对象之间的关系
Inversion of control
AOP
定义
分散在各个方法中的重复代码提取出来,然后在程序编译或运行时,再将这些提取出来的代码应用到需要执行的地方。这种采用横向提取机制的方式。
动态代理方式
CGLIB代理
类
JDK动态代理
接口
Java AOP框架
AspectJ
区别
与SpringAOP
编译时增强
基于字节码操作
SpringAOP
运行时增强
基于代理
实用
日志
事务
权限
异常捕获
传播机制
REQUIRED
NOT_SUPPORTED
容器不为这个方法开启事务
REQUIRES_NEW
MANDATORY
NEVER
SUPPORTS
线程安全问题
发生在单例Bean中,当多个线程访问对象的非静态成员变量时,写操作会有线程安全问题。
解决
ThreadLocal将变量保存起来
如何解决循环依赖的问题
通过三级缓存
构造器方式的循环依赖
描述
无法解决
BeanCurrentlyInCreationException
类属性的循环依赖
基于引用传递
如何为我们创建对象
1.通过构造器创建对象
2.通过静态工厂创建
3.通过实例工厂创建
SpringMVC
SpringMVC的工作流程
1.用户发送请求至前端控制器DispatcherServlet
2.DispatcherServlet收到请求调用HandlerMapping处理器映射器
4.DispatcherServlet调用HandlerAdapter处理器适配器
5.HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。
6.Controller执行完成返回ModelAndView
7.HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet
8.DispatcherServlet将ModelAndView传给ViewReslover视图解析器
9.ViewReslover解析后返回具体View
10.DispatcherServlet根据View进行渲染视图
11.DispatcherServlet响应用户
注解面试点
Component注解与Bean注解的区别
Component注解注解在类上,表示Spring将会为这个类创建bean
Mysql
隔离级别
默认
可重复读
这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。
问题
幻读
幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。
InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题
模式
快照读
读取的是记录的可见版本
当前读
实现技术是mvcc
防止幻读
四种
Read Uncommitted(读取未提交内容)脏读
啥也不做(查询也不加锁)
导致脏读,不可重复读,幻读
脏读
所谓的脏读,其实就是读到了别的事务回滚前的脏数据
。比如事务B执行过程中修改了数据X,在未提交前,事务A读取了X,而事务B却回滚了,这样事务A就形成了脏读。
不可重复读
Read Committed(读取提交内容)大部分数据库的默认隔离级别
一个事务只能看见已经提交事务所做的改变。这种隔离级别 也称所谓的不可重复读(Nonrepeatable Read)
使用快照,避免脏读,但避免不了不可重复读,幻读
当前事务先进行了一次数据读取,然后再次读取到的数据是别的事务修改成功的数据,导致两次读取到的数据不匹配,也就照应了不可重复读的语义
发生在update,delete中
Repeatable Read(可重复读)
事务A在读到一条数据之后,此时事务B对该数据进行了修改并提交,那么事务A再读该数据,读到的还是原来的内容。
读快照,不可修改,但避免不了新增删除,所以会产生幻读
事务A首先根据条件索引得到N条数据,然后事务B改变了这N条数据之外的M条或者增添了M条符合事务A搜索条件的数据,导致事务A再次搜索发现有N+M条数据了,就产生了幻读。
insert中
MVCC与Next-Key
Serializable(可串行化)
这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上表级共享锁。在这个级别,可能导致大量的超时现象和锁竞争。
两种事务隔离技术
mvcc
mvcc的优势是不加锁,并发性高。缺点是不是实时数据。
next-key
next-key的优势是获取实时数据,但是需要加锁
索引
数据结构
B+tree
支持范围查询
Hash
范围查找不支持
创建时候可以选择创建什么数据类型的索引
为何用B+tree
1.平衡二叉树的话数据一多,树高就高,增加IO次数,不利于查询
2.为何不用B树
一张表最多可以建多少个索引
虽然提高查询效率,牺牲插入,删除性能,会索引重排
对唯一性较高,经常不发生变更的,经常查询的字段可以建立索引
比如电话号码
索引失效
SQL语句写法
or
union
优化查询,调优
explain执行计划,查看各方面的数据,或建索引
为什么推荐InnoDB表必须有主键?
保证会有主键索引树的存在(因为数据存放在主键索引树上面),如果没有mysql会自己生成一个rowid作为自增的主键主键索引
为什么推荐使用整型的自增主键?
方便查找比较
新增数据的时候只需要在最后加入,不会大规模调整树结构
MyISAM与InnoDB
区别一
MyISAM
表相关文件有三个
表数据与索引文件分开存储
InnoDB
表相关文件有二个
索引与数据在同一个文件中 (.idb)
区别二
适合高并发,重负荷
表级锁,不支持事务
一般小项目可以使用
锁
全局锁
表锁
行锁
间隙锁
读写锁
InnoDB如何加锁
锁机制
共享/排它锁
共享锁(Share Locks,记为S锁),读取数据时加S锁
共享锁之间不互斥,简记为:读读可以并行
排他锁(eXclusive Locks,记为X锁),修改数据时加X锁
排他锁与任何锁互斥,简记为:写读,写写不可以并行
普通的select仍然可以读到数据(快照读)。
意向锁
表级别的锁
InnoDB为了支持多粒度锁机制(multiple granularity locking),即允许行级锁与表级锁共存,而引入了意向锁(intention locks)。意向锁是指,未来的某个时刻,事务可能要加共享/排它锁了,先提前声明一个意向。
记录锁
记录锁,它封锁索引记录
它封锁索引记录中的间隔,或者第一条索引记录之前的范围,又或者最后一条索引记录之后的范围。依然是
临键锁
记录锁与间隙锁的组合,它的封锁范围,既包含索引记录,又包含索引区间。
插入意向锁
自增锁
自增锁是一种特殊的表级别锁(table-level lock),专门针对事务插入AUTO_INCREMENT类型的列。
MVCC
多版本并发控制
MVCC只在RC和RR两个隔离级别下工作,其他两个隔离级别都和MVCC不兼容
目的
为了实现数据库的并发控制而设计的一种协议
Redis
单线程
有种情况严格意义上并不是单线程的
RDB时(命令:bgsave)
会Fork出一个子线程在后台备份数据,但还是可以操作redis
String字符串
hash哈西
set集合
zset有序集合
SortSet
排行榜
list列表
还有三种特殊的
bitMap
Geo
HelperLogLog
5.0
Stream
Redlock(红锁)
为何这么快
Redis处理的这些请求都是计算型
纯内存操作
带有I/O多路复用功能
持久化
1.RDB
隔一段时间将内存快照写入硬盘
2.AOP
同时开启
AOP数据全一点但是速度慢,RDB速度快但数据不全,一般应急都先用RDB先恢复大部分,然后使用AOF补全剩下的数据
集群(高可用)
主备同步
启动数据初始化
rewrite
INCR命令(分布式锁)
分布式锁框架Redission
Sub/Pub
BitMap
常见问题
双写一致性
面试
如何保证缓存与数据库的双写一致性?
1.读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
2.更新的时候,先更新数据库,然后再删除缓存。
最初的缓存不一致问题
先更新数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致。
先删除缓存,再更新数据库。如果数据库更新失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。因为读的时候缓存没有,所以去读了数据库中的旧数据,然后更新到缓存中。
复杂的数据不一致问题分析
数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。随后数据变更的程序完成了数据库的修改。完了,数据库和缓存中的数据不一样了
出现
每秒并发读是几万
<左侧灰色框>
简单也可以说
双删操作处理
先删除缓存,再删除数据库,过1s后再删除缓存,防止删除数据库数据时,数据又写入缓存
并发竞争
大Key
热点Key
缓存击穿
1.定时更新
后台设置一个Job定时去刷新缓存的过期时间
2.检查更新
将过期时间一并存入缓存,获取到后,判断是否小于一定时间,小于后,在更新过期时间,同1
3.分级缓存
4.加锁
5.简单的将热点key设置成永不过期
缓存穿透
指有人用数据库中不存在的某个key访问,数据库中没有该key值,自然也不会写入缓存中,所有该请求的查询都会直接到数据库。如果对该key的并发访问量过大,则会压垮数据库。
从请求出发
对查出为空的key,也在缓存中建立key value对,只是过期时间设的短一点,比如5minetes。
从数据库出发
采用过滤器,把所有数据库中不可能存在的数据hash到一张大的bitmap中,如果key在数据库中不存在,将会被bitmap拦截。
布隆过滤器
对可能的key做一定的hash并存储在Bitmap里,不存在就过滤掉,存在的就按正常流程走
缓存雪崩
缓存雪崩,是指在某一个时间段,缓存集中过期失效。
1.分散缓存过期时间
如果是热点key的话就设置永不过期
致命的缓存雪崩,是缓存服务器某个节点宕机或断网
对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。
使用场景
不适合
数据量太大、数据访问频率非常低的业务都不适合使用Redis。
例子
计数器/限速器
点赞;收藏数;请求限流
好友关系
集合,并集,交集
简单消息队列
除了Redis自身的发布/订阅模式,我们也可以利用List来实现一个队列机制
到货通知、邮件发送之类的需求,不需要高可靠,但是会带来非常大的DB压力,完全可以用List来完成异步解耦
Session共享
访问的uv统计
HelperLoglog
网络层使用epoll解决高并发问题
RPC
Docker
微服务
Spring Cloud
Eureka
服务注册流程
Zuul
网关
feigh
hytrix
熔断器
判断什么时候回熔断
ribbon
负载均衡
负载均衡的策略有多少种
轮询
权重
Dubbo
调用链路
Kafka
RabbitMQ
Vhost
作用
应用隔离
消息基于什么传输
信道
信道是建立在真实的TCP连接内的虚拟连接,且每条TCP连接上的信道数量没有限制
为何不基于TCP
TCP连接的创建和销毁开销较大,且并发数受系统资源限制,会造成性能瓶颈
消息如何分发
若该队列至少有一个消费者订阅,消息将以循环(round-robin)的方式发送给消费者。
名词
Publisher
routingKey
exchange
bindKey
queue
Customer
Nginx
单个IP访问频率过多拉黑设置
过滤IP方式
1.过滤单个IP
2.过滤地址段
3.过滤所有地址
4.只允许单个IP
一致性Hash
加权
MongoDB
什么是MongoDB
MongoDB是一个文档数据库,提供好的性能,领先的非关系型数据库。
存储结构
BSON
什么是BSON
一种类似JSON的二进制的存储格式
与JSON的区别
多了date类型与二进制的数据
NoSQL
Not Only SQL
存储结构易扩展
查询速度更快
GridFS
存储大量小文件功能
例如: 存储用户头像
一条文档最大16M
分片技术
分片是将数据水平切分到不同的物理节点。当应用数据越来越大的时候,数据量也会越来越大。当数据量增长时,单台机器有可能无法存储数据或可接受的读取写入吞吐量。利用分片技术可以添加更多的机器来应对数据量增加以及读写操作的要求。
支持的数据类型
支持主键外键关系
默认是不支持的,需要硬编码,实现难度较大且不够直观
基本命令
创建索引
db.collection.createIndex()
查询
db.collectionName.find({key:value})
更新
删除
db.collection.remove()
场景
1.随时应对动态增加的数据项时可以优先考虑使用NoSQL数据库
2.在处理非结构化/半结构化的大数据时
3.在水平方向上进行扩展时
MongoDB支持存储过程吗?
MongoDB支持存储过程,它是javascript写的,保存在db.system.js表中。
索引
一些查询指令
大于小于
$gt $lt $gte $lte
数组包含
{$elemMatch:{$eq: key }} {$elemMatch:{$eq: {name: 1 } }}
不支持事务
更多的是在代码层面去对事务有所实现
设计模式
1.单例模式
懒汉
饿汉
双重检查
volatile
2.工厂模式
3.代理模式
4.策略模式
5.状态模式
6.观察者模式
7.模板模式
8.建造者模式
9.适配器模式
10.装饰器模式
JVM
内存泄漏
该被释放的对象没有释放,一直被某个或某些实例所持有却不再被使用导致 GC 不能回收。
1.尽早释放无用对象的引用 好的办法是使用临时变量的时候,让引用变量在推出活动域后自动设置为null,暗示垃圾收集器来收集该对象,防止发生内存泄漏。
2.程序进行字符串处理时,尽量避免使用String,而应该使用StringBuffer。 因为String类是不可变的,每一个String对象都会独立占用内存一块区域。
3.尽量少用静态变量 因为静态变量是全局的,存在方法区,GC不会回收
4.避免集中创建对象,尤其是大对象,如果可以的话尽量使用流操作
5.尽量运用对象池技术以提高系统性能
6.不要在经常调用的方法中创建对象,尤其忌讳在循环中创建对象
7.优化配置
达到一定质变即会引起内存溢出
java程序运行较慢
GC 为了能够正确释放对象,GC 必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC 都需要进行监控。
垃圾回收
垃圾回收器
CMS
G1
回收机制
标记-清除算法
标记-整理算法
标记-复制算法
分代回收
年轻代
老年代
永久代/元空间
内存模型
堆
栈
方法区
本地方法栈
程序计数器
java类的加载机制
1.代码被编译器编译后生成的二进制字节流(.class)文件
2.JVM把Class文件加载到内存,并进行验证、准备、解析、初始化
3.形成被JVM直接使用的Java类型
流程
加载-》验证-》准备-》解析-》初始化-》使用-》卸载
面试点
类的初始化时机
主动引用
创建类的实例。即通过new的方式,new一个对象。
调用类的静态变量(非final修饰的常量) 和静态方法
通过反射对类进行调用。
初始化某个类的子类,则父类也会被初始化
Java虚拟机启动时,指定的main方法所在的类,需要被提前初始化。
被动引用
子类调用父类的静态变量,只有父类初始化,而子类不会进行初始化。
通过数据定义引用类,不会触发类的初始化
final 常量不会触发类的初始化,因为编译阶段就存储在常量池中。
类的生命周期
当这个类的class对象不再被引用,即类不可触及时,Class对象就会结束生命周期。这个类在方法区的数据也会被卸载,从而结束这个类的生命周期。
总结
所以,一个类结束生命周期,取决于代表它的Class对象何时结束生命周期。
类的加载器
种类
启动类加载器:BootStrapClassLoader
主要来加载java核心类库,无法被直接调用
扩展类加载器:ExtensionClassLoader
用来加载jvm提供的扩展类库中的java类
系统类加载器:SystemClassLoader
Java应用都由他来加载
生命周期
执行了System.exit()方法.
程序正常执行结束。
程序在执行过程中遇到了异常或错误而并未处理,导致异常终止。
由于依赖的操作系统出现错误,而导致Java虚拟机进程终止。
性能调优
1.设置堆的最大最小值
-xms -xmx
2.调整年轻代老年代的比例
原则就是减少 gc stw
STW
Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。
Full GC内存泄漏排查
jasvism
dump
监测配置 自动dump
CPU 100%
Top命令查看进程资源消耗,然后是jdk bin目录下的jstack命令查看堆栈信息找出对应出现问题的代码位置
内存溢出
多线程
线程池
参数意义
核心线程数corePoolSize
默认无线程,接到任务就常见,除非初始预创建,一个或者全部
最大线程数maximumPoolSize
当workQueue满后,线程总数小于maximunPoolSize时,创建线程处理任务
空闲时间keepAliveTime
线程数大于corePoolSize时,无任务时线程终止
缓冲队列workQueue
LinkedBlockingQueue
无界队列
无界:小心内存溢出
ArrayBlockingQueue
运行机制
1.默认无线程数(不初始创建),任务来了就创建新的线程
2.如果任务过来,线程池中的线程数小于corePoolSize,也会新增线程去处理任务;
3.当任务过来,线程池中的线程数等于corePoolSize时,workQueue未满,会将任务新加入到queue中;
5.若任务过来,线程总数等于maximumPoolSize,workQueue已满,则触发拒绝策略;
优先级
corePoolSize > WorkQueue > MaximumPoolSize > 拒绝策略
拒绝策略
抛异常
丢弃
重试
丢弃最早提交的任务
状态
new
runnable就绪
等待cpu的调度
start() 后
running
cpu调度后
blocking
sleep()后,等待cpu的重新调度
dead
执行完毕
什么时候需要用到线程池
当线程创建与销毁的时间大于线程任务执行的时间
多线程一定是最快的吗
不一定
redis是单线程的
因为IO操作时,常发生阻塞,这时多线程的强大就体现出来了
多线程会频繁切换线程上下文
方法
join()
等待该线程先执行完毕后才会往下执行
wait()
释放锁
不阻塞
yiely()
让出cpu的使用权,重新让所有线程争抢cpu执行机会
sleep()
阻塞
不释放锁
notify()与notifyAll()有什么区别
IO
BIO
NIO
粘包拆包
多路复用
Netty
序列化
MQTT协议
介绍
轻量级基于代理的发布/订阅的消息传输协议
TCP/IP
协议
QOS服务质量
Qos = 0
客户端只发一次,无论是否收到,
导致只会消费一次消息
用于一些不是很重要的数据
Qos = 1
客户端发送一次消息,服务器给个回复,客户端若没收到,一直发送
导致会产生至少一个消息
比如状态的变更,多收一次也没关系
Qos = 2
客户端缓存消息,并发送给服务端,服务端收到并缓存消息,并给回复消息,客户端收到后在发个消息给服务端,服务端收到后发送消息给订阅端,并发消息给客户端,客户端把缓存的消息删除,订阅方收到消息后,缓存消息,给服务端发个收到消息,服务端又发个回复消息,订阅方消费信息,删除消息,并发给服务端一个完成的消息,服务端删除消息,
确保一次传输
发布方与服务器有四次通信,一次消息发送,两次消息的互相确认,一次服务器发给发布方完成的消息
订阅方不一定要在线
第三方:EMQ服务器
共享订阅
Java面试
集合
HashMap
1.7
数组+链表
Entry
头插
会产生什么问题
1.resize()扩容的时候,rehash重新插入时产生环形链表,调用get方法后发生死循环
扩容
条件
条件1:总size大于阈值(数组长度*加载因子)的时候
条件2:存放新值的时候与当前存放的数据发生Hash碰撞
1.8
数组+链表+红黑树
当链表长度大于8的时候,且数组长度大于64的时候,转化为红黑树。如果数组大小小于64,则进行扩容。
Node
尾插
解决环形链表,防止死循环
存放新值时,数组数据总数大于阈值,并不要求发生hash碰撞
扩容机制
新数组为原数据大小的两倍,数据重新hash,重新放入数组中
多线程不安全
多个线程插入同一位置,导致数据丢失
疑问
为何1.8加入红黑树
默认使用二分法查询红黑树,查询效率更高
为何1.8改为尾插法
不会产生环形链表,解决死循环问题
为何扩容需要重新hash
获取index值与数组长度有关
扩容为2的次幂
方便位运算
数据均匀分布
遍历
1.entrySet()然后获取迭代器遍历
2.建一个BiCusumer的子类,用forEach()遍历
3.用keySet()然后获取key的迭代器遍历
4.用values()进行值遍历
HashTable
线程安全,性能不佳原因
ConcurrentHashMap
1.8前
锁分段技术(Segment)继承了ReentrantLock
数据分为16份,分别加锁
扩容: 将每段Segment下的数组扩大至两倍
优点:快速定位和减少重排次数
1.8后
放弃了索锁分段技术
CAS+Synchronized
CAS
比较和替换
不接受NullKey与NullValue
数组+链表+红黑树
安全失败
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
为何是线程安全的
Segment分段锁,将数据分为16份,分别加锁
CAS + Synchronized
类加载器
BootstrapClassLoader
所以可以使用Unsafe.getUnsafe()方法
ArrayList
数组
初始0长度,add后初始为10,扩容为1.5倍
查询快(按角标查询),删除新增较慢
线程不安全
LinkedList
带有头结点与尾节点的双向链表
插入方式有两种,头插与尾插
查询比较慢,删除新增比较快
快速失败
在使用迭代器对集合对象进行遍历的时候,如果 A 线程正在对集合进行遍历,此时 B 线程对集合进行修改(增加、删除、修改),或者 A 线程在遍历过程中对集合进行修改,都会导致 A 线程抛出 ConcurrentModificationException 异常。
动态代理
cglib动态代理
Jdk动态代理
Mybatis
#与¥的区别
#{}使用的预处理模式
${}使用的字符串替换的方式
Dao与xml方法匹配
mapper标签上的namespace指定
嵌套查询与嵌套结果
插入数据获取ID
1.设置useGeneratedKeys
2.selectKey
select LAST_INSERT_ID();
SqlSession后续执行
1.SqlSession中有executor
执行器
SimpleExecutor
不重复使用预处理
select或者update
ReuseExecutor
重复使用预处理
不关闭statement对象,放入map中重复使用
BatchExecutor
必须手动的刷新
executor.doFlushStatements() ,简称批处理刷新
执行update,不支持select
缓存
一级缓存
概要
缓存的命中条件
1.同一会话
2.方法名,类名必须一致(statement id必须一致)
3.RowBounds 结果条数一致,结果范围一致
4. sql一致,参数一致
如何保证数据一致性
会话级缓存
必须是同一会话才会命中
缓存Map的key值是什么
CacheKey
BaseExecutor的createCacheKey方法
决定命中条件
如何判断两个CacheKey相等
两个cachekey对象的HashCode是否相等
判断checksum是否相等
update时 checkSum+=HashCode
判断count是否相等
update时count++
逐一判断以上五个元素是否相等
二级缓存
应用级别的缓存
随应用的生命周期
默认是不开启的
需要在xml中配置cache标签
eviction缓存回收策略
1.软引用
2.弱引用
3.先进先出
按照缓存进入的顺序来移除它们
4.最少使用的
移除最长时间不被使用的对象
可以全局配置
cacheEnabled
共享缓存中数据进行序列化操作和反序列化操作
实现Serializable
共同
修改操作
清除缓存操作
因为不需要访问缓存并会更改数据
缓存采用同一个缓存基类,二级缓存实现比较复杂,一级比较简单;所以二级缓存比一级缓存复杂的多
动态SQL
表中字段与对象属性名不一致
1.sql语句中使用as转换
2.xml中使用resultMap的property,column属性建立映射关系
3.如果是驼峰命名与下划线形式,可以使用全局配置mapUnderscoreToCamelCase=true来配置
批量插入如何实现
Session与Cookie
Cookie
浏览器禁用后Session不可使用,可使用URL重写
安全问题:URL拿到就可以登录
客户端
最大为4K
大部分浏览器最大可以设置20个Cookie
都是会话跟踪技术
Session
服务端
session的实现依赖于Cookie,SessionId存在Cookie中
Jenkins
K8S
Spring Boot
ZooKeeper
选举机制
Git
秒杀系统
限流(拦截请求)
客户的恶意下单
synchronized
底层实现原理
ACC_SYNCHRONIZED
执行线程需要先获取到monitor
代码块
monitorenter
monitorexit
enter加1,exit或者异常减一
两者区别
方法:设置ACC_SYNCHRONIZED实现
代码块:monitor enter与exit实现
jvm层面
锁膨胀(锁升级)
过程
无锁
偏向锁
利用了对象头中的ThreadId不会释放
比较对象头中的ThreadID是否与自身一致,一致则变为无锁状态,不一致则判断对方是否存活,存活则升级为轻量级锁,死亡则变为无锁状态
JVM参数
偏向锁是默认开启的,而且开始时间一般是比应用程序启动慢几秒,如果不想有这个延迟,那么可以使用-XX:BiasedLockingStartUpDelay=0;
不想要偏向锁,那么可以通过-XX:-UseBiasedLocking = false来设置;
轻量级锁
自旋获取资源
重量级锁
原因
发生了资源的争抢
缺点
升级不可逆
特征保证
可见性
内存强制刷新
可重入性
计数器
原子性
单一线程持有
有序性
Lock
jdk层面
ReentrantReadWriteLock
ReadLock
WriteLock
ReentrantLock
FairSync
NonfairSync
如何保证不同线程可见性
被volatile修饰的变量,在发生写操作时,会发出一个汇编指令,这个指令会触发mesi协议,会存在有一个总线嗅探的东西,这个总线嗅探会让cpu不断检测主内存中该变量的值,一旦有变化,其他的cpu会立即将该变量从自己的缓存中清除,再去主内存中去重新获取这个值
缓存行锁定
一个线程改变值后,另一个线程立刻知晓
1.保证内存可见性
保证修改了volatile的值会立即更新到主存,其他的线程操作时都会获取新值
2.防止指令重排
volatile仅能实现变量的修改可见性,不能保证原子性
然后,如果说取出数据后,cpu切出,另一个线程对该值操作,并成功写入主存,切回来后,操作完重新写入。
锁的分类
1
乐观锁
拿不到锁继续执行
悲观锁
拿不到锁一直尝试去拿锁
常见的线程锁
2
公平锁
非公平锁
3
重入锁
不可重入锁
死锁
1.互斥
都需要等待其他线程释放锁
2.不可抢占
3.占有且等待
4.循环等待
形成等待环路
简介
线程A拿着锁等待B释放,线程B拿着锁等待B释放
破坏
不会破坏1
破坏2,线程A等待另一个资源,一段时间主动释放持有资源
破坏3,每个线程一次性获取所有资源
破坏4,线性化获取资源
CAS(无锁)
CAS的作用是比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存中的值一致为止
乐观锁策略
存在的问题
ABA问题
解释
因为CAS会检查旧值有没有变化,这里存在这样一个有意思的问题。比如一个旧值A变为了成B,然后再变成A,刚好在做CAS时检查发现旧值并没有变化依然为A,但是实际上的确发生了变化。
可以沿袭数据库中常用的乐观锁方式,添加一个版本号可以解决。原来的变化路径A->B->A就变成了1A->2B->3C。
自旋时间过长
使用CAS时非阻塞同步,也就是说不会将线程挂起,会自旋(无非就是一个死循环)进行下一次尝试,如果这里自旋时间过长对性能是很大的消耗。
只能保证一个共享变量的原子操作
解决方案
利用对象整合多个共享变量,即一个类中的成员变量就是这几个共享变量。然后将这个对象做CAS操作就可以保证其原子性。atomic中提供了AtomicReference来保证引用对象之间的原子性。
CAS vs Synchronized
主要的区别
CAS并不是武断的间线程挂起,当CAS操作失败后会进行一定的尝试,而非进行耗时的挂起唤醒的操作,因此也叫做非阻塞同步。
元老级的Synchronized(未优化前)最主要的问题是:在存在线程竞争的情况下会出现线程阻塞和唤醒锁带来的性能问题,因为这是一种互斥同步(阻塞同步)。
操作系统的临界区机制
牵扯到用户态与内核态之间的转变
Synchronized与Lock区别
1.Synchronized是jvm的底层实现,而Lock是jdk的一个接口,提供了各种API
2.Synchronzied会自动释放锁,Lock需要手动unlock
4.Synchronized可以锁方法与代码块,lock只能锁代码块
5.Synchronized是非公平锁,ReentrantLock可以指定是否为公平锁
6.Synchronized不能知道线程有没有拿到锁,而Lock能
7.Lock可以使用读锁提高线程读效率
劣势:锁的升级不可逆
ThreadLocal
ThreadLocal是一个本地线程副本变量工具类
线程隔离特性
什么时候用
将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不同的变量值完成操作的场景。
ThreadLocalMap
ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也独立实现。
key只能是ThreadLocal对象
Hash冲突怎么解决
和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式
线性探测
初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
key是弱引用,value是强引用
ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。
Linux
1.查询进程 ps -ef|grep xxx
2.动态查看日志 tail -f xxx.log
3.查看所有用户
cat /etc/passwd
条目: admin:x:1000:1000:centos-loumt:/home/admin:/bin/bash
4.tar包
tar
5.从某个文件内查询某个内容
grep \"内容\" 文件路径
分布式
分布式锁
1.Redis的RedLock
zk锁
分布式事务一致性
Java基础
三大特性
封装
面向对象的封装即是将一个对象的属性与行为定义在一个类中,其中私有不公开的用private去修饰,公开的用public修饰,也提供修改属性的set与获取属性的get
继承
可以使用一个基础类来定义一个更具体的类,用关键字extends该基础类,可以修改基础方法,新增属性与行为,提供代码的重用性,也体现了该语言的扩展性
多态
编译时与运行时类型不一致就是多态性,多态增强了软件的灵活性和扩展性。
同步异步
相对于服务器而言
触发IO时,同步等待其返回,数据返回后才继续往下,异步是不等待返回值直接往下走,内核通知后结果返回
阻塞非阻塞
相对于客户端而言
简单理解为需要做一件事能不能立即得到返回应答,如果不能立即获得返回,需要等待,那就阻塞了,否则就可以理解为非阻塞。
servlet
filter
内存模型(JMM)
JDK与JRE的区别
JRE是Java基础运行环境,包含JVM与Java核心类库
序列化与反序列化有什么作用
转储
两种介质之间的传输,比如内存到数据库,比如对象在网络上的传输
序列化: 对象转为有序字节流
反序列化: 字节流转为对象
Java工具
JMeter
多线程测试工具
PostMan
Java反编译
javap -v *.class
缓存与数据库在高并发中不一致 解决方案如下:更新数据的时候,根据数据的唯一标识,将操作路由之后,发送到一个 jvm 内部队列中。读取数据的时候,如果发现数据不在缓存中,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也发送同一个 jvm 内部队列中。一个队列对应一个工作线程,每个工作线程串行拿到对应的操作,然后一条一条的执行。这样的话,一个数据变更的操作,先删除缓存,然后再去更新数据库,但是还没完成更新。此时如果一个读请求过来,没有读到缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成。这里有一个优化点,一个队列中,其实多个更新缓存请求串在一起是没意义的,因此可以做过滤,如果发现队列中已经有一个更新缓存的请求了,那么就不用再放个更新请求操作进去了,直接等待前面的更新操作请求完成即可。待那个队列对应的工作线程完成了上一个操作的数据库的修改之后,才会去执行下一个操作,也就是缓存更新的操作,此时会从数据库中读取最新的值,然后写入缓存中。如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回;如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值。高并发的场景下,该解决方案要注意的问题:读请求长时阻塞由于读请求进行了非常轻度的异步化,所以一定要注意读超时的问题,每个读请求必须在超时时间范围内返回。该解决方案,最大的风险点在于说,可能数据更新很频繁,导致队列中积压了大量更新操作在里面,然后读请求会发生大量的超时,最后导致大量的请求直接走数据库。务必通过一些模拟真实的测试,看看更新数据的频率是怎样的。另外一点,因为一个队列中,可能会积压针对多个数据项的更新操作,因此需要根据自己的业务情况进行测试,可能需要部署多个服务,每个服务分摊一些数据的更新操作。如果一个内存队列里居然会挤压 100 个商品的库存修改操作,每个库存修改操作要耗费 10ms 去完成,那么最后一个商品的读请求,可能等待 10 * 100 = 1000ms = 1s 后,才能得到数据,这个时候就导致读请求的长时阻塞。一定要做根据实际业务系统的运行情况,去进行一些压力测试,和模拟线上环境,去看看最繁忙的时候,内存队列可能会挤压多少更新操作,可能会导致最后一个更新操作对应的读请求,会 hang 多少时间,如果读请求在 200ms 返回,如果你计算过后,哪怕是最繁忙的时候,积压 10 个更新操作,最多等待 200ms,那还可以的。如果一个内存队列中可能积压的更新操作特别多,那么你就要加机器,让每个机器上部署的服务实例处理更少的数据,那么每个内存队列中积压的更新操作就会越少。其实根据之前的项目经验,一般来说,数据的写频率是很低的,因此实际上正常来说,在队列中积压的更新操作应该是很少的。像这种针对读高并发、读缓存架构的项目,一般来说写请求是非常少的,每秒的 QPS 能到几百就不错了。我们来实际粗略测算一下。如果一秒有 500 的写操作,如果分成 5 个时间片,每 200ms 就 100 个写操作,放到 20 个内存队列中,每个内存队列,可能就积压 5 个写操作。每个写操作性能测试后,一般是在 20ms 左右就完成,那么针对每个内存队列的数据的读请求,也就最多 hang 一会儿,200ms 以内肯定能返回了。经过刚才简单的测算,我们知道,单机支撑的写 QPS 在几百是没问题的,如果写 QPS 扩大了 10 倍,那么就扩容机器,扩容 10 倍的机器,每个机器 20 个队列。读请求并发量过高这里还必须做好压力测试,确保恰巧碰上上述情况的时候,还有一个风险,就是突然间大量读请求会在几十毫秒的延时 hang 在服务上,看服务能不能扛的住,需要多少机器才能扛住最大的极限情况的峰值。但是因为并不是所有的数据都在同一时间更新,缓存也不会同一时间失效,所以每次可能也就是少数数据的缓存失效了,然后那些数据对应的读请求过来,并发量应该也不会特别大。多服务实例部署的请求路由可能这个服务部署了多个实例,那么必须保证说,执行数据更新操作,以及执行缓存更新操作的请求,都通过 Nginx 服务器路由到相同的服务实例上。比如说,对同一个商品的读写请求,全部路由到同一台机器上。可以自己去做服务间的按照某个请求参数的 hash 路由,也可以用 Nginx 的 hash 路由功能等等。热点商品的路由问题,导致请求的倾斜万一某个商品的读写请求特别高,全部打到相同的机器的相同的队列里面去了,可能会造成某台机器的压力过大。就是说,因为只有在商品数据更新的时候才会清空缓存,然后才会导致读写并发,所以其实要根据业务系统去看,如果更新频率不是太高的话,这个问题的影响并不是特别大,但是的确可能某些机器的负载会高一些。
JUC
lock()
unlock()
tryLock()
两大核心
atomic的基础
AQS
locks的基础
TCP协议
TCP通信中的KeepAlive
如果没有KeepAlive机制
只要客户端与服务器建立了TCP连接,不管两者之间是否有数据通信,不管中间的路由器是否崩溃,不管网线是否断开,连接依旧保持建立,这就会导致积累很多空闲甚至已经不可用的连接。
KeepAlive可以在客户端实现,也可以在服务器端实现,或者两边都实现。
比如,客户端每隔一段时间就利用空闲的连接向服务器发送一个数据包,这个数据包并没有其它的作用,只是为了检测一下服务器是否还处于活动状态。如果服务器未响应这个数据包,在一段时间后,客户端再次发送一个数据包,如果服务器仍然没有响应,那么客户端就将该socket关闭。
KeepAlive并不是TCP规范的一部分,RFC甚至列出了不使用它的理由。但在几乎所有的TCP/IP协议栈(不管是Linux还是Windows)中,都实现了KeepAlive功能。
TCP socket的KeepAlive默认是关闭的,可以通过socket.setKeepAlive(true)来否启它。
三次握手建立连接
客户端发送连接请求给服务端,服务端发送收到请求消息给客户端,客户端发送消息给服务器让服务端知道客户端收到回应了
为何不采用两次握手
防止网络问题产生的延迟消息建立的连接
为何不用四次
多余
四次挥手断开连接
客户端发送FIN(finish)给服务器,服务ack给客户端,服务器FIN给客户端,客户端ACK
一些概念
全双工,所以双发都需要断开并确认
面试题
一个String字符串,统计字符出现的次数
前端实现二级联动
spring容器
Spring与SpringMVC分别拥有一个容器,Spring作为父容器,SpringMVC拥有子容器
0 条评论
回复 删除
下一页