互联网大厂Java面试:核心知识大考验
面试官:请简要介绍一下Java核心知识在实际项目中的重要性。
王铁牛:Java核心知识就像是项目的基石呀,比如面向对象编程那一套,封装、继承、多态,能让代码结构清晰,可维护性强。就像我之前做的一个电商项目,商品类就用封装把属性和方法包起来,不同类型商品继承商品类,根据各自特点实现多态,很方便管理和扩展。
面试官:不错,那说说JUC在高并发场景下的应用。
王铁牛:JUC提供了好多并发工具类呢,像CountDownLatch可以用来控制线程等待,等所有条件都满足了再一起执行。还有CyclicBarrier,能让一组线程互相等待,直到都到达某个点再继续执行。在我们项目的抢购模块,就用CountDownLatch控制库存检查线程都完成后再统一处理下单逻辑。
面试官:嗯,回答得可以。接下来问几个关于JVM的问题,类加载机制分哪几个阶段?
王铁牛:类加载机制分加载、验证、准备、解析、初始化这几个阶段。加载就是把类的字节码文件加载到内存;验证是检查字节码文件的正确性;准备阶段为类的静态变量分配内存并设置初始值;解析就是把符号引用替换为直接引用;初始化就是执行类的静态代码块和为静态变量赋值。
第一轮结束。
面试官:谈谈多线程中线程安全问题以及如何解决。
王铁牛:线程安全问题就是多个线程同时访问共享资源可能导致数据不一致。解决办法可以用synchronized关键字,给共享资源加锁。或者用Lock接口,像ReentrantLock,它比synchronized更灵活,可以实现公平锁等。在我们项目的用户登录模块,就用synchronized锁住用户信息,防止并发登录出问题。
面试官:那线程池有哪些参数,分别有什么作用?
王铁牛:线程池有corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler这些参数。corePoolSize是核心线程数,当提交任务数小于它时,就创建核心线程执行任务;maximumPoolSize是最大线程数,当任务数超过核心线程数且workQueue满了,就创建非核心线程执行任务;keepAliveTime是非核心线程的存活时间,当非核心线程空闲时间超过这个值就会被销毁;unit是keepAliveTime的时间单位;workQueue是任务队列,用来存放提交的任务;threadFactory是线程工厂,用来创建线程;handler是拒绝策略,当线程池满了且任务队列也满了,就用这个策略处理新提交的任务。
面试官:如何合理配置线程池参数?
王铁牛:这个得根据具体业务来,要是任务执行时间短,核心线程数可以小一点,任务队列可以大一点;要是任务执行时间长,核心线程数就得大一点。像我们项目的日志处理模块,任务执行快,核心线程数设为5,队列设为100。
第二轮结束。
面试官:讲讲HashMap的底层实现原理。
王铁牛:HashMap底层是数组加链表再加红黑树。当往HashMap里put元素时,先计算key的哈希值,然后通过哈希值找到对应的数组下标。如果下标为空,就直接插入新节点;如果不为空,就遍历链表或红黑树,看是否有相同key的节点,有就更新值,没有就插入新节点。当链表长度超过8且数组长度大于64时,链表会转换为红黑树。
面试官:ArrayList是线程安全的吗?为什么?
王铁牛:ArrayList不是线程安全的。因为它的方法没有用synchronized修饰,在多线程环境下同时读写可能会出问题,比如一个线程在添加元素时,另一个线程可能在读取元素,导致数据不一致。
面试官:如何将ArrayList转换为线程安全的?
王铁牛:可以用Collections.synchronizedList方法把ArrayList包装成线程安全的。或者用CopyOnWriteArrayList,它是写时复制的机制,写操作时会复制一份新的数组,读操作在原数组上进行,保证线程安全。
第三轮结束。
面试官:请简述Spring的核心特性。
王铁牛:Spring的核心特性有依赖注入,通过IoC容器管理对象的依赖关系。还有面向切面编程,能把一些通用功能像日志、事务等和业务逻辑分离。另外它有强大的事务管理功能,能方便地控制事务的传播、隔离级别等。
面试官:Spring Boot与传统Spring相比有哪些优势?
王铁牛:Spring Boot配置简单,它有自动配置功能,能根据引入的依赖自动配置好多东西。开发速度快,内置了Tomcat等服务器,直接就能运行项目。而且它和各种框架集成方便,能快速搭建微服务项目。
面试官:说说MyBatis的工作原理。
王铁牛:MyBatis先读取配置文件,解析SQL语句。然后通过SQL Mapper接口和映射文件,把Java对象和SQL语句关联起来。当调用接口方法时,MyBatis会根据传入的参数动态生成SQL语句,再通过JDBC执行SQL,最后把结果封装成Java对象返回。
面试结束,面试官表示会让王铁牛回家等通知。
答案:
-
Java核心知识在实际项目中的重要性:
- 面向对象编程:
- 封装:将数据和操作数据的方法封装在一起,提高数据的安全性和代码的可维护性。例如在电商项目中,商品类封装商品的属性(如名称、价格等)和方法(如计算总价等),外部只能通过特定接口访问和修改内部数据,防止数据被随意篡改。
- 继承:子类可以继承父类的属性和方法,实现代码复用。不同类型的商品(如电子产品、服装等)可以继承商品类,共享商品的基本属性和方法,同时各自再添加特有的属性和方法。
- 多态:同一个方法可以根据对象的不同类型表现出不同的行为。在商品展示模块,不同类型商品的展示方法可以通过多态实现,根据商品类型调用不同的展示逻辑,增强了代码的灵活性和扩展性。
- 其他核心知识:如异常处理可以增强程序的健壮性,在文件读取、网络请求等操作中捕获和处理异常,避免程序因异常而崩溃。
- 面向对象编程:
-
JUC在高并发场景下的应用:
- CountDownLatch:
- 用于控制线程等待,直到某个条件满足。例如在电商抢购模块,有多个库存检查线程,当所有库存检查线程都完成检查后,再统一处理下单逻辑。可以创建一个CountDownLatch,初始值为库存检查线程的数量,每个库存检查线程完成后调用countDown()方法,主线程调用await()方法等待,直到CountDownLatch的值变为0,再执行下单操作。
- CyclicBarrier:
- 让一组线程互相等待,直到都到达某个点再继续执行。比如在一个多步骤的业务流程中,有多个线程分别处理不同步骤,当所有线程都完成各自步骤后,再一起进入下一步骤。可以创建一个CyclicBarrier,指定参与等待的线程数量,每个线程在完成自己的步骤后调用await()方法,当所有线程都调用await()方法后,所有线程同时继续执行后续逻辑。
- 其他工具类:如Semaphore可以控制同时访问某个资源的线程数量;Exchanger可以在两个线程之间交换数据等,在不同的高并发场景中发挥作用。
- CountDownLatch:
-
JVM类加载机制的阶段:
- 加载:
- 将类的字节码文件从磁盘加载到内存中,生成一个Class对象。这个过程包括通过类的全限定名找到对应的字节码文件,读取字节码文件的二进制数据,并将其转换为方法区中的运行时数据结构,然后在堆中创建一个Class对象,作为方法区中该类数据的访问入口。
- 验证:
- 检查字节码文件的正确性,确保其符合Java虚拟机规范。验证内容包括文件格式验证、元数据验证、字节码验证和符号引用验证等。例如检查字节码文件的魔数是否正确,类的继承关系是否合法,字节码指令是否合法等,防止恶意字节码或不符合规范的代码进入虚拟机。
- 准备:
- 为类的静态变量分配内存并设置初始值。这些静态变量所使用的内存都将在方法区中进行分配。对于基本数据类型,初始值为0(如int类型初始值为0,boolean类型初始值为false);对于引用类型,初始值为null。例如类中有一个静态int变量count,在准备阶段会为其分配内存并初始化为0。
- 解析:
- 把符号引用替换为直接引用。符号引用是一种间接引用,它以一组符号来描述所引用的目标,如类名、方法名等。直接引用是指向目标的指针、相对偏移量或一个能直接定位到目标的句柄。解析过程会将符号引用解析为具体的内存地址或句柄,以便在后续的执行过程中能够直接访问目标。
- 初始化:
- 执行类的静态代码块和为静态变量赋值。当初始化开始时,Java虚拟机才真正开始执行类中定义的Java程序代码。例如类中有静态代码块static { System.out.println("类初始化"); },在初始化阶段会执行该代码块。如果类中有静态变量的赋值操作,也会在这个阶段完成。
- 加载:
-
多线程中线程安全问题及解决办法:
- 线程安全问题:
- 多个线程同时访问共享资源时,可能会导致数据不一致。比如多个线程同时对一个共享的计数器进行增减操作,可能会出现结果错误的情况。因为在多线程环境下,一个线程读取共享资源的值,另一个线程可能同时修改了该值,导致读取到的数据不准确。
- 解决办法:
- synchronized关键字:
- 可以给共享资源加锁,保证同一时间只有一个线程能访问该资源。例如在用户登录模块,对用户信息这个共享资源加锁,当一个线程进入同步代码块或方法时,会获取到锁,其他线程必须等待锁被释放才能进入。
- Lock接口(如ReentrantLock):
- 比synchronized更灵活。可以实现公平锁,即按照线程请求锁的顺序来分配锁,避免某些线程一直等待。还可以在获取锁和释放锁的地方进行更精细的控制,比如可以尝试获取锁,如果获取不到可以执行其他逻辑,而不是像synchronized那样一直阻塞。例如在一个资源竞争激烈的场景中,使用ReentrantLock的tryLock方法尝试获取锁,若获取不到则进行降级处理,使用缓存数据等。
- synchronized关键字:
- 线程安全问题:
-
线程池的参数及作用:
- corePoolSize:
- 核心线程数。当提交的任务数小于corePoolSize时,线程池会创建核心线程来执行任务。核心线程会一直存活在线程池中,除非设置了allowCoreThreadTimeOut为true。例如一个线程池corePoolSize设为5,当提交的任务数小于5时,会创建5个核心线程来执行任务。
- maximumPoolSize:
- 最大线程数。当提交的任务数超过corePoolSize且任务队列workQueue已满时,线程池会创建非核心线程来执行任务,直到线程数达到maximumPoolSize。如果超过这个数量,新提交的任务会根据拒绝策略进行处理。比如corePoolSize为5,队列容量为10,当提交任务数超过15时,就会创建非核心线程,最多创建到maximumPoolSize指定的数量。
- keepAliveTime:
- 非核心线程的存活时间。当非核心线程空闲时间超过keepAliveTime时,会被销毁。这样可以在任务量减少时减少线程资源的浪费。例如keepAliveTime设为60秒,一个非核心线程空闲超过60秒就会被销毁。
- unit:
- keepAliveTime的时间单位。可以是TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)等。
- workQueue:
- 任务队列。用来存放提交的任务。当任务数小于corePoolSize时,任务会直接交给核心线程执行;当任务数超过corePoolSize时,任务会被放入workQueue中。常见的任务队列有ArrayBlockingQueue(有界队列)、LinkedBlockingQueue(无界队列)等。例如使用ArrayBlockingQueue,需要指定队列容量,当队列满了且任务数超过corePoolSize时,就会根据其他策略处理新任务。
- threadFactory:
- 线程工厂。用来创建线程,可以自定义线程的名称、优先级等属性。例如可以创建一个线程工厂,为每个创建的线程设置一个有意义的名称,方便调试和监控。
- handler:
- 拒绝策略。当线程池满了且任务队列也满了,新提交的任务会用这个策略处理。常见的拒绝策略有AbortPolicy(直接抛出异常)、CallerRunsPolicy(调用者运行策略,即由提交任务的线程来执行任务)、DiscardPolicy(丢弃新提交的任务)、DiscardOldestPolicy(丢弃队列中最老的任务)等。比如使用AbortPolicy,当线程池和队列都满时,新任务会抛出RejectedExecutionException异常。
- corePoolSize:
-
合理配置线程池参数:
- 如果任务执行时间短:
- 核心线程数可以设置小一点,因为任务执行快,不需要太多常驻线程。任务队列可以设置大一点,用来缓冲短时间内大量提交的任务。例如对于一些简单的日志记录任务,核心线程数设为2 - 3个,任务队列设为50 - 100。这样少量核心线程快速处理任务,任务队列可以暂存来不及处理的任务。
- 如果任务执行时间长:
- 核心线程数就得设置大一点,保证有足够的线程来处理长时间任务。比如在一个复杂的数据分析任务中,核心线程数设为5 - 8个,根据实际任务量和服务器资源调整。避免任务长时间堆积,导致处理延迟。
- 如果任务执行时间短:
-
HashMap的底层实现原理:
- HashMap底层是数组加链表再加红黑树。
- 当往HashMap里put元素时:
- 先计算key的哈希值,通过哈希函数将key转换为一个整数。例如key为字符串“hello”,通过哈希函数得到一个哈希值。
- 然后通过哈希值找到对应的数组下标。计算哈希值与数组长度取模的结果,得到数组下标。如果数组下标对应的位置为空,就直接插入新节点。
- 如果不为空,就遍历链表或红黑树(当链表长度超过8且数组长度大于64时,链表会转换为红黑树),看是否有相同key的节点。如果有相同key的节点,就更新其值;如果没有,就插入新节点。
- 例如有一个HashMap,初始容量为16,负载因子为0.75。当插入第一个元素时,计算其哈希值,假设得到哈希值为10,10 % 16 = 10,就将元素插入到数组下标为10的位置。如果后续又插入元素,哈希值计算后对应的下标也是10,就会在该位置形成链表。当链表长度超过8且数组长度大于64时,链表会转换为红黑树,以提高查找效率。
-
ArrayList是否线程安全及如何转换为线程安全:
- 是否线程安全:
- ArrayList不是线程安全的。因为它的方法没有用synchronized修饰,在多线程环境下同时读写可能会出问题。比如一个线程在添加元素时,另一个线程可能在读取元素,导致数据不一致。例如一个线程在ArrayList的中间位置插入元素,会改变数组的结构,另一个线程同时读取元素时,可能会读到错误的数据。
- 转换为线程安全的方法:
- Collections.synchronizedList方法:
- 可以把ArrayList包装成线程安全的。例如List list = new ArrayList<>(); List synchronizedList = Collections.synchronizedList(list); 这样在多线程环境下访问synchronizedList时,会自动加锁,保证线程安全。
- CopyOnWriteArrayList:
- 它是写时复制的机制。写操作时会复制一份新的数组,读操作在原数组上进行。当一个线程调用add等写方法时,会创建一个新的数组,将原数组的元素复制到新数组,在新数组上进行添加操作,操作完成后将新数组赋值给原来的引用。读操作直接读取原数组,保证了读操作的线程安全和性能。例如在一个读多写少的场景中,使用CopyOnWriteArrayList可以避免加锁带来的性能开销,同时保证读操作的正确性。
- Collections.synchronizedList方法:
- 是否线程安全:
-
Spring的核心特性:
- 依赖注入(DI):
- 通过IoC容器管理对象的依赖关系。比如一个Service类依赖于一个Dao类,在传统方式下,Service类需要自己创建Dao类的实例。而在Spring中,可以通过配置文件或注解,让IoC容器自动创建Dao类的实例并注入到Service类中。这样可以降低类之间的耦合度,提高代码的可维护性和可测试性。
- 面向切面编程(AOP):
- 能把一些通用功能像日志、事务等和业务逻辑分离。例如可以通过AOP配置,在方法执行前后添加日志记录,或者在业务逻辑中添加事务管理。在一个用户注册业务中,可以通过AOP在注册方法执行前后记录用户注册信息和操作时间,同时通过AOP配置事务,确保注册过程中的数据库操作要么全部成功,要么全部
- 依赖注入(DI):