面试准备

Article Directory
  1. 1. Spring 为何需要三级缓存解决循环依赖,而不是二级缓存?
  2. 2. Java八股文面试题视频教程,Java面试八股文宝典(含阿里、腾迅大厂java面试真题…)
  3. 3. 基本数据类型的存储
  4. 4. Spring常见面试总结
  5. 5. Java线程池七个参数详解
  6. 6. Synchronized和ReentrantLock的区别
  7. 7. 牛客某个人的八股
  8. 8. JWT详解
  9. 9. 手写单例模式
  10. 10. ThreadPoolExecutor
    1. 10.1. 知识
  11. 11. Synchronized和ReentrantLock的区别
    1. 11.1. synchronize锁升级
    2. 11.2. 手写有关参数的
  12. 12. Redis内存回收
    1. 12.1. Redis过期策略
    2. 12.2. Redis淘汰策略
  13. 13. IO多路复用
  14. 14. 运行时非运行时异常
  15. 15. HashMap为什么线程不安全
  16. 16. 类型擦除与解决方案
  17. 17. Java通过反射获取泛型类型信息
  18. 18. Java中的泛型会被类型擦除,那为什么在运行期仍然可以使用反射获取到具体的泛型类型?
  19. 19. RabbitMQ和Kafka比较
  20. 20. Comparable和Comparator两个接口的区别
  21. 21. Spring框架中的单例Bean是线程安全的吗?
  22. 22. ThreadLocal使用不当造成内存溢出
  23. 23. 多线程run()中如何捕获运行时异常?
  24. 24. java正确关流_JAVA中如何优雅的关闭各种流
  25. 25. 快速失败(fail-fast)以及其中的问题与安全失败(fail-safe)
    1. 25.1. Fail-fast
      1. 25.1.1. 单线程
      2. 25.1.2. 多线程
    2. 25.2. fail-safe
  26. 26. java中的hashCode计算方法总结
  27. 27. 为什么重写equals方法,还必须要重写hashcode方法
  28. 28. Java中抽象类和接口的介绍及二者间的区别
    1. 28.1. 抽象类的特点
    2. 28.2. 接口
  29. 29. 数据量很大的排序问题 大量数据如何排序
  30. 30. 大数据的文件归并排序
  31. 31. ThreadLocal 详解
  32. 32. 进程和程序的区别
  33. 33. 反射得到的对象和new的对象区别
  34. 34. 设计模式:静态代理和动态代理的区别 以及 springAop和cglib的区别
    1. 34.1. 性能对比:
    2. 34.2. 核心思想:
    3. 34.3. 值得注意的是
  35. 35. 红黑树
  36. 36. Java在云原生的破局利器——AOT(JIT与AOT)
  37. 37. java实现多继承的三种方式
  38. 38. Linux 查看日志的方法
  39. 39. 魔鬼面试官必问:ConcurrentHashMap 线程安全吗?
  40. 40. 线程创建的三种方式
  41. 41. 时间空间复杂度概念
  42. 42. 二分排序

面试自己的一些准备

Spring 为何需要三级缓存解决循环依赖,而不是二级缓存?

Java八股文面试题视频教程,Java面试八股文宝典(含阿里、腾迅大厂java面试真题…)

基本数据类型的存储

基本数据类型,如果声明在方法内,则存储在方法栈中,如果在类内声明,则存储在堆中;如果是引用类型,则声明在方法中时,通过方法栈指向堆,声明在类中时存储在堆中,指向值存储区域

Spring常见面试总结

Java线程池七个参数详解

Synchronized和ReentrantLock的区别

Synchnorized类锁和对象锁的区别,对象锁的时候,就是交换的堆中的实例对象的markworld;

而对象锁的时候,在加载对象的时候会在堆中生成一个class对象的(java.lang.class,这个与方法取中的instanceKlass的java_mirror字段互指),对这个对象进行加锁。

cdg5DH

H0ieGt

牛客某个人的八股

JWT详解

手写单例模式

ThreadPoolExecutor

知识

字节二面问Java ThreadPool线程池?我来教你应对

ScreenShot2022-08-29at10.15.10 ScreenShot2022-08-29at10.16.03 ScreenShot2022-08-29at10.16.22

美团二面:聊聊线程池设计与原理,由表及里趣味解析

注意看:线程池监控里面,获取线程池大小,获取当前活跃工作线程数量等,都需要使用ReentrantLock来开锁实现。

ScreenShot2022-08-29at10.12.32

ThreadPoolExecutor源码实现中用的是ReentrantLock锁?

公平锁:并发环境中,每个线程在获取时会先查看此锁维护的队列,如果为空,或者当前线程是等待队列的第一个就占有锁,否则就会加入到队列中,以后会按照FIFO的规则从队列中取到自己。

非公平锁:上来就直接尝试占有锁,如果尝试失败,再采用类似公平锁的方式。

ReentrantLock而言,通过构造器指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。

Synchronized和ReentrantLock的区别

qubie

synchronize锁升级

锁的四种状态及升级过程

线程A在进入同步代码块前,①先检查MarkWord中的线程ID是否与当前线程ID一致,如果一致(还是线程A获取锁对象),则无需使用CAS来加锁、解锁。

如果不一致,②再检查是否为偏向锁,如果不是,则自旋等待锁释放。

如果是,③再检查该线程是否存在(偏向锁不会主动释放锁),如果不在,则CAS设置线程ID为线程A的ID,此时依然是偏向锁。

如果还在,④则暂停该线程,同时将锁标志位设置为00即轻量级锁(将MarkWord复制到该线程的栈帧中并将MarkWord设置为栈帧中锁记录)。线程A自旋等待锁释放。

⑤如果自旋次数到了该线程还没有释放锁,或者该线程还在执行,线程A还在自旋等待,这时又有一个线程B过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。

⑥如果该线程释放锁,则会唤醒所有阻塞线程,重新竞争锁。

手写有关参数的

Redis内存回收

内存过期策略 + 内存淘汰策略

Redis过期策略

周期删除

惰性删除:用的时候看下是否删除

redis中两者相结合

Redis淘汰策略

彻底弄懂Redis的内存淘汰策略

默认:noeviction,不淘汰任何key,但是内存满时不允许写入新的数据。

IMG_2E888F9DFDEE-1

IO多路复用

ScreenShot2022-08-29at15.09.54

ScreenShot2022-08-29at15.11.01

运行时非运行时异常

KaHVP3

下面这一题貌似选C,其他的都是对的。

1331662474023_.pic

c2jCx6

HashMap为什么线程不安全

JDK1.7 HashMap线程不安全体现在:死循环、数据丢失
JDK1.8 HashMap线程不安全体现在:数据覆盖

类型擦除与解决方案

Java通过反射获取泛型类型信息

在Java中可以通过反射获取泛型信息的场景有如下三个:

  • (1)成员变量的泛型
  • (2)方法参数的泛型
  • (3)方法返回值的泛型

在Java中不可以通过反射获取泛型信息的场景有如下两个:

  • (1)类、接、泛型方法 声明的泛型。(只要是声明处的泛型,那就都获取不了,但应该可以通过字节码技术进行获取,因为这些泛型信息应该还是被保存在了class文件中没有丢失的)
  • (2)局部变量的泛型,是因为反射连局部变量都访问不了,就更不要提访问它的泛型了,非要访问的话必须通过字节码技术。

Java中的泛型会被类型擦除,那为什么在运行期仍然可以使用反射获取到具体的泛型类型?

运行期只能获取当前class文件中包含泛型信息的泛型类型

而不能在运行时动态获取某个泛型引用的类型

可以通过反射获取到的泛型信息一定是某个class作为成员变量、方法返回值等位置的具体泛型类型,比如:

1
2
3
4
5
public class Test<T> {
private Test<Integer> test;

private T item;
}

可以通过反射获取成员test的Integer泛型信息,但是无法获取item的实际类型。

RabbitMQ和Kafka比较

RabbitMQ和Kafka比较

优先选择RabbitMQ的条件:

·高级灵活的路由规则
·消息时序控制(控制消息过期或消息延迟)
·高级的容错处理能力,在消费者更有可能处理消息不成功的情景中(瞬时或持久)
·更简单的消费者实现

优先选择Kafka的条件:

·吞吐量更高
·严格的消息顺序
·延长消息留存时间,包括过去消息重放的可能,消息回溯
·传统解决方案无法满足的高伸缩能力

Comparable和Comparator两个接口的区别

![image-20220912192322174](/Users/dinosaur/Library/Application Support/typora-user-images/image-20220912192322174.png)

Spring框架中的单例Bean是线程安全的吗?

答案:不是

Spring默认的bean是单例的,也没有进行封装处理,所以不是线程安全的。

但是,共享不一定会有线程安全问题。
如果某个bean我们定义了共享数据,且可以对共享数据进行修改,这样才会造成线程安全问题。比如我们在线程中定义一个count++,那么这个数就是不安全的,每次线程到来都会被执行一次,count值加一。
controller、service、dao本身不是安全的,但是,我们只在这些方法中相互调用,那么不会产生线程安全问题。
Dao层会操作数据库,但是每一个和数据库连接的connection,都会被数据库的事务机制管理,如果开启了Spring事务机制,那么也会被其管理。这都不会造成线程安全问题。
如果,我们必须要在bean中定义有状态的实例或变量,那么我们可以有以下几种办法,保证安全。

1 使用ThreadLocal,把变量变成线程私有的
2 synchronized、lock、CAS,保证互斥访问临界区
3 把bean从单例(singleton)设置成原型(protopyte)

ThreadLocal使用不当造成内存溢出

ThreadLocal,底层使用map,当前线程的对象作为key,value则是你想让变成私有属性的变量,或实例对象。那么当该线程要使用这个变量或实例时候,就可以从map中get出来。
每一个线程到来后,都会去创建map,如果是线程池中的核心线程,这个线程不死亡,它每次执行到这里都会往map中存对象,可能导致内存溢出。
如何解决呢?线程每次get(key)使用过后,就把map中的数据删除掉,remove(key)。

多线程run()中如何捕获运行时异常?

在实现Runnable或者继承Thread类实现多线程的时候,都要实现或者重写run()方法,查看源码可以看到run()是不允许抛出异常的,那么如果在run()方法中产生了运行时异常如何捕获呢?

Thread类中有UncaughtExceptionHandler这个接口来实现对异常的捕获,我们只需将其方法重写,创建这个实现类的实例O,在线程start之前,使用thread.setUncaughtExceptionHandler(O)将这个实例传入此方法。下面直接看例子。如果在将第四行代码注释掉后可以发现直接抛出异常,无法捕获异常,即使你在主函数中用try,catch捕获也是会直接抛出异常的。那么将第四行注释取消后,运行结果如下,可以看到已经捕获异常。

java正确关流_JAVA中如何优雅的关闭各种流

java优雅关闭io流

快速失败(fail-fast)以及其中的问题与安全失败(fail-safe)

Fail-fast

单线程

当在用迭代器(Iterator)或者增强for循环(增强for循环的底层也是迭代器)对一个集合进行遍历操作时,如果遍历的过程中集合的结构发生了变化,就会抛出并发修改异常ConcurrentModificationException。

要想避免抛出异常,应该使用Iterator对象的remove()方法。

多线程

当一个线程在对一个集合进行遍历操作的时候,如果其他线程对这个集合的结构进行了修改,就会抛出并发修改异常ConcurrentModificationException。

fail-safe

Java在java.util.concurrent包中为我们提供了安全失败的集合类,如 ConcurrentHashMap、CopyOnWriteArrayList等。其原理在于开始遍历之前会将原集合完整的复制一份,前者在复制的集合上进行遍历,在原集合上进行元素添加、删除操作,后者反之,这样就可以避免了ConcurrentModificationException异常。

当然,这类集合也有一些缺点:iterator不能保证返回集合更新后的数据,因为其工作在集合克隆上,而非集合本身。其次,创建集合拷贝需要相应的开销,包括时间和内存。

java中的hashCode计算方法总结

string的属性 value 就是字符数组,计算方法,s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1]

1
2
3
4
5
6
7
8
9
10
11
12
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;

for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}

为什么重写equals方法,还必须要重写hashcode方法

1.为了提高效率

2.为了保证同一个对象

a 保证在equals相同的情况下hashcode值必定相同,如果重写了equals而未重写hashcode方法,可能就会出现两个没有关系的对象equals相同的(因为equal都是根据对象的特征进行重写的),但hashcode确实不相同的。

b hash类存储结构(HashSet、HashMap等等)添加元素会有重复性校验,校验的方式就是先取hashCode判断是否相等(找到对应的位置,该位置可能存在多个元素),然后再取equals方法比较(极大缩小比较范围,高效判断),最终判定该存储结构中是否有重复元素。

Java中抽象类和接口的介绍及二者间的区别

抽象类的特点

1、抽象类不能被实例化,即不能使用new关键字来实例化对象,只能被继承;

2、包含抽象方法的一定是抽象类,但是抽象类不一定含有抽象方法;

3、抽象类中的抽象方法的修饰符只能为public或者protected,默认为public;

4、抽象类中的抽象方法只有方法体,没有具体实现;

5、如果一个子类实现了父类(抽象类)的所有抽象方法,那么该子类可以不必是抽象类,否则就是抽象类;

6、抽象类可以包含属性、方法、构造方法,但是构造方法不能用于实例化,主要用途是被子类调用。

接口

1、接口可以包含变量、方法;变量被隐士指定为public static final,方法被隐士指定为public abstract(JDK1.8之前);

2、接口支持多继承,即一个接口可以extends多个接口,间接的解决了Java中类的单继承问题;

3、一个类可以实现多个接口;

4、JDK1.8中对接口增加了新的特性:

​ 4.1、默认方法(default method):JDK 1.8允许给接口添加非抽象的方法实现,但必须使用default关键字修饰;定义了default的方法可以不被实现子类所实现,但只能被实现子类的对象调用;如果子类实现了多个接口,并且这些接口包含一样的默认方法,则子类必须重写默认方法;

​ 4.2、静态方法(static method):JDK 1.8中允许使用static关键字修饰一个方法,并提供实现,称为接口静态方法。接口静态方法只能通过接口调用(接口名.静态方法名)。

数据量很大的排序问题 大量数据如何排序

问题一:若有1T的数据,比如 只有两列,身份证号和姓名 需要实现由大到小排序,你用什么办法,能否做到 复杂度为O(n),说说你的思路和想法?

问题二:有10个G的数据,也是一样,比如两列,身份证号和姓名,如果两条数据一样,则表示该两条数据重复了,现在给你512的内存,把这10G中重复次数最高的10条数据取出来。

我的思路是:这么大的数据,用普通的排序一定不行,

可以这样,用身份证号的前三位切割这个数据,这样会分成999份,

每一份再进行排序,比如构造一个平衡二叉树,最典型的的就是TreeMap和TreeSet(TreeSet底层是使用了TreeMap算法,而TreeMap算法底层是实现了红黑树的平衡二叉树的排序);

然后按照文件名进行排序,这样就实现了大数据排序;

因为排序二叉树的复杂度为O(lgn)到O(n) ;

因此我们可以做到 O(n)

问题二:

解法是一样的 按照身份证号前三位 分割999份,然后对这每个文件找到重复的最多的十条,这样,我们得到了999个文件,每个文件有 10条数据

在对这个999*10条进行排序找到 重复率最高的十条即可;

大数据的文件归并排序

第一 将数据分成若干份(我们这里以10份为例子),每份都接近内存的最大数据量,这样子,将文件上的数据读入内存中,将它排序,结束后我们就可以得到10份有序的文件数据了。

第二步,归并文件,将两个文件归并成一个有序的文件后,在让这个有序的文件与第三个文件归并,这样依次下来,我们就可以得到一个有序的文件了。

ThreadLocal 详解

进程和程序的区别

进程是动态的,程序是静态的:程序是有序代码的集合,进程是程序的执行。进程有核心态/用户态。

程序是我们写下来的一段代码,而进程的产生是因为代码开始执行了

进程的组成包括程序、数据和进程控制块(即进程状态信息)

反射得到的对象和new的对象区别

1 在使用反射的时候,必须确保这个类已经加载并已经连接了。使用new的时候,这个类可以没有被加载,也可以已经被加载。
2 new出来的对象我们无法访问其中的私有属性,但是通过反射出来的对象我们可以通过setAccessible()方法来访问其中的私有属性。
3 new静态编译,反射动态编译。new关键字是强类型的,效率较高。反射是弱类型的,效率低。
4 反射提供了一种更加灵活的方式创建对象,得到对象的信息。Spring AOP和Java动态代理都是基于反射。

设计模式:静态代理和动态代理的区别 以及 springAop和cglib的区别

1 JDK动态代理是通过持有对象来实现的(就是一个类继承Proxy,然后持有该类(一开始用object引用持有,后面确定的时候传参放入.class)):1.动态代理的实现类的类型是不固定的,所以只能是Object,因为Object能包容一切
增强的代码要留给开发人员自己写,所以不能写死

2 通过继承父类所有公开的方法,然后重写这些方法,在重写时增强这些方法。

性能对比:

jdk创建对象的速度远大于cglib,这是由于cglib创建对象时需要操作字节码。
cglib执行速度略大于jdk,所以比较适合单例模式。
另外由于cglib的大部分类是直接对Java字节码进行操作,这样生成的类会在JVM的永久代中。如果动态代理操作过多,容易造成永久代满,触发OutOfMemory。

核心思想:

jdk是通过持有对象来实现增强的;cglib是通过继承来实现增强的

值得注意的是

spring默认使用jdk动态代理,如果类没有接口,在使用cglib实现动态代理。

红黑树

  • AVL的左右子树高度差不能超过1,每次进行插入/删除操作时,几乎都需要通过旋转操作保持平衡
  • 在频繁进行插入/删除的场景中,频繁的旋转操作使得AVL的性能大打折扣

红黑树通过牺牲严格的平衡换取插入/删除时少量的旋转操作,整体性能优于AVL
红黑树插入时的不平衡,不超过两次旋转就可以解决;删除时的不平衡,不超过三次旋转就能解决
红黑树的红黑规则,保证最坏的情况下,也能在 O(log2N)时间内完成查找操作。

规则:

  1. 节点不是黑色,就是红色(非黑即红)
  2. 根节点为黑色
  3. 叶节点为黑色(叶节点是指末梢的空节点 NilNull
  4. 一个节点为红色,则其两个子节点必须是黑色的(根到叶子的所有路径,不可能存在两个连续的红色节点)
  5. 每个节点到叶子节点的所有路径,都包含相同数目的黑色节点(相同的黑色高度)

Java在云原生的破局利器——AOT(JIT与AOT)

java实现多继承的三种方式

三个实现java多继承的方法:多层继承、内部类、接口

1 多层继承:一般不建议超过三层,孙子类会获得父类的所有非私有方法和属性。(B继承A,C继承B,C就间接性的的继承A和B)

2 成员内部类:直接在类的内部定义成员类,使之成为该类的一部分,达到高内聚的效果。

3 接口:java中可以继承多个接口,实现该接口的实现类需要实现该接口及父接口的所有方法。(IC继承IB,IC impl实现IC 就相当于继承了它们)

Linux 查看日志的方法

魔鬼面试官必问:ConcurrentHashMap 线程安全吗?

线程创建的三种方式

而使用Runnable接口和Callable接口的方式,区别在于前者不能获得线程执行结束的返回值,后者可以获得线程执行结束的返回值。

而继承父类和实现接口这两种方式的优缺点是: - 采用接口的方式创建线程,优点是线程类还可以继承于其他类,并且多个线程可以共享一个线程体,适合多个线程处理同一份资源的情况。缺点是编程稍微麻烦一点点。 - 采用继承的方式创建线程,优点是编程稍微简单一点点。缺点是因为线程类已经继承了Thread类,所以就不能继承其他的父类了。 所以,通常情况下,更推荐采用接口的方式来创建线程。如果需要返回值,就使用Callable接口,否则使用Runnable接口即可。

时间空间复杂度概念

在进行算法分析时,语句总的执行次数 T(n) 是关于问题规模 n 的函数,进而分析 T(n) 随 n 的变化情况并确定 T(n) 的数量级。算法的时间复杂度,也就是算法的时间量度,记作:T(n) = O(f(n)) 。它表示随问题的规模 n 的增大,算法执行时间的增长率和 f(n) 的增长率相同,称作算法的渐进时间复杂度,简称为时间复杂度。其中 f(n) 是问题规模 n 的某个函数。

算法的空间复杂度通过计算算法所需要的存储空间实现,算法的空间复杂度的计算公式记作:S(n)=O(f(n)),其中,n 为问题规模,f(n) 为语句关于 n 所占存储空间的函数。

二分排序

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
// 二分排序中,关键字的比较次数采用折半查找,数量级为O(nlogn), * 但是元素移动次数为O(n^2),因此时间复杂度为O(n^2)。

public class BinarySort {
/** * 二分法插入排序是在插入第i个元素时,对前面的0~i-1元素进行折半, * 先跟他们中间的那个元素比,如果小,则对前半再进行折半,否则对后半进行折半, * 直到left>right,然后再把第i个元素前1位与目标位置之间的所有元素后移, * 再把第i个元素放在目标位置上。 */
public static void binarySort(int[] source){
int left,right,middle;//left>right说明找到了位置
int temp;
for(int i = 1;i<source.length;i++){
left = 0;//初始化左指向首个元素
right = i-1;//右指向需要插入元素的左侧
temp = source[i];//记录需要排序的元素
while(left<=right){
middle = (right+left)/2;
if(source[middle]>temp){//中间元素大于待插入元素,说明插入位置在middle左侧
right = middle -1;//将右指向为左半
}else {
left = middle + 1;
}
}
for(int j = i-1;j>=left;j--){//插入操作,将元素右移
source[j+1]=source[j];
}
source[left] = temp;
spy(source);//显示排序的过程
}
}
/** * 方便观察每次排序,可以窥探数组排序的情况 * @param source */
public static void spy(int[] source){
for(int k = 0;k<source.length;k++){
System.out.printf(source[k]+" ");
}
System.out.println();
}
}

Author: Jcwang

Permalink: http://example.com/2022/08/29/%E5%85%AB%E8%82%A1%E9%9D%A2%E8%AF%95%E5%87%86%E5%A4%87/