跳到主要内容
Java java 算法
Java 面试核心知识点整理 Java 面试的核心知识点,涵盖基础概念(OOP、集合、字符串)、高级特性(并发、JVM、内存管理)以及主流框架(Spring、MyBatis)。内容包括类加载、线程池、锁机制、垃圾回收、Spring 循环依赖解决方案及事务管理等关键技术点,并提供代码示例辅助理解,旨在帮助求职者系统复习 Java 技术栈。
宁静 发布于 2026/3/24 更新于 2026/5/2 27K 浏览Java 面试核心知识点整理
第一章 Java 基础篇
1. 你是怎样理解 OOP 面向对象
面向对象是利于语言对现实事物进行抽象。面向对象具有以下特征:
继承:继承是从已有类得到继承信息创建新类的过程
封装:封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口
多态性:多态性是指允许不同子类型的对象对同一消息作出不同的响应
2. 重载与重写区别
重载发生在本类,重写发生在父类与子类之间
重载的方法名必须相同,重写的方法名相同且返回值类型必须相同
重载的参数列表不同,重写的参数列表必须相同
重写的访问权限不能比父类中被重写的方法的访问权限更低
构造方法不能被重写
3. 接口与抽象类的区别
抽象类要被子类继承,接口要被类实现
接口可多继承接口,但类只能单继承
抽象类可以有构造器、接口不能有构造器
抽象类:除了不能实例化抽象类之外,它和普通 Java 类没有任何区别
抽象类:抽象方法可以有 public、protected 和 default 这些修饰符、接口:只能是 public
抽象类:可以有成员变量;接口:只能声明常量
4. 深拷贝与浅拷贝的理解
深拷贝和浅拷贝就是指对象的拷贝,一个对象中存在两种类型的属性,一种是基本数据类型,一种是实例对象的引用。
浅拷贝是指,只会拷贝基本数据类型的值,以及实例对象的引用地址,并不会复制一份引用地址所指向的对象,也就是浅拷贝出来的对象,内部的类属性指向的是同一个对象
深拷贝是指,既会拷贝基本数据类型的值,也会针对实例对象的引用地址所指向的对象进行复制,深拷贝出来的对象,内部的类执行指向的不是同一个对象
5. sleep 和 wait 区别
sleep 方法
属于 Thread 类中的方法
释放 cpu 给其它线程 不释放锁资源
sleep(1000) 等待超过 1s 被唤醒
wait 方法
属于 Object 类中的方法
释放 cpu 给其它线程,同时释放锁资源
wait(1000) 等待超过 1s 被唤醒
wait() 一直等待需要通过 notify 或者 notifyAll 进行唤醒
wait 方法必须配合 synchronized 一起使用,不然在运行时就会抛出 IllegalMonitorStateException 异常
锁释放时机代码演示
public static void main (String[] args) {
Object o = new Object ();
Thread thread = new Thread (() -> {
synchronized (o) {
System.out.println( + LocalDateTime.now() + + Thread.currentThread().getName());
{
o.wait( );
System.out.println( + LocalDateTime.now() + + Thread.currentThread().getName());
} (InterruptedException e) {
(e);
}
}
});
thread.start();
{
Thread.sleep( );
} (InterruptedException e) {
(e);
}
System.out.println( + LocalDateTime.now() + + Thread.currentThread().getName());
(o){
System.out.println( + LocalDateTime.now() + + Thread.currentThread().getName());
}
}
"新线程获取锁时间:"
" 新线程名称:"
try
2000
"新线程获取释放锁锁时间:"
" 新线程名称:"
catch
throw
new
RuntimeException
try
100
catch
throw
new
RuntimeException
"主线程获取锁时间:"
" 主线程名称:"
synchronized
"主线程获取释放锁锁时间:"
" 主线程名称:"
6. 什么是自动拆装箱 int 和 Integer 有什么区别 基本数据类型,如 int,float,double,boolean,char,byte,不具备对象的特征,不能调用方法。
装箱:将基本类型转换成包装类对象
拆箱:将包装类对象转换成基本类型的值
java 为什么要引入自动装箱和拆箱的功能?主要是用于 java 集合中,List list=new ArrayList();
list 集合如果要放整数的话,只能放对象,不能放基本类型,因此需要将整数自动装箱成对象。
**实现原理:**javac 编译器的语法糖,底层是通过 Integer.valueOf() 和 Integer.intValue() 方法实现。
Integer 是 int 的包装类,int 则是 java 的一种基本数据类型
Integer 变量必须实例化后才能使用,而 int 变量不需要
Integer 实际是对象的引用,当 new 一个 Integer 时,实际上是生成一个指针指向此对象;而 int 则是直接存储数据值
Integer 的默认值是 null,int 的默认值是 0
7. ==和 equals 区别
==
如果比较的是基本数据类型,那么比较的是变量的值
如果比较的是引用数据类型,那么比较的是地址值(两个对象是否指向同一块内存)
equals
如果没重写 equals 方法比较的是两个对象的地址值
如果重写了 equals 方法后我们往往比较的是对象中的属性的内容
equals 方法是从 Object 类中继承的,默认的实现就是使用==
8. String 能被继承吗 为什么用 final 修饰
不能被继承,因为 String 类有 final 修饰符,而 final 修饰的类是不能被继承的。
String 类是最常用的类之一,为了效率,禁止被继承和重写。
为了安全。String 类中有 native 关键字修饰的调用系统级别的本地方法,调用了操作系统的 API,如果方法可以重写,可能被植入恶意代码,破坏程序。Java 的安全性也体现在这里。
9. String buffer 和 String builder 区别
StringBuffer 与 StringBuilder 中的方法和功能完全是等价的,
只是 StringBuffer 中的方法大都采用了 synchronized 关键字进行修饰,因此是线程安全的,而 StringBuilder 没有这个修饰,可以被认为是线程不安全的。
在单线程程序下,StringBuilder 效率更快,因为它不需要加锁,不具备多线程安全而 StringBuffer 则每次都需要判断锁,效率相对更低
10. final、finally、finalize
**final:**修饰符(关键字)有三种用法:修饰类、变量和方法。修饰类时,意味着它不能再派生出新的子类,即不能被继承,因此它和 abstract 是反义词。修饰变量时,该变量使用中不被改变,必须在声明时给定初值,在引用中只能读取不可修改,即为常量。修饰方法时,也同样只能使用,不能在子类中被重写。
**finally:**通常放在 try…catch 的后面构造最终执行代码块,这就意味着程序无论正常执行还是发生异常,这里的代码只要 JVM 不关闭都能执行,可以将释放外部资源的代码写在 finally 块中。
**finalize:**Object 类中定义的方法,Java 中允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在销毁对象时调用的,通过重写 finalize() 方法可以整理系统资源或者执行其他清理工作。
11. Object 中有哪些方法
protected Object clone()--->创建并返回此对象的一个副本。
boolean equals(Object obj)--->指示某个其他对象是否与此对象'相等
protected void finalize()--->当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。
Class<? extends Object> getClass()--->返回一个对象的运行时类。
int hashCode()--->返回该对象的哈希码值。
void notify()--->唤醒在此对象监视器上等待的单个线程。
void notifyAll()--->唤醒在此对象监视器上等待的所有线程。
String toString()--->返回该对象的字符串表示。
void wait()--->导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法。
void wait(long timeout)--->导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量。
void wait(long timeout, int nanos)--->导致当前的线程等待,直到其他线程调用此对象的 notify()
12. 说一下集合体系
13. ArrayList 和 LinkedList 区别
ArrayList 是实现了基于动态数组的数据结构,LinkedList 基于链表的数据结构。
对于随机访问 get 和 set,ArrayList 效率优于 LinkedList,因为 LinkedList 要移动指针。
对于新增和删除操作 add 和 remove,LinkedList 比较占优势,因为 ArrayList 要移动数据。这一点要看实际情况的。若只对单条数据插入或删除,ArrayList 的速度反而优于 LinkedList。但若是批量随机的插入删除数据,LinkedList 的速度大大优于 ArrayList。因为 ArrayList 每插入一条数据,要移动插入点及之后的所有数据。
14. HashMap 底层是 数组 + 链表 + 红黑树,为什么要用这几类结构
数组 Node<K,V>[] table ,哈希表,根据对象的 key 的 hash 值进行在数组里面是哪个节点
链表的作用是解决 hash 冲突,将 hash 值取模之后的对象存在一个链表放在 hash 值对应的槽位
红黑树 JDK8 使用红黑树来替代超过 8 个节点的链表,主要是查询性能的提升,从原来的 O(n) 到 O(logn),
通过 hash 碰撞,让 HashMap 不断产生碰撞,那么相同的 key 的位置的链表就会不断增长,当对这个 Hashmap 的相应位置进行查询的时候,就会循环遍历这个超级大的链表,性能就会下降,所以改用红黑树
15. HashMap 和 HashTable 区别
线程安全性不同
HashMap 是线程不安全的,HashTable 是线程安全的,其中的方法是 Synchronized,在多线程并发的情况下,可以直接使用 HashTable,但是使用 HashMap 时必须自己增加同步处理。
是否提供 contains 方法
HashMap 只有 containsValue 和 containsKey 方法;HashTable 有 contains、containsKey 和 containsValue 三个方法,其中 contains 和 containsValue 方法功能相同。
key 和 value 是否允许 null 值
Hashtable 中,key 和 value 都不允许出现 null 值。HashMap 中,null 可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为 null。
数组初始化和扩容机制
HashTable 在不指定容量的情况下的默认容量为 11,而 HashMap 为 16,Hashtable 不要求底层数组的容量一定要为 2 的整数次幂,而 HashMap 则要求一定为 2 的整数次幂。
HashTable 扩容时,将容量变为原来的 2 倍加 1,而 HashMap 扩容时,将容量变为原来的 2 倍。
16. 线程的创建方式
继承 Thread 类创建线程
实现 Runnable 接口创建线程
使用 Callable 和 Future 创建线程 有返回值
使用线程池创建线程
代码演示 import java.util.concurrent.*;
public class threadTest {
public static void main (String[] args) throws ExecutionException, InterruptedException {
ThreadClass thread = new ThreadClass ();
thread.start();
Thread.sleep(100 );
System.out.println("#####################" );
RunnableClass runnable = new RunnableClass ();
new Thread (runnable).start();
Thread.sleep(100 );
System.out.println("#####################" );
FutureTask futureTask = new FutureTask (new CallableClass ());
futureTask.run();
System.out.println("callable 返回值:" + futureTask.get());
Thread.sleep(100 );
System.out.println("#####################" );
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor (1 , 1 , 2 , TimeUnit.SECONDS, new ArrayBlockingQueue <>(10 ));
threadPoolExecutor.execute(thread);
threadPoolExecutor.shutdown();
Thread.sleep(100 );
System.out.println("#####################" );
ExecutorService executorService = Executors.newFixedThreadPool(5 );
executorService.execute(thread);
executorService.shutdown();
}
}
class ThreadClass extends Thread {
@Override
public void run () {
System.out.println("我是继承 thread 形式:" + Thread.currentThread().getName());
}
}
class RunnableClass implements Runnable {
@Override
public void run () {
System.out.println("我是实现 runnable 接口:" + Thread.currentThread().getName());
}
}
class CallableClass implements Callable <String> {
@Override
public String call () {
System.out.println("我是实现 callable 接口:" );
return "我是返回值,可以通过 get 方法获取" ;
}
}
17. 线程的状态转换有什么(生命周期)
新建状态 (New) :线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
就绪状态 (Runnable): 也被称为'可执行状态'。线程对象被创建后,其它线程调用了该对象的 start() 方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被 CPU 调度执行。
运行状态 (Running):线程获取 CPU 权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
阻塞状态 (Blocked):阻塞状态是线程因为某种原因放弃 CPU 使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
等待阻塞 -- 通过调用线程的 wait() 方法,让线程等待某工作的完成。
同步阻塞 -- 线程在获取 synchronized 同步锁失败 (因为锁被其它线程所占用),它会进入同步阻塞状态。
其他阻塞 -- 通过调用线程的 sleep() 或 join() 或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep() 状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。
死亡状态 (Dead):线程执行完了或者因异常退出了 run() 方法,该线程结束生命周期。
18. Java 中有几种类型的流
19. 请写出你最常见的 5 个 RuntimeException
java.lang.NullPointerException
空指针异常;出现原因:调用了未经初始化的对象或者是不存在的对象。
java.lang.ClassNotFoundException
指定的类找不到;出现原因:类的名称和路径加载错误;通常都是程序试图通过字符串来加载某个类时可能引发异常。
java.lang.NumberFormatException
字符串转换为数字异常;出现原因:字符型数据中包含非数字型字符。
java.lang.IndexOutOfBoundsException
数组角标越界异常,常见于操作数组对象时发生。
java.lang.IllegalArgumentException
方法传递参数错误。
java.lang.ClassCastException
数据类型转换异常。
20. 谈谈你对反射的理解
反射机制
所谓的反射机制就是 java 语言在运行时拥有一项自观的能力。通过这种能力可以彻底了解自身的情况为下一步的动作做准备。
Java 的反射机制的实现要借助于 4 个类:class,Constructor,Field,Method;其中 class 代表的时类对象,Constructor-类的构造器对象,Field-类的属性对象,Method-类的方法对象。通过这四个对象我们可以粗略的看到一个类的各个组成部分。
Java 反射的作用
在 Java 运行时环境中,对于任意一个类,可以知道这个类有哪些属性和方法。对于任意一个对象,可以调用它的任意一个方法。这种动态获取类的信息以及动态调用对象的方法的功能来自于 Java 语言的反射(Reflection)机制。
Java 反射机制提供功能
在运行时判断任意一个对象所属的类。
在运行时构造任意一个类的对象。
在运行时判断任意一个类所具有的成员变量和方法。
在运行时调用任意一个对象的方法
21. 什么是 java 序列化,如何实现 java 序列化
序列化是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。序列化是为了解决在对对象流进行读写操作时所引发的问题。
序列化的实现:将需要被序列化的类实现 Serializable 接口,该接口没有需要实现的方法,implements Serializable 只是为了标注该对象是可被序列化的,然后使用一个输出流 (如:FileOutputStream) 来构造一个 ObjectOutputStream(对象流) 对象,接着,使用 ObjectOutputStream 对象的 writeObject(Object obj) 方法就可以将参数为 obj 的对象写出 (即保存其状态),要恢复的话则用输入流。
22. Http 常见的状态码
200 OK //客户端请求成功
301 Permanently Moved(永久移除),请求的 URL 已移走。Response 中应该包含一个 Location URL, 说明资源现在所处的位置
302 Temporarily Moved 临时重定向
400 Bad Request //客户端请求有语法错误,不能被服务器所理解
401 Unauthorized //请求未经授权,这个状态代码必须和 WWW-Authenticate 报头域一起使用
403 Forbidden //服务器收到请求,但是拒绝提供服务
404 Not Found //请求资源不存在,eg:输入了错误的 URL
500 Internal Server Error //服务器发生不可预期的错误
503 Server Unavailable //服务器当前不能处理客户端的请求,一段时间后可能恢复正常
23. GET 和 POST 的区别
GET 请求的数据会附在 URL 之后(就是把数据放置在 HTTP 协议头中),以?分割 URL 和传输数据,参数之间以&相连,如:login.action?name=zhagnsan&password=123456。POST 把提交的数据则放置在是 HTTP 包的包体中。
GET 方式提交的数据最多只能是 1024 字节,理论上 POST 没有限制,可传较大量的数据。其实这样说是错误的,不准确的:'GET 方式提交的数据最多只能是 1024 字节',因为 GET 是通过 URL 提交数据,那么 GET 可提交的数据量就跟 URL 的长度有直接关系了。而实际上,URL 不存在参数上限的问题,HTTP 协议规范没有对 URL 长度进行限制。这个限制是特定的浏览器及服务器对它的限制。IE 对 URL 长度的限制是 2083 字节 (2K+35)。对于其他浏览器,如 Netscape、FireFox 等,理论上没有长度限制,其限制取决于操作系统的支持。
POST 的安全性要比 GET 的安全性高。注意:这里所说的安全性和上面 GET 提到的'安全'不是同个概念。上面'安全'的含义仅仅是不作数据修改,而这里安全的含义是真正的 Security 的含义,比如:通过 GET 提交数据,用户名和密码将明文出现在 URL 上,因为 (1) 登录页面有可能被浏览器缓存,(2) 其他人查看浏览器的历史纪录,那么别人就可以拿到你的账号和密码了,除此之外,使用 GET 提交数据还可能会造成 Cross-site request forgery 攻击。
Get 是向服务器发索取数据的一种请求,而 Post 是向服务器提交数据的一种请求,在 FORM(表单)中,Method 默认为"GET",实质上,GET 和 POST 只是发送机制不同,并不是一个取一个发!
24. Cookie 和 Session 的区别
Cookie 是 web 服务器发送给浏览器的一块信息,浏览器会在本地一个文件中给每个 web 服务器存储 cookie。以后浏览器再给特定的 web 服务器发送请求时,同时会发送所有为该服务器存储的 cookie
Session 是存储在 web 服务器端的一块信息。session 对象存储特定用户会话所需的属性及配置信息。当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去
Cookie 和 session 的不同点
无论客户端做怎样的设置,session 都能够正常工作。当客户端禁用 cookie 时将无法使用 cookie
在存储的数据量方面:session 能够存储任意的 java 对象,cookie 只能存储 String 类型的对象
第二章 Java 高级篇
1. HashMap 底层源码 HashMap 的底层结构在 jdk1.7 中由数组 + 链表实现,在 jdk1.8 中由数组 + 链表 + 红黑树实现,以数组 + 链表的结构为例。
JDK1.8 之前 Put 方法:
HashMap 基于哈希表的 Map 接口实现,是以 key-value 存储形式存在,即主要用来存放键值对。HashMap 的实现不是同步的,这意味着它不是线程安全的。它的 key、value 都可以为 null。此外,HashMap 中的映射不是有序的。
JDK1.8 之前 HashMap 由 数组 + 链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突 (两个对象调用的 hashCode 方法计算的哈希码值一致导致计算的数组索引值相同) 而存在的('拉链法'解决冲突).JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(或者红黑树的边界值,默认为 8)并且当前数组的长度大于 64 时,此时此索引位置上的所有数据改为使用红黑树存储。
补充:将链表转换成红黑树前会判断,即使阈值大于 8,但是数组长度小于 64,此时并不会将链表变为红黑树。而是选择进行数组扩容。
这样做的目的是因为数组比较小,尽量避开红黑树结构,这种情况下变为红黑树结构,反而会降低效率,因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡。同时数组长度小于 64 时,搜索时间相对要快些。所以综上所述为了提高性能和减少搜索时间,底层在阈值大于 8 并且数组长度大于 64 时,链表才转换为红黑树。具体可以参考 treeifyBin 方法。
当然虽然增了红黑树作为底层数据结构,结构变得复杂了,但是阈值大于 8 并且数组长度大于 64 时,链表转换为红黑树时,效率也变的更高效。
注意:可以结合百度 hashmap 源码解析进行更深入的了解。
2. JVM 内存分哪几个区,每个区的作用是什么
方法区
有时候也成为永久代,在该区内很少发生垃圾回收,但是并不代表不发生 GC,在这里进行的 GC 主要是对方法区里的常量池和对类型的卸载
方法区主要用来存储已被虚拟机加载的类的信息、常量、静态变量和即时编译器编译后的代码等数据。
该区域是被线程共享的。
方法区里有一个运行时常量池,用于存放静态编译产生的字面量和符号引用。该常量池具有动态性,也就是说常量并不一定是编译时确定,运行时生成的常量也会存在这个常量池中。
虚拟机栈
虚拟机栈也就是我们平常所称的栈内存,它为 java 方法服务,每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接和方法出口等信息。
虚拟机栈是线程私有的,它的生命周期与线程相同。
局部变量表里存储的是基本数据类型、returnAddress 类型(指向一条字节码指令的地址)和对象引用,这个对象引用有可能是指向对象起始地址的一个指针,也有可能是代表对象的句柄或者与对象相关联的位置。局部变量所需的内存空间在编译器间确定
操作数栈的作用主要用来存储运算结果以及运算的操作数,它不同于局部变量表通过索引来访问,而是压栈和出栈的方式
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。动态链接就是将常量池中的符号引用在运行期转化为直接引用。
本地方法栈
本地方法栈和虚拟机栈类似,只不过本地方法栈为 Native 方法服务。
堆
java 堆是所有线程所共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都在这里创建,因此该区域经常发生垃圾回收操作。
程序计数器:
内存空间小,字节码解释器工作时通过改变这个计数值可以选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理和线程恢复等功能都需要依赖这个计数器完成。该内存区域是唯一一个 java 虚拟机规范没有规定任何 OOM 情况的区域。
3. Java 中垃圾收集的方法有哪些
复制算法 年轻代中使用的是 Minor GC,这种 GC 算法采用的是复制算法 (Copying)
a) 效率高,缺点:需要内存容量大,比较耗内存
b) 使用在占空间比较小、刷新次数多的新生区
标记 - 清除 老年代一般是由标记清除或者是标记清除与标记整理的混合实现
a) 效率比较低,会差生碎片。
标记 - 整理 老年代一般是由标记清除或者是标记清除与标记整理的混合实现
a) 效率低速度慢,需要移动对象,但不会产生碎片。
4. 如何判断一个对象是否存活 (或者 GC 对象的判定方法)
引用计数法
所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一。当一个对象的引用计数器为零时,说明此对象没有被引用,也就是'死对象',将会被垃圾回收.
引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象 A 引用对象 B,对象 B 又引用者对象 A,那么此时 A,B 对象的引用计数器都不为零,也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法。
可达性算法 (引用链法)
该算法的基本思路就是通过一些被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时(即从 GC Roots 节点到该节点不可达),则证明该对象是不可用的。
在 java 中可以作为 GC Roots 的对象有以下几种:虚拟机栈中引用的对象、方法区类静态属性引用的对象、方法区常量池引用的对象、本地方法栈 JNI 引用的对象。
5. 什么情况下会产生 StackOverflowError(栈溢出)和 OutOfMemoryError(堆溢出)怎么排查
引发 StackOverFlowError 的常见原因有以下几种
无限递归循环调用(最常见)
执行了大量方法,导致线程栈空间耗尽
方法内声明了海量的局部变量
native 代码有栈上分配的逻辑,并且要求的内存还不小,比如 java.net.SocketInputStream.read0 会在栈上要求分配一个 64KB 的缓存(64 位 Linux)。
引发 OutOfMemoryError 的常见原因有以下几种
内存中加载的数据量过于庞大,如一次从数据库取出过多数据
集合类中有对对象的引用,使用完后未清空,使得 JVM 不能回收
代码中存在死循环或循环产生过多重复的对象实体
启动参数内存值设定的过小
排查:可以通过 jvisualvm 进行内存快照分析
栈溢出、堆溢出案例演示
public class StackOverFlowTest {
private static int count = 1 ;
public static void main (String[] args) {
getOutOfMem();
}
public static void getDieCircle () {
System.out.println(count++);
getDieCircle();
}
public static void getOutOfMem () {
while (true ) {
Object o = new Object ();
System.out.println(o);
}
}
}
6. 什么是线程池,线程池有哪些(创建) 线程池就是事先将多个线程对象放到一个容器中,当使用的时候就不用 new 线程而是直接去池中拿线程即可,节省了开辟子线程的时间,提高的代码执行效率
在 JDK 的 java.util.concurrent.Executors 中提供了生成多种线程池的静态方法。
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(4);
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(4);
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
这 4 种线程池底层 全部是 ThreadPoolExecutor 对象的实现,阿里规范手册中规定线程池采用 ThreadPoolExecutor 自定义的,实际开发也是。
newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。这种类型的线程池特点是:
工作线程的创建数量几乎没有限制 (其实也有限制的,数目为 Interger.MAX_VALUE), 这样可灵活的往线程池中添加线程。
如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间 (默认为 1 分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
在使用 CachedThreadPool 时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。
newFixedThreadPool
创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。FixedThreadPool 是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。
newSingleThreadExecutor
创建一个单线程化的 Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序 (FIFO, LIFO, 优先级) 执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。
newScheduleThreadPool
创建一个定长的线程池,而且支持定时的以及周期性的任务执行。例如延迟 3 秒执行。
7. 为什么要使用线程池
线程池做的工作主要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最 大数量,超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。
主要特点:线程复用;控制最大并发数:管理线程。
第一:降低资源消耗。通过重复利用己创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进 行统一的分配,调优和监控
8. 线程池底层工作原理
第一步:线程池刚创建的时候,里面没有任何线程,等到有任务过来的时候才会创建线程。当然也可以调用 prestartAllCoreThreads() 或者 prestartCoreThread() 方法预创建 corePoolSize 个线程
第二步:调用 execute() 提交一个任务时,如果当前的工作线程数<corePoolSize,直接创建新的线程执行这个任务
第三步:如果当时工作线程数量>=corePoolSize,会将任务放入任务队列中缓存
第四步:如果队列已满,并且线程池中工作线程的数量<maximumPoolSize,还是会创建线程执行这个任务
第五步:如果队列已满,并且线程池中的线程已达到 maximumPoolSize,这个时候会执行拒绝策略,JAVA 线程池默认的策略是 AbortPolicy,即抛出 RejectedExecutionException 异常
9. ThreadPoolExecutor 对象有哪些参数 怎么设定核心线程数和最大线程数 拒绝策略有哪些
**corePoolSize:**核心线程数,
在 ThreadPoolExecutor 中有一个与它相关的配置:allowCoreThreadTimeOut(默认为 false),当 allowCoreThreadTimeOut 为 false 时,核心线程会一直存活,哪怕是一直空闲着。而当 allowCoreThreadTimeOut 为 true 时核心线程空闲时间超过 keepAliveTime 时会被回收。
**maximumPoolSize:**最大线程数
线程池能容纳的最大线程数,当线程池中的线程达到最大时,此时添加任务将会采用拒绝策略,默认的拒绝策略是抛出一个运行时错误(RejectedExecutionException)。值得一提的是,当初始化时用的工作队列为 LinkedBlockingDeque 时,这个值将无效。
**keepAliveTime:**存活时间,
当非核心空闲超过这个时间将被回收,同时空闲核心线程是否回收受 allowCoreThreadTimeOut 影响。
**unit:**keepAliveTime 的单位。
**workQueue:**任务队列
常用有三种队列,即 SynchronousQueue,LinkedBlockingDeque(无界队列),ArrayBlockingQueue(有界队列)。
**threadFactory:**线程工厂,
ThreadFactory 是一个接口,用来创建 worker。通过线程工厂可以对线程的一些属性进行定制。默认直接新建线程。
**RejectedExecutionHandler:**拒绝策略
也是一个接口,只有一个方法,当线程池中的资源已经全部使用,添加新线程被拒绝时,会调用 RejectedExecutionHandler 的 rejectedExecution 法。默认是抛出一个运行时异常。
需要分析线程池执行的任务的特性:CPU 密集型还是 IO 密集型
每个任务执行的平均时长大概是多少,这个任务的执行时长可能还跟任务处理逻辑是否涉及到网络传输以及底层系统资源依赖有关系
如果是 CPU 密集型,主要是执行计算任务,响应时间很快,cpu 一直在运行,这种任务 cpu 的利用率很高,那么线程数的配置应该根据 CPU 核心数来决定,CPU 核心数=最大同时执行线程数,加入 CPU 核心数为 4,那么服务器最多能同时执行 4 个线程。过多的线程会导致上下文切换反而使得效率降低。那线程池的最大线程数可以配置为 cpu 核心数 +1 如果是 IO 密集型,主要是进行 IO 操作,执行 IO 操作的时间较长,这是 cpu 出于空闲状态,导致 cpu 的利用率不高,这种情况下可以增加线程池的大小。这种情况下可以结合线程的等待时长来做判断,等待时间越高,那么线程数也相对越多。一般可以配置 cpu 核心数的 2 倍。
一个公式:线程池设定最佳线程数目 = ((线程池设定的线程等待时间 + 线程 CPU 时间)/
线程 CPU 时间 )* CPU 数目
这个公式的线程 cpu 时间是预估的程序单个线程在 cpu 上运行的时间(通常使用 loadrunner 测试大量运行次数求出平均值)
AbortPolicy:直接抛出异常,默认策略;
CallerRunsPolicy:用调用者所在的线程来执行任务;
DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
DiscardPolicy:直接丢弃任务;当然也可以根据应用场景实现 RejectedExecutionHandler 接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务
10. 常见线程安全的并发容器有哪些
CopyOnWriteArrayList、CopyOnWriteArraySet、ConcurrentHashMap
CopyOnWriteArrayList、CopyOnWriteArraySet 采用写时复制实现线程安全
ConcurrentHashMap 采用分段锁的方式实现线程安全
11. Atomic 原子类了解多少 原理是什么 Java 的原子类都存放在并发包 java.util.concurrent.atomic 下,如下图:
使用原子的方式更新基本类型
AtomicInteger:整型原子类
AtomicLong:长整型原子类
AtomicBoolean:布尔型原子类
使用原子的方式更新数组里的某个元素
AtomicIntegerArray:整形数组原子类
AtomicLongArray:长整形数组原子类
AtomicReferenceArray:引用类型数组原子类
AtomicReference:引用类型原子类
AtomicStampedReference:原子更新引用类型里的字段原子类
AtomicMarkableReference:原子更新带有标记位的引用类型
AtomicIntegerFieldUpdater:原子更新整形字段的更新器
AtomicLongFieldUpdater:原子更新长整形字段的更新器
AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,以及解决使用 CAS 进行原子更新时可能出现的 ABA 问题
AtomicInteger 类利用 CAS (Compare and Swap) + volatile + native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
CAS 的原理,是拿期望值和原本的值作比较,如果相同,则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是个本地方法,这个方法是用来拿'原值'的内存地址,返回值是 valueOffset;另外,value 是一个 volatile 变量,因此 JVM 总是可以保证任意时刻的任何线程总能拿到该变量的最新值。
12. synchronized 底层实现是什么 lock 底层是什么 有什么区别 Synchronized 原理:
方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM 可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有 monitor(虚拟机规范中用的是管程一词),然后再执行方法,最后再方法完成 (无论是正常完成还是非正常完成) 时释放 monitor。
代码块的同步是利用 monitorenter 和 monitorexit 这两个字节码指令。它们分别位于同步代码块的开始和结束位置。当 jvm 执行到 monitorenter 指令时,当前线程试图获取 monitor 对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器 +1;当执行 monitorexit 指令时,锁计数器 -1;当锁计数器为 0 时,该锁就被释放了。如果获取 monitor 对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。
Lock 的存储结构:一个 int 类型状态值(用于锁的状态变更),一个双向链表(用于存储等待中的线程)
Lock 获取锁的过程:本质上是通过 CAS 来获取状态值修改,如果当场没获取到,会将该线程放在线程等待链表中。
Lock 释放锁的过程:修改状态值,调整等待链表。
Lock 大量使用 CAS+ 自旋。因此根据 CAS 特性,lock 建议使用在低锁冲突的情况下。
Lock 的加锁和解锁都是由 java 代码配合 native 方法(调用操作系统的相关方法)实现的,而 synchronize 的加锁和解锁的过程是由 JVM 管理的
当一个线程使用 synchronize 获取锁时,若锁被其他线程占用着,那么当前只能被阻塞,直到成功获取锁。而 Lock 则提供超时锁和可中断等更加灵活的方式,在未能获取锁的 条件下提供一种退出的机制。
一个锁内部可以有多个 Condition 实例,即有多路条件队列,而 synchronize 只有一路条件队列;同样 Condition 也提供灵活的阻塞方式,在未获得通知之前可以通过中断线程以 及设置等待时限等方式退出条件队列。
synchronize 对线程的同步仅提供独占模式,而 Lock 即可以提供独占模式,也可以提供共享模式
synchronized Lock 关键字 类 自动加锁和释放锁 需要手动调用 unlock 方法释放锁 jvm 层面的锁 API 层面的锁 非公平锁 可以选择公平或者非公平锁 锁是一个对象,并且锁的信息保存在了对象中 代码中通过 int 类型的 state 标识 有一个锁升级的过程 无
13. 了解 ConcurrentHashMap 吗 为什么性能比 HashTable 高,说下原理 ConcurrentHashMap 是线程安全的 Map 容器,JDK8 之前,ConcurrentHashMap 使用锁分段技术,将数据分成一段段存储,每个数据段配置一把锁,即 segment 类,这个类继承 ReentrantLock 来保证线程安全,JKD8 的版本取消 Segment 这个分段锁数据结构,底层也是使用 Node 数组 + 链表 + 红黑树,从而实现对每一段数据就行加锁,也减少了并发冲突的概率。
hashtable 类基本上所有的方法都是采用 synchronized 进行线程安全控制,高并发情况下效率就降低,ConcurrentHashMap 是采用了分段锁的思想提高性能,锁粒度更细化
14. ConcurrentHashMap 底层原理
Java7 中 ConcurrentHashMap 使用的分段锁,也就是每一个 Segment 上同时只有一个线程可以操作,每一个 Segment 都是一个类似 HashMap 数组的结构,它可以扩容,它的冲突会转化为链表。但是 Segment 的个数一但初始化就不能改变。
public V put (K key, V value) {
Segment<K,V> s;
if (value == null )
throw new NullPointerException ();
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject
(segments, (j << SSHIFT) + SBASE)) == null )
s = ensureSegment(j);
return s.put(key, hash, value, false );
}
@SuppressWarnings("unchecked")
private Segment<K,V> ensureSegment (int k) {
final Segment<K,V>[] ss = this .segments;
long u = (k << SSHIFT) + SBASE;
Segment<K,V> seg;
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null ) {
Segment<K,V> proto = ss[0 ];
int cap = proto.table.length;
float lf = proto.loadFactor;
int threshold = (int )(cap * lf);
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry [cap];
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null ) {
Segment<K,V> s = new Segment <K,V>(lf, threshold, tab);
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null ) {
if (UNSAFE.compareAndSwapObject(ss, u, null , seg = s))
break ;
}
}
}
return seg;
}
final V put (K key, int hash, V value, boolean onlyIfAbsent) {
HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1 ) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null ) {
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break ;
}
e = e.next;
}
else {
if (node != null )
node.setNext(first);
else
node = new HashEntry <K,V>(hash, key, value, first);
int c = count + 1 ;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null ;
break ;
}
}
} finally {
unlock();
}
return oldValue;
}
Java8 中的 ConcurrentHashMap 使用的 Synchronized 锁加 CAS 的机制。结构 Node 数组 + 链表 / 红黑树,Node 是类似于一个 HashEntry 的结构。它的冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又退回链表。
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
// key 和 value 不能为空
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
// f = 目标位置元素
Node<K,V> f; int n, i, fh;// fh 后面存放目标位置的元素 hash 值
if (tab == null || (n = tab.length) == 0)
// 数组桶为空,初始化数组桶(自旋+CAS)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 桶内为空,CAS 放入,不加锁,成功了就直接 break 跳出
if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
// 使用 synchronized 加锁加入节点
synchronized (f) {
if (tabAt(tab, i) == f) {
// 说明是链表
if (fh >= 0) {
binCount = 1;
// 循环加入新的或者覆盖节点
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
// 红黑树
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
### 15. 了解 volatile 关键字不
1. volatile 是 Java 提供的最轻量级的同步机制,保证了共享变量的可见性,被 volatile 关键字修饰的变量,如果值发生了变化,其他线程立刻可见,避免出现脏读现象。
2. volatile 禁止了指令重排,可以保证程序执行的有序性,但是由于禁止了指令重排,所以 JVM 相关的优化没了,效率会偏弱
### 16. synchronized 和 volatile 有什么区别
1. volatile 本质是告诉 JVM 当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
2. volatile 仅能用在变量级别,而 synchronized 可以使用在变量、方法、类级别。
3. volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。
4. volatile 不会造成线程阻塞,synchronized 可能会造成线程阻塞。
5. volatile 标记的变量不会被编译器优化,synchronized 标记的变量可以被编译器优化。
### 17. Java 类加载过程
1. 加载 加载时类加载的第一个过程,在这个阶段,将完成一下三件事情:
通过一个类的全限定名获取该类的二进制流。
将该二进制流中的静态存储结构转化为方法去运行时数据结构。
在内存中生成该类的 Class 对象,作为该类的数据访问入口。
2. 验证 验证的目的是为了确保 Class 文件的字节流中的信息不回危害到虚拟机。在该阶段主要完成以下四钟验证:
文件格式验证:验证字节流是否符合 Class 文件的规范,如主次版本号是否在当前虚拟机范围内,常量池中的常量是否有不被支持的类型.
元数据验证:对字节码描述的信息进行语义分析,如这个类是否有父类,是否集成了不被继承的类等。
字节码验证:是整个验证过程中最复杂的一个阶段,通过验证数据流和控制流的分析,确定程序语义是否正确,主要针对方法体的验证。如:方法中的类型转换是否正确,跳转指令是否正确等。
符号引用验证:这个动作在后面的解析过程中发生,主要是为了确保解析动作能正确执行。
3. 准备
准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中。
4. 解析
该阶段主要完成符号引用到直接引用的转换动作。解析动作并不一定在初始化动作完成之前,也有可能在初始化之后。
5. 初始化
初始化时类加载的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的 Java 程序代码。
### 18. 什么是类加载器,类加载器有哪些
类加载器就是把类文件加载到虚拟机中,也就是说通过一个类的全限定名来获取描述该类的二进制字节流。
1. 主要有以下四种类加载器
启动类加载器 (Bootstrap ClassLoader) 用来加载 java 核心类库,无法被 java 程序直接引用
扩展类加载器 (extension class loader ):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类
系统类加载器(system class loader )也叫应用类加载器:它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader() 来获取它
用户自定义类加载器,通过继承 java.lang.ClassLoader 类的方式实现
2. 什么时候会使用到加载器?java 中的加载器是按需加载,什么时候用到,什么时候加载
- new 对象的时候
- 访问某个类或者接口的静态变量,或者对该静态变量赋值时
- 调用类的静态方法时
- 反射
- 初始化一个类的子类时,其父类首先会被加载
- JVM 启动时标明的启动类,也就是文件名和类名相同的那个类
### 19. 简述 java 内存分配与回收策略以及 Minor GC 和 Major GC(full GC)
1. **内存分配**
**栈区**:栈分为 java 虚拟机栈和本地方法栈
**堆区**:堆被所有线程共享区域,在虚拟机启动时创建,唯一目的存放对象实例。堆区是 gc 的主要区域,通常情况下分为两个区块年轻代和年老代。更细一点年轻代又分为 Eden 区,主要放新创建对象,From survivor 和 To survivor 保存 gc 后幸存下的对象,默认情况下各自占比 8 :1 :1 。
**方法区**:被所有线程共享区域,用于存放已被虚拟机加载的类信息,常量,静态变量等数据。被 Java 虚拟机描述为堆的一个逻辑部分。习惯是也叫它永久代(permanment generation)
**程序计数器**:当前线程所执行的行号指示器。通过改变计数器的值来确定下一条指令,比如循环,分支,跳转,异常处理,线程恢复等都是依赖计数器来完成。线程私有的。
2. **回收策略以及 Minor GC 和 Major GC**
- 对象优先在堆的 Eden 区分配
- 大对象直接进入老年代
- 长期存活的对象将直接进入老年代
当 Eden 区没有足够的空间进行分配时,虚拟机会执行一次 Minor GC.Minor GC 通常发生在新生代的 Eden 区,在这个区的对象生存期短,往往发生 GC 的频率较高,回收速度比较快;Full Gc/Major GC 发生在老年代,一般情况下,触发老年代 GC 的时候不会触发 Minor GC,但是通过配置,可以在 Full GC 之前进行一次 Minor GC 这样可以加快老年代的回收速度。
### 20. 如何查看 java 死锁
1. ####演示死锁
```java
package com.ssg.mst;
public class 死锁 {
private static final String lock1 = "lock1" ;
private static final String lock2 = "lock2" ;
public static void main (String[] args) {
Thread thread1 = new Thread (() -> {
while (true ) {
synchronized (lock1) {
try {
System.out.println(Thread.currentThread().getName() + lock1);
Thread.sleep(1000 );
synchronized (lock2){
System.out.println(Thread.currentThread().getName() + lock2);
}
} catch (InterruptedException e) {
throw new RuntimeException (e);
}
}
}
});
Thread thread2 = new Thread (() -> {
while (true ) {
synchronized (lock2) {
try {
System.out.println(Thread.currentThread().getName() + lock2);
Thread.sleep(1000 );
synchronized (lock1){
System.out.println(Thread.currentThread().getName() + lock1);
}
} catch (InterruptedException e) {
throw new RuntimeException (e);
}
}
}
});
thread1.start();
thread2.start();
}
}
程序运行,进程没有停止。
通过 jps 查看 java 进程,找到没有停止的进程
通过 jstack 9060 查看进程具体执行信息
21. Java 死锁如何避免
一个资源每次只能被一个线程使用
一个线程在阻塞等待某个资源时,不释放已占有资源
一个线程已经获得的资源,在未使用完之前,不能被强行剥夺
若干线程形成头尾相接的循环等待资源关系
这是造成死锁必须要达到的 4 个条件,如果要避免死锁,只需要不满足其中某一个条件即可。而其中前 3 个条件是作为锁要符合的条件,所以要避免死锁就需要打破第 4 个条件,不出现循环等待锁的关系。
要注意加锁顺序,保证每个线程按同样的顺序进行加锁
要注意加锁时限,可以针对锁设置一个超时时间
要注意死锁检查,这是一种预防机制,确保在第一时间发现死锁并进行解决
第三章-java 框架篇
1. 简单的谈一下 SpringMVC 的工作流程
用户发送请求至前端控制器 DispatcherServlet
DispatcherServlet 收到请求调用 HandlerMapping 处理器映射器。
处理器映射器找到具体的处理器,生成处理器对象及处理器拦截器 (如果有则生成) 一并返回给 DispatcherServlet。
DispatcherServlet 调用 HandlerAdapter 处理器适配器
HandlerAdapter 经过适配调用具体的处理器 (Controller,也叫后端控制器)。
Controller 执行完成返回 ModelAndView
HandlerAdapter 将 controller 执行结果 ModelAndView 返回给 DispatcherServlet
DispatcherServlet 将 ModelAndView 传给 ViewReslover 视图解析器
ViewReslover 解析后返回具体 View
DispatcherServlet 根据 View 进行渲染视图(即将模型数据填充至视图中)。
DispatcherServlet 响应用户
2. 说出 Spring 或者 SpringMVC 中常用的 5 个注解
@Component 基本注解,标识一个受 Spring 管理的组件
@Controller 标识为一个表示层的组件
@Service 标识为一个业务层的组件
@Repository 标识为一个持久层的组件
@Autowired 自动装配
@Qualifier("") 具体指定要装配的组件的 id 值
@RequestMapping() 完成请求映射
@PathVariable 映射请求 URL 中占位符到请求处理方法的形参
3. 简述 SpringMVC 中如何返回 JSON 数据 Step1:在项目中加入 json 转换的依赖,例如 jackson,fastjson,gson 等
Step2:在请求处理方法中将返回值改为具体返回的数据的类型,例如数据的集合类 List等
Step3:在请求处理方法上使用@ResponseBody 注解
4. 谈谈你对 Spring 的理解 Spring 是一个开源框架,为简化企业级应用开发而生。Spring 可以是使简单的 JavaBean 实现以前只有 EJB 才能实现的功能。Spring 是一个 IOC 和 AOP 容器框架。
控制反转(IOC),传统的 java 开发模式中,当需要一个对象时,我们会自己使用 new 或者 getInstance 等直接或者间接调用构造方法创建一个对象。而在 spring 开发模式中,spring 容器使用了工厂模式为我们创建了所需要的对象,不需要我们自己创建了,直接调用 spring 提供的对象就可以了,这是控制反转的思想。
依赖注入(DI),spring 使用 javaBean 对象的 set 方法或者带参数的构造方法为我们在创建所需对象时将其属性自动设置所需要的值的过程,就是依赖注入的思想。
面向切面编程(AOP),在面向对象编程(oop)思想中,我们将事物纵向抽成一个个的对象。而在面向切面编程中,我们将一个个的对象某些类似的方面横向抽成一个切面,对这个切面进行一些如权限控制、事物管理,记录日志等公用操作处理的过程就是面向切面编程的思想。AOP 底层是动态代理,如果是接口采用 JDK 动态代理,如果是类采用 CGLIB 方式实现动态代理。
5. Spring 中常用的设计模式
代理模式——spring 中两种代理方式,若目标对象实现了若干接口,spring 使用 jdk 的 java.lang.reflect.Proxy 类代理。若目标兑现没有实现任何接口,spring 使用 CGLIB 库生成目标类的子类。
单例模式——在 spring 的配置文件中设置 bean 默认为单例模式。
模板方式模式——用来解决代码重复的问题。
比如:RestTemplate、JmsTemplate、JpaTemplate
工厂模式——在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用同一个接口来指向新创建的对象。Spring 中使用 beanFactory 来创建对象的实例。
6. Spring 循环依赖问题
常见问法 请解释一下 spring 中的三级缓存
三级缓存分别是什么?三个 Map 有什么异同?
什么是循环依赖?请你谈谈?看过 spring 源码吗?
如何检测是否存在循环依赖?实际开发中见过循环依赖的异常吗?
多例的情况下,循环依赖问题为什么无法解决?
什么是循环依赖?
两种注入方式对循环依赖的影响?
三级缓存 名称 对象名 含义 一级缓存 singletonObjects 存放已经经历了完整生命周期的 Bean 对象 二级缓存 earlySingletonObjects 存放早期暴露出来的 Bean 对象,Bean 的生命周期未结束(属性还未填充完) 三级缓存 singletonFactories 存放可以生成 Bean 的工厂
四个关键方法 package org.springframework.beans.factory.support;
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
private final Map<String, Object> singletonObjects = new ConcurrentHashMap <>(256 );
private final Map<String, Object> earlySingletonObjects = new HashMap <>(16 );
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap <>(16 );
}
debug 源代码过程 需要 22 个断点 (可选)
1,A 创建过程中需要 B,于是 A 将自己放到三级缓里面,去实例化 B
2,B 实例化的时候发现需要 A,于是 B 先查一级缓存,没有,再查二级缓存,还是没有,再查三级缓存,找到了 A 然后把三级缓存里面的这个 A 放到二级缓存里面,并删除三级缓存里面的 A
3,B 顺利初始化完毕,将自己放到一级缓存里面 (此时 B 里面的 A 依然是创建中状态)
然后回来接着创建 A,此时 B 已经创建结束,直接从一级缓存里面拿到 B,然后完成创建,并将 A 自己放到一级缓存里面。
总结 1,Spring 创建 bean 主要分为两个步骤,创建原始 bean 对象,接着去填充对象属性和初始化。
2,每次创建 bean 之前,我们都会从缓存中查下有没有该 bean,因为是单例,只能有一个。
3,当创建 A 的原始对象后,并把它放到三级缓存中,接下来就该填充对象属性了,这时候发现依赖了 B,接着就又去创建 B,同样的流程,创建完 B 填充属性时又发现它依赖了 A 又是同样的流程,不同的是:这时候可以在三级缓存中查到刚放进去的原始对象 A。
所以不需要继续创建,用它注入 B,完成 B 的创建既然 B 创建好了,所以 A 就可以完成填充属性的步骤了,接着执行剩下的逻辑,闭环完成
Spring 解决循环依赖依靠的是 Bean 的"中间态"这个概念,而这个中间态指的是已经实例化但还没初始化的状态—>半成品。实例化的过程又是通过构造器创建的,如果 A 还没创建好出来怎么可能提前曝光,所以构造器的循环依赖无法解决
其他衍生问题 问题 1:为什么构造器注入属性无法解决循环依赖问题?
由于 spring 中的 bean 的创建过程为先实例化 再初始化 (在进行对象实例化的过程中不必赋值) 将实例化好的对象暴露出去,供其他对象调用,然而使用构造器注入,必须要使用构造器完成对象的初始化的操作,就会陷入死循环的状态
问题 2:一级缓存能不能解决循环依赖问题?不能
在三个级别的缓存中存储的对象是有区别的 一级缓存为完全实例化且初始化的对象 二级缓存实例化但未初始化对象 如果只有一级缓存,如果是并发操作下,就有可能取到实例化但未初始化的对象,就会出现问题
问题 3:二级缓存能不能解决循环依赖问题?
理论上二级缓存可以解决循环依赖问题,但是需要注意,为什么需要在三级缓存中存储匿名内部类 (ObjectFactory),原因在于 需要创建代理对象 eg:现有 A 类,需要生成代理对象 A 是否需要进行实例化 (需要) 在三级缓存中存放的是生成具体对象的一个匿名内部类,该类可能是代理类也可能是普通的对象,而使用三级缓存可以保证无论是否需要是代理对象,都可以保证使用的是同一个对象,而不会出现,一会儿使用普通 bean 一会儿使用代理类
7. 介绍一下 Spring bean 的生命周期、注入方式和作用域 Bean 的生命周期
(1)默认情况下,IOC 容器中 bean 的生命周期分为五个阶段:
调用构造器 或者是通过工厂的方式创建 Bean 对象
给 bean 对象的属性注入值
调用初始化方法,进行初始化,初始化方法是通过 init-method 来指定的.
使用
IOC 容器关闭时,销毁 Bean 对象.
(2)当加入了 Bean 的后置处理器后,IOC 容器中 bean 的生命周期分为七个阶段:
调用构造器 或者是通过工厂的方式创建 Bean 对象
给 bean 对象的属性注入值
执行 Bean 后置处理器中的 postProcessBeforeInitialization
调用初始化方法,进行初始化,初始化方法是通过 init-method 来指定的.x
执行 Bean 的后置处理器中 postProcessAfterInitialization
使用
IOC 容器关闭时,销毁 Bean 对象
只需要回答出第一点即可,第二点也回答可适当 加分。
注入方式:
通过 setter 方法注入
通过构造方法注入
Singleton 单例的
Prototype 原型的
Request
Session
8. 请描述一下 Spring 的事务管理 (1)声明式事务管理的定义:用在 Spring 配置文件中声明式的处理事务来代替代码式的处理事务。这样的好处是,事务管理不侵入开发的组件,具体来说,业务逻辑对象就不会意识到正在事务管理之中,事实上也应该如此,因为事务管理是属于系统层面的服务,而不是业务逻辑的一部分,如果想要改变事务管理策划的话,也只需要在定义文件中重新配置即可,这样维护起来极其方便。
基于 TransactionInterceptor 的声明式事务管理:两个次要的属性:transactionManager,用来指定一个事务治理器,并将具体事务相关的操作请托给它;其他一个是 Properties 类型的 transactionAttributes 属性,该属性的每一个键值对中,键指定的是方法名,方法名可以行使通配符,而值就是表现呼应方法的所运用的事务属性。
(2)基于@Transactional 的声明式事务管理:Spring 2.x 还引入了基于 Annotation 的体式格式,具体次要触及@Transactional 标注。@Transactional 可以浸染于接口、接口方法、类和类方法上。算作用于类上时,该类的一切 public 方法将都具有该类型的事务属性。
(3)编程式事物管理的定义:在代码中显式挪用 beginTransaction()、commit()、rollback() 等事务治理相关的方法,这就是编程式事务管理。Spring 对事物的编程式管理有基于底层 API 的编程式管理和基于 TransactionTemplate 的编程式事务管理两种方式。
9. MyBatis 中 #{}和${}的区别是什么 #{}是预编译处理,${}是字符串替换;
Mybatis 在处理#{}时,会将 sql 中的#{}替换为?号,调用 PreparedStatement 的 set 方法来赋值;
Mybatis 在处理时,就是把时,就是把{}替换成变量的值;
使用#{}可以有效的防止 SQL 注入,提高系统安全性。
10. Mybatis 中一级缓存与二级缓存
MyBatis 的缓存分为一级缓存和 二级缓存。
一级缓存是 SqlSession 级别的缓存,默认开启。
二级缓存是 NameSpace 级别 (Mapper) 的缓存,多个 SqlSession 可以共享,使用时需要进行配置开启。
缓存的查找顺序:二级缓存 => 一级缓存 => 数据库
11. MyBatis 如何获取自动生成的 (主) 键值 在标签中使用 useGeneratedKeys 和 keyProperty 两个属性来获取自动生成的主键值。
示例:
<insert id ="insertname" usegeneratedkeys ="true" keyproperty ="id" >
insert into names (name) values (#{name})
</insert >
12. 简述 Mybatis 的动态 SQL,列出常用的 6 个标签及作用 动态 SQL 是 MyBatis 的强大特性之一 基于功能强大的 OGNL 表达式。
动态 SQL 主要是来解决查询条件不确定的情况,在程序运行期间,根据提交的条件动态的完成查询
常用的标签:
: 进行条件的判断
:在判断后的 SQL 语句前面添加 WHERE 关键字,并处理 SQL 语句开始位置的 AND 或者 OR 的问题
:可以在 SQL 语句前后进行添加指定字符 或者去掉指定字符.
: 主要用于修改操作时出现的逗号问题
:类似于 java 中的 switch 语句。在所有的条件中选择其一
:迭代操作
13. Mybatis 如何完成 MySQL 的批量操作 MyBatis 完成 MySQL 的批量操作主要是通过标签来拼装相应的 SQL 语句
例如:
<insert id ="insertBatch" >
insert into tbl_employee(last_name,email,gender,d_id) values
<foreach collection ="emps" item ="curr_emp" separator ="," >
(#{curr_emp.lastName},#{curr_emp.email},#{curr_emp.gender},#{curr_emp.dept.id})
</foreach >
</insert >
14. 谈谈怎么理解 SpringBoot 框架 Spring Boot 是 Spring 开源组织下的子项目,是 Spring 组件一站式解决方案,主要是简化了使用 Spring 的难度,简省了繁重的配置,提供了各种启动器,开发者能快速上手。
独立运行
Spring Boot 而且内嵌了各种 servlet 容器,Tomcat、Jetty 等,现在不再需要打成 war 包部署到容器中,Spring Boot 只要打成一个可执行的 jar 包就能独立运行,所有的依赖包都在一个 jar 包内。
简化配置
spring-boot-starter-web 启动器自动依赖其他组件,简少了 maven 的配置。除此之外,还提供了各种启动器,开发者能快速上手。
自动配置
Spring Boot 能根据当前类路径下的类、jar 包来自动配置 bean,如添加一个 spring-boot-starter-web 启动器就能拥有 web 的功能,无需其他配置。
无代码生成和 XML 配置
Spring Boot 配置过程中无代码生成,也无需 XML 配置文件就能完成所有配置工作,这一切都是借助于条件注解完成的,这也是 Spring4.x 的核心功能之一。
应用监控
Spring Boot 提供一系列端点可以监控服务及应用,做健康检测。
Spring Boot 缺点:
Spring Boot 虽然上手很容易,但如果你不了解其核心技术及流程,所以一旦遇到问题就很棘手,而且现在的解决方案也不是很多,需要一个完善的过程。
相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online