- 50分钟左右
- 自我介绍
- Java多态,重载、重写,static方法重写
static 方法不能被重写
- 线程池
线程池,本质上是一种对象池,用于管理线程资源。
在任务执行前,需要从线程池中拿出线程来执行。
在任务执行完成之后,需要把线程放回线程池。
通过线程的这种反复利用机制,可以有效地避免直接创建线程所带来的坏处。
我们先来看看线程池带来了哪些好处。
- 降低资源的消耗。线程本身是一种资源,创建和销毁线程会有CPU开销;创建的线程也会占用一定的内存。
- 提高任务执行的响应速度。任务执行时,可以不必等到线程创建完之后再执行。
- 提高线程的可管理性。线程不能无限制地创建,需要进行统一的分配、调优和监控。
通过上图,我们看到了线程池的主要处理流程。我们的关注点在于,任务提交之后是怎么执行的。大致如下:
- 判断核心线程池是否已满,如果不是,则创建线程执行任务
- 如果核心线程池满了,判断队列是否满了,如果队列没满,将任务放在队列中
- 如果队列满了,则判断线程池是否已满,如果没满,创建线程执行任务
- 如果线程池也满了,则按照拒绝策略对任务进行处理
在jdk里面,我们可以将处理流程描述得更清楚一点。来看看ThreadPoolExecutor
的处理流程。
我们将概念做一下映射。
corePool
-> 核心线程池maximumPool
-> 线程池BlockQueue
-> 队列RejectedExecutionHandler
-> 拒绝策略
Executors
Executors
是一个线程池工厂,提供了很多的工厂方法,我们来看看它大概能创建哪些线程池。
// 创建单一线程的线程池
public static ExecutorService newSingleThreadExecutor();
// 创建固定数量的线程池
public static ExecutorService newFixedThreadPool(int nThreads);
// 创建带缓存的线程池
public static ExecutorService newCachedThreadPool();
// 创建定时调度的线程池
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize);
// 创建流式(fork-join)线程池
public static ExecutorService newWorkStealingPool();
入门级例子
为了更直观地理解线程池,我们通过一个例子来宏观地了解一下线程池用法。
public class ThreadPoolTest {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
executor.submit(() -> {
System.out.println("thread id is: " + Thread.currentThread().getId());
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
}
手动创建线程池
理论上,我们可以通过Executors
来创建线程池,这种方式非常简单。但正是因为简单,所以限制了线程池的功能。比如:无长度限制的队列,可能因为任务堆积导致OOM,这是非常严重的bug,应尽可能地避免。怎么避免?归根结底,还是需要我们通过更底层的方式来创建线程池。
抛开定时调度的线程池不管,我们看看ThreadPoolExecutor
。它提供了好几个构造方法,但是最底层的构造方法却只有一个。那么,我们就从这个构造方法着手分析。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler);
这个构造方法有7个参数,我们逐一来进行分析。
corePoolSize
,线程池中的核心线程数maximumPoolSize
,线程池中的最大线程数keepAliveTime
,空闲时间,当线程池数量超过核心线程数时,多余的空闲线程存活的时间,即:这些线程多久被销毁。unit
,空闲时间的单位,可以是毫秒、秒、分钟、小时和天,等等workQueue
,等待队列,线程池中的线程数超过核心线程数时,任务将放在等待队列,它是一个BlockingQueue
类型的对象threadFactory
,线程工厂,我们可以使用它来创建一个线程handler
,拒绝策略,当线程池和等待队列都满了之后,需要通过该对象的回调函数进行回调处理
这些参数里面,基本类型的参数都比较简单,我们不做进一步的分析。我们更关心的是workQueue
、threadFactory
和handler
,接下来我们将进一步分析。
3. 拒绝策略-handler
所谓拒绝策略,就是当线程池满了、队列也满了的时候,我们对任务采取的措施。或者丢弃、或者执行、或者其他...
jdk自带4种拒绝策略,我们来看看。
CallerRunsPolicy
// 在调用者线程执行AbortPolicy
// 直接抛出RejectedExecutionException
异常DiscardPolicy
// 任务直接丢弃,不做任何处理DiscardOldestPolicy
// 丢弃队列里最旧的那个任务,再尝试执行当前任务
这四种策略各有优劣,比较常用的是DiscardPolicy
,但是这种策略有一个弊端就是任务执行的轨迹不会被记录下来。所以,我们往往需要实现自定义的拒绝策略, 通过实现RejectedExecutionHandler
接口的方式。
submit()
用于提交一个需要返回果的任务。该方法返回一个Future
对象,通过调用这个对象的get()
方法,我们就能获得返回结果。get()
方法会一直阻塞,直到返回结果返回。另外,我们也可以使用它的重载方法get(long timeout, TimeUnit unit)
,这个方法也会阻塞,但是在超时时间内仍然没有返回结果时,将抛出异常TimeoutException
。
如何正确配置线程池的参数
前面我们讲到了手动创建线程池涉及到的几个参数,那么我们要如何设置这些参数才算是正确的应用呢?实际上,需要根据任务的特性来分析。
- 任务的性质:CPU密集型、IO密集型和混杂型
- 任务的优先级:高中低
- 任务执行的时间:长中短
- 任务的依赖性:是否依赖数据库或者其他系统资源
不同的性质的任务,我们采取的配置将有所不同。在《Java并发编程实践》中有相应的计算公式。
通常来说,如果任务属于CPU密集型,那么我们可以将线程池数量设置成CPU的个数,以减少线程切换带来的开销。如果任务属于IO密集型,我们可以将线程池数量设置得更多一些,比如CPU个数*2。
PS:我们可以通过
Runtime.getRuntime().availableProcessors()
来获取CPU的个数。
线程池监控
如果系统中大量用到了线程池,那么我们有必要对线程池进行监控。利用监控,我们能在问题出现前提前感知到,也可以根据监控信息来定位可能出现的问题。
那么我们可以监控哪些信息?又有哪些方法可用于我们的扩展支持呢?
首先,ThreadPoolExecutor
自带了一些方法。
long getTaskCount()
,获取已经执行或正在执行的任务数long getCompletedTaskCount()
,获取已经执行的任务数int getLargestPoolSize()
,获取线程池曾经创建过的最大线程数,根据这个参数,我们可以知道线程池是否满过int getPoolSize()
,获取线程池线程数int getActiveCount()
,获取活跃线程数(正在执行任务的线程数)
其次,ThreadPoolExecutor
留给我们自行处理的方法有3个,它在ThreadPoolExecutor
中为空实现(也就是什么都不做)。
protected void beforeExecute(Thread t, Runnable r)
// 任务执行前被调用protected void afterExecute(Runnable r, Throwable t)
// 任务执行后被调用protected void terminated()
// 线程池结束后被调用
总结
通过这篇文章,我们已经对Java线程池有了一个比较全面和深入的理解。根据前人的经验,我们需要注意下面几点:
- 尽量使用手动的方式创建线程池,避免使用
Executors
工厂类 - 根据场景,合理设置线程池的各个参数,包括线程池数量、队列、线程工厂和拒绝策略
- 在调线程池
submit()
方法的时候,一定要尽量避免任务执行异常被吞掉的问题
- lambda表达式,这个不会
在Java语法层面Lambda表达式允许函数作为一个方法的参数(函数作为参数传递到方法中),或者把代码看成数据。Lambda表达式用于简化Java中接口式的匿名内部类,被称为函数式接口的概念。函数式接口就是一个只具有一个抽象方法的普通接口,像这样的接口就可以使用Lambda表达式来简化代码的编写。
- Java里的Error和Exception
Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java中,错误通过Error的子类描述。
Exception(异常):是程序本身可以处理的异常。Exception 类有一个重要的子类 RuntimeException。RuntimeException 类及其子类表示“JVM 常用操作”引发的错误。例如,若试图使用空值对象引用、除数为零或数组越界,则分别引发运行时异常(NullPointerException、ArithmeticException)和 ArrayIndexOutOfBoundException。
Exception(异常)分两大类:运行时异常和非运行时异常(编译异常)。程序中应当尽可能去处理这些异常。
1.运行时异常:都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。
2.非运行时异常 (编译异常):是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。
三、 异常处理的机制
在 Java 应用程序中,异常处理机制为:抛出异常,捕捉异常。
1. 抛出异常:当一个方法出现错误引发异常时,方法创建异常对象并交付运行时系统,异常对象中包含了异常类型和异常出现时的程序状态等异常信息。运行时系统负责寻找处置异常的代码并执行。详细信息请查看《简述throw-throws异常抛出》。
2. 捕获异常:在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器(exception handler)。潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适 的异常处理器。运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。当运行时系统遍历调用栈而未找到合适 的异常处理器,则运行时系统终止。同时,意味着Java程序的终止。详细信息请查看《Java:简述try-catch-finally异常捕获》。
- 如何中止一个正在运行的线程interrupt
- wait和notify
wait 是线程进去WAITING状态
notify唤醒WAITING状态的线程
- 可重入锁和不可重入锁、
已经持有锁的线程可以再次进去加锁的代码块就是可重入,反之就是不可重入
- 实习相关
- 算法:towsum,找出给定数组里所有和为target两个数的组合,数字不能重用
- 智力题:两根不均匀的香,1小时烧完,怎么得到15分钟
- 智力题:门外3个开关,门内三盏灯,门外看不到门内的状况,只能进去一次,怎么确定所以开关对应哪个灯
1.开始是自我介绍,问项目,问难点在哪里,你的成就感的项目
2.堆排序
3.GC回收器
4.进程和线程
5.锁
6.事务的锁
7.主键查询需要几次
8.聚集索引和非聚集索引
9.根据实习前端的经验问了一些cookie和session还有request前后传递参数的方式:
request获得参数的两种方式get和post
10.请求的格式
拼多多 社招 java 一面面经
1、简单做一下自我介绍把,为什么这么快就想换工作。。。。你说一下你简历里的这个XX项目。
2、看你在项目中用了redis,我们先聊聊redis吧,常用的数据结构有哪几种,在你的项目中用过哪几种,以及在业务中使用的场景,redis的hash怎么实现的,rehash过程讲一下和JavaHashMap的rehash有什么区别?redis cluster有没有了解过,怎么做到高可用的?redis集群和哨兵机制有什么区别?redis的持久化机制了解吗?你们在项目中是怎么做持久化的?遇到过redis的hotkey吗?怎么处理的?redis是单线程的吗?单线程为什么还这么快?讲一讲redis的内存模型?
3.我看你还用了RabbitMQ,简单说一下RabbitMQ的工作原理?如何保证消息的顺序执行?Kafka了解吗?和RabbitMQ有什么区别?你为啥不用kafka来做,当时怎么考虑的?
4、我看你简历里说熟悉计算机网络,来聊一聊计算机网络吧。了不了解tcp/udp,简单说下两者的区别?tcp为什么要三次握手和四次挥手?两次握手可以不?会有什么问题?
tcp怎么保证有序传输的,讲下tcp的快速重传和拥塞机制,知不知道time_wait状态,这个状态出现在什么地方,有什么用?
5、http与https有啥区别?https是怎么做到安全的?
6、有没有了解过协程?说下协程和线程的区别?用过哪些linux命令?如查看内存使用、网络情况?
free -h查看内存,协程在用户态,没有上下文切换开销
7、你了解哪些设计模式啊。挑一个熟悉的讲讲?(除了单例模式)在项目中有用过设计模式吗?讲讲你怎么用的?简单说一下代理模式和装饰器模式?
8、你们数据库有没有用到分库分表,怎么做的?分库分表以后全局id怎么生成的?
9、索引的常见实现方式有哪些,有哪些区别?MySQL的存储引擎有哪些,有哪些区别?InnoDB使用的是什么方式实现索引,怎么实现的?说下聚簇索引和非聚簇索引的区别?
索引的常见实现方式有Hash索引,B树索引,B+树索引,MySQL的存储引擎有InnoDB和MySIAM, InnoDB使用的是B+树索引,索引和真实数据存储到一起就是聚集索引,否之就是非聚集索引。
10、看你简历提到了raft算法,讲下raft算法的基本流程?raft算法里面如果出现脑裂怎么处理?有没有了解过paxos和zookeeper的zab算法,他们之前有啥区别?
11、聊聊java基础吧,如果我是想一个人的姓名一样就认为他们equal,能现场写下我们怎么重写equals吗?如果两个对象,一个是cat,一个是dog,我们认为他们的name属性一样就一样,怎么重写equals
12,还有点时间,写个题吧
leetcode406. 根据身高重建队列
假设有打乱顺序的一群人站成一个队列。 每个人由一个整数对(h, k)表示,其中h是这个人的身高,k是排在这个人前面且身高大于或等于h的人数。 编写一个算法来重建这个队列。
注意:
总人数少于1100人。
示例
输入:
[[7,0], [4,4], [7,1], [5,0], [6,1], [5,2]]
输出:
[[5,0], [7,0], [5,2], [6,1], [4,4], [7,1]]
一面 30min
1 2 3 4 5 6 7 8 9 |
|
二面 20 min
1 2 3 4 5 6 7 8 9 10 11 |
|
- Java基础。自动拆装箱如何实现,String,StringBuffer,StringBuilder的异同以及各自的实现。
- JVM基础。JVM的内存模型,常见的垃圾回收算法。
标记清楚
复制
标记整理
- 事务ACID,编程时如何保证事务,分布式情况下如何保证事务。
- 由于分布式相关场景我没有接触过,因此面试官一直诱导我去设计实现一个分布式事务。
- 数据库乐观锁和悲观锁。如何实现一个乐观锁。
- 消息队列使用场景,Kafka的架构以及原理。
- 什么是restful api,和rpc调用有什么区别。
- 单例的几种写法。volatile关键字有什么作用。
以上就是电话面试的大体问题,面试完之后,又发给我三道算法题目,要求我一小时内完成,下面是三道算法题:
- 翻转一个long类型数字。例如输入123456L,输出654321L。- Leetcode翻转integer的变种。考察能否正确处理溢出的情况。
- 输入一个double,要求返回与它最接近的.49或.99的数字。例如12.77返回12.99,11.02返回10.99,12.61返回12.49。
- 有三个线程ABC分别向一个数组中写入a,l,i,要求最终的写入结果形如alialiali...写入次数由A线程决定。
这三道题目做的还比较顺利,第二天面试官又联系我阐述一下第一题和第三题的思路,然后通知我可以参加下一轮了。
二面(电话面试一小时)
二面主要考察了一些开放式的问题。
- 首先还是自我介绍。主要是工作后的经历。介绍一下工作一年所在team的产品,我承担了什么职责。
- 开放式问题。如何设计一个rpc框架。
- 开放式问题。如何设计一个服务注册中心。
- 集合类源码。HashMap是如何实现的,扩容的过程,为什么要扩容为2倍。HashMap中的链表替换为数组可以吗?时间复杂度相同吗?
- 集合类源码。线程安全的HashMap是什么?(HashTable和ConcurrentHashMap)ConcurrentHashMap是如何实现的?(Java7分段锁和Java8的CAS+Lock)和HashTable相比有什么优势?
- 红黑树的结构,时间复杂度是多少,如何计算的
- 什么是CAS操作,如何实现一个自定义锁
- 数据库设计。有一张很大的order表,如何设计能够提升查询效率(同时满足根据买家id和卖家id查询)?
二面也同样是一小时左右,面试过程还算顺利。只是当时我在厦门鼓浪屿的一家小餐馆吃晚饭,周围的嘈杂和闷热使我很烦躁,感觉面试官态度有些傲慢……ps.一面二面结束后面试官都各种暗示我要疯狂加班能不能接受blabla……
三面(电话面试一个半小时)
二面结束后的第三天,就收到了现场三面的通知。然而我还在厦门旅行,因此改为了电话面试。
三面是一个大Boss,因此面试的问题都更考察一些分析问题的能力。
- 介绍一下你工作一年学习到什么?所在项目的架构是什么样的?UI/UX设计有哪些规范(由于我说我学到了一些UI/UX设计方法,因此面试官就问了)?
- 数据隔离级别,脏读幻读。
- 线程池原理。
线程数组,任务队列
- Synchronized的实现,锁的升级过程。
- K8s的作用,K8s的底层架构。
- 对我业余时间做的一些项目做了介绍。
- 你觉得加入阿里你能给阿里带来什么?
- 进入阿里你需要忍受很多困难,需要迎难而上,如果绩效考评拿到差评,你会怎么办?
三面总的来说也还算顺利,面试官也算和蔼。
1. 集合类源码
- ArrayList:内部数据结构,数组扩容机制
- LinkedList:内部数据结构,为什么使用双向链表
- HashMap:内部数据结构,put方法的完整流程,扩容机制
- LikedHashMap:内部数据结构,如何实现一个Cache
- TreeMap:内部数据结构,时间复杂度
- CurrentHashMap:内部数据结构,Java7分段锁,Java8 CAS+Synchronized
2. Java基础
- 自动拆装箱原理
- String,StringBuffer和StringBuilder
- Throwable
- reader和stream
- NIO
3. JVM基础
- JVM内存模型
- 常见垃圾回收算法
4. 并发编程基础
- Synchronized关键字原理
- wait,notify,sleep
- 安全的终止线程以及线程的状态转换
- 自定义Lock
- 线程池原理
5. 数据库基础
- 数据库三范式,事务ACID,隔离级别,视图,索引
- JPA实体状态
- EntityManger
6. 网络基础
- TCP/IP
7. 常见设计模式
- 装饰者,模板方法,策略,工厂,状态
总结
整个流程从一面到三面结束大约持续了10天左右。总的来说,问题都是预期范围内的,虽然面试过程中问到了一些分布式相关问题,我都没有任何经验,这时候不要放弃,主动说出你的思路,然后在面试官的诱导下,相信你能说出属于的答案。