2023年Java核心技术面试第十篇(篇篇万字精讲)

目录

十九 . 一个线程两次调用start()方法会出现什么情况?线程的生命周期和状态转移。

19.1 典型回答

19.1.1 线程生命周期:

19.1.2 计时等待详细解释:

 19.2 深入扩展考察

19.2.1 线程是什么?

19.2.2 Green Thread详细解释:

二十. Java程序产生死锁的情况以及如何进行定位,修复?

20.1 典型回答

20.1.1 定位死锁

20.1.1 .1 详细解释:


十九 . 一个线程两次调用start()方法会出现什么情况?线程的生命周期和状态转移。

线程是Java并发的基础元素。理解,操纵线程是必备技能。

19.1 典型回答

Java线程是不允许启动2次的,第二次调用必然会抛出illegalThreadStateException,运行时异常,多次调用start()被认为编程错误。

19.1.1 线程生命周期:

线程状态被明确定义在其公告内部枚举类型java.lang.Thread.State中:

分别是:

1. 新建(new):

表示线程被创建出来还没有真正启动的状态,可以认为是一个Java内部状态。

2. 就绪(runnable):

表示该线程已经在JVM中执行,当然由于执行需要计算资源,它可能是正在运行,也可能是还在等待系统分配给它CPU片段,在就绪队列里面排队。

3. 阻塞(blocked)

状态和讲的同步相关,阻塞表示线程在等待Monitor lock 。

如:

线程试图通过synchronized去获取某个锁,但是其他线程已经独占了,就会导致阻塞问题,使得线程处于阻塞状态。

4. 等待(waiting)

表示正在等待其他线程采取某些操作,

场景:类似生产者消费者模式,发现任务条件并没有满足,会让当前消费者线程等待(wait),另外的生产者线程去准备任务数据,然后通过类似notify等动作,通知消费者继续工作。

Thread.join()也会令线程进入等待状态。

5. 计时等待(timed_wait)

进入条件和等待状态类似,但是调用的是存在超时条件方法,比如wait或join等方法的指定超时版本。

6. 终止(terminated):

不管是意外退出还是正常执行结束,线程已经完成使命,终止运行。

当我们进行第二次调用start()方法的时候,线程可能处于终止或者其他(非new)状态,但是无论如何,都是不可以再次启动的。

19.1.2 计时等待详细解释:

当一个线程等待其他线程完成某个任务,并设置了超时时间,可以使用timed_wait方法。

public class TimedWaitExample {
    private boolean isTaskComplete = false;

    public synchronized void waitForTask() {
        try {
            // 设置等待超时时间为5秒
            wait(5000);
            if (!isTaskComplete) {
                System.out.println("任务未能在指定时间内完成,继续执行其他操作");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public synchronized void completeTask() {
        // 模拟任务完成
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        isTaskComplete = true;
        notifyAll();
    }

    public static void main(String[] args) {
        TimedWaitExample example = new TimedWaitExample();

        // 创建等待任务的线程
        Thread waitThread = new Thread(() -> {
            example.waitForTask();
        });

        // 创建完成任务的线程
        Thread completeThread = new Thread(() -> {
            example.completeTask();
        });

        // 启动线程
        waitThread.start();
        completeThread.start();

        try {
            // 等待两个线程执行完成
            waitThread.join();
            completeThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

TimedWaitExample类包含了一个waitForTask方法和一个completeTask方法。在waitForTask方法中,调用了带有超时参数的wait方法,等待任务完成或超过5秒的时间限制。在completeTask方法中,模拟了任务的完成,并通过notifyAll方法唤醒处于等待状态的线程。

main方法中,创建了一个等待任务的线程和一个完成任务的线程,并启动它们。然后,使用join方法等待两个线程执行完成。

如果完成任务的线程在5秒内完成了任务,则等待线程会被唤醒并继续执行。如果任务未能在指定时间内完成,等待线程会输出一条提示信息并继续执行其他操作。

例子展示了如何使用timed_wait方法来实现线程等待超时的功能。

 19.2 深入扩展考察

面试热身题,进行对基本状态的简单的流转进行介绍,对线程进行理解是对我们日常开发和诊断分析有很大的帮助,都是必备的基础。

作为突破口,进行从各个不同的角度考察你对线程的掌握。

19.2.1 线程是什么?

操作系统的角度,我们可以简单的认为,线程是系统调度的最小单元,一个进程可以包含多个线程,作为任务真正运作者,有自己的栈(Stack),寄存器(Register),本地存储(Thread Local)等,但是会和进程内其他线程共享描述符,虚拟地址空间等。

具体实现中,线程分为内核线程,用户线程,Java的线程实现其实是与虚拟机相关的。

对于我们最熟悉的JDk,线程也进行了一个演进过程,基本上在Java 1.2 后,JDK已经抛弃了Green Thread,也就是用户调度的线程,现在的模型是一对一映射到操作系统内核线程。

19.2.2 Green Thread详细解释:

Green Thread模型的设计初衷是为了使Java程序能够在不依赖底层操作系统的情况下运行,并具备跨平台的能力。在Green Thread模型中,Java虚拟机自己实现了对线程的调度和管理,而不是依赖于底层操作系统的线程支持。

然而,由于Green Thread模型没有直接与操作系统内核进行交互,因此导致了一些限制和问题:

  1. 无法充分利用多核处理器的性能:由于Green Thread模型的线程调度和管理是由Java虚拟机自己完成的,它无法直接利用多核处理器的并行计算能力。在这种模型下,即使在具有多个物理核心的处理器上运行Java程序,所有的线程仍然只能通过单个物理核心来执行,不能实现真正的并行计算。

  2. 无法与底层操作系统进行充分的集成:Green Thread模型无法直接与底层操作系统进行交互,因此无法充分利用操作系统提供的各种线程调度算法和特性。

  3. 对资源的占用较大:由于Green Thread模型需要自己实现线程调度和管理,它需要占用较多的内存资源。每个Green Thread都需要分配一定的堆栈空间,而且它们的调度算法和状态维护也需要一定的计算和存储开销。

为了克服这些限制和问题,从Java 1.2版本开始,JDK采用了一对一映射到操作系统内核线程的模型,也称为"native thread"模型。这种模型能够更好地利用多核处理器的性能,并与底层操作系统进行充分的集成,提供更高效和可靠的线程支持。

Green Thread模型在提供跨平台能力方面具有优势,但无法充分利用多核处理器并与底层操作系统进行充分的集成。因此,JDK在Java 1.2之后放弃了Green Thread模型,转而使用一对一映射到操作系统内核线程的模型,以提供更强大和高效的线程支持。

二十. Java程序产生死锁的情况以及如何进行定位,修复?

20.1 典型回答

死锁是一种特定的程序状态,在实体之间,由于循环依赖导致彼此一直处于等待中,没有任何个体可以继续前进。

死锁不仅仅是在线程之间会发生,存在资源独占的进程之间同样也可能出现死锁。

通常来说,我们大多是聚焦在多线程场景中的死锁,指两个或多个线程之间,由于互相持有对方需要的锁,

20.1.1 定位死锁

定位死锁最常见的方式就是利用jstack工具获取线程栈,然后定位互相之间的依赖关系,进而找到死锁,如果是明细的死锁,我们可以通过jstack进行定位。

程序运行发生死锁后,绝大多数情况,无法在线进行解决,只能进行重启,修正程序本身的问题。

20.1.1 .1 详细解释:

当程序发生死锁时,通常情况下无法立即在运行时解决它,因为死锁是由于线程之间的资源竞争导致的相互等待,形成了一个循环依赖的状态。

死锁通常发生在多个线程同时请求一组共享资源,并且每个线程都持有一部分资源并等待其他线程释放它们所需的资源。当发生这种情况时,没有任何一个线程能够继续执行下去,它们被阻塞在等待资源释放的状态中,从而导致程序无法继续正常执行。

解决死锁问题需要针对程序本身进行修正,以消除或避免死锁的产生。以下是一些常见的方法:

  1. 分析和检测死锁:使用工具或技术来分析和检测死锁。例如,通过线程转储分析工具(如jstack)、死锁检测工具(如Java自带的jconsole、VisualVM或第三方工具)来查看线程的状态和死锁信息。

  2. 梳理锁的获取顺序:确保线程获取锁的顺序是一致的,避免出现循环依赖的情况。例如,如果线程A先获取锁1,再获取锁2,那么其他线程也应该按照相同的顺序获取这两个锁。这种预防措施可以减少死锁的发生。

  3. 避免长时间持有锁:尽量减少在持有锁的情况下进行耗时的操作,比如I/O操作或者远程调用。可以使用异步操作或者将操作拆分为更小的单元,以便在持有锁期间减少执行时间。

  4. 使用超时机制:在获取锁时设置一个超时时间,在等待超过一定时间后放弃获取锁并采取相应的处理策略。这可以避免线程无限期地等待锁而导致死锁。

  5. 死锁恢复策略:当检测到死锁时,程序可以采取恢复策略,例如释放已经获得的锁并回退一些操作,然后重新尝试执行。这个策略需要根据具体的业务场景来设计和实现。

尽管有以上的方法来预防和解决死锁问题,但有时候死锁发生的原因非常复杂,可能需要对程序进行彻底的重构才能解决。在这种情况下,重新启动程序是一种常见的解决方法,因为它可以清除死锁并重新开始执行。

总结起来,当程序发生死锁时,无法在线进行解决的大多数情况下需要重启程序,并通过修正程序本身的问题来避免死锁的再次发生。这涉及到分析和检测死锁、优化锁的获取顺序、避免长时间持有锁、使用超时机制以及实施死锁恢复策略等方法。

你可能感兴趣的:(java,面试,开发语言)