真等不及了,冲阿里去了!

2024年 3月 6日 50.2k 0

大家好,我是小林。

阿里巴巴上周开启了 25 届的实习招聘,阿里是Java 一线大厂,到底面试难度如何呢?同学们准备好了吗?

今天分享一位校招同学阿里的 Java 后端开发的面经,阿里的风格会比较关注 Java 和后端组件,而网络和系统考察通常都比较少的,所以准备面阿里的同学 Java 和后端组件这两方面的要准备好。

这次的 Java 后端开发的面经,考察的知识点, 针对八股文的涉及的内容,我帮大家列了一下:

  • Java 基础:面向对象、多态、重载
  • Java 集合:HashMap 连环问、红黑树
  • Java并发:volatile、Synchronized、ReentrantLock
  • Java 线程池:线程池参数、核心线程数设置
  • MySQL:索引结构、建立索引规则、explain、联合索引

八股这块一共问了 30 分钟,其余时间问了项目方面的内容。

Java基础

讲一下Java面向对象的特点

封装、继承、多态是Java面向对象编程的三大特点。

  • 封装(Encapsulation):封装是面向对象编程的基本特点之一,它将数据和方法封装在对象内部,隐藏对象的内部实现细节,只暴露必要的接口供外部访问。通过封装,可以实现信息的隐藏和保护,提高代码的安全性和可靠性。
  • 继承(Inheritance):继承是面向对象编程的重要特点,它允许一个类(子类)继承另一个类(父类)的属性和方法。子类可以重用父类的代码,并可以通过扩展和重写来增加新的功能或修改现有功能。继承提高了代码的复用性和可维护性,同时也体现了类与类之间的关系。
  • 多态(Polymorphism):多态是面向对象编程的核心概念之一,它允许不同对象对同一消息作出不同的响应。在Java中,多态性通过方法重载和方法重写来实现。方法重载是指在同一个类中可以定义多个同名方法,但参数列表不同;方法重写是指子类可以重写父类的方法,实现不同的行为。多态性提高了代码的灵活性和扩展性,使得程序更易于理解和维护。

用过“多态”吗?举一个具体例子

一个具体的例子是,假设有一个动物类(Animal)和它的两个子类:狗类(Dog)和猫类(Cat)。它们都有一个名为“makeSound”的方法,但是每种动物发出的声音是不同的。

class Animal {
    public void makeSound() {
        System.out.println("Some sound");
    }
}

class Dog extends Animal {
    public void makeSound() {
        System.out.println("Woof");
    }
}

class Cat extends Animal {
    public void makeSound() {
        System.out.println("Meow");
    }
}

public class PolymorphismExample {
    public static void main(String[] args) {
        Animal dog = new Dog();
        Animal cat = new Cat();
        
        dog.makeSound(); // 输出:Woof
        cat.makeSound(); // 输出:Meow
    }
}

通过多态性,可以创建一个Animal类型的引用指向一个具体的Dog或Cat对象。当调用这个引用的“makeSound”方法时,根据实际指向的对象类型,会执行相应子类的方法,从而实现不同动物发出不同声音的效果。这样就体现了多态的特性,同一个方法调用可以产生不同的行为,提高了代码的灵活性和可扩展性。

多态和重载有什么关系?

重载是一种编译时多态,而多态是一种运行时多态。两者都是实现多态性的方式,但发生的时间点和机制不同。

  • 重载是指在同一个类中,方法名相同但参数列表不同的情况,通过参数个数、类型或顺序的不同来区分不同的方法。重载是静态绑定的概念,编译器在编译期间根据方法的参数列表来确定调用哪个方法。
  • 多态是指同一个方法名可以在不同的类中有不同的实现,不同的子类可以重写父类的方法,通过父类引用指向子类对象时,根据实际对象的类型来确定调用哪个方法。多态是动态绑定的概念,运行时根据对象的实际类型来确定调用哪个方法。

Java集合

Java中的HashMap了解吗?

了解的,HashMap是Java中常用的一种数据结构,它基于哈希表实现,用于存储键值对

聊聊HashMap的底层结构

在 JDK 1.7 版本之前, HashMap 数据结构是数组和链表,HashMap通过哈希算法将元素的键(Key)映射到数组中的槽位(Bucket)。如果多个键映射到同一个槽位,它们会以链表的形式存储在同一个槽位上,因为链表的查询时间是O(n),所以冲突很严重,一个索引上的链表非常长,效率就很低了。

所以在 JDK 1.8 版本的时候做了优化,当一个链表的长度超过8的时候就转换数据结构,不再使用链表存储,而是使用红黑树,查找时使用红黑树,时间复杂度O(log n),可以提高查询性能,但是在数量较少时,即数量小于6时,会将红黑树转换回链表。

图片图片

为什么要引入红黑树,而不用其他树?

  • 为什么不使用二叉排序树?问题主要出现在二叉排序树在添加元素的时候极端情况下会出现线性结构。比如由于二叉排序树左子树所有节点的值均小于根节点的特点,如果我们添加的元素都比根节点小,会导致左子树线性增长,这样就失去了用树型结构替换链表的初衷,导致查询时间增长。所以这是不用二叉查找树的原因。
  • 为什么不使用平衡二叉树呢?红黑树不追求"完全平衡",而而AVL是严格平衡树,因此在增加或者删除节点的时候,根据不同情况,旋转的次数比红黑树要多。红黑树读取略逊于AVL,维护强于AVL,空间开销与AVL类似,内容极多时略优于AVL,维护优于AVL。

基本上主要的几种平衡树看来,红黑树有着良好的稳定性和完整的功能,性能表现也很不错,综合实力强,在诸如STL的场景中需要稳定表现。

HashMap会出现红黑树一直增高变成无限高的情况吗?

不能无限增长。当集合中的节点数超过了阈值,HashMap会进行扩容,这时原始的红黑树节点会被打散,可能会退化成链表结构。

HashMap读和写的时间复杂度是多少?

HashMap的读取(查找)和写入(插入、更新、删除)操作的时间复杂度均为O(1),即常数时间复杂度。

这是因为HashMap内部使用哈希表来存储键值对,通过计算键的哈希值可以直接定位到对应的存储位置,从而实现快速的读取和写入操作。

在理想情况下,HashMap可以在常数时间内完成查找和插入操作,具有高效的性能表现。

HashMap是线程安全的吗?怎么解决?

不是线程安全的。

  • JDK 1.7 HashMap 采用数组 + 链表的数据结构,多线程背景下,在数组扩容的时候,存在 Entry 链死循环和数据丢失问题。
  • JDK 1.8 HashMap 采用数组 + 链表 + 红黑二叉树的数据结构,优化了 1.7 中数组扩容的方案,解决了 Entry 链死循环和数据丢失问题。但是多线程背景下,put 方法存在数据覆盖的问题。

解决的方式:

  • 使用ConcurrentHashMap:ConcurrentHashMap是Java提供的线程安全的哈希表实现,它通过分段锁(Segment)和CAS操作来保证线程安全性,适用于高并发环境。
  • 使用Collections.synchronizedMap:可以通过Collections工具类中的synchronizedMap方法来创建一个线程安全的HashMap,该方法会返回一个同步的Map对象,但性能可能不如ConcurrentHashMap。

解决线程安全问题还有哪些办法?

  • 使用同步关键字synchronized:可以通过在方法或代码块上使用synchronized关键字来实现线程安全,确保同一时刻只有一个线程可以访问共享资源。
  • 使用volatile关键字:可以使用volatile关键字修饰变量,保证变量的可见性,即一个线程修改了变量的值,其他线程能立即看到最新值,从而避免数据不一致的问题。
  • 使用线程安全的工具类:Java中提供了诸如AtomicInteger、AtomicLong、CountDownLatch等线程安全的工具类,可以帮助解决并发场景下的线程安全性问题。
  • 使用并发容器:Java中提供了多种线程安全的并发容器,如ConcurrentLinkedQueue、CopyOnWriteArrayList等,可以替代传统的非线程安全容器来解决多线程并发访问问题。

Java并发

volatile关键字是如何保证内存可见性的?底层是怎么实现的?

volatile关键字通过两种机制来保证内存可见性:

  • 禁止指令重排序:在程序运行时,为了提高性能,编译器和处理器可能会对指令进行重排序,这可能会导致变量的更新操作被延迟执行或者乱序执行,从而使得其他线程无法立即看到最新的值。使用volatile关键字修饰的变量会禁止指令重排序,保证变量的更新操作按照代码顺序执行。
  • 内存屏障(Memory Barrier):在多核处理器架构下,每个线程都有自己的缓存,volatile关键字会在写操作后插入写屏障(Write Barrier),在读操作前插入读屏障(Read Barrier),确保变量的更新能够立即被其他线程看到,保证内存可见性。

通过禁止指令重排序和插入内存屏障,volatile关键字能够保证被修饰变量的更新操作对其他线程是可见的,从而有效解决了多线程环境下的内存可见性问题。

为什么需要保证内存可见性?

如果不保证内存可见性,就会出现数据脏读,一个线程修改了共享变量的值,但其他线程无法立即看到最新值,导致其他线程读取到了过期数据,从而产生错误的结果。

通过保证内存可见性,避免数据不一致性和并发访问带来的问题,保证程序的正确性和稳定性。

volatile为什么要禁止指令重排,能举一个具体的指令重排出现问题的例子吗

禁止指令重排是为了确保程序的执行顺序与代码编写顺序一致,特别是在多线程环境下,避免出现意外的结果。具体来说,如果不禁止指令重排,可能会导致以下问题:

举例来说,假设有如下代码片段:

int a = 0;
boolean flag = false;

// 线程1
a = 1;
flag = true;

// 线程2
if (flag) {
    System.out.println(a);
}

如果发生指令重排,可能会导致线程2在判断flag时先于a的赋值操作,那么线程2就会打印出0,而不是预期的1。这种情况下,禁止指令重排可以确保线程2在看到flag为true时,也能看到a被正确赋值为1,避免出现问题。

因此,通过禁止指令重排,可以保证程序的执行顺序符合代码逻辑,避免出现意外的行为,特别是在涉及多线程并发的情况下更为重要。

Synchronized的底层原理是什么?锁升级的过程了解吗?

  • 底层实现:Synchronized关键字底层是使用monitor对象锁实现的,每一个对象关联一个monitor对象,而monitor对象可以看成是一个对象锁,它采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其他线程再想获取这个对 象锁时会被阻塞住,这样就能保证拥有锁的线程可以安全的执行临界区的代码。
  • 锁升级:是指JVM根据锁的竞争情况和对象的状态,将对象的锁从偏向锁、轻量级锁升级为重量级锁的过程。偏向锁是指针对无竞争的情况下,锁会偏向于第一个获取锁的线程;轻量级锁是指针对短时间内只有一个线程竞争锁的情况下,使用CAS操作来避免阻塞;重量级锁是指多个线程竞争同一个锁时,通过操作系统的互斥量来实现线程阻塞和唤醒。锁升级的过程是为了提高多线程并发访问的效率和性能。

线程是怎么确定拿到锁的?

线程确定拿到锁的过程:是通过检查锁的状态并尝试获取锁来实现的。在JVM中,锁信息具体是存放在Java对象头中的。

当一个线程尝试进入synchronized代码块或方法时,JVM会检查对应对象的锁状态。如果对象的锁未被其他线程持有,即锁状态为可获取,那么该线程将成功获取锁并进入临界区执行代码。

锁信息具体放到哪的?

锁的状态信息是Java对象头中的 Mark Word 字段,这个字段对象关于锁的信息、垃圾回收信息等。

图片图片

Java 对象在内存中的表示

JVM通过操作对象的头部信息来实现锁的获取、释放以及等待队列的管理。当线程成功获取锁后,对象的头部信息会被更新为当前线程的标识,表示该线程拥有了这个锁。

其他线程在尝试获取同一个锁时,会检查对象的头部信息,如果锁已经被其他线程持有,它们将会被阻塞直到锁被释放。

Synchronized加锁和ReentrantLock加锁有什么区别?

synchronized 和 ReentrantLock 都是 Java 中提供的可重入锁:

  • 用法不同:synchronized 可用来修饰普通方法、静态方法和代码块,而 ReentrantLock 只能用在代码块上。
  • 获取锁和释放锁方式不同:synchronized 会自动加锁和释放锁,当进入 synchronized 修饰的代码块之后会自动加锁,当离开 synchronized 的代码段之后会自动释放锁。而 ReentrantLock 需要手动加锁和释放锁
  • 锁类型不同:synchronized 属于非公平锁,而 ReentrantLock 既可以是公平锁也可以是非公平锁。
  • 响应中断不同:ReentrantLock 可以响应中断,解决死锁的问题,而 synchronized 不能响应中断。
  • 底层实现不同:synchronized 是 JVM 层面通过监视器实现的,而 ReentrantLock 是基于 AQS 实现的。

Java 线程池

线程池了解过吗?有哪些核心参数?

了解过的,线程池是为了减少频繁的创建线程和销毁线程带来的性能损耗。

线程池分为核心线程池,线程池的最大容量,还有等待任务的队列,提交一个任务,如果核心线程没有满,就创建一个线程,如果满了,就是会加入等待队列,如果等待队列满了,就会增加线程,如果达到最大线程数量,如果都达到最大线程数量,就会按照一些丢弃的策略进行处理。

图片图片

线程池的构造函数有7个参数:

图片图片

  • corePoolSize:线程池核心线程数量。默认情况下,线程池中线程的数量如果

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论