又到了秋招的季节,今年的就业形势整体非常严峻,由于下游公司的广告预算等大幅度减少,对于一向依赖广告收入的互联网公司是有很大影响的,所以很多公司无论社招还是校招 headcount 都减少了,因此竞争十分激烈,如何拿到心仪的 offer 是摆在求职者面前一个永恒的话题。
虽然面试套路众多,但对于技术面试来说,主要还是考察一个人的技术能力和沟通能力。不同类型的面试官根据自身的理解问的问题也不尽相同,没有规律可循。有些面试官喜欢问自己擅长的问题,比如在实际编程中遇到的或者他自己一直在琢磨的这方面的问题,还有些面试官,尤其是大厂的比如 BAT 的面试官喜欢问面试者认为自己擅长的,然后通过提问的方式深挖细节,刨根到底。
举个例子,当你一刚坐下的时候,面试官也许就会问第一个问题:问点基础的,说下你平时用得多的比较熟悉的 Java 数据结构?你可能毫不犹豫的就说是 ArrayList. 然后面试官可能就会问到以下问题:
讲讲其原理,初始容量、如何扩容、是浅拷贝还是深拷贝?删除一个元素的时候底层是如何实现的?如何对 ArrayList 去重?ArrayList 是线程安全的么?讲讲其 fast-fail 机制?一个线程安全的 ArrayList 如何实现? 讲讲 CopyOnWriteArrayList 如何实现的以及它的优缺点?ArrayList 跟 LinkedList 区别?最后可能还会让你讲讲 Collections.sort() 方法,这个用来对 List 排序的实现算法以及这种实现算法的优势。
大家可以看到就一个 ArrayList 就会有这么多的问题。所以面试的准备就需要有既要有深度又要有广度,相比较而言,深度为佳,毕竟优秀的面试官都是对你熟悉的知识节点来深挖细节,而不是所谓的蜻蜓点水,当然深度做得很好的情况下也要适当关注一下广度,广度其实是一个知识面的问题,也是一个程序员的视野问题,优秀的面试者都是把点线面结合得很好,用一个立体思维的方式来参加面试,当然也容易在面试过程中立于不败之地。
(一) SpringCloud 相关面试题。
问题1:
请讲一下你项目中用到的技术栈?
解答:
你可以说说比如spring cloud 框架,这里可以顺便罗列些组件类似Eureka,ribbon,feign,consul等等。如下图
问题2:
为啥要用Eureka,Eureka怎么实现服务的注册与发现?
解答:
A 服务 如果要访问B服务 必须要知道B服务的ip 和端口。
首先B服务启动的时候可以通过Eureka Client 向 Eureka server注册自己的ip 和端口。Eureka server 里面由一个注册表存放各个服务的 ip和端口 地址。注册完后并不是一次性动作,还有这个续约的过程,就跟工作签完合同后 需要几年一续一样。
B服务的 Eureka Client需要定期(默认30秒)向Eureka Server发送一次心跳来续订租约,以便让Eureka Server知道自已仍在运行,如果Eureka Server在90秒内未收到续订心跳,则会将B服务的 Client实例从其注册表中删除。
对于A服务呢? 如何得到B服务的ip和端口呢。
A服务也是通过自己的 Eureka Client 从 Eueka Server获取 服务注册列表来查找其他服务信息,获取服务列表后会在本地进行缓存,可以定期(默认30秒)更新服务注册列表信息。
可以看到A服务作为服务消费者直接定期拉取然后更新自己的缓存,实际拿的时候从自己的缓存拿。
问题3:
A服务知道了B服务的地址和端口,在Spring Cloud中A服务如何调用B服务?
解答:
用feign。
看下面的例子,如果B服务是 contract-microservice 微服务。
A服务想要调用里面的 /contract/getDetail接口,直接调用一个函数 getContractDetailsById 搞定.
@FeignClient(value = “contract-microservice”) public interface
ContractClient {
@RequestMapping(method = RequestMethod.GET, value = “/contract/getDetail”) ContractDataDTO
getContractDetailsById(@RequestParam(“contractId”) Long contractId);
}
@FeignClient 把httpclient需要做的一些组装发送 给做了。
比如
Feign会针对这个接口创建一个动态代理。
(这一点其实跟mybatis里的xxxMaper 调用自己的某个函数类似,所以有些东西都是相通的)
接着要是调用那个函数getContractDetailsById,本质就是会调用 Feign创建的动态代理,Feign的动态代理会根据接口上的@RequestMapping等注解,来动态构造出请求的服务的地址最后针对这个地址,发起请求、解析响应
问题4:
B服务如果部署多台机器,A服务如何选择哪一台机器 和端口呢?
解答:
这里就是一个负载均衡机制,feign默认集成了ribbon ,ribbon里的默认负载均衡是Round Robin轮询算法。轮询法是将请求按顺序轮流地分配到服务器上,均衡地对待后端的每一台服务器,而不关心服务器实际的连接数和当前的系统负载。
一般负载均衡有下面几种方法。
常见的负载均衡算法包含:
1、轮询法(Round Robin)
2、加权轮询法(Weight Round Robin)
3、随机法(Random)
4、加权随机法(Weight Random)
5、平滑加权轮询法(Smooth Weight Round Robin)
6、源地址哈希法(Hash)
7、最小连接数法(Least Connections)
当然如果你能讲讲ribbon如何通过拦截器 ,拦截请求 ,再加入负载均衡算法 实现负载均衡就更好。
问题5:
Eureka集群能保证强一致性么?
解答:不能。
这其实也是一个分布式系统的CAP问题。
一个分布式系统不可能同时满足C(一致性)、A(可用性)和P(分区容错性)。由于分区容错性P在是分布式系统中必须要保证的,因此我们只能在A和C之间进行权衡。
对于Eureka集群相对来说高可用比强一致性更重要所以 选择的是AP.对于C只能是弱一致性,无法保证强一致性。与之对应的是zookeeper选择的是CP.
由于定了这种基调所以。
Eureka各个节点都是Peer to Peer 模式,不是那种master-slave模式,也就是结点之间是对等的关系,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。
而Eureka client 在向某个Eureka Server 注册时如果发现连接失败,则会自动切换至其它节点,只要有一台Eureka还在,就能保证注册服务可用(保证可用性),只不过查到的信息可能不是最新的(不保证强一致性)。
Eureka还有一种自我保护机制,如果在15分钟内超过85%的节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,此时会出现以下几种情况:
Eureka不再从注册列表中移除因为长时间没收到心跳而应该过期的服务
Eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上(即保证当前节点依然可用)
当网络稳定时,当前实例新的注册信息会被同步到其它节点中
因此, Eureka可以很好的应对因网络故障导致部分节点失去联系的情况,不会使整个注册服务瘫痪。
对于Eureka如何保持弱一致性。可以参考
SpringCloud 注册中心 Eureka 集群是怎么保持数据一致的?
考虑到Eureka 的AP特性,所以不得不提到它的缓存机制,具体参考一篇文章。
问题6:
用的什么网关?网关起什么作用。
解答:
可以答zuul,不过现很多用Spring Cloud gateway.
网关作用。
路由:动态路由,设置路由规则。请求经过网关后路由到后端具体的机器上。
性能:API高可用,负载均衡,容错机制。
安全:登陆验证,黑白名单验证。跨域等处理。
日志:可以进行日志打印。
限流:流控处理,保证服务可用,不挂掉,不雪崩。
问题7:
如何限流?限流算法相关。
解答:
常见的限流算法有:计数器、滑动窗口,令牌桶、漏桶。
计数器 设置一个计数器counter,每当一个请求过来的时候,counter就加1,如果在时间范围内大于限定的数量,就扔掉请求。
实现方法简单。用AtomicLong的incrementAndGet()。 或者redis计数器 都可以实现,但缺点明显:
有临界问题,还有会出现突刺现象导致长时间扔掉请求。比如在1分钟内 100的限流中,在第一秒 突然来了100个,那后面的59秒的请求都会被扔掉。
滑动窗口
前面说了固定窗口存在临界值问题,要解决这种临界值问题,显然只用一个窗口是解决不了问题的。假设我们仍然设定1秒内允许通过的请求是200个,但是在这里我们需要把1秒的时间分成多格,假设分成5格(格数越多,流量过渡越平滑),每格窗口的时间大小是200毫秒,每过200毫秒,就将窗口向前移动一格。为了便于理解,
图中将窗口划为5份,每个小窗口中的数字表示在这个窗口中请求数,所以通过观察上图,可知在当前时间快(200毫秒)允许通过的请求数应该是20而不是200(只要超过20就会被限流),因为我们最终统计请求数时是需要把当前窗口的值进行累加,进而得到当前请求数来判断是不是需要进行限流。
详细的限流算法可以参考下面的这篇文章。
现在一般公司都采用阿里的senitnel 组件 在网关限流,sentinel基于时间滑动窗口算法限流。
问题8:
从前端调后端接口有的时候要经过多次的后端微服务调用,如果一个请求有问题了如何快速定位服务故障点?
解答:
用 Zipkin。
zipKin 跟踪采集微服务信息的示意图如下:
问题9:
当微服务众多的时候,如何管理其配置?
解答:
在分布式系统中,由于服务数量巨多,为了方便服务配置文件统一管理,实时更新,所以需要分布式配置中心组件。在Spring Cloud中,有分布式配置中心组件spring cloud config ,它支持配置服务放在配置服务的内存中(即本地),也支持放在远程Git仓库中。在spring cloud config 组件中,分两个角色,一是config server,二是config client。
当然还可以说说其他的组件比如阿里巴巴的diamond 和 携程网的appolo,都是使用极其方便,用户也多。
问题10:
说说springCloud与dubbo的主要区别
dubbo,由于是二进制的传输,占用带宽会更少,采用长链接,比较适合于内部服务之间的通信。
springCloud是http协议传输,带宽会比较多,同时使用http协议一般会使用JSON报文,消耗会更大.适用于第三方接口提供。(但也不是强绑定,也可以使用 RPC 库,或者采用 HTTP 2.0 + 长链接方式(Fegin 可以灵活设置))
**
**
问题11:
创建线程的三种方式
解答:
1)thread, 2) runnable,3)callable 加future.
问题12:
一个线程的生命周期
解答:
线程的生命周期包含5个阶段,包括:新建、就绪、运行、阻塞、销毁。如下图:
问题13:
几种内置的线程池以及参数。
解答:
newCachedThreadPool:用来创建一个可以无限扩大的线程池,适用于服务器负载较轻,执行很多短期异步任务。
newFixedThreadPool:创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于可以预测线程数量的业务中,或者服务器负载较重,对当前线程数量进行限制。
newSingleThreadExecutor:创建一个单线程的线程池,适用于需要保证顺序执行各个任务,并且在任意时间点,不会有多个线程是活动的场景。
newScheduledThreadPool:可以延时启动,定时启动的线程池,适用于需要多个后台线程执行周期任务的场景。
newWorkStealingPool:创建一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用cpu数量的线程来并行执行,适用于大耗时的操作,可以并行来执行。
corePoolSize,maximumPoolSize,keepAliveTime,workQueue,threadFactory,handler(线程饱和策略)
问题14:
如何配置线程池里的线程数?
解答:
先看任务是cpu密集型还是IO密集型。
cpu密集型:n+1
IO密集型:2n+1
服务器性能IO优化 中发现一个估算公式:
最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
问题15:
future有什么用,有缺陷么?如何改进?改进的原理?
解答:
Future 在异步编程中经常用到,Future 表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。然而 Future 接口调用 get()方法取得处理的结果值时是阻塞性的,如果调用 Future 对象的 get()方法时,如果这个线程还没执行完成,就一直主线程main阻塞到此线程完成为止,就算和它同时进行的其它线程已经执行完了,也要等待这个耗时线程执行完才能获取结果,大大影响运行效率。那么使用多线程就没什么意义了。
鉴于 Future 的缺陷,JDK 1.8 并发包也提供了CompletionService接口可以解决这个问题,它的take()方法哪个线程先完成就先获取谁的 Futrue 对象。
也就是由future get() 转为take() 或者poll() 再get()
实现原理,下图可以看见,CompletionService 里加了一个BlockingQueue来专门存每个线程的计算结果,一旦一个线程得到结果,就写道BlockingQueue,然后take 或者poll 的时候去拿这个结果。 take() 如果队列没有值会堵塞,而poll会直接返回。
问题16:
线程池的三种队列区别:SynchronousQueue、LinkedBlockingQueue 和ArrayBlockingQueue
解答:
SynchronousQueue没有容量,是无缓冲等待队列,是一个不存储元素的阻塞队列,会直接将任务交给消费者,必须等队列中的添加元素被消费后才能继续添加新的元素。
LinkedBlockingQueue是一个无界缓存等待队列。当前执行的线程数量达到corePoolSize的数量时,剩余的元素会在阻塞队列里等待。(所以在使用此阻塞队列时maximumPoolSizes就相当于无效了),每个线程完全独立于其他线程。生产者和消费者使用独立的锁来控制数据的同步,即在高并发的情况下可以并行操作队列中的数据。
ArrayBlockingQueue
ArrayBlockingQueue是一个有界缓存等待队列,可以指定缓存队列的大小,当正在执行的线程数等于corePoolSize时,多余的元素缓存在ArrayBlockingQueue队列中等待有空闲的线程时继续执行,当ArrayBlockingQueue已满时,加入ArrayBlockingQueue失败,会开启新的线程去执行,当线程数已经达到最大的maximumPoolSizes时,再有新的元素尝试加入ArrayBlockingQueue时会报错
问题17:
ThreadLocal的实现原理
解答:
Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,即每个线程都有一个属于自己的ThreadLocalMap。
ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值。
每个线程在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
问题18:
死锁四个必要条件,怎么预防死锁?
解答:
互斥条件:一个资源每次只能被一个进程使用,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
循环等待条件: 若干进程间形成首尾相接循环等待资源的关系.
小例子:
1. public class DeadLock implements Runnable{
private int flag = 1;
private static final Object o1 = new Object();
private static final Object o2 = new Object();
public void setFlag(int flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag == 1) {
synchronized (o1) {
System.out.println(Thread.currentThread().getName() + " o1");
try {
Thread.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + " o2");
}
}
}
if (flag == 2) {
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + " o2");
try {
Thread.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println(Thread.currentThread().getName() + " o1");
}
}
}
}
public static void main(String[] args) {
System.out.println(myPow(2.0000,10));
DeadLock deadLock1 = new DeadLock();
DeadLock deadLock2 = new DeadLock();
deadLock1.setFlag(1);
Thread thread1= new Thread(deadLock1, "Thread1");
thread1.start();
deadLock2.setFlag(2);
Thread thread2= new Thread(deadLock2, "Thread2");
thread2.start(); }
避免死锁的方法。
加锁顺序(线程按顺序办事)
加锁时限 (线程请求所加上权限,超时就放弃,同时释放自己占有的锁)
死锁检测
问题19:
现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?
解答:
用JOIN 方法实现。
问题20:
CycliBarriar和CountdownLatch有什么区别?
解答:
CyclicBarrier 可以重复使用,而 CountdownLatch 不能重复使用
countDownLatch 允许一个或多个线程,等待其他一组线程完成操作,再继续执行。
CyclicBarrier: 允许一组线程相互之间等待,达到一个共同点,再继续执行。
问题21:
说说Executor,Executors,ExecutorService 区别?
解答:
Executor 接口对象能执行我们的线程任务;
Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。
ExecutorService 接口继承了Executor接口并进行了扩展,提供了更多的方法,我们能够获得任务执行的状态并且可以获取任务的返回值。
问题21:
mysql int的大小
解答:
tinyint:1,smallint:2,mediumint:3,int:4,bigint:8
问题22:
解答:
有数据表student(id, sex, name, class_id)和class(id, name),
写一个SQL查询女同学(sex=“female”)人数最多的班级的名称。
select b.name,count(*) as count from student a, class b where a.class_id = b.id and a.sex = ‘female’ group by b.name order by count desc limit 1;
问题23:
B树和B+树的区别,都什么组件使用了他们?
mongodb 使用B树,mysql使用B+树。
B树:
(1)多路,非二叉树
(2)每个节点既保存索引,又保存数据
(3)搜索时相当于二分查找
B+最核心的特点如下:
(1)多路非二叉
(2)只有叶子节点保存数据
(3)搜索时相当于二分查找
(4)增加了相邻接点的指向指针。
问题24:
讲讲 InnoDB聚集索引和普通索引,回表查询,覆盖索引。
解答:
InnoDB聚集索引的叶子节点存储行记录,因此, InnoDB必须要有,且只有一个聚集索引:
(1)如果表定义了PK,则PK就是聚集索引;
(2)如果表没有定义PK,则第一个not NULL unique列是聚集索引;
(3)否则,InnoDB会创建一个隐藏的row-id作为聚集索引;
普通索引的叶子节点存储主键值和索引字段值。
这里有一个回表查询的问题。
举个栗子。
表 student((primary key) id, sex, KEY name, class_id)
主键key 为 id,
普通索引为 name
当查询
select id,sex,name from student where name=’马福报’;
这个时候需要回表,因为 name 的叶子节点并没有存 sex 字段, 需要通过name 拿到聚集索引id字段,然后根据id字段去拿 name字段信息, 所以也叫回表。
再来看覆盖索引
select id,sex,name from student where name=‘马福报’;
对上面的语句
如果加一个联合索引。
alter table student add key idx_student_sex_name(sex,name)
这个时候再查
select id,sex,name from student where name=‘马福报’;
不需要回表了,只查一次。因为被索引覆盖了。
问题25:
innodb在RR下解决了幻读么?怎么解决的?
解答:
解决了。
1)在快照读读情况下,mysql通过mvcc来避免幻读。
一般的 select * from … where … 语句都是快照读
2) 在当前读读情况下,mysql通过next-key来避免幻读。
next-key 锁包含两部分
记录锁(行锁)
间隙锁
select * from … where … for update select * from … where … lock in share mode update … set … where … delete from. . where …
如何解决:
1) 快照读就是每一行数据中额外保存两个隐藏的列,插入这个数据行时的版本号,删除这个数据行时的版本号(可能为空)。
也就是将历史数据存一份快照,所以其他事务增加与删除数据,对于当前事务来说是不可见的。
2) 将当前数据行与数据库中的上下行之间的间隙锁定,让另一个事务下的操作等待 知道当前事务操作完成,从而保证此范围内读取的数据是一致的。
这里例子由于篇幅原因我就不列举了。有兴趣的可以参考下面的链接。
问题26:
事务特性和事务隔离级别
解答:
事务具有原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)四个特性,简称 ACID
隔离级别。
读未提交(READ UNCOMMITTED)
读提交 (READ COMMITTED)
可重复读 (REPEATABLE READ)
串行化 (SERIALIZABLE)
问题27:
讲讲mysql 的redo log 和 undo log 日志。
解答:
1)redo log 是重写日志,提供前滚操作,Redo 记录某 数据块 被修改 后 的值,可以用来恢复未写入 data file 的已成功事务更新的数据,Redo Log 保证事务的持久性
2)undo log 是回退日志,提供回滚 操作,Undo 记录某 数据 被修改 前 的值,可以用来在事务失败时进行 rollback;Undo Log 保证事务的原子性(在 InnoDB 引擎中,还用 Undo Log 来实现 MVCC)
3)redo log 通常是物理日志,记录的是 数据页的物理修改,而不是某一行或某几行修改成怎样怎样,它用来恢复提交后的物理数据页(恢复数据页,且只能恢复到最后一次提交的位置)。
4) undo log 用来回滚行记录到某个版本。undo log 一般是逻辑日志,根据每行记录进行记录。
redo是数据库日志,主要是对页面(PAGE)的修改信息;undo是事务日志,记录的是对记录(TUPLE)的变更,是更高一层的抽象。崩溃发生前一刻,许多对数据库的写入还没有被持久化了页面上,这时就需要靠redo恢复到崩溃发生前的瞬间。数据库恢复完成后,数据库可能发现有一些正在运行中的事务,事务的上下文都已经丢失了,要对这些事务回滚,这时就需要undo来恢复记录原来的值。
问题二、undo是一条记录被更新之前写入的,“写undo”这个操作,本身也是对页面的修改,也会有对应的redo。而redo每一次写页面都会记。
问题三、页面是分槽页(slot-page)结构,整体上看存储结构就是页面->区->段三层结构。redo是物理逻辑日志,undo是逻辑日志,概念上是这样的。undo也作为MVCC的版本链,一个undo就是前版本和后版本之前的差异。
问题28:
如下图 高度为3的B+树大约会存多少条记录?
解答:
两千万多条。
大家都了解B+树是存在硬盘上的,读取硬盘上的文件是通过读取一个个的磁盘块来实现的。每读一个磁盘块就有一次 IO.
磁盘块的大小 根据文件系统的不同而不同,对于 ext3 或者ext4 文件系统( df -T
命令查看文件系统类型),可以通过命令
tune2fs -l /dev/vda1 | grep "Block size
得到磁盘块的大小为4096(4K) ,对于innodb来说一页大小就包含4个磁盘块,
也就是说上面图中的磁盘块1,磁盘块2 ,磁盘块3 等等实际上是一个页,每个包含四个磁盘块 也就是 16K大小。
在计算高度为3的B+树之前,为了简单起见,我们可以看看高度如果为2的B+树会存多少条记录?当为2的时候,我们可以看见能存多少行 实际上是等于
一个非叶子节点(也就是一页) 能存多少指针数量
乘以
这个指针指向的叶子节点(一页) 中包含的数据条数。
由于非叶子节点存的是健值 +指针 ,我们假设主键ID为bigint类型,长度为8字节,
而指针大小在InnoDB中是6字节,
一共是14字节。那么一页中能存16384/14 约等于 1170个指针。
每个指针指向的叶子节点中包含的数据条数,这个也可以大体判断:因为叶子节点是一页, 一页的大小为16384,16k, 一般数据库表中的一行大约就1k,多数情况下,当然也不排除一个表有几十个字段,有些字段还是text类型,但对于大多数的表来说一般1k够用了。所以这个数就为16k/1k = 16
所以对于高度为2的B+树大约会存1170*16 = 18720 条数据。
对于高度为3的B+树 ,由于有两层非叶子节点,所以存储数量就会变为1170117016 = 21902400 大概2千万左右。
问题29:
解答:
介绍下用数据库实现秒杀中的乐观锁。
比如一个秒杀活动,扣库存。
解答:
加一个版本号 version,类似下面的小函数。
goods(id,stock,version)
secKill(int num,int buy) {
if (sql(update goods set stock =stock -#{buy},version=#{version}+1 where id = #{id} and version= #{version})>0) {
return “扣掉库存";
}
//重试,递归调用。
thread.sleep(1); //随机休眠,削峰填谷。
secKill(num,buy)
}
(四)JVM,java se,http相关
问题30:
能说说synchronized的锁升级吗?
解答:
jdk1.6以后对synchronized的锁进行了优化,锁的级别从低到高逐步升级:无锁->偏向锁->轻量级锁->重量级锁。但是锁的升级是单向的,只能从低到高升级,不会降级。JVM中对象在内存中的布局分为三块区域:对象头、实例变量和填充数据。对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。
如果一个线程获得了锁,锁就进入偏向模式,此时Mark Word的结构变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,这样就省去了大量有关锁申请的操作,提高了程序的性能。
在锁竞争激烈的场合,每次申请锁的线程都是不相同的,偏向锁就失效了,偏向锁失败后升级为轻量级锁。轻量级锁是相对于使用操作系统互斥量来实现的传统锁而言的,它所适应的场景是线程交替执行同步块,如果存在同一时间访问同一锁的情况,就会膨胀为重量级锁。
轻量级锁失败后,还会进行自适应自旋。大多数情况下线程持有锁的时间都不会太长,如果操作系统层面直接挂起,线程之间的切换时需要从用户态转换到核心态,这个状态的转换需要比较长的时间。自旋锁假设在当前的线程很快可以获得锁,虚拟机会让当前线程做50个循环或100循环空循环。在经过若干次循环后,如果得到锁就顺利进入临界区。如果还不能获得锁就会将线程挂起,升级为重量级锁了。注意,JVM还会自动调整自旋次数,这次成功了,下次自旋的次数会多;如果失败了,下次自旋的次数就会减少。
问题31:
java 系统运行一段时间,出现周期性的卡顿,如何破?
解答:
jvm虚拟机中将堆分为新生代和老年代,持久代。
*(备注:堆又划分为:年轻代、老年代、永久代(JDK1.7)/元空间(JDK1.8),元空间与永久代的区别在于:永久代使用的是虚拟机内存,元空间则采用本地内存*。)*
而当new了一个对象以后,由于是强引用,这个对象在经历minorGC的时候,年龄会变大,在达到参数MaxTenuringThreshold的值的时候,就会进入到老年代中。
一直进行这个过程,那么老年代中的活着的对象就会越来越多,最后老年代满了以后发生fullGC,而fullGC是很耗时间的,尤其是当老年代越大,那么fullGC就越耗时间。这个系统周期性出现这个问题的就是由于对象周期性地把老年代填充满了,然后jvm虚拟机周期性地去进行fullGC去回收垃圾,当回收的时候系统性能就下降,当回收结束时系统性能就上升。
那么如何解决呢?通过调整新生代与老年代的比例(该值可以通过参数 –XX:NewRatio 来指定),调低老年代占的内存大小,这样老年代很快就满了,就会提前进行fullGC,直到调整到发生fullGC时候对于系统性能影响不大的时候(用户察觉不出来),那么调优结束。
问题32:
如果没有和GCRoot有任何引用的情况下,这时候GC认为就是不可达对象,也就是垃圾对象。必须和GCRoot有引用关系才认为是可达对象。
其中哪些可以作为GCRoot的对象?
解答:
1)有虚拟机栈(栈帧中的局部变量区,也叫做局部表量表)中引用的对象,
2)方法区的类属性所引用的对象,
3)方法区中常量所引用的对象,
4)本地方法栈中JNI(Native方法)引用的对象。
问题33:
JVM 内存模型。
解答:
如下图所示:
1、堆
堆是Java虚拟机管理的最大一块内存区域,存放所有对象实例和数组,因为堆存放的对象是线程共享的,所以多线程的时候需要同步机制;堆又划分为:年轻代、老年代、永久代(JDK1.7)/元空间(JDK1.8),元空间与永久代的区别在于:永久代使用的是虚拟机内存,元空间则采用本地内存。
2、虚拟机栈
虚拟机栈描述的是线程进栈出栈的过程,线程结束内存自动释放,它用来存储当前线程运行方法所需要的数据、指令、返回地址(即局部变量和正在调用的方法),方法被调用时会在栈中开辟一块叫栈帧的空间,方法运行在栈帧空间中。
3、本地方法栈
本地方法栈与虚拟机栈的作用十分相似,区别是虚拟机栈执行的是Java方法服务,而本地方法栈则为虚拟机使用native方法服务,可能底层调用的c或者c++方法。
4、方法区
方法区同堆一样,是所有线程共享的内存区域,又被称为非堆,用于存储已被虚拟机加载的类信息、常量、静态变量等。
5、程序计数器
程序计数器是一块很小的内存空间,它是线程私有的,可以认作是当前线程的行号指示器。
问题34:
hashMap面试系列。
比如1.7.1.8的区别 之类的 一堆面试题,建议参考。
问题35:
HashMap的哈希函数怎么设计的吗?
回答:
hash函数是先拿到 key 的hashcode,是一个32位的int值,然后让hashcode的高16位和低16位进行异或操作。
问题36:
说说tcp三次握手的过程?
解答:
1,client想要向server发送数据,请求连接。这时client想服务器发送一个数据包,其中同步位(SYN)被置为1,表明client申请TCP连接,序号为j。
2,当server接收到了来自client的数据包时,解析发现同步位为1,便知道client是想要简历TCP连接,于是将当前client的IP、端口之类的加入未连接队列中,并向client回复接受连接请求,想client发送数据包,其中同步位为1,并附带确认位ACK=j+1,表明server已经准备好分配资源了,并向client发起连接请求,请求client为建立TCP连接而分配资源。
3,client向server回复一个ACK,并分配资源建立连接。server收到这个确认时也分配资源进行连接的建立。
问题37:
为什么需要第三次握手?
解答:
如果没有第三次握手,可能会出现如下情况:如果只有两次握手,那么server收到了client的SYN=1的请求连接数据包之后,便会分配资源并且向client发送一个确认位ACK回复数据包。那么,如果在client与server建立连接的过程中,由于网络不顺畅等原因造成的通信链路中存在着残留数据包,即client向server发送的请求建立连接的数据包由于数据链路的拥塞或者质量不佳导致该连接请求数据包仍然在网络的链路中,这些残留数据包会造成如下危害危害:当client与server建立连接,数据发送完毕并且关闭TCP连接之后,如果链路中的残留数据包才到达server,那么server就会认为client重新发送了一次连接申请,便会回复ACK包并且分配资源。并且一直等待client发送数据,这就会造成server的资源浪费。
问题38:
第三次握手失败了怎么办?
解答:
当client与server的第三次握手失败了之后,即client发送至server的确认建立连接报文段未能到达server,server在等待client回复ACK的过程中超时了,那么server会向client发送一个RTS报文段并进入关闭状态,即:并不等待client第三次握手的ACK包重传,直接关闭连接请求,这主要是为了防止泛洪攻击,即坏人伪造许多IP向server发送连接请求,从而将server的未连接队列塞满,浪费server的资源。
问题39:
三次握手有什么缺陷可以被黑客利用,用来对服务器进行攻击?
解答:
黑客仿造IP大量的向server发送TCP连接请求报文包,从而将server的半连接队列(上文所说的未连接队列,即server收到连接请求SYN之后将client加入半连接队列中)占满,从而使得server拒绝其他正常的连接请求。即拒绝服务攻击。
问题40:
说说 判断http是否传完的标记?
解答:
1) http协议有正文大小说明的
content-length
2) 分块传输chunked的话 读到0\r\n\r\n 就是读完了
http响应内容比较大的话,会分成多个tcp segment 发送,不是最后一个segment
的话, tcp的payload不会有http header字段,如果是最后一个tcp segment 的话,
就会有http header 字段,同时, 数据的最后会有 “0\r\n\r\n” 这个东西,这个东西
就表示数据都发送完了。
问题41:
为什么String 和 Integer适合做hashmap的key?
解答
因为string是final的 。 不可变性保证了hash的唯一性
Integer内部重写了hashcode 与 equals方法
问题42:
Java对象的回收算法
解答:
分代收集法:它根据对象的生存周期,将堆分为新生代和老年代。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担保,所以可以使用标记整理或者标记清除。
标记清除法:将需要回收的对象标记出来,批量清除。缺点是效率低,产生碎片。
标记整理法:将需要回收的对象标记出来,移动到一端再清除,避免了碎片的产生。
复制法:将新生代内存划分为8:1:1三部分,较大的叫Eden区,其余是两块较小的Survior区。每次都会优先使用Eden区,若Eden区满,就将对象复制到第二块内存区上,然后清除Eden区,如果此时存活的对象太多,以至于Survivor不够时,会将这些对象通过分配担保机制复制到老年代中。
问题43:
CMS 与G1的区别
解答:
区别一: 使用范围不一样
CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用
G1收集器收集范围是老年代和新生代。不需要结合其他收集器使用
区别二: STW的时间
CMS收集器以最小的停顿时间为目标的收集器。
G1收集器可预测垃圾回收的停顿时间(建立可预测的停顿时间模型)
区别三: 垃圾碎片
CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片
G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。
区别四: 垃圾回收的过程不一样
CMS收集器 G1收集器
初始标记 1.初始标记
并发标记 2. 并发标记
重新标记 3. 最终标记
并发清除 4. 筛选回收
(五)组件综合相关。
问题44:
MySQL和Redis的缓存如何解决数据一致性
流程如下图所示:
(1)更新数据库数据
(2)数据库会将操作信息写入binlog日志当中
(3)订阅程序提取出所需要的数据以及key
(4)另起一段非业务代码,获得该信息
(5)尝试删除缓存操作,发现删除失败
(6)将这些信息发送至消息队列
(7)重新从消息队列中获得该数据,重试操作。
这里可以结合使用canal(阿里的一款开源框架),通过该框架可以对MySQL的binlog进行订阅.
至于消息队列可以考虑 kafka,RocketMq等等。
问题45:
Kafka 为什么那么快?
解答:
首先,Kafka 作为一个消息系统,通过 topic 的方式来管理 message,把这些消息都顺序写入磁盘文件来提高写入速度,其实这些消息并没有实时写入磁盘,而是充分利用了现代操作系统分页存储来利用内存提高IO效率。
其次,它的工作原理是直接利用操作系统的 page 来实现文件到物理内存的直接映射。完成映射之后你对物理内存的操作会被同步到硬盘上(操作系统在适当的时候)。
再次,Kafka 也基于 sendfile 实现 zero copy,简化网络上和两个本地文件之间的数据传输,sendfile 的引入不仅减少了数据复制,还减少了上下文切换。
最后,Kafka 为了能网络上提高传输数据的效率,message 也支持压缩。在很多情况下,系统的瓶颈不是 CPU 或磁盘,而是网络 IO,对于需要在广域网上的数据中心之间发送消息的数据流水线尤其如此。
问题46:
如何设计一个类似RPC框架?
解答:
在一个典型 RPC 的使用场景中,包含了服务发现、负载、容错、网络传输、序列化等组件,其中“RPC 协议”就指明了程序如何进行网络传输和序列化。如下图。
就跟我前面提到的一样,从大的方面来说就是A服务需要调B服务的 某个接口。
与前面不一样的地方是 A服务不是通过http来调。而是通过类似 调本地的函数 的方式 来调用。能通过这种方式来调用 是因为把逻辑做了封装。
举个栗子
B服务 里面的类实现如下。
public interface IRpcService {
String method1(String name);
}
public class RpcService implements IRpcService {
public String method1(String name) {
System.out.println("name "+" 是外星人");
}
}
A 服务里面想调用B服务里的RpcService 的method1方法实现,现在在A服务里面 只有一个
接口描述如下。
public interface IRpcService {
String method1(String name);
}
怎么做?
先我们new 一个下面的数据结构 RpcRequest 来 存 这个 接口的名称,方法名称,参数,参数类型。
public class RpcRequest {
// 类名
private String className;
// 方法名
private String methodName;
// 请求参数的数据类型
private Class<?>[] parameterTypes;
// 请求的参数
private Object[] parameters;
}
然后我们需要把 这个RpcRequest 通过协议序列化 为字节流,因为当发送到服务端的时候我们好反序列化这个字节流,得到RpcRequest 里的方法,然后反射调用,执行类里的方法得到结果,在发送会A服务端。*
这里的协议有很多种比如jdk.
protobuf,kyro,Hessian, 还有我们在微服务用的JSON.最常用的fastjson 。
都可以。
由于netty 支持自定义 coder 。所以只需要实现 ByteToMessageDecoder 和 MessageToByteEncoder 两个接口。就解决了序列化的问题。加上netty 天然适合网络收发。很多开源组件的网络层都是基于netty来开发。
可以看出我们需要一个函数来统一处理逻辑。
1.建rpcRequst,填充里面的内容。
2.序列化成字节流发送到B端。
3.接收返回的结果。
这些逻辑对A服务想调B服务里所有的类都是适用的,所以这个时候我们可以用动态代理,把逻辑进行统一的封装,到时再调每一个接口的方法的时候都会走同样的逻辑。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
RpcRequest request = new RpcRequest();
String requestId = UUID.randomUUID().toString();
String className = method.getDeclaringClass().getName();
String methodName = method.getName();
Class<?>[] parameterTypes = method.getParameterTypes();
request.setRequestId(requestId);
request.setClassName(className);
request.setMethodName(methodName);
request.setParameterTypes(parameterTypes);
request.setParameters(args);
return Transporters.send(request).getResult();
}
具体的例子的阐述可以参考下面的文档,我这里只解释了 如何运用动态代理来实现逻辑的统一封装,后面的server端流程可以参考下面文档。
对于容错机制可以参考下。
dubbo 容错机制:
1) 失败自动切换机制是由 FailoverClusterInvoker 类控制
失败了重试,根据配置次数重试。
2)失败自动恢复会在调用失败后,返回一个空结果给服务消费者。并通过定时任务对失败的调用进行重试,适合执行消息通知等操作。
3)快速失败只会进行一次调用,失败后立即抛出异常。适用于幂等操作,比如新增记录。实现逻辑如下:
4)失败安全是指,当调用过程中出现异常时,仅会打印异常,而不会抛出异常。适用于写入审计日志等操作。
5)并行调用多个服务提供者会在运行时通过线程池创建多个线程,并发调用多个服务提供者
6)Broadcast Cluster
BroadcastClusterInvoker 会逐个调用每个服务提供者,如果其中一台报错,再循环调用结束后,BroadcastClusterInvoker 会抛出异常。该类通常用于通知所有提供者更新缓存或日志等本地资源信息。
问题 46.
讲讲秒杀场景 如何应付。
解答:
网上有很多答案,这里长话短说。
首先要隔离,秒杀业务和其他业务做到数据和部署隔离,不要影响正常网站访问。
其次秒杀的场景从前端包括cdn,从后端主要通过缓存和异步来抗高并发的流量,
避免大流量穿透db。
这个时候 马福报的面试官,估计会追问怎么通过缓存和异步来扛呢?
答:
举个例子,比如有5个商品要秒杀,每个商品1000件。
首先在秒杀 之前 把 库存写到缓存里
比如商品A .
String productId =“secondskill_6535539962765541376”;
jedisConnector.set(productId,“1000”);
当然手动写到redis 也可以。
然后用redis去扛并发流量。
但这里有一个问题,怎么判断用户抢到了呢?
根据剩余库存大小,如果大于等于0 ,表示抢到了。
这里需要判断redis 库存和扣库存的一个原子操作, 需要用lua脚本来保证原子性。
可以写一个类似下面的lua脚本,jedis或者jedisCluster直接调用。。
StringBuilder sb = new StringBuilder();
sb.append(" local stock = tonumber(redis.call('get', KEYS[1]));");
sb.append(" local num = tonumber(ARGV[1]);");
sb.append(" if (stock >= num) then");
sb.append(" return redis.call('incrby', KEYS[1], 0 - num);");
sb.append(" end;");
sb.append(" return -1;");
luastr = sb.toString.
每次接口执行。
List<String> keys = new ArrayList<>();
keys.add(productId);
List<String> args = new ArrayList<>();
args.add(Integer.toString(1)); //每次可以抢几个,这里设为一个。
Object leftCount = jedisConnector.eval(luastr,keys,args);
if (leftCount!=null && Integer.valueOf(leftCount.toString())>=0) {
log.error("拿到库存。");
//发相关信息到队列 rocketmq 或者kafka.
// 解耦,在消费队列里可以把订单写到 db 然后 发消息通知用户已经抢到的一些比较耗时的逻辑。
}
47.
由于篇幅和时间关系,就写到这里,还有一些题目比如redis相关题目,Spring,SpringBoot 的相关题目,设计模式等等,当然还有一些算法题,一般每一轮面试会有一两道算法题,现在已经是互联网公司的标配,有时间我再整理下写写。最后讲讲面试的方法。
(六)面试成功的方法。
在我做程序员的职业生涯里,有幸面试了不少的程序员,当然我自己也参加过不少的面试。总接下成功的面试者要做到以下几点。
有一说一。 这点很重要,诚实是一个好的品质,遇到不会的及时告知面试官自己不会,别胡诌,说错了会给人很不好的印象。
守正出奇。这里的“正”指的是自己做的项目,一定要对自己的项目烂熟于心。出奇是只要对项目中比较流行的技术点,技术实现要理解,要会延展,要深入细节,所以读几个比较流行的开源代码这个时候就比较重要了,读读Spring框架代码,mybatis,kafka等开源代码 也是能出奇制胜的几个法宝,关键时候可以为自己震场子用。
单点突破, 一定要在自己的项目中提取几个点,也就是提取最有价值的地方,最有技术含量的地方,这个往往能决定你水平的上限。但有的人会说我平时就做的写CRUD之类的,怎么提取?这个需要你平时多思考,不要为做项目而做项目,要多提炼,多了解行业最新的实现方式,对比你的实现。 只要你多想,无论在业务上还是在技术上都可以找到核心点,然后突破。到时在面试官面前就会娓娓道来。
灵活引导, 在面试过程中要善于灵活调动面试官,引导面试官聚焦到你熟悉的领域,怎么引导? 第一 需要在简历中只写你最擅长的部分,别洋洋洒洒一股脑全写,到时给自己挖坑。第二在面试中有意无意的引导面试官 往你最擅长的技术点上靠。
知己知彼百战不殆,第一要了解自己,了解自己的技术弱点,技术盲区,即时查漏补缺。第二 要了解面试的公司,可以提前上网看看面经,大体了解下面试的流程 和一些常问的技术套路,做到心中有数。面试虽然有的时候看似一个小概率事件,但总体上来说还是一个可预测的事件,只要下到足够的功夫。
带着必胜的信心参加面试吧,每一个选择做程序员的都或多或少,怀有改变世界的梦想,面对BAT 或者TMD的面试官 一定要有用技术实力碾压他们的决心和信念,没什么大不了的,他们只不过比你早进几天公司而已。
最后希望大家都拿到满意的offer.
网友评论