大厂面试系列一些问题的解答
- MVCC,binlog,redolog,undolog都是什么,起什么作用?
(1)undolog 也就是我们常说的回滚日志文件 主要用于事务中执行失败,进行回滚,以及MVCC中对于数据历史版本的查看。由引擎层的InnoDB引擎实现,是逻辑日志,记录数据修改被修改前的值,比如"把id='B' 修改为id = 'B2' ,那么undo日志就会用来存放id ='B'的记录”。当一条数据需要更新前,会先把修改前的记录存储在undolog中,如果这个修改出现异常,则会使用undo日志来实现回滚操作,保证事务的一致性。当事务提交之后,undo log并不能立马被删除,而是会被放到待清理链表中,待判断没有事物用到该版本的信息时才可以清理相应undolog。它保存了事务发生之前的数据的一个版本,用于回滚,同时可以提供多版本并发控制下的读(MVCC),也即非锁定读。 (2)redoLog 是重做日志文件是记录数据修改之后的值,用于持久化到磁盘中。redo log包括两部分:一是内存中的日志缓冲(redo log buffer),该部分日志是易失性的;二是磁盘上的重做日志文件(redo log file),该部分日志是持久的。由引擎层的InnoDB引擎实现,是物理日志,记录的是物理数据页修改的信息,比如“某个数据页上内容发生了哪些改动”。当一条数据需要更新时,InnoDB会先将数据更新,然后记录redoLog 在内存中,然后找个时间将redoLog的操作执行到磁盘上的文件上。不管是否提交成功我都记录,你要是回滚了,那我连回滚的修改也记录。它确保了事务的持久性。 (3)MVCC多版本并发控制是MySQL中基于乐观锁理论实现隔离级别的方式,用于读已提交和可重复读取隔离级别的实现。在MySQL中,会在表中每一条数据后面添加两个字段:最近修改该行数据的事务ID,指向该行(undolog表中)回滚段的指针。Read View判断行的可见性,创建一个新事务时,copy一份当前系统中的活跃事务列表。意思是,当前不应该被本事务看到的其他事务id列表。 (4)binlog由Mysql的Server层实现,是逻辑日志,记录的是sql语句的原始逻辑,比如"把id='B' 修改为id = ‘B2’。binlog会写入指定大小的物理文件中,是追加写入的,当前文件写满则会创建新的文件写入。 产生:事务提交的时候,一次性将事务中的sql语句,按照一定的格式记录到binlog中。用于复制和恢复在主从复制中,从库利用主库上的binlog进行重播(执行日志中记录的修改逻辑),实现主从同步。业务数据不一致或者错了,用binlog恢复
- Kafka是如何实现高吞吐率的?
1)顺序读写:kafka的消息是不断追加到文件中的,这个特性使kafka可以(2)充分利用磁盘的顺序读写性能 (3)零拷贝:跳过“用户缓冲区”的拷贝,建立一个磁盘空间和内存的直接映射,数据不再复制到“用户态缓冲区” (4)文件分段:kafka的队列topic被分为了多个区partition,每个partition又分为多个段segment,所以一个队列中的消息实际上是保存在N多个片段文件中 (5)批量发送:Kafka允许进行批量发送消息,先将消息缓存在内存中,然后一次请求批量发送出去 (6)数据压缩:Kafka还支持对消息集合进行压缩,Producer可以通过GZIP或Snappy格式对消息集合进行压缩
- Http请求的完全过程
(1)浏览器根据域名解析IP地址(DNS),并查DNS缓存 (2)浏览器与WEB服务器建立一个TCP连接 (3)浏览器给WEB服务器发送一个HTTP请求(GET/POST):一个HTTP请求报文由请求行(request line)、请求头部(headers)、空行(blank line)和请求数据(request body)4个部分组成。 (4)服务端响应HTTP响应报文,报文由状态行(status line)、相应头部(headers)、空行(blank line)和响应数据(response body)4个部分组成。 (5)浏览器解析渲染
- Spring的@Transactional如何实现的?
(1)配置文件开启注解驱动,在相关的类和方法上通过注解@Transactional标识。 (2)spring 在启动的时候会去解析生成相关的bean,这时候会查看拥有相关注解的类和方法,并且为这些类和方法生成代理,并根据@Transaction的相关参数进行相关配置注入,这样就在代理中为我们把相关的事务处理掉了(开启正常提交事务,异常回滚事务)。 (3)真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的
- 为什么要使用线程池?
(1)降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 (2)提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。 (3)提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
- rpc框架实现原理?
主要有以下几个步骤: (1)建立通信: 首先要解决通讯的问题:即A机器想要调用B机器,首先得建立起通信连接。主要是通过在客户端和服务器之间建立TCP连接,远程过程调用的所有相关的数据都在这个连接里面进行传输交换。 通常这个连接可以是按需连接(需要调用的时候就先建立连接,调用结束后就立马断掉),也可以是长连接(客户端和服务器建立起连接之后保持长期持有,不管此时有无数据包的发送,可以配合心跳检测机制定期检测建立的连接是否存活有效),多个远程过程调用共享同一个连接。 (2)服务寻址: 解决寻址的问题:即A机器上的应用A要调用B机器上的应用B,那么此时对于A来说如何告知底层的RPC框架所要调用的服务具体在哪里呢? 通常情况下我们需要提供B机器(主机名或IP地址)以及特定的端口,然后指定调用的方法或者函数的名称以及入参出参等信息,这样才能完成服务的一个调用。比如基于Web服务协议栈的RPC,就需要提供一个endpoint URI,或者是从UDDI服务上进行查找。如果是RMI调用的话,还需要一个RMI Registry来注册服务的地址。 (3)网络传输: ①序列化 当A机器上的应用发起一个RPC调用时,调用方法和其入参等信息需要通过底层的网络协议如TCP传输到B机器,由于网络协议是基于二进制的,所有我们传输的参数数据都需要先进行序列化(Serialize)或者编组(marshal)成二进制的形式才能在网络中进行传输。然后通过寻址操作和网络传输将序列化或者编组之后的二进制数据发送给B机器。 ②反序列化 当B机器接收到A机器的应用发来的请求之后,又需要对接收到的参数等信息进行反序列化操作(序列化的逆操作),即将二进制信息恢复为内存中的表达方式,然后再找到对应的方法(寻址的一部分)进行本地调用(一般是通过生成代理Proxy去调用, 通常会有JDK动态代理、CGLIB动态代理、Javassist生成字节码技术等),之后得到调用的返回值。 (4)服务调用: B机器进行本地调用(通过代理Proxy)之后得到了返回值,此时还需要再把返回值发送回A机器,同样也需要经过序列化操作,然后再经过网络传输将二进制数据发送回A机器,而当A机器接收到这些返回值之后,则再次进行反序列化操作,恢复为内存中的表达方式,最后再交给A机器上的应用进行相关处理(一般是业务逻辑处理操作)。
- redis热key怎么解决?
(1)利用二级缓存: 比如利用ehcache,或者一个HashMap都可以。在你发现热key以后,把热key加载到系统的JVM中。针对这种热key请求,会直接从jvm中取,而不会走到redis层。假设此时有十万个针对同一个key的请求过来,如果没有本地缓存,这十万个请求就直接怼到同一台redis上了。 现在假设,你的应用层有50台机器,OK,你也有jvm缓存了。这十万个请求平均分散开来,每个机器有2000个请求,会从JVM中取到value值,然后返回数据。避免了十万个请求怼到同一台redis上的情形。 (2)备份热key: 这个方案也很简单。不要让key走到同一台redis上不就行了。我们把这个key,在多个redis上都存一份不就好了。接下来,有热key请求进来的时候,我们就在有备份的redis上随机选取一台,进行访问取值,返回数据。
- 不同年代GC收集器有哪些?
(1)serial收集器:单线程,工作时必须暂停其他工作线程。多用于client机器上,使用复制算法 (2)ParNew收集器:serial收集器的多线程版本,server模式下虚拟机首选的新生代收集器。复制算法 (3)Parallel Scavenge收集器:复制算法,可控制吞吐量的收集器。吞吐量即有效运行时间。 (4)Serial Old收集器:serial的老年代版本,使用整理算法。 (5)Parallel Old收集器:第三种收集器的老年代版本,多线程,标记整理 (6)CMS收集器:目标是最短回收停顿时间。标记清除算法实现,分四个阶段: ?初始标记:GC Roots直连的对象做标记 ?并发标记:多线程方式GC Roots Tracing ?重新标记:修正第二阶段标记的记录 ?并发清除。 缺点:标记清除算法的缺点,产生碎片。CPU资源敏感。
- ES脑裂问题分析及优化
(1)脑裂问题可能的成因 ?网络问题:集群间的网络延迟导致一些节点访问不到master,认为master挂掉了从而选举出新的master,并对master上的分片和副本标红,分配新的主分片 ?节点负载:主节点的角色既为master又为data,访问量较大时可能会导致ES停止响应造成大面积延迟,此时其他节点得不到主节点的响应认为主节点挂掉了,会重新选取主节点。 ?内存回收:data节点上的ES进程占用的内存较大,引发JVM的大规模内存回收,造成ES进程失去响应。 (2)脑裂问题解决方案: ?减少误判:discovery.zen.ping_timeout节点状态的响应时间,默认为3s,可以适当调大,如果master在该响应时间的范围内没有做出响应应答,判断该节点已经挂掉了。调大参数(如6s,discovery.zen.ping_timeout:6),可适当减少误判。 ?选举触发 discovery.zen.minimum_master_nodes:1 该参数是用于控制选举行为发生的最小集群主节点数量。 当备选主节点的个数大于等于该参数的值,且备选主节点中有该参数个节点认为主节点挂了,进行选举。官方建议为(n/2)+1,n为主节点个数(即有资格成为主节点的节点个数) 增大该参数,当该值为2时,我们可以设置master的数量为3,这样,挂掉一台,其他两台都认为主节点挂掉了,才进行主节点选举。 ?角色分离:即master节点与data节点分离,限制角色
- 讲一讲类加载的过程
一般来说,我们把 Java 的类加载过程分为三个主要步骤:加载,连接,初始化,具体行为在 Java 虚拟机规范里有非常详细的定义。 (1)首先是加载过程(Loading),它是 Java 将字节码数据从不同的数据源读取到 JVM 中,并映射为 JVM 认可的数据结构(Class 对象),这里的数据源可能是各种各样的形态,比如 jar 文件,class 文件,甚至是网络数据源等;如果输入数据不是 ClassFile 的结构,则会抛出 ClassFormatError。加载阶段是用户参与的阶段,我们可以自定义类加载器,去实现自己的类加载过程。 (2)第二阶段是连接(Linking),这是核心的步骤,简单说是把原始的类定义信息平滑地转入 JVM 运行的过程中。这里可进一步细分成三个步骤:1,验证(Verification),这是虚拟机安全的重要保障,JVM 需要核验字节信息是符合 Java 虚拟机规范的,否则就被认为是 VerifyError,这样就防止了恶意信息或者不合规信息危害 JVM 的运行,验证阶段有可能触发更多 class 的加载。2,准备(Pereparation),创建类或者接口中的静态变量,并初始化静态变量的初始值。但这里的“初始化”和下面的显示初始化阶段是有区别的,侧重点在于分配所需要的内存空间,不会去执行更进一步的 JVM 指令。3,解析(Resolution),在这一步会将常量池中的符号引用(symbolic reference)替换为直接引用。在 Java 虚拟机规范中,详细介绍了类,接口,方法和字段等各方面的解析。 (3)最后是初始化阶段(initialization),这一步真正去执行类初始化的代码逻辑,包括静态字段赋值的动作,以及执行类定义中的静态初始化块内的逻辑,编译器在编译阶段就会把这部分逻辑整理好,父类型的初始化逻辑优先于当前类型的逻辑。再来谈谈双亲委派模型,简单说就是当加载器(Class-Loader)试图加载某个类型的时候,除非父类加载器找不到相应类型,否则尽量将这个任务代理给当前加载器的父加载器去做。使用委派模型的目的是避免重复加载 Java 类型。
- 不可重复读和幻读区别
(1)"不可重复读" 是指在一个事务内,多次读同一数据。在这个事务还没有结束时,bai另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。 (2)幻觉读是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。
- 单例对象会被jvm的gc时回收吗
(1)jvm卸载类的判定条件如下: ①该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。 ②加载该类的ClassLoader已经被回收。 ③该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。 (2)只有上面三个条件都满足,jvm才会在垃圾收集的时候卸载类。显然,单例的类不满足条件一,因此单例类也不会被卸载。也就是说,只要单例类中的静态引用指向jvm堆中的单例对象,那么单例类和单例对象都不会被垃圾收集,依据根搜索算法,对象是否会被垃圾收集与未被使用时间长短无关,仅仅在于这个对象是不是不可回收的。
- Get和Post区别
(1)Get是不安全的,因为在传输过程,数据被放在请求的URL中;Post的所有操作对用户来说都是不可见的。 (2)Get传送的数据量较小,这主要是因为受URL长度限制;Post传送的数据量较大,一般被默认为不受限制。 (3)Get限制Form表单的数据集的值必须为ASCII字符;而Post支持整个ISO10646字符集。 (4)Get执行效率却比Post方法好。Get是form提交的默认方法。 GET产生一个TCP数据包;POST产生两个TCP数据包。(非必然,客户端可灵活决定)
- 死锁的4个必要条件
(1)互斥条件:一个资源每次只能被一个线程使用; (2)请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放; (3)不剥夺条件:进程已经获得的资源,在未使用完之前,不能强行剥夺; (4)循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系
- Redis 的数据结构及使用场景
1)String字符串:字符串类型是 Redis 最基础的数据结构,首先键都是字符串类型,而且 其他几种数据结构都是在字符串类型基础上构建的,我们常使用的 set key value 命令就是字符串。常用在缓存、计数、共享Session、限速等。 (2)Hash哈希:在Redis中,哈希类型是指键值本身又是一个键值对 结构,添加命令:hset key field value。哈希可以用来存放用户信息,比如实现购物车。 (3)List列表(双向链表):列表(list)类型是用来存储多个有序的字符串。可以做简单的消息队列的功能。 (4)Set集合:集合(set)类型也是用来保存多个的字符串元素,但和列表类型不一 样的是,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过 索引下标获取元素。利用 Set 的交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。 (5)Sorted Set有序集合(跳表实现):Sorted Set 多了一个权重参数 Score,集合中的元素能够按 Score 进行排列。可以做排行榜应用,取 TOP N 操作
- ZAB协议
ZAB协议包括两种基本的模式:崩溃恢复和消息广播。当整个 Zookeeper 集群刚刚启动或者Leader服务器宕机、重启或者网络故障导致不存在过半的服务器与 Leader 服务器保持正常通信时,所有服务器进入崩溃恢复模式,首先选举产生新的 Leader 服务器,然后集群中 Follower 服务器开始与新的 Leader 服务器进行数据同步。 当集群中超过半数机器与该 Leader 服务器完成数据同步之后,退出恢复模式进入消息广播模式,Leader 服务器开始接收客户端的事务请求生成事物提案来进行事务请求处理。
- volatile作用
(1)volatile在多处理器开发中保证了共享变量的“ 可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。(共享内存,私有内存) (2)volatile关键字通过“内存屏障”来防止指令被重排序。
- 什么是值传递和引用传递
(1)值传递是对基本型变量而言的,传递的是该变量的一个副本,改变副本不影响原变量. (2)引用传递一般是对于对象型变量而言的,传递的是该对象地址的一个副本, 并不是原对象本身 。
- Java内存模型
Java虚拟机规范中将Java运行时数据分为六种: (1)程序计数器:是一个数据结构,用于保存当前正常执行的程序的内存地址。Java虚拟机的多线程就是通过线程轮流切换并分配处理器时间来实现的,为了线程切换后能恢复到正确的位置,每条线程都需要一个独立的程序计数器,互不影响,该区域为“线程私有”。 (2)Java虚拟机栈:线程私有的,与线程生命周期相同,用于存储局部变量表,操作栈,方法返回值。局部变量表放着基本数据类型,还有对象的引用。 (3)本地方法栈:跟虚拟机栈很像,不过它是为虚拟机使用到的Native方法服务。 (4)Java堆:所有线程共享的一块内存区域,对象实例几乎都在这分配内存。 (5)方法区:各个线程共享的区域,储存虚拟机加载的类信息,常量,静态变量,编译后的代码。 (6)运行时常量池:代表运行时每个class文件中的常量表。包括几种常量:编译时的数字常量、方法或者域的引用。
- string.stringbuilder.stringbuffer的区别,为什么string不可变
(1)区别 ①String是字符串常量,而StringBuffer和StringBuilder是字符串变量。由String创建的字符内容是不可改变的,而由StringBuffer和StringBuidler创建的字符内容是可以改变的。 ②StringBuffer是线程安全的,而StringBuilder是非线程安全的。StringBuilder是从JDK 5开始,为StringBuffer类补充的一个单线程的等价类。我们在使用时应优先考虑使用StringBuilder,因为它支持StringBuffer的所有操作,但是因为它不执行同步,不会有线程安全带来额外的系统消耗,所以速度更快。 (2)String为什么不可变: 虽然String、StringBuffer和StringBuilder都是final类,它们生成的对象都是不可变的,而且它们内部也都是靠char数组实现的,但是不同之处在于,String类中定义的char数组是final的,而StringBuffer和StringBuilder都是继承自AbstractStringBuilder类,它们的内部实现都是靠这个父类完成的,而这个父类中定义的char数组只是一个普通是私有变量,可以用append追加。因为AbstractStringBuilder实现了Appendable接口
- 为什么在重写equals方法的时候要重写hashcode的方法?
(1)我们知道判断的时候先根据hashcode进行的判断,相同的情况下再根据equals()方法进行判断。如果只重写了equals方法,而不重写hashcode的方法,造成hashcode的值不同,而equals()方法判断出来的结果为true。 (2)在Java中的一些容器中,不允许有两个完全相同的对象,插入的时候,如果判断相同则会进行覆盖。这时候如果只重写了equals()的方法,而不重写hashcode的方法,Object中hashcode是根据对象的存储地址转换而形成的一个哈希值。这时候就有可能因为没有重写hashcode方法,造成相同的对象散列到不同的位置而造成对象的不能覆盖的问题。
- 反射的作用是什么?
(1)反射的主要作用是用来扩展系统和动态调用程序集。 (2)所谓扩展系统就是先把系统写好,系统里面定义接口,后面开发的人去写接口的代码。 (3)动态调用程序集就是利用反射去调用编译好的dll,当然此时的dll没有被引用到你所建的工程里面。
- 同步与异步区别?
(1)同步,可以理解为在执行完一个函数或方法之后,一直等待系统返回值或消息,这时程序是出于阻塞的,只有接收到返回的值或消息后才往下执行其他的命令。 (2)异步,执行完函数或方法后,不必阻塞性地等待返回值或消息,只需要向系统委托一个异步过程,那么当系统接收到返回值或消息时,系统会自动触发委托的异步过程,从而完成一个完整的流程。
- Java中overload override的区别
(1)Overload是重载的意思,Override是覆盖的意思,也就是重写。 (2)重写Override表示子类中的方法可以与父类中的某个方法的名称和参数完全相同,通过子类创建的实例对象调用这个方法时,将调用子类中定义的方法,这相当于把父类中的方法给覆盖了,这也是多态性的一种表现。 (3)重载overload的特点就是与返回值无关,只看参数列表,所以重载的方法可以改变返回值类型。所以,如果两个方法的参数列表完全一样,是不能通过让它们的返回值类型不同来实现重载的。 (4)override是覆盖一个方法并且对其重写,以求达到不同的作用。对我们来说最熟悉的覆盖就是对接口方法的实现,在接口中一般只是对方法进行了声明,而我们在实现时,就需要实现接口声明的所有方法。除了这个典型的用法以外,我们在继承中也可能会在子类覆盖父类中的方法。 (5)overload对我们来说可能比较熟悉,可以翻译为重载,它是指我们可以定义一些名称相同的方法,通过定义不同类型的输入参数来区分这些方法,然后再调用时,JVM就会根据不同的参数样式,来选择合适的方法执行。 (6)方法的重写和重载是Java多态性的不同表现。重写是父类与子类之间多态性的一种表现,而重载是一个类中多态性的一种表现。
- 线程的创建方式
(1)继承Thread ①定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。 ②创建Thread子类的实例,即创建了线程对象。 ③调用线程对象的start()方法来启动该线程。 (2)实现Runnable接口 ①定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。 ②创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。 ③调用线程对象的start()方法来启动该线程。 (3)实现Callable接口 ①创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。 ②创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。 ③使用FutureTask对象作为Thread对象的target创建并启动新线程。 ④调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
- LinkList和ArrayList的区别
(1)ArrayList的底层实现就是数组,且ArrayList实现了RandomAccess,表示它能快速随机访问存储的元素,通过下标 index 访问,只是我们需要用 get() 方法的形式,数组支持随机访问,查询速度快,增删元素慢; (2)LinkedList的底层实现是链表,LinkedList没有实现RandomAccess 接口,链表支持顺序访问,查询速度慢,增删元素快;
- 垃圾回收算法
(1)标记—清除算法 标记—清除算法是最基础的收集算法,它分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程其实就是前面的可达性分析算法中判定垃圾对象的标记过程。 (2)复制算法 复制算法是针对标记—清除算法的缺点,在其基础上进行改进而得到的,它将可用内存按容量分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面,然后再把已使用过的内存空间一次清理掉。 (3)标记—整理算法 复制算法比较适合于新生代,在老年代中,对象存活率比较高,如果执行较多的复制操作,效率将会变低,所以老年代一般会选用其他算法,如标记—整理算法。该算法标记的过程与标记—清除算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存
- Hashtable 和 HashMap的区别?
( 1)主要区别在于 HashMap允许将 null作为一个 entry的 key或者 value,而 Hashtable不允许。由于非线程安全,多线程情况下,效率上可能高于 Hashtable。 Hashtable和 HashMap采用的 hash/rehash 算法都大概一样,所以单线程性能不会有很大的差异。 ( 2) Hashtable的方法是 Synchronize的,而 HashMap不是,在多个线程访问 Hashtable时,不需要自己为它的方法实现同步,而 HashMap 就必须为之提供外同步 (Collections.synchronizedMap)。 ( 3) HashMap是 Hashtable的轻量级实现(非线程安全的实现),他们都完成了 Map接口, Hashtable继承自 Dictionary类,而 HashMap是 Java1.2引进的 Map interface的一个实现。
- servlet的线程安全问题?
答:如果代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。那么我们都知道servlet是多线程的,同时一个servlet实现类只会有一个实例对象,也就是它是Singleton的,所以多个线程是可能会访问同一个servlet实例对象的。同一个实例对象被多个线程访问,如果没有做同步处理,那么servlet就是非线程安全的,如果做了同步处理,就是线程安全。所以,servlet是否线程安全是由它的实现来决定的,如果它内部的属性或方***被多个线程改变,它就是线程不安全的,反之,就是线程安全的。
- 为什么要用volatile关键字?
答:在高并发时会出现并发模式异常,volatile可以防止指令重排,创建对象操作并不是一个原子操作,分为三个步骤 (1)构建对象:根据Person类元信息确定对象的大小,向JVM堆中申请一块内存区域并构建对象的默认信息(加载Person对象成员变量信息并赋默认值如 int类型为0,引用类型为null)。 (2)初始化对象:然后执行对象内部生成的init方法,初始化成员变量值,同时执行搜集到的{}代码块逻辑,最后执行对象构造方法。 (3)引用对象:对象实例化完毕后,再把栈中的Person对象引用地址指向Person对象在堆内存中的地址......
- java并发锁机制。
(1)偏向锁:JDK1.6提出来的一种锁优化的机制。其核心的思想是,如果程序没有竞争,则取消之前已经取得锁的线程同步操作。也就是说,若某一锁被线程获取后,便进入偏向模式,当线程再次请求这个锁时,就无需再进行相关的同步操作了,从而节约了操作时间,如果在此之间有其他的线程进行了锁请求,则锁退出偏向模式 (2)轻量级锁:如果偏向锁失败,Java虚拟机就会让线程申请轻量级锁,轻量级锁在虚拟机内部,使用一个成为BasicObjectLock的对象实现的,这个对象内部由一个BasicLock对象和一个持有该锁的Java对象指针组成。BasicObjectLock对象放置在Java栈帧中。在BasicLock对象内部还维护着displaced_header字段,用于备份对象头部的Mark Word。 (3)重量级锁:当轻量级锁失败,虚拟机就会使用重量级锁。重量级锁在操作过程中,线程可能会被操作系统层面挂起,如果是这样,线程间的切换和调用成本就会大大提高。 (4)自旋锁:它可以使线程在没有取得锁的时候,不被挂起,而转去执行一个空循环,若在若干个空循环后,线程如果可以获得锁,则继续执行。若线程依然不能获得锁,才会被挂起。使用自旋锁后,线程被挂起的几率相对减少,线程执行的连贯性相对加强。因此,对于那些锁竞争不是很激烈,锁占用时间很短的并发线程,具有一定的积极意义,但对于锁竞争激烈,单线程锁占用很长时间的并发程序,自旋锁在自旋等待后,往往毅然无法获得对应的锁,不仅仅白白浪费了CPU时间,最终还是免不了被挂起的操作 ,反而浪费了系统的资源。
- 了解zookeeper的leader选取算法吗,讲一下它的流程。
(1)自增选举轮次。Zookeeper规定所有有效的投票都必须在同一轮次中,在开始新一轮投票时,会首先对logicalclock进行自增操作。 (2)初始化选票。在开始进行新一轮投票之前,每个服务器都会初始化自身的选票,并且在初始化阶段,每台服务器都会将自己推举为Leader。 (3)发送初始化选票。完成选票的初始化后,服务器就会发起第一次投票。Zookeeper会将刚刚初始化好的选票放入sendqueue中,由发送器WorkerSender负责发送出去。 (4)接收外部投票。每台服务器会不断地从recvqueue队列中获取外部选票。如果服务器发现无法获取到任何外部投票,那么就会立即确认自己是否和集群中其他服务器保持着有效的连接,如果没有连接,则马上建立连接,如果已经建立了连接,则再次发送自己当前的内部投票。 (5)判断选举轮次。在发送完初始化选票之后,接着开始处理外部投票。在处理外部投票时,会根据选举轮次来进行不同的处理。 ·外部投票的选举轮次大于内部投票。若服务器自身的选举轮次落后于该外部投票对应服务器的选举轮次,那么就会立即更新自己的选举轮次(logicalclock),并且清空所有已经收到的投票,然后使用初始化的投票来进行PK以确定是否变更内部投票。最终再将内部投票发送出去。 ·外部投票的选举轮次小于内部投票。若服务器接收的外选票的选举轮次落后于自身的选举轮次,那么Zookeeper就会直接忽略该外部投票,不做任何处理,并返回步骤4。 ·外部投票的选举轮次等于内部投票。此时可以开始进行选票PK。 (6)选票PK。在进行选票PK时,符合任意一个条件就需要变更投票。 · 若外部投票中推举的Leader服务器的选举轮次大于内部投票,那么需要变更投票。 · 若选举轮次一致,那么就对比两者的ZXID,若外部投票的ZXID大,那么需要变更投票。 · 若两者的ZXID一致,那么就对比两者的SID,若外部投票的SID大,那么就需要变更投票。 (7)变更投票。经过PK后,若确定了外部投票优于内部投票,那么就变更投票,即使用外部投票的选票信息来覆盖内部投票,变更完成后,再次将这个变更后的内部投票发送出去。 (8)选票归档。无论是否变更了投票,都会将刚刚收到的那份外部投票放入选票集合recvset中进行归档。recvset用于记录当前服务器在本轮次的Leader选举中收到的所有外部投票。 (9)统计投票。完成选票归档后,就可以开始统计投票,统计投票是为了统计集群中是否已经有过半的服务器认可了当前的内部投票,如果确定已经有过半服务器认可了该投票,则终止投票。否则返回步骤(4)。 (10)更新服务器状态。若已经确定可以终止投票,那么就开始更新服务器状态,服务器首选判断当前被过半服务器认可的投票所对应的Leader服务器是否是自己,若是自己,则将自己的服务器状态更新为LEADING,若不是,则根据具体情况来确定自己是FOLLOWING或是OBSERVING。
- 双亲委派机制及其使用原因?
(1)当某个特定的类加载器它在接到需要加载类的请求时,这个类会首先查看自己已加载完的类中是否包含这个类,如果有就返回,没有的话就会把加载的任务交给父类加载器加载,以此递归,父类加载器如果可以完成类加载任务,就返回它,当父类加载器无法完成这个加载任务时,才会不得已自己去加载。这种机制就叫做双亲委派机制。 (2)原因: 双亲委派机制能够保证多加载器加载某个类时,最终都是由一个加载器加载,确保最终加载结果相同。
- treemap和HashMap的区别?
(1)HashMap是通过hashcode()对其内容进行快速查找的;HashMap中的元素是没有顺序的;TreeMap中所有的元素都是有某一固定顺序的,如果需要得到一个有序的结果,就应该使用TreeMap; (2)HashMap继承AbstractMap类;覆盖了hashcode() 和equals() 方法,以确保两个相等的映射返回相同的哈希值;TreeMap继承SortedMap类;他保持键的有序顺序; (3)HashMap:基于hash表实现的;使用HashMap要求添加的键类明确定义了hashcode() 和equals();为了优化HashMap的空间使用,可以调优初始容量和负载因子;TreeMap:基于红黑树实现的;TreeMap就没有调优选项,因为红黑树总是处于平衡的状态;
- java内存泄露解决
堆的dump文件,通过jmx的mbean生产当前 的heap信息, 用eclipse自带的静态分析工具Mat(windDBG)打开 分析内存泄露:那些被怀疑为内存泄露,哪些占用空间大,对象调用关系
- 分布式锁的实现原理和有多少种实现方式?
目前主流的分布式锁的实现方式有三种: ①借助数据库来实现,新建一张锁表; 操作前向表添加一条锁记录(锁id建立唯一索引),成功添加者获得锁权限,处理完后删除锁记录来释放锁。 ②基于缓存实现,如memcache 和 redis; memcache的add操作具有原子性,可以保证同一个key add操作只有一个成功,来获取锁权限,利用缓存的失效时间来解决死锁问题。相对于第一种方案,这种方案性能更好,而且操作更方便。 ③通过zookeeper实现; 客户端会在zookeeper生成一个临时的目录节点,存储在一个序列中,每次节点序号最小的节点对应的客户端获得锁,处理完成后删除最小节点,而且可重复获取锁(通过判断序号是否和最小的节点相同)。这种方式可以实现阻塞分布式锁,和锁的重复获取问题。
- HashMap和Hashtable的区别
主要的区别有:线程安全性,同步,以及速度。 (1)HashMap几乎可以等价于Hashtable,除了HashMap是非synchronized的,并可以接受null(HashMap可以接受为null的键值(key)和值(value),而Hashtable则不行)。 HashMap是非synchronized,而Hashtable是synchronized,这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。 (2)另一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别。 由于Hashtable是线程安全的也是synchronized,所以在单线程环境下它比HashMap要慢。如果你不需要同步,只需要单一线程,那么使用HashMap性能要好过Hashtable。 HashMap不能保证随着时间的推移Map中的元素次序是不变的。
- HashMap中是否任何对象都可以做为key,用户自定义对象做为key有没有什么要求?
用自定义类作为key,必须重写equals()和hashCode()方法。 自定义类中的equals() 和 hashCode()都继承自Object类。 Object类的hashCode()方法返回这个对象存储的内存地址的编号。 而equals()比较的是内存地址是否相等。
- 对sql进行优化的原则有哪些?
(1)减少返回不必要的数据 (2)减少物理和逻辑读次数 (3)减少计算次数
- String和StringBuffer的区别
1)运行速度:StringBuilder >StringBuffer >String String是字符串常量,不可变,每次改变只是创建一个新的对象,然后GC回收掉老的那个,所以执行速度最慢,另外两个是字符串对象,可变。 (2)线程安全: StringBuilder是线程不安全的,StringBuffer是线程安全的,看是否带synchronized关键字。多线程则采用StringBuffer,单线程则要建议用速度较快的StringBuilder。 (3)String:适用于少量的字符串操作的情况,String是final类,无法被继承。StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况。 StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况。
- 如果A和B对象循环引用,是否可以被GC?
答:这个循环引用是否被回收,就看这个循环引用是否挂在根上,A引用B,B引用A,A和B并没有挂在某个内存元和根上,当他们的生命周期结束的时候。这两个对象都有可能被回收。
- Error、Exception和RuntimeException的区别,作用又是什么?
Error是Throwable 的子类,用于指示合理的应用程序不应该试图捕获的严重问题。大多数这样的错误都是异常条件。虽然 ThreadDeath 错误是一个“正规”的条件,但它也是 Error 的子类,因为大多数应用程序都不应该试图捕获它。在执行该方法期间,无需在其 throws 子句中声明可能抛出但是未能捕获的 Error的任何子类,因为这些错误可能是再也不会发生的异常条件。 Exception类及其子类是 Throwable 的一种形式,它指出了合理的应用程序想要捕获的条件。 RuntimeException是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类。可能在执行方法期间抛出但未被捕获的RuntimeException 的任何子类都无需在 throws 子句中进行声明。它是Exception的子类。
- reader和inputstream区别
(1)InputStream是表示字节输入流的所有类的超类;Reader是用于读取字符流的抽象类 (2)InputStream提供的是字节流的读取,而非文本读取,这是和Reader类的根本区别。 即用Reader读取出来的是char数组或者String ,使用InputStream读取出来的是byte数组。
- hashCode的作用;
hashCode方法的主要作用是为了配合基于散列的集合一起正常运行,这样的散列集合包括HashSet、HashMap以及HashTable。 Java中的hashCode方法就是根据一定的规则将与对象相关的信息(比如对象的存储地址,对象的字段等)映射成一个数值,这个数值称作为散列值。
- Java中的内存溢出是如何造成的?
(1)内存中加载的数据量过于庞大,如一次从数据库取出过多数据; (2)集合类中有对对象的引用,使用完后未清空,使得JVM不能回收; (3)代码中存在死循环或循环产生过多重复的对象实体; (4)使用的第三方软件中的BUG; (5)启动参数内存值设定的过小;
- springMVC的工作原理图
(1)客户端发出一个http请求给web服务器,web服务器对http请求进行解析,如果匹配 DispatcherServlet的请求映射路径(在web.xml中指定), web容器将请求转交给DispatcherServlet; (2)DipatcherServlet接收到这个请求之后将根据请求的信息(包括URL、Http方法、请求报文头和请 求参数Cookie等) 以及HandlerMapping的配置找到处理请求的处理器(Handler); (3)DispatcherServlet根据HandlerMapping找到对应的Handler,将处理权交给Handler(Handler将 具体的处理进行封装), 再由具体的HandlerAdapter对Handler进行具体的调用。 (4)Handler对数据处理完成以后将返回一个ModelAndView()对象给DispatcherServlet; (5)Handler返回的ModelAndView()只是一个逻辑视图并不是一个正式的视图,DispatcherSevlet通过 ViewResolver将逻辑视图转化为真正的视图View; (6)Dispatcher通过model解析出ModelAndView()中的参数进行解析最终展现出完整的view并返回给客户端;
- RPC框架和普通http有什么区别和优势?
基于Tcp封装还是http封装的? (1)1、RPC是一种API,HTTP是一种无状态的网络协议。RPC可以基于HTTP协议实现,也可以直接在TCP协议上实现。 (2)RPC主要是用在大型网站里面,因为大型网站里面系统繁多,业务线复杂,而且效率优势非常重要的一块,这个时候RPC的优势就比较明显了。 (3)HTTP主要是用在中小型企业里面,业务线没那么繁多的情况下。 (4)HTTP开发方便简单、直接。开发一个完善的RPC框架难度比较大。 (5)HTTP发明的初衷是为了传送超文本的资源,协议设计的比较复杂,参数传递的方式效率也不高。开源的RPC框架针对远程调用协议上的效率会比HTTP快很多。 (6)HTTP需要事先通知,修改Nginx/HAProxy配置。RPC能做到自动通知,不影响上游。 (7)HTTP大部分是通过Json来实现的,字节大小和序列化耗时都比Thrift要更消耗性能。RPC,可以基于Thrift实现高效的二进制传输。
- GC的基本原理?什么时候需要GC?为什么需要GC?
GC (Garbage Collection)的基本原理:将内存中不再被使用的对象进行回收,GC中用于回收的方法称为收集器,由于GC需要消耗一些资源和时间,Java在对对象的生命周期特征进行分析后,按照新生代、旧生代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停 (1)对新生代的对象的收集称为minor GC; (2)对旧生代的对象的收集称为Full GC; (3)程序中主动调用System.gc()强制执行的GC为Full GC。 不同的对象引用类型, GC会采用不同的方法进行回收,JVM对象的引用分为了四种类型: (1)强引用:默认情况下,对象采用的均为强引用(这个对象的实例没有其他对象引用,GC时才会被回收) (2)软引用:软引用是Java中提供的一种比较适合于缓存场景的应用(只有在内存不够用的情况下才会被GC) (3)弱引用:在GC时一定会被GC回收 (4)虚引用:由于虚引用只是用来得知对象是否被GC
- 怎样避免死锁?
(1)破坏“不可剥夺”条件:一个进程不能获得所需要的全部资源时便处于等待状态,等待期间他占有的资源将被隐式的释放重新加入到 系统的资源列表中,可以被其他的进程使用,而等待的进程只有重新获得自己原有的资源以及新申请的资源才可以重新启动,执行。 (2)破坏”请求与保持条件”:第一种方法静态分配即每个进程在开始执行时就申请他所需要的全部资源。第二种是动态分配即每个进程在申请所需要的资源时他本身不占用系统资源。 (3)破坏“循环等待”条件:采用资源有序分配其基本思想是将系统中的所有资源顺序编号,将紧缺的,稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的进程
- BEAN的生命周期
(1)应用启动的时候检查加载需要被Spring管理的bean. (2)根据实现的接口,依次设置beanName,BeanFactory,ApplicationContext应用上下文。 (3)根据实现的接口,依次调用加载前,设置值,自定义初始化方法,加载完成后。 (4)bean已经可以用了,存活直到上下文也被销毁。 (5)销毁的时候调用destroy方法和自定义的销毁方法。
欢迎搜索关注本人与朋友共同开发的微信面经小程序【大厂面试助手】和公众号【微瞰技术】