LeetCode刷题记录与面试整理
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
yuanguangxin 01b2dc5056
git init
5 years ago
.idea git init 5 years ago
src git init 5 years ago
.gitignore git init 5 years ago
LeetCode.iml git init 5 years ago
README.md git init 5 years ago

README.md

ZooKeeper

  1. CAP定理

一个分布式系统不可能同时满足以下三种,一致性C:Consistency,可用性A:Available,分区容错性P:Partition Tolerance. 在此ZooKeeper保证的是CPZooKeeper不能保证每次服务请求的可用性在极端环境下ZooKeeper可能会丢弃一些请求消费者程序需要重新请求才能获得结果。 另外在进行leader选举时集群都是不可用所以说ZooKeeper不能保证服务可用性。Base理论CA强一致性和最终一致性

  1. ZAB协议

ZAB协议包括两种基本的模式崩溃恢复和消息广播。当整个 Zookeeper 集群刚刚启动或者Leader服务器宕机、重启或者网络故障导致不存在过半的服务器与 Leader 服务器保持正常通信时,所有服务器进入崩溃恢复模式,首先选举产生新的 Leader 服务器,然后集群中 Follower 服务器开始与新的 Leader 服务器进行数据同步。 当集群中超过半数机器与该 Leader 服务器完成数据同步之后退出恢复模式进入消息广播模式Leader 服务器开始接收客户端的事务请求生成事物提案来进行事务请求处理。

  1. 选举算法和流程:

FastLeaderElection(默认提供的选举算法): 目前有5台服务器每台服务器均没有数据它们的编号分别是1,2,3,4,5,按编号依次启动,它们的选择举过程如下: (1)服务器1启动给自己投票然后发投票信息由于其它机器还没有启动所以它收不到反馈信息服务器1的状态一直属于Looking。 (2)服务器2启动给自己投票同时与之前启动的服务器1交换结果由于服务器2的编号大所以服务器2胜出但此时投票数没有大于半数所以两个服务器的状态依然是LOOKING。 (3)服务器3启动给自己投票同时与之前启动的服务器1,2交换信息由于服务器3的编号最大所以服务器3胜出此时投票数正好大于半数所以服务器3成为leader服务器1,2成为follower。 (4)服务器4启动给自己投票同时与之前启动的服务器1,2,3交换信息尽管服务器4的编号大但之前服务器3已经胜出所以服务器4只能成为follower。 (5)服务器5启动后面的逻辑同服务器4成为follower。

  1. 节点类型:

(1)持久 (2)持久顺序 (3)临时 (4)临时顺序

  1. 如何保证事务的顺序一致性

(1)Leader为每个请求生成zxid下发proposal给FollowerFollower会将请求写入到pendingTxns阻塞队列及txnLog中然后发送ack给Leader。 (2)ack过半Leader发送commit请求给所有FollowerFollower对比commit request的zxid和前面提到的pendingTxns的zxid不一致的话Follower退出重新跟Leader同步。 (3)Follower处理commit请求如果不是本Follower提交的写请求直接调用FinalRequestProcessor做持久化触发watches如果是本Follower提交则做一些特殊处理主要针对客户端连接断开的场景然后调用FinalRequestProcessor等后续处理流程。 (4)FinalRequestProcessor做持久化返回客户端。

Redis

  1. 应用场景

(1)缓存 (2)共享Session (3)消息队列系统 (4)分布式锁

  1. 单线程的Redis为什么快

(1)纯内存操作 (2)单线程操作,避免了频繁的上下文切换 (3)合理高效的数据结构 (4)采用了非阻塞I/O多路复用机制

  1. Redis 的数据结构及使用场景

(1)String字符串:字符串类型是 Redis 最基础的数据结构,首先键都是字符串类型,而且 其他几种数据结构都是在字符串类型基础上构建的,我们常使用的 set key value 命令就是字符串。常用在缓存、计数、共享Session、限速等。 (2)Hash哈希:在Redis中哈希类型是指键值本身又是一个键值对 结构形如value={{field1value1}...{fieldNvalueN}}添加命令hset key field value。哈希可以用来存放用户信息比如实现购物车 (3)List列表双向链表:列表list类型是用来存储多个有序的字符串。可以做简单的消息队列的功能。 (4)Set集合集合set类型也是用来保存多个的字符串元素但和列表类型不一 样的是,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过 索引下标获取元素。利用 Set 的交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。 (5)Sorted Set有序集合跳表实现Sorted Set 多了一个权重参数 Score集合中的元素能够按 Score 进行排列。可以做排行榜应用,取 TOP N 操作。

  1. Redis 的数据过期策略

Redis 中数据过期策略采用定期删除+惰性删除策略 (1)定期删除策略Redis 启用一个定时器定时监视所有的 key判断key是否过期过期的话就删除。这种策略可以保证过期的 key 最终都会被删除,但是也存在严重的缺点:每次都遍历内存中所有的数据,非常消耗 CPU 资源,并且当 key 已过期,但是定时器还处于未唤起状态,这段时间内 key 仍然可以用。 (2)惰性删除策略:在获取 key 时,先判断 key 是否过期,如果过期则删除。这种方式存在一个缺点:如果这个 key 一直未被使用,那么它一直在内存中,其实它已经过期了,会浪费大量的空间。 这两种策略天然的互补,结合起来之后,定时删除策略就发生了一些改变,不在是每次扫描全部的 key 了,而是随机抽取一部分 key 进行检查,这样就降低了对 CPU 资源的损耗惰性删除策略互补了为检查到的key基本上满足了所有要求。 但是有时候就是那么的巧,既没有被定时器抽取到,又没有被使用,这些数据又如何从内存中消失?没关系,还有内存淘汰机制,当内存不够用时,内存淘汰机制就会上场。淘汰策略分为: (1)当内存不足以容纳新写入数据时新写入操作会报错。Redis 默认策略) (2)当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 Key。LRU推荐使用 (3)当内存不足以容纳新写入数据时,在键空间中,随机移除某个 Key。 (4)当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 Key。这种情况一般是把 Redis 既当缓存,又做持久化存储的时候才用。 (5)当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 Key。 (6)当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 Key 优先移除。

Redis的LRU具体实现 用栈的形式会导致执行select *的时候大量非热点数据占领头部数据,所以需要改进。 Redis每次按key获取一个值的时候都会更新value中的lru字段为当前秒级别的时间戳。Redis初始的实现算法很简单随机从dict中取出五个key,淘汰一个lru字段值最小的。 在3.0的时候又改进了一版算法首先第一次随机选取的key都会放入一个pool中(pool的大小为16),pool中的key是按lru大小顺序排列的。接下来每次随机选取的keylru值必须小于pool中最小的lru才会继续放入直到将pool放满。放满之后每次如果有新的key需要放入需要将pool中lru最大的一个key取出。淘汰的时候直接从pool中选取一个lru最小的值然后将其淘汰。

  1. 如何解决 Redis 缓存雪崩问题

(1)使用 Redis 高可用架构:使用 Redis 集群来保证 Redis 服务不会挂掉 (2)缓存时间不一致,给缓存的失效时间,加上一个随机值,避免集体失效 (3)限流降级策略:有一定的备案,比如个性推荐服务不可用了,换成热点数据推荐服务

  1. 如何解决 Redis 缓存穿透问题

(1)在接口做校验 (2)存null值缓存击穿加锁 (3)布隆过滤器拦截: 将所有可能的查询key 先映射到布隆过滤器中查询时先判断key是否存在布隆过滤器中存在才继续向下执行如果不存在则直接返回。 布隆过滤器将值进行多次哈希bit存储布隆过滤器说某个元素在可能会被误判。布隆过滤器说某个元素不在那么一定不在。

  1. redis的持久化机制

redis为了保证效率数据缓存在了内存中但是会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件中以保证数据的持久化。Redis的持久化策略有两种 (1)RDB快照形式是直接把内存中的数据保存到一个dump的文件中定时保存保存策略。 当Redis需要做持久化时Redis会fork一个子进程子进程将数据写到磁盘上一个临时RDB文件中。当子进程完成写临时文件后将原来的RDB替换掉。 (2)AOF把所有的对Redis的服务器进行修改的命令都存到一个文件里命令的集合。 使用AOF做持久化每一个写命令都通过write函数追加到appendonly.aof中。aof的默认策略是每秒钟fsync一次在这种配置下就算发生故障停机也最多丢失一秒钟的数据。 缺点是对于相同的数据集来说AOF的文件体积通常要大于RDB文件的体积。根据所使用的fsync策略AOF的速度可能会慢于RDB。 Redis默认是快照RDB的持久化方式。

  1. redis主从复制主从同步

(1)从节点执行slaveofmasterIP保存主节点信息 (2)从节点中的定时任务发现主节点信息建立和主节点的socket连接 (3)从节点发送Ping信号主节点返回Pong两边能互相通信 (4)连接建立后,主节点将所有数据发送给从节点(数据同步) (5)主节点把当前的数据同步给从节点后,便完成了复制的建立过程。接下来,主节点就会持续的把写命令发送给从节点,保证主从数据一致性。

主从刚刚连接的时候进行全量同步RDB全同步结束后进行增量同步(AOF)。

  1. Redis和memcached的区别

(1)存储方式上memcache会把数据全部存在内存之中断电后会挂掉数据不能超过内存大小。redis有部分数据存在硬盘上这样能保证数据的持久性。 (2)数据支持类型上memcache对数据类型的支持简单只支持简单的key-value而redis支持五种数据类型。 (3)用底层模型不同它们之间底层实现方式以及与客户端之间通信的应用协议不一样。redis直接自己构建了VM机制因为一般的系统调用系统函数的话会浪费一定的时间去移动和请求。 (4)value的大小redis可以达到1GB而memcache只有1MB。

  1. redis并发竞争key的解决方案

(1)分布式锁+时间戳 (2)利用消息队列

  1. Redis与Mysql双写一致性方案

先更新数据库,再删缓存。数据库的读操作的速度远快于写操作的,所以脏数据很难出现。可以对异步延时删除策略,保证读请求完成以后,再进行删除操作。

Mysql

  1. 事务的基本要素

(1)原子性:事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行 (2)一致性:事务开始前和结束后,数据库的完整性约束没有被破坏。 (3)隔离性:同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。 (4)持久性:事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。

  1. 事务的并发问题

(1)脏读事务A读取了事务B更新的数据然后B回滚操作那么A读取到的数据是脏数据 (2)不可重复读事务A多次读取同一数据事务B在事务A多次读取的过程中对数据作了更新并提交导致事务A多次读取同一数据时结果不一致。 (3)幻读A事务读取了B事务已经提交的新增数据。注意和不可重复读的区别这里是新增不可重复读是更改或删除

  1. MySQL事务隔离级别

事务隔离级别 | 脏读 | 不可重复读 | 幻读 读未提交 | 是 | 是 |是 不可重复读 | 否 | 是 |是 可重复读 | 否 | 否 |是 串行化 | 否 | 否 |否

在MySQL可重复读的隔离级别中并不是完全解决了幻读的问题而是解决了读数据情况下的幻读问题。而对于修改的操作依旧存在幻读问题就是说MVCC对于幻读的解决时不彻底的。 通过索引加锁,间隙锁可以解决幻读的问题。

  1. Mysql的逻辑结构

最上层的服务类似其他CS结构比如连接处理授权处理。第二层是Mysql的服务层包括SQL的解析分析优化存储过程触发器视图等也在这一层实现。 最后一层是存储引擎的实现类似于Java接口的实现Mysql的执行器在执行SQL的时候只会关注API的调用完全屏蔽了不同引擎实现间的差异。 比如Select语句先会判断当前用户是否拥有权限其次到缓存内存查询是否有相应的结果集如果没有再执行解析sql优化生成执行计划调用API执行。

  1. MVCC,redolog,undolog

undoLog 也就是我们常说的回滚日志文件 主要用于事务中执行失败进行回滚以及MVCC中对于数据历史版本的查看。 由引擎层的InnoDB引擎实现,是逻辑日志,记录数据修改被修改前的值,比如"把id='B' 修改为id = 'B2' 那么undo日志就会用来存放id ='B'的记录” 当一条数据需要更新前,会先把修改前的记录存储在undolog中,如果这个修改出现异常,,则会使用undo日志来实现回滚操作,保证事务的一致性。 当事务提交之后undo log并不能立马被删除,而是会被放到待清理链表中,待判断没有事物用到该版本的信息时才可以清理相应undolog。 它保存了事务发生之前的数据的一个版本用于回滚同时可以提供多版本并发控制下的读MVCC也即非锁定读。

redoLog 是重做日志文件是记录数据修改之后的值,用于持久化到磁盘中。 redo log包括两部分一是内存中的日志缓冲(redo log buffer),该部分日志是易失性的;二是磁盘上的重做日志文件(redo log file),该部分日志是持久的。 由引擎层的InnoDB引擎实现,是物理日志,记录的是物理数据页修改的信息,比如“某个数据页上内容发生了哪些改动” 当一条数据需要更新时,InnoDB会先将数据更新然后记录redoLog 在内存中然后找个时间将redoLog的操作执行到磁盘上的文件上。不管是否提交成功我都记录你要是回滚了那我连回滚的修改也记录。它确保了事务的持久性。

MVCC多版本并发控制是MySQL中基于乐观锁理论实现隔离级别的方式用于读已提交和可重复读取隔离级别的实现。 在MySQL中会在表中每一条数据后面添加两个字段最近修改该行数据的事务ID指向该行undolog表中回滚段的指针。 Read View判断行的可见性创建一个新事务时copy一份当前系统中的活跃事务列表。意思是当前不应该被本事务看到的其他事务id列表。

  1. binlog

由Mysql的Server层实现,是逻辑日志,记录的是sql语句的原始逻辑比如"把id='B' 修改为id = B2。binlog会写入指定大小的物理文件中,是追加写入的,当前文件写满则会创建新的文件写入。 产生:事务提交的时候,一次性将事务中的sql语句,按照一定的格式记录到binlog中。 用于复制和恢复在主从复制中从库利用主库上的binlog进行重播(执行日志中记录的修改逻辑),实现主从同步。业务数据不一致或者错了用binlog恢复。

  1. InnoDB的行锁模式

(1)共享锁(S)用法lock in share mode又称读锁允许一个事务去读一行阻止其他事务获得相同数据集的排他锁。若事务T对数据对象A加上S锁则事务T可以读A但不能修改A其他事务只能再对A加S锁而不能加X锁直到T释放A上的S锁。这保证了其他事务可以读A但在T释放A上的S锁之前不能对A做任何修改。 (2)排他锁(X)用法for update又称写锁允许获取排他锁的事务更新数据阻止其他事务取得相同的数据集共享读锁和排他写锁。若事务T对数据对象A加上X锁事务T可以读A也可以修改A其他事务不能再对A加任何锁直到T释放A上的锁。 在没有索引的情况下InnoDB只能使用表锁。

7.为什么选择B+树作为索引结构

(1)Hash索引Hash索引底层是哈希表哈希表是一种以key-value存储数据的结构所以多个数据在存储关系上是完全没有任何顺序关系的所以对于区间查询是无法直接通过索引查询的就需要全表扫描。所以哈希索引只适用于等值查询的场景。 而B+ 树是一种多路平衡查询树,所以他的节点是天然有序的(左子节点小于父节点、父节点小于右子节点),所以对于范围查询的时候不需要做全表扫描 (2)二叉查找树:解决了排序的基本问题,但是由于无法保证平衡,可能退化为链表。 (3)平衡二叉树:通过旋转解决了平衡的问题,但是 旋转操作 效率太低。 (4)红黑树:通过舍弃严格的平衡和引入红黑节点,解决了 AVL旋转效率过低的问题但是在磁盘等场景下树仍然太高IO次数太多。 (5)B+树在B树的基础上将非叶节点改造为不存储数据 纯索引节点,进一步降低了树的高度;此外将叶节点使用指针连接成链表,范围查询更加高效。

  1. B+树的叶子节点都可以存哪些东西

可能存储的是整行数据也有可能是主键的值。B+树的叶子节点存储了整行数据的是主键索引也被称之为聚簇索引。而索引B+ Tree的叶子节点存储了主键的值的是非主键索引也被称之为非聚簇索引

  1. 覆盖索引

指一个查询语句的执行只用从索引中就能够取得,不必从数据表中读取。也可以称之为实现了索引覆盖。

  1. 查询在什么时候不走(预期中的)索引?

(1)模糊查询 %like (2)索引列参与计算,使用了函数 (3)非最左前缀顺序 (4)where对null判断 (5)where不等于 (6)or操作有至少一个字段没有索引

  1. Mysql排序原理

JVM

  1. 运行时数据区域

(1)程序计数器: 程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选 取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。是线程私有”的内存。

(2)Java虚拟机栈

与程序计数器一样Java虚拟机栈Java Virtual Machine Stacks也是线程私有的它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型每个方法在执行的同时 都会创建一个栈帧 ,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

(3)本地方法栈

本地方法栈Native Method Stack与虚拟机栈所发挥的作用是非常相似的它们之间的区别不过是虚拟机栈为虚拟机执行Java方法也就是字节码服务而本地方法栈则为虚 拟机使用到的Native方法服务。

(4)Java堆

对于大多数应用来说Java堆是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例几乎所有的对象实例都在这里分配内存。 从内存回收的角度来看由于现在收集器基本都采用分代收集算法所以Java堆中还可以细分为新生代和老年代再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。

2.分代回收

HotSpot JVM把年轻代分为了三部分1个Eden区和2个Survivor区分别叫from和to。一般情况下新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后如果仍然存活将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC年龄就会增加1岁当它的年龄增加到一定程度时就会被移动到年老代中。 因为年轻代中的对象基本都是朝生夕死的,所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。 在GC开始的时候对象只会存在于Eden区和名为“From”的Survivor区Survivor区“To”是空的。紧接着进行GCEden区中所有存活的对象都会被复制到“To”而在“From”区中仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中没有达到阈值的对象会被复制到“To”区域。 经过这次GC后Eden区和From区已经被清空。这个时候“From”和“To”会交换他们的角色也就是新的“To”就是上次GC前的“From”新的“From”就是上次GC前的“To”。不管怎样都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程直到“To”区被填满“To”区被填满之后会将所有对象移动到年老代中。

3.常见的垃圾回收机制

(1)引用计数法: 引用计数法是一种简单但速度很慢的垃圾回收技术。每个对象都含有一个引用计数器,当有引用连接至对象时,引用计数加1。当引用离开作用域或被置为null时,引用计数减1。虽然管理引用计数的开销不大,但这项开销在整个程序生命周期中将持续 发生。垃圾回收器会在含有全部对象的列表上遍历,当发现某个对象引用计数为0时,就释放其占用的空间。

(2)可达性分析算法: 这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点从这些节点开始向下搜索搜索所走过的路径称为引用链当一个对象到GC Roots没有任何引用链相连用图论的话来说就是从GC Roots到这个对象不可达则证明此对象是不可用的。 在Java语言中可作为GC Roots的对象包括下面几种 虚拟机栈(栈帧中的本地变量表)中引用的对象。 方法区中类静态属性引用的对象。 方法区中常量引用的对象。 本地方法栈中JNI即一般说的Native方法引用的对象。

  1. 垃圾回收算法

(1)停止-复制:先暂停程序的运行,然后将所有存活的对象从当前堆复制到另一个堆,没有被复制的对象全部都是垃圾。当对象被复制到新堆时,它们是一个挨着一个的,所以新堆保持紧凑排列,然后就可以按前述方法简单,直接的分配了。缺点是一浪费空间,两个堆之间要来回倒腾, 二是当程序进入稳定态时,可能只会产生极少的垃圾,甚至不产生垃圾,尽管如此,复制式回收器仍会将所有内存自一处复制到另一处。

(2)标记-清除:同样是从堆栈和静态存储区出发,遍历所有的引用,进而找出所有存活的对象。每当它找到一个存活的对象,就会给对象一个标记,这个过程中不会回收任何对象。只有全部标记工作完成的时候,清理动作才会开始。在清理过程中,没有标记的对象会被释放,不会发生任何复制动作。 所以剩下的堆空间是不连续的,垃圾回收器如果要希望得到连续空间的话,就得重新整理剩下的对象。

(3)标记-整理:它的第一个阶段与标记/清除算法是一模一样的均是遍历GC Roots然后将存活的对象标记。移动所有存活的对象且按照内存地址次序依次排列然后将末端内存地址以后的内存全部回收。因此第二阶段才称为整理阶段。

(4)分代收集算法把Java堆分为新生代和老年代然后根据各个年代的特点采用最合适的收集算法。新生代中对象的存活率比较低所以选用复制算法老年代中对象存活率高且没有额外空间对它进行分配担保所以使用“标记-清除”或“标记-整理”算法进行回收。

  1. Minor GC和Full GC

Minor GC触发条件当Eden区满时触发Minor GC。 Full GC触发条件 (1)调用System.gc时系统建议执行Full GC但是不必然执行 (2)老年代空间不足 (3)方法区空间不足 (4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存 (5)由Eden区、From Space区向To Space区复制时对象大小大于To Space可用内存则把该对象转存到老年代且老年代的可用内存小于该对象大小

  1. JVM类加载过程

类从被加载到虚拟机内存中开始到卸载出内存为止它的整个生命周期包括加载、验证、准备、解析、初始化、使用和卸载7个阶段。 加载: (1)通过一个类的全限定名来获取定义此类的二进制字节流 (2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构 (3)在内存中生成一个代表这个类的Class对象作为方法去这个类的各种数据的访问入口 验证: 验证是连接阶段的第一步这一阶段的目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求并且不会危害虚拟自身的安全。 准备: 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段这些变量所使用的内存都将在方法去中进行分配。这时候进行内存分配的仅包括类变量static而不包括实例变量实例变量将会在对象实例化时随着对象一起分配在Java堆中。 解析: 解析阶段是虚拟机将常量池内的符号Class文件内的符号引用替换为直接引用指针的过程。 初始化: 初始化阶段是类加载过程的最后一步开始执行类中定义的Java程序代码字节码

  1. 双亲委派模型

双亲委派的意思是如果一个类加载器需要加载类,那么首先它会把这个类请求委派给父类加载器去完成,每一层都是如此。一直递归到顶层,当父加载器无法完成这个请求时,子类才会尝试去加载。

  1. 什么情况下需要开始类加载过程的第一个阶段加载

(1)遇到new、getstatic、putstatic或invokestatic这4条字节码指令时如果类没有进行过初始化则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是使用new关键字实例化对象的时候、读取或设置一个类的静态字段被final修饰、已在编译期把结果放入常量池的静态字段除外的时候以及调用一个类的静态方法的时候。 (2)使用java.lang.reflect包的方法对类进行反射调用的时候如果类没有进行过初始化则需要先触发其初始化。 (3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。 (4)当虚拟机启动时用户需要指定一个要执行的主类包含main方法的那个类虚拟机会先初始化这个主类。

  1. CAS操作

CAS是英文单词CompareAndSwap的缩写中文意思是比较并替换。CAS需要有3个操作数内存地址V旧的预期值A即将要更新的目标值B。 CAS指令执行时当且仅当内存地址V的值与预期值A相等时将内存地址V的值修改为B否则就什么都不做。整个比较并替换的操作是一个原子操作。 如 Intel 处理器,比较并交换通过指令的 cmpxchg 系列实现。

CAS操作ABA问题如果在这段期间它的值曾经被改成了B后来又被改回为A那CAS操作就会误认为它从来没有被改变过。 Java并发包为了解决这个问题提供了一个带有标记的原子引用类“AtomicStampedReference”它可以通过控制变量值的版本来保证CAS的正确性。

  1. synchronized非公平可重入,ReentrantLock与AQS

  2. AtomicInteger

  3. i++操作的字节码指令

(1)将int类型常量加载到操作数栈顶 (2)将int类型数值从操作数栈顶取出并存储到到局部变量表的第1个Slot中 (3)将int类型变量从局部变量表的第1个Slot中取出并放到操作数栈顶 (4)将局部变量表的第1个Slot中的int类型变量加1 (5)表示将int类型数值从操作数栈顶取出并存储到到局部变量表的第1个Slot中即i中

Java基础

1.集合类

Map接口和Collection接口是所有集合框架的父接口 (1)Collection接口的子接口包括Set接口和List接口 (2)Map接口的实现类主要有HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等 (3)Set接口的实现类主要有HashSet、TreeSet、LinkedHashSet等 (4)List接口的实现类主要有ArrayList、LinkedList、Stack以及Vector等

2.HashMap如果我想要让自己的Object作为K应该怎么办

(1)重写hashCode()是因为需要计算存储数据的存储位置需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能这样虽然能更快但可能会导致更多的Hash碰撞 (2)重写equals()方法需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值xx.equals(null)必须返回false的这几个特性目的是为了保证key在哈希表中的唯一性

3.HashMap和ConcurrentHashMap

由于HashMap是线程不同步的虽然处理数据的效率高但是在多线程的情况下存在着安全问题因此设计了CurrentHashMap来解决多线程安全问题。 HashMap在put的时候插入的元素超过了容量由负载因子决定的范围就会触发扩容操作就是rehash这个会重新将原数组的内容重新hash到新的扩容数组中在多线程的环境下存在同时其他的元素也在进行put操作如果hash值相同可能出现同时在同一数组下用链表表示造成闭环导致在get时会出现死循环所以HashMap是线程不安全的。

在JDK1.7版本中ConcurrentHashMap维护了一个Segment数组Segment这个类继承了重入锁ReentrantLock并且该类里面维护了一个 HashEntry<K,V>[] table数组在写操作putremove扩容的时候会对Segment加锁所以仅仅影响这个Segment不同的Segment还是可以并发的所以解决了线程的安全问题同时又采用了分段锁也提升了并发的效率。 在JDK1.8版本中ConcurrentHashMap摒弃了Segment的概念而是直接用Node数组+链表+红黑树的数据结构来实现并发控制使用Synchronized和CAS来操作整个看起来就像是优化过且线程安全的HashMap。

  1. volatile

volatile在多处理器开发中保证了共享变量的“ 可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。(共享内存,私有内存)

  1. 死锁的4个必要条件

(1)互斥条件:一个资源每次只能被一个线程使用; (2)请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放; (3)不剥夺条件:进程已经获得的资源,在未使用完之前,不能强行剥夺; (4)循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
指定获取锁的顺序可避免死锁

  1. 如何指定多个线程的执行顺序

(1)设定一个 orderNum每个线程执行结束之后更新 orderNum指明下一个要执行的线程。并且唤醒所有的等待线程。 (2)在每一个线程的开始,要 while 判断 orderNum 是否等于自己的要求值!!不是,则 wait是则执行本线程。

  1. 为什么要使用线程池

(1)减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。 (2)可以根据系统的承受能力,调整线程池中工作线程的数目,放置因为消耗过多的内存,而把服务器累趴下

  1. 核心线程池ThreadPoolExecutor内部实现

(1)corePoolSize指定了线程池中的线程数量 (2)maximumPoolSize指定了线程池中的最大线程数量 (3)keepAliveTime线程池维护线程所允许的空闲时间 (4)unit: keepAliveTime 的单位。 (5)workQueue任务队列被提交但尚未被执行的任务。 (6)threadFactory线程工厂用于创建线程一般用默认的即可。 (7)handler拒绝策略。当任务太多来不及处理如何拒绝任务。

  1. HashSet和HashMap

HashSet的value存的是一个static finial PRESENT = newObject()。而HashSet的remove是使用HashMap实现,则是map.remove而map的移除会返回value,如果底层value都是存null,显然将无法分辨是否移除成功。

  1. Boolean占几个字节

未精确定义字节。Java语言表达式所操作的boolean值在编译之后都使用Java虚拟机中的int数据类型来代替而boolean数组将会被编码成Java虚拟机的byte数组每个元素boolean元素占8位。

  1. Java 虚拟机是如何判定两个 Java 类是相同的(同名类怎么加载)

判定两个 Java 类是相同的Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。

  1. GC中Stop the worldSTW

在执行垃圾收集算法时Java应用程序的其他所有除了垃圾收集收集器线程之外的线程都被挂起。此时系统只能允许GC线程进行运行其他线程则会全部暂停等待GC线程执行完毕后才能再次运行。这些工作都是由虚拟机在后台自动发起和自动完成的是在用户不可见的情况下把用户正常工作的线程全部停下来这对于很多的应用程序尤其是那些对于实时性要求很高的程序来说是难以接受的。 但不是说GC必须STW,你也可以选择降低运行速度但是可以并发执行的收集算法,这取决于你的业务。

消息队列

  1. 为什么需要消息队列

解耦,异步处理,削峰/限流

  1. Kafka 如何保证可靠性

如果我们要往 Kafka 对应的主题发送消息,我们需要通过 Producer 完成。前面我们讲过 Kafka 主题对应了多个分区,每个分区下面又对应了多个副本;为了让用户设置数据可靠性, Kafka 在 Producer 里面提供了消息确认机制。也就是说我们可以通过配置来决定消息发送到对应分区的几个副本才算消息发送成功。可以在定义 Producer 时通过 acks 参数指定(在 0.8.2.X 版本之前是通过 request.required.acks 参数设置的,详见 KAFKA-3043。这个参数支持以下三种值 acks = 0意味着如果生产者能够通过网络把消息发送出去那么就认为消息已成功写入 Kafka 。在这种情况下还是有可能发生错误,比如发送的对象无能被序列化或者网卡发生故障,但如果是分区离线或整个集群长时间不可用,那就不会收到任何错误。在 acks=0 模式下的运行速度是非常快的(这就是为什么很多基准测试都是基于这个模式),你可以得到惊人的吞吐量和带宽利用率,不过如果选择了这种模式, 一定会丢失一些消息。 acks = 1意味若 Leader 在收到消息并把它写入到分区数据文件(不一定同步到磁盘上)时会返回确认或错误响应。在这个模式下,如果发生正常的 Leader 选举,生产者会在选举时收到一个 LeaderNotAvailableException 异常,如果生产者能恰当地处理这个错误,它会重试发送悄息,最终消息会安全到达新的 Leader 那里。不过在这个模式下仍然有可能丢失数据,比如消息已经成功写入 Leader但在消息被复制到 follower 副本之前 Leader发生崩溃。 acks = all这个和 request.required.acks = -1 含义一样):意味着 Leader 在返回确认或错误响应之前,会等待所有同步副本都收到悄息。如果和 min.insync.replicas 参数结合起来,就可以决定在返回确认前至少有多少个副本能够收到悄息,生产者会一直重试直到消息被成功提交。不过这也是最慢的做法,因为生产者在继续发送其他消息之前需要等待所有副本都收到当前的消息。

  1. Kafka消息是采用Pull模式还是Push模式

Kafka最初考虑的问题是customer应该从brokes拉取消息还是brokers将消息推送到consumer也就是pull还push。在这方面Kafka遵循了一种大部分消息系统共同的传统的设计producer将消息推送到brokerconsumer从broker拉取消息。 push模式下当broker推送的速率远大于consumer消费的速率时consumer恐怕就要崩溃了。最终Kafka还是选取了传统的pull模式。Pull模式的另外一个好处是consumer可以自主决定是否批量的从broker拉取数据。 Pull有个缺点是如果broker没有可供消费的消息将导致consumer不断在循环中轮询直到新消息到t达。为了避免这点Kafka有个参数可以让consumer阻塞知道新消息到达。

  1. Kafka是如何实现高吞吐率的

(1)顺序读写kafka的消息是不断追加到文件中的这个特性使kafka可以充分利用磁盘的顺序读写性能 (2)零拷贝:跳过“用户缓冲区”的拷贝,建立一个磁盘空间和内存的直接映射,数据不再复制到“用户态缓冲区” (3)文件分段kafka的队列topic被分为了多个区partition每个partition又分为多个段segment所以一个队列中的消息实际上是保存在N多个片段文件中 (4)批量发送Kafka允许进行批量发送消息先将消息缓存在内存中然后一次请求批量发送出去 (5)数据压缩Kafka还支持对消息集合进行压缩Producer可以通过GZIP或Snappy格式对消息集合进行压缩

5.MQ与RPC的选择

实时返回,返回值,消费策略(速度)

Spring

  1. Bean的生命周期

(1)实例化一个Bean也就是我们通常说的new (2)按照Spring上下文对实例化的Bean进行配置也就是IOC注入 (3)如果这个Bean实现了BeanNameAware接口会调用它实现的setBeanName(String beanId)方法此处传递的是Spring配置文件中Bean的ID (4)如果这个Bean实现了BeanFactoryAware接口会调用它实现的setBeanFactory()传递的是Spring工厂本身 (5)如果这个Bean实现了ApplicationContextAware接口会调用setApplicationContext(ApplicationContext)方法传入Spring上下文该方式同样可以实现步骤4但比4更好以为ApplicationContext是BeanFactory的子接口有更多的实现方法 (6)如果这个Bean关联了BeanPostProcessor接口将会调用postProcessBeforeInitialization(Object obj, String s)方法BeanPostProcessor经常被用作是Bean内容的更改并且由于这个是在Bean初始化结束时调用After方法也可用于内存或缓存技术 (7)如果这个Bean在Spring配置文件中配置了init-method属性会自动调用其配置的初始化方法 (8)如果这个Bean关联了BeanPostProcessor接口将会调用postAfterInitialization(Object obj, String s)方法 (9)当Bean不再需要时会经过清理阶段如果Bean实现了DisposableBean接口会调用其实现的destroy方法 (10)最后如果这个Bean的Spring配置中配置了destroy-method属性会自动调用其配置的销毁方法

2.Final类的动态代理

cglib是基于继承的方式实现类的动态代理因此无法实现对final方法的代理。JDK是通过反射

计算机网路

1.get和post区别

(1)Get是不安全的因为在传输过程数据被放在请求的URL中Post的所有操作对用户来说都是不可见的。 (2)Get传送的数据量较小这主要是因为受URL长度限制Post传送的数据量较大一般被默认为不受限制。 (3)Get限制Form表单的数据集的值必须为ASCII字符而Post支持整个ISO10646字符集。 (4)Get执行效率却比Post方法好。Get是form提交的默认方法。 (5)GET产生一个TCP数据包POST产生两个TCP数据包。非必然客户端可灵活决定

2.Http请求的完全过程

(1)浏览器根据域名解析IP地址DNS,并查DNS缓存 (2)浏览器与WEB服务器建立一个TCP连接 (3)浏览器给WEB服务器发送一个HTTP请求GET/POST一个HTTP请求报文由请求行request line、请求头部headers、空行blank line和请求数据request body4个部分组成。 (4)服务端响应HTTP响应报文报文由状态行status line、相应头部headers、空行blank line和响应数据response body4个部分组成。 (5)浏览器解析渲染

3.tcp和udp区别

(1)TCP面向连接;UDP是无连接的即发送数据之前不需要建立连接 (2)TCP提供可靠的服务。也就是说通过TCP连接传送的数据无差错不丢失不重复且按序到达;UDP尽最大努力交付即不保 证可靠交付 (3)TCP面向字节流实际上是TCP把数据看成一连串无结构的字节流UDP是面向报文的UDP没有拥塞控制因此网络出现拥塞不会使源主机的发送速率降低对实时应用很有用如IP电话实时视频会议等 (4)每一条TCP连接只能是点到点的;UDP支持一对一一对多多对一和多对多的交互通信 (5)TCP首部开销20字节;UDP的首部开销小只有8个字节 (6)TCP的逻辑通信信道是全双工的可靠信道UDP则是不可靠信道 TCP的优点 可靠,稳定 TCP的可靠体现在TCP在传递数据之前会有三次握手来建立连接而且在数据传递时有确认、窗口、重传、拥塞控制机制在数据传完后还会断开连接用来节约系统资源。 TCP的缺点 慢,效率低,占用系统资源高,易被攻击 TCP在传递数据之前要先建连接这会消耗时间而且在数据传递时确认机制、重传机制、拥塞控制机制等都会消耗大量的时间而且要在每台设备上维护所有的传输连接事实上每个连接都会占用系统的CPU、内存等硬件资源。 而且因为TCP有确认机制、三次握手机制这些也导致TCP容易被人利用实现DOS、DDOS、CC等攻击。 UDP的优点比TCP稍安全 UDP没有TCP的握手、确认、窗口、重传、拥塞控制等机制UDP是一个无状态的传输协议所以它在传递数据时非常快。没有TCP的这些机制UDP较TCP被攻击者利用的漏洞就要少一些。但UDP也是无法避免攻击的比如UDP Flood攻击…… UDP的缺点 不可靠,不稳定 因为UDP没有TCP那些可靠的机制在数据传递时如果网络质量不好就会很容易丢包。 基于上面的优缺点,那么: 什么时候应该使用TCP 当对网络通讯质量有要求的时候比如整个数据要准确无误的传递给对方这往往用于一些要求可靠的应用比如HTTP、HTTPS、FTP等传输文件的协议POP、SMTP等邮件传输的协议。 在日常生活中常见使用TCP协议的应用如下 浏览器用的HTTP FlashFXP用的FTP Outlook用的POP、SMTP Putty用的Telnet、SSH QQ文件传输 ………… 什么时候应该使用UDP 当对网络通讯质量要求不高的时候要求网络通讯速度能尽量的快这时就可以使用UDP。 比如日常生活中常见使用UDP协议的应用如下 QQ语音 QQ视频 TFTP ……

  1. 三次握手

第一次握手建立连接时客户端发送syn包syn=x到服务器并进入SYN_SENT状态等待服务器确认SYN同步序列编号Synchronize Sequence Numbers。 第二次握手服务器收到syn包必须确认客户的SYNack=x+1同时自己也发送一个SYN包syn=y即SYN+ACK包此时服务器进入SYN_RECV状态 第三次握手客户端收到服务器的SYN+ACK包向服务器发送确认包ACK(ack=y+1此包发送完毕客户端和服务器进入ESTABLISHEDTCP连接成功状态完成三次握手。

  1. 四次挥手

(1)客户端进程发出连接释放报文并且停止发送数据。释放数据报文首部FIN=1其序列号为seq=u等于前面已经传送过来的数据的最后一个字节的序号加1此时客户端进入FIN-WAIT-1终止等待1状态。 TCP规定FIN报文段即使不携带数据也要消耗一个序号。 (2)服务器收到连接释放报文发出确认报文ACK=1ack=u+1并且带上自己的序列号seq=v此时服务端就进入了CLOSE-WAIT关闭等待状态。TCP服务器通知高层的应用进程客户端向服务器的方向就释放了这时候处于半关闭状态即客户端已经没有数据要发送了但是服务器若发送数据客户端依然要接受。这个状态还要持续一段时间也就是整个CLOSE-WAIT状态持续的时间。 (3)客户端收到服务器的确认请求后此时客户端就进入FIN-WAIT-2终止等待2状态等待服务器发送连接释放报文在这之前还需要接受服务器发送的最后的数据。 (4)服务器将最后的数据发送完毕后就向客户端发送连接释放报文FIN=1ack=u+1由于在半关闭状态服务器很可能又发送了一些数据假定此时的序列号为seq=w此时服务器就进入了LAST-ACK最后确认状态等待客户端的确认。 (5)客户端收到服务器的连接释放报文后必须发出确认ACK=1ack=w+1而自己的序列号是seq=u+1此时客户端就进入了TIME-WAIT时间等待状态。注意此时TCP连接还没有释放必须经过2MSL最长报文段寿命的时间后当客户端撤销相应的TCB后才进入CLOSED状态。 (6)服务器只要收到了客户端发出的确认立即进入CLOSED状态。同样撤销TCB后就结束了这次的TCP连接。可以看到服务器结束TCP连接的时间要比客户端早一些

  1. 为什么连接的时候是三次握手,关闭的时候却是四次握手

因为当Server端收到Client端的SYN连接请求报文后可以直接发送SYN+ACK报文。其中ACK报文是用来应答的SYN报文是用来同步的。但是关闭连接时当Server端收到FIN报文时很可能并不会立即关闭SOCKET所以只能先回复一个ACK报文告诉Client端"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了我才能发送FIN报文因此不能一起发送。故需要四步握手。

数据结构与算法

  1. 排序算法

(1)冒泡排序 (2)选择排序:选择排序与冒泡排序有点像,只不过选择排序每次都是在确定了最小数的下标之后再进行交换,大大减少了交换的次数 (3)插入排序将一个记录插入到已排序的有序表中从而得到一个新的记录数增1的有序表 (4)快速排序:通过一趟排序将序列分成左右两部分,其中左半部分的的值均比右半部分的值小,然后再分别对左右部分的记录进行排序,直到整个序列有序。

int partition(int a[],  int low, int high){
    int key = a[low];
    while( low < high ){
        while(low < high && a[high] >= key) high--;
        a[low] = a[high];
        while(low < high && a[low] <= key) low++;
        a[high] = a[low];
    }
    a[low] = key;
    return low;
}

void quick_sort(int a[], int low, int high){
    if(low >= high) return;
    int keypos = partition(a, low, high);
    quick_sort(a, low, keypos-1);
    quick_sort(a, keypos+1, high);
}

(5)堆排序假设序列有n个元素,先将这n建成大顶堆然后取堆顶元素与序列第n个元素交换然后调整前n-1元素使其重新成为堆然后再取堆顶元素与第n-1个元素交换再调整前n-2个元素...直至整个序列有序。 (6)希尔排序:先将整个待排记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录基本有序时再对全体记录进行一次直接插入排序。 (7)归并排序:把有序表划分成元素个数尽量相等的两半,把两半元素分别排序,两个有序表合并成一个

实际问题

  1. 高并发系统的限流详解及实现

在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流。

缓存缓存比较好理解在大型高并发系统中如果没有缓存数据库将分分钟被爆系统也会瞬间瘫痪。使用缓存不单单能够提升系统访问速度、提高并发访问量也是保护数据库、保护系统的有效方式。大型网站一般主要是“读”缓存的使用很容易被想到。在大型“写”系统中缓存也常常扮演者非常重要的角色。比如累积一些数据批量写入内存里面的缓存队列生产消费以及HBase写数据的机制等等也都是通过缓存提升系统的吞吐量或者实现系统的保护措施。甚至消息中间件你也可以认为是一种分布式的数据缓存。 降级:服务降级是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行。降级往往会指定不同的级别,面临不同的异常等级执行不同的处理。根据服务方式:可以拒接服务,可以延迟服务,也有时候可以随机服务。根据服务范围:可以砍掉某个功能,也可以砍掉某些模块。总之服务降级需要根据不同的业务需求采用不同的降级策略。主要的目的就是服务虽然有损但是总比没有好。 限流:限流可以认为服务降级的一种,限流就是限制系统的输入和输出流量已达到保护系统的目的。一般来说系统的吞吐量是可以被测算的,为了保证系统的稳定运行,一旦达到的需要限制的阈值,就需要限制流量并采取一些措施以完成限制流量的目的。比如:延迟处理,拒绝处理,或者部分拒绝处理等等。 常见的限流算法有计数器、漏桶和令牌桶算法。漏桶算法在分布式环境中消息中间件或者Redis都是可选的方案。发放令牌的频率增加可以提升整体数据处理的速度而通过每次获取令牌的个数增加或者放慢令牌的发放速度和降低整体数据处理速度。而漏桶不行因为它的流出速率是固定的程序处理速度也是固定的。

  1. 如何在大量的数据中找出不重复的整数

位图法用2个 bit 来表示各个数字的状态00 表示这个数字没出现过01 表示这个数字出现过一次10 表示这个数字出现了多次。那么这 232 个整数,总共所需内存为 232*2b=1GB。因此当可用内存超过 1GB 时,可以采用位图法。

  1. 如何从5亿个数中找出中位数

分治法,分治法的思想是把一个大的问题逐渐转换为规模较小的问题来求解。对于这道题,顺序读取这 5 亿个数字,对于读取到的数字 num如果它对应的二进制中最高位为 1则把这个数字写到 f1 中,否则写入 f0 中。通过这一步,可以把这 5 亿个数划分为两部分,而且 f0 中的数都大于 f1 中的数。划分之后,可以非常容易地知道中位数是在 f0 还是 f1 中,依次类推。

  1. 秒杀并发情况下库存为负数问题

(1)for update显示加锁 (2)把udpate语句写在前边先把数量-1之后select出库存如果>-1就commit,否则rollback。 update products set quantity = quantity-1 WHERE id=3; select quantity from products WHERE id=3 for update; (3)update语句在更新的同时加上一个条件 quantity = select quantity from products WHERE id=3; update products set quantity = ($quantity-1) WHERE id=3 and queantity = $quantity;