京东18届一年半经验社招面经
- ZooKeeper
- CAP定理
- ZAB协议
- Leader选举算法和流程
- Redis
- Redis的应用场景
- 单线程的Redis为什么快
- Redis 的数据结构及使用场景(必考)
- zset跳表的数据结构(必考)
- Redis的数据过期策略(必考)
- Redis的LRU过期策略的具体实现
- 如何解决Redis缓存雪崩,缓存穿透问题
- Redis的持久化机制(必考)
- redis主从复制,主从同步
- Redis和memcached的区别
- redis并发竞争key的解决方案
- Redis与Mysql双写一致性方案
- Redis的管道pipeline
- Mysql
- 事务的基本要素(事务特性)
- 事务隔离级别、如何解决事务的并发问题(脏读,幻读)(必考)
- MVCC,binlog,redolog,undolog都是什么,起什么作用(必考)
- binlog和redolog的区别
- Mysql如何保证一致性和持久性
- InnoDB的行锁
- myisam和innodb的区别,什么时候选择myisam
- 为什么选择B+树作为索引结构(必考)
- 索引B+树的叶子节点都可以存哪些东西(必考)
- 查询在什么时候不走(预期中的)索引(必考)
- sql如何优化
- explain是如何解析sql的
- order by原理
- JVM
- 运行时数据区域(内存模型)(必考)
- 分代回收
- 垃圾回收机制(必考)
- 哪些对象可以作为GC Roots
- 垃圾回收算法(必考)
- Minor GC和Full GC触发条件
- GC中Stop the world(STW)
- 各垃圾回收器的特点及区别
- G1和CMS的比较
- 双亲委派模型
- JDBC和双亲委派模型关系
- Java并发
- HashMap和ConcurrentHashMap区别(必考)
- ConcurrentHashMap的数据结构(必考)
- 高并发HashMap的环是如何产生的
- volatile作用(必考)
- Atomic类如何保证原子性(CAS操作)(必考)
- CAS操作ABA问题
- synchronized和Lock的区别(必考)
- 线程之间如何通信
- 为什么要使用线程池(必考)
- 核心线程池ThreadPoolExecutor的参数(必考)
- ThreadPoolExecutor的工作流程(必考)
- 如何控制线程池线程的优先级
- AQS理论的数据结构
- Java基础
- HashMap如果我想要让自己的Object作为K应该怎么办
- Boolean占几个字节
- jdk1.8/jdk1.7都分别新增了哪些特性
- Exception和Error区别
- Spring
- Spring的IOC/AOP的实现(必考)
- 动态代理的实现方式(必考)
- Spring的后置处理器
- Spring的@Transactional如何实现的(必考)
- Spring的事务传播级别
- 消息队列
- 为什么需要消息队列
- Kafka的文件存储机制
- Kafka 如何保证可靠性
- Kafka消息是采用Pull模式,还是Push模式
- Kafka是如何实现高吞吐率的
- Kafka判断一个节点还活着的两个条件
- 操作系统
- 进程和线程
- 进程的组成部分
- 进程的通信方式
- 进程间五种通信方式的比较
- 死锁的4个必要条件
- 如何避免(预防)死锁
- 计算机网路
- Get和Post区别
- Http请求的完全过程
- tcp和udp区别
- tcp和udp的优点
- 三次握手
- 为什么不能两次握手
- 四次挥手
- 为什么连接的时候是三次握手,关闭的时候却是四次握手
- 其他
- 面试感受及评价
- 最后
ZooKeeper
CAP定理
- 一个分布式系统不可能同时满足以下三种,一致性(C:Consistency),可用性(A:Available),分区容错性(P:Partition Tolerance). 在此ZooKeeper保证的是CP,ZooKeeper不能保证每次服务请求的可用性,在极端环境下,ZooKeeper可能会丢弃一些请求,消费者程序需要重新请求才能获得结果。 另外在进行leader选举时集群都是不可用,所以说,ZooKeeper不能保证服务可用性。(Base理论CA强一致性和最终一致性)
- 推荐阅读
- 谈谈分布式系统的CAP理论
- CAP理论中的P到底是个什么意思?
- 分布式理论(二) - BASE理论
ZAB协议
- ZAB协议包括两种基本的模式:崩溃恢复和消息广播。当整个 Zookeeper 集群刚刚启动或者Leader服务器宕机、重启或者网络故障导致不存在过半的服务器与 Leader 服务器保持正常通信时,所有服务器进入崩溃恢复模式,首先选举产生新的 Leader 服务器,然后集群中 Follower 服务器开始与新的 Leader 服务器进行数据同步。 当集群中超过半数机器与该 Leader 服务器完成数据同步之后,退出恢复模式进入消息广播模式,Leader 服务器开始接收客户端的事务请求生成事物提案来进行事务请求处理。
- 推荐阅读:看大牛如何分析Zookeeper ZAB 协议
Leader选举算法和流程
- 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。
- 推荐阅读:【分布式】Zookeeper的Leader选举
Redis
Redis的应用场景
(1)缓存
(2)共享Session
(3)消息队列系统
(4)分布式锁
推荐阅读:Redis常见的应用场景解析
单线程的Redis为什么快
(1)纯内存操作
(2)单线程操作,避免了频繁的上下文切换
(3)合理高效的数据结构
(4)采用了非阻塞I/O多路复用机制
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 操作。
zset跳表的数据结构(必考)
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过期策略的具体实现
- 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最小的值然后将其淘汰。
- 推荐阅读:Redis中的LRU淘汰策略分析
如何解决Redis缓存雪崩,缓存穿透问题
- 缓存雪崩:
- (1)使用 Redis 高可用架构:使用 Redis 集群来保证 Redis 服务不会挂掉
- (2)缓存时间不一致,给缓存的失效时间,加上一个随机值,避免集体失效
- (3)限流降级策略:有一定的备案,比如个性推荐服务不可用了,换成热点数据推荐服务
- 缓存穿透
- (1)在接口做校验
- (2)存null值(缓存击穿加锁)
- (3)布隆过滤器拦截: 将所有可能的查询key 先映射到布隆过滤器中,查询时先判断key是否存在布隆过滤器中,存在才继续向下执行,如果不存在,则直接返回。 布隆过滤器将值进行多次哈希bit存储,布隆过滤器说某个元素在,可能会被误判。布隆过滤器说某个元素不在,那么一定不在。
- 推荐阅读:缓存穿透,缓存击穿,缓存雪崩解决方案分析
Redis的持久化机制(必考)
redis主从复制,主从同步
-
(1)从节点执行slaveofmasterIP,保存主节点信息
(2)从节点中的定时任务发现主节点信息,建立和主节点的socket连接
(3)从节点发送Ping信号,主节点返回Pong,两边能互相通信
(4)连接建立后,主节点将所有数据发送给从节点(数据同步)
(5)主节点把当前的数据同步给从节点后,便完成了复制的建立过程。接下来,主节点就会持续的把写命令发送给从节点,保证主从数据一致性。
主从刚刚连接的时候,进行全量同步(RDB);全同步结束后,进行增量同步(AOF)。
-
推荐阅读:深入学习Redis(3):主从复制
Redis和memcached的区别
-
(1)存储方式上:memcache会把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。redis有部分数据存在硬盘上,这样能保证数据的持久性。
(2)数据支持类型上:memcache对数据类型的支持简单,只支持简单的key-value,而redis支持五种数据类型。
(3)用底层模型不同:它们之间底层实现方式以及与客户端之间通信的应用协议不一样。redis直接自己构建了VM机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
(4)value的大小:redis可以达到1GB,而memcache只有1MB。
redis并发竞争key的解决方案
- (1)分布式锁+时间戳 (2)利用消息队列
- 推荐阅读:高并发架构系列:Redis并发竞争key的解决方案详解
Redis与Mysql双写一致性方案
- 先更新数据库,再删缓存。数据库的读操作的速度远快于写操作的,所以脏数据很难出现。可以对异步延时删除策略,保证读请求完成以后,再进行删除操作。
- 推荐阅读:Redis与Mysql双写一致性方案解析
Redis的管道pipeline
- 对于单线程阻塞式的Redis,Pipeline可以满足批量的操作,把多个命令连续的发送给Redis Server,然后一一解析响应结果。Pipelining可以提高批量处理性能,提升的原因主要是TCP连接中减少了“交互往返”的时间。 pipeline 底层是通过把所有的操作封装成流,redis有定义自己的出入输出流。在 sync() 方法执行操作,每次请求放在队列里面,解析响应包。
Mysql
事务的基本要素(事务特性)
-
(1)原子性:事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行
(2)一致性:事务开始前和结束后,数据库的完整性约束没有被破坏。
(3)隔离性:同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。
(4)持久性:事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。
事务隔离级别、如何解决事务的并发问题(脏读,幻读)(必考)
- MySQL 四种事务隔离级的说明
- 数据库事务隔离级别-- 脏读、幻读、不可重复读
MVCC,binlog,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列表。
- binlog由Mysql的Server层实现,是逻辑日志,记录的是sql语句的原始逻辑,比如"把id=‘B’ 修改为id = ‘B2’。binlog会写入指定大小的物理文件中,是追加写入的,当前文件写满则会创建新的文件写入。 产生:事务提交的时候,一次性将事务中的sql语句,按照一定的格式记录到binlog中。用于复制和恢复在主从复制中,从库利用主库上的binlog进行重播(执行日志中记录的修改逻辑),实现主从同步。业务数据不一致或者错了,用binlog恢复。
- 推荐阅读MySQL(5)| 五分钟搞清楚 MVCC 机制
binlog和redolog的区别
- 区别:
- redolog是在InnoDB存储引擎层产生,而binlog是MySQL数据库的上层服务层产生的。
- 两种日志记录的内容形式不同。MySQL的binlog是逻辑日志,其记录是对应的SQL语句。而innodb存储引擎层面的重做日志是物理日志。
- 两种日志与记录写入磁盘的时间点不同,binlog日志只在事务提交完成后进行一次写入。而innodb存储引擎的重做日志在事务进行中不断地被写入,并日志不是随事务提交的顺序进行写入的。
- binlog不是循环使用,在写满或者重启之后,会生成新的binlog文件,redolog是循环使用。
- binlog可以作为恢复数据使用,主从复制搭建,redolog作为异常宕机或者介质故障后的数据恢复使用。
- 推荐阅读:一文带你看懂binlog和redo log
Mysql如何保证一致性和持久性
- MySQL为了保证ACID中的一致性和持久性,使用了WAL(Write-Ahead Logging,先写日志再写磁盘)。Redo log就是一种WAL的应用。当数据库忽然掉电,再重新启动时,MySQL可以通过Redo log还原数据。也就是说,每次事务提交时,不用同步刷新磁盘数据文件,只需要同步刷新Redo log就足够了。
InnoDB的行锁
- 共享锁(S):用法lock in share mode,又称读锁,允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
- 排他锁(X):用法for update,又称写锁,允许获取排他锁的事务更新数据,阻止其他事务取得相同的数据集共享读锁和排他写锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。在没有索引的情况下,InnoDB只能使用表锁。
- 推荐阅读:mysql锁——innodb的行级锁
myisam和innodb的区别,什么时候选择myisam
- https://blog.csdn.net/u010598360/article/details/81482225
为什么选择B+树作为索引结构(必考)
- Hash索引:Hash索引底层是哈希表,哈希表是一种以key-value存储数据的结构,所以多个数据在存储关系上是完全没有任何顺序关系的,所以,对于区间查询是无法直接通过索引查询的,就需要全表扫描。所以,哈希索引只适用于等值查询的场景。而B+ 树是一种多路平衡查询树,所以他的节点是天然有序的(左子节点小于父节点、父节点小于右子节点),所以对于范围查询的时候不需要做全表扫描
- 二叉查找树:解决了排序的基本问题,但是由于无法保证平衡,可能退化为链表。
- 平衡二叉树:通过旋转解决了平衡的问题,但是旋转操作效率太低。
- 红黑树:通过舍弃严格的平衡和引入红黑节点,解决了 AVL旋转效率过低的问题,但是在磁盘等场景下,树仍然太高,IO次数太多。
- B+树:在B树的基础上,将非叶节点改造为不存储数据纯索引节点,进一步降低了树的高度;此外将叶节点使用指针连接成链表,范围查询更加高效。
- 推荐阅读:一步步分析为什么B+树适合作为索引的结构 以及索引原理 (阿里面试)
索引B+树的叶子节点都可以存哪些东西(必考)
- 可能存储的是整行数据,也有可能是主键的值。B+树的叶子节点存储了整行数据的是主键索引,也被称之为聚簇索引。而索引B+ Tree的叶子节点存储了主键的值的是非主键索引,也被称之为非聚簇索引
- 推荐阅读:什么是覆盖索引?如何利用覆盖索引进行SQL语句优化?
查询在什么时候不走(预期中的)索引(必考)
- 模糊查询 %like
- 索引列参与计算,使用了函数
- 非最左前缀顺序
- where对null判断
- where不等于
- or操作有至少一个字段没有索引
- 需要回表的查询结果集过大(超过配置的范围)
-
推荐阅读:
MySQL高级 之 索引失效与优化详解
sql优化的几种方式
sql如何优化
- 创建并使用正确的索引
- 只返回需要的字段
- 减少交互次数(批量提交)
- 设置合理的Fetch Size(数据每次返回给客户端的条数)
explain是如何解析sql的
- 推荐阅读:MySQL 性能优化神器 Explain 使用分析
order by原理
JVM
运行时数据区域(内存模型)(必考)
- 程序计数器:程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。是线程私有”的内存。
- Java虚拟机栈:与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧 ,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
- 本地方法栈:本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
- Java堆:对于大多数应用来说,Java堆是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
- 方法区(1.8叫元数据):方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
分代回收
-
HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。
-
因为年轻代中的对象基本都是朝生夕死的,所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。
-
在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
垃圾回收机制(必考)
- 引用计数法:引用计数法是一种简单但速度很慢的垃圾回收技术。每个对象都含有一个引用计数器,当有引用连接至对象时,引用计数加1。当引用离开作用域或被置为null时,引用计数减1。虽然管理引用计数的开销不大,但这项开销在整个程序生命周期中将持续发生。垃圾回收器会在含有全部对象的列表上遍历,当发现某个对象引用计数为0时,就释放其占用的空间。
- 可达性分析算法:这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
哪些对象可以作为GC Roots
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(即一般说的Native方法)引用的对象。
垃圾回收算法(必考)
- 复制:先暂停程序的运行,然后将所有存活的对象从当前堆复制到另一个堆,没有被复制的对象全部都是垃圾。当对象被复制到新堆时,它们是一个挨着一个的,所以新堆保持紧凑排列,然后就可以按前述方法简单,直接的分配了。缺点是一浪费空间,两个堆之间要来回倒腾,二是当程序进入稳定态时,可能只会产生极少的垃圾,甚至不产生垃圾,尽管如此,复制式回收器仍会将所有内存自一处复制到另一处。
- 标记-清除:同样是从堆栈和静态存储区出发,遍历所有的引用,进而找出所有存活的对象。每当它找到一个存活的对象,就会给对象一个标记,这个过程中不会回收任何对象。只有全部标记工作完成的时候,清理动作才会开始。在清理过程中,没有标记的对象会被释放,不会发生任何复制动作。所以剩下的堆空间是不连续的,垃圾回收器如果要希望得到连续空间的话,就得重新整理剩下的对象。
- 标记-整理:它的第一个阶段与标记/清除算法是一模一样的,均是遍历GC Roots,然后将存活的对象标记。移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。因此,第二阶段才称为整理阶段。
- 分代收集算法:把Java堆分为新生代和老年代,然后根据各个年代的特点采用最合适的收集算法。新生代中,对象的存活率比较低,所以选用复制算法,老年代中对象存活率高且没有额外空间对它进行分配担保,所以使用“标记-清除”或“标记-整理”算法进行回收。
Minor GC和Full GC触发条件
- Minor GC触发条件:当Eden区满时,触发Minor GC。
- Full GC触发条件:
- 调用System.gc时,系统建议执行Full GC,但是不必然执行
- 老年代空间不足
- 方法区空间不足
- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
- 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
GC中Stop the world(STW)
-
在执行垃圾收集算法时,Java应用程序的其他所有除了垃圾收集收集器线程之外的线程都被挂起。此时,系统只能允许GC线程进行运行,其他线程则会全部暂停,等待GC线程执行完毕后才能再次运行。这些工作都是由虚拟机在后台自动发起和自动完成的,是在用户不可见的情况下把用户正常工作的线程全部停下来,这对于很多的应用程序,尤其是那些对于实时性要求很高的程序来说是难以接受的。
但不是说GC必须STW,你也可以选择降低运行速度但是可以并发执行的收集算法,这取决于你的业务。
各垃圾回收器的特点及区别
-
新生代收集器
- Serial收集器
- ParNew 收集器
- Parallel Scavenge 收集器
-
老年代收集器
- Serial Old收集器
- Parallel Old收集器
- CMS收集器
- G1收集器
-
推荐阅读:
JVM垃圾回收
深入理解JVM(3)——7种垃圾收集器
G1和CMS的比较
- CMS收集器是获取最短回收停顿时间为目标的收集器,因为CMS工作时,GC工作线程与用户线程可以并发执行,以此来达到降低手机停顿时间的目的(只有初始标记和重新标记会STW)。但是CMS收集器对CPU资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致引用程序变慢,总吞吐量下降。
- CMS仅作用于老年代,是基于标记清除算法,所以清理的过程中会有大量的空间碎片。
- CMS收集器无法处理浮动垃圾,由于CMS并发清理阶段用户线程还在运行,伴随程序的运行自热会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理它们,只好留待下一次GC时将其清理掉。
- G1是一款面向服务端应用的垃圾收集器,适用于多核处理器、大内存容量的服务端系统。G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短STW的停顿时间,它满足短时间停顿的同时达到一个高的吞吐量。
- 从JDK 9开始,G1成为默认的垃圾回收器。当应用有以下任何一种特性时非常适合用G1:Full GC持续时间太长或者太频繁;对象的创建速率和存活率变动很大;应用不希望停顿时间长(长于0.5s甚至1s)。
- G1将空间划分成很多块(Region),然后他们各自进行回收。堆比较大的时候可以采用,采用复制算法,碎片化问题不严重。整体上看属于标记整理算法,局部(region之间)属于复制算法。
- G1 需要记忆集 (具体来说是卡表)来记录新生代和老年代之间的引用关系,这种数据结构在 G1 中需要占用大量的内存,可能达到整个堆内存容量的 20% 甚至更多。而且 G1 中维护记忆集的成本较高,带来了更高的执行负载,影响效率。所以 CMS 在小内存应用上的表现要优于 G1,而大内存应用上 G1 更有优势,大小内存的界限是6GB到8GB。
双亲委派模型
JDBC和双亲委派模型关系
Java并发
HashMap和ConcurrentHashMap区别(必考)
-
由于HashMap是线程不同步的,虽然处理数据的效率高,但是在多线程的情况下存在着安全问题,因此设计了CurrentHashMap来解决多线程安全问题。
-
HashMap在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的。
ConcurrentHashMap的数据结构(必考)
-
在JDK1.7版本中,ConcurrentHashMap维护了一个Segment数组,Segment这个类继承了重入锁ReentrantLock,并且该类里面维护了一个 HashEntry[] table数组,在写操作put,remove,扩容的时候,会对Segment加锁,所以仅仅影响这个Segment,不同的Segment还是可以并发的,所以解决了线程的安全问题,同时又采用了分段锁也提升了并发的效率。在JDK1.8版本中,ConcurrentHashMap摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap。
-
推荐阅读:HashMap? ConcurrentHashMap? 相信看完这篇没人能难住你!
高并发HashMap的环是如何产生的
volatile作用(必考)
-
volatile在多处理器开发中保证了共享变量的“ 可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。(共享内存,私有内存)
-
volatile关键字通过“内存屏障”
来防止指令被重排序。
-
推荐阅读:Java并发编程:volatile关键字解析
Atomic类如何保证原子性(CAS操作)(必考)
- CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。如 Intel 处理器,比较并交换通过指令的 cmpxchg 系列实现。
CAS操作ABA问题
- 如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。
- 推荐阅读:Java CAS 原理剖析
synchronized和Lock的区别(必考)
- 首先synchronized是java内置关键字在jvm层面,Lock是个java类。
- synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁,并且可以主动尝试去获取锁。
- synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁。
- 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了。
- synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
- Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
- 推荐阅读:谈谈-synchronized和reentrantlock-的区别
线程之间如何通信
为什么要使用线程池(必考)
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
核心线程池ThreadPoolExecutor的参数(必考)
- corePoolSize:指定了线程池中的线程数量
- maximumPoolSize:指定了线程池中的最大线程数量
- keepAliveTime:线程池维护线程所允许的空闲时间
- unit: keepAliveTime 的单位。
- workQueue:任务队列,被提交但尚未被执行的任务。
- threadFactory:线程工厂,用于创建线程,一般用默认的即可。
- handler:拒绝策略。当任务太多来不及处理,如何拒绝任务。
- 推荐阅读:threadpoolexecutor构造函数重要参数分析
ThreadPoolExecutor的工作流程(必考)
线程池的线程执行规则跟任务队列有很大的关系。
- 下面都假设任务队列没有大小限制:
- 如果线程数量<=核心线程数量,那么直接启动一个核心线程来执行任务,不会放入队列中。
- 如果线程数量>核心线程数,但<=最大线程数,并且任务队列是LinkedBlockingDeque的时候,超过核心线程数量的任务会放在任务队列中排队。
- 如果线程数量>核心线程数,但<=最大线程数,并且任务队列是SynchronousQueue的时候,线程池会创建新线程执行任务,这些任务也不会被放在任务队列中。这些线程属于非核心线程,在任务完成后,闲置时间达到了超时时间就会被清除。
- 如果线程数量>核心线程数,并且>最大线程数,当任务队列是LinkedBlockingDeque,会将超过核心线程的任务放在任务队列中排队。也就是当任务队列是LinkedBlockingDeque并且没有大小限制时,线程池的最大线程数设置是无效的,他的线程数最多不会超过核心线程数。
- 如果线程数量>核心线程数,并且>最大线程数,当任务队列是SynchronousQueue的时候,会因为线程池拒绝添加任务而抛出异常。
- 任务队列大小有限时
- 当LinkedBlockingDeque塞满时,新增的任务会直接创建新线程来执行,当创建的线程数量超过最大线程数量时会抛异常。
- SynchronousQueue没有数量限制。因为他根本不保持这些任务,而是直接交给线程池去执行。当任务数量超过最大线程数时会直接抛异常。
- 推荐阅读:Java多线程-线程池ThreadPoolExecutor构造方法和规则
如何控制线程池线程的优先级
- 把现有线程池改为优先级队列
- 推荐阅读:基于优先级队列java线程池
AQS理论的数据结构
Java基础
HashMap如果我想要让自己的Object作为K应该怎么办
- 重写hashCode()是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞;
- 重写equals()方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性;
Boolean占几个字节
未精确定义字节。Java语言表达式所操作的boolean值,在编译之后都使用Java虚拟机中的int数据类型来代替,而boolean数组将会被编码成Java虚拟机的byte数组,每个元素boolean元素占8位。
jdk1.8/jdk1.7都分别新增了哪些特性
- [Jdk1.7与 jdk1.8的区别和最新的特征](https://swenfang.github.io/2019/05/12/面试总结/Jdk1.7与 jdk1.8的区别和最新的特征/)
Exception和Error区别
-
Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java中,错误通过Error的子类描述。
-
Exception(异常):是程序本身可以处理的异常。Exception 类有一个重要的子类 RuntimeException。RuntimeException 异常由Java虚拟机抛出。NullPointerException(要访问的变量没有引用任何对象时,抛出该异常)、ArithmeticException(算术运算异常,一个整数除以0时,抛出该异常)和 ArrayIndexOutOfBoundsException (下标越界异常)。
Spring
Spring的IOC/AOP的实现(必考)
- IOC(控制反转)就是依赖倒置原则的一种代码设计思路。就是把原先在代码里面需要实现的对象创建、对象之间的依赖,反转给容器来帮忙实现。
- AOP底层实现原理:动态代理
- 推荐阅读:理解Spring的AOP和IOC实现原理
动态代理的实现方式(必考)
-
JDK动态代理:利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。
-
CGlib动态代理:利用ASM(开源的Java字节码编辑库,操作字节码)开源包,将代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
-
区别:JDK代理只能对实现接口的类生成代理;CGlib是针对类实现代理,对指定的类生成一个子类,并覆盖其中的方法,这种通过继承类的实现方式,不能代理final修饰的类。
-
Spring AOP 基于AspectJ注解如何实现AOP : AspectJ是一个AOP框架,它能够对java代码进行AOP编译(一般在编译期进行),让java代码具有AspectJ的AOP功能(当然需要特殊的编译器)
Spring的后置处理器
-
BeanPostProcessor:Bean的后置处理器,主要在bean初始化前后工作。
-
InstantiationAwareBeanPostProcessor:继承于BeanPostProcessor,主要在实例化bean前后工作; AOP创建代理对象就是通过该接口实现。
-
BeanFactoryPostProcessor:Bean工厂的后置处理器,在bean定义(bean definitions)加载完成后,bean尚未初始化前执行。
-
BeanDefinitionRegistryPostProcessor:继承于BeanFactoryPostProcessor。其自定义的方法postProcessBeanDefinitionRegistry会在bean定义(bean definitions)将要加载,bean尚未初始化前真执行,即在BeanFactoryPostProcessor的postProcessBeanFactory方法前被调用。
-
推荐阅读:bean的生命周期
Spring的@Transactional如何实现的(必考)
-
配置文件开启注解驱动,在相关的类和方法上通过注解@Transactional标识。
-
spring 在启动的时候会去解析生成相关的bean,这时候会查看拥有相关注解的类和方法,并且为这些类和方法生成代理,并根据@Transaction的相关参数进行相关配置注入,这样就在代理中为我们把相关的事务处理掉了(开启正常提交事务,异常回滚事务)。
-
真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。
-
推荐阅读:@Transaction必知必会
Spring的事务传播级别
-
REQUIRED(默认):支持使用当前事务,如果当前事务不存在,创建一个新事务。
-
SUPPORTS:支持使用当前事务,如果当前事务不存在,则不使用事务。
-
MANDATORY:强制,支持使用当前事务,如果当前事务不存在,则抛出Exception。
-
REQUIRES_NEW:创建一个新事务,如果当前事务存在,把当前事务挂起。
-
NOT_SUPPORTED:无事务执行,如果当前事务存在,把当前事务挂起。
-
NEVER:无事务执行,如果当前有事务则抛出Exception。
-
NESTED:嵌套事务,如果当前事务存在,那么在嵌套的事务中执行。如果当前事务不存在,则表现跟REQUIRED一样。
-
推荐阅读:Spring事务传播机制
消息队列
为什么需要消息队列
-
异步:主系统接收一个请求,在本地执行完SQL以后,需要分别调用A,B,C三个子系统的接口,执行时间分别为200ms,100ms,300ms,则总的执行时间为10+200+100+300 = 610(ms)。但是一旦使用了MQ之后,主系统只需要发送3条消息到MQ中的3个消息队列,然后就返回给用户了。消息发送到MQ耗时20ms,那么用户感知到这个接口的总时间就为10+20=30(ms)。
-
解耦:开始的时候,主系统在用户发生某个操作的时候,需要把用户提交的数据同时推送到A、B两个系统的时候。
随着业务快速迭代,这个时候系统C,D也想要这个数据,主系统修改接口,增加C,D的接入
随着业务再迭代,这个时候系统B不要这个数据,主系统修改接口,删除B的接入
… …业务不断迭代, 主系统需要不断调整接口。
引入MQ以后,主系统只负责将生产的数据投递到MQ,其它事情不用关心。各个子系统可以随时订阅/取消对消息的消费。
-
削峰填谷:DB支持的最大QPS为1000,平常的时候,用户访问请求为100QPS,系统访问正常。 高峰的时候,大量用户请求瞬间涌入,DB的请求达到5000QPS,直接被打死,绝望。
引入MQ以后,消息被MQ保存起来了,然后系统就可以按照自己的消费能力来消费,比如每秒1000个数据,这样慢慢写入数据库,这样就不会打死数据库了:
Kafka的文件存储机制
- Kafka中消息是以topic进行分类的,生产者通过topic向Kafka broker发送消息,消费者通过topic读取数据。然而topic在物理层面又能以partition为分组,一个topic可以分成若干个partition。partition还可以细分为segment,一个partition物理上由多个segment组成,segment文件由两部分组成,分别为“.index”文件和“.log”文件,分别表示为segment索引文件和数据文件。这两个文件的命令规则为:partition全局的第一个segment从0开始,后续每个segment文件名为上一个segment文件最后一条消息的offset值。
Kafka 如何保证可靠性
如果我们要往 Kafka 对应的主题发送消息,我们需要通过 Producer 完成。前面我们讲过 Kafka 主题对应了多个分区,每个分区下面又对应了多个副本;为了让用户设置数据可靠性, Kafka 在 Producer 里面提供了消息确认机制。也就是说我们可以通过配置来决定消息发送到对应分区的几个副本才算消息发送成功。可以在定义 Producer 时通过 acks 参数指定。这个参数支持以下三种值:
- acks = 0:意味着如果生产者能够通过网络把消息发送出去,那么就认为消息已成功写入 Kafka 。在这种情况下还是有可能发生错误,比如发送的对象无能被序列化或者网卡发生故障,但如果是分区离线或整个集群长时间不可用,那就不会收到任何错误。在 acks=0 模式下的运行速度是非常快的(这就是为什么很多基准测试都是基于这个模式),你可以得到惊人的吞吐量和带宽利用率,不过如果选择了这种模式, 一定会丢失一些消息。
- acks = 1:意味若 Leader 在收到消息并把它写入到分区数据文件(不一定同步到磁盘上)时会返回确认或错误响应。在这个模式下,如果发生正常的 Leader 选举,生产者会在选举时收到一个 LeaderNotAvailableException 异常,如果生产者能恰当地处理这个错误,它会重试发送悄息,最终消息会安全到达新的 Leader 那里。不过在这个模式下仍然有可能丢失数据,比如消息已经成功写入 Leader,但在消息被复制到 follower 副本之前 Leader发生崩溃。
- acks = all(这个和 request.required.acks = -1 含义一样):意味着 Leader 在返回确认或错误响应之前,会等待所有同步副本都收到悄息。如果和min.insync.replicas 参数结合起来,就可以决定在返回确认前至少有多少个副本能够收到悄息,生产者会一直重试直到消息被成功提交。不过这也是最慢的做法,因为生产者在继续发送其他消息之前需要等待所有副本都收到当前的消息。
Kafka消息是采用Pull模式,还是Push模式
Kafka最初考虑的问题是,customer应该从brokes拉取消息还是brokers将消息推送到consumer,也就是pull还push。在这方面,Kafka遵循了一种大部分消息系统共同的传统的设计:producer将消息推送到broker,consumer从broker拉取消息。push模式下,当broker推送的速率远大于consumer消费的速率时,consumer恐怕就要崩溃了。最终Kafka还是选取了传统的pull模式。Pull模式的另外一个好处是consumer可以自主决定是否批量的从broker拉取数据。Pull有个缺点是,如果broker没有可供消费的消息,将导致consumer不断在循环中轮询,直到新消息到达。为了避免这点,Kafka有个参数可以让consumer阻塞知道新消息到达。
Kafka是如何实现高吞吐率的
- 顺序读写:kafka的消息是不断追加到文件中的,这个特性使kafka可以充分利用磁盘的顺序读写性能
- 零拷贝:跳过“用户缓冲区”的拷贝,建立一个磁盘空间和内存的直接映射,数据不再复制到“用户态缓冲区”
- 文件分段:kafka的队列topic被分为了多个区partition,每个partition又分为多个段segment,所以一个队列中的消息实际上是保存在N多个片段文件中
- 批量发送:Kafka允许进行批量发送消息,先将消息缓存在内存中,然后一次请求批量发送出去
- 数据压缩:Kafka还支持对消息集合进行压缩,Producer可以通过GZIP或Snappy格式对消息集合进行压缩
Kafka判断一个节点还活着的两个条件
- 节点必须可以维护和 ZooKeeper 的连接,Zookeeper 通过心跳机制检查每个节点的连接
- 如果节点是个 follower,他必须能及时的同步 leader 的写操作,延时不能太久
操作系统
进程和线程
- **进程是操作系统资源分配的最小单位,线程是CPU任务调度的最小单位。**一个进程可以包含多个线程,所以进程和线程都是一个时间段的描述,是CPU工作时间段的描述,不过是颗粒大小不同。
- 不同进程间数据很难共享,同一进程下不同线程间数据很易共享。
- 每个进程都有独立的代码和数据空间,进程要比线程消耗更多的计算机资源。线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器,线程之间切换的开销小。
- 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉。
- 系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。
进程的组成部分
进程由进程控制块(PCB)、程序段、数据段三部分组成。
进程的通信方式
- 无名管道:半双工的,即数据只能在一个方向上流动,只能用于具有亲缘关系的进程之间的通信,可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。
- FIFO命名管道:FIFO是一种文件类型,可以在无关的进程之间交换数据,与无名管道不同,FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。
- 消息队列:消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。
- 信号量:信号量是一个计数器,信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
- 共享内存:共享内存指两个或多个进程共享一个给定的存储区,一般配合信号量使用。
进程间五种通信方式的比较
- 管道:速度慢,容量有限,只有父子进程能通讯。
- FIFO:任何进程间都能通讯,但速度慢。
- 消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题。
- 信号量:不能传递复杂消息,只能用来同步。
- 共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存。
死锁的4个必要条件
- 互斥条件:一个资源每次只能被一个线程使用;
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放;
- 不剥夺条件:进程已经获得的资源,在未使用完之前,不能强行剥夺;
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
如何避免(预防)死锁
- 破坏“请求和保持”条件:让进程在申请资源时,一次性申请所有需要用到的资源,不要一次一次来申请,当申请的资源有一些没空,那就让线程等待。不过这个方法比较浪费资源,进程可能经常处于饥饿状态。还有一种方法是,要求进程在申请资源前,要释放自己拥有的资源。
- 破坏“不可抢占”条件:允许进程进行抢占,方法一:如果去抢资源,被拒绝,就释放自己的资源。方法二:操作系统允许抢,只要你优先级大,可以抢到。
- 破坏“循环等待”条件:将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序提出(指定获取锁的顺序,顺序加锁)。
计算机网路
Get和Post区别
- Get是不安全的,因为在传输过程,数据被放在请求的URL中;Post的所有操作对用户来说都是不可见的。
- Get传送的数据量较小,这主要是因为受URL长度限制;Post传送的数据量较大,一般被默认为不受限制。
- Get限制Form表单的数据集的值必须为ASCII字符;而Post支持整个ISO10646字符集。
- Get执行效率却比Post方法好。Get是form提交的默认方法。
- GET产生一个TCP数据包;POST产生两个TCP数据包。(非必然,客户端可灵活决定)
Http请求的完全过程
- 浏览器根据域名解析IP地址(DNS),并查DNS缓存
- 浏览器与WEB服务器建立一个TCP连接
- 浏览器给WEB服务器发送一个HTTP请求(GET/POST):一个HTTP请求报文由请求行(request line)、请求头部(headers)、空行(blank line)和请求数据(request body)4个部分组成。
- 服务端响应HTTP响应报文,报文由状态行(status line)、相应头部(headers)、空行(blank line)和响应数据(response body)4个部分组成。
- 浏览器解析渲染
tcp和udp区别
- TCP面向连接,UDP是无连接的,即发送数据之前不需要建立连接。
- TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付。
- TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流,UDP是面向报文的,UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)
- 每一条TCP连接只能是点到点的,UDP支持一对一,一对多,多对一和多对多的交互通信。
- TCP首部开销20字节,UDP的首部开销小,只有8个字节。
- TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道。
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。
三次握手
- 第一次握手:建立连接时,客户端发送syn包(syn=x)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。
- 第二次握手:服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(syn=y),即SYN+ACK包,此时服务器进入SYN_RECV状态;
- 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。
为什么不能两次握手
-
TCP是一个双向通信协议,通信双方都有能力发送信息,并接收响应。如果只是两次握手, 至多只有连接发起方的起始序列号能被确认, 另一方选择的序列号则得不到确认。
-
这主要是为了防止已失效的请求连接报文忽然又传送到了,从而产生错误。
假定A向B发送一个连接请求,由于一些原因,导致A发出的连接请求在一个网络节点逗留了比较多的时间。此时A会将此连接请求作为无效处理 又重新向B发起了一次新的连接请求,B正常收到此连接请求后建立了连接,数据传输完成后释放了连接。如果此时A发出的第一次请求又到达了B,B会以为A又发起了一次连接请求,如果是两次握手:此时连接就建立了,B会一直等待A发送数据,从而白白浪费B的资源。 如果是三次握手:由于A没有发起连接请求,也就不会理会B的连接响应,B没有收到A的确认连接,就会关闭掉本次连接。
四次挥手
- 客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
- 服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
- 客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
- 服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
- 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
- 服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。
为什么连接的时候是三次握手,关闭的时候却是四次握手
因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,“你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
其他
高并发系统的设计与实现
在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流。
- 缓存:缓存比较好理解,在大型高并发系统中,如果没有缓存数据库将分分钟被爆,系统也会瞬间瘫痪。使用缓存不单单能够提升系统访问速度、提高并发访问量,也是保护数据库、保护系统的有效方式。大型网站一般主要是“读”,缓存的使用很容易被想到。在大型“写”系统中,缓存也常常扮演者非常重要的角色。比如累积一些数据批量写入,内存里面的缓存队列(生产消费),以及HBase写数据的机制等等也都是通过缓存提升系统的吞吐量或者实现系统的保护措施。甚至消息中间件,你也可以认为是一种分布式的数据缓存。
- 降级:服务降级是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行。降级往往会指定不同的级别,面临不同的异常等级执行不同的处理。根据服务方式:可以拒接服务,可以延迟服务,也有时候可以随机服务。根据服务范围:可以砍掉某个功能,也可以砍掉某些模块。总之服务降级需要根据不同的业务需求采用不同的降级策略。主要的目的就是服务虽然有损但是总比没有好。
- 限流:限流可以认为服务降级的一种,限流就是限制系统的输入和输出流量已达到保护系统的目的。一般来说系统的吞吐量是可以被测算的,为了保证系统的稳定运行,一旦达到的需要限制的阈值,就需要限制流量并采取一些措施以完成限制流量的目的。比如:延迟处理,拒绝处理,或者部分拒绝处理等等。
高并发系统的限流如何实现
常见的限流算法有计数器、漏桶和令牌桶算法。漏桶算法在分布式环境中消息中间件或者Redis都是可选的方案。发放令牌的频率增加可以提升整体数据处理的速度,而通过每次获取令牌的个数增加或者放慢令牌的发放速度和降低整体数据处理速度。而漏桶不行,因为它的流出速率是固定的,程序处理速度也是固定的。
面试感受及评价
除了外企,体验最好的就是阿里。绝对的脱颖而出,无论是面试官的专业程度还是面试官对参与面试人员的态度都完全突出于其他公司。非常的尊重人,以及会引导我去作出正确的回答,唯一就是阿里的HR是非常强势的,永远有一票否决权。而有些公司面试官会故意误导你,想方设法让你说出错误的答案,并且有些态度极其傲慢,让人感觉很不尊重人。这里点名批评面试体验最差的两家公司:美团和Boss直聘。
外企的话,体验都很好,但是我都还没面试完,后面会更新的。微软是英文面的,亚马逊不是。这俩都是以算法为主,微软除了算法还聊了操作系统和计算机网络,亚马逊聊了较长时间的项目细节。
最后
最后说下自己的情况,17年在京东实习,19年7月离职。正式工作时间很短,就一年(算实习两年),而且19年有半年的时间准备考研所以有半年的空档期,这也是为什么我被很多HR挂了的原因。虽然Offer没拿几个,但是一半多都面到HR面了,所以对于两三年经验的感觉整理的问题还是比较有代表性的。