开源中国 Java开发 一面 面经
1. 自我介绍
您好,我是XXX,目前就读于XX大学计算机科学与技术专业。我的技术方向是Java后端开发,对分布式系统和高并发场景有浓厚兴趣。
在技术能力方面,我熟练掌握Java语言和面向对象编程,熟悉Spring、Spring Boot、MyBatis等主流框架。数据库方面熟悉MySQL的使用和优化,了解Redis缓存的应用场景。对多线程编程、JVM原理、网络编程有一定理解。
项目经验方面,我做过电商平台、抽奖系统等项目。在抽奖系统项目中,我负责核心抽奖逻辑的设计和实现,包括奖品库存管理、中奖概率控制、防刷机制等。通过Redis缓存和异步处理优化了系统性能,支持每秒上千次的抽奖请求。
我的学习能力比较强,善于通过阅读源码和实践来深入理解技术。平时会关注开源社区,学习优秀项目的设计思想。我对技术充满热情,希望能加入贵公司,在实际项目中不断提升自己,为团队创造价值。
2. 死锁的概念,如何避免死锁?
死锁的定义:
死锁是指两个或多个线程互相持有对方需要的资源,导致所有线程都无法继续执行的状态。比如线程A持有资源1等待资源2,线程B持有资源2等待资源1,两个线程互相等待,永远无法继续。
死锁的四个必要条件:
互斥条件是指资源不能被多个线程同时使用,一个线程占用资源时,其他线程必须等待。
请求与保持条件是指线程已经持有至少一个资源,同时又请求新的资源,在等待新资源时不释放已有资源。
不可剥夺条件是指线程已获得的资源,在使用完之前不能被强制剥夺,只能由线程自己释放。
循环等待条件是指存在一个线程资源的循环等待链,每个线程都在等待下一个线程持有的资源。
避免死锁的方法:
第一种方法是破坏请求与保持条件。一次性申请所有需要的资源,要么全部获得,要么全部不获得。这样不会出现持有部分资源等待其他资源的情况。但这种方式资源利用率低,而且很多时候无法预知需要哪些资源。
第二种方法是破坏不可剥夺条件。当线程申请资源失败时,主动释放已持有的资源,等待一段时间后重新申请。但这种方式实现复杂,而且可能导致活锁。
第三种方法是破坏循环等待条件。给资源编号,线程必须按照固定顺序申请资源。比如规定必须先申请编号小的资源,再申请编号大的资源。这样就不会形成循环等待。这是最常用的方法。
第四种方法是使用超时机制。申请资源时设置超时时间,超时后放弃申请并释放已持有的资源。Java的tryLock方法就支持超时。
实际应用:
在实际开发中,要注意锁的申请顺序。如果多个线程都需要申请多个锁,要保证申请顺序一致。比如转账操作,从账户A转到账户B,需要同时锁定两个账户。如果一个线程先锁A再锁B,另一个线程先锁B再锁A,就可能死锁。正确的做法是按照账户ID大小排序,总是先锁ID小的账户。
使用JDK提供的并发工具类,比如ReentrantLock的tryLock方法,可以设置超时时间,避免无限等待。使用并发容器如ConcurrentHashMap,内部已经处理好了锁的问题,不容易出现死锁。
定期检查系统是否有死锁。可以使用jstack工具dump线程堆栈,查看是否有线程处于BLOCKED状态且互相等待。发现死锁后分析原因,调整锁的申请顺序。
3. 单链表如何找到中间节点?如何找到倒数第K个节点?
找中间节点:
使用快慢指针法。定义两个指针,快指针每次移动两步,慢指针每次移动一步。当快指针到达链表末尾时,慢指针正好在中间位置。
如果链表有奇数个节点,慢指针指向中间节点。如果有偶数个节点,慢指针指向中间两个节点的第一个。如果要返回第二个中间节点,可以让快指针初始指向head.next。
这个方法的时间复杂度是O(n),空间复杂度是O(1),只需要遍历一次链表。
找倒数第K个节点:
也使用双指针法。让第一个指针先走K步,然后两个指针同时移动。当第一个指针到达末尾时,第二个指针正好在倒数第K个位置。
要注意边界情况。如果K大于链表长度,第一个指针会走到null,这时要返回null或抛出异常。如果K等于链表长度,返回头节点。
这个方法的时间复杂度也是O(n),空间复杂度是O(1),只需要遍历一次链表。
给定节点找前一个节点:
单链表只能从前往后遍历,无法直接找到前一个节点。需要从头节点开始遍历,找到next指向给定节点的节点。
如果给定的是头节点,没有前一个节点,返回null。如果给定节点不在链表中,也返回null。
这个方法的时间复杂度是O(n),需要遍历链表。如果需要频繁查找前一个节点,可以使用双向链表,每个节点有prev指针指向前一个节点,这样查找前一个节点的时间复杂度是O(1)。
实际应用:
这些算法在实际开发中很有用。比如LRU缓存的实现,需要快速找到链表中间节点进行淘汰。比如链表的删除操作,需要找到待删除节点的前一个节点,修改它的next指针。
掌握这些基础算法,可以更好地理解和使用链表数据结构,在面试和实际工作中都很重要。
4. JWT令牌与Cookie+Session的区别,为什么使用JWT?
Cookie+Session机制:
传统的认证方式是Cookie+Session。用户登录后,服务器创建Session对象存储用户信息,生成Session ID返回给客户端。客户端把Session ID保存在Cookie中,后续请求都带上这个Cookie。服务器根据Session ID从Session存储中获取用户信息。
这种方式的缺点是服务器需要存储Session,占用内存。在分布式系统中,Session需要共享,要么使用Session复制,要么使用集中式Session存储如Redis。而且Cookie不能跨域,移动端APP无法使用Cookie。
JWT机制:
JWT是JSON Web Token的缩写,是一种无状态的认证方式。用户登录后,服务器生成JWT令牌返回给客户端。JWT包含用户信息和签名,客户端保存JWT,后续请求在Header中携带JWT。服务器验证JWT的签名,解析出用户信息。
JWT由三部分组成,用点号分隔。Header包含令牌类型和签名算法。Payload包含用户信息和过期时间等声明。Signature是对Header和Payload的签名,防止篡改。
JWT的优点:
第一是无状态,服务器不需要存储Session,节省内存。特别适合分布式系统和微服务架构,不需要Session共享。
第二是跨域支持好。JWT可以放在HTTP Header中,不受Cookie跨域限制。移动端APP可以方便地使用JWT。
第三是性能好。不需要每次请求都查询Session存储,只需要验证签名和解析JWT,速度更快。
第四是扩展性好。JWT的Payload可以包含任意信息,比如用户角色、权限等,不需要额外查询数据库。
JWT的缺点:
第一是无法主动失效。JWT一旦签发,在过期前一直有效,无法主动撤销。如果用户退出登录或修改密码,旧的JWT仍然可以使用。解决办法是设置较短的过期时间,或者维护一个黑名单。
第二是JWT体积大。JWT包含用户信息,比Session ID大很多,每次请求都要传输,增加网络开销。
第三是安全性问题。JWT的Payload是Base64编码,不是加密,任何人都可以解码看到内容。所以不能在JWT中存储敏感信息。而且如果密钥泄露,攻击者可以伪造JWT。
使用场景:
对于单体应用,Session方式更简单。对于分布式系统、微服务架构、移动端APP,JWT更合适。
在实际项目中,我使用JWT实现认证。用户登录后生成JWT,设置2小时过期。同时生成Refresh Token,设置7天过期。JWT过期后,用Refresh Token换取新的JWT,实现无感刷新。这样既保证了安全性,又提供了良好的用户体验。
5. 详细介绍抽奖系统的设计和实现
系统背景:
我做的抽奖系统是为电商平台设计的营销工具,用于促销活动。系统支持多种奖品、概率配置、每日抽奖次数限制、防刷机制等功能。日均抽奖次数在10万左右,高峰期每秒上千次请求。
核心功能:
奖品管理包括奖品的创建、库存设置、中奖概率配置。活动管理包括活动的创建、时间设置、参与条件配置。抽奖逻辑包括概率计算、库存扣减、中奖记录保存。用户管理包括抽奖次数限制、中奖记录查询、奖品发放。
技术架构:
后端使用Spring Boot构建,数据库使用MySQL存储活动、奖品、中奖记录等数据。Redis用于缓存热点数据、控制抽奖次数、扣减库存。RabbitMQ用于异步处理中奖通知、奖品发放等任务。
抽奖流程:
用户点击抽奖按钮,前端发送请求到后端。后端首先验证用户是否有抽奖资格,包
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
Java面试圣经,带你练透java圣经
