【Java面试题】第九期:Java进阶篇,每周10道,根本停不下来~

在这里插入图片描述

1️⃣ 在异常处理时,何时应抛出异常?何时应捕获异常?

(1)抛出异常:若抛出异常则相当于告诉上层方法一种可能出现异常的提示,通常表示程序内部无法处理这个异常,需要交由被调用处来处理。一般应用在不该由程序内部来处理的异常 或者程序内部无法处理的异常情况。例如,参数不符合要求、文件不存在等情况,这时就需要使用 throws 抛出异常。

以下是一段显式抛出异常的代码:

public void divide(int num1, int num2) throws ArithmeticException {
    if (num2 == 0) {
        throw new ArithmeticException("除数不能为0");
    }
    int result = num1 / num2;
    System.out.println("结果为:" + result);
}

这段代码中,定义了一个 divide 方法,用于计算两个整数的商。如果除数为0,我们会抛出一个 ArithmeticException 异常,并且在异常中传入一个字符串作为异常信息。如果除数不为0,我们会正常计算并输出结果。注意,我们在方法的声明中使用了 throws 关键字,表示该方法可能会抛出 ArithmeticException 异常。

(2)捕获异常:与抛出异常相反,当程序内部代码执行过程中出现逻辑异常情况时,应该捕获异常。例如,读取文件时发生IO异常,这时就需要使用 try-catch 语句捕获异常。

以下是一段显式捕获异常的代码:

try {
  // 可能会抛出异常的代码段
  int a = 10 / 0; // 除以0会抛出ArithmeticException异常
} catch (ArithmeticException e) {
  // 捕获ArithmeticException异常,并输出异常信息
  System.out.println("捕获到ArithmeticException异常:" + e.getMessage());
} finally {
  // finally代码段始终会被执行,无论是否抛出异常
  System.out.println("finally代码段被执行");
}

在上述代码中,使用了 try-catch-finally 语句块来捕获可能会抛出ArithmeticException异常的代码段。如果代码段抛出了ArithmeticException异常,catch语句块会被执行,输出异常信息;如果代码段没有抛出异常,catch语句块不会被执行。无论如何,finally语句块都会被执行,这里简单输出了一条信息。


2️⃣ CopyOnWriteArrayList的底层原理?

CopyOnWriteArrayList(以下简称COWArrayList)是Java集合框架中线程安全的List实现,在多线程并发访问时非常高效。它的底层原理是基于 “写时复制”(Copy-On-Write) 技术实现的。

COWArrayList在初始化时与ArrayList类似,内部使用一个数组来存储元素。不同的是,COWArrayList内部维护了一个 volatile修饰的数组副本,称为“快照数组”。在对COWArrayList进行修改操作(如add、remove等)时,COWArrayList不会直接对快照数组进行修改,而是先将快照数组复制一份,然后在新数组上进行修改操作。修改完成后,再将新数组设置为COWArrayList内部的数组

这种写时复制的策略使得COWArrayList在读操作时不需要加锁,因为读操作只涉及到快照数组的读取,不会对COWArrayList的内部数据造成影响。在写操作时,由于涉及到数组的复制和替换,所以写操作会比较耗时,但是由于读操作不需要加锁,所以读操作的效率非常高,适用于读多写少的场景

需要注意的是,由于COWArrayList的修改操作并不是直接作用于快照数组,而是先进行复制再修改,所以在并发修改时,读取到的数据可能比较旧,存在一定的数据不一致性。但是由于COWArrayList保证了最终一致性,所以不会出现数据丢失或数据错误的情况。


3️⃣ 在项目中,如何排查JVM问题?

在项目中排查JVM问题,大致可以遵循以下步骤:

  • 确认问题现象:通过观察日志、监控数据、异常堆栈等方式,确认JVM问题的具体表现,如OOM、死锁、线程假死等;

  • 收集信息:收集与JVM问题有关的信息,如JVM启动参数、GC日志、线程转储快照、堆转储快照等。可以使用JVM自带的工具,如jstat、jmap、jstack等工具进行收集;

  • 分析信息:根据收集到的信息,分析JVM问题的原因。如分析GC日志,确定GC是否频繁、是否存在内存泄漏等问题;

  • 解决问题:根据分析结果,进行问题修复。如调整JVM参数、优化代码、修复内存泄漏等;

  • 验证结果:验证问题是否解决,观察JVM的表现是否符合预期。

需要注意的是,JVM问题排查是一项综合性的工作,需要熟练掌握JVM相关的知识和工具,同时也需要有一定的排查经验。在排查JVM问题时,应该有耐心和细心,排查过程中应该做好记录和归档,方便后续分析和总结。

🔍 关于JVM的问题排查工具、调优工具和参数,可以参考我的另外一篇博文:
【Java面试题】第八期:Java进阶篇,每周10道,根本停不下来~


4️⃣ 一个对象从创建到被GC回收,要经历怎样的过程?

一个对象从创建到被垃圾回收需要经历以下过程:

  • 创建对象:在Java程序中使用new关键字创建一个对象时,会在堆内存中为该对象分配一块内存空间,并将该对象的成员变量初始化;
  • 引用对象:在程序中使用一个对象时,需要将对象的地址保存在一个引用变量中,以便后续操作;
  • 使用对象:程序可以使用对象的方法和属性,修改对象的状态;
  • 变量失去引用:当一个对象不再被任何引用变量引用时,它就变成了垃圾对象;
  • 垃圾回收:JVM会自动检测垃圾对象,并释放其占用的内存空间;
  • 对象销毁:当对象被垃圾回收后,它的内存空间将被释放,对象被销毁。

🔍 关于类加载过程、类加载机制、对象创建过程,可以参考我的另外一篇博文:
【Java面试题】第七期:Java进阶篇,每周10道,根本停不下来~

🔍 关于垃圾回收判断算法、垃圾回收算法、各类垃圾回收器、分代垃圾回收过程,可以参考我的另外一篇博文:
【Java面试题】第八期:Java进阶篇,每周10道,根本停不下来~


5️⃣ 对线程安全的理解?

线程安全是指在多线程并发访问时,程序仍然能够正确地执行,不会产生不可预期的结果

具体来说,就是在多个线程对共享资源进行访问时,保证对资源的操作是正确的,不会出现竞态条件(race condition)、死锁(deadlock)等问题。线程安全是保证程序可靠性的基本要求之一,一些常见的实现线程安全的方式包括使用互斥锁(mutex)、信号量(semaphore)、读写锁(read-write lock)等同步机制来控制访问共享资源的时序。


6️⃣ 线程池为什么是先把任务添加到阻塞队列,而不是先直接创建最大线程数的线程?

线程池采用阻塞队列的方式来存储任务,而不是直接创建最大线程数的线程,主要有以下几个原因:

  • 避免线程数量过多,保证系统性能。如果直接创建最大线程数的线程,那么在高并发情况下,很容易出现线程数量过多的情况,这会导致系统的性能下降,甚至会引起系统崩溃;

  • 提高程序的稳定性。由于线程的创建和销毁是需要消耗系统资源的,如果频繁地创建和销毁线程,会导致系统资源的浪费,进而降低程序的稳定性;

  • 线程复用,提高任务处理的效率。如果直接创建最大线程数的线程,那么在处理任务较少的情况下,很多线程会处于空闲状态,这会导致系统资源的浪费。而采用阻塞队列的方式,可以让线程池中的线程在处理完任务后,继续等待新的任务,提高任务处理的效率。


7️⃣ 什么是死锁?如何避免死锁?

在这里插入图片描述
死锁是指多个进程在执行过程中,因竞争资源而造成的一种僵局,若无外力作用,这些进程都将无法继续执行下去。
简单来说,就是两个或多个进程在互相等待对方释放资源,导致都无法继续执行的一种状态。在死锁状态下,系统中的进程无法继续执行,也无法被终止,因此会导致系统崩溃或停止响应。

造成死锁主要有以下几个原因:

  • 互斥条件:进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用;
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放;
  • 不剥夺条件:进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由进程自己释放;
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源的关系。

以上条件同时满足时,便会发生死锁。所以若要避免死锁,只需要打破其中一个条件即可,通常会打破第四个条件,通过破坏循环等待条件:对所有资源进行编号,进程按照编号递增的顺序请求资源,释放资源则按照递减的顺序进行,避免循环等待从而避免死锁。

全部的避免死锁的方式有以下几种:

  • 破坏互斥条件:对于不需要互斥的资源,可以不加锁或者采用共享锁的方式,避免多个进程同时占用同一个资源;
  • 破坏请求与保持条件:一次性请求所需的全部资源,避免持有一部分资源而请求另一部分资源的情况发生;
  • 破坏不剥夺条件:当一个进程在等待某些资源时,如果发现已经获得的资源不能满足请求,可以主动释放已经占有的资源,避免因等待而导致死锁;
  • 破坏循环等待条件:对所有资源进行编号,进程按照编号递增的顺序请求资源,释放资源则按照递减的顺序进行,避免循环等待;
  • 使用资源分配图:通过资源分配图可以判断死锁是否存在,如果存在可以通过改变资源的分配方式来避免死锁。

上述方法可以单独使用,也可以结合使用,以达到避免死锁的目的。


8️⃣ synchronized底层实现原理?

synchronized 是 Java 中的一个关键字,用于实现线程的同步。当一个 synchronized 关键字被应用于某个方法或代码块时,它会使得该方法或代码块在同一时间只能被一个线程访问,其他线程需要等待当前线程执行完毕才能访问。

具体来说,当一个线程想要执行一个被 synchronized 关键字修饰的方法或代码块时,它必须先获得相应对象的锁。如果该锁已经被其他线程占用,则当前线程会被阻塞,直到其他线程释放了该锁。当该方法或代码块执行完毕时,当前线程会释放该对象的锁,以便其他线程可以获取该锁并访问该方法或代码块。

通过 synchronized 实现的同步机制可以避免多个线程同时访问共享资源造成的并发问题,例如数据竞争和死锁等问题。但是,需要注意的是,在使用 synchronized 进行同步时,需要注意锁的粒度,不宜锁住过多的代码,以避免锁竞争降低程序的性能。

🍎 首先,在了解其底层原理之前,需要先明白对象在内存中的存储布局
在这里插入图片描述

  • 对象头(Header)
    • 标记字段(Mark Word) :存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等,它是实现轻量级锁和偏向锁的关键;
    • 类型指针(Klass Pointer):对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;
  • 实例数据(Instance Data):记录了对象里面的变量数据;
  • 对齐填充(Padding):作为对齐使用,在64位服务系统中,规定对象内存必须要能被8字节整除,如果不能整除,会通过对齐来补充。

在64位虚拟机的 标记字段(Mark Word)的结构数据详情如下:

在这里插入图片描述

🍑 由上面的图可以了解 synchronized 锁是通过在对象头中的标记字段(Mark Word)存储不同的值,来标记各种不同的锁。这也是1.6版本对synchronized锁的优化(在早期版本的synchronized锁是一种重量级锁,每次获取锁和释放锁都需要进行大量的操作,导致性能较低)。而不同锁之间的锁升级的过程 具体如下:

  • 无锁:没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功;
  • 偏向锁:指在没有竞争的情况下,锁会偏向于第一个获取锁的线程,避免了每次获取锁都需要进行CAS操作的开销;
  • 轻量级锁:指在竞争不激烈的情况下,锁会使用CAS操作来进行快速的加锁和解锁,避免了重量级锁的开销;
  • 重量级锁:指在竞争激烈的情况下,锁会使用操作系统提供的互斥量来进行加锁和解锁,保证了线程安全性,但开销较大。

🍍 最后来看其底层原理,synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层操作系统的Mutex Lock(互斥锁)来实现 的。操作系统实现线程之前的切换需要从用户态转换到核心态,成本高耗时长,因此synchronized效率低(但在JDK1.6和JDK1.7版本中,引入了锁升级、锁消除、锁膨胀来优化性能)。

monitor描述为对象监视器,使用syncrhoized加锁的同步代码块在字节码引擎中执行时,主要就是通过锁对象的monitor的取用(monitorenter)与释放(monitorexit)来实现的。

monitor内部状态流转情况如下图(具体文字描述可参考链接):

在这里插入图片描述


9️⃣ 说一说关键字 volatile的作用?与synchronized的区别?

首先需要了解 多线程的三大特性是原子性、可见性和有序性

  • 原子性:一个操作是不可中断的,要么全部执行成功,要么全部不执行;
  • 有序性:程序执行的顺序按照代码的先后顺序执行;
  • 可见性:当一个线程修改了变量的值,该值会立刻同步到主内存当中,因此,当其他线程读取这个变量的时候,会读取该变量的最新值。

volatile 关键字用于修饰变量,保证了多线程三大特性中的有序性(通过内存屏障实现)、可见性(该特性基于Java语言的的先行性规则)。而volatile并不能保证被修饰变量的操作原子性。

  • 保证可见性:当一个变量被声明为volatile类型时,每个线程在读取该变量时都会从内存中读取最新的值,而不是使用缓存中的旧值,从而保证了可见性;
  • 保证有序性:volatile关键字主要依靠内存屏障来实现。内存屏障是一种CPU指令,可以保证在屏障之前和之后的指令不会被重排序,从而保证了程序执行的顺序性。当一个线程写入volatile变量时,会在写入操作之后插入一个内存屏障指令,从而保证写入操作完成之后,后续的读取操作不会被重排序到写入操作之前执行。

volatile与synchronized的区别如下:

  • 使用范围不同:volatile只能作用于变量。synchronized可以用在方法、同步代码块等地方,使用范围较大;
  • 作用不同:volatile保证可见性和有序性,不能保证原子性。synchronized主要保证操作原子性;
  • 是否造成阻塞不同:volatile不会造成线程阻塞;synchronized可能会造成线程阻塞。

🔟 ThreadLocal的底层原理?

在这里插入图片描述

ThreadLocal是Java中的一个线程本地变量,它可以为每个线程创建一个独立的副本,各个线程之间互不干扰。ThreadLocal的底层实现主要依赖于ThreadLocalMap类

当我们调用ThreadLocal的set()方法时,实际上是通过当前线程获取一个ThreadLocalMap对象,然后将ThreadLocal对象作为key,要设置的值作为value,存储到ThreadLocalMap中。当我们调用ThreadLocal的get()方法时,也是通过当前线程获取ThreadLocalMap对象,然后根据ThreadLocal对象取出对应的值。

由于每个ThreadLocal对象都会对应一个独立的ThreadLocalMap对象,因此不同的线程之间的数据是相互隔离的,互不干扰的。

需要注意的是,由于ThreadLocalMap中使用了弱引用来引用ThreadLocal对象,因此在实际使用过程中,如果我们没有手动调用remove()方法将ThreadLocal对象从ThreadLocalMap中移除,就有可能出现内存泄漏的情况。


图片来源:

[Java并发与多线程](十二)死锁——从产生到消除
Synchronized的底层实现原理(看这篇就够了)
synchronized实现原理及ReentrantLock源码
通过ThreadLocal实现一个上下文管理组件

在这里插入图片描述

  • 16
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小山code

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值