后端面试题记录1

159 阅读18分钟

面试题

反射性能及实现方式

在Java中,反射通常比直接的接口调用性能要差。这是因为反射涉及到以下几个额外的操作,这些操作都会增加额外的开销:

  1. 动态解析:反射需要在运行时解析类、接口、字段和方法的信息,这是一个动态查找的过程,涉及到查找元数据和可能的类型检查。

  2. 安全检查:Java虚拟机(JVM)需要检查是否允许访问特定的类和成员,这涉及到安全权限的检查。

  3. 中间层:通过反射调用方法时,JVM会创建一个中间层来处理调用,这意味着额外的间接层。

  4. 缓存问题:直接调用可以被优化和缓存,而反射调用通常不能被缓存,因为它们是动态的,每次调用都可能不同。

 

因此,在性能敏感的应用中,通常建议避免使用反射,尤其是在频繁调用的场景下。如果确实需要使用反射,可以考虑以下优化措施:

  • 缓存结果:由于反射调用不能被JVM优化,可以手动缓存反射调用的结果,比如MethodFieldConstructor对象,以避免重复的解析开销。

  • 减少反射调用次数:尽可能减少反射调用的次数,比如在初始化阶段进行反射操作,然后使用直接调用。

 

  • 使用代理:在某些情况下,可以使用动态代理来减少反射的使用。

 

Java反射的实现方式主要包括:

  1. Java API:Java提供了java.lang.reflect包,其中包含了ClassFieldMethodConstructor等类,用于反射操作。

  2. 字节码操作库:如ASM、Javassist等,这些库可以在运行时动态生成或修改字节码,实现更复杂的反射操作。

  3. CGLIB:CGLIB是一个强大的高性能代码生成库,它通过使用一个小而快的字节码处理框架来扩展Java字节码处理技术,可以用于实现AOP(面向切面编程)。

  4. 动态代理:Java提供了动态代理机制,可以在运行时创建实现了一组接口的新类,这些类实现了接口的所有方法,并且可以自定义方法的行为。

  5. 注解处理器:Java的注解处理器可以在编译时读取注解,并生成相应的代码,这也是一种反射的实现方式,但它是在编译时而不是运行时。

总的来说,反射是一个强大的特性,它提供了动态访问和操作Java对象的能力,但同时也带来了性能上的开销。在实际应用中,需要根据具体需求和性能要求来决定是否使用反射。

单凭CAS没有锁是否能保证并发安全:

CAS操作可以保证单个变量的并发安全,因为它确保了在更新变量值时的原子性。但是,CAS并不能保证以下情况的并发安全:

  1. 复合操作的原子性:如果需要执行多个步骤的操作,比如先读取一个值,然后计算新值,最后写回,即使每个步骤都是原子的,整个复合操作仍然可能不是原子的。在多线程环境中,其他线程可能在中间步骤插入操作,导致数据不一致。

  2. 可见性问题:CAS操作本身不提供操作的内存可见性保证。也就是说,一个线程对变量的修改可能对其他线程不可见,除非使用了volatile关键字或者其他内存屏障。

  3. ABA问题:CAS操作可能会遇到ABA问题。如果一个值原来是A,然后变成了B,再变回A,那么使用CAS操作的线程会错误地认为值没有变化,因为它比较的是A和A。

  4. 循环延迟问题:当多个线程竞争同一个资源时,CAS操作可能会导致线程长时间自旋,消耗CPU资源,这称为循环延迟问题。

好的,以下是我按照你的要求整理的答案,带有标号和排版,方便你复制:


1. CAS是什么了解吗?在哪个集合里有应用?

CAS(Compare and Swap)是一种常用的无锁并发控制原理,常用于实现原子操作。CAS操作通过比较一个变量的当前值与预期值,如果相同则更新为新值,否则不进行更新。CAS保证了操作的原子性,可以避免锁的使用,提升性能。

CAS广泛应用于集合的实现中,特别是在无锁数据结构中。例如,Java中的java.util.concurrent.atomic包提供了多种原子变量类(如AtomicInteger、AtomicLong),这些类内部都使用CAS来实现线程安全。此外,CAS还应用于一些锁优化策略和并发控制算法中,如乐观锁。


2. happens before了解吗?

happens-before 是一个重要的并发原理,指的是在多线程程序中,某个操作必须在另一个操作之前发生,保证程序的执行顺序。它是Java内存模型(JMM)中的核心概念,用于确保线程之间的可见性和顺序性。根据happens-before原则,若线程A的某个操作发生在线程B的操作之前,且A和B之间有相关同步机制(如volatile、synchronized、锁等),那么线程B可以看到线程A的操作结果。


3. synchronized和ReentrantLock区别,什么情况用后者好?

sychronized和ReentrantLock都是Java中用于同步的工具,但它们有几个关键区别:

  • 锁机制:synchronized是隐式锁,通过JVM自动管理,而ReentrantLock是显式锁,由程序员手动控制。
  • 灵活性:ReentrantLock提供了更多的控制能力,如可以尝试锁定(tryLock())、定时锁定(lockInterruptibly())等,而sychronized的功能较为简单。
  • 性能:在高并发情况下,ReentrantLock可能提供更好的性能,特别是当需要中断、超时或尝试获取锁等场景时。

使用ReentrantLock更适合需要精细控制的场景,如需要尝试获取锁、需要公平锁或者在多线程环境下需要中断响应的情况。


4. 光CAS没锁,可以保证并发安全吗?

CAS本身并不能完全保证并发安全。CAS是原子操作,但它是基于乐观锁的,如果多线程同时竞争修改一个变量,CAS会多次尝试,直到操作成功为止。在高竞争场景下,CAS可能会导致“自旋”问题,消耗大量的CPU资源。为了保证更强的并发安全,通常还需要其他机制(如volatile变量、原子类或者悲观锁)来进一步保障线程安全。


5. volatile作用,原理,内存屏障什么意思?和synchronized区别?

  • volatile作用:volatile是Java中的一个关键字,用于保证变量的可见性。它告诉JVM,每次读取该变量时都必须从主内存中读取,而不是从线程的本地缓存中读取,从而保证不同线程间的可见性。
  • 原理:当一个变量声明为volatile时,JVM会插入内存屏障(Memory Barrier)指令,确保对该变量的读写操作不会被优化,并且在多个线程之间保持同步。
  • 内存屏障:内存屏障是硬件或操作系统对内存操作的顺序性控制。它强制要求前面操作的结果对其他线程可见,防止指令重排。
  • 与synchronized区别:synchronized不仅保证了可见性,还保证了原子性和排他性;而volatile只保证可见性,不能保证复合操作的原子性。因此,如果需要保证多个操作的原子性,仍需使用sychronized。

6. JMM了解吗?

JMM(Java Memory Model)是Java内存模型,用于定义Java程序中变量的访问规则,特别是涉及多线程时如何保证线程间共享数据的正确性。JMM主要解决了以下几个问题:

  • 可见性:确保一个线程对共享变量的修改对其他线程可见。
  • 有序性:确保多线程之间对变量的操作按一定顺序执行,避免因指令重排导致错误结果。
  • 原子性:确保对某些共享数据的操作是原子性的,避免并发冲突。

JMM通过内存屏障、锁、volatile等机制保证线程间的可见性和有序性。


7. 怎么使用线程池,几个参数,怎么配置这些参数?cpu密集型和io密集型的配置有啥区别?拒绝策略有哪些?

  • 线程池使用:可以通过Executors类或ThreadPoolExecutor来创建线程池。主要参数包括:
    • 核心线程数(corePoolSize):线程池中保持活动的最小线程数。
    • 最大线程数(maximumPoolSize):线程池中允许的最大线程数。
    • 线程空闲时间(keepAliveTime):空闲线程在终止前等待新任务的最大时间。
    • 队列(workQueue):任务队列,用于存储待执行的任务。
  • 配置区别:
    • 对于CPU密集型任务,线程池的核心线程数通常设置为CPU核心数(例如Runtime.getRuntime().availableProcessors()),因为每个线程都将占用一个CPU资源。
    • 对于IO密集型任务,线程池的核心线程数可以设置为更大的数目,因为线程在等待I/O时并不占用CPU资源。
  • 拒绝策略:
    • AbortPolicy:直接抛出异常。
    • CallerRunsPolicy:由调用者线程来执行任务。
    • DiscardPolicy:丢弃任务。
    • DiscardOldestPolicy:丢弃队列中最旧的任务。

8. 反射的性能比起正常好还是差?有啥实现方式?

反射的性能通常较差,因为反射需要在运行时进行类的解析、方法调用等操作,相比直接调用,它需要更多的时间和资源。反射是通过java.lang.reflect包提供的,包括Method、Field、Constructor等类用于访问类的信息和方法。

反射在性能上比直接调用差,但它的优势在于能够在运行时动态加载类、调用方法等,适用于需要灵活性和可扩展性的场景。


9. 数据库,表锁和行锁什么情况会触发?

  • 表锁:表锁会在访问整个表时触发,通常在对整个表进行操作(如DROP、ALTER、TRUNCATE)时使用。表锁会阻塞对该表的其他所有操作。
  • 行锁:行锁在操作表中的单行数据时触发,通常在执行SELECT FOR UPDATE、UPDATE、DELETE时应用。行锁允许对同一张表的不同数据行进行并发操作。

10. MySQL的聚簇索引?间隙锁是什么?

  • 聚簇索引:聚簇索引是数据库表中数据存储的一种方式,数据按索引顺序存储在表中。每个表只能有一个聚簇索引,因为数据本身的物理顺序与聚簇索引顺序一致。聚簇索引的性能在范围查询时表现更好。
  • 间隙锁:间隙锁是InnoDB存储引擎中的一种锁机制,用于锁住索引中的“间隙”部分(即索引项之间的空白区域),避免其他事务插入到锁定区间中。这种锁可以避免幻读(phantom reads)问题。

11. Java 8流介绍下,有啥优点?

Java 8引入了流(Streams)API,主要用于对集合进行函数式操作。流提供了更加简洁和声明式的方式来处理数据,优点包括:

  • 更简洁:流式操作可以通过链式调用来完成复杂的操作,减少代码量。

  • 更清晰:使用流API时,可以更清楚地表达数据处理的意图。

  • 并行化:流支持并行操作,可以通过parallelStream()实现多核处理,从而提高性能。

11. Java 8流(Stream)介绍与优点

Java 8引入的Stream API 是一个功能强大的抽象,它允许你以声明式的方式处理集合(如 List, Set, Map)中的元素。通过流,你可以利用函数式编程的特性,以更简洁、灵活的方式对数据进行操作。Stream的操作通常包括中间操作和终止操作,并且能够支持并行处理。

1. 什么是Java 8流?

Stream是一个顺序元素的集合,它支持函数式操作,允许我们以声明式的方式来处理数据。一个流是一个可以读取的数据序列,它并不存储数据,而是以惰性计算的方式对数据源进行操作。流的元素通常通过数据结构(如集合、数组等)或I/O通道(如文件流)来提供。

流的操作大致分为两类:

  • 中间操作(Intermediate Operations):返回一个新的流,不会立即执行操作,只有在执行终止操作时才会触发计算。这些操作可以是链式的,例如 map(), filter(), sorted() 等。
  • 终止操作(Terminal Operations):会触发流的计算并产生结果或副作用,例如 collect(), forEach(), reduce() 等。

2. Java 8流的优点

  1. 简化代码: 使用Stream API,我们能够以更加简洁的方式处理集合,避免了冗长的循环和条件判断,代码更加清晰、易懂。例如,传统的for循环需要手动处理条件、添加元素等,而Stream则通过内置的中间操作(如 filter() 和 map())来实现数据转换。

    示例:

    java

    // 传统的做法 List list = Arrays.asList("apple", "banana", "orange", "grape"); List result = new ArrayList<>(); for (String s : list) { if (s.startsWith("a")) { result.add(s.toUpperCase()); } } System.out.println(result); // 使用Stream API List result = list.stream() .filter(s -> s.startsWith("a")) .map(String::toUpperCase) .collect(Collectors.toList()); System.out.println(result);

    通过Stream API,我们可以将复杂的循环逻辑转化为流式操作,使代码更加清晰、简洁。

  2. 支持函数式编程: Java 8的Stream API采用了函数式编程风格,支持lambda表达式和方法引用。这使得代码更加简洁,并且能够实现更高阶的操作,如高阶函数、组合函数等。

    示例:在Stream操作中,我们可以直接使用lambda表达式进行数据处理:

    java

    List numbers = Arrays.asList(1, 2, 3, 4, 5); int sum = numbers.stream() .filter(n -> n > 2) .mapToInt(Integer::intValue) .sum(); System.out.println(sum); // 输出 12

  3. 支持并行处理: 使用Stream API,我们可以轻松地通过调用 parallelStream() 来并行处理数据,而不需要显式地使用线程和同步机制。这使得在多核机器上能够自动地进行数据分割和处理,从而提高性能。

    示例:

    java

    List list = Arrays.asList(1, 2, 3, 4, 5); int sum = list.parallelStream() .mapToInt(Integer::intValue) .sum(); System.out.println(sum);

    在这个例子中,parallelStream() 启用了并行流,自动使用多个线程来处理数据。

  4. 惰性求值(Lazy Evaluation): Stream API中的许多操作(例如filter()、map())是惰性求值的,即只有在终止操作(如 collect()、forEach())被调用时,流的操作才会真正执行。这有助于提高性能,避免不必要的计算。

    例如,在以下代码中,filter() 和 map() 的操作不会在调用 collect() 之前执行:

    java

    List list = Arrays.asList("apple", "banana", "orange", "grape"); List result = list.stream() .filter(s -> { System.out.println("filter: " + s); return s.startsWith("a"); }) .map(s -> { System.out.println("map: " + s); return s.toUpperCase(); }) .collect(Collectors.toList()); // 输出将显示filter和map操作的顺序

    由于惰性求值,只有在执行 collect() 时才会触发过滤和转换操作。

  5. 代码可读性和可维护性: 由于函数式编程风格的特性,Stream API使得数据处理流程更具可读性。例如,filter()、map() 和 reduce() 等方法链式调用,不仅能够简化代码,还能使得数据的处理逻辑更加清晰。

  6. 跨平台的一致性: 由于Java的Stream API是基于集合的抽象,开发者可以用一致的方式处理各种数据源,包括集合、数组、文件、网络流等。这种一致性降低了学习成本,并能够更加灵活地扩展。

3. 适用场景

  • 数据集合的处理:对于大部分的集合处理任务,Stream API提供了简洁高效的解决方案,尤其是在进行过滤、转换、排序、聚合等操作时。
  • 并行计算:对于需要高性能的计算任务(如大数据量的处理),通过 parallelStream() 可以轻松地实现并行处理。
  • IO操作:对于文件、数据库等I/O密集型任务,Stream API提供了便捷的数据读取和写入接口。

总结

Java 8的Stream API极大地提升了集合操作的简洁性和灵活性,尤其适合函数式编程的场景。通过流的中间操作和终止操作,我们能够以声明式的方式进行数据处理,并且通过并行流等功能提高处理效率。它让开发者能够更容易地编写可维护、高效的代码,并且让数据处理变得更加自然。

12. 流式并发有了解么?

流式并发通常指的是使用Java 8引入的Stream API来进行并行流操作(parallelStream()),在多核处理器上以并行的方式处理数据。与传统的单线程处理相比,流式并发通过自动将任务分配到多个线程,充分利用多核CPU的资源,从而加速数据处理。并行流的使用非常简单,只需要将流转换为并行流即可:

java

List numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); int sum = numbers.parallelStream().mapToInt(Integer::intValue).sum();

在上述代码中,parallelStream()使得数据处理能够并行执行,自动将任务分配到多个CPU核心。

然而,流式并发并不是在所有场景下都能提高性能,尤其是在任务较小或计算量较低的情况下,开启并行流可能会导致线程管理的额外开销。因此,流式并发适合数据量大、计算密集型的操作,而对于IO密集型操作(如文件读取、数据库操作等)并行流可能不会带来明显的性能提升,甚至可能增加系统的复杂度。


13. 异步编程Future有了解吗?

Future是Java中用于实现异步编程的接口,允许你在后台线程中执行任务,并在将来某个时间点获取任务的结果。它与传统的同步方法调用不同,因为它不会阻塞调用线程,而是允许调用者继续执行其他操作,直到需要结果时再阻塞等待。

Future接口有几个常用方法:

  • get():阻塞直到任务完成,并返回计算结果。如果任务异常终止,get()方法会抛出异常。
  • cancel():尝试取消任务的执行,如果任务已经开始执行,取消可能失败。
  • isDone():检查任务是否完成。
  • isCancelled():检查任务是否被取消。

java

ExecutorService executor = Executors.newCachedThreadPool(); Callable task = () -> { Thread.sleep(1000); // Simulate long-running task return 123; }; Future future = executor.submit(task); // Do something else... // Get the result of the task (blocks if the task is not yet completed) Integer result = future.get(); System.out.println(result);

在上述代码中,任务通过submit()方法提交给线程池,返回一个Future对象,允许调用者在需要时获取任务的结果。


14. Kafka和RabbitMQ区别,什么场景用各自?消息如何保证先进先出的?

  • Kafka 和 RabbitMQ 都是流行的消息中间件,但它们的设计理念和使用场景有所不同:
    • Kafka:
      • 分布式流平台:Kafka是一个分布式流平台,专为高吞吐量、大规模数据流的处理设计。它采用了发布/订阅模式,通过分区机制进行数据的负载均衡。
      • 适用场景:Kafka适用于实时数据流、大数据平台、日志收集、事件驱动的架构等。它非常适合用于处理大量消息、日志数据的传输和存储,具有强大的横向扩展能力。
      • 消息顺序保证:Kafka通过**分区(partition)**来保证消息顺序。每个分区内的消息是按顺序存储的,因此同一个分区内的消费者会按顺序消费消息。不同分区之间的消息顺序无法保证。
    • RabbitMQ:
      • 消息队列系统:RabbitMQ是一个基于AMQP协议的消息队列系统,提供消息的可靠传输、确认机制、以及复杂的路由规则(如交换机、队列等)。
      • 适用场景:RabbitMQ适用于需要消息确认、事务支持和复杂路由的场景,如金融系统中的消息传递、任务队列等。
      • 消息顺序保证:RabbitMQ通过队列来保证消息顺序。默认情况下,消息按照它们进入队列的顺序进行消费,确保了先进先出(FIFO)顺序。
  • 消息先进先出(FIFO)保证:
    • Kafka:Kafka保证在同一分区内的消息顺序是按照生产者发布的顺序进行消费的。因此,如果多个消费者从同一个分区中消费消息,它们将按照顺序处理消息。
    • RabbitMQ:RabbitMQ保证同一个队列中的消息按照发送顺序进行消费,确保FIFO顺序。在RabbitMQ中,消费者从队列中取出消息时,消息是按照队列中的顺序逐个处理的。