在40岁老架构师尼恩的(50+)读者社区中,经常有小伙伴,需要面试阿里、 百度、头条、美团、京东等大厂。
下面是一个小伙伴成功拿到通过了阿里三次技术面试,小伙伴通过三个多小时技术拷问,最终拿到 offer。
从这些题目来看:阿里的面试,偏重底层知识和原理,大家来看看吧。
现在把面试真题和参考答案收入咱们的宝典,大家看看,收个阿里Offer需要学点啥?
当然对于中高级开发来说,这些面试题,也有参考意义。
这里把题目以及参考答案,收入咱们的《尼恩Java面试宝典》 V78,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
Redis是一种开源的、基于内存的数据库系统,它可以用作数据库、缓存和消息中间件。
Redis是一种高性能的开源key-value内存数据库,它支持各种复杂数据结构,例如字符串、哈希、列表、集合等。Redis的特点是速度快,可以处理非常大的数据集,而且具有很强的可扩展性和数据持久化功能。
Redis的源码是用C语言编写的,它采用单线程模型,通过异步IO和事件驱动机制实现高并发。其架构包括网络层、客户端请求处理、命令执行引擎、键空间与过期管理、持久化、复制、Sentinel(哨兵)以及集群等模块。
Redis具有以下特点:
Redis应用场景包括:
Redis的重功能及作用:
Redis支持多种数据结构,如字符串、散列表、列表、集合和有序集合等。Redis支持多种操作,如读取、写入、删除、事务处理、消息传输等。Redis还支持多种复杂的数据操作,如映射、排序、过滤等。
Redis的源码,宏观层面主要分成两大部分:
redis客户端与redis服务器之间的通信是通过TCP协议进行的。
Redis服务器源码包括以下几个部分:
Redis服务器源码非常庞大和复杂,如果要完整地研究和理解Redis的源码,需要有较强的编程能力和C/C++语言基础。
Redis集群是由多个Redis实例构成的分布式系统,它能够扩展到非常大的数据集,并提供高可用性和负载均衡功能。
Redis集群的功能作用:
Redis集群适合以下使用场景:
Redis集群的两种主要的实现方式:
1. Redis Sentinel实现
Redis Sentinel是一个特殊的Redis实例,它可以监控其他Redis实例的健康状态,并在发生故障时自动进行故障转移。Redis Sentinel通过主从复制实现故障转移,当主服务器出现故障时,Sentinel会将一个从服务器升级为新的主服务器,从而保证系统的高可用性。
Redis Sentinel实现的优点是实现简单,只需要开启Sentinel服务即可;缺点是不支持数据的水平扩展,通常适用于小规模集群环境。
2. Redis Cluster实现
Redis Cluster是Redis官方提供的集群实现方式,它采用分区(sharding)方式来保证系统的可扩展性和高可用性,每个数据片段被存储在不同的节点上,从而实现水平扩展。
Redis Cluster采用哈希槽位映射算法来对数据进行分片,每个节点负责一部分哈希槽位,当需要访问某个键值对时,客户端会先计算该键的哈希值,然后根据哈希槽位映射算法找到负责该哈希槽位的节点,从而实现负载均衡。
Redis Cluster实现的优点是支持数据的水平扩展和自动故障转移等功能;缺点是实现相对复杂,需要在多个节点上运行Redis实例,并进行相关配置。但是,在大规模集群环境下,Redis Cluster是更为合适的选择。
综上所述,Redis Sentinel实现适合小规模集群环境,而Redis Cluster实现适合大规模集群环境。
保证 MySQL 数据不丢有多种方法,主要包括以下几点:
1. 数据库备份:
定期备份数据库,以防止数据丢失。可以使用 MySQL 自带的备份工具或者第三方备份工具来进行备份。
2. 数据库复制:
使用 MySQL 的主从复制或者多主复制来进行数据备份和灾备。在主从复制中,主库写入数据后,从库会自动同步数据。在多主复制中,多个主库之间相互同步数据。
3. 数据库事务
使用数据库事务来保证数据的一致性和完整性。在事务中,如果某个操作失败,整个事务会回滚到之前的状态,从而保证数据不丢失。
4. 数据库高可用:
使用数据库集群或者主备架构来保证数据库的高可用性。
在集群或主备架构中,当一个节点出现故障时,可以自动切换到备用节点,从而保证服务的连续性和数据的不丢失。
5. 数据库监控:
使用数据库监控工具来监控数据库的运行状态,及时发现并解决潜在的问题,从而保证数据的安全和可靠性。
数据库读写分离,主要解决高并发时,提高系统的吞吐量。
读写分离数据库模型如下
方案一:强制走主库
● 写请求是直接写主库,然后同步数据到从库
● 读请求一般直接读从库,除非强制读主库
在高并发场景或者网络不佳的场景,如果存在较大的主从同步数据延迟,这时候读请求去读从库,就会读到旧数据。这时候最简单暴力的方法,就是强制读主库。
方案二:缓存标记法
在高并发场景或者网络不佳的场景,如果存在较大的主从同步数据延迟,这时候读请求去读从库,就会读到旧数据。这时候最简单暴力的方法,就是强制读主库。但是这样就违背了读写分离的初衷。
优化方案就是使用缓存标记法:
更新主库数据,并在缓存中设置一个标记,表示数据已更新。发起读请求,先判断数据已更新的标识,在缓存中有更新标记。则走主库;如果没有,请求走从库。
这个方案解决了数据不一致问题,但是每次请求都要先跟缓存打交道,会影响系统吞吐。
如何防止大流量请求把缓存击垮,可以引入多级缓存的架构。
关于多级缓存架构,请参见40岁老架构师尼恩的100W三级缓存组件架构的原理和实操,那个非常重要。
秒杀系统首先是一个分布式后台系统,首先来看看,如何设计一个分布式后台系统?
何设计一个分布式后台系统主要从需要考虑以下几个方面:
1. 架构设计:
秒杀系统需要采用分布式架构,将请求分散到多个服务器上,以提高系统的并发能力和稳定性。可以使用负载均衡器来分发请求,使用缓存技术来减轻数据库的压力。
2. 数据库设计:
秒杀系统需要采用高性能数据库,例如Redis等,来存储商品信息和用户订单信息。可以使用缓存技术来减轻数据库的压力,同时使用数据库事务来保证数据的一致性和完整性。
3. 接口设计:
秒杀系统需要设计高性能的接口,以应对高并发的请求。可以采用异步处理的方式,将请求放入消息队列中,异步地处理请求,从而提高系统的并发能力。
4. 安全设计:
秒杀系统需要采用安全措施,防止恶意攻击和刷单等行为。
可以采用验证码、IP限制、用户限制等方式来保证系统的安全性和公平性。
5. 系统测试:
秒杀系统需要进行充分的系统测试,包括压力测试、性能测试、安全测试等,以保证系统的可靠性和稳定性。
6. 业务设计:
秒杀系统需要设计合理的业务规则,例如限制每个用户的购买数量、限制每个商品的秒杀数量等,以保证系统的公平性和可持续性。
综上所述,设计一个分布式后台系统,需要考虑多个方面,需要综合考虑系统的性能、安全、可靠性和公平性等因素。
秒杀系统首先是一个高并发后台系统,再来看看,在高并发的情况下,设计秒杀系统需要考虑那些问题。
秒杀具有持续时间短和并发量大的特点,秒杀持续时间只有几分钟,而一般公司都为了制造轰动效应,会以极低的价格来吸引用户,因此参与抢购的用户会非常的多,短时间内会有大量请求涌进来。
一般在秒杀时间点
(比如:双十一12点)前几分钟,用户并发量才真正突增,达到秒杀时间点时,并发量会达到顶峰。活动是大量用户抢少量商品的场景,其实绝大部分用户秒杀会失败,只有极少部分用户能够成功。
峰值持续的时间其实是非常短的,很容易出现瞬时高并发的情况,下面用一张图直观的感受一下流量的变化:
像这种瞬时高并发的场景,传统的系统很难应对
所以,高并发秒杀,需要额外的从高并发、超高并发的维度,进行架构设计和架构优化:
为了防止流量过大,造成系统崩溃或者无法正常使用,需要对流量进行限制。
使用限流措施来控制请求流量,可以保证系统的稳定性和可用性。
例如,使用令牌桶算法(Token Bucket Algorithm)或漏桶算法(Leaky Bucket Algorithm)来限制请求速率。
为啥要限流呢?
很多刷子用户,并不会像我们一样老老实实,通过秒杀页面点击秒杀按钮,抢购商品。他们可能在自己的服务器上,模拟正常用户登录系统,跳过秒杀页面,直接调用秒杀接口。
如果是我们手动操作,一般情况下,一秒钟只能点击一次秒杀按钮。
但是如果是服务器,一秒钟可以请求成上千接口。这种差距实在太明显了,如果不做任何限制,绝大部分商品可能是被机器抢到,而非正常的用户,有点不太公平。
所以,我们有必要识别这些非法请求做一些限制。
目前有两种常用的限流方式:
秒杀最终的本质是数据库的更新,但是有很多大量无效的请求,我们最终要做的就是如何把这些无效的请求过滤掉,防止渗透到数据库。
为了避免高并发导致系统崩溃,可以采用降级策略,即当系统压力过大时,降低系统的并发量,保证系统的稳定性。
为了提高系统的性能,可以采用数据库分库分表的方式,将数据拆分成多个表,以提高查询效率。
为了减轻数据库的负载,可以采用缓存的方式,将一些热门数据缓存在内存中,加快查询速度。
使用缓存技术来减轻数据库的负担,可以提高系统的性能和可用性。例如,使用Redis或Memcached等内存缓存来缓存热点数据,减少数据库的访问次数。
为了避免阻塞主线程,可以采用异步的方式,将一些耗时的操作放到子线程中,避免阻塞主线程。
使用异步处理来处理大量请求,可以提高系统的吞吐量和响应速度。例如,使用mq异步处理 来处理请求。
我们都知道在真实的秒杀场景中,有三个核心流程:
而这三个核心流程中,真正并发量大的是秒杀功能,下单和支付功能实际并发量很小。
所以,我们在设计秒杀系统时,有必要把下单和支付功能从秒杀的主流程中拆分出来,特别是下单功能要做成mq异步处理的。而支付功能,比如支付宝支付,是业务场景本身保证的异步。
于是,秒杀后下单的流程变成如下:
为了提升下单的效率,并且防止下单服务的失败。
需要将下单这一操作进行异步处理。最常采用的办法是使用消息队列,消息队列最显著的三个优点:异步、削峰、解耦。这里可以采用RocketMQ,在后台经过了限流、库存校验之后,流入到这一步骤的就是有效请求。
然后发送到队列里,队列接受消息,异步下单。
下完单,入库没有问题可以用短信通知用户秒杀成功。假如失败的话,可以采用补偿机制,重试。
秒杀详情页面是用户流量的第一入口,所以是并发量最大的地方。
如果这些流量都能直接访问服务端,恐怕服务端会因为承受不住这么大的压力,而直接挂掉。
秒杀详情页面绝大多数内容是固定的,比如:商品名称、商品描述、图片等。为了减少不必要的服务端请求,通常情况下,会对秒杀详情页面做静态化
处理。
用户浏览商品等常规操作,并不会请求到秒杀服务端。只有到了秒杀时间点,并且用户主动点了秒杀按钮才允许访问秒杀服务端。这样能过滤大部分无效页面请求。
但只做页面静态化还不够,因为用户分布在全国各地,有些人在北京,有些人在成都,有些人在深圳,地域相差很远,网速各不相同。
为了性能考虑,一般会将css、js和图片等静态资源文件提前缓存到CDN上,它的全称是Content Delivery Network,即内容分发网络。
CDN 让用户能够就近访问秒杀页面,降低网络拥塞,提高用户访问响应速度和命中率。
采取安全措施来保护系统的安全性,包括防止SQL注入、XSS攻击等。例如,对用户输入的数据进行过滤和验证,使用SSL/TLS加密通信等。
为了及时发现系统的异常情况,可以采用监控和报警的方式,及时发现和解决问题。
从业务视角,再来看看,秒杀系统有哪些特殊的业务架构难题:
分析秒杀的业务场景,最重要的有一点就是超卖问题,
在多个用户同时发起对同一个商品的下单请求时,先查询商品库存,再修改商品库存,会出现资源竞争问题,导致库存的最终结果出现异常。问题:
当商品A一共有库存15件,用户甲先下单10件,用户乙下单8件,这时候库存只能满足一个人下单成功,如果两个人同时提交,就出现了超卖的问题。
常见的解决的三种方案
通过悲观锁解决超卖
通过乐观锁解决超卖
通过分段执行的排队方案解决超卖
悲观锁 和乐观锁的性能都比较低。尼恩在这里,重点介绍 高性能版本的 异步分段锁方案 。
分阶段排队下单方案
将提交操作变成两段式:
申请阶段:
将存库从MySQL前移到Redis中,所有的预减库存的操作放到内存中,由于Redis中不存在锁故不会出现互相等待,并且由于Redis的写性能和读性能都远高于MySQL,这就解决了高并发下的性能问题。
确认阶段:
然后通过队列等异步手段,将变化的数据异步写入到DB中。
引入队列,然后数据通过队列排序,按照次序更新到DB中,完全串行处理。当达到库存阀值的时候就不在消费队列,并关闭购买功能。这就解决了超卖问题。
异步分段锁架构图
基于异步分段锁的性能提升
一个高性能秒杀的场景:
假设一个商品1分钟6000订单,每秒的 600个下单操作。
在排队阶段,每秒的 600个预减库存的操作,对于 Redis 来说,没有任何压力。甚至每秒的 6000个预减库存的操作,对于 Redis 来说,也是压力不大。
但是在下单阶段,就不一样了。假设加锁之后,释放锁之前,查库存 -> 创建订单 -> 扣减库存,经过优化,每个IO操作100ms,大概200毫秒,一秒钟5个订单。600个订单需要120s,2分钟才能彻底消化。
如何提升下单阶段的性能呢?
可以使用Redis 分段锁。
为了达到每秒600个订单,可以将锁分成 600 /5 =120 个段,每个段负责5个订单,600个订单,在第二个阶段1秒钟下单完成。
有关Redis分段锁的详细知识,请阅读下面的博文:
Redis分布式锁 (图解-秒懂-史上最全)
基于异步分段锁优点:
解决超卖问题,库存读写都在内存中,故同时解决性能问题。
基于异步分段锁缺点:
由于异步写入DB,可能存在数据不一致,存在某一时刻DB和Redis中数据不一致的风险。
可能存在少买,也就是如果拿到号的人不真正下订单,可能库存减为0,但是订单数并没有达到库存阀值。
对于普通用户来讲,看到的只是一个比较简单的秒杀页面,在未达到规定时间,秒杀按钮是灰色的,一旦到达规定时间,灰色按钮变成可点击状态。这部分是针对小白用户的,如果是稍微有点电脑功底的用户,会通过F12看浏览器的network看到秒杀的url,通过特定软件去请求也可以实现秒杀。或者提前知道秒杀url的人,一请求就直接实现秒杀了。
这个问题我们需要考虑解决。
现在的秒杀大多都会出来针对秒杀对应的软件,这类软件会模拟不断向后台服务器发起请求,一秒几百次都是很常见的,如何防止这类软件不断发起的的重复无效请求进行限流,是我们需要认真考虑的。
接口防刷的话,需要入手的方面很多:
(1)前端限流
首先第一步就是通过前端限流,用户在秒杀按钮点击以后发起请求,那么在接下来的5秒是无法点击(通过设置按钮为disable)。这一小举措开发起来成本很小,但是很有效。
(2)同一个用户xx秒内重复请求直接拒绝
具体多少秒需要根据实际业务和秒杀的人数而定,一般限定为10秒。
具体的做法就是通过Redis的键过期策略,首先对每个请求都从String value = redis.get(userId);
如果获取到这个value为空或者为null,表示它是有效的请求,然后放行这个请求。如果不为空表示它是重复性请求,直接丢掉这个请求。
如果有效,采用redis.setexpire(userId,value,10).value可以是任意值,一般放业务属性比较好,这个是设置以userId为key,10秒的过期时间(10秒后,key对应的值自动为null)。
(3)对同一ip限流
有时候只对某个用户限流是不够的,有些高手可以模拟多个用户请求,这时需要用Nginx加同一ip限流功能。
(4) 加验证码
通常情况下,用户在请求之前,需要先输入验证码。用户发起请求之后,服务端会去校验该验证码是否正确。只有正确才允许进行下一步操作,否则直接返回,并且提示验证码错误。
此外,验证码一般是一次性的,同一个验证码只允许使用一次,不允许重复使用。
(5)业务层检查
业务服务层进行订单的数量检查,一个用户超过设置的订单数量, 下单失败。
很多请求进来,都需要后台查询库存,这是一个频繁读的场景。
可以使用Redis来预减库存,在秒杀开始前可以在Redis设值,比如redis.set(goodsId,100),这里预放的库存为100可以设值为常量,每次下单成功之后,Integer stock = (Integer)redis.get(goosId);
然后判断sock的值,如果小于常量值就减去1;
不过注意当取消的时候,需要增加库存,增加库存的时候也得注意不能大于之间设定的总库存数(查询库存和扣减库存需要原子操作,此时可以借助lua脚本)下次下单再获取库存的时候,直接从Redis里面查就可以了。
Object类是所有Java类的基类,它里面定义了一些通用的方法,如下:
除此之外,Object类还提供了许多其他的静态方法和构造方法,例如:
在分布式环境下开发时,经常会遇到分布式事务问题。
分布式事务是一种解决分布式系统中的事务一致性问题的方案。在分布式系统中,由于各个节点之间的数据存储和处理是分离的,因此容易出现数据不一致的问题。
而分布式事务可以确保在分布式系统中的多个节点之间进行的多个操作的原子性、一致性和持久性,从而保证分布式系统的可靠性和可用性。
简单点来说,分布式事务是指涉及到多个数据库或应用服务器的事务,需要保证所有操作都要么全部成功,要么全部失败。
通常情况下,业务项目上,基本上的分布式事务可以使用以下两种方式实现:
1. 两阶段提交(2PC)
2PC是一种经典的分布式事务协议,它将分布式事务划分为两个阶段:准备阶段和提交阶段。在准备阶段,各参与节点进行数据校验,并向一个协调器发送是否同意提交事务的消息;在提交阶段,协调器向各个参与节点发送提交请求,参与节点执行事务并向协调器发送完成消息。
2PC方案的优点是简单易懂,适用于较小规模的分布式事务场景,但它存在单点故障风险和性能瓶颈等问题。
2. TCC事务
TCC事务是一种比较新颖的分布式事务方案,它将事务分为Try、Confirm、Cancel三个阶段,分别对应资源预留、执行确认、异常撤销等操作。TCC事务通过代码层面的控制,实现了无锁化和高性能的分布式事务处理。
TCC方案的优点是性能较好,且没有2PC的单点故障问题,但需要开发人员自己实现事务控制逻辑,实现较为复杂。
总之,在分布式环境下需要使用分布式事务方案来保证多个节点间数据的一致性。2PC和TCC都是比较常用的方案,需要根据具体业务场景选择合适方案。
什么场景用分布式事务,有其他方案嘛?
一般在保证数据的一致性和可靠性的高并发、分布式场景,使用分布式事务,比如秒杀系统中,通常会使用分布式事务来保证数据的一致性和可靠性。
使用分布式事务的主要原因是,在高并发场景下,如果每个请求都直接访问数据库,可能会出现数据不一致的情况。例如,当一个用户同时下单和支付时,如果这两个操作分别被不同的请求处理,就可能出现订单信息不完整或者支付失败的情况。而使用分布式事务可以将多个操作打包成一个事务,确保这些操作要么全部提交成功,要么全部回滚失败。
除了分布式事务,还有一些其他的方案可以用于实现高并发场景下的秒杀系统。
其中一些方案包括:
总之,如何使用分布式事务,是否使用分布式事务,决于具体的业务需求和系统架构设计。
在实际应用中,通常需要综合考虑多个因素来选择最合适的方案。
分布式事务是指涉及到多个数据库或应用服务器的事务,需要保证所有操作要么全部成功,要么全部失败。为了解决这种问题,目前主要有以下几种分布式事务解决方案:
1. 两阶段提交(2PC)
2PC是最经典的分布式事务协议之一,它将分布式事务划分为两个阶段: 准备阶段和提交阶段。在准备阶段,各参与节点进行数据校验,并向协调器发送是否同意提交事务的消息;在提交阶段,协调器向各个参与节点发送提交请求,参与节点执行事务并向协调器发送完成消息。
2PC方案的优点是较为简单易懂,适用于较小规模的分布式事务场景。但是,它存在单点故障风险和性能瓶颈等问题。
2. TCC事务
TCC事务是一种相对新颖的分布式事务方案,它将事务分为Try、Confirm、Cancel三个阶段,分别对应资源预留、执行确认、异常撤销等操作。TCC事务通过代码层面的控制,实现了无锁化和高性能的分布式事务处理。
TCC方案的优点是性能较好,且没有2PC的单点故障问题,但需要开发人员自己实现事务控制逻辑,实现较为复杂。
3. Saga事务
Saga事务是一种将分布式事务拆解为多个本地事务的方案,它通过在各个本地事务之间传递消息,最终完成整个分布式事务。Saga事务与2PC和TCC不同,它可以实现更加灵活的事务处理,但需要开发人员自行设计和实现事务处理逻辑。
4. 消息队列
消息队列是一种异步通信方式,可以通过消息队列来保证数据的最终一致性。在分布式事务中,可以使用消息队列将事务处理过程异步化,从而提高系统的并发能力和可靠性。
综上所述,分布式事务有2PC、TCC、Saga事务和消息队列等多种解决方案。需要根据具体业务场景选择合适的方案。
JDK6、7、8提供了许多不同的新特性,以下是其中一些的详细说明:
JDK6新特性:
JDK7新特性:
JDK8新特性:
总之,JDK6、7、8提供了许多不同的新特性,包括并发API增强、Lambda表达式、Stream API、新的日期/时间API等。这些新特性可以帮助Java开发人员更加高效和方便地开发应用程序。
HTTPS(Hyper Text Transfer Protocol Secure)是一种通过加密和认证来保护网络通信安全的协议。它是HTTP的安全版,可以有效地防止黑客窃听、中间人攻击等网络安全问题,广泛应用于电子商务、在线支付、社交媒体等领域。
HTTPS的原理可以简单概括为:使用公开密钥加密算法对数据进行加密,确保数据在传输过程中不被窃取或篡改;使用数字证书验证服务端身份,确保用户连接的是正规的服务器。
HTTPS的工作流程如下:
总之,HTTPS通过SSL/TLS协议对数据传输进行了加密和解密,保证了通信的安全性。
关于Https协议的底层原理和抓包分析, 请参见 尼恩《Java高并发核心编程 卷1 加强版》 PDF。 最新消息,尼恩三部曲+面试题,帮助小伙伴涨薪一倍多。
Redis的持久化是指将Redis中的数据写入磁盘,以便在Redis重启后能够恢复数据。Redis提供了两种持久化方式:RDB和AOF。
1. RDB持久化
RDB持久化是将Redis在内存中的数据定期dump到磁盘中,生成一个RDB文件。RDB文件是一个二进制文件,包含了Redis在某个时间点上的数据快照。RDB文件的生成可以手动触发,也可以通过配置文件设置定期自动触发。
RDB持久化的原理是Redis会fork出一个子进程,将数据写入到一个临时文件中,然后将临时文件替换为旧的RDB文件。在这个过程中,Redis会将所有新的写操作缓存在内存中,直到持久化完成后再将缓存的写操作应用到新的RDB文件中。
2. AOF持久化
AOF持久化是将Redis的写操作以追加的方式写入到一个日志文件中,即AOF文件。AOF文件是一个文本文件,包含了Redis的所有写操作,以及执行这些操作所需的参数。AOF文件的生成可以手动触发,也可以通过配置文件设置定期自动触发。
AOF持久化的原理是Redis会将所有新的写操作追加到AOF文件中,当AOF文件过大时,Redis会自动执行一次重写操作,将AOF文件中的冗余操作删除,从而减小AOF文件的大小。
总的来说,RDB持久化和AOF持久化各有优劣。RDB持久化的优点是生成的文件较小,恢复数据的速度较快;而AOF持久化的优点是数据的完整性更好,可以做到每秒钟一次的持久化,从而减少数据的丢失。在实际使用中,可以根据实际情况选择适合自己的持久化方式。
volatile是Java中的一种关键字,用于确保多线程环境下变量的可见性和有序性。
在多线程编程中,使用volatile可以解决一些内存可见性和指令重排序问题,从而避免了一些难以调试和排查的问题。
作用:
原理:
在Java内存模型中,每个线程都有自己的工作内存和主内存。当一个线程要读取共享变量的值时,它会先将该变量的值从主内存中复制到自己的工作内存中,然后再执行操作;当一个线程要写入共享变量的值时,它会先将该变量的值写入自己的工作内存中,然后再将其刷新到主内存中。
volatile关键字通过以下两种方式保证多线程环境下的可见性和有序性:
需要注意的是,volatile只能保证单个变量的原子性操作,对于复合操作或者涉及多个变量的操作,还需要使用synchronized等同步机制来保证线程安全。
总之,volatile关键字可以保证多线程环境下变量的可见性和有序性,通过将变量的值立即更新到主内存中来实现。在多线程编程中,使用volatile可以提高程序的并发性和可靠性。
关于 java jmm volatile 的实现原理, 请参见 尼恩《Java高并发核心编程 卷2 加强版》 PDF。 最新消息,尼恩三部曲+面试题,帮助小伙伴涨薪一倍多。
Java中的JMM(Java内存模型)是一种规范,用于定义多线程程序中的内存访问行为。它确保了多线程程序的正确性和可见性,从而避免了常见的线程安全问题。
在JMM中,volatile是一种关键字,用于确保变量的可见性和顺序性。当一个变量被声明为volatile时,它的值将始终从主存中读取,而不是从线程的本地缓存中读取。当一个线程修改了一个volatile变量的值时,这个值将立即被写回主存中,而不是在一段时间后才被写回。
volatile的实现原理是通过内存屏障(memory barrier)来实现的。
内存屏障是一种硬件或软件机制,用于确保内存操作的顺序性和可见性。
在Java中,volatile变量的读写操作会被编译器和CPU优化,为了确保操作的顺序性和可见性,Java会在编译时和运行时插入内存屏障。
具体来说,当一个线程访问一个volatile变量时,它会执行一个load指令,这个指令会强制从主存中读取变量的值,并将其存储在线程的本地缓存中。当一个线程修改一个volatile变量的值时,它会执行一个store指令,这个指令会强制将变量的值写回主存中。在这些指令周围,Java会插入内存屏障,确保指令的顺序性和可见性。
总之,volatile通过内存屏障来实现变量的可见性和顺序性,从而确保了多线程程序的正确性和可靠性。
关于 java jmm volatile 的实现原理, 请参见 尼恩《Java高并发核心编程 卷2 加强版》 PDF。 最新消息,尼恩三部曲+面试题,帮助小伙伴涨薪一倍多。
秒杀场景是指在短时间内出现高并发请求的情况,为了保护系统的稳定性,需要对请求进行限流。常用的限流算法有以下几种:
1. 令牌桶算法
令牌桶算法是一种固定窗口限流算法,它通过令牌桶来控制请求的频率。令牌桶中会不断产生令牌,并将其放入桶中,每当一个请求到达时,就从桶中获取一个令牌,如果桶中没有令牌,则请求被拒绝。
2. 漏桶算法
漏桶算法也是一种固定窗口限流算法,它通过一个带有固定速率的漏桶,来控制请求的频率。当请求到达时,先进入漏桶中,然后以固定速率流出,如果漏桶已满,则请求被拒绝。
3. 计数器算法
计数器算法是一种简单的限流算法,它通过记录请求次数和时间戳,来控制请求的频率。当请求到达时,判断当前时间与最近一次请求的时间差是否小于设定阈值,如果小于,则请求被拒绝。
总之,在秒杀场景中,需要对请求进行限流来保护系统的稳定性。常用的限流算法有令牌桶算法、漏桶算法、计数器算法等,需要根据实际情况选择合适的算法。
七层网络模型是一种分层的网络模型,它将网络分为七个层次,每一层都有自己的功能和特点。其中,最底层的是物理层,它主要负责将比特流转换为电信号,并进行物理传输。
TCP/IP 协议是一种常用于网络通信的协议,它由四个层次组成,分别是应用层、传输层、网络层和数据链路层。其中,传输层是一个非常重要的层次,它负责在不同的网络之间传递数据,并保证数据的可靠传输。
在传输层中,TCP 协议使用三次握手来确保数据的可靠传输。这三次握手分别是:
这三次握手的过程中,客户端和服务器会交换很多数据包,但是每一次握手都是独立的,即每一次握手只传输一个数据包。只有当三次握手都成功完成后,客户端和服务器才会建立起一个 TCP 连接,并开始传输数据。
通过三次握手,TCP 协议可以确保数据的可靠传输,即使在数据传输过程中出现了一些问题,TCP 协议也可以通过重新发送数据包来恢复数据的传输。这也是 TCP 协议被广泛使用的重要原因之一。
线程池是一种常见的线程管理机制,它可以提高线程的利用率和性能,并且可以避免线程创建和销毁的开销。线程池中包含一组可重用的线程,这些线程可以被多个任务共享,从而减少了线程创建和销毁的开销。当有新的任务到来时,线程池会从池中取出一个空闲的线程来执行任务,当任务执行完成后,线程会返回到池中,等待下一个任务的到来。
线程池的工作原理如下:
线程池的优点主要有以下几点:
总之,线程池是一种非常有用的线程管理机制,它可以提高程序的性能、可靠性和可扩展性,是多线程编程中不可缺少的一部分。
关于线程池它的工作原理, 请参见 尼恩《Java高并发核心编程 卷2 加强版》 PDF。 最新消息,尼恩三部曲+面试题,帮助小伙伴涨薪一倍多。
首先,说一下概念误区,jvm内存模型很容易被误解为 JvM内存结构。
JVM内存结构是指Java程序在运行时所使用的内存结构和组织方式。Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存区域划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有些区域随着虚拟机进程的启动而存在,有些区域则是依赖线程的启动和结束而建立和销毁。
Java 虚拟机所管理的内存被划分为如下几个区域:
和jvm内存结构不同,JVM(Java Virtual Machine)内存模型定义了Java程序中各种变量的访问规则,包括变量的读取、写入和同步等操作,以确保多线程程序的正确性和可靠性。
JVM内存模型主要分为两个部分:线程工作内存和主内存。
线程工作内存是每个线程独有的内存区域,它包含了线程执行时所需要的所有变量和对象,以及线程执行的指令集。每个线程都有自己的工作内存,线程之间的变量不共享,因此线程之间的通信需要通过主内存来进行。
主内存是所有线程共享的内存区域,它包含了所有的变量和对象。当一个线程需要访问某个变量时,它首先需要将变量从主内存中读取到自己的工作内存中,然后对变量进行操作。操作完成后,线程需要将变量的值写回主内存,以便其他线程可以看到这个变量的最新值。
JVM内存模型通过锁和内存屏障等机制来确保多线程程序的正确性和可靠性。锁用于同步多个线程对共享变量的访问,内存屏障用于保证变量的可见性和顺序性。在Java中,volatile关键字可以用来保证变量的可见性和顺序性,synchronized关键字可以用来保证线程的互斥和同步,而Lock接口和Atomic类可以用来实现更灵活的同步机制。
关于jvm内存模型原理, 请参见 尼恩《Java高并发核心编程 卷2 加强版》 PDF。 最新消息,尼恩三部曲+面试题,帮助小伙伴涨薪一倍多。
垃圾回收机制是Java程序中非常重要的一部分,它可以自动地处理程序中不再使用的对象,从而避免内存泄漏的问题。
Java中有两种垃圾回收机制:
Java中的内存分代模型:
1. 内存分代模型
分代模型并不是一种垃圾回收算法,而是一种内存管理模型。
将java中的内存分为不同区域,在GC时不同区域采用不同的算法,提高回收效率。
内存分代模型将java堆内存中的区域分成两部分新生代(new)和老年代(old),两块区域的默认比例为1:2。
新生代:新生代中的对象被使用过,并且被引用过。
老年代:一旦新对象生成,就会被放入老年代中。老年代中的对象没有被使用过,也没有被引用过。
新生代又分为一个伊甸园区(eden)和两个存活区(survivor),s0和s1,默认比例为8:1:1
新生代的GC被称为YGC/MinorGC,老年代的GC被称为Full GC/MajorGC。
2. 分代垃圾回收算法
垃圾回收的过程一般是由Java虚拟机自动进行的,开发人员不需要手动干预。
但是,开发人员可以通过设置垃圾回收的参数来控制垃圾回收的频率和方式。垃圾回收机制对于Java程序的性能和稳定性都非常重要,需要开发人员认真对待。
类加载机制是指Java虚拟机(JVM)如何加载和链接Java程序中的类。在Java中,类的加载和链接是由JVM负责完成的,JVM会在运行时动态地将类加载到内存中,并进行链接、初始化等操作。
类加载机制指的是:虚拟机将描述类的数据从class文件加载到内存中,对加载的数据进行验证,解析,初始化,最后得到虚拟机认可后转化为直接可以使用的java类型的过程
类加载机制一共有七个阶段:加载,验证,准备,解析,初始化,使用,卸载。其中的验证,准备,解析合称为连接阶段。
加载,验证,准备,初始化,卸载的顺序是确定是,另两个由动态绑定等情况可能会在初始化后面。
画个草图:
双亲委派模型是Java类加载机制的核心概念之一。它是一种基于层次结构的类加载模型,用于描述Java类加载器之间的父子关系。简单来说,双亲委派模型就是“委托”父类加载器去加载子类的类,而不是自己直接去加载。
类加载器有是三个:启动类加载器、扩展类加载器、应用程序加载器(系统加载器)
具体来说,双亲委派模型包含以下几个阶段:
总之,双亲委派模型通过将类加载委托给父类加载器来实现对类的管理和控制,从而保证了Java类的安全性、可靠性和稳定性。
工作过程是:如果一个类加载器收到了一个类加载的请求,它首先不会去加载类,而是去把这个请求委派给父加载器去加载,直到顶层启动类加载器,如果父类加载不了(不在父类加载的搜索范围内),才会自己去加载。
双亲委派模型的意义在于不同的类之间分别负责所搜索范围内的类的加载工作,这样能保证同一个类在使用中才不会出现不相等的类,举例:如果出现了两个不同的Object,明明是该相等的业务逻辑就会不相等,应用程序也会变得混乱。
进程间通信(IPC,InterProcess Communication)的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。下面简述一下这六种方式:
综上所述,不同的进程间通信方式各有优缺点,应根据具体的应用场景选择合适的方式。通常情况下,管道适用于亲缘关系进程之间的通信;消息队列适用于无连接的进程间通信;共享内存适用于需要高效数据共享的场景;套接字适用于需要跨平台、跨语言的网络通信。
String、StringBuffer 和 StringBuilder 都是 Java 中用于处理字符串的类,它们的区别如下:
在使用上,一般而言:
总结:优先考虑StringBuffer!
第三方库是指由其他开发者创建和维护的,可以用于解决特定问题或实现特定功能的开源代码库。
Java 作为一门广泛应用于企业级开发的语言,拥有许多优秀的第三方库,以下是其中一些常用的库及其作用和应用场景:
以上是一些常用的Java第三方库,它们可以帮助开发人员简化Java编程,提高开发效率和代码质量。
工厂模式: 是Java中最常用的设计模式之一, 它提供了一种创建对象的最佳方式.
在工厂模式中, 我们在使用工厂类创建对象时 不会对客户端暴露 创建逻辑, 并且是通过使用一个共同的接口来指向新创建的对象
三种实现方法
简单工厂模式: 定义一个创建对象的工厂类, 由工厂类决定创建出哪一种对象的实例, 工厂类内部已封装创建出哪一种对象的实例的逻辑代码
Phone 手机接口
interface Phone {
// 运行手机的抽象方法
void run();
}
IPhone 苹果手机实现类
class IPhone implements Phone{
@Override
public void run() {
System.out.println("运行苹果手机");
}
}
HuaweiPhone 华为手机实现类
class HuaweiPhone implements Phone {
@Override
public void run() {
System.out.println("运行华为手机");
}
}
PhoneFactory 定义一个创建Phone对象实例的手机工厂类, 内部已封装创建出哪一种品牌手机实例的逻辑代码
class PhoneFactory {
public Phone createPhone(String phoneType) {
Phone phone = null;
if ("IPhone".equals(phoneType)) {
phone = new IPhone();
} else if ("HuaweiPhone".equals(phoneType)) {
phone = new HuaweiPhone();
}
return phone;
}
}
测试
public class SimpleFactory {
public static void main(String[] args) {
// 创建手机工厂类
PhoneFactory phoneFactory = new PhoneFactory();
// 通过手机工厂类创建 IPhone手机实例对象
Phone IPhone = phoneFactory.createPhone("IPhone");
IPhone.run(); // 运行苹果手机
// 通过手机工厂类创建 HuaweiPhone手机实例对象
Phone HuaweiPhone = phoneFactory.createPhone("HuaweiPhone");
HuaweiPhone.run(); // 运行华为手机
}
}
简单工厂模式: 是使用工厂类来创建不同实例的对象, 可以将创建对象的方法静态化, 代码更简洁明了
public class PhoneFactory {
public static Phone createPhone(String phoneType) {
Phone phone = null;
if ("IPhone".equals(phoneType)) {
phone = new IPhone();
} else if ("HuaweiPhone".equals(phoneType)) {
phone = new HuaweiPhone();
}
return phone;
}
}
// 可以直接通过 PhoneFactory.createPhone("phoneType") 创建手机对象
简单工厂模式存在问题
工厂方法模式: 先定义一个工厂父类 (负责定义创建对象的抽象接口). 再定义一个工厂子类 (负责生成具体的对象), 工厂方法模式将对象的实例化推迟到工厂子类
Phone 手机抽象类、IPhone 苹果手机实现类 和 HuaweiPhone 华为手机实现类
/**
* Phone 手机抽象类: 提供运行手机的抽象方法
*/
abstract class Phone {
abstract void run();
}
/**
* IPhone 运行苹果手机
*/
class IPhone extends Phone{
@Override
public void run() {
System.out.println("运行苹果手机");
}
}
/**
* HuaweiPhone 运行华为手机
*/
class HuaweiPhone extends Phone {
@Override
public void run() {
System.out.println("运行华为手机");
}
}
先定义一个工厂抽象父类: 提供创建手机对象的抽象方法
abstract class PhoneFactory {
abstract Phone createPhone();
}
再定义一个苹果工厂子类: 负责创建具体的对象(苹果手机对象)
再定义一个华为工厂子类: 负责创建具体的对象(华为手机对象)
class IPhoneFactory extends PhoneFactory {
@Override
public IPhone createPhone() {
return new IPhone();
}
}
class HuaweiPhoneFactory extends PhoneFactory {
@Override
public HuaweiPhone createPhone() {
return new HuaweiPhone();
}
}
测试:
public class FactoryMethod {
public static void main(String[] args) {
// 苹果子类工厂 创建 苹果手机对象
IPhoneFactory iPhoneFactory = new IPhoneFactory();
IPhone iPhone = iPhoneFactory.createPhone();
iPhone.run(); // 运行苹果手机
// 华为子类工厂 创建 华为手机对象
HuaweiPhoneFactory huaweiPhoneFactory = new HuaweiPhoneFactory();
HuaweiPhone huaweiPhone = huaweiPhoneFactory.createPhone();
huaweiPhone.run(); // 运行华为手机
}
}
工厂方法模式存在问题: 对象父类 与 对象工厂父类 一一对应, 对象子类 与 对象工厂子类 一一对应. 即: 一个具体工厂子类只能创建一类产品
抽象工厂模式: Abstarct Factory Pattern, 是围绕一个超级工厂创建其他工厂, 该超级工厂又称为其他工厂的工厂.
在抽象工厂模式中, 超级工厂中提供多个接口, 每个接口负责创建一个相关对象的其他工厂, 工厂创建的对象 不需要显式指定它们的类, 指向抽象类
在抽象工厂模式中, 超级工厂子类负责创建具体的对象: 一个具体超级工厂子类创建多类产品
/**
* 电子产品超级工厂: 超级工厂创建其他工厂
* 创建手机工厂的抽象方法
* 创建电脑工厂的抽象方法
*/
abstract class ElectronicProductsFactory {
/**
* 手机工厂 创建Phone手机抽象对象
* @return
*/
abstract Phone createPhoneFactory();
/**
* 电脑工厂 创建Computer电脑抽象对象
* @return
*/
abstract Computer createComputerFactory();
}
在抽象工厂模式中, 超级工厂子类负责创建具体的对象: 一个具体超级工厂子类创建多类产品
/**
* 苹果超级工厂子类: 创建苹果手机和苹果电脑对象
*/
class AppleFactory extends ElectronicProductsFactory {
@Override
IPhone createPhoneFactory() {
return new IPhone();
}
@Override
AppleComputer createComputerFactory() {
return new AppleComputer();
}
}
/**
* 华为超级工厂子类: 创建华为手机和华为电脑对象
*/
class HuaweiFactory extends ElectronicProductsFactory {
@Override
HuaweiPhone createPhoneFactory() {
return new HuaweiPhone();
}
@Override
HuaweiComputer createComputerFactory() {
return new HuaweiComputer();
}
}
定义 产品抽象类 和 产品实体类
/**
* 电子产品抽象族类
*/
abstract class ElectronicProducts {
abstract void run();
}
/**
* 手机电子产品抽象类
*/
abstract class Phone extends ElectronicProducts {
@Override
abstract void run();
}
/**
* 电脑电子产品抽象类
*/
abstract class Computer extends ElectronicProducts {
@Override
abstract void run();
}
/**
* IPhone 苹果手机实体类
*/
class IPhone extends Phone{
@Override
public void run() {
System.out.println("运行苹果手机");
}
}
/**
* HuaweiPhone 华为手机实体类
*/
class HuaweiPhone extends Phone {
@Override
public void run() {
System.out.println("运行华为手机");
}
}
/**
* AppleComputer 苹果电脑实体类
*/
class AppleComputer extends Computer {
@Override
public void run() {
System.out.println("运行苹果电脑");
}
}
/**
* HuaweiComputer 华为电脑实体类
*/
class HuaweiComputer extends Computer {
@Override
public void run() {
System.out.println("运行华为电脑");
}
}
抽象工厂模式的优缺点:
将实例化对象的代码提取处理, 放到一个类中统一管理和维护, 达到和主项目的依赖关系的解耦, 从而提提高项目的扩展和维护性.
工厂模式的依赖抽象原则:
在尼恩的(50+)读者社区中,很多、很多小伙伴需要进大厂、拿高薪。
尼恩团队,会持续结合一些大厂的面试真题,给大家梳理一下学习路径,看看大家需要学点啥?
前面用多篇文章,给大家介绍字节、滴滴的真题:
《百度狂问3小时,大厂offer到手,小伙真狠!》
《饿了么太狠:面个高级Java,抖这多硬活、狠活》
《字节狂问一小时,小伙offer到手,太狠了!》
《收个滴滴Offer:从小伙三面经历,看看需要学点啥?》
这些真题,都会收入到 史上最全、持续升级的 PDF电子书 《尼恩Java面试宝典》。
本文收录于 《尼恩Java面试宝典》 V78版。
基本上,把尼恩的 《尼恩Java面试宝典》吃透,大厂offer很容易拿到滴。另外,下一期的 大厂面经大家有啥需求,可以发消息给尼恩。
《吃透8图1模板,人人可以做架构》
《10Wqps评论中台,如何架构?B站是这么做的!!!》
《阿里二面:千万级、亿级数据,如何性能优化? 教科书级 答案来了》
《峰值21WQps、亿级DAU,小游戏《羊了个羊》是怎么架构的?》
《100亿级订单怎么调度,来一个大厂的极品方案》
《2个大厂 100亿级 超大流量 红包 架构方案》
… 更多架构文章,正在添加中
尼恩 架构笔记、面试题 的PDF文件更新,▼请到下面【技术自由圈】公号取 ▼