java 面试题汇总整理

java有哪四种引用类型

在Java中,有四种引用类型,用于控制对象的生命周期和垃圾回收行为。这些引用类型包括:

  1. 强引用(Strong Reference)

    • 强引用是最常见的引用类型,它们是默认的引用类型。
    • 当一个对象具有强引用时,垃圾回收器不会回收这个对象,即使内存不足也不会回收。
    • 只有当所有强引用都不再指向某个对象时,垃圾回收器才会回收它。
  2. 软引用(Soft Reference)

    • 软引用用于描述一些还有用但不是必需的对象。
    • 当内存不足时,垃圾回收器会尝试回收被软引用引用的对象,但只有在内存真正不足的情况下才会回收。
    • 软引用通常用于缓存等场景,以便在内存不足时释放缓存。
  3. 弱引用(Weak Reference)

    • 弱引用用于描述一些非必需的对象,但在下一次垃圾回收时就会被回收。
    • 弱引用通过java.lang.ref.WeakReference类来实现。
    • 一旦没有强引用引用一个对象,即使还有弱引用引用它,垃圾回收器也会回收该对象。
  4. 虚引用(Phantom Reference)

    • 虚引用是最弱的引用类型,用于监控对象被垃圾回收的情况。
    • 虚引用通过java.lang.ref.PhantomReference类来实现。
    • 虚引用不会阻止对象被回收,但允许在对象被回收之前执行一些清理操作。

引用类型的选择取决于对象的生命周期和应用程序的需求。强引用用于维持对象的持久性,而软引用和弱引用用于实现缓存和内存敏感的对象,虚引用则用于执行对象的清理操作。理解这些引用类型可以帮助开发人员更好地管理内存和资源。

ThreadLocal 使用的哪种类型的引用

ThreadLocal 使用弱引用(Weak Reference)来管理对象的引用。这意味着当一个对象只被 ThreadLocal 引用时,如果没有其他强引用指向这个对象,垃圾回收器有可能在适当的时候回收该对象。弱引用的使用允许 ThreadLocal 在不再需要对象时自动释放对对象的引用,从而避免潜在的内存泄漏问题。

ThreadLocal 主要用于在每个线程中存储独立的数据,每个线程拥有自己的数据副本。当线程结束时,线程本地存储中的数据会被自动回收,因此使用弱引用确保了在没有外部强引用的情况下,线程局部存储中的数据可以被垃圾回收。

需要注意的是,尽管 ThreadLocal 使用弱引用来管理对象引用,但在某些情况下,如果没有正确清理 ThreadLocal 变量,仍然可能导致内存泄漏问题。因此,使用 ThreadLocal 时需要小心,确保在不再需要线程局部存储中的数据时进行适当的清理或移除。

threadLocal是什么,简单的介绍一下

ThreadLocal 是 Java 中的一个类,用于在多线程环境中创建线程局部变量。线程局部变量是一种特殊的变量,每个线程都拥有自己的副本,互不干扰。简单来说,ThreadLocal 允许你为每个线程存储和维护其自己的数据,这些数据对其他线程是不可见的。

以下是 ThreadLocal 的一些关键特点和用途:

  1. 线程隔离ThreadLocal 变量允许每个线程在其内部存储自己的数据,不会与其他线程共享。这可以用于避免线程之间的数据冲突和竞态条件。

  2. 数据共享ThreadLocal 可以用于在多线程应用程序中共享数据,而不需要显式地传递数据参数或使用全局变量。

  3. 线程上下文ThreadLocal 常用于存储与线程相关的上下文信息,如用户身份认证信息、数据库连接、会话信息等。

  4. 线程安全性:由于每个线程都拥有自己的变量副本,ThreadLocal 变量本身在多线程环境中通常是线程安全的。

  5. 内存泄漏风险:需要小心管理 ThreadLocal 变量,以防止可能的内存泄漏。如果不及时清理 ThreadLocal 变量,可能会导致不再需要的数据一直存在于内存中。

使用 ThreadLocal 的典型场景包括:

  • 在Web应用程序中存储用户会话信息,以便每个用户的请求都能访问其自己的会话数据。
  • 在数据库连接池中管理数据库连接,确保每个线程都有自己的数据库连接,而不需要共享。
  • 在日志记录中存储线程特定的上下文信息,如请求ID或用户身份信息。

需要注意的是,虽然 ThreadLocal 可以解决多线程环境中的一些问题,但过度使用它也可能导致代码变得难以理解和维护,因此在使用时需要谨慎考虑,确保合理使用。

子类获取父类的泛型类型

在 Java 中,子类无法直接获取父类的泛型类型参数信息。泛型信息在编译时会被类型擦除,因此在运行时无法直接访问泛型类型参数。但是,可以通过反射来获取泛型类型参数的信息,尽管这需要更复杂的代码。

以下是一个示例,演示了如何使用反射获取父类的泛型类型参数的类型:

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

class Parent<T> {
    public Parent() {
        // 使用反射获取泛型类型参数的类型
        Type genericSuperclass = getClass().getGenericSuperclass();
        if (genericSuperclass instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;
            Type[] typeArguments = parameterizedType.getActualTypeArguments();
            if (typeArguments.length > 0) {
                Type typeArgument = typeArguments[0];
                System.out.println("Generic Type: " + typeArgument.getTypeName());
            }
        }
    }
}

class Child extends Parent<String> {
    // 子类不需要显式传递泛型类型参数的类型
}

public class Main {
    public static void main(String[] args) {
        Child child = new Child();
    }
}

在上面的示例中,父类 Parent 在其构造函数中使用反射获取了泛型类型参数的类型。子类 Child 继承了父类,不需要显式传递泛型类型参数的类型。

需要注意的是,这种方式使用了 Java 的反射机制,可能会导致代码复杂性增加,并且反射操作会带来一定的性能开销。因此,建议在真正需要获取泛型类型信息时再使用这种方法,同时要小心处理异常情况。

线程的sleep和wait的区别?

sleepwait 都是用于线程控制的方法,但它们在用途和行为上有明显的区别:

  1. sleep

    • sleepThread类的静态方法,用于使当前线程进入休眠状态(暂停执行),让其他线程有机会运行。
    • sleep 方法接受一个参数,即线程休眠的时间,以毫秒为单位。线程在休眠期间不会释放持有的锁,因此其他线程无法获得锁并执行同步方法或代码块。
    • sleep 方法通常用于实现等待一段时间后再执行某个操作的场景,如定时任务等。
    try {
        Thread.sleep(1000); // 休眠1秒
    } catch (InterruptedException e) {
        // 处理中断异常
    }
    
  2. wait

    • waitObject 类的方法,用于在多线程之间进行协调和通信。调用 wait 方法会使当前线程进入等待状态,同时释放对象的锁。
    • wait 方法通常与 notifynotifyAll 方法一起使用,以实现线程之间的协作。等待的线程会在其他线程调用相同对象的 notifynotifyAll 方法时被唤醒。
    • wait 方法必须在同步块或同步方法中调用,因为它要求当前线程拥有对象的监视器锁(即在同步块或同步方法中获取的锁)。
    synchronized (lockObject) {
        try {
            lockObject.wait(); // 等待其他线程唤醒
        } catch (InterruptedException e) {
            // 处理中断异常
        }
    }
    

总结:

  • sleep 主要用于线程的休眠,等待一段时间后恢复执行,不会释放锁。
  • wait 主要用于线程之间的协作和通信,等待其他线程的通知或条件满足时恢复执行,并且会释放对象的锁。
  • wait 必须在同步块或同步方法中使用,而 sleep 不需要。
  • 使用 sleep 时,线程会在指定的时间后自动唤醒,而使用 wait 时,线程需要等待其他线程显式唤醒。

线程池中确定核心线程数的个数的公式?

确定线程池中核心线程数的个数没有一个固定的公式,因为它依赖于多个因素,包括应用程序的性质、硬件资源、负载预测和性能要求等。然而,可以根据以下一般原则来决定核心线程数:

  1. CPU核心数:通常情况下,将核心线程数设置为可用CPU核心数是一个合理的起点。这可以充分利用多核处理器的性能,使得线程池能够并行执行任务。

  2. 任务性质:任务的性质对核心线程数也有影响。如果任务是CPU密集型的,那么核心线程数可以设置得较低,因为CPU密集型任务不会频繁地阻塞等待外部资源。如果任务是I/O密集型的,可能需要更多的线程来处理等待I/O操作完成的情况。

  3. 响应时间要求:如果应用程序对响应时间有较高的要求,可能需要增加核心线程数,以减少任务等待执行的时间。较多的核心线程可以更快地响应任务请求。

  4. 负载预测:根据负载预测来调整核心线程数也是一个常见的做法。如果可以预测到应用程序的负载将在未来的某个时间段内增加,可以提前增加核心线程数以满足预期的需求。

  5. 性能测试:最终的线程数设置通常需要经过性能测试和监控来确定。通过观察线程池的性能指标,如平均响应时间、任务执行时间、任务队列长度等,可以调整核心线程数以获得最佳性能。

需要注意的是,线程池的性能不仅取决于核心线程数,还与任务队列、最大线程数、拒绝策略等参数有关。因此,在确定核心线程数时,也需要综合考虑这些参数的配置。

最终,确定核心线程数的个数通常是一个基于应用程序特性和性能需求的经验性决策,需要根据实际情况进行调整和优化。在实际应用中,可以使用性能测试和监控工具来帮助确定最佳的核心线程数。

简单介绍一下epoll多路复用

epoll 是一种在Linux操作系统上提供的多路复用机制,用于高效地管理大量的文件描述符(通常是套接字文件描述符),以实现高性能的I/O多路复用。epoll 在Linux中取代了传统的selectpoll系统调用,它的设计目标是更好地处理大规模的并发连接和事件处理。

以下是epoll多路复用的一些关键特点和工作原理:

  1. 事件驱动epoll是事件驱动的,它等待并处理发生在文件描述符上的事件。这些事件可以包括套接字上有新连接到达、套接字可读或可写、套接字发生错误等。

  2. 高性能:与selectpoll相比,epoll在处理大量文件描述符时表现更好,因为它使用了更高效的数据结构和算法。epoll不会随着文件描述符数量的增加而线性扩展,因此适用于高并发的场景。

  3. 三个接口epoll提供了三个主要的系统调用接口,分别是epoll_createepoll_ctlepoll_wait

    • epoll_create:用于创建一个epoll实例,返回一个文件描述符,用于后续的epoll操作。
    • epoll_ctl:用于向epoll实例中添加、修改或删除文件描述符,并指定关注的事件类型。
    • epoll_wait:用于等待事件发生,一旦有事件发生,它会返回一个包含就绪文件描述符的列表。
  4. 边缘触发和水平触发epoll支持两种事件触发方式,即边缘触发(Edge-Triggered,ET)和水平触发(Level-Triggered,LT)。边缘触发仅在文件描述符状态发生变化时通知一次,而水平触发则在文件描述符就绪时通知。

  5. 高度可配置epoll提供了丰富的配置选项,可以根据需要调整不同的参数,以满足具体的应用场景。

epoll多路复用在高性能网络编程中被广泛使用,特别是在服务器端应用程序中,因为它能够有效地管理大量并发连接和高吞吐量的I/O操作。但需要注意,epoll是Linux特有的机制,不同的操作系统使用不同的多路复用机制,如BSD上的kqueue和Windows上的IOCP。因此,在编写跨平台的应用程序时需要考虑不同操作系统的差异。

造成线程阻塞的原因

线程阻塞是指线程在执行过程中被暂停或阻止继续执行的状态。线程阻塞的原因多种多样,以下是一些常见的造成线程阻塞的原因:

  1. 等待I/O操作完成:线程在进行输入/输出操作时,如果涉及到读取文件、网络通信、数据库查询等需要等待外部资源返回的操作,线程会被阻塞,直到I/O操作完成。

  2. 等待锁:当线程尝试访问一个已经被其他线程持有的锁时,它会被阻塞,直到锁被释放。这种情况常见于多线程并发编程中,用于确保多个线程之间的同步。

  3. 等待条件满足:线程可能被要求等待某个条件达到,然后才能继续执行。这通常与线程间的协作和通信有关,例如使用waitnotify方法。

  4. 休眠(Sleep):线程可以通过调用Thread.sleep方法来主动休眠一段时间,使线程进入阻塞状态,等待指定的时间后才会继续执行。

  5. 等待系统资源:有些操作需要等待系统资源的可用性,如申请内存或文件句柄。线程可能会因为等待系统资源而被阻塞。

  6. 死锁:死锁是多线程编程中的一种特殊情况,其中多个线程相互等待对方释放锁,导致所有线程都无法继续执行。

  7. 线程调度:线程调度器可能会导致线程在多个就绪线程之间切换,这也可能导致线程被阻塞。

  8. 异常情况:某些异常情况,如未捕获的异常,可能会导致线程被终止或异常退出,从而造成线程阻塞。

  9. 等待用户输入:在图形用户界面(GUI)或命令行应用程序中,线程可能会等待用户输入或交互操作,直到用户完成输入才能继续执行。

线程阻塞是多线程编程中需要特别注意的问题之一,因为不合理的阻塞可能导致性能下降或应用程序的不稳定性。因此,开发人员需要仔细管理线程的状态,确保线程在需要阻塞时合理地等待,以避免潜在的问题。

什么是读写锁

读写锁(Read-Write Lock)是一种用于多线程编程的同步机制,它允许多个线程同时读取共享数据,但在写操作时需要互斥,即写操作是排他的。读写锁的目的是提高多线程环境下对共享数据的访问效率,特别是在读操作比写操作频繁的情况下。

读写锁具有两种锁状态:读锁(共享锁)和写锁(排他锁),并提供以下特性:

  1. 多个线程可以同时持有读锁:多个线程可以同时获取并持有读锁,这意味着它们可以并发地读取共享数据而不会相互干扰。这对于读操作频繁但写操作较少的情况非常有用。

  2. 写锁是排他的:当一个线程持有写锁时,其他线程不能同时持有读锁或写锁。这确保了写操作是互斥的,防止写线程和读线程之间的竞争条件。

  3. 写锁优先于读锁:如果一个线程持有写锁,并且有其他线程在等待获取写锁,那么写锁的优先级更高,这意味着写线程将获得写锁,并且其他线程将等待写线程释放锁后才能继续执行。

读写锁通常用于以下情况:

  • 当读操作频繁而写操作较少时,可以使用读写锁来提高性能,因为多个线程可以同时读取数据而无需互斥。
  • 当需要对共享数据进行读取和更新操作时,可以使用读写锁来确保写操作的互斥性,以防止写线程与读线程之间的竞争条件。

在Java中,java.util.concurrent包提供了ReentrantReadWriteLock类,它是一个可重入的读写锁的实现,可以在多线程编程中使用。使用读写锁需要谨慎,根据具体的应用场景来合理选择何时获取读锁和写锁,以充分利用多线程的并发性和提高性能。

什么是可重入锁

可重入锁(Reentrant Lock)是一种支持同一个线程多次获取锁的锁机制。也就是说,当线程已经获得了某个锁后,如果再次尝试获取同一个锁,它会成功而不会被阻塞。可重入锁的主要特点是同一个线程可以多次获取锁,而不会发生死锁。

可重入锁通常用于多线程编程中,提供了更灵活的锁定机制,允许线程在多个嵌套的方法调用中获取和释放锁,而不必担心死锁或其他同步问题。这对于复杂的线程逻辑和代码组织非常有用。

在Java中,java.util.concurrent包提供了ReentrantLock类,它是可重入锁的一种实现。以下是可重入锁的一些关键特点:

  1. 可重入性:同一个线程可以多次获得同一个可重入锁,而不会被阻塞。线程每次获取锁后,都会维护一个锁计数器,当计数器为零时,锁被释放。

  2. 公平性:可重入锁可以配置为公平锁或非公平锁。在公平锁模式下,锁将按照请求的顺序分配给等待线程。在非公平锁模式下,锁可能会分配给等待时间较短的线程,以提高性能。

  3. 条件变量:可重入锁通常提供了条件变量(Condition)的支持,允许线程等待特定的条件得到满足后再继续执行。这在一些线程协作的场景中非常有用。

  4. 可中断性:可重入锁允许等待锁的线程响应中断,当线程处于等待锁状态时,可以通过中断线程来取消等待。

  5. 超时等待:可重入锁通常支持超时等待,允许线程在一定时间内等待锁,如果超过指定的时间仍未获取锁,线程可以继续执行其他操作。

可重入锁是一种强大的同步机制,但需要谨慎使用,以避免潜在的死锁和性能问题。在多线程编程中,它可以用来代替传统的synchronized关键字来实现更灵活的同步控制。

java的stream流

Java 8引入了Stream流,它是一个用于处理集合数据(包括数组、集合等)的新的抽象概念。Stream提供了一种声明性的、函数式的方式来处理数据,可以大大简化集合操作,使代码更具可读性和可维护性。

以下是Stream流的一些关键概念和用法:

  1. 创建流:可以通过多种方式创建Stream流,包括从集合、数组、文件等数据源创建。例如:

    List<String> list = Arrays.asList("apple", "banana", "cherry");
    Stream<String> stream = list.stream();
    
  2. 中间操作:中间操作是对流进行处理的一系列操作,这些操作不会立即执行,而是返回一个新的流。中间操作包括filtermapdistinctsortedlimit等,用于筛选、转换、排序和截断数据。

    Stream<String> filteredStream = list.stream()
        .filter(s -> s.startsWith("a"))
        .map(String::toUpperCase);
    
  3. 终端操作:终端操作是对流进行最终处理的操作,它们会触发流的处理并返回结果。终端操作包括forEachcollectreducecountminmax等,用于遍历、收集、统计、计算等操作。

    filteredStream.forEach(System.out::println);
    long count = list.stream().count();
    
  4. 惰性求值Stream流是惰性求值的,这意味着在执行终端操作之前,中间操作不会立即执行。这样可以减少不必要的计算和内存开销。

  5. 流的并行处理Stream可以轻松实现并行处理,通过调用parallel方法将流转换为并行流,可以充分利用多核处理器的性能。

    Stream<String> parallelStream = list.parallelStream();
    
  6. 可复用性Stream流是可复用的,可以对同一个流进行多次操作,每次操作都会返回一个新的流,不会影响原始流。

    Stream<String> stream1 = list.stream().filter(s -> s.length() > 5);
    Stream<String> stream2 = stream1.map(String::toUpperCase);
    
  7. 自动关闭资源Stream可以自动关闭底层的资源,例如文件流。当使用try-with-resources块时,Stream会在块结束时自动关闭。

    try (Stream<String> lines = Files.lines(Paths.get("file.txt"))) {
        lines.forEach(System.out::println);
    } catch (IOException e) {
        // 处理异常
    }
    

Stream流提供了一种更现代、更函数式的方式来处理集合数据,可以使代码更加清晰、简洁,并且支持并行处理,有助于提高性能。在使用Stream时,需要了解各种中间操作和终端操作的用法,以满足不同的数据处理需求。

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