目录
- 前言
- 一、Map和HashMap的区别?
- 二、String, StringBuilder和StringBuffer的区别
- 三、如何在大数组中找到从小到大的第1亿个数字?
- 四、ThreadLocal是什么,怎么使用?
- 五、Servlet的生命周期
- 六、CPU占用过高的排查思路
- 七、Redis是单线程还是多线程?有什么好处?
- 八、Redis能否做消息队列?
- 九、synchronized对象锁和类锁
- 十、最终一致性是什么?如何实现?
- 十一、Accept和Content-Type
- 十二、APP消息推送的过程
- 十三、HTTP和FTP在传输层上的不同
- 十四、内连接和外连接,WHERE和ON
- 十五、Undo Log和MVCC
- 十六、int、double、float的长度,32位机和64位机int长度是否一致?
- 十七、HashMap是否允许键为null?连续存放两个null会如何?使用HashMap的注意事项
- 十八、ConcurrentHashMap的put、get和resize过程
- 十九、AQS在获取锁的时候为什么不使用synchronized而使用CAS自旋?
- 二十、synchronized锁升级的过程
- 二十一、CMS的垃圾收集过程
- 二十二、CMS的缺点
- 二十三、频繁发生Full GC,如何定位问题?
- 二十四、内存泄漏如何处理?
- 二十五、JVM调优命令
- 二十六、InnoDB并发会出现的问题,如何解决?
- 二十七、事务是如何实现的?
- 二十八、左连接、右连接、内连接,默认是哪种?
- 二十九、sleep和wait的区别
- 三十、Object类有哪些方法?
- 三十一、concurrent包下有哪些常用的类?
- 后记
前言
“实战面经”是本专栏的第二个部分,本篇博文是第七篇博文,如有需要,可:
一、Map和HashMap的区别?
Map是接口,HashMap是Map接口的主要实现类。
二、String, StringBuilder和StringBuffer的区别
String
是基础的字符串类型,长度不可变StringBuffer
和StringBuilder
的长度是可变的。- 另外,
StringBuffer
是线程安全的,使用了synchronized
关键字加锁实现。
三、如何在大数组中找到从小到大的第1亿个数字?
使用词频统计法:
-
32位整型数值范围在 − 2 31 -2^{31} −231~ 2 31 − 1 2^{31}-1 231−1,约42.9亿个数。设置一个长度为10万的数组,第一次遍历,数组的每一位统计43000范围内的 “词频”。
-
从第0位开始累加,发现到第i位的时候和 s u m i − 1 + c o u n t i sum_{i-1}+count_i sumi−1+counti超过了1亿。
-
设置一个长度为43000的数组,第二次遍历,统计这个范围内每个数字的 “词频”。
-
从第0位,以 s u m i − 1 sum_{i-1} sumi−1为基数开始累加,发现在第j位的时候和超过了1亿,那么第j位代表的数字就是所求数字。
四、ThreadLocal是什么,怎么使用?
-
ThreadLocal
用于存储单个线程的生命周期中共享的数据,在Thread
类中有个ThreadLocalMap
类型的成员用来存储每个线程的ThreadLocal
。 -
使用的时候,将要共享的对象用
ThreadLocal
封装一层,然后通过set赋值,get取值; -
需要注意的一点是,
ThreadLocalMap
以弱引用的ThreadLocal
作为键,但是值是强引用,如果键被回收了,线程长时间不结束,就会造成值无法被回收,造成内存溢出,所以用完之后应该手动调用remove。
五、Servlet的生命周期
一个 Servlet 的生命周期分为以下几个阶段:
-
加载和实例化:
在 Web 服务器启动时或在某个 Servlet 首次收到请求时(取决于服务器配置),服务器会加载 Servlet 类并实例化 Servlet 对象。这个步骤只会执行一次。 -
初始化 (init):
在 Servlet 对象被实例化后,Servlet 容器会调用 Servlet 的init()
方法进行初始化。这个方法也只执行一次,通常用于一些资源初始化或配置加载。 -
请求处理 (service):
当 Servlet 收到一个客户端请求时,服务器会调用 Servlet 的service()
方法进行请求处理。根据不同的 HTTP 方法(如 GET、POST 等),service()
方法会调用相应的doGet()
、doPost()
等特定方法。不同请求之间可能由并发处理。在 Servlet 生命周期内,service()
及相关方法可能被调用多次。 -
销毁 (destroy):
当 Servlet 容器关闭或需要销毁 Servlet 时,会调用destroy()
方法。在这个阶段,Servlet 会释放所有分配的资源,进行清理操作。
六、CPU占用过高的排查思路
-
单核主机使用
top
命令,多核主机使用htop
命令,查看进程的CPU使用情况。 -
taskset
可以解决CPU使用不均衡的情况,它可以让程序运行在指定的CPU上。
七、Redis是单线程还是多线程?有什么好处?
Redis是单线程,通过IO多路复用技术来处理并发请求的问题。
好处:
- 不用考虑各种锁的的问题,避免了加锁解锁造成的性能消耗;
- 避免了进程之间切换造成的性能消耗。
八、Redis能否做消息队列?
Redis 可以用作消息队列。Redis 中的 List 和Pub/Sub
机制可以让我们实现类似消息队列的功能。
下面是一个使用 Jedis(一个流行的 Redis Java 客户端库)作为 Redis 客户端,以 List 结构实现消息队列的 Java 示例代码。首先确保已经通过 Maven 或 Gradle 将 Jedis 依赖添加到我们的项目中。
- Maven 依赖:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.7.0</version>
</dependency>
- Gradle 依赖:
implementation "redis.clients:jedis:3.7.0"
现在,我们可以按照以下示例代码,使用 Jedis 和 Redis List 结构创建生产者和消费者来操作消息队列。
- 生产者(Producer.java):
import redis.clients.jedis.Jedis;
public class Producer {
public static final String QUEUE_KEY = "message_queue";
public static void main(String[] args) {
// 连接 Redis
Jedis jedis = new Jedis("localhost");
// 模拟生产消息
int index = 0;
while (index < 10) {
String message = "Message: " + index;
// 将消息插入到队列中
jedis.rpush(QUEUE_KEY, message);
System.out.println("Produced: " + message);
try {
// 暂停 1 秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
index++;
}
// 关闭 Redis 连接
jedis.close();
}
}
- 消费者(Consumer.java):
import redis.clients.jedis.Jedis;
public class Consumer {
public static final String QUEUE_KEY = "message_queue";
public static void main(String[] args) {
// 连接 Redis
Jedis jedis = new Jedis("localhost");
// 消费消息
while (true) {
// 从队列中取出消息(此操作为阻塞式,即没有消息时,将一直等待)
String message = jedis.blpop(0, QUEUE_KEY).get(1);
System.out.println("Consumed: " + message);
}
}
}
通过这个示例代码,我们使用Redis 实现了一个消息队列。Producer 将消息推送(生产)到队列中,而 Consumer 从队列中拉取(消费)并输出消息。
九、synchronized对象锁和类锁
- 对象锁: 加在非静态方法上或者
synchronized(this)
作用: 同步操作该类的同一个对象的所有线程,操作不同对象的线程互相不影响
示例(假设对实现了Runnable接口,加了对象锁的类MyRunnable进行多线程同步):
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(3, 3, 0L,
TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
MyRunnable runnable = new MyRunnable();
threadPool.submit(runnable);
threadPool.submit(runnable);
threadPool.submit(runnable);
解释: 用线程池创建3个线程操作同一个对象,这3个线程将会因为MyRunnable中的对象锁而同步执行。
- 类锁: 加在静态方法上或者
synchronized(XXX.class)
作用: 同步操作该类的任一对象的所有线程
示例(假设对实现了Runnable接口,加了类锁的类MyRunnable进行多线程同步):
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(4, 4, 0L,
TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
MyRunnable runnable1 = new MyRunnable();
MyRunnable runnable2 = new MyRunnable();
threadPool.submit(runnable1);
threadPool.submit(runnable1);
threadPool.submit(runnable2);
threadPool.submit(runnable2);
解释: 用线程池创建4个线程分别操作2个对象,这4个线程将会因为MyRunnable中的类锁而同步执行。
说明: 使用类锁和对象锁需要根据实际情况来选择,如果实现一个容器类,就应该使用对象锁,如果针对一个全局的资源,就应该使用类锁。
十、最终一致性是什么?如何实现?
-
分布式系统在某一时刻各个节点的数据不一致,但是经过一段时间最终趋于一致。
-
可以通过以下的方法:
1) 重试
2) 幂等
3) 状态机
4) 恢复日志
5) 异步校验(Paxos算法)
十一、Accept和Content-Type
-
Accept属于请求报头,表示发送端希望接受的数据类型
-
Content-Type属于实体报头,表示发送端发送的实体数据的类型
-
MediaType: 互联网媒体类型,也叫做MIME类型,包括:
a) text/html
b) text/plain
c) text/xml
d) application/json
e) application/xml
…
等15种
十二、APP消息推送的过程
分为Pull和Push两种
- Pull是客户端与服务端建立短连接,监听服务端是否有更新信息
- Push是客户端与服务端建立长连接,服务端将更新信息推送到客户端
十三、HTTP和FTP在传输层上的不同
-
HTTP仅建立一个数据连接,FTP建立一个数据连接和一个控制连接
-
HTTP使用TCP的80等端口,FTP使用TCP的20和21端口
-
HTTP不需要身份验证,FTP需要使用密码进行身份验证
十四、内连接和外连接,WHERE和ON
-
对于内连接的两个表,驱动表中的记录在被驱动表中找不到匹配的记录,该记录不会加入到最后的结果集;对于外连接的两个表,驱动表中的记录即使在被驱动表中没有匹配的记录,也仍然需要加入到结果集。
-
左外连接即选取左边的表作为驱动表,右外连接即选取右边的表作为驱动表。
-
使用
WHERE
进行过滤时,不符合过滤条件的记录都不会被加入到结果集;使用ON
进行过滤时,如果无法在被驱动表中找到匹配过滤条件的记录,那么该纪录仍然会被加入到结果集中,对应的被驱动表记录的各个字段使用NULL
填充。 -
一般情况下,涉及单表的过滤条件使用
WHERE
,涉及多表的过滤条件用ON
。一般把ON
子句的过滤条件称为连接条件。
十五、Undo Log和MVCC
-
MVCC: 多版本并发控制
-
Undo Log: 用于回滚和MVCC中实现快照读
-
快照读产生快照的时机: 在事务进行第一次快照读SELECT时。
-
快照读在RR隔离级别下部分解决幻读问题: 之所以说是部分解决,是因为有特例:
1) A事务对表T进行快照读,未提交;
2) B事务向表T中插入记录X;
3) A事务对记录X进行更新,记录X中标记最后一个更新它的事务的隐藏字段标记 为A,X现在对事务A可见;
4) A事务再次查询,发生幻象读。 -
原因: 上述原因就是因为事务A是快照读,快照读是无锁非阻塞的。如果事务A以
SELECT … FOR UPDATE
的形式开启当前读,事务B插入记录X就会被阻塞住,直到事务A提交,这样就不会发生幻读问题。
十六、int、double、float的长度,32位机和64位机int长度是否一致?
-
int 32位,float 32位,double 64位。
-
32位机和64位机下int长度相等,因为Java是平台无关的。
十七、HashMap是否允许键为null?连续存放两个null会如何?使用HashMap的注意事项
-
HashMap允许键为null,因为无法调用hashCode函数,所以指定键为null的键值对放在0号桶里。
-
连续以null为键put两次只会用新的Value覆盖老的值。
-
使用HashMap要注意的事项有以下几点:
1) HashMap在resize和rehash的时候比较消耗性能,最好在创建的时候能估算一 下大致需要的容量,避免resize。
2) HashMap是非线程安全的,想要线程安全就是用ConcurrentHashMap。
十八、ConcurrentHashMap的put、get和resize过程
-
put
的时候通过锁分段技术,只锁住需要操作的那个桶,对于Hashtable
锁住整个表来说并发性能有指数级的提升。 -
get
函数是不上锁的,因为node数组用volatile
修饰过了,所以任何线程对它的修改对其他线程是立即可见的,所以不需要上锁。 -
resize
的时候单线程创建新表,多线程rehash
。
十九、AQS在获取锁的时候为什么不使用synchronized而使用CAS自旋?
CAS的时候线程不会进入阻塞状态,避免了上下文切换带来的性能损耗。
二十、synchronized锁升级的过程
锁膨胀的方向: 无锁
--> 偏向锁
--> 轻量级锁
--> 重量级锁
-
偏向锁: 减少同一线程获取锁的代价
大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得。如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需检查Mark Word的锁标记位为偏向锁,以及当前线程Id等于Mark Word的ThreadID即可,这样就省去大量有关锁申请的操作。 -
轻量级锁: 适用于线程交替执行同步块的场景
当第二个线程加入锁竞争的时候,偏向锁就会升级为轻量级锁。通过CAS自旋来加解锁。如果存在多个线程同一时间访问同一个锁的情况,就会膨胀为重量级锁。
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要CAS操作,没有额外的性能消耗,和执行非同步方法相比仅存在纳秒级的差异 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 只有一个线程访问同步块或同步方法的场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了响应速度 | 若线程长时间抢不到锁,自旋会消耗CPU性能 | 线程交替执行同步块或同步方法的场景 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢,在多线程下,频繁的获取释放锁,会带来巨大的性能消耗 | 追求吞吐量,同步块或者同步方法执行时间较长的场景 |
二十一、CMS的垃圾收集过程
CMS即Concurrent Mark Sweep,并发标记清除,以最小停顿时间为目的
-
初始标记(Stop the world): 通过可达性分析算法,标记与GC ROOT直接关联的对象。
-
并发标记: 用户线程继续执行,以上一阶段标记的对象为起点,通过可达性分析算法开始并发标记。
-
并发预处理: 查找并发标记阶段晋升到老年代的对象,以及被修改了的对象。
-
重新标记(Stop the world): 通过可达性分析算法,并发标记堆中剩余的对象。
-
并发清理: 用户线程继续执行,并发清理垃圾对象。
-
并发重置: 重置CMS收集器的内置数据,为下次回收做准备。
二十二、CMS的缺点
-
并发: 并发意味着多线程抢占CPU资源,即GC线程与用户线程抢占CPU。这可能会造成用户线程执行效率下降。
-
标记: 并发清理阶段用户线程还在运行,这段时间就可能产生新的垃圾,即浮动垃圾。浮动垃圾在此次GC无法清除,只能等到下次清理。
-
清除: 使用标记—清除算法会造成内存碎片化的问题。
二十三、频繁发生Full GC,如何定位问题?
-
CMS并发失败: CMS收集器会有一个阈值CMSInitiatingOccupancyFraction表示当老年代空间的百分之多少被使用的时候就出发CMS回收,默认是92%。在并发清理阶段留8%老年代给用户线程继续执行使用。如果这个阈值设置的太高,用户线程获得不到足够的内存,就会发生并发模式失败(Concurrent Mode Failure),就会使用Serial Old收集器执行一次FullGC,所以需要查看CMSInitiatingOccupancyFraction这个域值是不是设得太大。
-
年轻代空间不足: 当出现年轻代Eden区放不下的大对象时就会直接进入到老年代,频繁地发生Full GC可能是年轻代的比例过低(默认年轻代:老年代 = 2:1)。
二十四、内存泄漏如何处理?
-
代码检查: 一些编码习惯
1) 涉及到实现了AutoCloseable接口的类时,实例化的时候最好用try-with- resources,在业务逻辑执行结束后会自动调用close方法。自定义的需要申请释放资源 的类也应该实现AutoCloseable。
2) 用完ThreadLocal之后应该手动调用remove,不然会发生内存泄漏。 -
专业的工具: 使用JProfiler这类专业的工具来详细判断原因(都是那个类的对象造 成的、平均生命多长等)。
二十五、JVM调优命令
-
jstat(JVM statistics Monitoring): 是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
-
jmap(JVM Memory Map): 用于生成heap dump文件,可以查询finalize执行队列、Java堆的详细信息,如当前使用率、当前使用的是哪种收集器等。
-
jhat(JVM Heap Analysis Tool): 与jmap搭配使用,用来分析jmap生成的dump文件。
二十六、InnoDB并发会出现的问题,如何解决?
-
死锁问题: A事务和B事务都在等待对方释放锁的状态,InnoDB的事务监控可以检测到这种死锁的状态,并强制使其中的一个事务回滚。
-
锁等待问题: 如果事务B在等待事务A释放锁,而事务A长时间未提交。可以通过设置加锁等待超时来解决,等待超过一定的时间就放弃。
二十七、事务是如何实现的?
数据库事务(Transaction)是一系列用于保证多个数据库操作在完成后同时提交或同时回滚的手段。数据库事务通常遵循 ACID
属性(原子性、一致性、隔离性和持久性),以确保多个数据库操作之间保持数据一致性。
事务的实现依赖于数据库管理系统(DBMS)以及其类型(如关系型、非关系型,或分布式等)。以下概述了关系型数据库(例如 MySQL、PostgreSQL 和 SQL Server)中事务的一般实现方法:
-
原子性:
事务保证了所有操作要么全部成功提交,要么全部回滚。为实现原子性,数据库管理系统通常使用日志记录(写前日志,Write-Ahead Logging
,缩写为WAL)跟踪事务中已执行的操作。如果事务失败,日志记录有助于恢复初始状态,即回滚所有中间操作。 -
一致性:
数据库会根据其约束和规则在事务的起始和结束时将数据保持在一致状态。如果某个事务违反了约束,数据库将不允许提交该事务,并触发回滚。 -
隔离性:
隔离性是通过数据库的共享锁(Shared Lock)
和排它锁(Exclusive Lock)
机制实现的。这些锁允许数据库管理系统在多个并发事务之间给各资源添加访问限制,从而避免问题,如脏读(Dirty Read)
和丢失更新(Lost Update)
。数据库管理系统通常提供不同的隔离级别(如读未提交、读已提交、可重复读和串行化),以平衡性能和隔离要求。 -
持久性:
一旦事务成功提交,其对数据库所做的更改将永久保存在存储介质上。这包括先将日志刷新到持久存储中,然后将数据页的更改落盘。
不同的数据库管理系统可能采用不同的技术或方法来实现上述事务属性,例如多版本并发控制(MVCC
)或 OCC
(Optimistic Concurrency Control,乐观并发控制)技术。这些技术根据系统的规模、性能需求以及其他因素进行选择。
总之,事务是由数据库管理系统实现的一种机制,通过记录日志、设置锁和实施约束等手段,确保多个操作的原子性、一致性、隔离性和持久性。
二十八、左连接、右连接、内连接,默认是哪种?
-
内连接即驱动表中的记录在被驱动表中找不到时,不出现在结果集中。
-
左连接和右连接属于外连接,驱动表中的记录在被驱动表中找不到时也要出现在结果集中,结果集中相应的位置用
NULL
填充。左连接就是以左边的表作为驱动表,右边的表作为被驱动表;右连接相反。 -
如果默认指的是使用
SELECT … FROM t1, t2
,这种方式相当于SELECT … FROM t1 INNER JOIN t2
,属于内连接。
二十九、sleep和wait的区别
-
sleep
是Thread
类的方法,wait
是Object
类中定义的方法; -
sleep
方法可以在任何地方使用,wait
方法只能在被synchronized
修饰的方法或synchronized
块中使用; -
Thread.sleep
只会让出CPU,不会导致锁行为的改变,Object.wait
不仅让出CPU,还会释放已经占有的同步资源锁。
三十、Object类有哪些方法?
-
hashCode: 获取对象的哈希值
-
toString: 获取对象的字符串
-
clone: 获取对象的拷贝
三十一、concurrent包下有哪些常用的类?
-
ConcurrentHashMap
-
线程池相关的类
-
Semaphore
、CountDownLatch
、CyclicBarrier
-
还有
atomic
和locks
包
后记
这篇面经也问了很多问题,其中各种锁的占比非常大。