开源中国 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面试圣经,带你练透java圣经

全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务