# Mall-second-kill-project **Repository Path**: one8090/Mall-second-kill-project ## Basic Information - **Project Name**: Mall-second-kill-project - **Description**: 秒杀 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-04-14 - **Last Updated**: 2022-04-23 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 谈谈你对AQS的理解 AQS是AbstractQueuedSynchronizer的简称,是并发编程中比较核心的组件。 在很多大厂的面试中,面试官对于并发编程的考核要求相对较高,简单来说,如果你不懂并发编程,那么你很难通过大厂高薪岗位的面试。 Hello,大家好, 我是Mic, 一个工作了14年的程序员,今天来和大家聊聊并发编程中的AQS组件。 我们来看一下,关于“谈谈你对AQS的理解“,看看普通人和高手是如何回答的! ## 普通人的回答 AQS全称是AbstractQueuedSynchronizer,它是J.U.C包中Lock锁的底层实现,可以用它来实现多线程的同步器! ## 高手的回答 AQS是多线程同步器,它是J.U.C包中多个组件的底层实现,如Lock、CountDownLatch、Semaphore等都用到了AQS. 从本质上来说,AQS提供了两种锁机制,分别是排它锁,和 共享锁。 排它锁,就是存在多线程竞争同一共享资源时,同一时刻只允许一个线程访问该共享资源,也就是多个线程中只能有一个线程获得锁资源,比如Lock中的ReentrantLock重入锁实现就是用到了AQS中的排它锁功能。 共享锁也称为读锁,就是在同一时刻允许多个线程同时获得锁资源,比如CountDownLatch和Semaphore都是用到了AQS中的共享锁功能。 ## 结尾 好的,关于普通人和高手对于这个问题的回答,哪个更加好呢? 你们如果有更好的回答,可以在下方评论区留言。 另外,我整理了一张比较完整的并发编程知识体系的脑图,大家感兴趣的可以私信我获取。 本期的普通人VS高手面试系列就到这里结束了,喜欢的朋友记得一键三连,加个关注, 我是Mic,一个工作了14年的Java程序员,咱们下期再见。 # fail-safe机制与fail-fast机制分别有什么作用 前段时间一个小伙伴去面试,遇到这样一个问题。 ”fail-safe机制与fail-fast机制分别有什么作用“ 他说他听到这个问题的时候,脑子里满脸问号。那么今天我们来看一下,关于这个问题,普通人和高手应该如何回答吧。 ## 普通人的回答 额… . 嗯 … (持续几秒后,贴一个搞笑的图,比如5 year s latter 之类,然后再配个一脸蒙蔽。。) ## 高手的回答 fail-safe和fail-fast ,是多线程并发操作集合时的一种失败处理机制。 Fail-fast : 表示快速失败,在集合遍历过程中,一旦发现容器中的数据被修改了,会立刻抛出ConcurrentModificationException异常,从而导致遍历失败,像这种情况(贴下面这个图)。 定义一个Map集合,使用Iterator迭代器进行数据遍历,在遍历过程中,对集合数据做变更时,就会发生`fail-fast`。 java.util包下的集合类都是快速失败机制的, 常见的的使用fail-fast方式遍历的容器有HashMap和ArrayList等。 ![carbon-2022030202](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/carbon-2022030202.png) Fail-safe,表示失败安全,也就是在这种机制下,出现集合元素的修改,不会抛出ConcurrentModificationException。 原因是采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容, 在拷贝的集合上进行遍历。由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到 比如这种情况(贴下面这个图) , 定义了一个CopyOnWriteArrayList,在对这个集合遍历过程中,对集合元素做修改后,不会抛出异常,但同时也不会打印出增加的元素。 java.util.concurrent包下的容器都是安全失败的,可以在多线程下并发使用,并发修改。 常见的的使用fail-safe方式遍历的容器有ConcerrentHashMap和CopyOnWriteArrayList等。 ![carbon-2022030203](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/carbon-2022030203.png) ## 结尾 好的, fail-safe和fail-fast 的作用,你理解了吗? 你们是否有更好的回答方式? 欢迎在评论区给我留言! 本期的普通人VS高手面试系列就到这里结束了,喜欢的朋友记得一键三连,加个关注, 我是Mic,一个工作了14年的Java程序员,咱们下期再见。 # 谈谈你对Seata的理解 很多面试官都喜欢问一些“谈谈你对xxx技术的理解”, 大家遇到这种问题时,是不是完全不知道从何说起,有同感小伙伴的call 1. 那么我们来看一下,普通人和高手是如何回答这个问题的? ## 普通人 Seata是用来解决分布式事务问题的框架。是阿里开源的中间件。 实际项目中我没有用过,我记得Seata里面有几种事务模型,有一种AT模式、还有TCC模式。 然后AT是一种二阶段提交的事务,它是采用的最终一致性来实现数据的一致性。 ## 高手 1. 在微服务架构下,由于数据库和应用服务的拆分,导致原本一个事务单元中的多个DML操作, 变成了跨进程或者跨数据库的多个事务单元的多个DML操作, 而传统的数据库事务无法解决这类的问题,所以就引出了分布式事务的概念。 2. 分布式事务本质上要解决的就是跨网络节点的多个事务的数据一致性问题,业内常见的解决方法有两种 1. 强一致性,就是所有的事务参与者要么全部成功,要么全部失败,全局事务协调者需要知道每个事务参与者的执行状态,再根据状态来决定数据的提交或者回滚! 2. 最终一致性,也叫弱一致性,也就是多个网络节点的数据允许出现不一致的情况,但是在最终的某个时间点会达成数据一致。 基于CAP定理我们可以知道,强一致性方案对于应用的性能和可用性会有影响,所以对于数据一致性要求不高的场景,就会采用最终一致性算法。 3. 在分布式事务的实现上,对于强一致性,我们可以通过基于XA协议下的二阶段提交来实现,对于弱一致性,可以基于TCC事务模型、可靠性消息模型等方案来实现。 4. 市面上有很多针对这些理论模型实现的分布式事务框架,我们可以在应用中集成这些框架来实现分布式事务。 而Seata就是其中一种,它是阿里开源的分布式事务解决方案,提供了高性能且简单易用的分布式事务服务。 Seata中封装了四种分布式事务模式,分别是: * AT模式,是一种基于本地事务+二阶段协议来实现的最终数据一致性方案,也是Seata默认的解决方案 ![image-20220302110420998](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220302110420998.png) * TCC模式,TCC事务是Try、Confirm、Cancel三个词语的缩写,简单理解就是把一个完整的业务逻辑拆分成三个阶段,然后通过事务管理器在业务逻辑层面根据每个分支事务的执行情况分别调用该业务的Confirm或者Cacel方法。 ![Overview of a global transaction](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/seata_tcc-1.png) * Saga模式,Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者。 ![Saga模式示意图](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/TB1Y2kuw7T2gK0jSZFkXXcIQFXa-445-444.png) * XA模式,XA可以认为是一种强一致性的事务解决方法,它利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种 事务模式。 ![img](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/TB1hSpccIVl614jSZKPXXaGjpXa-1330-924.png) 从这四种模型中不难看出,在不同的业务场景中,我们可以使用Seata的不同事务模型来解决不同业务场景中的分布式事务问题,因此我们可以认为Seata是一个一站式的分布式事务解决方案。 ## 结尾 屏幕前的小伙伴们,你是否通过高手的回答找到了这类问题的回答方式呢? 面试的时候遇到这种宽泛的问题时,先不用慌, 首先自己要有一个回答的思路。 按照技术的话术,就是先给自己大脑中的知识建立一个索引,然后基于索引来定位你的知识。 我对于这类问题,建立的索引一般有几个: 1. 它是什么 2. 它能解决什么问题 3. 它有哪些特点和优势 4. 它的核心原理,为什么能解决这类问题 大家对照这几个索引去回答今天的这个面试题,是不是就更清晰了? 好的,本期的普通人VS高手面试系列就到这里结束了,喜欢的朋友记得一键三连,加个关注, 我是Mic,一个工作了14年的Java程序员,咱们下期再见。 # Spring Boot的约定优于配置,你的理解是什么? 对于Spring Boot约定优于配置这个问题,看看普通人和高手是如何回答的? ## 普通人的回答 嗯, 在Spring Boot里面,通过约定优于配置这个思想,可以让我们少写很多的配置, 然后就只需要关注业务代码的编写就行。嗯! ## 高手的回答 我从4个点方面来回答。 1. 首先, 约定优于配置是一种软件设计的范式,它的核心思想是减少软件开发人员对于配置项的维护,从而让开发人员更加聚焦在业务逻辑上。 2. Spring Boot就是约定优于配置这一理念下的产物,它类似于Spring框架下的一个脚手架,通过Spring Boot,我们可以快速开发基于Spring生态下的应用程序。 3. 基于传统的Spring框架开发web应用,我们需要做很多和业务开发无关并且只需要做一次的配置,比如 1. 管理jar包依赖 2. web.xml维护 3. Dispatch-Servlet.xml配置项维护 4. 应用部署到Web容器 5. 第三方组件集成到Spring IOC容器中的配置项维护 而在Spring Boot中,我们不需要再去做这些繁琐的配置,Spring Boot已经自动帮我们完成了,这就是约定由于配置思想的体现。 4. Spring Boot约定由于配置的体现有很多,比如 1. Spring Boot Starter启动依赖,它能帮我们管理所有jar包版本 2. 如果当前应用依赖了spring mvc相关的jar,那么Spring Boot会自动内置Tomcat容器来运行web应用,我们不需要再去单独做应用部署。 3. Spring Boot的自动装配机制的实现中,通过扫描约定路径下的spring.factories文件来识别配置类,实现Bean的自动装配。 4. 默认加载的配置文件application.properties 等等。 总的来说,约定优于配置是一个比较常见的软件设计思想,它的核心本质都是为了更高效以及更便捷的实现软件系统的开发和维护。 以上就是我对这个问题的理解。 ## 结尾 好的,本期的普通人VS高手面试系列就到这里结束了,对于这个问题,你知道该怎么回答了吗? 另外,如果你有任何面试相关的疑问,欢迎评论区给我留言。 我是Mic,一个工作了14年的Java程序员,咱们下期再见。 # 滴滴二面:kafka的零拷贝原理? 最近一个学员去滴滴面试,在第二面的时候遇到了这个问题: ”请你简单说一下Kafka的零拷贝原理“ 然后那个学员努力在大脑里检索了很久,没有回答上来。 那么今天,我们基于这个问题来看看,普通人和高手是如何回答的! ## 普通人的回答 零拷贝是一种减少数据拷贝的机制,能够有效提升数据的效率 ## 高手的回答 在实际应用中,如果我们需要把磁盘中的某个文件内容发送到远程服务器上,如图 那么它必须要经过几个拷贝的过程,如图(贴图)。 1. 从磁盘中读取目标文件内容拷贝到内核缓冲区 2. CPU控制器再把内核缓冲区的数据赋值到用户空间的缓冲区中 3. 接着在应用程序中,调用`write()`方法,把用户空间缓冲区中的数据拷贝到内核下的Socket Buffer中。 4. 最后,把在内核模式下的SocketBuffer中的数据赋值到网卡缓冲区(NIC Buffer) 5. 网卡缓冲区再把数据传输到目标服务器上。 ![preview](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/view.png) 在这个过程中我们可以发现,数据从磁盘到最终发送出去,要经历4次拷贝,而在这四次拷贝过程中,有两次拷贝是浪费的,分别是: 1. 从内核空间赋值到用户空间 2. 从用户空间再次复制到内核空间 除此之外,由于用户空间和内核空间的切换会带来CPU的上线文切换,对于CPU性能也会造成性能影响。 而零拷贝,就是把这两次多于的拷贝省略掉,应用程序可以直接把磁盘中的数据从内核中直接传输给Socket,而不需要再经过应用程序所在的用户空间,如下图所示。 零拷贝通过DMA(Direct Memory Access)技术把文件内容复制到内核空间中的Read Buffer, 接着把包含数据位置和长度信息的文件描述符加载到Socket Buffer中,DMA引擎直接可以把数据从内核空间中传递给网卡设备。 在这个流程中,数据只经历了两次拷贝就发送到了网卡中,并且减少了2次cpu的上下文切换,对于效率有非常大的提高。 ![preview](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/view-20220304160741793.png) 所以,所谓零拷贝,并不是完全没有数据赋值,只是相对于用户空间来说,不再需要进行数据拷贝。对于前面说的整个流程来说,零拷贝只是减少了不必要的拷贝次数而已。 在程序中如何实现零拷贝呢? * 在Linux中,零拷贝技术依赖于底层的sendfile()方法实现 * 在Java中,FileChannal.transferTo() 方法的底层实现就是 sendfile() 方法。 除此之外,还有一个 mmap 的文件映射机制 它的原理是:将磁盘文件映射到内存, 用户通过修改内存就能修改磁盘文件。使用这种方式可以获取很大的I/O提升,省去了用户空间到内核空间复制的开销。 以上就是我对于Kafka中零拷贝原理的理解 ## 结尾 好的,本期的普通人VS高手面试系列就到这里结束了。 本次的面试题涉及到一些计算机底层的原理,基本上也是业务程序员的知识盲区。 但我想提醒大家,做开发其实和建房子一样,要想楼层更高更稳,首先地基要打牢固。 另外,如果你有任何面试相关的疑问,欢迎评论区给我留言。 我是Mic,一个工作了14年的Java程序员,咱们下期再见。 # innoDB如何解决幻读 前天有个去快手面试的小伙伴私信我,他遇到了这样一个问题: “InnoDB如何解决幻读”? 这个问题确实不是很好回答,在实际应用中,很多同学几乎都不关注数据库的事务隔离性。 所有问题基本就是CRUD,一把梭~ 那么今天,我们看一下 关于“InnoDB如何解决幻读”这个问题,普通人和高手的回答! ## 普通人 嗯,我印象中,幻读是通过MVCC机制来解决的,嗯.... MVCC类似于一种乐观锁的机制,通过版本的方式来区分不同的并发事务,避免幻读问题! ## 高手 我会从三个方面来回答: 1、 Mysql的事务隔离级别 Mysql有四种事务隔离级别,这四种隔离级别代表当存在多个事务并发冲突时,可能出现的脏读、不可重复读、幻读的问题。 其中InnoDB在RR的隔离级别下,解决了幻读的问题。 ![image-20220306154449430](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220306154449430.png) 2、 **什么是幻读?** 那么, 什么是幻读呢? 幻读是指在同一个事务中,前后两次查询相同的范围时,得到的结果不一致(我们来看这个图) * 第一个事务里面我们执行了一个范围查询,这个时候满足条件的数据只有一条 * 第二个事务里面,它插入了一行数据,并且提交了 * 接着第一个事务再去查询的时候,得到的结果比第一查询的结果多出来了一条数据。 ![image-20220301151549601](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220301151549601.png) 所以,幻读会带来数据一致性问题。 **3、 InnoDB如何解决幻读的问题** InnoDB引入了间隙锁和next-key Lock机制来解决幻读问题,为了更清晰的说明这两种锁,我举一个例子: 假设现在存在这样(图片)这样一个B+ Tree的索引结构,这个结构中有四个索引元素分别是:1、4、7、10。 ![image-20220306155849177](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220306155849177.png) 当我们通过主键索引查询一条记录,并且对这条记录通过`for update`加锁(请看这个图片) ![image-20220306161435984](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220306161435984.png) 这个时候,会产生一个记录锁,也就是行锁,锁定`id=1`这个索引(请看这个图片)。 ![image-20220302175018903](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220302175018903.png) 被锁定的记录在锁释放之前,其他事务无法对这条记录做任何操作。 前面我说过对幻读的定义: 幻读是指在同一个事务中,前后两次查询相同的范围时,得到的结果不一致! 注意,这里强调的是范围查询, 也就是说,InnoDB引擎要解决幻读问题,必须要保证一个点,就是如果一个事务通过这样一条语句(如图)进行锁定时。 ![image-20220306161951090](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220306161951090.png) 另外一个事务再执行这样一条(显示图片)insert语句,需要被阻塞,直到前面获得锁的事务释放。 ![image-20220306162324367](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220306162324367.png) 所以,在InnoDB中设计了一种间隙锁,它的主要功能是锁定一段范围内的索引记录(如图) 当对查询范围`id>4 and id <7`加锁的时候,会针对B+树中(4,7)这个开区间范围的索引加间隙锁。 意味着在这种情况下,其他事务对这个区间的数据进行插入、更新、删除都会被锁住。 ![image-20220306163023219](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220306163023219.png) 但是,还有另外一种情况,比如像这样(图片) ![image-20220306163933925](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220306163933925.png) 这条查询语句是针对`id>4`这个条件加锁,那么它需要锁定多个索引区间,所以在这种情况下InnoDB引入了next-key Lock机制。 next-key Lock相当于间隙锁和记录锁的合集,记录锁锁定存在的记录行,间隙锁锁住记录行之间的间隙,而next-key Lock锁住的是两者之和。(如图所示) ![image-20220306164559217](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220306164559217.png) 每个数据行上的`非唯一索引列`上都会存在一把**next-key lock**,当某个事务持有该数据行的**next-key lock**时,会锁住一段**左开右闭区间**的数据。 因此,当通过`id>4`这样一种范围查询加锁时,会加next-key Lock,锁定的区间范围是:(4, 7] , (7,10],(10,+∞] ![image-20220306164958923](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220306164958923.png) 间隙锁和next-key Lock的区别在于加锁的范围,间隙锁只锁定两个索引之间的引用间隙,而next-key Lock会锁定多个索引区间,它包含记录锁和间隙锁。 当我们使用了范围查询,不仅仅命中了Record记录,还包含了Gap间隙,在这种情况下我们使用的就是临键锁,它是MySQL里面默认的行锁算法。 **4 、总结** 虽然InnoDB中通过间隙锁的方式解决了幻读问题,但是加锁之后一定会影响到并发性能,因此,如果对性能要求较高的业务场景中,可以把隔离级别设置成RC,这个级别中不存在间隙锁。 以上就是我对于`innoDB如何解决幻读问题`的理解! ## 结尾 好的,通过这个面试题可以发现,大厂面试对于基本功的考察还是比较严格的。 不过,不管是为了应付面试,还是为以后的职业规划做铺垫, 技术能力的高低都是你在这个行业的核心竞争力。 本期的普通人VS高手面试系列的视频就到这里结束了, 我是Mic,一个工作了14年的Java程序员,咱们下期再见。 # CPU飙高系统反应慢怎么排查? 面试过程中,场景类的问题更容易检测出一个开发人员的基本能力。 这不,一个小伙伴去阿里面试,第一面就遇到了关于“CPU飙高系统反应慢怎么排查”的问题? 对于这个问题,我们来看看普通人和高手的回答! ## 普通人 嗯, CPU飙高的原因可能是线程创建过多导致的 ## 高手 好的,关于这个问题,我从四个方面来回答。 1. CPU是整个电脑的核心计算资源,对于一个应用进程来说,CPU的最小执行单元是线程。 2. 导致CPU飙高的原因有几个方面 1. CPU上下文切换过多,对于CPU来说,同一时刻下每个CPU核心只能运行一个线程,如果有多个线程要执行,CPU只能通过上下文切换的方式来执行不同的线程。上下文切换需要做两个事情 1. 保存运行线程的执行状态 2. 让处于等待中的线程执行 这两个过程需要CPU执行内核相关指令实现状态保存,如果较多的上下文切换会占据大量CPU资源,从而使得cpu无法去执行用户进程中的指令,导致响应速度下降。 在Java中,文件IO、网络IO、锁等待、线程阻塞等操作都会造成线程阻塞从而触发上下文切换 2. CPU资源过度消耗,也就是在程序中创建了大量的线程,或者有线程一直占用CPU资源无法被释放,比如死循环! CPU利用率过高之后,导致应用中的线程无法获得CPU的调度,从而影响程序的执行效率! 3. 既然是这两个问题导致的CPU利用率较高,于是我们可以通过`top`命令,找到CPU利用率较高的进程,在通过`Shift+H`找到进程中CPU消耗过高的线程,这里有两种情况。 1. CPU利用率过高的线程一直是同一个,说明程序中存在线程长期占用CPU没有释放的情况,这种情况直接通过jstack获得线程的Dump日志,定位到线程日志后就可以找到问题的代码。 2. CPU利用率过高的线程id不断变化,说明线程创建过多,需要挑选几个线程id,通过jstack去线程dump日志中排查。 4. 最后有可能定位的结果是程序正常,只是在CPU飙高的那一刻,用户访问量较大,导致系统资源不够。 以上就是我对这个问题的理解! ## 结尾 从这个问题来看,面试官主要考察实操能力,以及解决问题的思路。 如果你没有实操过,但是你知道导致CPU飙高这个现象的原因,并说出你的解决思路,通过面试是没问题的。 好的,本期的普通人VS高手面试系列的视频就到这里结束了, 如果你在面试的时候遇到了一些比较刁钻也奇葩的问题,欢迎在评论区给我留言,我是Mic。 我是Mic,一个工作了14年的Java程序员,咱们下期再见。 # lock和synchronized区别 今天来分享一道阿里一面的面试题,“lock和synchronized的区别”。 对于这个问题,看看普通人和高手的回答! ## 普通人 嗯,lock是J.U.C包里面提供的锁,synchronized是Java中的同步关键字。 他们都可以实现多线程对共享资源访问的线程安全性。 ## 高手 下面我从3个方面来回答 1. 从功能角度来看,Lock和Synchronized都是Java中用来解决线程安全问题的工具。 2. 从特性来看, 1. Synchronized是Java中的同步关键字,Lock是J.U.C包中提供的接口,这个接口有很多实现类,其中就包括ReentrantLock重入锁 2. Synchronized可以通过两种方式来控制锁的粒度,(贴图) ![carbon2022030701](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/carbon2022030701.png) 一种是把synchronized关键字修饰在方法层面, 另一种是修饰在代码块上,并且我们可以通过Synchronized加锁对象的声明周期来控制锁的作用范围,比如锁对象是静态对象或者类对象,那么这个锁就是全局锁。 如果锁对象是普通实例对象,那这个锁的范围取决于这个实例的声明周期。 Lock锁的粒度是通过它里面提供的`lock()`和`unlock()`方法决定的(贴图),包裹在这两个方法之间的代码能够保证线程安全性。而锁的作用域取决于Lock实例的生命周期。 ![carbon2022030702](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/carbon2022030702.png) 3. Lock比Synchronized的灵活性更高,Lock可以自主决定什么时候加锁,什么时候释放锁,只需要调用`lock()`和`unlock()`这两个方法就行,同时Lock还提供了非阻塞的竞争锁方法`tryLock()`方法,这个方法通过返回true/false来告诉当前线程是否已经有其他线程正在使用锁。 Synchronized由于是关键字,所以它无法实现非阻塞竞争锁的方法,另外,Synchronized锁的释放是被动的,就是当Synchronized同步代码块执行完以后或者代码出现异常时才会释放。 4. Lock提供了公平锁和非公平锁的机制,公平锁是指线程竞争锁资源时,如果已经有其他线程正在排队等待锁释放,那么当前竞争锁资源的线程无法插队。而非公平锁,就是不管是否有线程在排队等待锁,它都会尝试去竞争一次锁。 Synchronized只提供了一种非公平锁的实现。 3. 从性能方面来看,Synchronized和Lock在性能方面相差不大,在实现上会有一些区别,Synchronized引入了偏向锁、轻量级锁、重量级锁以及锁升级的方式来优化加锁的性能,而Lock中则用到了自旋锁的方式来实现性能优化。 以上就是我对于这个问题的理解。 ## 结尾 这个问题主要是考察求职责对并发基础能力的掌握。 在实际应用中,线程以及线程安全性是非常重要和常见的功能,对于这部分内容如果理解不够深刻,很容易造成生产级别的故障。 好的,本期的普通人VS高手面试系列的视频就到这里结束了,喜欢的朋友记得点赞收藏。 如果在面试过程中遇到了比较刁钻和奇葩的问题,欢迎评论区给我留言! 我是Mic,一个工作了14年的Java程序员,咱们下期再见。 # 线程池如何知道一个线程的任务已经执行完成 一个小伙伴私信了一个小米的面试题,问题是: “线程池如何知道一个线程的任务已经执行完成”? 说实话,这个问题确实很刁钻,毕竟像很多工作5年多的小伙伴,连线程池都没用过,怎么可能回答出来这个问题呢? 下面我们来看看普通人和高手遇到这个问题的回答思路。 ## 普通人 嗯.. (临场发挥吧) ## 高手 好的,我会从两个方面来回答。 1. 在线程池内部,当我们把一个任务丢给线程池去执行,线程池会调度工作线程来执行这个任务的run方法,run方法正常结束,也就意味着任务完成了。 所以线程池中的工作线程是通过同步调用任务的`run()`方法并且等待`run`方法返回后,再去统计任务的完成数量。 2. 如果想在线程池外部去获得线程池内部任务的执行状态,有几种方法可以实现。 1. 线程池提供了一个`isTerminated()`方法,可以判断线程池的运行状态,我们可以循环判断`isTerminated()`方法的返回结果来了解线程池的运行状态,一旦线程池的运行状态是`Terminated`,意味着线程池中的所有任务都已经执行完了。想要通过这个方法获取状态的前提是,程序中主动调用了线程池的`shutdown()`方法。在实际业务中,一般不会主动去关闭线程池,因此这个方法在实用性和灵活性方面都不是很好。 2. 在线程池中,有一个`submit()`方法,它提供了一个Future的返回值,我们通过`Future.get()`方法来获得任务的执行结果,当线程池中的任务没执行完之前,`future.get()`方法会一直阻塞,直到任务执行结束。因此,只要`future.get()`方法正常返回,也就意味着传入到线程池中的任务已经执行完成了! 3. 可以引入一个CountDownLatch计数器,它可以通过初始化指定一个计数器进行倒计时,其中有两个方法分别是`await()`阻塞线程,以及`countDown()`进行倒计时,一旦倒计时归零,所以被阻塞在`await()`方法的线程都会被释放。 基于这样的原理,我们可以定义一个CountDownLatch对象并且计数器为1,接着在线程池代码块后面调用`await()`方法阻塞主线程,然后,当传入到线程池中的任务执行完成后,调用`countDown()`方法表示任务执行结束。 最后,计数器归零0,唤醒阻塞在`await()`方法的线程。 ![carbon-20220307002](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/carbon-20220307002.png) 3. 基于这个问题,我简单总结一下,不管是线程池内部还是外部,要想知道线程是否执行结束,我们必须要获取线程执行结束后的状态,而线程本身没有返回值,所以只能通过阻塞-唤醒的方式来实现,future.get和CountDownLatch都是这样一个原理。 以上就是我对于这个问题的回答! ## 结尾 大家可以站在面试官的角度来看高手的回答, 不难发现,高手对于技术基础的掌握程度,是非常深和全面的。这也是面试官考察这类问题的目的。 因此,Mic提醒大家,除了日常的CRUD以外,抽出部分时间去做技术深度和广度的学习是非常有必要的。 好的,本期的普通人VS高手面试系列的视频就到这里结束了,喜欢的朋友记得点赞收藏。 我是Mic,一个工作了14年的Java程序员,咱们下期再见。 # HashMap是怎么解决哈希冲突的? 常用数据结构基本上是面试必问的问题,比如HashMap、LinkList、ConcurrentHashMap等。 关于HashMap,有个学员私信了我一个面试题说: “HashMap是怎么解决哈希冲突的?” 关于这个问题,我们来模拟一下普通人和高手对于这个问题的回答。 ## 普通人 嗯.... HashMap我好久之前看过它的源码,我记得好像是通过链表来解决的! ## 高手 嗯,这个问题我从三个方面来回答。 1. 要了解Hash冲突,那首先我们要先了解Hash算法和Hash表。(如图) 1. Hash算法,就是把任意长度的输入,通过散列算法,变成固定长度的输出,这个输出结果是散列值。 2. Hash表又叫做“散列表”,它是通过key直接访问在内存存储位置的数据结构,在具体实现上,我们通过hash函数把key映射到表中的某个位置,来获取这个位置的数据,从而加快查找速度。 ![image-20220309201241144](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220309201241144.png) 2. 所谓hash冲突,是由于哈希算法被计算的数据是无限的,而计算后的结果范围有限,所以总会存在不同的数据经过计算后得到的值相同,这就是哈希冲突。 3. 通常解决hash冲突的方法有4种。 1. 开放定址法,也称为线性探测法,就是从发生冲突的那个位置开始,按照一定的次序从hash表中找到一个空闲的位置,然后把发生冲突的元素存入到这个空闲位置中。ThreadLocal就用到了线性探测法来解决hash冲突的。 向这样一种情况(如图),在hash表索引1的位置存了一个key=name,当再次添加key=hobby时,hash计算得到的索引也是1,这个就是hash冲突。而开放定址法,就是按顺序向前找到一个空闲的位置来存储冲突的key。 ![image-20220310135640704](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220310135640704.png) 2. 链式寻址法,这是一种非常常见的方法,简单理解就是把存在hash冲突的key,以单向链表的方式来存储,比如HashMap就是采用链式寻址法来实现的。 向这样一种情况(如图),存在冲突的key直接以单向链表的方式进行存储。 ![image-20220310145348381](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220310145348381.png) 3. 再hash法,就是当通过某个hash函数计算的key存在冲突时,再用另外一个hash函数对这个key做hash,一直运算直到不再产生冲突。这种方式会增加计算时间,性能影响较大。 4. 建立公共溢出区, 就是把hash表分为基本表和溢出表两个部分,凡事存在冲突的元素,一律放入到溢出表中。 4. HashMap在JDK1.8版本中,通过链式寻址法+红黑树的方式来解决hash冲突问题,其中红黑树是为了优化Hash表链表过长导致时间复杂度增加的问题。当链表长度大于8并且hash表的容量大于64的时候,再向链表中添加元素就会触发转化。 以上就是我对这个问题的理解! ## 结尾 这道面试题主要考察Java基础,面向的范围是工作1到5年甚至5年以上。 因为集合类的对象在项目中使用频率较高,如果对集合理解不够深刻,容易在项目中制造隐藏的BUG。 所以,再强调一下,面试的时候,基础是很重要的考核项!! 本期的普通人VS高手面试系列的视频就到这里结束了,喜欢的朋友记得点赞收藏。 我是Mic,一个工作了14年的Java程序员,咱们下期再见。 # 什么叫做阻塞队列的有界和无界 昨天一个3年Java经验的小伙伴私信我,他说现在面试怎么这么难啊! 我只是面试一个业务开发,他们竟然问我: 什么叫阻塞队列的有界和无界。现在面试也太卷了吧! 如果你也遇到过类似问题,那我们来看看普通人和高手的回答吧! ## 普通人 有界队列就是说队列中的元素个数是有限制的,而无界对接表示队列中的元素个数没有限制! 嗯!!! ## 高手 1. (如图),阻塞队列,是一种特殊的队列,它在普通队列的基础上提供了两个附加功能 1. 当队列为空的时候,获取队列中元素的消费者线程会被阻塞,同时唤醒生产者线程。 2. 当队列满了的时候,向队列中添加元素的生产者线程被阻塞,同时唤醒消费者线程。 ![image-20220313201420205](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220313201420205.png) 2. 其中,阻塞队列中能够容纳的元素个数,通常情况下是有界的,比如我们实例化一个`ArrayBlockingList`,可以在构造方法中传入一个整形的数字,表示这个基于数组的阻塞队列中能够容纳的元素个数。这种就是有界队列。 3. 而无界队列,就是没有设置固定大小的队列,不过它并不是像我们理解的那种元素没有任何限制,而是它的元素存储量很大,像LinkedBlockingQueue,它的默认队列长度是Integer.Max_Value,所以我们感知不到它的长度限制。 4. 无界队列存在比较大的潜在风险,如果在并发量较大的情况下,线程池中可以几乎无限制的添加任务,容易导致内存溢出的问题! 以上就是我对这个问题的理解! ## 结尾 阻塞队列在生产者消费者模型的场景中使用频率比较高,比较典型的就是在线程池中,通过阻塞队列来实现线程任务的生产和消费功能。 基于阻塞队列实现的生产者消费者模型比较适合用在异步化性能提升的场景,以及做并发流量缓冲类的场景中! 在很多开源中间件中都可以看到这种模型的使用,比如在Zookeeper源码中就大量用到了阻塞队列实现的生产者消费者模型。 OK,本期的普通人VS高手面试系列的视频就到这里结束了,喜欢的朋友记得点赞收藏。 我是Mic,一个工作了14年的Java程序员,咱们下期再见。 # Dubbo的服务请求失败怎么处理? 今天分享的面试题,几乎是90%以上的互联网公司都会问到的问题。 “Dubbo的服务请求失败怎么处理”? 对于这个问题,我们来看一下普通人和高手的回答。 ## 普通人 嗯… 我记得, Dubbo请求处理失败以后,好像是会重试。 嗯! ## 高手 Dubbo是一个RPC框架,它为我们的应用提供了远程通信能力的封装,同时,Dubbo在RPC通信的基础上,逐步在向一个生态在演进,它涵盖了服务注册、动态路由、容错、服务降级、负载均衡等能力,基本上在微服务架构下面临的问题,Dubbo都可以解决。 而对于Dubbo服务请求失败的场景,默认提供了重试的容错机制,也就是说,如果基于Dubbo进行服务间通信出现异常,服务消费者会对服务提供者集群中其他的节点发起重试,确保这次请求成功,默认的额外重试次数是2次。 除此之外,Dubbo还提供了更多的容错策略,我们可以根据不同的业务场景来进行选择。 1. 快速失败策略,服务消费者只发起一次请求,如果请求失败,就直接把错误抛出去。这种比较适合在非幂等性场景中使用 2. 失败安全策略,如果出现服务通信异常,直接把这个异常吞掉不做任何处理 3. 失败自动恢复策略,后台记录失败请求,然后通过定时任务来对这个失败的请求进行重发。 4. 并行调用多个服务策略,就是把这个消息广播给服务提供者集群,只要有任何一个节点返回,就表示请求执行成功。 5. 广播调用策略,逐个调用服务提供者集群,只要集群中任何一个节点出现异常,就表示本次请求失败 要注意的是,默认基于重试策略的容错机制中,需要注意幂等性的处理,否则在事务型的操作中,容易出现多次数据变更的问题。 以上就是我对这个问题的理解! ## 结尾 这类的问题,并不需要去花太多时间去背,如果你对于整个技术体系有一定的了解,你就很容易想象到最基本的处理方式。 即便是你对Dubbo不熟悉,也能回答一两种! OK,本期的普通人VS高手面试系列的视频就到这里结束了,喜欢的朋友记得点赞收藏。 另外,我也陆续收到了很多小伙伴的面试题,我会在后续的内容中逐步更新给到大家! 我是Mic,一个工作了14年的Java程序员,咱们下期再见。 # ConcurrentHashMap 底层具体实现知道吗?实现原理是什么? 之前分享过一期HashMap的面试题,然后有个小伙伴私信我说,他遇到了一个ConcurrentHashMap的问题不知道怎么回答。 于是,就有了这一期的内容!! 我是Mic,一个工作了14年的Java程序员,今天我来分享关于 ”ConcurrentHashMap 底层实现原理“ 这个问题, 看看普通人和高手是如何回答的! ## 普通人 嗯.. ConcurrentHashMap是用数组和链表的方式来实现的,嗯… 在JDK1.8里面还引入了红黑树。 然后链表和红黑树是解决hash冲突的。嗯…… ## 高手 这个问题我从这三个方面来回答:(下面这三个点,打印在屏幕上) 1. ConcurrentHashMap的整体架构 2. ConcurrentHashMap的基本功能 3. ConcurrentHashMap在性能方面的优化 * ConcurrentHashMap的整体架构(字幕提示) (如图所示),这个是ConcurrentHashMap在JDK1.8中的存储结构,它是由数组、单向链表、红黑树组成。 当我们初始化一个ConcurrentHashMap实例时,默认会初始化一个长度为16的数组。由于ConcurrentHashMap它的核心仍然是hash表,所以必然会存在hash冲突问题。 ConcurrentHashMap采用链式寻址法来解决hash冲突。 当hash冲突比较多的时候,会造成链表长度较长,这种情况会使得ConcurrentHashMap中数据元素的查询复杂度变成O(~n~)。因此在JDK1.8中,引入了红黑树的机制。 当数组长度大于64并且链表长度大于等于8的时候,单项链表就会转换为红黑树。 另外,随着ConcurrentHashMap的动态扩容,一旦链表长度小于8,红黑树会退化成单向链表。 ![image-20220313224920782](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220313224920782.png) * ConcurrentHashMap的基本功能(字幕提示) ConcurrentHashMap本质上是一个HashMap,因此功能和HashMap一样,但是ConcurrentHashMap在HashMap的基础上,提供了并发安全的实现。 并发安全的主要实现是通过对指定的Node节点加锁,来保证数据更新的安全性(如图所示)。 ![image-20220313230533685](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220313230533685.png) * ConcurrentHashMap在性能方面做的优化(字幕提示) 如果在并发性能和数据安全性之间做好平衡,在很多地方都有类似的设计,比如cpu的三级缓存、mysql的buffer_pool、Synchronized的锁升级等等。 ConcurrentHashMap也做了类似的优化,主要体现在以下几个方面: * 在JDK1.8中,ConcurrentHashMap锁的粒度是数组中的某一个节点,而在JDK1.7,锁定的是Segment,锁的范围要更大,因此性能上会更低。 * 引入红黑树,降低了数据查询的时间复杂度,红黑树的时间复杂度是O(~logn~)。 * (如图所示),当数组长度不够时,ConcurrentHashMap需要对数组进行扩容,在扩容的实现上,ConcurrentHashMap引入了多线程并发扩容的机制,简单来说就是多个线程对原始数组进行分片后,每个线程负责一个分片的数据迁移,从而提升了扩容过程中数据迁移的效率。 ![image-20220313231950178](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220313231950178.png) * ConcurrentHashMap中有一个size()方法来获取总的元素个数,而在多线程并发场景中,在保证原子性的前提下来实现元素个数的累加,性能是非常低的。ConcurrentHashMap在这个方面的优化主要体现在两个点: * 当线程竞争不激烈时,直接采用CAS来实现元素个数的原子递增。 * 如果线程竞争激烈,使用一个数组来维护元素个数,如果要增加总的元素个数,则直接从数组中随机选择一个,再通过CAS实现原子递增。它的核心思想是引入了数组来实现对并发更新的负载。 ![image-20220313232019737](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220313232019737.png) 以上就是我对这个问题的理解! ## 结尾 从高手的回答中可以看到,ConcurrentHashMap里面有很多设计思想值得学习和借鉴。 比如锁粒度控制、分段锁的设计等,它们都可以应用在实际业务场景中。 很多时候大家会认为这种面试题毫无价值,当你有足够的积累之后,你会发现从这些技术底层的设计思想中能够获得 很多设计思路。 本期的普通人VS高手面试系列的视频就到这里结束了,喜欢的朋友记得点赞收藏。 另外,我也陆续收到了很多小伙伴的面试题,我会在后续的内容中逐步更新给到大家! 我是Mic,一个工作了14年的Java程序员,咱们下期再见。 # b树和b+树的理解 数据结构与算法问题,困扰了无数的小伙伴。 很多小伙伴对数据结构与算法的认知有一个误区,认为工作中没有用到,为什么面试要问,问了能解决实际问题? 图灵奖获得者: Niklaus Wirth 说过: 程序=数据结构+算法, 也就说我们无时无刻都在和数据结构打交道。 只是作为Java开发,由于技术体系的成熟度较高,使得大部分人认为:程序应该等于 框架 + SQL 呀? 今天我们就来分析一道数据结构的题目:”B树和B+树“。 关于这个问题,我们来看看普通人和高手的回答! ## 普通人 嗯. 我想想 … 嗯… Mysql里面好像是用了B+树来做索引的! 然后… (乌鸦飞过) ## 高手 为了更清晰的解答这个问题,我打算从三个方面来回答: * 了解二叉树、AVL树、B树的概念 * B树和B+树的应用场景 * 1. B树是一种多路平衡查找树,为了更形象的理解,(我们来看这张图)。 二叉树,每个节点支持两个分支的树结构,相比于单向链表,多了一个分支。 二叉查找树,在二叉树的基础上增加了一个规则,左子树的所有节点的值都小于它的根节点,右子树的所有子节点都大于它的根节点。 ![image-20220314115513853](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220314115513853.png) (如图),二叉查找树会出现斜树问题,导致时间复杂度增加,因此又引入了一种平衡二叉树,它具有二叉查找树的所有特点,同时增加了一个规则:”它的左右两个子树的高度差的绝对值不超过1“。平衡二叉树会采用左旋、右旋的方式来实现平衡。 ![image-20220314115535464](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220314115535464.png) (如图),而B树是一种多路平衡查找树,它满足平衡二叉树的规则,但是它可以有多个子树,子树的数量取决于关键字的数量,比如这个图中根节点有两个关键字3和5,那么它能够拥有的子路数量=关键字数+1。 因此从这个特征来看,在存储同样数据量的情况下,平衡二叉树的高度要大于B树。 ![image-20220314115948794](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220314115948794.png) B+树,其实是在B树的基础上做的增强,最大的区别有两个: 1. B树的数据存储在每个节点上,而B+树中的数据是存储在叶子节点,并且通过链表的方式把叶子节点中的数据进行连接。 2. B+树的子路数量等于关键字数 (如图所示)这个是B树的存储结构,从B树上可以看到每个节点会存储数据。 ![20220314003](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/20220314003.png) (如图所示)这个是B+树,B+树的所有数据是存储在叶子节点,并且叶子节点的数据是用双向链表关联的。 ![ ](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/20220314004.png) 2. B树和B+树,一般都是应用在文件系统和数据库系统中,用来减少磁盘IO带来的性能损耗。 以Mysql中的InnoDB为例,当我们通过`select`语句去查询一条数据时,InnoDB需要从磁盘上去读取数据,这个过程会涉及到磁盘IO以及磁盘的随机IO(如图所示) 我们知道磁盘IO的性能是特别低的,特别是随机磁盘IO。 因为,磁盘IO的工作原理是,首先系统会把数据逻辑地址传给磁盘,磁盘控制电路按照寻址逻辑把逻辑地址翻译成物理地址,也就是确定要读取的数据在哪个磁道,哪个扇区。 为了读取这个扇区的数据,需要把磁头放在这个扇区的上面,为了实现这一个点,磁盘会不断旋转,把目标扇区旋转到磁头下面,使得磁头找到对应的磁道,这里涉及到寻道事件以及旋转时间。 ![image-20220315134210543](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220315134210543.png) 很明显,磁盘IO这个过程的性能开销是非常大的,特别是查询的数据量比较多的情况下。 所以在InnoDB中,干脆对存储在磁盘块上的数据建立一个索引,然后把索引数据以及索引列对应的磁盘地址,以B+树的方式来存储。 如图所示,当我们需要查询目标数据的时候,根据索引从B+树中查找目标数据即可,由于B+树分路较多,所以只需要较少次数的磁盘IO就能查找到。 ![image-20220315144805358](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220315144805358.png) 3. 为什么用B树或者B+树来做索引结构?原因是AVL树的高度要比B树的高度要高,而高度就意味着磁盘IO的数量。所以为了减少磁盘IO的次数,文件系统或者数据库才会采用B树或者B+树。 以上就是我对B树和B+树的理解! ## 结尾 数据结构在实际开发中非常常见,比如数组、链表、双向链表、红黑树、跳跃表、B树、B+树、队列等。 在我看来,数据结构是编程中最重要的基本功之一。 学了顺序表和链表,我们就能知道查询操作比较多的场景中应该用顺序表,修改操作比较多的场景应该使用链表。 学了队列之后,就知道对于FIFO的场景中,应该使用队列。 学了树的结构后,会发现原来查找类的场景,还可以更进一步提升查询性能。 基本功决定大家在技术这个岗位上能够走到的高度。 好的,本期的普通人VS高手面试系列的视频就到这里结束了,喜欢的朋友记得点赞收藏。 如果最近大家遇到一些场景类和方案设计类的问题,欢迎私信我,我在后续的内容中给大家做解答! 我是Mic,一个工作了14年的Java程序员,咱们下期再见。 # 能谈一下CAS机制吗? 一个小伙伴私信我,他说遇到了一个关于CAS机制的问题,他以为面试官问的是CAS实现单点登录。 心想,这个问题我熟啊,然后就按照单点登录的思路去回答,结果面试官一直摇头。 他来和我说,到了面试结束都没明想白自己回答这么好,怎么就没有当场给我发offer呢? 实际上,面试官问的是并发编程中的CAS机制。 下面我们来看看普通人和高手对于CAS机制的回答吧 ## 普通人 CAS,是并发编程中用来实现原子性功能的一种操作,嗯,它类似于一种乐观锁的机制,可以保证并发情况下对共享变量的值的更改的原子性。 嗯, 像AtomicInteger这个类中,就用到了CAS机制。嗯… ## 高手 CAS是Java中Unsafe类里面的方法,它的全称是CompareAndSwap,比较并交换的意思。它的主要功能是能够保证在多线程环境下,对于共享变量的修改的原子性。 我来举个例子,比如说有这样一个场景(如图),有一个成员变量state,默认值是0, 定义了一个方法`doSomething()`,这个方法的逻辑是,判断state是否为0 ,如果为0,就修改成1。 这个逻辑看起来没有任何问题,但是在多线程环境下,会存在原子性的问题,因为这里是一个典型的,Read - Write的操作。 一般情况下,我们会在doSomething()这个方法上加同步锁来解决原子性问题。 ![carbon20220318001](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/carbon20220318001.png) 但是,加同步锁,会带来性能上的损耗,所以,对于这类场景,我们就可以使用CAS机制来进行优化 这个是优化之后的代码(如图) 在doSomething()方法中,我们调用了unsafe类中的compareAndSwapInt()方法来达到同样的目的,这个方法有四个参数, 分别是:当前对象实例、成员变量state在内存地址中的偏移量、预期值0、期望更改之后的值1。 CAS机制会比较state内存地址偏移量对应的值和传入的预期值0是否相等,如果相等,就直接修改内存地址中state的值为1. 否则,返回false,表示修改失败,而这个过程是原子的,不会存在线程安全问题。 ![carbon20220318002](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/carbon20220318002.png) CompareAndSwap是一个native方法,实际上它最终还是会面临同样的问题,就是先从内存地址中读取state的值,然后去比较,最后再修改。 这个过程不管是在什么层面上实现,都会存在原子性问题。 所以呢,CompareAndSwap的底层实现中,在多核CPU环境下,会增加一个Lock指令对缓存或者总线加锁,从而保证比较并替换这两个指令的原子性。 CAS主要用在并发场景中,比较典型的使用场景有两个。 1. 第一个是J.U.C里面Atomic的原子实现,比如AtomicInteger,AtomicLong。 2. 第二个是实现多线程对共享资源竞争的互斥性质,比如在AQS、ConcurrentHashMap、ConcurrentLinkedQueue等都有用到。 以上就是我对这个问题的理解。 ## 结尾 最近大家也发现了我的视频内容在高手回答部分的变化。 有些小伙伴说,你面试怎么还能带图来,明显作弊啊。 其实主要是最近很多的面试题都偏底层,而底层的内容涵盖的知识面比较广,大家平时几乎没有接触过。 所以,如果我想要去把这些知识传递给大家,就得做很多的图形和内容结构的设计,否则大家听完之后还是一脸懵逼。 好的,本期的普通人VS高手面试系列的视频就到这里结束了,喜欢的朋友记得点赞收藏。 我是Mic,一个工作了14年的Java程序员,咱们下期再见。 # 请说一下网络四元组 之前我一直在向大家征集一些刁钻的面试题,然后今天就收到了这样的一个问题。 “请你说一下网络四元组的理解”,他说他听到这个问题的时候,完全就懵了。 "这个是程序员应该懂的吗? 你是让我去做啥?造火箭吗?"(声音的起伏) 好吧,关于这个问题,我们来看看普通人和高手的回答。 ## 普通人 1. 啥,刚刚你问了什么?四元组? 四元组是什么东西?(内心戏+表情) 嗯..... 四元组是?(要说出来) 2. 我明白你意思,我不配做程序员,我自己滚~(内心戏+表情) ## 高手 四元组,简单理解就是在TCP协议中,去确定一个客户端连接的组成要素,它包括源IP地址、目标IP地址、源端口号、目标端口号。 正常情况下,我们对于网络通信的认识可能是这样(如图)。 服务端通过ServerSocket建立一个对指定端口号的监听,比如8080。 客户端通过目标ip和端口就可以和服务端建立一个连接,然后进行数据传输。 ![image-20220318214531492](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220318214531492.png) 但是我们知道的是,一个Server端可以接收多个客户端的连接,比如像这种情况(如图)。 那,当多个客户端连接到服务端的时候,服务端需要去识别每一个连接。 ![image-20220318222048758](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220318222048758.png) 并且(如图),TCP是全双工协议,也就是说数据允许在连接的两个方向上同时传输,因此这里的客户端,如果是反向通信,它又变成了服务端。 ![image-20220318222926416](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220318222926416.png) 所以基于这两个原因,就引入了四元组的设计,也就是说,当一个客户端和服务端建立一个TCP连接的时候,通过源IP地址、目标IP地址、源端口号、目标端口号来确定一个唯一的TCP连接。因为服务器的IP和端口是不变的,只要客户端的IP和端口彼此不同就OK了。 比如像这种情况(如图),同一个客户端主机上有三个连接连到Server端,那么这个时候源IP相同,源端口号不同。此时建立的四元组就是(10.23.15.3,59461 , 192.168.8.135,8080) 其中,源端口号是每次建立连接的时候系统自动分配的。 ![image-20220318230045901](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220318230045901.png) 以上就是我对于四元组的理解。 ## 结尾 网络部分的知识,可能大家作为一个CURD工程师,觉得没必要去理解。 但是未来呢?至少国内没有条件允许大家做一辈子CRUD,所以建议大家要“终局思维”来看待自己的职业规划。 好的,本期的普通人VS高手面试系列的视频就到这里结束了,喜欢的朋友记得点赞收藏。 我是Mic,一个工作了14年的Java程序员,咱们下期再见。 # 什么是服务网格? 今天继续来分享一个有趣的面试题,“什么是服务网格”? 服务网格这个概念出来很久了,从2017年被提出来,到2018年正式爆发,很多云厂商和互联网企业都在纷纷向服务网格靠拢。像蚂蚁集团、美团、百度、网易等一线互联网公司,都有服务网格的落地应用。 在我看来呢,服务网格是微服务架构的更进一步升级,它的核心目的是实现网络通信与业务逻辑的分离,使得开发人员更加专注在业务的实现上。 那么基于这个问题,我们来看看普通人和高手的回答。 ## 普通人 嗯? 内心戏:服务网格?服务网格是什么东西? 嗯, 很抱歉,这个问题我不是很清楚。 ## 高手 服务网格,也就是Service Mesh,它是专门用来处理服务通讯的基础设施层。它的主要功能是处理服务之间的通信,并且负责实现请求的可靠性传递。 Service Mesh,我们通常把他称为第三代微服务架构,既然是第三代,那么意味着他是在原来的微服务架构下做的升级。 为了更好的说明Service Mesh,那我就不得不说一下微服务架构部分的东西。 首先,当我们把一个电商系统以微服务化架构进行拆分后,会的到这样的一个架构(如图),其中包括Webserver、payment、inventory等等。 ![image-20220319153141256](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220319153141256.png) (如图)这些微服务应用,会被部署到Docker容器、或者Kubernetes集群。由于每个服务的业务逻辑是独立的,比如payment会实现支付的业务逻辑、order实现订单的处理、Webserver实现客户端请求的响应等。 ![image-20220319155839424](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220319155839424.png) (如图)所以,服务之间必须要相互通信,才能实现功能的完整性。比如用户把一个商品加入购物车,请求会进入到Webserver,然后转发到shopping cart进行处理,并存到数据库。 ![image-20220319155900416](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220319155900416.png) 而在这个过程中,每个服务之间必须要知道对方的通信地址,并且当有新的节点加入进来的时候,还需要对这些通信地址进行动态维护。所以,在第一代微服务架构中,每个微服务除了要实现业务逻辑以外,还需要解决上下游寻址、通讯、以及容错等问题。 (如图)于是,在第二代微服务架构下,引入了服务注册中心来实现服务之间的寻址,并且服务之间的容错机制、负载均衡也逐步形成了独立的服务框架,比如主流的Spring Cloud、或者Spring Cloud Alibaba。 ![image-20220319162844396](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220319162844396.png) 在第二代微服务架构中,负责业务开发的小伙伴不仅仅需要关注业务逻辑,还需要花大量精力去处理微服务中的一些基础性配置工作,虽然Spring Cloud已经尽可能去完成了这些事情,但对于开发人员来说,学习Spring Cloud,以及针对Spring Cloud的配置和维护,仍然存在较大的挑战。另外呢,也增加了整个微服务的复杂性。 实际上,在我看来,“微服务中所有的这些服务注册、容错、重试、安全等工作,都是为了保证服务之间通信的可靠性”。 于是,就有了第三代微服务架构,Service Mesh。 (如图)原本模块化到微服务框架里的微服务基础能力,被进一步的从一个SDK中演进成了一个独立的代理进程-SideCar SideCar的主要职责就是负责各个微服务之间的通信,承载了原本第二代微服务架构中的服务发现、调用容错、服务治理等功能。使得微服务基础能力和业务逻辑迭代彻底解耦。 ![image-20220319165030924](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220319165030924.png) 之所以我们称Service Mesh为服务网格,是因为在大规模微服务架构中,每个服务的通信都是由SideCar来代理的,各个服务之间的通信拓扑图,看起来就像一个网格形状(如图)。 ![image-20220319170808603](C:\Users\NINGMEI\AppData\Roaming\Typora\typora-user-images\image-20220319170808603.png) Istio是目前主流的Service Mesh开源框架。 以上就是我对服务网格的理解。 ## 结尾 Service Mesh架构其实就是云原生时代的微服务架构,对于大部分企业来说,仍然是处在第二代微服务架构下。 所以,很多小伙伴不一定能够知道。 不过,技术是在快速迭代的,有一句话叫“时代抛弃你的时候,连一句再见也不会说”,就像有些人在外包公司干了10多年 再出来面试,发现很多公司要求的技术栈,他都不会。所以,建议大家要时刻刷新自己的能力,保持竞争优势! 好的,本期的普通人VS高手面试系列的视频就到这里结束了,喜欢的朋友记得点赞收藏。 我是Mic,一个工作了14年的Java程序员,咱们下期再见。 # Redis和Mysql如何保证数据一致性 今天分享一道一线互联网公司高频面试题。 “Redis和Mysql如何保证数据一致性”。 这个问题难倒了不少工作5年以上的程序员,难的不是问题本身,而是解决这个问题的思维模式。 下面来看看普通人和高手对于这个问题的回答。 ## 普通人 嗯.... Redis和Mysql的数据一致性保证是吧?我想想。 嗯 , 就是,Mysql的数据发生变化以后,可以同步修改Redis里面的数据。 ## 高手 一般情况下,Redis用来实现应用和数据库之间读操作的缓存层,主要目的是减少数据库IO,还可以提升数据的IO性能。 这是它的整体架构。 当应用程序需要去读取某个数据的时候,首先会先尝试去Redis里面加载,如果命中就直接返回。如果没有命中,就从数据库查询,查询到数据后再把这个数据缓存到Redis里面。 ![image-20220321194004261](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220321194004261.png) (如图)在这样一个架构中,会出现一个问题,就是一份数据,同时保存在数据库和Redis里面,当数据发生变化的时候,需要同时更新Redis和Mysql,由于更新是有先后顺序的,并且它不像Mysql中的多表事务操作,可以满足ACID特性。所以就会出现数据一致性问题。 ![image-20220321202345646](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220321202345646.png) 在这种情况下,能够选择的方法只有几种。 1. 先更新数据库,再更新缓存 2. 先删除缓存,再更新数据库 如果先更新数据库,再更新缓存,如果缓存更新失败,就会导致数据库和Redis中的数据不一致。 ![image-20220321210016652](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220321210016652.png) 如果是先删除缓存,再更新数据库,理想情况是应用下次访问Redis的时候,发现Redis里面的数据是空的,就从数据库加载保存到Redis里面,那么数据是一致的。但是在极端情况下,由于删除Redis和更新数据库这两个操作并不是原子的,所以这个过程如果有其他线程来访问,还是会存在数据不一致问题。 ![image-20220321211003351](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220321211003351.png) 所以,如果需要在极端情况下仍然保证Redis和Mysql的数据一致性,就只能采用最终一致性方案。 (如图)比如基于RocketMQ的可靠性消息通信,来实现最终一致性。 ![image-20220321212602212](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220321212602212.png) (如图)还可以直接通过Canal组件,监控Mysql中binlog的日志,把更新后的数据同步到Redis里面。 ![image-20220321213239428](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220321213239428.png) 因为这里是基于最终一致性来实现的,如果业务场景不能接受数据的短期不一致性,那就不能使用这个方案来做。 以上就是我对这个问题的理解。 ## 结尾 在面试的时候,面试官喜欢问各种没有场景化的纯粹的技术问题,比如说:“你这个最终一致性方案”还是会存在数据不一致的问题啊?那怎么解决? 先不用慌,技术是为业务服务的,所以不同的业务场景,对于技术的选择和方案的设计都是不同的,所以这个时候,可以反问面试官,具体的业务场景是什么? 一定要知道的是,一个技术方案不可能cover住所有的场景,明白了吗? 好的,好的,本期的普通人VS高手面试系列的视频就到这里结束了,喜欢的朋友记得点赞收藏。 另外,最近从评论区收到的面试问题,都有点太泛了,比如:说一下mongoDB啊,说一下kafka啊、说一下并发啊,这些问题都是要几个小时才能彻底说清楚,建议大家提具体一点的问题。 我是Mic,一个工作了14年的Java程序员,咱们下期再见。 # Spring Boot中自动装配机制的原理 最近一个粉丝说,他面试了4个公司,有三个公司问他:“Spring Boot 中自动装配机制的原理” 他回答了,感觉没回答错误,但是怎么就没给offer呢? 对于这个问题,看看普通人和高手该如何回答。 ## 普通人 嗯… Spring Boot里面的自动装配,就是@EnableAutoConfiguration注解。 嗯…它可以实现Bean的自动管理,不需要我们手动再去配置。 ## 高手 自动装配,简单来说就是自动把第三方组件的Bean装载到Spring IOC器里面,不需要开发人员再去写Bean的装配配置。 在Spring Boot应用里面,只需要在启动类加上@SpringBootApplication注解就可以实现自动装配。 @SpringBootApplication是一个复合注解,真正实现自动装配的注解是@EnableAutoConfiguration。 (如图)自动装配的实现主要依靠三个核心关键技术。 1. 引入Starter启动依赖组件的时候,这个组件里面必须要包含@Configuration配置类,在这个配置类里面通过@Bean注解声明需要装配到IOC容器的Bean对象。 2. 这个配置类是放在第三方的jar包里面,然后通过SpringBoot中的约定优于配置思想,把这个配置类的全路径放在classpath:/META-INF/spring.factories文件中。这样SpringBoot就可以知道第三方jar包里面的配置类的位置,这个步骤主要是用到了Spring里面的SpringFactoriesLoader来完成的。 3. SpringBoot拿到所第三方jar包里面声明的配置类以后,再通过Spring提供的ImportSelector接口,实现对这些配置类的动态加载。 在我看来,SpringBoot是约定优于配置这一理念下的产物,所以在很多的地方,都会看到这类的思想。它的出现,让开发人员更加聚焦在了业务代码的编写上,而不需要去关心和业务无关的配置。 其实,自动装配的思想,在SpringFramework3.x版本里面的@Enable注解,就有了实现的雏形。@Enable注解是模块驱动的意思,我们只需要增加某个@Enable注解,就自动打开某个功能,而不需要针对这个功能去做Bean的配置,@Enable底层也是帮我们去自动完成这个模块相关Bean的注入。 以上,就是我对Spring Boot自动装配机制的理解。 ![image-20220322101025232](https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/image-20220322101025232.png) ## 结尾 发现了吗?高手和普通人的回答,并不是回答的东西多和少。 而是让面试官看到你对于这个技术领域的理解深度和自己的见解,从而让面试官在一大堆求职者中,对你产生清晰的印象。 好的,本期的普通人VS高手面试系列的视频就到这里结束了,喜欢的朋友记得点赞收藏。 我是Mic,一个工作了14年的Java程序员,咱们下期再见。 # 死锁的发生原因和怎么避免 一个去阿里面试的小伙伴私信我说:今天被一个死锁的问题难到了。 平常我都特意看了死锁这块的内容,但是回答的时候就想不起来。 这里可能存在一个误区,认为技术是要靠记的。 大家可以想想,平时写代码的时候,这些代码是背下来的吗? 遇到一个需求的时候,能够立刻提供解决思路,这个也是记下来的吗? 所有的技术问题,都可以用一个问题来解决: “如果让你遇到这个问题,你会怎么设计”? 当你大脑一篇空白时,说明你目前掌握的技术只能足够支撑你写CURD的能力。 好了,下面来看看普通人和高手是如何回答这个问题的。 ## 普通人 临场发挥… ## 高手 (如图),死锁,简单来说就是两个或者两个以上的线程在执行的过程中,争夺同一个共享资源造成的相互等待的现象。 如果没有外部干预,线程会一直阻塞无法往下执行,这些一直处于相互等待资源的线程就称为死锁线程。 image-20210612222402287 导致死锁的条件有四个,也就是这四个条件同时满足就会产生死锁。 - 互斥条件,共享资源 X 和 Y 只能被一个线程占用; - 请求和保持条件,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X; - 不可抢占条件,其他线程不能强行抢占线程 T1 占有的资源; - 循环等待条件,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。 导致死锁之后,只能通过人工干预来解决,比如重启服务,或者杀掉某个线程。 所以,只能在写代码的时候,去规避可能出现的死锁问题。 按照死锁发生的四个条件,只需要破坏其中的任何一个,就可以解决,但是,互斥条件是没办法破坏的,因为这是互斥锁的基本约束,其他三方条件都有办法来破坏: - 对于“请求和保持”这个条件,我们可以一次性申请所有的资源,这样就不存在等待了。 - 对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。 - 对于“循环等待”这个条件,可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了。 以上就是我对这个问题的理解。 ## 结尾 发现了吗? 当大家理解了死锁发生的条件,那么对于这些条件的破坏, 是可以通过自己的技术积累,来设计解决方法的。 所有的技术思想和技术架构,都是由人来设计的,为什么别人能够设计? 本质上,还是技术积累后的结果!越是底层的设计,对于知识面的要求就越多。 好的,本期的普通人VS高手面试系列的视频就到这里结束了,喜欢的朋友记得点赞收藏。 我是Mic,一个工作了14年的Java程序员,咱们下期再见。