收集大量Java经典面试题目📚,内容涵盖了包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 等知识点🏝️。适合准备Java面试的读者参考和复习🌟📢。
❗ ❗ ❗
关注公众号:枫蜜柚子茶 ✅✅
🗳
📑 回 复 “ Java面试 ” 获 取 完 整 资 料⬇ ⬇ ⬇
📖Java并发编程面试Top123道题🔥🔥
1️⃣ 基 础 知 识
2️⃣ 并 发 理 论 🚩
3️⃣ 线 程 池
4️⃣ 并 发 容 器5️⃣ 并 发 队 列
6️⃣ 并 发 工 具 类
1. Java中垃圾回收有什么目的?什么时候进行垃圾回收?
- ◾ 垃圾回收是在内存中存在没有引用的对象或超过作用域的对象时进行的。
- ◾ 垃圾回收的目的是识别并且丢弃应用不再使用的对象来释放和重用资源。
2. 线程之间如何通信及线程之间如何同步
📛在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步。通信是指线 程之间以如何来交换信息。 一般线程之间的通信机制有两种:共享内存和消息传递。
Java的并发采用的是共享内存模型➰, Java线程之间的通信总是隐式进行,整个通信过程对程序员完 全透明。如果编写多线程程序的Java程序员不理解隐式进行的线程之间通信的工作机制,很可能会 遇到各种奇怪的内存可见性问题。
3. Java内存模型
⭕共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入时,能对另一 个线程可见。从抽象的角度来看, JMM定义了线程和主内存之间的抽象关系:线程之间的共享变 量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本 地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
🔘 从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:
- 1️⃣. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
- 2️⃣. 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。
下面通过示意图来说明线程之间的通信
✅ 总结:什么是Java内存模型:java内存模型简称jmm,定义了一个线程对另一个线程可见。共享变 量存放在主内存中,每个线程都有自己的本地内存,当多个线程同时访问一个数据的时候,可能本 地内存没有及时刷新到主内存,所以就会发生线程安全问题。
4. 如果对象的引用被置为null,垃圾收集器是否会立即释放对象占用的内存?
- ◾ 不会,在下一个垃圾回调周期中,这个对象将是被可回收的。
- ◾ 也就是说并不会立即被垃圾收集器立刻回收,而是在下一次垃圾回收时才会释放其占用的内存。
5. finalize()方法什么时候被调用?析构函数(finalization)的目的是什么?
垃圾回收器📂(garbage colector)决定回收某对象时,就会运行该对象的finalize()方法;finalize是Object类的一个方法,该方法在Object类中的声明protected void finalize() throwsThrowable { } 在垃圾回收器执行时会调用被回收对象的finalize()方法,可以覆盖此方法来实现对 其资源的回收。注意: 一旦垃圾回收器准备释放对象占用的内存,将首先调用该对象的finalize()方 法,并且下一次垃圾回收动作发生时,才真正回收对象占用的内存空间。
- ◾ GC本来就是内存回收了,应用还需要在finalization做什么呢? 答案是大部分时候,什么都 不用做(也就是不需要重载)。只有在某些很特殊的情况下,比如你调用了一些native的方法 (一般是C写的),可以要在finaliztion里去调用C的释放函数。
- ◾ Finalizetion主要用来释放被对象占用的资源(不是指内存,而是指其他资源,比如文件(File Handle)、端口(ports)、数据库连接(DB Connection)等)。然而,它不能真正有效地工作。
6. 什么是重排序
程序执行的顺序按照代码的先后顺序执行。
一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,进行重新排序(重排序),它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和 代码顺序执行的结果是一致的。
int int a = r = a = 5; //语句1
r = 3; //语句2
a + 2; //语句3
a*a; //语句4
则因为重排序,他还可能执行顺序为(这里标注的是语句的执行顺序) 2-1-3-4 ,1-3-2-4 但绝不可能 2-1-4-3 ,因为这打破了依赖关系。
显然重排序对单线程运行是不会有任何问题,但是多线程就不一定了,所以我们在多线程编程时就 得考虑这个问题了。
7. 重排序实际执行的指令步骤
- 1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 2. 指令级并行的重排序。现代处理器采用了指令级并行技术(ILP)来将多条指令重叠执行。如果不 存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在 乱序执行。
❗这些重排序对于单线程没问题,但是多线程都可能会导致多线程程序出现内存可见性问题。
8. 重排序遵守的规则
as-if-serial:
- 1. 不管怎么排序,结果不能改变
- 2. 不存在数据依赖的可以被编译器和处理器重排序
- 3. 一个操作依赖两个操作,这两个操作如果不存在依赖可以重排序
- 4. 单线程根据此规则不会有问题,但是重排序后多线程会有问题
9. as-if-serial规则和happens-before规则的区别
- ◾ as-if-serial语义保证单线程内程序的执行结果不被改变, happens-before关系保证正确同步的多 线程程序的执行结果不被改变。
- ◾ as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。 happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。
- ◾ as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽 可能地提高程序执行的并行度。
- ◾ as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。 happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。
10. 并发关键字 synchronized ?
◾ 在 Java 中,synchronized 关键字是用来控制线程同步的,就是在多线程的环境下,控制 synchronized 代码段不被多个线程同时执行。 synchronized可以修饰类、方法、变量。
◾ 另外,在 Java早期版本中, synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上 的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时 需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这 也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。 JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来 减少锁操作的开销。
11. 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗
synchronized关键字最主要的三种使用方式:
⭕ 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
⭕ 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个 实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个 实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实 例对象锁。
⭕修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
💡 总结: synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String) 因为JVM中,字符串常量池具有缓存功能!
12. 单例模式了解吗?给我解释一下双重检验锁方式实现单例模式! ℽ
双重校验锁实现对象单例(线程安全)
说明:
双锁机制的出现是为了解决前面同步问题和性能问题,看下面的代码,简单分析下确实是解决了多 线程并行进来不会出现重复new对象,而且也实现了懒加载
public class Singleton {
private volatile static Singleton uniqueInstance; private Singleton() {}
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码 if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton(); }
} }
return uniqueInstance; }
}
另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。
uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:
- 1. 为 uniqueInstance 分配内存空间
- 2. 初始化 uniqueInstance
- 3. 将 uniqueInstance 指向分配的内存地址
- 2. 初始化 uniqueInstance
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时uniqueInstance 还未被初始化。使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
13. 说一下 synchronized 底层实现原理?
◾ Synchronized的语义底层是通过一个monitor (监视器锁)的对象来完成。
◾ 每个对象有一个监视器锁(monitor)。每个Synchronized修饰过的代码当它的monitor被占用时就 会处于锁定状态并且尝试获取monitor的所有权 ,过程:
- 1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为 monitor的所有者。
- 2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
- 3、如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再 重新尝试获取monitor的所有权。
synchronized是可以通过 反汇编指令 javap命令,查看相应的字节码文件。
14. synchronized可重入的原理
重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁。底层原理维护一个计数器,当线 程获取该锁时,计数器加一,再次获得该锁时继续加一,释放锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁。
15. 什么是自旋
⭕ 很多 synchronized 里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁 可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然synchronized 里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在synchronized 的边界做忙循环,这就是自旋。如果做了多次循环发现还没有获得锁,再阻塞,这样可能是一种更 好的策略。
◾ 忙循环:就是程序员用循环让一个线程等待,不像传统方法wait(), sleep() 或 yield() 它们都放弃了 CPU控制,而忙循环不会放弃CPU,它就是在运行一个空循环。这么做的目的是为了保留CPU缓存,在多核系统中, 一个等待线程醒来的时候可能会在另一个内核运行,这样会重建缓存。为了避 免重建缓存和减少等待重建的时间就可以使用它了。