Fork me on GitHub

面经整理6

SSM系列问题推荐阅读:SSM常见面试问题

汇总:趣链、蘑菇街、随手记、网易、招银、IBM、阿里

趣链Java一面之lh篇

自我介绍。
讲一下项目。

1. 类加载的过程。

三个阶段:加载、连接、初始化。

  • 加载:①class文件加载到内存中;②方法区生成运行时类文件;③堆区生成class对象,作为访问方法区中类文件的访问入口。
  • 连接:①验证文件正确性;②为类的静态变量分配内存,并初始化默认值;③符号引用转换成直接引用。
  • 初始化:为类变量赋予正确的初始值。

详解 classLoader 的loadClass

PriorityQueue 实现大根堆

因为 PriorityQueue默认是小根堆,那么该怎么实现大根堆呢?

1
2
3
4
5
6
7
8
private static final int DEFAULT_INITIAL_CAPACITY = 11;
Queue<Integer> maxHeap = new PriorityQueue<>(DEFAULT_INITIAL_CAPACITY, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2-o1; // 关键:compare函数 return的结果如果小于0则保持原位置,大于 0 则交换。
}
});

compartor 采用了策略模式,使用策略对象来改变它的行为。

comparator 用法扩展阅读 Comparator的用法

2. 用过哪些集合类。

Map、List、Set。
List:ArrayList、LinkedList、Vector;
Map:HashMap、HashTable、TreeMap、LinkedHashMap

1
2
3
4
5
6
7
8
9
10
11
12
// TreeMap 自定义Comparator,先按名字排序,名字相同者按年龄排序:
TreeMap<Student,String> tmap = new TreeMap<Student,String>(new Comparator<Student>(){
@Override
public int compare(Student s1,Student s2){
int num = s1.getName().compareTo(s2.getName());
if(num == 0){
return new Integer(s1.getAge()).compareTo(s2.getAge());
}
return num;
}
});

3. HashMap和HashTable区别。

  • HashMap:①线程不安全;②key和value可为null;③扩容机制不同:初始默认16,两倍扩容。④链表长度大于8后,且桶的数量大于等于64 时,链表转为红黑树(桶数量低于64时会优先扩容)。
  • HashTable: ① 线程安全,Synchronized锁,效率低,目前已经用ConcurrentHashMap代替使用;②key不可为null;③初始默认11,2n+1扩容。④没有转红黑树的机制。

4. 讲一下FutureTask,怎么获取返回值的,其他几种多线程的实现比较。

深度解析 futureTask:

  1. 关键点一:RunnableFuture 接口,它同时继承了 Runnable、Future 两个接口,而 FutureTask 正是它的实现类。
  2. 关键点二:用户自己实现的 **Callable 实现类。
  3. 执行步骤:
    1. 首先对线程池执行 submit(**Callable)方法,内部 **Callable 是作为参数放进 FutureTask 实例(**FutureTask)中的,所以其实是在执行execute(**FutureTask),而带有业务逻辑的 call()方法逻辑也就顺理成章的成了 run()方法逻辑。
    2. execute(**FutureTask)方法会触发**FutureTaskrun()方法,执行完成后,会封装成 Future 对象返回。
    3. 最后可以通过**FutureTask.get()方法拿到返回值(get 方法其实是线程阻塞的,所以 java 中的 Future 用法并不是真正意义上的异步操作)。
    4. 注:可以使用 Thread.start()方法代替线程池执行的 submit()方法,两者后面的执行逻辑是相似的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// ①FutureTask 单独使用
/** 1. Callable接口及发散的类,带着线程工作逻辑,被new出来。
* 2. Future接口及发散类,new时传入Callable实例。
* 3. Thread接口及发散类,new 时传入Future实例。
* 4. 启动线程,可以get返回值(get方法会阻塞线程,)。
*/
public class ConstruceCallable<V> implements Callable<V>{
@Override
public V call() throws Exception{
// todo 业务逻辑
return V;
}
}

Callable<V> oneCallable = new ConstruceCallable<V>();
FutureTask<V> task = new FutureTask<V>(oneCallable);
Thread thread = new Thread(task);
thread.start(); // 启动线程
task.get(); // 拿到返回值V


// ②FutureTask 结合线程池使用
/** 1. new 一个 ThreadPoolExecutor
* 2. 在一个List<FutureTask> 中,逐个新增FutureTask实例
* 3. 接收task列表,执行executor.submit 方法
* 4. 遍历list获取返回值。
*/
ThreadPoolExecutor executor = new ThreadPoolExecutor(5,10,10L,
TimeUnit.SECONDS,new LinkedBlockingDeque<>(10));
List<FutureTask> tasks = new ArrayList<>();
for(int i =0;i<10;++i){
FutureTask task = new FutureTask(new SomeCallable());
executor.submit(task);
tasks.add(task);

}
for(FutureTask futureTask : tasks){
System.out.println(futureTask.get());
}


比较:

线程池七大参数的关系:其中比较容易让人误解的是:corePoolSize,maximumPoolSize,workQueue之间关系。
1.当线程数小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。
2.当线程数达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行
3.当workQueue已满,且maximumPoolSize>corePoolSize时,新提交任务会创建新线程执行任务
4.当提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理
5.当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程
6.当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭

线程池的四种拒绝策略:

  1. CallerRunsPolicy:线程调用运行该任务的 execute 本身。这个策略显然不想放弃执行任务。但是由于池中已经没有任何资源了,那么就直接使用调用该execute的线程本身来执行。
  2. AbortPolicy:处理程序遭到拒绝将抛出运行时 RejectedExecutionException。这种策略直接抛出异常,丢弃任务。
  3. DiscardPolicy:不能执行的任务将被删除。这种策略和AbortPolicy几乎一样,也是丢弃任务,只不过他不抛出异常。
  4. DiscardOldestPolicy:如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序。该策略就稍微复杂一些,在pool没有关闭的前提下首先丢掉缓存在队列中的最早的任务,然后重新尝试运行该任务。

推荐阅读:FutureTask的底层实现

为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。

5. MySQL怎么实现事务的,在SSM框架中你是怎么做的?

6. 用过哪些设计模式,讲一个你最常用的,讲一下你对代理模式的理解。

  1. 代理模式就是通过代理来控制对象的访问。可以详细访问到对象的方法,并且在方法先后添加一些业务逻辑。
  2. 主要用在AOP、事务、日志打印、权限控制、远程调用、安全代理等。
  3. 代理模式通常有两种实现方式:静态代理和动态代理,后者又有两种实现,jdk和cglib。
    1. 静态代理,一般就是加一层包装类的形式,原始类当作参数传入包装类,通过调用包装类间接调用目标对象。
    2. jdk 方式,使用 implements 了InvocationHandler 的某类 handler,将原始接口target当作参数field传入构造方法,然后在代理类中invoke 方法中调用 method.invoke(target,args )。main方法首先获取这个某类 handler,然后通过 Proxy.newProxyInstance 拿到实例,之后随便调用 target 类的方法。
  4. jdk:面向接口生成代理,自带的Proxy和InvocationHandler。

  1. cglib:没有接口这一硬性条件。基于ASM,是一种直接操作字节码的框架,推荐阅读:Cglib及其基本使用

  2. 详细参考第 11 题。

  3. ASM 阅读推荐:ASM,精华总结如下:

    1. ASM 是一种动态修改字节码数组的工具,跟一般的代理类有所区别,ASM 的最终目的是生成可以被装载的class 文件。

    2. ASM技术对程序员隐藏了字节码偏移的细节,程序员只需要操作一个类似于树的数据结构,对字节码进行遍历即可。

    3. 具体做法是实现一个ClassVisitor 接口的类,重写相关的visit 方法,ASM 会自动调用这些 visit 方法

1
2
3
4
5
6
7
8
9
10
11
// 以下例子中重写了 visitField 方法,将修饰符修改为 Private
class AccessClassAdapter extends ClassAdapter {
public AccessClassAdapter(ClassVisitor cv) {
super(cv);
}
public FieldVisitor visitField(final int access, final String name,
final String desc, final String signature, final Object value) {
int privateAccess = Opcodes.ACC_PRIVATE;
return cv.visitField(privateAccess, name, desc, signature, value);
}
}

7. 手写反转单链表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void reverse(TreeNode head){
if(null == head || null == head.next()){
return;
}
TreeNode pre, cur, next;
pre = null; cur = head;

while(null != cur){
if(null == cur.next()){
// cur 是原链最后一个结点
cur.next() = pre;
break;
}
next = cur.next(); // 核心思路:
cur.next() = pre; // 1. 让 cur 的后继指针指向它的 pre
pre = cur; // 2. 用 next 记住 cur 移动的下一位,保证不断链
cur = next; // 3. 再让 cur 来到 next 的位置
}
head.next() = cur;
}

按步骤 123 依此进行反转

8. 项目中实现了哪些功能。有没有文字处理的功能。

个人项目流程:

  1. 将java代码生成字节数组;
    • 1.1 热替换字节码数组;
    • 1.2 自定义类加载器;
  2. 然后将字节数组转换成Class类(类加载);
  3. 反射调用的方式执行代码,获得执行结果;

扩展:HotSpot 将字节码编译成机器码的两种方式:

  1. 解释执行:需要时,将字节码逐条翻译成机器码并执行。
    • 优点:无需等待编译。
    • 约占80%代码。
  2. 编译执行:将部分字节码提前全部翻译好,然后执行,即 JIT。
    • 优点:实际运行速度更快。
    • 采取了分层编译的方式,内置多个即时编译器,这些编译器有着不同的编译速度和各自特色,可以根据程序运行信息选取合适编译器,对编译后的执行效率进行优化。
    • 约占20%代码。
  3. 混合模式中,大部分打码采用解释执行,对于反复执行的热点代码,以方法为单位进行编译执行。

补充 :字节码中与方法调用相关的指令

  1. invokestatic:用于调用静态方法;
  2. invokespecial:用于调用私有实例方法、构造器,以及使用 super关键字调用父类的实例方法或构造器,和所实现接口的默认方法;
  3. invokevirtual:用于调用非私有实例方法;
  4. invokeinterface:用于调用接口方法;
  5. invokedynamic:用于调用动态方法。

参考“《深入拆解 Java 虚拟机(极客时间)》04.JVM 是如何执行方法调用的(上)?”

虚方法调用对性能影响很大

所以 JIT 采取了两种优化手段:内联缓存和方法内联,用来加速动态绑定。

  1. 内联缓存:核心过程:利用缓存(快),避免基于方法表的动态绑定(慢)。
    • 缓存内容:虚方法中调用者的动态类型,以及该类型对应的目标方法。 - 所谓动态类型,就是运行时在程序内部动态生成的类或者类型。从多态的角度来看,理解为不同的子类这种情况。

    • 分类:单态内联(JVM 中采用),多态内联,超多态内联。

      • 多态内联,缓存多种动态类型及其目标方法。

参考“《深入拆解 Java 虚拟机(极客时间)》05.JVM 是如何执行方法调用的(下)?”

9. 知道JavaCompile的底层实现吗,具体怎么做的?不调api,你会怎么实现这个功能?

10. 讲一下spring的ioc原理,怎么实现依赖注入的,具体是在哪一个环节注入属性的?

IOC实现:

  1. 首先要有一个 Resource 接口及发散的几个类,用于解决IOC容器中内容从哪里来的问题。也就是配置文件从哪里读、怎么读的问题。
  2. 然后要有一个 BeanDefiniton 类及发散的几个类,用来解决Bean的具体定义问题(包括名字、类型、属性值或引用等),相当于把这些告诉IOC容器,让容器可以根据这个定义创建实例。
  3. 然后要有一个 BeanFactory 接口及发散的几个类,用于解决 IOC 容器在已获取到 Bean 的定义情况下,如何装配、获取Bean实例的问题。
    • 其中有一个AutowireCapableBeanFactory 类,是一种具有自动装配功能的BeanFactory,实现了doCreateBean方法。具体有三步:①通过 BeanDefinition 中保存的类信息实例化一个对象;②把对象保存在 BeanDefinition 中,以备下次获取;③为其装配属性。装配属性时,通过 BeanDefinition 中维护的 PropertyValues 集合类,把 String - Value 键值对注入到 Bean 的属性中去。如果 Value 的类型是 BeanReference 则说明其是一个引用(对应于 XML 中的 ref),通过 getBean 对其进行获取,然后注入到属性中。
  4. 最后要有一个 ApplicationContext 接口及发散的几个类,对前面三个进行功能的封装,解决根据地址获得IOC容器并使用的问题。

依赖注入: 组件之间的依赖关系由容器在运行期间确定。

  1. 应用程序依赖于IOC容器,由IOC容器将对象需要的外部资源(比如其他对象、资源、常量数据等),注入到这个对象中。
  2. BeanDefinition决定了依赖的具体关系的定义。
  3. BeanFactory 进行注入的实施工作。

墙裂推荐阅读:tiny-spring 分析
IOC与DI的区别讲的很好:控制反转和依赖注入的理解(通俗易懂)

  • bean的实例化前调用,也就是将AbsractBeanDefinition转换为BeanWrapper 前的处理。给子类一个修改BeanDefinition的机会,也就是说当程序经过这个方法(即applyBeanPostProcessorsBeforeInstantiation())后,bean可能已经不是我们认为的bean了,而是或许成为了一个经过处理的代理bean,可能是通过 cglib 生成的,也可能是通过其它技术生成的。

上一段参考:实例化的前置处理

11. aop的实现原理,你的项目中怎么做的?

JDK的方式:

  1. 可以通过 Proxy 的 newProxyInstance(obj.getClassLoader(), obj.getClass().getInterfaces(), handler),可以返回 obj 的代理对象 proxy。
  2. InvocationHandler 接口有个invoke方法。当调用proxy.func(args)方法时,对象内部将委托给 handler.invoke(proxy, func, args) 函数实现。

cglib的方式:

利用BeanPostProcessor接口和BeanFactoryAware接口,分别可以获取AOP在IOC容器中植入的位置,以及为哪些对象提供植入的清单。
切点通知器PointcutAdvisor类,用于提供对哪个对象的哪个方法进行什么样的拦截 的具体内容。动态代理的步骤:

口述过程:首先是,在所有的Bean被实例化之前,“创造代理对象”的类即AutoProxyCreator先被实例化;普通bean在被实例化、初始化时,判断类是否是要被拦截的目标,如果是,则取出这个类的信息,并找到“欲拦截的方法”,“拦截的具体操作”,统统交给AopProxy生成代理。AopProxy生成一个InvocationHandler,在其中的invoke方法被执行。

代码实现及原理剖析:Spring AOP 实现原理

12. HTTP请求涉及的协议,以及依次用到的协议的先后顺序。

访问一个URL经历了哪些过程(最详尽版!)

13. HTTPS请求的过程,这个过程是在TCP建立连接之前还是之后?

当然是之后了。

14. 安全证书和server公钥之间的关系。

数字证书 = 数字签名 + (server 的公钥 & server 的个人信息)。

其中(server 的公钥 & server 的个人信息)可以使用 Hash 算法得到消息摘要。
消息摘要使用 CA 的私钥可以得到数字签名。
client 验证证书的过程:

取出证书中的(server 的公钥 & server 的个人信息),使用相同的 Hash 算法得到消息摘要1;
取出证书中的数字签名,使用 CA 的公钥解密,得到消息摘要2;
比较两份消息摘要,如果不同,说明可能遭到了篡改。

15. Redis的持久化机制。

16. 有没有做过Redis集群?

17. redis的主从复制是怎么一个过程?

  1. 单向的,只能从master到slave。
  2. 作用:数据热备、服务冗余(备机)、读写分离(负载均衡)、实现高可用的基础。
  3. 过程:
    1. slave 开启主从复制,slave存储有master的ip 和端口信息;slave每秒一次调用复制函数,一旦发现有可用主机,就根据ip和端口创建socket连接;slave发送ping命令进行首次请求;身份验证;将自身端口信息发给master。
    2. 数据同步:可分为全量复制和部分复制两种模式。
    3. 同步完后,master发送写命令给slave,slave执行写命令。此阶段master-slave之间还维持心跳机制。
  4. 心跳机制:用于主从复制的超时判断、数据安全。心跳机制过程:master -> slave 发送ping;slave -> master 发送replconf ack。
  5. 主从复制可能出现的问题:延迟不一致(措施:监控延迟);数据过期(定期删、惰性删);故障切换(哨兵);复制超时、复制中断(超时释放资源或者重新建立连接)。

参考阅读:深入学习Redis(3):主从复制

18. 知道zookeeper吗?讲一下

19. 知道git flow吗?讲一下

Gitflow 工作流程使用两个并行的、长期运行的分支来记录项目的历史记录,分别是 master 和 develop 分支。

  • Master,随时准备发布线上版本的分支,其所有内容都是经过全面测试和核准的(生产就绪)。
    • Hotfix,维护(maintenance)或修复(hotfix)分支是用于给快速给生产版本修复打补丁的。修复(hotfix)分支很像发布(release)分支和功能(feature)分支,除非它们是基于 master 而不是 develop 分支。
  • Develop,是合并所有功能(feature)分支,并执行所有测试的分支。只有当所有内容都经过彻底检查和修复后,才能合并到 master 分支。
    • Feature,每个功能都应留在自己的分支中开发,可以推送到 develop 分支作为功能(feature)分支的父分支。

20. JVM中的堆最大量在32位,64位机器上的区别。

21. redis怎么实现过期的?

22. redis 的 lru?

有很多种实现,这里提两种:链表法,链表&HashMap 法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 链表法,伪代码
List<Integer> lru = new LinkedList<>();
void put(Entry entry){
if(this.get(entry.key) != -1){
lru.putHead(lru.get(entry));
}else{
lru.putHead(entry);
if(IsFull() == true){
lru.dropTail();
}
}
}

// 链表&HashMap 法 ,待补充

23. 用过微服务吗?

24. linux操作熟悉吗?

蘑菇街 Java 一面

1. 项目中做词法、语法解析了吗?

2. 项目中旧的字节码有没有做卸载?

JVM学习七:JVM之类加载器之类的卸载

精华:

1. 加载时,方法区形成某类的二进制数据(运行时数据结构),对应堆中该类的 class 对象(类的实例对象,唯一存在,除非类被卸载了,也就是比如`**ServiceImpl.class`指代的东西),之后不管是 new 还是反射或者 newInstance 拿到的都是另一种实例对象,跟上面的 class 对象不一样。
2. 卸载就是对 class 对象、classLoader 对象的引用都删除的过程。怎么删除?让栈中对 classLoader 的引用、对 class 对象的引用,对实例对象的引用,以及实例对象都置 null 即可。

3. 写多线程一般用到哪些类?

4. 多线程的可见性问题,为什么会有这个问题?

5. G1 原理?

6. maven 使用的中央仓库,还是自己做了 maven 镜像?

7. MySQL 数据库的主从复制。

8. TCP 的拆包、粘包问题。

前置知识:

  • UDP 是基于报文的,不会发生拆包、粘包现象。UDP 首部有一个参数会指出数据报文长度,因此在应用层可以很好地将不同的数据报文区分开。
  • TCP 是基于字节流的,在 TCP 的首部没有表示数据长度的字段,所以可能发生拆包、粘包的现象。

什么是拆包、粘包

  1. 接收端收到的一个包中,存在着发送端发来的两个包的数据,即出现了粘包,主要问题:接收端不知两个数据包边界。
  2. 接收端收到两个包,但这两个包要么是缺少一端,要么是多出一部分,主要问题:同时出现了拆包跟粘包。

发生的原因:

拆包:一次发送数据大于缓存区大小、最大报文长度。

粘包:缓存区多个包的数据一次性发出去;接收端应用层没有及时读取缓存区数据包。

TCP 采用的解决办法:

解决原则:让每一个数据包知道自己的边界信息。具体:

  1. 发送端可以将每一个包封装成固定的长度不足补0,接收端每次从缓冲区读取固定长度的数据。
  2. 在包之间设置边界,比如添加特殊标记等。
  3. 包首部增加包长度的字段。

参考资料:TCP粘包,拆包及解决方法

9. 一般 Web 开发会分为几层?

10. 如果使用单例模式拿到的对象,在 JVM 中只能有一个吗?

11. 一个Tomcat 可以部署多个项目吗?

12. 一个 Tomcat 是运行在一个 JVM 上的吗?(其实是问 Tomcat 跟 JVM 的关系)

JVM :Tomcat :J2EE = 1 : 1 : N。运行应用程序的 JVM 就是运行 Tomcat 的那个 JVM。

参考:面经整理5,进入页面搜索“Tomcat”即可。

13. 一个 JVM 上多个应用程序,他们有可能发生类冲突吗?jar 包冲突吗?

14. Tomcat 的 classLoader 架构图?

随手记 Java 一面

1. Redis 缓存有个过期时间,过期了也就没了,或者有一种比较实时的,修改时马上修改这个缓存吗?

  • 项目中采用的是高一致性的主动更新策略。拿到真实数据后,立即更新缓存数据。
    • 如果采用弱一致性的做法,可以只更新缓存,然后让缓存异步地批量更新数据库。
  • 对于交互时保存的缓存数据 ,设置过期时间。请求接口时,先请求Redis缓存,如果命中则返回命中数据,否则还是执行HTTP请求调用接口。

2. 更新记录时,你是怎么让Redis知道自己的缓存失效的?

3. JVM类加载的默认加载先后顺序。

检查类是否已被加载的检查顺序是自底而上,尝试加载顺序是自顶而下。
推荐阅读:java中类的加载顺序介绍(ClassLoader)

4. 四种GC算法的细节,优缺点比较。

5. 分代收集算法中,新生代使用什么算法?

6. 新生代、老年代各采用什么算法?为什么用?

7. 什么情况下会从新生代升级成老年代?

  1. 生命周期较长的对象进入老年代;
  2. 动态判定:相同年龄的对象的总内存超过了Survivor内存空间的一半的对象,进入老年代。
  3. Minor GC触发内存分配担保时;
  4. 大对象直接进入老年代。

8. 分配担保机制讲一下。

  1. 在Minor GC之前,JVM检查老年代最大可用连续可用空间是否大于新生代所有对象总空间。
  2. 如果大于,Minor GC 可以保证是安全的。
  3. 如果不成立,JVM 会检查对HandlePromotionFailure的设置是否允许担保失败。
  4. 如果允许担保失败(冒风险),会继续检查老年代最大连续可用空间是否大于历次晋升到老年代对象的平均大小。
    1. 如果大于,可以尝试进行一次有风险的GC;
    2. 如果小于,说明不愿意冒险,将进行一次Full GC。

9. 偏向锁是什么,什么情况下会取消偏向锁?

10. 锁的轻量级、重量级讲一下区分。

11. 轻量锁和偏向锁会在哪里做什么标记吗?

12. Spring是怎么解决循环依赖的?详细一点。

首先Spring不支持原型bean的循环依赖,也无法解决构造器中的循环依赖问题,这里指的都是单例bean。

  • 涉及到三种缓存:
    1. ① singletonObjects;② earlySingletonObjects;③ singletonFactories。
    2. 缓存①是完全初始化好的bean的缓存;
    3. 缓存②是存放原始bean的缓存;
    4. 缓存③是存放bean工厂的缓存。

创建bean 并顺便缓存的过程:

  1. doGetBean()方法开始,会先尝试从缓存1中获取bean,此对象可能有三种状态(null、原始bean、完全态的bean)。
  2. 若状态为 null(即缓存中没取到 bean),就需要创建bean,首先调用createBeanInstance()创建一个原始bean,然后将单例的 beanFactory 添加到缓存3中(从这个工厂就可以获取原始对象的引用,也就是所谓的“早期引用”)。
  3. 之后向原始 bean 中注入属性并解析依赖(所谓循环依赖,通常就卡在2.5步上)。
  4. 执行完成后,返回完全实例化后的 bean,同时放入缓存1中。

出现循环依赖时,取缓存的过程:

  1. 先从 singletonObjects 即缓存1中取bean 实例。如果没取到,则去 earlySingletonObjects 即缓存2中取,如果没取到,则从singletonFactories即缓存3中取出 ObjectFactory 对象,然后从中获取原始 bean 实例的引用(即早期引用)。
  2. 获取成功后,将该原始bean实例放入 earlySingletonObjects 即缓存2中,同时将 ObjectFactory对象从 singletonFactories 中移除。
  3. 拿到原始 bean 的引用,就可以完成另一个被依赖的 bean 的初始化了,如此循环依赖被解决。

推荐阅读:Spring-bean的循环依赖以及解决方式

13. Spring IOC 为解决循环依赖问题使用的缓存机制。

参考 上一题(T 12),此处不赘述。

14 .ConcurrentHashMap怎么保证在扩容操作时的线程安全?

本题分两步来看:

  1. 先看 HashMap 本身的扩容操作
  2. 再看 ConcurrentHashMap 的扩容 get put 操作

1. 先看 HashMap 本身的扩容操作

  • HashMap 的扩容:
  • 先涉及两个参数:newCap 和 newThreshold,newCap 通常是原来的2倍,阈值(Threshold)也变为原来的2倍。
  • 扩容后要将键值对Hash的重新计算,然后移动到合适的位置上去,如下:
    1. 在链表中,如果e.hash & oldCap == 0,则保持在原本的位置上,并且相同计算结果的结点按原来的相对位置接在后面。
    2. 如果e.hash & oldCap == 1,则这些结点都要放在原位置j + oldCap 的位置上,这些结点相对位置不变。
    3. 在红黑树中,如果需要扩容操作,红黑树也需要拆分后重新映射。研究拆分之前,建议先阅读下边的扩展内容——红黑树的树化步骤。现在说一下拆分过程:首先红黑树中保留了原链表结点的 next 指针,所以分组方式跟原链表完全相同,将分成两种不同的链表。
    4. 红黑树拆分后变成两个链表,长度自然会变短,如果长度小于等于 6 ,那么此半个红黑树将保持链表状态;如果长度超过 6 ,那么将继续树化,成为一颗红黑树。

推荐阅读:java-并发-ConcurrentHashMap高并发机制-jdk1.8

扩展:红黑树的树化步骤,如下:

  1. 将链表普通结点改造成 TreeNode 树形节点链表;
  2. 将得到的链表转化成红黑树。
    1. 形成红黑树时需要比较结点间的大小:① 首先比较 hash 的大小;② 如果相等,则检查键类是否实现了 Comparable 接口,若是则调用 compareTo 方法进行比较;③ 若仍无法比较大小,则调用tieBreakOrder()方法进行仲裁,仲裁后就有大小的区别了。
    2. 链表转红黑树后,原链表的连接顺序依旧被保留了下来(next 指针来实现)。

2. 再看 ConcurrentHashMap 的各种骚操作的线程安全:

以下内容来自:ConcurrentHashMap源码分析(1.8)

0. 使用 Unsafe 的方法执行的原子性操作

tabAt() 用来返回节点数组的指定位置的节点的原子操作。
casTabAt() cas原子操作,在指定位置设定值
setTabAt() 原子操作,在指定位置设定值

0.1 关于sizeCtl 变量

-1 :代表table正在初始化,其他线程应该交出CPU时间片
-N: 表示正有N-1个线程执行扩容操作(高 16 位是 length 生成的标识符,低 16 位是扩容的线程数,最大 65535)
大于 0: 如果table已经初始化,代表table容量,默认为table大小的0.75,如果还未初始化,代表需要初始化的大小

1. 初始化操作:

首先有一个执行“初始化操作”的线程,然后观察 sizectl 参数,如果小于 0 ,此线程自旋等待;如果大于等于 0 ,则利用 CAS 操作将其设为 -1,此 CAS 操作保证以下操作的线程安全:为数组开辟内存,将 sizeCtl 设为数组长度的 3/4(即sc = n - (n >>> 2))。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 初始化完整源码:
/**
* Initializes table, using the size recorded in sizeCtl.
*/
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}

2. put 操作

  1. 先拿到欲添加的 key 的 hash(执行(h ^ (h >>> 16)) & HASH_BITS;)。
  2. 若 table 还没有申请到内存,则先执行初始化操作,即本题上一节。
  3. 若将要放置的位置没有元素,会执行casTabAt()方法尝试添加。
  4. 若检测到当前元素的hash为moved状态(说明正在执行transfer()操作,此操作会调用ForwardingNode()方法,此方法会将元素的hash设置为moved)。说明正处于数组扩张的数据复制阶段,则此线程也会参与去复制即helpTransfer,通过允许多线程复制的功能,以此来减少数组的复制所带来的性能损失。
  5. 若当前位置有元素,则使用 Synchronized 的方式加锁,对以下操作进行线程安全控制:① 若是链表,遍历链表,若同hash同key,则替换该value;不然,新建node加到链表末尾???存疑,可详看下方链接② 若是红黑树,则添加到红黑树中。

大神打架:关于HashMap在put时Node插入方向的问题

3. get操作

get 操作无锁,支持并发

4. 链表转树操作

  1. 在执行treeifyBin()转树方法时,若桶的数量小于 64 时,优先触发扩容操作,细节参考本题下一节:扩容操作。
  2. 若桶数量多于 64 时,使用Synchronized 方式加锁,对以下操作进行线程安全控制:① 将普通结点转换为 TreeNode 结点;② 将 TreeNode 组成的链表构造出 Treebin 对象,在 Treebin 对象的构造方法中,链表被转换成了红黑树。

5. 扩容操作

首先调用tryPresize()方法(支持并发),确定扩容的目标值(决定扩容的次数),以及根据sizeCtl参数选择进入不同的分支。
最终来到transfer方法处。

  1. 首先如果多线程一起进行扩容操作,那么每个线程最少处理 16 个长度的数组元素,以避免此方法占用过多的 CPU 使用。
  2. 第一个进入扩容的线程负责初始化一个新的table,长度是旧的两倍。
  3. 然后分配一个区间的桶(一般是16 个)给此线程,完成下标的控制。
    1. 如果扩容结束,可以尝试领取新的区间;如果无法领取,那么 sizeCtl 减一,扩容的线程减少一个。
    2. 如果数组i处桶是空的,就尝试用 CAS 占位,将占位符 fwd 插入。
    3. 如果桶不是空,而且已经有了占位符,说明已有其他线程处理过此操作,那么当前线程将跳过这个桶。
    4. 如果以上都不是,而且扩容操作没有完成,那么将开始同步处理这个桶。
  4. 处理每个桶的行为是同步的,使用Synchronized关键词修饰,剩下的操作与HashMap基本一致,不再赘述。

15. 公平锁非公平锁讲一下。

16. CountDownLatch 和 CyclicBarrier。

countDownLatch是倒计时器,可以用于模拟多线程同时触发验证并行性的场景。一个或者多个线程,等待其他多个线程完成某件事情之后才能执行。

  1. 主线程 new 一个 CountDownLatch(同时指定计数的个数),然后开启线程池,紧接着执行countdownlatch.wait()方法,主线程阻塞。
  2. 线程池的业务代码中执行 countDownLatch.countDown()将个数减一,等到个数减为零时,主线程从 await()处被唤醒。

CyclicBarrier是循环栅栏,可以用于多线程计算数据,最后合并计算结果的应用场景。多个线程互相等待,直到到达同一个同步点,再继续一起执行。

  1. 主线程 new 一个 CyclicBarrier(同时指定计数的个数),然后开启线程池。
  2. 线程池的业务代码中执行 cyclicBarrier.await()方法,线程被阻塞,等到有足够个数的线程被阻塞时,这些线程会被唤醒继续执行 await()后面的代码。

参考链接:AQS文末

补充:将Redis配置为缓存,在Spring中是怎么做的?

  1. 配置三样东西:①Spring对缓存的支持也就是cacheManager(本次采用的实现类:RedisCacheManager);②Redis对话使用的RedisTemplate,③连接工厂。

  2. 首先启动Spring 缓存支持,创建一个CacheManager的Bean,

  3. 使用的三个注解:Cacheable、CacheEvit、CachePut。

    1. Cacheable:当重复使用相同参数调用方法的时候,方法本身不会被调用执行,即方法本身被略过了,取而代之的是方法的结果直接从缓存中找到并返回了。
    2. CacheEvit: 调用时会删除掉数据库和缓存里面的值。可以在@CacheEvict 里面添加condition 表达式,让其满足什么条件的时候才删除缓存。可以设置是否清除掉缓存中所有数据。
    3. CachePut:使用@CachePut标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。

    使用 spring-cache 有三个步骤:

    在启动类上加入 @EnableCaching 注解;

    使用 CacheManager 初始化要使用的缓存框架,使用 @CacheConfig 注解注入要使用的资源;

    使用 @Cacheable 等注解对资源进行缓存。
    而针对缓存操作的注解有三个:

    @Cacheable 表示如果缓存系统里没有这个数值,就将方法的返回值缓存起来;

    @CachePut 表示每次执行该方法,都把返回值缓存起来;

    @CacheEvict 表示执行方法的时候,清除某些缓存值。
    非常简单,对缓存的操作也无非是 CRUD。

    <<<<拉勾教育、八点一刻:Redis缓存一致性设计

补充:Redis不支持事务回滚,那么事务中Redis崩溃怎么办?

  1. 使用Aof持久化方式时,Redis服务器如果宕机,可能只执行了事务中的一部分操作;
  2. 那么Redis服务器会在重启前检查上述状态,同时退出运行,并输出报错信息;
  3. 可以借助redis-check-aof工具修复上述的只增文件,会把执行不完全的事务删除。

为什么不支持回滚?因为Redis认为回滚无法解决任何程序错误问题,而为了运行速度的考虑,所以不支持回滚。

补充:Java中什么时候会出现内存泄漏,举个栗子?

【important】错误地保持了强引用(比如赋值给了static 变量),那么对象就可能没机会转变为类似弱引用的可达性状态了。判断内存泄漏的思路:检查弱引用指向的对象是否被垃圾收集。

定义就是:不再被使用的对象的内存不会被回收。

  1. 单例对象持有其他短生命周期对象的引用。
  2. 静态集合类中引用的对象,如果对象需要移除时,要把集合对象置null,整个集合 clear 掉。

举例如下。

1
2
3
4
5
String num = New String("abc");
List<String> list = new ArrayList<String>();
list.add(num);
num=null;
list.clear();//用完后需要及时 clear 掉

“abc”字符串有两个强引用指向它,num 和 list 集合,使用完后,都需要进行处理。

补充:静态内部类

静态内部类不需要依赖外部类的实例,也无法访问外部类的非静态的变量和方法。

补充:初始化顺序

存在继承的情况下,初始化顺序为:

  • 父类(静态变量、静态语句块)
  • 子类(静态变量、静态语句块)
  • 父类(实例变量、普通语句块)
  • 父类(构造函数)
  • 子类(实例变量、普通语句块)
  • 子类(构造函数)

补充:Redis的禁忌操作有哪些,注意事项,优化策略?

1. 键值设计

  1. key名设计
    • 原则:无特殊字符。
    • 建议:可读、可管理、简洁。
  2. value设计
    • 原则:拒绝大key,防止网卡流量、大查询。
    • 建议:控制数据类型合适、控制key的生命周期;使用hash,set,zset,list等对存储量过多的元素进行优化(比如100个桶,先hash取模,找到某一个key)。

2. 命令设计

  1. 注意O(N)命令,尽量避免使用;
  2. 禁止线上使用keys、flushall、flushdb等命令;
  3. 合理使用select;
  4. 使用批量操作提高效率。
  5. Redis事务较弱,建议不要过多使用
  6. Redis集群版本在使用Lua上有特殊要求
  7. monitor命令慎用。

3. 配置优化

  1. 限制同时连接的客户数量。
  2. 设置客户端连接时的超时时间,单位为秒。
  3. 限制脚本的最长运行时间,默认为5秒钟。
  4. 内存淘汰策略的选择。

4. 集群批量操作的优化

  1. IO优化的思路:

    (1) 命令本身的效率:例如sql优化,命令优化
    (2) 网络次数:减少通信次数
    (3) 降低接入成本:长连/连接池,NIO等
    (4) IO访问合并:O(n)到O(1)过程:批量接口(mget)

5. 其他优化

  1. redis间数据同步可以使用:redis-port
  2. 热key寻找

墙裂建议阅读:Redis注意事项及常见优化

补充:Redis中的Value值太大怎么办?

Value最多可以容纳的数据长度是512M。
可以用阿里云的大key搜索工具

推荐阅读:Redis中String类型的Value最大可以容纳数据长度

补充:高并发情况下,如何保证Redis缓存的一致性?

补充:分布式锁的实现

参考这一节的内容 :advanced-java 分布式锁

分布式锁主要从① 互斥②不能死锁③容错三个考点来陈述:

1. Redis 实现的分布式锁

Redis 使用 SETNX 命令来实现分布式锁,但有“原生 setnx”和“RedLock”两种方案

1.1 原生 setnx 方案

setnx 全称 set is not exists 。

redis 2.6之前的方案: SETNX key value

redis 2.6之后的方案:现在都以 SET + NX 参数的方案为主

1
SET resource_name my_random_value NX PX 30000
  • NX:表示只有 key 不存在的时候才会设置成功。(如果此时 redis 中存在这个 key,那么设置失败,返回 nil
  • PX 30000:意思是 30s 后锁自动释放。别人创建的时候如果发现已经有了就不能加锁了。
  • 返回 1,设置 key 成功;返回 0,设置 key 失败

使用以下 lua 脚本删除 key:

1
2
3
4
5
6
-- 删除锁的时候,找到 key 对应的 value,跟自己传过去的 value 做比较,如果是一样的才删除。
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end

缺陷:因为集群中数据使用异步保证数据的一致性,假设clientA 从 master 处拿到了锁,但 master 未完成数据的同步,此时 master 发生了 crash,系统将重新选举 master,clientB 可以从新 master 处拿到锁,于是 clientA 和 clientB 都获取到 key 的锁,集群中的缺陷就此暴露出了。(lee 理解:脑裂时不能保证互斥性)

改进办法:采用 RedLock 方案。

1.2 RedLock 方案

这个场景是假设有一个 redis cluster,有 5 个 redis master 实例。然后执行如下步骤获取一把锁:

  1. 获取当前时间戳,单位是毫秒;

  2. 跟上面类似,轮流尝试在每个 master 节点上创建锁,过期时间较短,一般就几十毫秒;

  3. 如果尝试创建锁失败,无论什么原因,一旦失败就立即尝试下一个节点。

  4. 尝试在大多数节点上建立一个锁,比如 5 个节点就要求是 3 个节点 n / 2 + 1

  5. 客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了;

  6. 要是锁建立失败了,那么就依次之前建立过的锁删除;

  7. 只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁

创建锁失败的原因可能是:当前 key 的锁已经被其他 client 占有、master 节点不可用

推荐阅读:《Redis官方文档》用Redis构建分布式锁

2. zk 实现的分布式锁

2.1 临时 znode 方案

zk 分布式锁,其实可以做的比较简单,就是某个节点尝试创建临时 znode,此时创建成功了就获取了这个锁;这个时候别的客户端来创建锁会失败,只能注册个监听器监听这个锁。释放锁就是删除这个 znode,一旦释放掉就会通知客户端,然后有一个等待着的客户端就可以再次重新加锁。

2.2 临时顺序节点方案

如果有一把锁,被多个人竞争,此时需要排队,第一个拿到锁的人会执行,然后释放锁;后面的每个人都会去监听排在自己前面的那个人创建的 node 上,一旦某个人释放了锁,排在自己后面的人就会被 zookeeper 给通知,一旦被通知了之后,就 ok 了,自己就获取到了锁,就可以执行代码了。

基本步骤:

1.建立一个节点,假如名为:lock 。节点类型为持久节点(PERSISTENT)
2.每当进程需要访问共享资源时,会调用分布式锁的lock()或tryLock()方法获得锁,这个时候会在第一步创建的lock节点下建立相应的顺序子节点,节点类型为临时顺序节点(EPHEMERAL_SEQUENTIAL),通过组成特定的名字name+lock+顺序号。
3.在建立子节点后,对lock下面的所有以name开头的子节点进行排序,判断刚刚建立的子节点顺序号是否是最小的节点,假如是最小节点,则获得该锁对资源进行访问。 (lee 理解:为什么是最小?因为拿到锁并释放锁的节点,会删除它的 znode,如果轮到当前节点,那么它所属的顺序号就应当是最小的)
4.假如不是该节点,就获得该节点的上一顺序节点,并给该节点是否存在注册监听事件。同时在这里阻塞。等待监听事件的发生,获得锁控制权。
5.当调用完共享资源后,调用unlock()方法,关闭zk,进而可以引发监听事件,释放该锁。
实现的分布式锁是严格的按照顺序访问的并发锁。

3. etcd 实现的分布式锁

[扩展]:有关 raft 协议大神级动画:raft,真不知道哪个神仙画的,太棒了!

对于每一个锁比如名字为 mylock,实际写入 key 时就叫做 key1=mylock/uuid1,如果两个 client 同时写 mylock 锁,写操作都会成功,但在 mylock 目录下会同时存在不同 uuid 的两个 key。UUID 可以保证全局的唯一性。使用每个 key 对应的一个自增的Revision号(进行一次事务,revision 自增 1),此 Revision 会返回给创建 key 的 client,由 client 记录下来。client 取 key 时,会把 mylock/下的所有 key-value 对都拿到,然后通过 revision 号来判断自己是否获得了锁。

租约:client 创建 key-value 时要设置租约期,租约到期时 key-value 会被删除,同时也可以被client续约。

避免死锁的方式:持有锁的 client 会创建一个定时任务作为心跳对 key 进行续约,一旦此 client 故障,那么租期到了就会自动释放锁,允许其他 client 来获取。

这里写的很详细(甚至有大量篇幅介绍etcd 以及 raft 协议),有兴趣可以了解:分布式锁的最佳实践之:基于 Etcd 的分布式锁

三种实现分布式锁方式的区别

  1. redis 方式,需要应用自己不断地去获取锁,比较消耗性能。而且如果请求锁的 app 挂掉了,需要等待超时时间后才能释放锁。
  2. zk 方式,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小。如果请求锁的 app 挂掉了,那么 znode 就会被删除,无需等待超时时间。通过一个自增序号判断 client 是否为获得锁的 client。
  3. etcd 跟 zk 很接近,也是通过一个自增序号判断是否为获得锁的 client。

另:这个链接里面,涉及到Redisson 方案,以及使用 setnx 时存在的缺陷,值得一看:阿里云专访Redisson作者Rui Gu:构建开源企业级Redis客户端之路

补充:网络并发量突增时,该怎么处理?

  1. 如果是几何型的递增,只能增加应用的集群节点、数据库集群节点或分布式模块管理。
  2. 如果是不规则的高峰模式,可以从应用和数据模型着手,减少服务器请求时间。
    1. 必要的数据缓存;
    2. 合理的静态化页面;
    3. 多节点应用集群;
  3. 如果高并发来自于恶意请求,不仅要改bug,还要限制IP访问。
  4. 请求放到消息队列里面;
    1. 优点:异步处理请求,消峰、降低系统耦合
    2. 缺点:一致性问题、可用性问题、复杂性问题。
  5. 图形验证码;
  6. 做服务降级。

补充:秒杀场景下保证库存数据的正确性

库存数据只需要达到最终一致,使用 MQ 做事件驱动加上 Redis 事务即可。增加/扣减库存时用 Redis 事务做原子操作,每次扣减库存时生成一个唯一 ID,归还时带上该唯一 ID 用于做幂等操作。下单出错或者关单归还库存时可以通过 MQ 异步做最终的事务补偿。

补充:服务器负载历史记录怎么查看?

shell 中的 uptime 命令,top 命令等都行

补充:数据库死锁的例子

MYSQL数据库死锁的原因和解决方法

网易 Lua 一面

1. Mybatis 在 SSM 框架中充当什么角色?

  1. 主要完成对 JDBC 的封装,去掉了繁琐的 JDBC 代码和结果集的设置。
  2. 使用 XML 或注解,将接口和普通Java类映射成数据库中的数据,解除了 sql 跟代码的耦合。

2. JDBC 做了哪些事情呢、以及 JDBC 的设计模式?

  1. 跟数据库建立连接,

  2. 使用 statement 执行 SQL 语句。(或者使用PreparedStatement,用?代替指定字符串,进行预编译,更加高效,还可防止SQL注入)。

  3. 执行查询,返回 ResultSet,循环调用next方法,获取每一行内容。

  4. 任何一个 jdbc 的 Driver 必须类似以下格式:

    1
    2
    3
    4
    5
    6
    // 向 DriverManger 注册自己
    public class MyJDBCDriver implements Driver {
    static {
    DriverManager.registerDriver(new MyJDBCDriver());
    }
    }

    使用 jdbc 时,使用Class.forName(com.**.**.MyJDBCDriver),相当于加载这个driver,上边代码中的 static 代码块就会执行,完成了MyJDBCDriver的实例化。

  5. 补充:jdbc 主要使用了桥接模式

    1. 桥接模式核心:一个抽象类使用了指向另一个接口的引用。抽象类可以有多种实现子类,接口也可以有多种实现,作用:使用桥接模式就是为了让抽象部分和实现部分都能够独立变化。抽象类设为 AC,接口设为 API,接口实现设为 Class1、Class2,AC 继承子类设为 ClassA、ClassB,真实使用代码为:

      1
      2
      ClassA classA = new AC(new Class1());
      ClassB classB = new AC(new Class2());

    2. 至于 jdbc 的桥接模式,参考4/5两项,不再做多余解释。

3. IDE 项目中另外起一个JVM、进程来做,有什么思路吗?

5. 跳表的建立规则,它怎么决定一个数据要不要上升到上一层。

一个链表内每一个结点可能包含多个指向后续元素的指针,后续节点个数是通过一个随机函数生成器得到

跳表是通过随机函数来决定某个数据要不要去哪一层。

推荐阅读:跳表(SkipList)

6. CPU 缓存的更新和替换策略。

CPU 采用了三级高速缓存,替换算法主要有三种:FIFO、LFU、LRU。其中LRU最常用。

7. 内存的值发生了变化后,cache要如何感知呢?

先看 cache 的写入策略:

  1. 写回法:当CPU写Cache命中时,只改变其缓存的内容,而不写入内存,直到替换策略把该块替换出来时才写入内存。这种方法减少了访问内存的次数,缩短了时间,也提高了内存带宽利用率,但在保持与内存内容的一致性上存在在隐患,并且使用写回法,必须为每个缓存块设置一个修改位,来反映此块是否被CPU修改过。
  2. 全写法:当写Cache命中时,立即在所有的等级存储介质里更新,即同时写进Cache与内存,而当Cache未命中时,直接向内存写入,而Cache不用设置修改位或相应的判断器。这种方法的好处是,当Cache命中时,由于缓存和内存是同时写入的,所以可以很好的保持缓存和内存内容的一致性,但缺点也很明显,由于每次写入操作都要更新所有的存储体,如果一次有大量的数据要更新,就要占用大量的内存带宽,而现在PC系统中,内存带宽本来就不宽裕,而写操作占用太多带宽的话,那主要的读操作就会受到比较大的影响。
  3. 写一次法:这是一种基于上面两种方法的写策略,它的特点是,除了第一次写Cache命中的时候要写入内存,其它时候都和写回法一样,只修改缓存。其实这也就是一种对缓存一致性的妥协,使得在缓存一致性和延迟中取的一个较好的平衡。

现在的CPU一般都有多个核,我们知道当某个核读取某个内存地址时,会把这个内存地址附近的64个字节放到当前核的cache line中,假设此时另外一个CPU核同时把这部分数据放到了对应的cache line中,这时候这64字节的数据实际上有三份,两份在CPU cache中,一份在主存中。自然而然就要考虑到数据一致性的问题,如何保证在某一个核中的数据做了改动时,其它的数据副本也能感知到变化呢?是由缓存一致性协议来保证的。缓存一致性协议也叫作MESI协议。
除了一致性协议外,还需要内存屏障的配合。

这两部分就不展开了,可以参考下面两个链接。

内容参考:性能服务端系列 – 处理器篇
扩展阅读:关于CPU Cache – 程序猿需要知道的那些事

8. select、poll、epoll的区别?

往下翻,参考:趣链 Java 一面之二 的第一题。

9. 实现一个数据结构,拥有栈的pop和push,同时提供一个min函数可以取最小值,怎么实现?

提供两个栈,stack1 和 stack2,stack1进行栈的基本操作,stack2的栈顶作为min的记录,如果压栈的元素小于stack2的栈顶元素,则压入stack2,如果出栈的元素等于stack2的栈顶元素,说明min元素要出栈,此时stack2的栈顶元素pop出。

10. 接上一题,如果要你提供多个最小值,你要怎么做?

接上题解法,stack2的后续几个元素就是多个最小值。

11. 大量数据的并行化处理思路

背景:8G 甚至更多的数据,无法一次性放入内存中,所以需要分治的思想(甚至可以使用多线程并行处理),有两种策略:

  1. 快速排序分治。先扫一遍数据,按大小分16 个区间(快排),并把数据放入。然后可以启动 16 个线程并行对这些数据进行排序,小区间排序完成后,整体数据就有序了。
  2. 归并排序分治。不提前扫数据,直接分成 16 个小数据集合,启动 16 个线程进行排序,排序完成后再将有序集合合并。

网易 Lua 二面

  1. 如何避免外来Java代码任意创建多线程等其他危险操作?

    只知道 redis 中可以嵌套 lua 脚本来实现自定义的逻辑。

招银网络 Java 一面

1. 事务中用了try-catch捕获了异常,那么事务还会回滚吗?

将异常捕获,并且在catch块中不对事务做显式提交(或其他应该做的操作如关闭资源等)=生吞掉异常.
一般不用try-catch捕获异常。如果非要捕获,那就要在catch语句块中显式地抛异常/显式地回滚。

try catch异常抛出与spring事务回滚策略相关

2. Servlet 的生命周期

  1. 加载
  2. 初始化
  3. 处理请求
  4. 服务终止
    Servlet生命周期

4. 事务A,包含语句B,B异常时,事务A会回滚吗?

会。

5. web.xml 能配置什么信息

过滤器、监听器、applicationContext、Servlet等。

6. String 跟 StringBuffer、StringBuilder的区别

String 是不可变类,StringBuffer 是可变类,线程安全。StringBuilder 是可变类,线程不安全。

从效率来讲,String -> StringBuffer -> StringBuilder, 先降后升。

String不可变的优点之一:String 的 hash 值也不可变,只需要计算一次,有利于作为 hashmap 的 key。

String 补充:

如果 new 一个 String,那么 String Object存在于堆里,如果 String str = “abc”,将不在堆里,而在字符串常量池中有字面量“abc”和 String Object。

7. wait 和 sleep 的区别

  1. sleep,让CPU不让锁。
  2. wait,放弃锁。

补充:用户级线程与内核级线程的区别:

包括有没有陷入内核:
阅读:用户线程与内核线程的区别

IBM Java 二面

1. 项目中 MySQL 数据表的设计有遵循范式吗?第二范式的要求?

  • 第二范式:在第一范式要求的基础上(表是平表),要求每一个非主属性完全函数依赖于码。

  • 3NF:首先属于2NF,然后每一个非主属性不传递依赖于码。任何非主属性不依赖于其他非主属性。

  • intent与Sentence的一对多关系。

2. 查询过程中碰到性能方面的问题吗?

虽然没有碰到,但还是尽量采用比较好的设计。比如:

  1. SQL优化
    1. 列出查询字段,避免select *;
    2. 索引列不能含 null,建索引会失败
    3. 避免通配符%,出现在搜寻词首,该列索引将不生效
    4. 避免 orderby中的计算表达式或非索引项
  2. 利用冗余设计,避免表连接。

3. 分页是怎么实现的?这个工具是一个Jar包吗?Sql语句中怎么实现?

用的PageHelper,引入PageHelper的依赖,PageHelper类实现了interceptor接口,是mybatis的拦截器。

  1. 传入当前页面、每页记录数,赋值到Page类中(,同时赋值到ThreadLocal中,成为线程私有);
  2. 然后PageHelper实现了interceptor接口,通过拦截器获取到Page类的参数,然后在SQLparser中完成分页SQL语句的拼装,最终完成分页操作。

不使用PageHelper,单纯sql实现:

1
2
3
4
5
6
7
8
9
10
// limit可以接受一到两个参数:
如果是一个参数,表示返回最大的记录行数目(LIMIT n 等价于 LIMIT 0,n)。
如果是两个参数,第一个指定返回记录行的偏移量(起始位置),第二个指定返回记录行的最大数目(记录条数)。
SELECT * FROM table LIMIT 5,10

SELECT * FROM table LIMIT 95,-1; // -1 表示到末尾

// limit offset用法:
返回四行、偏移量9
SELECT * FROM table LIMIT 4 OFFSET 9;

4. 你的Redis做缓存,key、value是怎么设计的?

5. Spring Schedule 的底层实现?执行频率?表达式?

  1. 实现 SchedulingConfigurer 接口,重写 configureTasks 方法。
  2. 创建一个 trigger触发,并增加一个runnable的task,放入业务逻辑。
  3. 从数据库中取出自定义的 cron 语句,然后得到 CronTrigger。
  4. 0 0 8 * * ?*每天8点执行一次

6. 批量数据的导入导出?有什么效果?easyExcel有什么突出的亮点?

使用EasyExcel进行批量数据的导入导出,具有映射到Java类的功能。

  • 导出时,可以自动生成表头。导入时,可以根据excel中的列号映射到java模型中。

EasyExcel的使用步骤:

  1. 添加maven依赖。
  2. 加上ExcelProperty的注解。

7. 如果你的API出现性能问题,你会怎么考虑去调优它?

  1. 架构设计上:
    1. 应用服务器设置集群、增加反向代理和负载均衡。
    2. 业务层可以使用Dubbo等RPC框架实现分布式调用,达到多节点同时处理计算。
    3. 使用redis、es等nosql实现存储。
  2. 代码角度上:
    1. 将某些环节设置为异步处理,比如本项目中,分析意图和调取用户信息的任务可以异步进行。在Future模式下,先返回一个future给调用者,等需要结果时再调用future.get()获得结果。唯一需要注意的是要设置一个超时时间。
    2. 检查线程池、数据库连接池的配置是否合适。
    3. sql 优化:减少聚合函数、增加必要索引等。

8. 你用JVM能做哪些方面的调优呢?

【强推!】R 大推荐 JVM 书籍:豆列:从表到里学习JVM实现

举个栗子,以高可用、低延迟为调优目标:

  1. 需要量化GC时间和频率对响应时间和可用性的影响。

上图说明:降低单次GC时间和GC次数,可以有效减少GC对响应时间的影响。

  • 选用合适的GC 收集器、重新设置内存比例、调整JVM参数等。

1. Major GC 和 Minor GC 太频繁

背景:新生对象太多、存活太少。动态年龄低的时候就已经晋升老年代,引起频繁Major GC。
步骤:

  1. 先尝试增加 Eden 空间,Minor GC频次降一半。
    • 虽然Eden区的扫描时间增加一倍,但是Minor GC 的间隔时间是以前的两倍,那么存活对象的数量将减小(短命对象就是优化点),这时对象的复制耗时会降低。
    • 所以需要确定对象的生命周期分布情况。
  2. 检查 new threshold 参数,也就是动态年龄判断(对象的晋升年龄阈值,很可能低于15)

2. 请求高峰期发生GC ,导致服务可用性下降。

背景:CMS 的重新标记阶段是STW的,所以需要降低此时间。

  • 跨代引用:重新标记阶段中,新生代持有指向老年代对象的引用,就是跨代引用。(虽然CMS是针对老年代的,但还是需要扫描)

  • 所以CMS的重复标记阶段要全堆扫描,那么堆中对象的数目影响了Remark阶段耗时。降低Remark阶段耗时问题转换成如何减少新生代对象数量。

  • Remark 前又一次可中断的预清理阶段,等待Minor GC的发生(有时限,超时会等不到Minor GC) 。

  • 优化 CMSScavengeBeforeRemark参数(增加此数值),用来保证Remark前强制进行一次Minor GC。 消除部分不可达对象,降低后期正式扫描时需要扫描的对象

强推:CMS 过程分析:图解CMS垃圾回收机制,你值得拥有
推荐阅读:美团GC实例

3. 比较 CMS、G1、ZGC

  1. 目标。CMS、G1:最短回收停顿时间;
  2. 共同特点:
    1. 都是并发清除器,对 CPU 敏感,
  3. CMS 特点:
    1. 标记清除算法,存在大量空间碎片,需要一次 Full GC 来处理(可以设置多次 CMS 触发一次 Full GC)。
    2. CMS 使用空间列表用于对象分配内存。
    3. 只作用于老年代和永久带。
  4. G1特点:
    1. 切分多个 Region,每次回收含垃圾最多的 Region(而不是全部),从而降低停顿。
    2. 可设置最大停顿时间。
    3. 使用写屏障。
  5. ZGC 特点:
    1. 与 G1类似,都使用了Region(在 ZGC 中称为 Page),但 ZGC 分区不是为了减少停顿,。
    2. 不设最大停顿时间。
    3. GC 的停顿时间,不随堆的规模和存活对象的规模变化而变化。
    4. 但使用读屏障,而且采用并发压缩的过程。
    5. 建议查看本站另一篇文章,梳理的很清晰:ZGC 特性解读

9. Git 的git add commit 和push 的区别?

git add 是把文件添加到暂存区。
git commit 提交更改,把暂存区的所有内容提交到当前分支上。
git push 是将本地分支推送到远程分支。

补充:git面试题汇总:

  1. Git branch name 创建名字为name的branch
  2. Git checkout xxx_dev 切换到名字为xxx_dev的分支
  3. Git pull 从远程分支拉取代码到本地分支
  4. Git checkout -b name 创建并切换到name
  5. Git push origin name 执行推送的操作,完成本地分支向远程分支的同步
  6. Git log filename 查看文件提交历史
  7. Git log branch file 查看分支提交历史
  8. 我们在本地工程常会修改一些配置文件,这些文件不需要被提交,而我们又不想每次执行git status时都让这些文件显示出来,我们该如何操作?
    • 答:在Git工作区的跟目录下创建一个特殊的.gitignore文件,然后把忽略的文件名编辑进去,Git就会自动忽略这些文件。
  9. git提交代码时候写错commit信息后,如何重新设置commit信息?
    • 答:可以通过Git commit –amend 来对本次commit进行修改。
  10. 什么时候应使用 “git stash”?
    • git stash 命令把你未提交的修改(已暂存(staged)和未暂存的(unstaged))保存以供后续使用,以后就可以从工作副本中进行还原。
  11. 如何从 git 中删除文件,而不将其从文件系统中删除?
  12. 如果你在 git add 过程中误操作,你最终会添加不想提交的文件。但是,git rm 则会把你的文件从你暂存区(索引)和文件系统(工作树)中删除,这可能不是你想要的。所以:换成 git reset 操作。
  13. git 常规命令:

    git commit:是将本地修改过的文件提交到本地库中;
    git push:是将本地库中的最新信息发送给远程库;
    git pull:是从远程获取最新版本到本地,并自动merge;
    git fetch:是从远程获取最新版本到本地,不会自动merge;
    git merge:是用于从指定的commit(s)合并到当前分支,用来合并两个分支;

    1. git clone 本地

补充:Mybatis的逆向工程

使用MybatisGenerator工具和mybatis-generator-gui界面工具,根据现有数据库表结构的基础上,自动生成bean、sql语句的xml、Mapper等文件
步骤:

  1. 连接到数据库中。
  2. 指定各文件的存放位置。
  3. 生成代码。

补充:将Entity类实例Null 的属性字段过滤掉的最佳实践。通常用在 JPA 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// lee 理解:通过 Wrapper修饰 src,然后取出所有的属性,过滤掉实例中属性为 null 的属性
// lee 认为,这种设计模式也很值得学习
public class JpaUtil{
public static void copyNotNullProperties(Object src, Object target){
BeanUtils.copyProperties(src, target, getNullPropertyNames(src));
}
private static String[] getNullPropertyNames(Object object){
final BeanWrapper wrapper = new BeanWrapperImpl(object);
return Stream.of(wrapper.getPropertyDescriptors())
.map(PropertyDescriptor::getName)
.filter(propertyName -> wrapper.getPropertyValue(propertyName) == null)
.toArray(String[]::new);
}
}

补充:配置Spring的方式:

  1. XML 文件;
  2. 注解;
  3. Java配置:

Spring对Java配置的支持是由@Configuration注解和@Bean注解来实现的。由@Bean注解的方法将会实例化、配置和初始化一个新对象,这个对象将由Spring的IoC容器来管理。**@Bean声明所起到的作用与<bean/> 元素类似**。被@Configuration所注解的类则表示这个类的主要目的是作为bean定义的资源。被@Configuration声明的类可以通过在同一个类的内部调用@bean方法来设置嵌入bean的依赖关系。

推荐阅读:请搜关键词:Spring配置

补充:Restful 说一下:

  1. 网络上的信息定义为一种资源
  2. 使用 HTTP 协议中的 get、post、put、delete等操作方式代表资源的增删改查操作。
  3. 个人理解:比较理想化、不太适合复杂业务逻辑的项目。

补充:反射执行Java代码的优缺点?

优点:

  1. 能够动态获取类的实例,提高系统的灵活性和扩展性;
  2. 与Java动态编译相结合,可以实现更多功能。

缺点:

  1. 性能较低;
  2. 反射相对不太安全;
  3. 破坏了类的封装性, 可以获取这个类的私有方法和属性。

反射之本地实现

  1. 方法调用时,也就是将传入的参数准备好,执行 Method.invoke()方法,然后调用进入目标方法。
  2. 此方法会调用MethodAccessor接口的invoke()方法,然后进入委派实现DelegatingMethodAccessorImpl(),再然后进入本地实现NativeMethodAccessorImpl最终达到目标方法。
  3. 就是说会通过 Java 调用 C++,然后再转到 Java,比较耗时,适合只执行一次的目标方法,如果想多次执行,就会切换到动态实现了(调用次数超过15次,就由委派实现切换到动态实现)。

反射之动态实现

  1. 动态实现是一种将方法动态生成字节码的实现方式,先经过十分耗时的“生成字节码”的操作,然后通过字节码进行反射却不怎么耗时(比本地实现效率高上20倍)。
  2. 对比两种方式,如果是仅执行一次的方法,那么本地实现比较划算,如果是要多次执行的热点代码,将会切换到动态实现,通过字节码来执行反射更加合理。这种情况十分类似于 Java 代码中的解释执行跟编译执行的区别,如下:
    • 热点代码(编译执行 – 动态实现)
    • 冷门代码(解释执行 – 本地实现)

反射为什么效率低

  1. 变长参数方法导致的 Object 数组。
  2. 基本类型的自动装箱、拆箱。
  3. 某些场合的方法内联失效。

目前的优化方向:

  1. 方法内联。
  2. 关闭反射调用的 Inflation 机制,取消本地实现,全部使用动态实现。
  3. 取消每次反射调用前的检查。也就是method.setAccessible(true);

参考“《深入拆解 Java 虚拟机(极客时间)》07.JVM 是如何实现反射的?”

反射的运用领域?

  1. 反射可以拿到类的实例;
  2. 反射可以用来判断某类是不是另一个类的实例;
  3. 可以用Array.newInstance(Class,int)来构造该类型的数组;
  4. 自定义注解:在自定义注解时,需要三步①定义注解——相当于定义标记;②配置注解——把标记打在需要用到的程序代码中;解析注解——在编译期或运行时检测到标记,并进行特殊操作。其中第三步,就是通过反射来实现。参考:自定义注解-简书自定义注解-csdn
  5. 可以访问到类的成员(注:getDeclaredMethods()方法不会返回父类成员,但能够返回私有成员;getMethods()方法刚好相反),拿到成员后,可以:
  6. 使用Methods.setAccessible()方法可以绕过Java 的语言限制;
  7. 使用Constructor.newInstance()获得类的实例;
  8. 使用Method.invoke()来调用方法。

有关反射的内容:这里有篇文章看到热泪:假笨说-从一起GC血案谈到反射原理,有脑无脑强推!!!

阿里 Java 一面

1. Spring 怎么对 bean 进行增强或者修改。

可以通过bean的后处理器。

  1. bean 的后处理器中的BeforInitialization 和 AfterInitialization 方法。
  2. init-method 、destroy-method 方法;
  3. 实现*Aware 接口,在bean 中Spring框架的某些对象,比如ApplicationContext、beanFactory、beanName等。

2. CountDownLatch 细节

3. new 一个很大的对象,对象是怎么分配的

4. 最新的垃圾回收机制有了解过吗?

ZGC

5. 项目访问量对项目的影响?

6. Redis 的key、value能装类吗?

7. 怎么序列化类的?

一般通过ObjectOutputStream 和 ObjectInputStream 进行序列化和反序列化的。

  1. 类首先实现 Serializable 接口。
    1. 在不想被序列化的属性前加上 transient 关键字;
    2. 也可以在类中自定义 WriteObject 、ReadObject 方法,让某些 transient 修饰的属性按照自定义的方式完成序列化。
    3. 静态属性不会被序列化。
  2. 如果打算序列化父类的某些行为,那么父类也需要实现 Serializable 接口。所有引用对象也必须是可序列化的。
  3. 序列化是深拷贝的过程。
  4. 指定序列化ID,让相互传输数据的两个客户端之间类的序列化ID保持一致,这样才能正确地反序列化拿到数据。
    • 序列化ID 一般可以通过:类名,接口名,方法和属性等来生成的。
  5. 对象转为二进制举例:新建一个 ObjectOutputStream 对象,同时传入一个 OutputStream 作为存储二进制数据的位置。再调用writeObject()将对象写入。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 核心代码
// 一个名为 A 并实现 Serializable 接口的类此处未展示
// 类 A 不能是内部类,因为内部类的序列化要依赖于外部类
@Test
public void test() {
A a = new A();
a.value = 1;
a.name = "xiaoming";

FileOutputStream fos = null;
FileInputStream fis = null;

try{
fos = new FileOutputStream("temp");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(a);
fos.close();

fis = new FileInputStream("temp");
ObjectInputStream ois = new ObjectInputStream(fis);
A a2 = (A) ois.readObject();
fis.close();
System.out.println("==========");
System.out.println(a2.value);
System.out.println(a2.name);

}catch (IOException e){
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

参考阅读:Java基础18:Java序列化与反序列化

深拷贝扩展:

  1. 简短解说: 浅拷贝:拷贝对象与原始对象的引用类型引用同一个对象;深拷贝:拷贝对象与原始对象的引用类型引用不同对象。
  2. 欲实现引用属性的拷贝,就需要实现 cloneable 接口,并重写 clone 方法来实现。比如想拷贝类 A,就让类A按上面的做法。
  • 不重写 clone 方法,默认就是浅拷贝,重写了才是深拷贝。
  1. 欲实现引用对象的深拷贝,就需要让被引用的类也同样实现 cloneable 接口,并重写其 clone 方法来实现。也就是说类 A 携带有指向类 B 的引用,那么 A、B 都要实现 cloneable 接口并重写 clone 方法。但是此做法不利于后期的维护。
  2. 以上两种做法要么不全面,要么太难,所以可以采用序列化的方式来实现,参考:Java深拷贝与序列化

8. Redis 数据的序列化机制?

  1. 使用 RedisTemplate 中的一个序列化工具:GenericJackson2JsonRedisSerializer

推荐阅读:RedisTemplate序列化工具

9. 序列化成字符串然后存到Redis的value 中,这部分工作能否让Redis 完成?效率如何?

10. ConcurrentHashMap 的 put 方法

参考随手记第 14 题,往上翻。

11. MySQL 索引的优先原则

最左前缀原则。

12. 我的项目中动态编译、类加载的全部过程

类加载参考其他题目,这里说一下编译过程:

  1. 首先要涉及 JavaCompiler 这个类,它是 JDK 提供的动态编译的 api;
  2. 然后涉及 JavaFileObject 接口及发散类,它是封装源码和字节码的对象;
  3. 然后涉及 JavaFileManager 接口及发散类,编译器通过这个类来管理JavaFileObject对象;
  4. 然后调用getTask()方法生成编译任务并执行。获取到输出流,最终将输出流转换成字节数组。

补充: MySQL 的行溢出

行溢出:如果某条记录太大,即使叶子结点中还剩余一多半的空间但仍然存不下怎么办?这种情况称之为行溢出
简单的解决方式就是把记录存储在溢出页(磁盘的其它空闲地方)中,然后叶子结点中存储的是这个记录的指针。

参考资料:B+树在磁盘存储中的应用,包括了对 4KB 大小的解释。

补充:锁的 JVM 相关命令:

1
2
3
4
5
-XX:PreBlockSpin // 更改自旋等待的次数
-XX:+UseSpinning // 开启或关闭自旋锁
-XX:-UseBiasedLocking=false // 命令关闭偏向锁(default 开启)


补充:对象头细节:

对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。

  • Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。 - 锁标志位:无锁 01、偏向锁 01、轻量级锁 00、重量级锁 10。
  • Klass Pointer:对象指向它的类元数据(亦称Klass、类类型,即类信息存储的地方,在方法区)的指针。

补充:Atomic 源码细节:

比如 AtomicInteger 的getAndIncrement()调用的就是 Unsafe 类的 getAndAddInt()方法。此方法使用 do-while 配合 CAS 进行自旋:

1
2
3
4
5
6
7
8
9
10
// 来自 Unsafe类
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
// 取出内存中的值 var5
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); // CAS 实现 var5 + var4

return var5;
}

补充:Unsafe 类学习总结:

本小节知识点参考博客:Java魔法类:Unsafe应用解析

  1. Unsafe 的调用方必须是被Bootstrap CL 加载的类,否则会抛出安全异常,解决办法是:
    1. 方法一:将欲调用 Unsafe 的类的 jar 包添加到默认的 bootstrap 路径中。
    2. 方法二:使用反射获取单例对象 Unsafe。

  1. 内存的操作。以下主要讲解直接内存:通过Unsafe.allocateMemory分配内存、Unsafe.setMemory进行内存初始化,而后构建Cleaner对象用于跟踪DirectByteBuffer对象的垃圾回收。
  2. CAS 操作。代码可参考上一题——Atomic 源码细节。其中调用的 compareAndSwap*才是真正的原子操作,CAS#getAndAddInt()是在原子操作的基础上增加了自旋的逻辑。
  3. 线程调度

Java锁和同步器框架的核心类AbstractQueuedSynchronizer,就是通过调用LockSupport.park()和LockSupport.unpark()实现线程的阻塞和唤醒的,而LockSupport的park、unpark方法实际是调用Unsafe的park、unpark方式来实现。
4. 内存屏障。其实是 CPU 或者 IDE 对内存随机访问的一个安全点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作,避免代码重排序。体现在 Unsafe类中的*Fence()系列方法
5. Class 操作。不展开,直接看原博客。
6. 对象操作。主要应用场景是:非常规的对象实例化方式(比如绕过类构造器,绕过安全检查等)。
7. 数组相关。主要跟 AtomicIntegerArray中的数组操作中的元素定位有关。
8. 系统相关。系统指针大小(32位指针大小是4B,64位是8B);内存页大小(作者主机上内存页大小是4096B)。

补充:ReentrantLock 源码细节:

趣链 Java 一面之二

redis 高效的原因,IO 多路复用讲一下

  1. redis 是纯内存访问;
  2. redis 使用单线程,避免了线程的切换和竞争;
  3. redis 实现了 I/O 多路复用技术,IO效率很高。

IO 多路复用:用select、poll、epoll监听多个io对象,一旦某个io对象数据准备好了,就可以通知用户进程,完成业务。好处是单个进程可以处理多个socket连接。

  • select:当用户进程调用select,此用户进程被阻塞,然后select会轮询它负责的socket流,当任意一个socket中的数据准备好了,select就会返回,此时用户进程会调用read操作拷贝数据。
  • poll:基于链表来存储,没有最大连接数的限制(也就是说可扩展长度)。
  • epoll:基于事件驱动:
    • epoll 对象存放着“添加进来的事件”,这些事件挂载在红黑树中(能避免重复事件)
    • 上面的这些事件会跟设备驱动建立回调关系,当事件发生时,会调用这个回调函数。此时发生的事件也会被添加到一个双链表中。

对比 select、poll、epoll:

  1. select 和 poll 的时间复杂度都是O(n),都是无差别轮询所有流。两者本质没有差别,只是 poll 会把用户传入的数组拷到内核空间,然后逐个查询状态,而且 poll 是基于链表实现的。
  2. epoll 的时间复杂度是O(1),epoll 会把哪个流发生的什么 IO 事件发给我们,涉及到一些函数回调。

从 Java 内存模型角度讲一下 i++ 执行步骤

  1. 从主内存中取出变量i到工作内存;
  2. 工作内存完成+1操作;
  3. 写回主内存。

a=a+b 与 a+=b 的区别

  1. 前者,计算a+b,然后赋给a引用。
  2. 后者,先用一个temp对象存储a,然后和b相加,相加结果赋给a引用。
  3. += 涉及到自动类型转换的问题。

int 在32位和64位机子占的内存大小

都是4个字节。

补充姿势:

1字节 8 位:byte/8

2字节 CS(char 和 short)

4 字节 IF(int 和 float)

8 字节 LD(long 和 double)

32位和64位,java 内存的分配大小

堆内存大小受:32位/64位限制,可用虚拟内存限制,可用物理内存限制。
32位下,堆最大在1.5G~2G之间。64位要高30%左右。

git 工作流、revert、fix bugs

git revert 跟 git reset区别:

  1. git reset 恢复到之前提交的某个版本,之后提交的版本不要了。
  2. git revert 反向创建一个新版本,这个版本跟我们要回退的版本一致。

推荐阅读:Git恢复之前版本的两种方法reset、revert(图文详解)

fixBugs:

  • 先在bug分支修改,
  • 然后branch验证通过后,才被允许合并到master中。

git flow 简介

  1. 历史分支:master 和 develop;
  2. 功能分支:feature;
  3. 发布分支:release;
  4. 维护分支:hotfix;

多线程传输的场景下,设计一个系统

分析多线程可能出现的问题,针对这些问题提出解决方案

  1. 原子性问题;使用Synchronized关键字;
  2. 可见性问题;使用volatile关键字;
  3. 指令重排问题;使用volatile禁止指令重排。

原子性、内存可见性、重排序、顺序一致性、volatile、锁、final

ringbuffer

多线程竞争、原子性等

算法实现:rand 产生1到7,怎么实现1到10?

个人思路(乱来):

1
2
3
rand-106
rand-1)*9/609
1+(rand - 1)*9/6 就是110了吧

官方解法:

1
2
3
4
5
6
7
8
public int rand10(){
do{
int row = rand7();
int col = rand7();
int index = col + (row - 1)*7;
}while(index > 40);
return 1 + (index - 1)%10;
}

java.util.concurrent 中 CountDownLatch、CyclicaBarrier、Semaphore

Http 协议 与 Https 协议的区别,增加的 s 层细节

  1. 接收client访问时,server 返回数字证书,包括server的公钥;client使用预置的 CA 列表验证证书。
  2. client再生成一个随机的对称密钥,用server的公钥加密后发给server。server用自己的私钥解密,得到此对称密钥。
  3. 之后可以相互访问。

推荐阅读:HTTP-1-1

有了 HTTP协议,为什么还需要 RPC 协议呢?

RPC&Http

Http跟 RPC 不是同一级别的概念,但它们都是解决应用调用另一个应用的备选方案。

Http:可读性好、跨语言性、有防火墙支持等优势。最佳实践—— Restful 。

RPC:不是网络七层、头部信息少,相比 HTTP 能携带更多信息,效率高。可基于私有协议传输。最佳实践——Dubbo、gRpc、thrift。

趣链 Java 一面之三

1)介绍下自己的项目

2)JVM 垃圾回收算法

  1. 标记清除;先标记阶段然后清除阶段。
    • 适合老年代;
  2. 标记整理;先标记,然后存活对象向一端移动。
    • 适合老年代;
  3. 复制;一半使用,一半备用。将活着的对象复制到备用块上,然后将原内存块一次性清理掉。
    • 适合新生代。
  4. 分代回收。

收集器:(记忆:3对+1)
左边新生代都是复制算法,右边老年代。ParNew 是 serial 的多线程版本。ParNew 跟 Parallel Scavenge 几乎一样。

  • serial & serial Old(整)

  • ParNew & CMS(清)绝配。

  • Parallel Scavenge & Paralled Old(整)

  • G1

  • G1收集器的特点:用在服务器,在满足GC停顿时间要求的同时,还具备高吞吐量性能特征。

    1. 通过并发的方式,让GC线程与Java程序同时运行。
    2. 可以独立管理新生代和老年代。
    3. 可预测的停顿,建立一个可预测的停顿时间模型,让使用者明确指明一个长度为M毫秒的时间片段内。
    4. 具有一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的部分进行回收。

3)synchronized 与ReentrantLock 区别

  1. 都是可重入锁;
  2. 前者依赖于JVM,程序员看不到,后者依赖于API,可以查看JDK中它的源码;
  3. 后者有新功能:
    1. 等待可中断;
    2. 可实现公平锁;
    3. ReentrantLock 结合 Condition 可以选择性地通知某些线程(这些线程在Condition对象中注册),而不是使用 notify/notifyAll(效率低)。

4)java.util.concurrent 中 CountDownLatch、CyclicaBarrier、Semaphore

5)创建线程的 3 中方法:Thread、Callable、Runnable,区别,你的使用习惯

6)线程池

10)Http 协议 与 TCP 的区别

11)进程间通信:管程、Socket…

12)进程与线程的区别,有没有做过多进程的项目

13)MySql 索引

  • 聚簇索引:每张表主键构成B+树(存储顺序与索引顺序一致),叶子节点存放真实的数据行。
    • 主要用在InnoDB引擎上。
  • 非聚簇索引:数据行存储顺序与索引存储顺序不一致,叶子节点没有存放数据,存的是“键-指针对”,根据此指针再去其他索引树去查找。
    • 主要用在MyISAM引擎上。

InnoDB 跟 MyISAM 的区别:

14)注入攻击

15)跨域请求

16)项目中的拦截机制、Session

17)项目中考虑到的安全问题

18)Redis 缓存使用中当数据库中数据更新了,怎么实现缓存中的更新

19)Redis 的用处啥的

24)有没有使用过 git

25)git 的基本操作

26)git clone 与 git fork 的区别

27)合作项目中 git 的使用,主要是 master 和分支啥的

29)有没有使用过 rpc

-------------The End-------------