设计模式 | 设计模式应用场景 |
---|---|
单例模式 | 连接池,线程池,工厂类 |
工厂模式 | |
代理模式 | 功能增强,比如日志处理,权限判断等等 |
享元模式 | java 中的string,如果有就返回,否则创建并保存到字符串缓存池;数据库的数据池 |
迭代器模式 | 隐藏内部数据结构,对外提供统一的数据访问方式 |
数组与链表(连续内存与非连续内存,随机访问效率,删除效率不一样)
排序算法实现以及时间,空间复杂度,改良分析
堆和栈
二叉树,搜索树,完全二叉时,满二叉树,哈夫曼树(最优树)
二叉树的递归遍历,借助栈的非递归遍历
字典树(原理以及应用场景,可能的优化角度)
B 树,B+树,特点,以及应用场景(数据库索引)
邻接矩阵与邻接表
查找
hash法,hash冲突的解决方式(可能会考虑到redis集群时的hash算法)
冲突解决方法:开放地址法(线性探测(删除比较麻烦,只能标记为删除)
随机探测(步长随机数序列))
拉链法(指针数组+链表)
深度优先于广度优先
广度优先:类比成树中 层次遍历
深度优先:类比树中序遍历
图的基本概念
无向图:
有向图:
表示方式:
邻接表:
邻接矩阵:
最短路径(算法)
**单源最短路径:dj(贪心算法),**使用一维数组D[i] 记录起点到i的最短距离,
如果起点v0 到i需要经过 k,则D[i]=min{d[i],D[k]+e(k,i)}
多源最短路径:floyd(dp)
d[i] [j] 表示i与j之间的最短距离
递推公式: d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
最小生成树
可使用贪心算法:
算法名 | 描述 | 使用场景,复杂度 |
---|---|---|
prims | 设点集为S,已选取点为S1,未选取点集为S-S1, 1,初始化,选取第一个点2, 遍历 已选取点和未选取点之间最小边,并将该点加入S1,直到S-S1为空,完成 | 时间复杂度:o(n 2 ^2 2),与边数目无关,适用于稠密图 |
cruskal | 1.初始化 将边按照从小到大排列,所有点看成是孤立的;2,依次选取边, 如果该边的两个端点在同一个连通图中,则跳过;否选取该边;3,重复2直到只剩一个连通图,这就得到最小生成树 | 复杂度:o(elge) ,e是边的条数,这里主要是排序来体现的,适合于稀疏图 |
查找方式 | 数据结构 | 时间复杂度 |
---|---|---|
顺序查找 | 顺序存储,链式存储 | 顺序存储(o(1)),链式存储o(n) |
随机查找 | 顺序存储(o(1)),链式存储o(n) | |
二分查找 | 有序顺序存储结构 | o(lgn) |
hash查找 | o(1) | |
索引查找 | o(lgn)+o(k) 先使用二分查找找到块,然后在块中顺序查找 |
请求-》前端控制器(dispatchServlet)->调用处理映射器(HandlerMapping),生成处理器对象以及处理拦截器->DispatchServlet(前端控制器)->调用处理适配器进一步调用处理器-》执行处理器( Controller)->返回ModelAndView给dispatchServlet,传递modelAndView -》ViewResolver(视图解析器)-》返回具体View ->DispatchServlet对View 进行渲染-》响应用户
创建对象的权限:由开发者new 交由spring 去创建(需要将类的全限定名配置到xml文件中,spring底层根据反射去创建对象)
不使用ioc时:
A a=new A();
使用ioc后 A a=SpringContext.getBean("");
DI:是依赖注入(注入方式:构造+settter,一般使用后者)
3. spring中的bean有些什么类型?
4. spring 线程安全吗?
大多数bean 是单例的,而且spring并没有考虑线程安全,但是大多数bean是无状态的
每个bean自身的设计。不要在bean中声明任何有状态的实例变量或类变量,如果必须如此,那么就使用ThreadLocal把变量变为线程私有的,如果bean的实例变量或类变量需要在多个线程之间共享,那么就只能使用synchronized、lock、CAS等这些实现线程同步的方法了。
下面将通过解析ThreadLocal的源码来了解它的实现与作用,ThreadLocal是一个很好用的工具类,它在某些情况下解决了线程安全问题(在变量不需要被多个线程共享时)。
5. FactoryBean 与BeanFactory 的区别
BeanFactory,以Factory结尾,表示它是一个工厂类(接口),用于管理Bean的一个工厂。在Spring中,BeanFactory是IOC容器的核心接口,它的职责包括:实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。
FactoryBean 以Bean结尾,表示它是一个Bean,不同于普通Bean的是:它是实现了FactoryBean接口的Bean,根据该Bean的ID从BeanFactory中获取的实际上是FactoryBean的getObject()返回的对象,而不是FactoryBean本身,如果要获取FactoryBean对象,请在id前面加一个&符号来获取。
特性 | 描述 |
---|---|
原子性 | 将事务中所做的操作捆绑成一个原子单元,即对于事务所进行的数据修改等操作,要么全部执行,要么全部不执行。 |
隔离性 | 由并发事务所做的修改必须与任何其他事务所做的修改相隔离。事务查看数据时数据所处的状态,要么是被另一并发事务修改之前的状态,要么是被另一并发事务修改之后的状态,即事务不会查看由另一个并发事务正在修改的数据。这种隔离方式也叫可串行性 |
持久性 | 事务完成之后,它对系统的影响是永久的,即使出现系统故障也是如此 |
一致性 | 事务在完成时,必须使所有的数据都保持一致状态,而且在相关数据中,所有规则都必须应用于事务的修改,以保持所有数据的完整性。事务结束时,所有的内部数据结构都应该是正确的 |
9. 如何实现分布式事务
没有应用场景,个人分析,分布式事务的产生是由于某个业务操作需要在
本地事务的实现当然很简单,加上Transactional 注解
如果是分布式事务:
方案一:2PC 提交
前提:
存在一个节点作为协调者,其他节点作为参与者,节点间可以相互网络通信
所有节点采用预写日志,并且日志保存在可靠的存储设备上,即使节点损坏也不会导致日志丢失
所有节点都不会永久性损坏(损坏后可以恢复)
阶段 | 说明 |
---|---|
投票 | 本地执行事务成功(各节点事务信息写入redo,undo日志)的节点发消息给协调者,内容是"同意",如果某个节点执行失败,返回消息"终止" |
提交执行阶段 | 如果协调者收到的回复全部是"同意",协调者发起消息"正式提交",节点正式完成操作,参与者节点返回"完成"信息,协调者如果收到全部都回复为"完成"时,完成事务; 如果出现一个"终止",协调者向所有参与节点发起"回滚"请求,参与者节点利用之前的undo日志,执行回滚操作,然后发送给协调者"回滚完成",收到所有参与者的回滚完成消息后,协调者取消事务 |
//1. 获取连接
conn=getConn();
SavePoint savePoint=null;
// 2. 关闭自动提交事务 conn.setAutoCommit(false)
try{
A(发工资);
savePoint=conn.setSavePoint();
B(工资发放通知短信)(出现异常,如果A也回滚,那么就没工资了)
}catch(Exception e){
if(savePoint!=null){
// 说明 A 发工资是没有异常,那么就是B 发短信出现了异常
A 需要提交
conn.rollBack(savePoint);
conn.commit();
}
// A 操作出错,全部回滚
else{
conn.roolBack();
}
}
- 核心是定义切点:需要被增强的方法
- 定义通知:具体要增加的功能
- 定义切面:切点与通知组合形成切面
- 定义代理工厂对象(ProxyFactoryBean):将目标对象,切面组合,目标对象接口组合到一起用于制造代理对象
<bean id="myPointCut" class="org.springframework.aop.support.JdkRegexpMethodPointcut">
<property name="pattern" value=".*study"/>
bean>
<bean id="myAdvice" class="top.forethought.framework.aop.xml.StudyAdvice">
bean>
<bean id="myAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
<property name="advice" ref="myAdvice"/>
<property name="pointcut" ref="myPointCut"/>
bean>
<bean id="studentProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="studentObj"/>
<property name="interceptorNames" value="myAdvisor" />
<property name="proxyInterfaces" value="top.forethought.framework.aop.BaseInterface" />
bean>
<bean id="studentObj" class="top.forethought.framework.aop.xml.Student">bean>
问题 | 解答 |
---|---|
什么是AOP | 面向切面编程,是oop的补充 |
使用场景,用途 | 不改变原有代码,横向抽取(相对于继承而言)添加功能,比如事务管理,缓存,日志(可能是手写代码记录日志,有些日志是不需要记录的) |
spring aop | java 编写,运行期通过代理方式向目标类织入增强代理(cglib ,不依赖接口类,使用字节码增强),查看字节码发现是extends |
jdk 的invocationhandler | 依赖接口,反射生成动态代理类,查看生成的class文件发现是implements |
起初,jdk 动态代理运行效率低于cglib,创建效率高,但随着jdk版本的提高,1.8版本jdk动态代理的运行效率已经高于cglib
jdk 动态代理基于接口 (可以观察生成的字节码)
cglib 动态代理基于继承
spring 也可以配置使用jdk动态代理
proxy-target-class=true表示使用CGLib代理,false 则使用jdk 动态代理
13. 什么是微服务
单体应用,功能很多,使用的技术也较为统一,难以升级新技术,时间推移,代码维护难度比较大。扩展比较难,耦合可能比较高。
微服务:将某些相对独立的功能以服务的形式抽离出来,单独开发,不同的服务可以使用不同的技术开发,通过http通信的方式给整个应用提供支持,从外部看,是一个完整的整体。
悲观锁:悲观的认为极有可能存在修改数据的并发事务(包括本系统的其他事务或来自外部系统的事务),于是将处理的数据设置为锁定状态。悲观锁必须依赖数据库本身的锁机制才能真正保证数据访问的排他性
乐观锁:乐观的认为并发访问时很少的,使用版本号来标识,每次操作将版本号+1, 读取数据时获取一次数据库版本号,写入数据时,如果新的版本号大于数据库版本号,允许操作;否则认为是过期数据无法更新。Hibernate中通过Session的get()和load()方法从数据库中加载对象时可以通过参数指定使用悲观锁;而乐观锁可以通过给实体类加整型的版本字段再通过XML或@Version注解进行配置
2. hibernate 与mybatis 对比
3. hibernate 对象的三态(暂时态(新创建的),持久态(在数据库中有相应的记录,与session关联),游离态(与session 缓存失去关联),)
**1,加载**:是类加载的一个步骤,主要是
a,通过全限定名获取定义此类的二进制字节流
b,将字节流所代表的的静态数据结构转为方法区的运行时数据结构
c,在内存中生成一个代表此类额java.lang.Class 对象,作为方法区这个类的各个数据访问的入口
**2, 验证**: 确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,不对当前虚拟机造成危害
**3, 准备**:正式为**类变量**(static 修饰的,不包含成员变量)分配内存并设置类变量的初始值阶段(这里的初始值
通常指零值,但是如果是static final ,则会生成Constant Value 属性,值为java代码指定的值)
**4, 解析**:机将常量池中的符号引用替换为直接引用
**5, 初始化**:执行类中定义的代码(执行程序员代码的主观代码初始化)
堆:创建的实例对象在此,线程共享(由于是GC的主要发生场所,又叫GC堆)
栈:(本地方法栈+虚拟机栈(hotspot 虚拟机已经合并) :
对象的内存布局:
项 | 含义 |
---|---|
对象头 | 运行时数据(hash,GC分代年龄,锁状态标志,线程持有的锁,偏向线程id,偏向时间戳)+类型指针 |
实例数据 | 程序代码中定义的各种类型字段内容,包括从父类继承下来的 |
对齐填充 | 非必须,占位填充 |
算法名 | 描述 | 特点(适用场景) |
---|---|---|
标记-清除 | 通过GCRoot 对象,引用链标记,被标记的表示存活,未标记对象被清除 | 会产生大不连续内存碎片,如果后序出现大对象,会提前触发垃圾回收 |
复制 | 内存对半分,只能使用其中一半,将存活对象拷贝到另一半内存,然后清除之前的内存 | 无内存碎片,空间利用率低,如果存活率高,效率较低 |
标记-整理 | 标记,然后将存活对象往某一端移动,然后抹去边界之外的空间 | 无内存碎片,无空间浪费 |
分代收集 | 根据对象的存活率来使用不同的回收算法 | 少量存活,选用复制算法;大量存活,则选择标记-清除或者标记-整理 |
NIO(NEW IO),可以通过本地方法在堆外开辟内存(直接内存),减少在jvm定义的内存中复制,提高效率(基于缓冲与管道)
相关参数 | ||
---|---|---|
优先在Eden分配 | Eden区没足够空间分配时,虚拟机将发起MinorGC(发生在新生代的GC | |
大对象直接进入老年代 | 大对象是指需要连续分配内存的对象,比如长字符串以及数组 | 参数-XX:pREtenureSzieSizeThreshold,大于此值的对象直接在老年代分配 |
超期存活的对象进入老年代 | 对象年龄(Age)计数器,在Eden区出生,经过一次MinorGC 之后,仍然存活并且能被survivor区收纳,移到survivor区,年龄设置为1,每熬过一次MinorGC,年龄+1,默认是到达15时,进入到老年代 | 称为老年代的阈值可以使用-XX:MaxTenuringThreshold |
动态对象年龄判定 | 如果survivor区相同年龄对象的内存总和大于survivor区内存的一半,则将年龄>=该年龄的对象直接进入老年代 | |
空间分配担保 | 在发生MinorGC之前,检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果成立,MinorGc 就确保是安全的。如果不成立,检查HandlerPromotionFailure 是否设置为允许担保失败,如果是允许担保失败,继续检查老年代最大连续可用空间是否大于历次晋升到老年代对象的平均大小,如果大于,则尝试经进行这次有风险的MinorGC。 如果不允许冒险,使用FULLGC |
类加载器:用来实现类的加载动作
常见类加载器:
加载器名 | 描述 |
---|---|
启动类加载器(Bootstrap ClassLoader) | c++实现,加载java_home\lib 下的类 |
扩展类加载器(Extension ClassLoader) | java 实现,加载java_home\lib\ext 目录下 |
应用类加载器(ApplicationClassLoader) | 负责加载ClassPath 上指定的类库 ,(如果没有自定义类加载器,那么程序中默认是使用这个类加载器) |
双亲委派模型:双:指java虚拟机只存在两种不同的类加载器,意识c++ 实现的BootstrapClassLoader,另一种则是其他用java实现的类加载器
委派:是指类加载器加载对象时,总是尝试让自己的父类加载器去完成,如父类加载器不能完成,再自己完成类加载
优点: java 类随着他的类加载器一起具备了一种带有优先级的层次关系. 例如java.lang.Object,存在不于rt.jar,这样在各种类加载器环境中都是同一个类
单线程(没有上下文切换,操作大多基于内存),数据结构简单(hash的使用),
string,hash(适合对象 ,hset 对象名 属性名 值),zSet()有序
redis的动态字符串:
不以’\0’ 结束
字符串定义属性有:
属性 | 含义 |
---|---|
int free | 剩余可用空间长度 |
int len | 当前字符串内容长度 |
char [] buf | 字符串数组 |
相比c中传统的字符串有啥优点呢?
1. 不使用'\0'结尾,读取内容是o(1) :起始地址到len
2. 杜绝缓冲区溢出,c字符串只能通过'\0'来计算长度,
比如:连续的存放两个字符串 a='redis' b='memcache'
如果将a 修改为'redisclauster',那么会导致内存覆盖到字符串b,导致b 的内容发生改变
通过free 属性,可以判断出剩余的空闲内存能否容纳修改后的字符串值
如果不能容纳:free=修改后字符串长度(预分配内存)
len=修改后字符串长度
以后的修改可以减少内存的分配次数(应为预先分配了)
3. 减少内存分配次数
4. 可以保存二进制数据(因为不再以'\0'作为字符串结尾判断,而是通过len)
比较项 | redis | memcache |
---|---|---|
持久化 | aof(append ),rdb日志 | 无 |
数据类型 string ,list,set,hash,zset | ||
分布式 | 支持(redis clauster)) | 不支持(可以通过在客户端使用一致性hash来实现分布式存储,在存储和查询时,需要现在客户端计算一次数据所在节点) |
内存管理机制 | 长久未使用的valu交换到磁盘 | 一直在内存,使用特定长度的块存储数据,完全解决内存碎片问题,但是内存利用率不高 |
键过期时间 | 每个键可设置过期时间,到期自动删除 | |
内存淘汰策略 | 可设置内存最大使用量,超过时,执行数据淘汰策略(6中) |
机制:redis 每收到一个写命令,都通过write 函数追加到文件中(默认是appendonly.aof)
appendonly yes //启用aof持久化方式
# appendfsync always //每次收到写命令就立即强制写入磁盘,最慢的,但是保证完全的持久化,不推荐使用
appendfsync everysec //每秒钟强制写入磁盘一次,在性能和持久化方面做了很好的折中,推荐
# appendfsync no //完全依赖os,性能最好,持久化没保证
如何压缩aof 文件?提供bgrewriteaof 命令,收到此命令,redis 将内存中的数据已命令的方式保存到临时文件中,最后替换原来的文件
save 900 1 #900秒内如果超过1个key被修改,则发起快照保存
save 300 10 #300秒内容如超过10个key被修改,则发起快照保存
save 60 10000
策略 | 描述 |
---|---|
volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 |
volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 |
volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 |
allkeys-lru | 从所有数据集中选择使用lru |
allkeys-random | … |
noeviction | 禁止驱逐数据 |
使用 Redis 缓存数据时,为了提高缓存命中率,需要保证缓存数据都是热点数据。可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。
Redis 4.0 引入了 volatile-lfu 和 allkeys-lfu 淘汰策略,LFU 策略通过统计访问频率,将访问频率最少的键值对淘汰
a. 消息队列(由于Redis的列表是使用双向链表实现的,保存了头尾节点,所以在列表头尾两边插取元素都是非常快的)
b. 缓存
5. 缓存穿透:查询数据库不存在的数据:缓存null值(过期时间设置较小)
6. 缓存雪崩:缓存集中失效,不同类型产品设置不同过期时间,并且加上随机因子
7. 缓存击穿(单个点):某个缓存数据成为热点,失效时,大量的并发查询数据库(可以考虑将爆款设置为永不过期)
8. 缓存一致性
1.更新服务器时,立即更新缓存,或者删除对应缓存
2.读取缓存是,判断是否为最新缓存,若不是,需要冲到服务器读取
slave 发送请求让master传递最新的内存快照给自己
在传输过程中,master 正在接受的数据,放到与slave建立的连接的缓存里(list),快照传输完成后,发送缓存中的数据给slave
借助redis,memched等缓存工具,将服务器本地缓存同步到分布式缓存服务器,需要时间,实现难度较大
命令 | 含义 |
---|---|
setNX | set not exist:如果key不存在,就设置上,返回1,否则返回0 |
getset | 返回就值并且设置新值(相当于是原子操作,是单独使用get ,再set所不能达到的效果) |
分布式锁的实现:
现在有并发线程 c1,c2,锁被C3持有,并且C3 已经挂掉
c1,c2 分别使用 setNX("LOCK");//显然是失败(返回0)的,而且锁也没被释放
c1 del锁,加锁成功
c2 del 锁,加锁成功(但是这是错误的)
解决:
加锁时,将加锁时间戳,以及过期时间(即持有锁的最长时间)
并发线程 c1,c2,锁被C3持有,并且C3 已经挂掉
c4 setNX("LOCK")// 返回0
如果获取失败:
c4 get 命令,检查是否过期,
如果没过期,继续等待/ /检查上次加锁时间以及过期时间算出当前锁是否应该被释放 (lockTime+expirTime
命令multi 开启事务
set key1 val1
set key2 val2
....
命令:exec 提交事务
你会发现一旦一条指令出错,其余是**不会回滚的。**
redis 是将多个命令放入一个队列,输入exec 命令,依次执行队列中的命令
direct:
单发多接收:
化同步为异步(应用内部,流量削峰,比如下单秒杀,用户比较活跃,同步会导致计算机性能下降),松耦合(多个应用之间)
场景 | 描述 |
---|---|
单发单接收 | 简单的发送与接收,没有特别的处理 |
单发多接收 | 一个发送端,多个接收端,如分布式的任务派发。为了保证消息发送的可靠性,不丢失消息,使消息持久化了。同时为了防止接收端在处理消息时down掉,只有在消息处理完成后才发送ack消息 |
Publish/Subscribe | 发布、订阅模式,发送端发送广播消息,多个接收端接收 |
Routing (按路线发送接收 | 发送端按routing key发送消息,不同的接收端按不同的routing key接收消息 |
默认是没有开启持久化的,如果在产生消息时,指定消息时持久消息,那么消息会发送到持久化交换机,然后交给持久化队列,(会将信息记录在持久化日志中),如果消息被路由到非持久队列,那么持久化日志中就会删掉持久化记录
进程是指运行中的程序,有独立的内存空间
线程是进程中最小的执行单元,共享进程的内存空间,以及上下文,当然也有自己
新建(new 创建)
就绪(调用了start)
运行中(running)
阻塞(由于锁导致),
无限等待(需要其他线程唤醒),
有限等待(时间到,就继续),
终止(线程的run 执行完就结束)
synhronized | lock | 描述 | 方法 |
---|---|---|---|
隐示获取锁,释放锁(先获取,再释放,简化同步,但是缺少扩展) | 显式获取锁,释放锁 | ||
可中断的获取锁 | 在获取锁的过程,可以响应线程中断,当获取到锁的的线程被中断时,锁可以被释放) | lockInterruptibly() | |
尝试非阻塞式的获取锁 | 当前线程尝试获取锁,如果这一时刻锁没有被其他线程占有,则成功获取锁 | tryLock() | |
超时获取锁 | 在指定的截止时间之前返回(1.获得锁,返回true 2.被中断,抛出异常 3. 超时时间结束,返回false) | boolean tryLock(long time,TimeUnit unit) | |
名称 | 概念 | 特点 |
---|---|---|
偏向锁 | 如果对象头的偏向锁标志位指向当前线程,那么当前线程不需要通过cas 获取锁,直接进入同步块 | |
自旋锁 | 线程获取锁失败,通过循环尝试获取锁,占用cpu,避免线程切换带来开销 | 需要注意自旋的次数以及间隔时间,不要无限自旋下去(jvm默认自旋次数是10,可以使用-XX:PreBlockSpin更改;除此之外,1.6引入自适应自旋锁,自旋时间取决于当前线程上次获得锁用时长度,如果上次获取锁是在短时间内获取,那么这次自旋也很可能成功获取锁,那么jvm可以给出较长的时间,给他;如果上一次获取锁耗时很长,那么这次也认为他很难通过自旋获取到锁,那么就直接省略自旋操作) |
可重入锁 | 当前线程如果已经获取到锁,可以重复加锁 | 比如递归调用某种需要加锁的方法时,不会被自己阻塞 |
排它锁(独占锁) | 只能由一个线程获取到锁,如果锁已经被其他线程获取,当前线程被阻塞 | 比如synchronized,ReentrantLock |
共享锁 | 可多个线程同时获取到锁 | 比如semphore,信号量,可以控制并发的线程数(连接池可以使用) |
读写锁 | 使用锁分离的思想,读锁时共享可重入锁,写锁是独占可重入锁 | 写锁加锁过程:如果当前线程是已经获取到锁的线程,可以直接获取锁(可重入),如果写锁状态为0,表示没有线程获取到写锁,当前线程获取锁,写锁状态+1。 写锁释放:可以将写锁状态置为0,读锁加锁过程:如果当前线程是持有写锁的线程,或者是写锁状态为0,则可以获取读锁,读锁状态+1;否则需要被阻塞;锁降级:持有写锁的线程,可以直接获取读锁,然后释放持有的写锁 |
公平锁 | 线程最终获取到锁的顺序和请求锁的顺序一致(也就是FIFO) | |
非公平锁 | 同一个线程可能连续多次获取到锁 | 可减少上下文切换,提高吞吐量(也是锁的默认实现方式) |
并发同步器,使用来实现Lock接口定义方法功能的基础
同步队列:双向链表, 加锁时,如果同步队列为空,使用cas将其设置到队列头
如果同步队列不空,使用cas将其设置到队列尾部
释放锁:(头节点是获取同步状态成功的线程节点),释放锁时,会唤起后继节点
由于某些节点发生中断以及为null的情况,从队列尾部向前找,找到所谓的后继节点,然后由获取同步状态成功的线程去将头结点设置为自己所在节点(这个过程不需要cas)
每个非头部节点自旋检测自己的前驱结点是否为头节点,如果是尾头节点,那么该节点可以尝试获取同步状态;除此之外,所有节点之间基本没有数据通信
状态队列:
几个概念:
概念 | 解释 |
---|---|
mark word | 用于存储对象自身的运行时数据,比如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程id,偏向时间 |
对象头 | |
监视器 | 用来实现同步,与每一个java对象关联,每个java对象都有一个监视器与之对应,是synchronized实现内置锁的基础 |
自旋锁 (忙人) | 让不满足条件的线程,等待一段时间,看能不能获取锁,通过占用处理器,避免线程切换带来开销(注意自旋时间和次数应该有一定限制,到达限制,任然没有获取锁,则需要挂起等待) |
偏向锁(熟人) | 大多数情况下,同一个对象的锁总是由同一个线程多次获得。当一个线程访问同步块,并且获取到锁,会在对象头和栈帧中的锁记录里存储锁偏向(偏爱)的线程id,偏向锁是可重入锁(可以想象带上synchronized修饰的递归方法,就会同一个线程多次进入同步代码块,但不会每次每次进入去获取锁,第一次就获取到了)。如果锁的对象头中的markword存储着指向当前线程的偏向锁,则不需要重新通过cas 加锁,解锁,持有偏向锁的线程(不处于活动状态)时,才会释放锁。偏向锁无法使用自旋锁优化,因为有其他线程来竞争锁,就破坏了偏向锁的假设前提 |
轻量级锁 | 相对重量级锁而言,轻量级锁是为了减少在无实际竞争情况下,使用重量级锁带来的性能消耗。jvm 在当前线程的栈帧中创建用于存储锁记录的空间,并且将对象头中的mark word 复制到锁记录中,然后尝试CAS 将对象头的mark word 替换为指向锁记录的指针,如果成功,则表示当前线程获取锁成功,失败,则表示当前存在其他线程在竞争锁。当前线程采用自旋的方式获取,自旋失败,则升级为重量级锁 |
重量级锁 | 通过对象内部的监视器实现,底层依赖操作系统的Mutex Lock 实现,操作系统实现线程的切换需要从用户态到内核态的切换,成本很高。线程竞争不使用自旋,不消耗cpu。线程进入阻塞,等待被唤醒。 |
(必须使用在synchronized修饰的{} 内部)
wait 将持有该对象的线程进入waiting状态,同时会释放锁,只有其他线程获取到该对象的锁,执行对象的notify方法,然后释放锁,waiting状态才会转变为runnable
线程安全就是说多线程访问同一代码,不会产生不确定的结果
(主存与内存,线程是读取主存的变量的拷贝,操作完之后将结果写入主内存,多个线程操作可能导致,读取的虽然是同个数据,各自对自己的副本进行操作,写入主内存时,可能出现只保存其中一个线程的结果)
方式1:同步集合类,Vector,HashTable,
方式2:不可变容器类,unmodifiableXXX
方式3:读写分离,CopyOnWriteArrayList(对写加锁,读不加锁)
方式4:锁细化:1.7 的concurrentHashMap 分片锁, 1.8的头节点加锁
concurrentHashMap也有乐观锁的用法,在doPut 方法中,
方式5:skipList,无锁并发,利用概率算法计算出元素层(level),可实现o(lgn) 查找
插入节点时,需要判断后继节点之前插入marker,隔离防止并发操作
插入之后,新节点的next 指向marker 的next
方式5:跳跃表:ConcurrentSkipListMap,
a . hashmap (不安全)与hashTable(synchonized ,全表锁定) ,currentHashMap(分片锁,(1.7)(1.8 使用锁分离,只对头结点加锁)
b. ArrayList 与Vector
11. hashmap线程不安全,有线程安全的map吗?(并发包的currentHashMap(1.7分片锁),或者是Collections 下的unmodifierableMap,所有的写操作都是直接抛出异常)
10.hashmap与hashtable
后者是同步(速度就会降低)
迭代器:前者是fail-fast 类型,遍历过程不允许删除,后者(enumerator迭代器)允许
12. 保证线程安全的方式有哪些
可能面临的问题 | 解决方式 |
---|---|
ABA | cas 在操作值的时候,会检查值是否发生变化,如果没有发生变化,则更新,但是如果数据由A ->B->A ,检查发现A 没有改变,(事实上已经改变),解决思路使用 版本号,1A->2B->3A |
循环时间长,开销大 | CAS 如果循环时间过长,会对cpu造成较大的开销,解决思路:1. 如果jvm支持处理器提供的pause指令,可以延迟流水线执行指令,也可以避免在退出循环的时候因内存顺序冲突引起cpu流水线被清空 |
1,使用本地变量
2.使用不可边量
3.最小化锁的作用与范围
4. 使用线程池的Excutor,而不是直接new Thread 执行
5. 宁可使用同步,也不要使用现成的wait 与notify
6.使用BlockingQueueingQueue 实现生产-消费模式
7. 使用并发集合而不是使用加了锁的同步集合
8. 使用Semaphore 创建有界的访问
9. 宁可使用同步代码块,也不实用同步的方法
10. 避免使用静态变量(在并发情况下,使用final,使用只读集合)
线程调度方式 | 含义 |
---|---|
协同式调度 | 当前线程执行完,通知cpu切换到另一个线程(可能长时间阻塞,如果当前线程时长时间任务) |
抢占式调度 | 每个线程由系统来分配时间,线程切换不由线程自身决定,java 就是使用这种方式 |
注意:虽然java中使用抢占式调度,线程还是可以"建议"系统给自己分配多一些时间,设置线程优先级(java 有10个)
但是操作系统有可能自己来改变线程的优先级,比如windows,优先级推进器,发现某些工作效率很高的线程,那么会额外的给他更多的时间
三次握手与四次挥手
三次握手(为了确认双方发送与接受数据是没问题的)能保证一定建立连接吗?
a与b建立连接
第一次握手(syn=1,seq=client_isn) a说我想和你建立联系,你能不能收到我的消息啊(确认b能收到自己的消息)
第二次握手:(syn=1,seq=server_isn,ack=client_isn+1)b 回复可以啊,但是你能不能收到我发送的消息啊
第三次握手:(syn=0,seq=client_isn+1,ack=server_isn+1)a说,老弟,我能收到你的消息,赶紧发数据给我吧,老板催我干活呢
三次握手:保证了双方都信任对方能接受到自己的消息,那么就可以发正式的数据了
注:
ack 字段是为了告诉对方自己已经收到了你的ack-1 那条信息
seq 字段是为了标识这条记录是自己这边的编号,对方返回的ack 是基于我这个seq来的
四次挥手:假设A发起断开连接请求
A:“喂,我不说了。”A->FIN_WAIT1
B:“我知道了。等下,上一句还没说完。Balabala……”B->CLOSE_WAIT | A->FIN_WAIT2
B:”好了,说完了,我也不说了。”B->LAST_ACK
A:”我知道了。”A->TIME_WAIT | B->CLOSED
A等待2MSL,保证B收到了消息,否则重说一次”我知道了”,A->CLOSED
条件(全部满足发生死锁) | 描述 | 解决方式 |
---|---|---|
互斥 | 至少有一个资源处于非共享模式(即一次只有一个进程使用)如果另一个进程申请该资源,那么申请进程必须等到该资源被释放为止 | |
占有并等待 | 一个进程必须占有至少一个资源,并且等待另一个资源,而该资源为其他进程所占有 | |
非抢占 | 资源不能被抢占(即资源只能在进程完成任务后自动释放 | |
循环等待 | 一组等待进程,p0,pn ,p0 等待p1,p1等待p2,…pn 等待p1 |
解决或者是避免死锁的算法
银行家算法
哲学家吃饭问题
rpc(socket+反射)
rpc 是一种协议
实现此协议的有tomcat,dubbo 等等
什么是CPU流水线(将指令分成多步,不同指令各步操作重叠,从而实现几条指令并行处理,加速程序运行)
以洗车为例:A,B,C,D四辆车,都需要喷水->洗洁剂->擦洗->烘干四个步骤
如果不将洗车这个任务成多个小步骤,那么B 车必须等到A 烘干后才能开始洗车(有些步骤的场所就是空闲的)
如果使用流水线:
如果A 车到了洗洁剂,那么B 车可以前往喷水(这样很多步骤不会闲着,等其他步骤完成,提高了效率)
进程间通信的方式 | 描述 |
---|---|
套接字 | 不同主机上的进程间通信,交换任意的字节流 |
PCB:进程控制块,常驻内存,如果内存紧张,会将进程数据对换到磁盘,控制块挂起,当内存不紧张时,停止交换
控制块被调度时,将磁盘中进程读取到内存
名称 | 描述 | 分区排列顺序 | 特点 | 缺点 |
---|---|---|---|---|
首次适应 | 从头到尾到合适的分区 | 空闲分区以地址递增次序排列 | 综合看性能最好,算法开销小,回收分区后一般不需要对分区队列重新排序 | |
最佳适应 | 优先使用更小的分区,已保留更大的分区 | 空闲分区以容量递增次序排列 | 会有更多的大分区被保留,满足大进程使用 | 会产生很多太小、难以利用的碎片:算法开销大,回收分区可能需要对空闲分区队列重新排序 |
最坏适应 | 优先使用更大的分区,以防产生太小的不可用的内存碎片 | 空闲分区以容量递减排列 | 可以减少难以利用的小碎片 | 大分区容易被用完,不利于大进程:算法开销大,理由同上 |
临近适应 | 由首次适应演变而来,每次从上次查找结束位置开始查找 | 空闲分区以地址递增 | 不用每次从低地址的小分区开始检索,算法开销小,原因同首次适应算法 | 会使高分区的大分区也被用完 |
观察可以知道;首次适应,最佳适应,最坏适应,以及邻近适应中,首次适应是最佛系的,没有特别要求说是优先使用大,小空闲块,而是遇到合适的,就使用;不适合就继续往下找
首次适应于邻近适应的区别是:首次适应每次都是从头开始查找,邻近适应是从上次查找结束的位置开始
最佳适应与最坏适应区别是:前者是优先使用较小的空闲块,后者是优先使用大空闲块
innodb(事务性) vs Myisam(不支持事务)
innodb | Myisam |
---|---|
B+ | |
支持事务 | 不支持事务 |
支持表级锁和行级锁 | 只支持表级锁 |
redo log和undo log都属于InnoDB的事务日志
innodb 事务持久性的实现原理:使用redo log
数据存储在磁盘中,但是io速度很慢,于是有了缓冲(buffer pool),缓冲中保存磁盘中部分数据页的映射,
查询数据时,先到缓冲查询,,如果没有,读取磁盘,加入到缓冲,这提高了查询效率.
写数据也是先写入缓冲,定时将缓冲中数据刷到磁盘中(刷脏)
但是问题来了,如果突然断电,或者宕机,缓存中的数据就消失了,会导致丢失数据,无法保证事务持久性.
redo log 出现了,
1. 在日志将一个事务中需要执行的sql记录下来,
2. 接着往缓存写数据
3. 事物提交时,调用fsync 接口将redo log中的sql 执行刷入到磁盘
4. 如果宕机,只会丢失缓存中的数据,但是缓存中的数据是在记录到redolog 之后才添加的,也就是redo log还保留着数据的记录,读取redo log,执行操作,这就保证了事物的持久性
至于 redolog 也是磁盘io,为什么速度会比缓存中刷脏速度快呢?
5. 缓存刷脏数随机io,redolog是追加,属于顺序io
6. 刷脏是以数据页(Page)为单位的,MySQL默认页大小是16KB,
一个Page上一个小修改都要整页写入;而redo log中只包含真正需要写入的部分,无效IO大大减少
原子性,隔离性实现原理:undo log(原子性,单元要么都成功,要么都不成功一个出现失败,就需要回滚)
InnoDB实现回滚,靠的是undo log:当事务对数据库进行修改时,InnoDB会生成对应的undo log;
如果事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据
回滚到修改之前的样子。
undo log属于逻辑日志,它记录的是sql执行相关的信息。当发生回滚时,InnoDB会根据undo log的内容
做与之前相反的工作:对于每个insert,回滚时会执行delete;对于每个delete,回滚时会执行insert;
对于每个update,回滚时会执行一个相反的update,把数据改回去。
1.
读多写少:使用myisam
写多读少:(需要保证事物).使用innodb
B树 | B+树 |
---|---|
非叶子节点有数据 | 叶子节点无数据,只有key (节省空间,可存放更多的索引值),每个节点大小一致(一个界节点可以对应上磁盘上的一个数据页,避免了跨页查找) ,减少io,提高性能 |
叶子节点有数据 | 叶子节点有数据,并且有指针相连,成有序单链表(方便顺序查找) |
每行存储版本号,而不是存储事件实际发生的时间。
每次事物的开始这个版本号都会增加。自记录时间开始,每个事物都会保存记录的系统版本号。
化创建订单,减库存并行为串行
一般是:数据裤查库存,大于0,创建订单
但是如果此时是并行访问,可能只有1件商品,但是却创建了两件订单
改进:使用redis的 原子减操作,如果返回结果<0,表示没有库存,反回秒杀结束
但是又有另一个场景redis减库存成功,但是数据库创建订单失败
改进:化并行为串行,原子减操作>=0,
redis 设置 userid-goodsid 这样的记录,表示已经提交过秒杀订单,不能重复秒杀.
将秒杀请求放入消息队列
消费者取出消息队列中的订单请求数据,创建订单,减数据库秒杀库存
Note that it is generally necessary to override the {@code hashCode}
* method whenever this method is overridden, so as to maintain the
* general contract for the {@code hashCode} method, which states
* that equal objects must have equal hash codes.
hasmap 的普通方法:
key1,key2 如果equals 成立,但是没有重写hashcode,那么就能同时存在key1和key2 在hashmap中
在通过key1执行get方法时,通过equals 成立的key去获取数据,会出现获取到不同的数据
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
hashMap 的get 方法:
要求hash相同并且 (key == 或者equeals)
final Node<K,V> getNode(int hash, Object key) {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
http 协议
超文本传输协议,基于tcp实现
微信绑定登录时是如何实现的
如何获取openId 的??
在登录拦截器中,判断当前用户是否登录;
如果未登录,
调用微信api提供的一个接口(获取openid),主要参数:appid+回调接口地址(自己开发的)
微信api收到请求后,会回调你的接口
在自定义的回调接口中,需要调用weixin api的另一个接口,这时才能够返回openid
大致流程是这样的:
登录拦截器->
调用微信api接口1(带上appid,自定义回调接口1)
->微信调用自定义回调接口1(微信返回一个code)
->自定义回调接口1 处理逻辑:接收微信返回的code,然后再带上参数appid,请求微信api的2号接口
->微信api接口2返回openid
(有点像是三次握手,客户端发起请求,表明身份,服务端返回code,表示同意你请求,然后客户端带着code 去请求服务端获取数据)
5. 阅读计数怎么实现的
阅读记录表: userid-文章id生成一条记录(按照文章发布月份建立表,避免判断某篇文章是否被某人阅读还需要跨表查询的问题)
文章表 添加字段 :阅读次数,点赞数
前提:
文章表(文章id,title,content,点赞数,阅读数)
阅读记录表:(id,文章id,用户id,阅读时间,是否点赞)
阅读次数:同一用户阅读同一篇文章,重复阅读只算一次阅读
8 .如何获得阅读总数(某一篇文章)
先查缓存,存在,则返回
如果不存在,查数据库,并且存入缓存,返回
文章表添加字段记录总阅读数,总点赞数类似
10.阅读次数怎么更新,实时吗?
阅读次数 实时在缓存中更新,定时将缓存更新到数据库
查询文章详情时
1. 取缓存,是否存在用户阅读记录,
1.1 若存在,返回
2若不存在,则查数据库
2.1若依然不存在,存入阅读记录,缓存也存入一条记录,缓存中 总阅读次数+1
2.2如果数据库存在,也是存入缓存,返回(这里避免了短时间内反复阅读时,反复查询数据库,去判断是否需要插入阅读记录)
1. 查询缓存中是否存在点赞记录,
1.1如果存在,更新缓存中点赞状态;
2 如果不存在,取数据库点赞记录,
2.1 数据库存在,修改点赞状态(只是修改缓存中的点赞状态,数据库中此时还不修改)
2.2 如果不存在,数据库插入点赞记录;
2. 更新(或插入)缓存中点赞状态; 更新缓存中点赞总数
3. 定时器同步缓存中的点赞总数到数据库;同步各用户的点赞状态到数据库
1. 将按照文章发布年月来建立阅读记录表,一个月内发布的文章的阅读记录全部在同一张表,通过文章发布年月定位到阅读记录表名
2. 或者按照文章发布的日期,1-31 建立31张表,每月同一天发布的文章的阅读记录都放到同一张表
http 协议
如何分布式架构下的秒杀系统,如何设计,比如如何限流(令牌筒,扩容(负载均衡))
如何保证集群(主-从库的高可用,如果主库崩掉了咋办)
可投票选举新的主节点
崩掉的节点是有log的,redolog,重启会自动执行
消息队列分布式下面如何使用
线程池的配置方法(配置的理论依据,比如配置多少合适啊,)
需要考虑是cpu 密集型还是io密集型,如果是cpu密集,则表示cpu空闲时间少,线程数可以设置为N(cpu)+1
线程池的好处
a. 可以即时响应,任务到达时,不必创建线程后再执行
b. 提高线程的可管理性,线程是不能无限制创建的,线程池可以统一分配,调优,和监控
c. 降低资源消耗避免创建,销毁带来的消耗
线程池的实现原理
这里的是书上原话,感觉有问题!!
当一个任务到来时
a. 线程池查看核心线程是否都在工作
1. 如果不是,创建一个新的工作线程来执行任务
2. 否则,进入下一个步骤
b. 线程池判断工作队列是否已满
1. 如果没满,将新提交的任务储存在工作队列
2. 否则,进入下个流程
c. 线程池判断线程池中线程是否都处于工作状态
1. 如果没有,创建一个新的工作线程来执行任务
2. 否则交给饱和策略处理这个任务
线程池参数:
参数名 | 含义 |
---|---|
corePoolSize | 线程池基本大小,即使当前线程足够完成任务,但是线程数量小于该值,会继续创建线程 |
自我总结:不善于引导面试官去往自己擅长的(了解的方面引导)
比如:
(暗示了了四个可提问点,
1,数据结构实现(string,zMap,Hash,),可能还有事务等等
2,单线程有啥好处,
3,内存那就相对于磁盘了,数据存储方式
4,缓存可以延伸出适用场景)
暗示了其他提问点:
1. aop 与rdb有啥区别,具体说说(前者是基于日志(也就是文件),后者是基于二进制文件),持久化的效率不一样,
而且 配置方式也不一样,
2. memched也是key-val 型,这两者有啥区别? (可支持的数据类型,缓存淘汰机制,持久化,是否支持分布式,
等方面区别)
3. 订阅发布,与中间件,比如rabbitmq有啥区别?(可能需要从稳定性,分布式,使用场景等等方面去比较)
暗示:
4. 消息队列的其它使用场景
5. 消息队列使用到的设计模式(订阅-发布模式)
1.查询库存: (查数据库,多个线程查询数据库,对数据库造成压力,可以使用redis预减库存
,查库存可在redis 中进行,因为redis是单进程单线程的,并发的请求自然而然的变成了串行的方式)
2. 创建订单:如果是并发的创建订单,删减也可能会出现问题,当然数据库也有行级锁,表级锁,
还有MVCC,这些东西可以被暗示提出来
这可以使用redis 实现分布式锁,主要实现思路:
1.setNx
2.设置锁超时时间(比如锁最长持有10秒,这需要根据业务处理的时间来定,不能太长也不能太短),比如客户端挂了
锁没有释放,那么其他客户端一段时间可以获取到锁
3. 自旋获取锁,配置尝试次数
4. 限时获取锁,这指定时间内获取不到锁,就返回
5. 针对获取到锁的客户端没释放锁就挂掉的情况,其他客户端获取锁之前可以先setbnX,
如果失败,自旋获取当前锁超时时间,如果超时时间已经过了,删除锁,然后setNx,获取到锁
直观的感受是代替了new 操作
更多的是由于某些对象创建需要很多参数
工厂通过反射创建对象,其余参数配置放到配置文件
volitaile 变量在修改后,立刻将值刷回主内存,会将其他线程持有的该变量在本地内存置为失效,使得其他线程使用前必须从主内存读取
happens before :读在其他线程写之后(可以将其看成是一把锁,来理解)
因为在确定key的链表头结点位置时,是通过hash&size
如果不是2的n次幂
会出现:
比如 15
1111,全部是用上了
如果是10
0110 会有几位是浪费掉的
加入一个不一样的数,找出这个数
使用位运算异或:相同的数据异或结束坑定是0,0^任何数=数本身