这里是参考B站上的大佬做的面试题笔记。大家也可以去看视频讲解!!!
文章目录
- 11、HashMap和HashTable的区别及底层实现
- 12、ConcurrentHashMap原理简述,jdk7和jdk8的区别
- 13、如何实现一个IOC容器
- 14、什么是字节码,作用是什么
- 15、java类加载器有哪些
- 16、双亲委派模型
- 17、java中的异常体系
- 18、GC如何判断对象可以被回收
- 19、线程的生命周期及状态
- 20、sleep、wait、join、yield的区别
11、HashMap和HashTable的区别及底层实现
区别:
- HashMap方法没有synchronized修饰、线程非安全,HashTable线程安全
- HashMap允许key和value为null,而HashTable不允许
底层实现:数组+链表实现
jdk8开始链表高度到8,数组长度超过64,链表转变为红黑树,元素内部类Node节点存在
- 计算key的hash值,二次hash然后对数组长度取模,对应到数组下标
- 如果没有产生hash冲突(下标位置没有元素),则直接创建Node存入数组
- 如果产生hash冲突,先进性equal比较,相同则取代该元素不同,则判断链表高度插入链表,链表高度达到8,并且数组长度到64则转变为红黑树,长度低于6则将红黑树传回链表
- key为null,存在下标0的位置
数组扩容
12、ConcurrentHashMap原理简述,jdk7和jdk8的区别
jdk7:
-
数据结构:ReentrantLock+Segment+HashEntry,一个Segment中包含一个HashEntry数组,每个HashEntry又是一个链表结构
-
元素查询:二次hash,第一次Hash定位到Segment,第二次hash定位到元素所在的链表的头部
-
锁:Segment分段锁 Segment继承了ReentrantLock,锁定操作的Segment,其他的Segment不受影响,并发度为Segment个数,可以通过构造函数指定,数组扩容不会影响到其他的segment
-
get方法无需加锁,volatile保证
jdk8:
- 数据结构:synchronized+CAS+Node+红黑树,Node的val和next都用volatile修饰,保证可见性
- 查找,替换,赋值操作都使用CAS
- 锁:锁链表的head节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容时,阻塞所有的读写操作、并发扩容
- 读操作无锁:Node的val和next使用volatile修饰,读写线程对该变量互相可见。数组用volatile修饰,保证扩容时被读线程感知
13、如何实现一个IOC容器
1、配置文件配置包扫描路径
2、递归包扫描获取.class
文件
3、反射、确定需要交给IOC管理的类
4、对需要注入的类进行依赖注入
- 配置文件中指定需要扫描的包路径
- 定义一些注解,分别表示访问控制层、业务服务处、数据持久层、依赖注入注解,获取配置文件注解
- 从配置文件中获取需要扫描的包路径,获取到当前路径下的文件信息及文件夹信息,我们将当前路径下所有以
.class
结尾的文件添加到一个Set集合中进行存储 - 遍历这个Set集合,获取在类上有指定注解的类,并将其交给IOC容器,定义一个安全的Map用来存储这些对象
- 遍历整个IOC容器,获取到每一个类的实例,判断里面是有有依赖其他的类的实例,然后进行递归注入
14、什么是字节码,作用是什么
java中的编译器和解释器
java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟机的机器。这台虚拟机的机器在任何平台上都提供给编译程序一个共同的接口。
编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转化为特定系统的机器码执行。在java中,这种提供虚拟机理解的代码叫做字节码(即扩展名为.class的文件),它不面向任何特定的处理器,只面向虚拟机。
每一种平台的解释器是不同的,但是实现的虚拟机是相同的。java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后再特定的机器上运行,这也就是解释了java的编译与解释并存的特点。
java源代码----->编译器------->jvm可执行的java字节码(即虚拟机指令)------>jvm------->jvm中的解释器-------->机器可以执行的二进制机器码------>程序运行。
采用字节码的好处:
java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时保留了解释型语言可移植的特定。所以java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,java程序无需重新编译便可在多种不同的计算机上运行。
15、java类加载器有哪些
JDK自带有三个类加载器:bootstrap ClassLoader、ExtClassLoader、AppClassLoader.
- BootstrapClassLoader 是ExtClassLoader的父类加载器,默认负责加载
%JAVA_HOME%
下的jar包和class文件。 - ExtClassLoader是AppCLassLoader的父类加载器,负责加载
%JAVA_HOME%/lib/ext
文件夹下的jar包和class类。 - AppClassLoader是自定义加载器的父类,负责加载classpath下的类文件
- 继承ClassLoader实现自定义类加载器
16、双亲委派模型
双亲委派模型的好处:
- 主要是为了安全性,避免用户自己编写的类动态替换java的核心类,比如String。
- 同时也避免了类的重复加载,因为JVM中区分不同类,不仅仅是根据类名,相同的class文件被不同的ClassLoader加载就是不同的两个类。
17、java中的异常体系
- java中的所有的异常都来自顶级父类Throwable
- Throwable下有两个子类Exception和Error
- Error是程序无法处理的错误,一旦出现这个错误,则程序将被迫停止运行。
- Exception不会导致程序停止,又分为两个部分RunTimeException运行时异常和CheckException检查异常。
- RunTimeException常常发生在程序运行过程中,会导致程序当前线程执行失败。CheckedException常常发生在程序编译过程中,会导致程序编译不通过
铁汁们、铁汁们、铁汁们、都看到这里了。拜托、拜托、拜托、一键三连呗
18、GC如何判断对象可以被回收
- 引用计数法:每个对象有一个引用计数属性,新增一个引用计数加1,引用释放时计数减1,计数为0时可以回收。
- 可达性分析法:从GC Root开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,那么虚拟机就判断是可回收对象。
引用计数法,可能会出现A引用lB,B又引用了A,这时候就算他们都不在使用了,但因为相互引用计数器=1、永远无法完成回收。
GC Toots的对象有哪些:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中国JNI(即一般说的Native方法)引用的对象。
可达性算法中的不可达对象并不是并不是立即死亡的,对象拥有一次自我拯救的机会。对象被系统宣告死亡至少要经历两次标记过程:第一次是经过可达性分析发现没有GC Roots相连接的引用链,第二次是在由虚拟机自动建立的Finalizer队列中判断是否需要执行finalize()方法。
当对象变成(GC ROOts)不可达时,GC会判断该对象是否覆盖了finalize()方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize()方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize()方法。执行finalize()方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活"
每个对象只能触发一次finalize()方法
由于finalize()方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,不推荐大家使用,建议遗忘它
19、线程的生命周期及状态
1、线程通常有五种状态,创建、就绪、运行、阻塞和死亡状态。
2、阻塞的情况又分为三种:
-
等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入”等待池中“。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify或notifyAll方法才能被唤醒,wait是object类的方法
-
同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入”锁池“中
-
其他阻塞:运行的线程执行sleep或join方法,或者发出来I/O请求时,JVM会把该线程设置为阻塞状态。当sleep状态超时、join等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。sleep是Thread类的方法
1、新建状态(new):新建了一个线程对象
2、就绪状态(Runnable):线程对象创建后、其他线程调用了该对象的start()方法。该状态的线程位于
可运行线程池中,变得可运行,等待获取cpu的使用权。
3、运行状态(Runnbaleing):就绪状态的线程获取了CPU,执行程序代码。
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU的使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
5、死亡状态(Dead):线程执行完了或者因异常推出了run方法,该线程结束生命周期
20、sleep、wait、join、yield的区别
1、锁池
所有需要竞争同步锁的线程都会放在锁池中,比如当前对象的锁已经被其中一个线程得到,则其他线程需要在这个锁池进行等待,当前面的线程释放同步锁后锁池中的线程区竞争同步锁,当某个线程得到后会进入就绪队列进行等待CPU资源分配。
2、等待池
当我们调用wait()方法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁的。只有调用了notify()或notifyAll()后等待池的线程才会去竞争锁,notify()是随机从等待池选出一个线程放到锁池,而notifyAll()是将等待池中的所有线程放到锁池中。
1、sleep是Thread类的静态本地方法,wait是object类的本地方法
2、sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。
sleep就是把cpu的执行资格和执行权释放出去,不再运行此线程,当定时时间结束再取回cpu资源,参与cpu调度,获取到cpu资源后就可以继续运行了。而如果sleep时该线程有锁,那么sleep不会释放这个锁,而是把锁带着进入了冻结状态,也就是说其他需要这个锁的线程根本不可能获取到这个锁。也就是说无法执行程序。如果在睡眠期间其他线程调用了这个线程的interrupt方法,那么这个线程也就会抛出interruptexception异常返回,这点和wait是一样的
3、sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字
4、sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)。
5、sleep一般用于当前线程休眠,或者轮循暂停操作,wait则多用于多线程之间的通信
6、sleep会让出CPU执行时间且强制上下文切换,而wait则不一定,wait后可能还是有机会重新竞争到锁继续执行的。
yield()执行后线程直接进入就绪状态,马上释放了cpu的执行权,但是依然保留了cpu的执行资格,所以有可能cpu下次进行线程调度还会让这个线程获取到执行权继续执行。
join()执行后线程进入阻塞状态,例如在线程B中调用线程A的join(),那么线程B会进入到阻塞队列,直到线程A结束或中断线程