1.基础相关
1.1 重载(overload)和重写(overried)的区别是什么?
1.2 final,finally,finalize的区别是什么?
1.3 接口和抽象类的区别是什么?
1.4 String类中常用的方法有哪些?
1.5 访问修饰符有哪些?
1.6 ==和equals的区别是什么?
1.7 Java类加载的过程是什么样的?
1.8 你了解哪些类加载器?
1.9 面向对象的特征有哪些?
1.10 String,StringBuffer,StringBuilder的区别是什么?
1.11 反射的作用是什么?怎么理解反射?
2 jvm相关
2.1 说说Jvm内存如何分配?
2.2 说一下Jvm垃圾回收机制?
2.3 Jvm内存如何调优?
2.4 说说Jvm内存泄漏和内存溢出。
2.5 如何判断对象是否可以被回收?
2.6 Jvm中垃圾回收算法有哪些?
3 异常相关
3.1 异常的体系结构
3.2 throw和throws的区别是什么
3.3 异常的解决方式
4 多线程和锁相关
4.1 线程和进程的区别是什么?
4.2 创建线程的方式有哪些?
4.3 多线程之间的通信方式有哪些?
4.4 线程的生命周期是什么?
4.5 sleep方法和wait方法有什么区别?
4.6 如何解决线程安全的问题?
4.7 介绍一下你了解的线程池
4.8 线程池的工作原理是什么样的?
4.9 你对锁了解多少?
常用的锁有,互斥锁和自旋锁,读写锁,乐观锁和悲观锁,其实具体使用哪种锁,需要分析业务场景中访问共享资源的方式,和访问共享资源的冲突概率。
加锁的目的:保证共享资源在任意时间里,只有一个线程访问,这样就可以避免多线程导致共享数据错乱的问题
互斥锁:是一种独占锁,比如当线程A加锁成功后,此时互斥锁已经被线程A独占了,只要线程A没有释放手中的锁,线程B加锁就会失败,内核会把线程的状态从「运行」状态设置为「睡眠」状态,于是就会释放CPU让给其他线程,既然线程B释放掉了CPU,自然线程B加锁的代码就会被阻塞。
所以,如果能确定被锁住的代码执行时间很短,就不应该用互斥锁,而应该选用自旋锁,否则使用互斥锁
自旋锁:自旋锁是通过 CPU 提供的 CAS 函数,在「用户态」完成加锁和解锁操作,不会主动产生线程上下文切换,所以相比互斥锁来说,会快一些,开销也小一些。当发生多线程竞争锁的情况,加锁失败的线程会「忙等待」,直到它拿到锁。
互斥锁和自旋锁区别
- 互斥锁加锁失败后,线程会释放 CPU ,给其他线程;
- 自旋锁加锁失败后,线程会忙等待,直到它拿到锁;
读写锁:
- 当「写锁」没有被线程持有时,多个线程能够并发地持有读锁,这大大提高了共享资源的访问效率,因为「读锁」是用于读取共享资源的场景,所以多个线程同时持有读锁也不会破坏共享资源的数据。
- 但是,一旦「写锁」被线程持有后,读线程的获取读锁的操作会被阻塞,而且其他写线程的获取写锁的操作也会被阻塞
- 根据读优先锁和写优先锁会出现一个问题,就是一方获取资源后不释放,会导致另一方永远获取不到,这种情况可以使用公平锁,利用队列把获取锁的线程排队。
乐观锁:乐观锁做事比较乐观,它假定冲突的概率很低,它的工作方式是:先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。在线文档及svn,git就是一个乐观锁的场景,用户可以一起编辑,提交的时候判断版本号,如果冲突就修改提交。
悲观锁:悲观锁做事比较悲观,它认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁。前面提到的互斥锁为悲观锁,自旋锁为乐观锁,读写锁可以为乐观锁或者悲观锁。
总结:不管使用的哪种锁,我们的加锁的代码范围应该尽可能的小,也就是加锁的粒度要小,这样执行速度会比较快。再来,使用上了合适的锁,就会快上加快了
4.10 什么是死锁?
4.11 如何解决死锁?
4.12 死锁产出的条件
4.13 分布式锁实现的方式有哪些?
4.14 项目中如何采用分布式锁
5 集合相关
5.1 java中的集合有哪些?
5.2 HashMap底层原理
5.3 ArrayList 和 Linkedlist 区别?
5.4 concurrentHashMap的底层结构?
5.5 hashMap 和 和 hashTable 的区别?
6 javaweb相关
6.1 说一下servlet的生命周期
6.2 cookie 和 和 session 的区别?
6.3 cookie 浏览器禁止后,session 还有效么?
7 锁
7.1 常用的锁
常用的锁有,互斥锁和自旋锁,读写锁,乐观锁和悲观锁,其实具体使用哪种锁,需要分析业务场景中访问共享资源的方式,和访问共享资源的冲突概率。
加锁的目的:保证共享资源在任意时间里,只有一个线程访问,这样就可以避免多线程导致共享数据错乱的问题
互斥锁:是一种独占锁,比如当线程A加锁成功后,此时互斥锁已经被线程A独占了,只要线程A没有释放手中的锁,线程B加锁就会失败,内核会把线程的状态从「运行」状态设置为「睡眠」状态,于是就会释放CPU让给其他线程,既然线程B释放掉了CPU,自然线程B加锁的代码就会被阻塞。
所以,如果能确定被锁住的代码执行时间很短,就不应该用互斥锁,而应该选用自旋锁,否则使用互斥锁
自旋锁:自旋锁是通过 CPU 提供的 CAS 函数,在「用户态」完成加锁和解锁操作,不会主动产生线程上下文切换,所以相比互斥锁来说,会快一些,开销也小一些。当发生多线程竞争锁的情况,加锁失败的线程会「忙等待」,直到它拿到锁。
互斥锁和自旋锁区别
- 互斥锁加锁失败后,线程会释放 CPU ,给其他线程;
- 自旋锁加锁失败后,线程会忙等待,直到它拿到锁;
读写锁:
- 当「写锁」没有被线程持有时,多个线程能够并发地持有读锁,这大大提高了共享资源的访问效率,因为「读锁」是用于读取共享资源的场景,所以多个线程同时持有读锁也不会破坏共享资源的数据。
- 但是,一旦「写锁」被线程持有后,读线程的获取读锁的操作会被阻塞,而且其他写线程的获取写锁的操作也会被阻塞
- 根据读优先锁和写优先锁会出现一个问题,就是一方获取资源后不释放,会导致另一方永远获取不到,这种情况可以使用公平锁,利用队列把获取锁的线程排队。
乐观锁:乐观锁做事比较乐观,它假定冲突的概率很低,它的工作方式是:先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。在线文档及svn,git就是一个乐观锁的场景,用户可以一起编辑,提交的时候判断版本号,如果冲突就修改提交。
悲观锁:悲观锁做事比较悲观,它认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁。前面提到的互斥锁为悲观锁,自旋锁为乐观锁,读写锁可以为乐观锁或者悲观锁。
总结:不管使用的哪种锁,我们的加锁的代码范围应该尽可能的小,也就是加锁的粒度要小,这样执行速度会比较快。再来,使用上了合适的锁,就会快上加快了
7.2 分布式锁一般有三种实现方式:
- 数据库乐观锁;
- 基于Redis的分布式锁 setnx;
- 基于ZooKeeper的分布式锁。
7.3 分布式锁需满足以下四个条件:
- 互斥性。在任意时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
在项目中我们采用redis的分布式锁的方式,对秒杀和活动的库存进行控制,在项目中有个活动是用户抢优惠券,在每周三,周五早8,10都会有抢优惠券活动,需求是这样的,当用户领完一张优惠券之后,优惠券的数量必须减一,如果优惠券抢光了,就不允许再抢了。
在实现的时候,先从数据库中读出优惠券的数量进判断,当优惠券大于0,就允许用户进行领取优惠券,然后将优惠券的和数量减一,再写回数据库,当时考虑请求比较多,就使用了三台服务器去做分流,当时做的时候就出现一个问题:其中一台服务器上的 A 应用获取到了优惠券的数量之后,由于处理相关业务逻辑,未及时更新数据库的优惠券数量;在 A 应用处理业务逻辑的时候,另一台服务器上的 B 应用更新了优惠券数量。那么,等 A 应用去更新数据库中优惠券数量时,就会把 B 应用更新的优惠券数量覆盖掉。
当时考虑了三种解决方式:第一就是使用sql语句更新数据库,但是在没有分布式锁的时候优惠券可能出现负数,就是两个服务器同时发起抢券请求,都满足大于0的条件,就出现负数了,第二是使用乐观锁,但是会存在卡顿的情况,就是数据如果更新补上,导致长时间重试,第三就是使用redis的分布式锁了,通过锁互斥,防止多个客户端去同时更新优惠券数量。
当时想到的就是使用redis的setnx命令,就是set if not exist,当key设置成功后,返回1,否则就返回0,所以这里 setnx 设置成功可以表示成获取到锁,如果失败,则说明已经有锁,可以被视作获取锁失败。释放的话直接使用del,把key删除 就可以了,利用这个特性,让系统执行优惠券逻辑之前,先去redis中执行setnx命令,再根据指令的结果,判断是否获取到锁,如果获取到了就继续执行任务,执行完在使用del释放,如果没有获取到就等一段时间,再重新获取锁。其实在运行的过程中还碰到一个问题,就是持有锁的应用突然崩溃了,会造成死锁,因为没有释放,其他应用没机会获取锁,就造成了线上事故,当时是在key上加了一个过期时间,这样如果出现了问题,在一段时间后也会释放锁,不过setnx本身没办法设置超时时间,这里我用的lua脚本实现的redis的原子性,它的核心命令就是eval使用setnx命令后,再使用expire命令给key设置过期时间。释放都使用del。
其实除了在项目之外我还自己使用过redission(看门狗),因为其实使用redis+lua虽然实现了原子性,但是也存在问题,就是redis自身的隐患,因为lua脚本是用在redis的单实例上面,一旦redis本身出现问题,那么分布式锁就没法用,所以要搞成高可用的,但是redis的主从同步有延迟,这个延迟会产生一个边界条件,当主机的redis被建好锁了,还没有同步到从机,主机宕机了,然后从机升为主机,这个时候从机是没有主机设置好的锁数据,这个时候锁丢了,所以就可以采用redission开源的redis客户端,它会对集群中的每个 Redis,挨个去执行设置 Redis 锁的脚本,也就是集群中的每个 Redis 都会包含设置好的锁数据。
其实这个也存在争议,具体使用还得看项目适合哪种。