目录
- 前言
- 一、简历中项目的难点及解决方案
- 二、讲讲分布式锁的实现
- 三、AQS(Abstract Queued Synchronizer)的原理
- 四、ConcurrentHashMap的原理
- 五、MySQL InnoDB存储引擎中的MVCC解决了什么问题,MVCC的实现原理
- 六、平时怎么创建线程?为什么用线程池,线程池有什么好处?
- 七、创建线程池需要的关键参数有哪些?
- 八、线程池有哪几种任务拒绝策略?
- 九、Redis是什么?Key的删除的原理
- 十、如何保证Redis的高可用性?
- 十一、SQL分组求和排序
- 十二、CAS的原理,如何保证内存的可见性,会产生什么问题?
- 十三、Java内存模型中的8个操作
- 十四、有哪几种类加载器,双亲委派机制是什么?
- 十五、什么情况下不应该建立索引?
- 十六、RPC
- 后记
前言
“实战面经”是本专栏的第二个部分,本篇博文是第二篇博文,如有需要,可:
- 点击这里,返回本专栏的索引文章
- 点击这里,返回上一篇《【Java校招面试】实战面经(二)》
一、简历中项目的难点及解决方案
我在Artanis那个工程中遇到过一个Bug,就是DAO里的方法调用之后都没有效果,而且Service里调用了DAO之后的流程都不会执行。那时候不会用log4J这类的日志工具,然后就通过插桩缩小范围到sessionFactory.getCurrentSession()这个函数,后来仔细翻了翻tomcat的日志,发现了报错信息,按图索骥,查到Hibernate的getCurrentSession这个函数必须在事务里调用,否则就需要用createSession,于是把这个DAO切入到事务切面里,问题得以解决。之后也立即学习了Log4J的用法,在后面的工程里把日志打好。
二、讲讲分布式锁的实现
分布式锁的实现可以通过数据库
、Redis
和ZooKeeper
1. 数据库: 创建一个表,通过唯一性约束来确保每次只有一个线程可以获得锁。
缺点: 没有锁失效机制
2. Redis: Redis方法的主要问题是原子性问题,可以通过Lua脚本来实现。如果使用Spring的RedisTemplate可以调用SetIfAbsent函数的包含过期时间的那个重载。
缺点: 没有解决可重入的问题
3. ZooKeeper: 不太熟悉ZooKeeper
4. Redis的实现:
1) 加锁:
public boolean lock(String key, String value, long expire) {
Object result = redisTemplate.opsForValue().setIfAbsent(key, value, expire, TimeUnit.SECONDS);
return "OK".equals(result);
}
2) 解锁:
public boolean unLock(List<String> keys, String... args) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = redisTemplate.execute(script, keys, args);
return "OK".equals(result);
}
三、AQS(Abstract Queued Synchronizer)的原理
AQS是一个基于队列的用于实现线程同步的类的框架,用一个int类型的变量state表示同步状态,并提供了一系列的CAS操作来管理这个同步状态。ReentrantLock就是基于AQS实现的,用法是通过继承AQS实现其模版方法,然后将子类作为同步组件的内部类。
AQS定义了两种资源共享方式: 独占(Exclusive)
和 共享(Share)
。
四、ConcurrentHashMap的原理
-
锁分段技术: 首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
-
Hashtable
和SynchronizedMap
中采用的锁机制是一次锁住整个hash表,从而在同一时刻只能由一个线程对其进行操作。而ConcurrentHashMap中则是一次锁住一个桶,所以并发效率会有指数级的提升。
五、MySQL InnoDB存储引擎中的MVCC解决了什么问题,MVCC的实现原理
MVCC(Mutil-Version Concurrency Control),就是多版本并发控制。MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问。Mysql的InnoDB引擎中,在RC和RR这两种隔离级别下,通过Undo日志实现当前读。这样就实现了读-写的并发执行,提升了系统的性能。
六、平时怎么创建线程?为什么用线程池,线程池有什么好处?
通过线程池来创建线程。使用线程池可以:
1. 复用线程,避免大量创建销毁线程造成的CPU资源浪费。
2. 管理线程,便于监控线程的状态。
七、创建线程池需要的关键参数有哪些?
1. corePoolSize: 核心线程数
2. maxPoolSize: 最大线程数
3. workQueue: 等待队列
4. handler: 线程数大于maxPoolSize时需要执行的策略
八、线程池有哪几种任务拒绝策略?
1. AbortPolicy: 直接抛出异常,默认策略;
2. CallerRunsPolicy: 用调用者所在的线程来执行;
3. DiscardOldestPolicy: 丢弃队列中最靠前的任务,并执行当前任务;
4. DiscardPolicy: 直接丢弃任务。
九、Redis是什么?Key的删除的原理
1. Redis(REmote DIctionary Server)
,字面意思是远程字典服务器,它提供了字符串、哈希表、集合、排序集合和列表这5种数据类型供使用。
2. Key可以通过手动删除,一般是设定一个过期时间,过期策略有定时删除
、定期删除
和惰性删除
,当内存不足以分配给新创建的Key时,会触发淘汰策略,包括:
1) 直接抛出异常;
2) 对全部键进行LRU;
3) 对设置了过期时间的键进行LRU
4) 对全部键进行随机删除
5) 对设置了过期时间的键进行随即删除
6) 对设置了过期时间的键选择最早过期的删除
十、如何保证Redis的高可用性?
考虑使用Redis集群,见《实战面经(二)》第四题
十一、SQL分组求和排序
以 Artanis日报系统 为例,求每个组的成员数量,按从小到大排序
SELECT
GroupId AS `group_id`,
count(*) AS `member_count`
FROM
member
GROUP BY
GroupId
ORDER BY
member_count ASC;
十二、CAS的原理,如何保证内存的可见性,会产生什么问题?
1. CAS(Compare And Swap)
,在对一个对象赋予新值之前,先比较它现在的值和执行CAS之前的值,如果相同,就认为没有其他的线程改过它的值,就给它赋予新值,否则重试直到修改成功;
2. Atomic
包下的原子操作类的内存可见性是通过volatile关键字实现的(见面经总结2-9);
3. CAS会产生ABA问题和自旋消耗资源的问题:
1) ABA问题: 即线程1把一个对象的值由A改为B,然后又改为A,这样线程2 会认为这个对象的值没有变,就会执行CAS,这在一定情境下会造成问题。
解决方案: 用版本号机制实现乐观锁
2) 自旋消耗资源问题: 自旋次数过多会消耗大量的CPU资源。
解决方案: 设置最大的自选次数,超过这个次数就放弃修改。
十三、Java内存模型中的8个操作
1. lock: 作用于主内存,它把一个变量标记为一条线程独占状态;
2. read: 作用于主内存,它把变量值从主内存传送到线程的工作内存中,以便随后的load动作使用;
3. load: 作用于工作内存,它把read操作的值放入工作内存中的变量副本中;
4. use: 作用于工作内存,它把工作内存中的值传递给执行引擎,每当虚拟机遇到一个需要使用这个变量的指令时候,将会执行这个动作;
5. assign: 作用于工作内存,它把从执行引擎获取的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时候,执行该操作;
6. store: 作用于工作内存,它把工作内存中的一个变量传送给主内存中,以备随后的write操作使用;
7. write: 作用于主内存,它把store传送值放到主内存中的变量中。
8. unlock: 作用于主内存,它将一个处于锁定状态的变量释放出来,释放后的变量才能够被其他线程锁定;
十四、有哪几种类加载器,双亲委派机制是什么?
1. 类加载器包括:
1) Bootstrap ClassLoader
2) Ext ClassLoader
3) App ClassLoader
分别用于加载Java核心库(如java.lang)、ext文件夹下的类和应用程序的类。
2. 双亲委派机制: 即一个类加载器在加载类时先判断这个类是否已经加载了,如果没有就调用父加载器去加载。双亲委派机制解决了类加载的安全性问题,比如我们自己写一个恶意的java.lang.Integer类,让App ClassLoader来加载,它会调用Ext ClassLoader去加载,Ext又会调用Bootstrap,Bootstrap发现这个类已经加载过了,就不会再记载这个恶意的了。
十五、什么情况下不应该建立索引?
1) 数据量小的表不需要建立索引,建立会增加额外的索引开销;
2) 频繁变更的字段上不应该建立索引,会增加索引的维护成本。
十六、RPC
远程过程调用(RPC,Remote Procedure Call) 是一种允许在一台计算机(客户端)上调用另一台计算机(服务器)上的过程或方法的技术。RPC 使用请求-响应模式,客户端发起请求并等待响应。服务器接收请求,处理请求,返回结果给客户端。
在 Java 中,可以使用RMI(Remote Method Invocation)
实现RPC。以下是一个简单的示例:
1. 定义接口: 创建一个要在远程服务器上执行的接口。
Calculator.java:
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface Calculator extends Remote {
int add(int a, int b) throws RemoteException;
}
2. 实现接口: 为接口创建一个实现类。
CalculatorImpl.java:
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class CalculatorImpl extends UnicastRemoteObject implements Calculator {
public CalculatorImpl() throws RemoteException {
super();
}
public int add(int a, int b) {
return a + b;
}
}
3. 服务端: 创建一个 RMI 服务器,用于注册远程对象。
Server.java:
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Server {
public static void main(String[] args) {
try {
Calculator calculator = new CalculatorImpl();
Registry registry = LocateRegistry.createRegistry(1099);
registry.rebind("Calculator", calculator);
System.out.println("Calculator Server is ready.");
} catch (Exception e) {
System.err.println("Server exception: " + e.toString());
e.printStackTrace();
}
}
}
4. 客户端: 创建一个客户端,用于从远程获取对象并调用方法。
Client.java:
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Client {
public static void main(String[] args) {
try {
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
Calculator calculator = (Calculator) registry.lookup("Calculator");
System.out.println("1 + 2 = " + calculator.add(1, 2));
} catch (Exception e) {
System.err.println("Client exception: " + e.toString());
e.printStackTrace();
}
}
}
5. 要运行此示例,请进行以下操作:
1) 编译所有 .java 文件。
2) 运行 Server 类,以启动 RMI 服务器。
3) 在另一个命令窗口中,运行 Client 类。你应该会看到输出:“1 + 2 = 3”。
这是实现 Java RPC 的一个基本示例。实际应用场景可能更加复杂,可以考虑使用gRPC
等现代框架。
后记
同样,有部分题目链接到了前面的面经中,这部分重复考的题目也应该多加重视。