Java
基础
集合
ArrayList
- 底层是Object数组;使用无参构造函数创建的话默认为空数组,添加第一个元素后为10。
- 线程不安全。
- 头插时间复杂度O(n),尾插时间复杂度O(1),指定位置插入时间复杂度O(n)。
- 删除元素跟插入元素一样。
- 数组支持快速随机访问,所以查询时间复杂度O(1)。
- 每次扩容为原来1.5倍。
HashMap
- JDK1.7:底层采用Entry数组+链表实现;线程不安全。
- put数据时,对key的hashcode进行hash运算,在根据数组的长度获取要插入的下标位置;
- 当获取到的下标一样时,则插入到链表头部。
- 当元素数量达到CapacityLoadFactor时(默认160.75=12),进行扩容,扩容为原来的两倍,遍历原有数组,重新hash到新的数组,完成扩容。
- 头插会出现链表循环,假设多个线程刚好都同时触发扩容,会对数组中的元素重新hash。
- 默认大小为16。
- JDK1.8:底层采用Node数组+链表+红黑树实现;线程不安全。
- 当链表长度大于8时链表转为红黑树,但是如果数组大小小于64时优先选择扩容。
- 插入数据使用尾插法。
- 默认大小为16.
ConcurrentHashMap
JDK1.7 的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟 HashMap1.8 的结构一样,数组+链表/红黑树
JDK1.7ConcurrentHashMap把数组分为一个个Segment数组,Segment继承ReentrantLock,每个Segment又相当于一个entry数组,数组每个元素又是个链表。默认 Segment 的个数是 16 个,相当于最大并发16.
JDK1.8ConcurrentHashMap为Node数组,并发控制使用 synchronized 和 CAS 来操作。
并发
并发编程的源头?
1.可见性:由于CPU与内存之间的速度差异级别很大,CPU 增加了缓存,以均衡与内存的速度差异;对于多核CPU而言,由于每个线程跑在单独的CPU上,都对应有自身的缓存,多线程产生并发时,假设有两个线程要执行累加操作,每个线程从主内存读入数据(X=0),随后各自执行+1操作,缓存里都是1,随后各自写回主内存,这时主内存的值为1,而不是2。
2.原子性:线程切换带来的原子性问题,原本以为的x+=1操作是一条指令,但在CPU层面,是三条指令,把X从主内存加载到CPU寄存器,执行+1操作,写回内存。而线程切换是发生在CPU指令层面,也就是说多线程执行+=1操作,也会产生并发问题。
3.有序性:编译器为了优化性能,会重新调整语句的顺序,前提是调整前后逻辑不变。在双重检查创建单例的例子里
public class Singleton {
static Singleton instance;
static Singleton getInstance(){
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
new Singleton的操作,实际上是三步操作:
- 分配一块内存 M;
- 在内存 M 上初始化 Singleton 对象;
- 然后 M 的地址赋值给 instance 变量。
指令重排后,顺序会变成1,3,2。当多线程同时执行这代码块的时候,线程A先执行到new操作,而new操作具体执行到1,3后发生线程切换,此时线程B执行到if判断instance是否为空,此时判断不为空,就return出去,后续访问该对象,会报空指针异常,原因是该对象还没进行初始化就返回了。
什么是java内存模型?解决了什么问题?
Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。具体来说,这些方法包括 volatile、synchronized 和 final 三个关键字,以及六项Happens-Before 规则(前面一个操作的结果对后续操作是可见的):
- 程序的顺序性规则:一个线程中,按照程序顺序,前面的操作 Happens-Before 于后续的任意操作。
- volatile 变量规则:对一个 volatile 变量的写操作, Happens-Before 于后续对这个 volatile 变量的读操作。
- 传递性:如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C。
- 管程中锁的规则:对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。
- 线程 start() 规则:它是指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。
- 线程 join() 规则:主线程 A 等待子线程 B 完成(主线程 A 通过调用子线程 B的 join() 方法实现),当子线程 B 完成后(主线程 A 中 join(方法返回),主线程能够看到子线程的操作。
java内存模型解决了可见性和有序性问题。
synchronized和Lock的区别?
- synchronized是JVM提供的关键字,Lock以API的方式提供。
- synchronized用来修饰方法或代码块,修饰静态方法或修饰静态代码块,默认使用当前类对象进行加锁;修饰普通方法或普通代码块,默认使用当前对象进行加锁。
- synchronized会自动加锁解锁,Lock需要手动加锁解锁。
- synchronized和lock默认都是非公平锁,但是Lock可以创建公平锁。
- synchronized只有支持一个竞态条件,而Lock支持多个,通过condition。
- synchronized配套使用wait,notify,notifyAll; Lock配套使用await,signal,signalAll。
- Lock支持响应中断,lockInterruptibly() 方法。synchronized不支持。
- Lock支持非阻塞获取锁跟超时获取锁,tryLock()方法,tryLock(long time,TineUnit unit)方法。
java线程生命周期?
- NEW(初始化状态)
- RUNNABLE(可运行 / 运行状态)
- BLOCKED(阻塞状态)
- WAITING(无时限等待)
- TIMED_WAITING(有时限等待)
- TERMINATED(终止状态)
创建多少线程合适?
CPU密集型:CPU核数+1;IO密集型:CPU 核数 * [ 1 +(I/O 耗时 / CPU 耗时)]
线程池了解吗,如何创建线程池?
ThreadPoolExecutor,使用自定义参数创建线程池,不要使用内置线程池。原因是内置的线程池使用的是无界队列或者是核心线程数使用Integer最大值。
ThreadPoolExecutor有七大参数:核心线程数,最大线程数,工作队列,最大线程存活时间,存活时间单位,创建线程工厂,拒绝策略。
synchronized 锁升级了解吗?
无锁,偏向锁,轻量级锁,重量级锁。
什么是死锁,如何解决?
线程1持有锁A,申请锁B,线程2持有锁B,申请锁A,此时两个线程持有各自要申请的共享资源,相互等待。
死锁只能重启应用。
避免死锁的方法:
- 破坏占有且等待:申请共享资源一起申请,要么都获取成功,要么都失败。
- 破坏循环:申请资源的时候,资源按照一定的顺序排序,避免循环。
什么是活锁?什么是饥饿?
活锁指的是有时线程虽然没有发生阻塞,但仍然会存在执行不下去的情况,好比如路人甲从左手边出门,路人乙从右手边进门,两人为了不相撞,互相谦让,路人甲让路走右手边,路人乙也让路走左手边,结果是两人又相撞了。解决方式:谦让时加个随机时间。
饥饿指的是某些线程一直获取不到锁,没能执行代码块。解决方式:可采用公平锁,但是公平锁比非公平锁效率低。
sleep和wait区别?
- sleep是Thread的方法;wait是Object的方法。
- sleep任何地方都可以用,需要捕获异常;wait需要在synchronized代码块或synchronized方法内使用。
- sleep不会自动释放锁,wait会自动释放锁。
JVM
新特性
Redis
基本数据结构
常用的有string,list,hash,set,ZSet。不常用的有HyperLogLogs(基数统计)、Bitmap (位存储)、Geospatial (地理位置)
reids为什么那么快
1.单线程:Redis 单线程是指它对网络 IO 和数据读写的操作采用了一个线程,而采用单线程的一个核心原因是避免多线程开发的并发控制问题。单线程的 Redis 也能获得高性能,跟多路复用的 IO 模型密切相关,因为这避免了 accept() 和 send()/recv() 潜在的网络 IO 操作阻塞点。
2.基于内存
redis持久化方式
redis有哪些持久化方式?
1.AOF日志(Append Only File):记录的是执行的redis命令,执行完命令后,在写日志,保证日志的正确性。
2.RDB快照(Redis Database):保存的是redis实例在某一时刻的内存数据。
AOF的落盘时机?
1.aways:同步写回,每执行完一个redis命令后立刻将日志写回磁盘。
2.everysec :每秒写回,每执行完一个redis命令后,先将日志写到AOF文件的内存缓冲区,每隔一秒写回磁盘。
3.no:操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
AOF优缺点?
优点:
1.日志记录的是一个个redis命令,记录日志的时候快。
2.命令执行完后才记录日志,不会阻塞当前写操作。
缺点:
1.日志记录的是一个个redis命令,在进行日志回放数据的时候,相当于一条条执行命令,效率慢。
什么是AOF重写?会阻塞主线程吗?
AOF重写指的是在重写时,Redis 根据数据库的现状创建一个新的 AOF 文件,也就是说,读取数据库中的所有键值对,然后对每一个键值对用一条命令记录它的写入。AOF重写具备多变一能力,使原本多条命令合并成一条。
AOF重写不会阻塞主线程,由后台线程bgrewriteaof完成。重写具体来说分为一处拷贝,两处日志。
拷贝指的是重写时主线程会fork出子线程,然后把主线程的内存数据拷贝给子线程。
两处日志指的是:1.原本的AOF日志缓冲区。2.AOF重写的日志缓冲区。
执行RDB会阻塞主线程吗?
不会。
save:在主线程中执行,会导致阻塞;
bgsave:创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞,这也是 Redis RDB 文件生成的默认配置。
RDB优缺点?
优点:
和 AOF 相比,RDB 记录的是某一时刻的数据,并不是操作,所以,在做数据恢复时,可以直接把 RDB 文件读入内存,很快地完成恢复。
缺点:
1.每次都进行全量备份,效率慢。
2.备份时间间隔不好把控,太短的话频繁做内存快照导致系统开销大,数据量大写入磁盘对磁盘压力大;每次fork子线程后虽然不会阻塞主线程,但是fork这个操作会阻塞主线程,内存实例太大会导致fork时间增加,也就导致主线程无法读写。
RDB+AOF混合模式了解吗?
redis4.0后提出RDB+AOF混合使用,内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作
redis缓存应用
是更新缓存还是删缓存?
删缓存。
更新缓存的话,如果先更新缓存再更新数据库,或者先更新数据库再更新缓存,都会存在并发问题。
线程A先更新缓存A,线程B更新缓存B,线程B更新数据库B,线程A更新数据库A。此时缓存里的值是B数据库是A。
线程A先更新数据库A,线程B更新数据库B,线程B更新缓存B,线程A更新缓存A。此时缓存里的值是A数据库是B。
先更新数据库,再删除缓存,还是先删除缓存,再更新数据库?
先删除缓存,后更新数据库
如果有 2 个线程要并发「读写」数据,可能会发生以下场景:
1.线程 A 要更新 X = 2(原值 X = 1)
2.线程 A 先删除缓存
3.线程 B 读缓存,发现不存在,从数据库中读取到旧值(X = 1)
4.线程 A 将新值写入数据库(X = 2)
5.线程 B 将旧值写入缓存(X = 1)
最终 X 的值在缓存中是 1(旧值),在数据库中是 2(新值),发生不一致。
可见,先删除缓存,后更新数据库,当发生「读+写」并发时,还是存在数据不一致的情况。
先更新数据库,后删除缓存
依旧是 2 个线程并发「读写」数据:
1.缓存中 X 不存在(数据库 X = 1)
2.线程 A 读取数据库,得到旧值(X = 1)
3.线程 B 更新数据库(X = 2)
4.线程 B 删除缓存
5.线程 A 将旧值写入缓存(X = 1)
最终 X 的值在缓存中是 1(旧值),在数据库中是 2(新值),也发生不一致。
但实际上,这种情况发生的概率很低。需要刚好缓存失效,刚好读写并发,并且3+4的操作要比2+5的时间短,更新操作一般都是比读操作慢的,所以一般采用先更新数据库,后删除缓存。
为什么要引入消息队列保证一致性?
除了并发问题会导致缓存与数据库的一致性外,先更新数据库,后删除缓存这个操作分为两步操作。假设更新数据库后,删除缓存操作没能执行成功,也会导致一致性问题。一般可以采用重试机制,但是直接重试很大概率也会失败,或者重试次数不好把控,而且重试会一直「占用」这个线程资源,无法服务其它客户端请求。所以一般采用异步重试,异步重试采用引入消息队列,将重试请求发送到消息队列,由专门的消费者去重试。或者更直接的做法,为了避免第二步执行失败,我们可以把操作缓存这一步,直接放到消息队列中,由消费者来操作缓存。
延时双删有什么问题?
在先删除缓存,后更新数据库的操作中,由于并发读写的原因,导致读操作将原本数据库中的旧值写入到缓存中,导致数据不一致。所以可以在删除缓存,后更新数据库的操作后面在加上一个延迟删除缓存的操作。这样就可以错误的缓存就可以删除。但是这个延迟的时间不好把控,需要将延迟的时间设置成大于上述线程B读数据库+写缓存的操作。
什么是缓存雪崩?缓存击穿?缓存穿透?
一.缓存雪崩:是指大量的应用请求无法在 Redis 缓存中进行处理,紧接着,应用将大量请求发送到数据库层,导致数据库层的压力激增。
原因:1.缓存中有大量数据同时过期。2.Redis 缓存实例发生故障宕机了,无法处理请求,这就会导致大量请求一下子积压到数据库层,从而发生缓存雪崩
解决方案:设置不同过期时间。服务降级。保证redis高可用,构建主从节点集群,确保主节点挂了从节点能替代上。
二.缓存击穿:针对某个访问非常频繁的热点数据的请求,无法在缓存中进行处理,紧接着,访问该数据的大量请求,一下子都发送到了后端数据库,导致了数据库压力激增,会影响数据库处理其他请求。
原因:热点数据过期失效。
解决方案:对于访问特别频繁的热点数据,不设置过期时间。
三.缓存穿透:是指要访问的数据既不在 Redis 缓存中,也不在数据库中,导致请求在访问缓存时,发生缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据。
原因:业务层误操作:缓存中的数据和数据库中的数据被误删除了,所以缓存和数据库中都没有数据;恶意攻击:专门访问数据库中没有的数据
解决方案:缓存空值或缺省值。使用布隆过滤器快速判断数据是否存在,避免从数据库中查询数据是否存在,减轻数据库压力。
reids分布式锁
为什么需要分布式锁?
单机锁只能解决单个应用并发问题,如果应用集群部署,此时多个应用都会操作共享资源,如果还是使用单机锁,那就各锁各的,相当于还是可以并发执行。所以需要有一个共享访问的第三方系统可以提供原子性操作。
如何实现使用redis实现分布式锁?
加锁操作采用set ${key} ${value} ex ${expire time} nx 命令。保证set命令跟expire设置过期时间命令原子性。设置过期时间是为了万一客户端拿到锁后,由于某些原因导致锁不释放。加锁操作中key需要有一个唯一标识,给后续释放锁的时候判断,否则就会释放了别人的锁。
解锁操作采用lua脚本,先通过get命令获取自己的锁,拿到之后执行del操作。
锁过期怎么解决?
开启一个「守护线程」,定时去检测这个锁的失效时间,如果锁快要过期了,操作共享资源还未完成,那么就自动对锁进行「续期」,重新设置过期时间。redisson已经实现此功能,其中内置的守护线程称为watch dog 看门狗
Spring
bean的生命周期?
spring中bean的形式以beandefination存在
通过反射实例化对象==》 设置对象属性==》检查是否实现xxxAware接口==》执行beanPostProcesor前置处理==》如果实现initialingBean,执行 afterPropertiesSet==》检查是否有自定义initMethod==》执行beanPostProcesor后置处理==》使用中==》检查是否实现DisposableBean==》检查是否实现自定义distory方法
spring常用注解?
Bean相关:@Component,@Controller,@Service,@Repository,@Configuration,@Bean,@AutoWrite
全局异常相关:@ControllerAdvice,@ExceptionHandler
控制层相关:@ResponseBody,@RequestBody,@PathVariable,@RequestParams,@RequestMapping
事务:@Transactional
获取配置:@Value,@ConfigurationProperties
其他:@ConditionOnXXX
spring mvc执行流程?
spring启动时扫描@Conrtoller修饰的类,将url和对应的方法存储在handlerMapping中,请求过来时,经过DispatchServlet,根据url获取handlerAdapte,然后调用其handler方法,返回modelAndView对象,由视图解析器解析modelAndView对象并返回给浏览器。
spring用到哪些设计模式?
工厂模式:BeanFactory。
观察者模式:SpringEvent,具体使用方法:定义一个事件类继承ApplicationEvent,被观察者处注入ApplicationEventPublisher对象,调用publishEvent方法通知观察者,观察者通过@EventListener注解监听事件。
代理模式:SpringAop底层基于动态代理实现,如果被代理类实现接口,使用JDK动态代理,否则使用cglib。
装饰器模式:SpringCache,Spring通过装饰器模式增强缓存功能,如支持事务的缓存TransactionAwareCacheDecorator
适配器模式:SpringMvc中,因为定义一个controller类有多种方式,比如@Controller,实现Controller接口,或者继承HttpServlet接口;使用注解的话方法名是自定义的,实现Controller接口的话是实现handleRequest方法,继承HttpServlet接口则是实现service方法。那再DispatchServlet中,就会存在多层if判断这个controller是哪种实现,并执行对应的方法。所以Spring使用适配器重新定义出HandlerAdapte接口,每种controller都实现这个适配器,在DispatchServlet中就可以直接获取适配器并执行适配器中的方法。
责任链模式:Spring的拦截器Interceptor。
spring事务传播行为?
require(默认):如果当前存在事务,则加入当前事务,如果当前没有事务,则新建事务。
require new : 创建一个新事务,如果当前存在事务,则将其挂起。
nested:如果当前存在事务,则以嵌套事务方式运行,如果当前没有事务,跟require一样。
mandatory: 如果当前存在事务,则加入事务,否则抛异常。
support:如果当前存在事务,则加入事务,否则以非事务方式。
not support:以非事务方式运行,如果当前存在事务,则将其挂起。
never:以非事务方式运行,如果当前存在事务,则抛异常。
spring aop了解多少?
底层基于动态代理实现,可以将业务代码与非业务代码解耦,如常用的监控,限流,统计,事务,幂等等功能可以基于spring aop实现。
MySql
MyBatis
MQ
如何保证消息不丢失?
- 消息发送阶段:一般消息队列都是用请求确认机制,客户端发送消息给broker,broker会返回确认响应给客户端,客户端发送消息代码处理好异常即可,同步发送捕获异常,异步发送则正确处理好回调。
- 消息存储阶段:如果broker为单实例,则可以配置消息刷盘后才响应给发送端。如果broker为多节点,至少将消息发送到2 个以上的节点,再给客户端回复发送确认响应。
- 消息消费阶段:消费阶段跟发送阶段一样采用请求确认机制,当客户端拉取消息处理完业务逻辑后在返回确认响应给broker即可。
如何处理消息重复消费?
消费端做业务幂等。
- 可以根据业务结合数据库唯一索引,在消费消息时检查数据是否已经存在,不存在则消费消息。
- 可以使用数据库的乐观锁,给数据添加一个版本号,每次更新数据前,比较当前版本和消息中的版本是否一致,如果不一致则拒绝更新,更新数据后版本号+1。
- 可以使用全局唯一id,在发送消息时,给每条消息指定一个全局唯一的 ID,消费时,先根据这个 ID 检查这条消息是否有被消费过,如果没有消费过,才更新数据,然后将消费状态置为已消费。