JAVA面试题

常见面试题

  1. 进程间通信的几种方式

    进程间通信(Inter-Process Communication, IPC)是指不同进程之间进行数据交换和共享资源的过程。常用的进程间通信方式有以下几种:

    1. 管道(Pipe):管道是一种单向通信机制,只能用于具有父子关系的进程间通信。管道有匿名管道和命名管道两种类型,前者只能在本机上使用,后者可以在不同机器上使用。
    2. 消息队列(Message Queue):消息队列是一种消息传递机制,可以用于不同进程之间的通信。消息队列由消息类型、消息内容和消息标识符组成,接收方可以根据标识符和类型来接收对应的消息。
    3. 共享内存(Shared Memory):共享内存是一种将同一块物理内存映射到多个进程的机制,多个进程可以直接访问同一块内存空间,实现高效的数据共享。但是,共享内存需要解决进程间同步和互斥的问题。
    4. 信号量(Semaphore):信号量是一种用于进程同步的机制,可以控制多个进程对共享资源的访问。信号量包括计数信号量和二进制信号量两种类型,前者可以有多个进程同时访问,后者只能有一个进程访问。
    5. 套接字(Socket):套接字是一种可用于不同机器之间通信的机制,可以用于网络编程。进程可以通过套接字向其他进程发送数据或接收数据。

    以上是常用的进程间通信方式,它们各有优劣,需要根据具体情况选择合适的方式进行通信。

  2. 设计模式的六大原则

    设计模式的六大原则是软件设计的基本原则,也是设计模式能够被广泛应用的基础。它们包括:

    1. 单一职责原则(Single Responsibility Principle, SRP):一个类或模块应该只有一个职责,即只有一个引起它变化的原因。这样可以提高代码的内聚性和可维护性。
    2. 开闭原则(Open-Closed Principle, OCP):软件实体应该对扩展开放,对修改关闭。这样可以使软件更加稳定和易于扩展。
    3. 里氏替换原则(Liskov Substitution Principle, LSP):子类应该能够替换掉它的基类,并且不影响程序的正确性。这样可以提高代码的重用性和灵活性。
    4. 依赖倒置原则(Dependency Inversion Principle, DIP):高层模块不应该依赖低层模块,两者都应该依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖于抽象。这样可以降低模块之间的耦合度,提高代码的可扩展性。
    5. 接口隔离原则(Interface Segregation Principle, ISP):客户端不应该依赖于它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上。这样可以避免不必要的依赖,提高代码的可重用性和可维护性。
    6. 迪米特原则(Law of Demeter, LoD):一个对象应该对其他对象有最少的了解,即一个对象不应该直接依赖于其他对象的内部细节。这样可以降低对象之间的耦合度,提高代码的灵活性和可维护性。

    这六个原则是相互关联的,它们共同构成了软件设计的基础原则,对于设计模式的应用和软件开发都具有重要的指导作用。

  3. Bean的生命周期

    在Java中,Bean是指一个具有无参构造函数、私有属性、公共getter/setter方法的Java类。Bean的生命周期包括以下几个阶段:

    1. 实例化阶段:当一个Java应用程序需要使用某个Bean时,通过Java反射机制实例化该Bean,即调用该类的构造函数创建一个对象。此时,该对象的状态为初始状态。
    2. 属性赋值阶段:在实例化后,容器会使用setter方法或直接通过反射机制对Bean的属性进行赋值,以初始化Bean的状态。
    3. 初始化阶段:在Bean的属性被赋值之后,容器会调用Bean的初始化方法。Bean的初始化方法可以通过实现InitializingBean接口、在XML文件中通过init-method属性或在注解中使用@PostConstruct来实现。
    4. 使用阶段:Bean已经被成功实例化、属性被赋值和初始化完成,在这个阶段,Bean可以被容器或其他对象调用。
    5. 销毁阶段:当Bean不再被使用时,容器会调用Bean的销毁方法。Bean的销毁方法可以通过实现DisposableBean接口、在XML文件中通过destroy-method属性或在注解中使用@PreDestroy来实现。

    需要注意的是,Bean的生命周期也可能会因为特殊情况而被打断或重新开始,比如在实例化或初始化阶段出现异常等。此时,容器会根据具体的实现策略来处理Bean的生命周期。

  4. Redis在项目中的用途

    Redis是一种内存键值存储数据库,具有高性能、高可靠性、易扩展等特点。在项目中,Redis可以用于以下几个方面:

    1. 缓存: Redis可以用作缓存服务器,将一些经常读取的数据缓存到内存中,从而加速数据的读取速度,减轻数据库的负担,提高应用程序的性能。
    2. 分布式锁: Redis的分布式锁机制可以用于多个节点之间的同步操作,确保同一时间只有一个节点可以访问某个共享资源,避免资源竞争和数据不一致的问题。
    3. 计数器: Redis可以用作计数器,可以对某个值进行原子性的自增或自减操作,并提供高并发和高可靠性的支持。
    4. 会话管理: Redis可以用于存储用户会话信息,通过设置过期时间和定期清理操作可以实现会话管理和自动失效等功能。
    5. 消息队列: Redis的发布/订阅机制可以用于构建消息队列系统,实现异步消息的处理和传递,提高应用程序的可扩展性和性能。
    6. 地理位置服务: Redis支持对地理位置信息的存储和查询,可以用于构建位置服务系统,实现附近的人或物品查询等功能。

    总之,Redis是一个非常灵活和多用途的数据库,可以在项目中用于多个方面,提高应用程序的性能、可扩展性和可靠性。

  5. Redis数据类型
    Redis支持多种数据类型,包括:

    1. 字符串(String):最基本的数据类型,可以存储任何类型的数据,包括数字、文本、二进制数据等。
    2. 列表(List):一个序列集合,可以存储多个元素,支持从头部或尾部添加或删除元素,还可以对列表进行裁剪、遍历等操作。
    3. 集合(Set):一组无序的元素集合,每个元素都是唯一的,支持集合运算,如并集、交集、差集等。
    4. 散列(Hash):一个键值对的集合,可以存储多个字段和值,支持快速的字段查找、修改、删除操作。
    5. 有序集合(Sorted Set):一个有序的元素集合,每个元素都有一个分数值(score)与之关联,可以按照分数值进行排序,还支持多个元素之间的比较操作。

    除了以上基本数据类型,Redis还提供了一些高级数据类型和数据结构,如HyperLogLog、Bitmap、地理位置数据等,这些数据类型可以用于特定的应用场景,如数据统计、布隆过滤器、地理位置查询等。总之,Redis的数据类型非常丰富,可以满足各种不同的数据存储和处理需求。

  6. 关系型数据库和NoSQL的区别

​ 关系型数据库和NoSQL(Not Only SQL)数据库是两种不同的数据库类型,它们有以下区别:

  1. 数据模型: 关系型数据库采用表格模型(即关系模型)来组织数据,每个表格由多个行和列组成,表格之间通过外键关联。而NoSQL数据库则采用不同的数据模型,如文档模型、键值模型、列族模型、图形模型等,每种模型都有不同的组织方式和查询语言。
  2. 数据一致性: 关系型数据库通常采用ACID(原子性、一致性、隔离性和持久性)事务来保证数据一致性,每个事务都具有原子性和一致性,但会影响性能。而NoSQL数据库通常采用BASE(基本可用性、软状态、最终一致性)模型来保证数据一致性,每个操作都是原子性的,但不保证事务的隔离性和持久性。
  3. 数据规模: 关系型数据库通常适用于中小规模的数据存储和处理,对于海量数据的存储和查询则会存在性能问题。而NoSQL数据库则适用于大规模的数据存储和处理,可以支持高并发、高吞吐量的数据查询和处理。
  4. 数据复杂性: 关系型数据库对于结构化数据的处理效率较高,但对于非结构化数据和复杂查询则效率较低。而NoSQL数据库则对于非结构化数据和复杂查询的处理效率较高,可以更好地适应数据的复杂性。

总之,关系型数据库和NoSQL数据库各有其优缺点,应根据实际应用场景和需求选择适合的数据库类型。

  1. 数据库隔离级别 每个都解决了什么问题

数据库隔离级别是为了解决并发事务带来的一系列问题而设立的。以下是各个隔离级别所解决的问题:

  1. 读未提交(Read Uncommitted):这个隔离级别没有解决任何问题。在这个级别下,一个事务可以读取到其他事务尚未提交的数据,导致脏读问题。
  2. 读已提交(Read Committed):这个隔离级别解决了脏读问题。在这个级别下,一个事务只能读取到已经提交的数据,因此不会出现脏读问题。但是,由于事务提交的数据可能被其他事务修改,因此可能出现不可重复读问题。
  3. 可重复读(Repeatable Read):这个隔离级别解决了脏读和不可重复读问题。在这个级别下,事务可以读取到事务开始时已经存在的数据,并且其他事务对这些数据的修改对该事务不可见。但是,由于其他事务可以插入新数据,因此可能出现幻读问题。
  4. 可串行化(Serializable):这个隔离级别解决了脏读、不可重复读和幻读问题。在这个级别下,事务会按照串行执行的方式来运行,保证每个事务都看到其他事务已经提交的所有数据,不会出现脏读、不可重复读和幻读问题。但是,由于串行执行可能会带来性能问题,因此这个隔离级别往往不被广泛使用。

不同的隔离级别在解决并发问题的同时,也带来了不同的性能影响。因此,在实际应用中需要根据具体情况选择适合的隔离级别。

  1. Synchronized 底层
    在Java中,synchronized是一种重量级锁,是Java内置的线程同步机制,用于实现线程之间的同步,保证共享资源在同一时刻只能被一个线程访问。在底层实现上,synchronized主要使用了对象监视器(monitor)来实现同步。

每个Java对象都会有一个关联的监视器对象(也称为管程或锁),monitor控制着对对象的访问。每个monitor对象拥有一个锁和一个等待队列。一个线程获得锁时,锁就进入锁定状态,此时只有拥有锁的线程能够进入monitor,而其他线程则必须等待锁被释放。

当线程调用synchronized方法或代码块时,会首先尝试获得对象的monitor的锁,如果锁没有被其他线程占用,则该线程获得锁并继续执行;如果锁已经被其他线程占用,则该线程就会被阻塞,并被放入锁的等待队列中,等待锁被释放后再次尝试获得锁。

synchronized保证了代码块在同一时刻只有一个线程执行,这种互斥性是通过monitor的锁来实现的。此外,synchronized还保证了线程之间的可见性,即当一个线程修改了共享变量的值时,其他线程能够立即看到该变量的最新值,这种可见性是通过JVM在执行synchronized块之前将修改的变量刷新到主内存,以及执行synchronized块时从主内存重新读取变量值来实现的。

需要注意的是,synchronized的实现细节可能会因不同的JVM版本而有所差异。此外,在JDK 5之后,Java还提供了更加灵活和高效的锁实现,如ReentrantLock等。

  1. JAVA实现单例
    双重检查锁定单例模式
    双重检查锁定单例模式是一种性能较高的延迟加载实现方式,它通过使用双重检查锁定来避免每次调用getInstance()方法时都需要进行同步。实现代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Singleton {
private static volatile Singleton INSTANCE;

private Singleton() {
}

public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}

在上述代码中,我们使用一个volatile关键字来保证INSTANCE变量的可见性,在getInstance()方法中使用双重检查锁定来确保线程安全性。

静态内部类单例模式
静态内部类单例模式是一种优雅且线程安全的实现方式,它使用静态内部类来持有唯一实例对象,并在需要时进行加载。实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton {
private Singleton() {
}

private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}

public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

在上述代码中,我们使用一个静态内部类SingletonHolder来持有唯一实例对象,在getInstance()方法中直接返回该对象。由于静态内部类只会在需要时进行加载,因此可以避免在类加载时创建对象的开销,同时保证了线程安全

  1. 主键索引和唯一索引的区别

主键索引和唯一索引都是数据库中常用的索引类型,它们的区别如下:

  1. 主键索引是一种特殊的唯一索引,主键索引的值必须是唯一的,且不能为NULL。每个表只能有一个主键索引。主键索引是用来保证数据完整性和查询效率的。
  2. 唯一索引的值也必须是唯一的,但是可以为NULL。每个表可以有多个唯一索引。唯一索引主要是用来避免数据重复和提高查询效率的。
  3. 主键索引默认是聚簇索引,而唯一索引不一定是聚簇索引。
  4. 在InnoDB存储引擎中,主键索引和数据是存储在一起的,因此主键索引的查询效率比唯一索引更高。而唯一索引是通过一个单独的索引表来实现的,查询唯一索引时需要先在索引表中查找,然后再到原表中查找,因此查询效率相对较低。

因此,如果一个字段具有唯一性约束,并且需要频繁地进行查询操作,那么应该将其定义为主键索引。如果一个字段具有唯一性约束,但是不需要频繁查询,那么可以将其定义为唯一索引。

  1. 索引失效的场景

索引失效指的是当查询语句中的条件不满足索引使用的条件时,数据库无法使用索引来加速查询,而需要进行全表扫描,导致查询效率低下。常见的导致索引失效的场景包括:

  1. 对索引列进行了运算或函数操作:当查询语句中对索引列进行了运算或函数操作时,索引就无法被使用,因为索引列的值已经被改变了。例如:WHERE YEAR(create_time) = 2022

  2. 使用了索引列的范围查询:当查询语句中使用了索引列的范围查询时,索引就无法被使用,因为范围查询需要扫描索引中的多个值,而不是单个值。例如:WHERE create_time BETWEEN '2022-01-01' AND '2022-12-31'

  3. 对索引列进行了类型转换:当查询语句中对索引列进行了类型转换时,索引就无法被使用,因为类型转换会导致索引列的值发生改变。例如:WHERE CAST(id AS VARCHAR) = '123'

  4. 对索引列使用了不等于操作符:当查询语句中使用了索引列的不等于操作符时,索引就无法被使用,因为不等于操作符需要扫描索引中的多个值,而不是单个值。例如:WHERE status <> 'deleted'

  5. 对索引列使用了LIKE操作符:当查询语句中使用了索引列的LIKE操作符时,如果LIKE的模式以通配符(%或_)开头,则索引就无法被使用。例如:WHERE name LIKE '%Tom'

  6. 索引列存在NULL值:当查询语句中使用了索引列,并且该列存在NULL值时,索引就无法被使用。因为NULL值无法与其他值进行比较,因此无法使用B树等比较操作的索引。例如:WHERE age = NULL

  7. 索引列的数据重复率过高:当索引列中的数据重复率过高时,使用索引扫描的效率会降低。因为索引扫描时需要在B树上进行多次跳跃,而重复值越多,跳跃次数就越多,效率就越低。

  8. 数据库的日志知道哪些,分别讲讲

  • 二进制日志(Binary Log):记录了数据库的所有更改操作,包括插入、更新、删除等操作。与事务日志不同的是,二进制日志记录了每个操作的确切位置,以便在进行数据复制和恢复时使用

  • redo日志

    假设有如下 SQL 语句,向表 user 中插入一条记录:

1
INSERT INTO user (id, name, age) VALUES (1, 'Alice', 18);

数据库在执行此操作时,会在 redo 日志中记录这个操作,如下所示:

1
2
3
4
5
LSN  | Type     | Transaction ID | Data
----------------------------------------------
100 | Begin Tx | 1 |
101 | Insert | 1 | id=1,name='Alice',age=18
102 | Commit | 1 |

其中,LSN(Log Sequence Number)表示日志序列号,用于标识每个日志记录的唯一性;Type 表示日志类型;Transaction ID 表示事务 ID;Data 表示操作的数据内容。

在上述日志记录中,LSN 为 100 的日志表示事务的开始,LSN 为 101 的日志表示插入操作,LSN 为 102 的日志表示事务的提交。当数据库崩溃或出现其他故障时,可以根据 redo 日志将该操作重新执行,确保数据的一致性和可靠性。

  • undo 日志

假设有如下 SQL 语句,更新表 user 中 id 为 1 的记录:

1
UPDATE user SET age=20 WHERE id=1;

数据库在执行此操作时,会在 undo 日志中记录这个操作,如下所示:

1
2
3
4
5
LSN  | Type     | Transaction ID | Data
----------------------------------------------
200 | Begin Tx | 2 |
201 | Update | 2 | id=1,age=18
202 | Commit | 2 |

其中,LSN、Type、Transaction ID 的含义同上,Data 表示该操作的相反操作,用于撤销操作或回滚事务。

在上述日志记录中,LSN 为 200 的日志表示事务的开始,LSN 为 201 的日志表示更新操作,LSN 为 202 的日志表示事务的提交。当需要撤销该操作时,数据库会根据 undo 日志将该操作的相反操作执行一遍,即将 age 从 20 回滚到 18。这样可以确保数据的一致性和可靠性。

  1. JVM(Java Virtual Machine)内存模型指的是JVM在运行Java程序时所管理的内存结构。JVM内存模型主要分为以下几个部分:
  • 程序计数器(Program Counter Register):每个线程都有一个程序计数器,用于记录线程当前执行的字节码指令的地址。
  • Java虚拟机栈(Java Virtual Machine Stacks):Java虚拟机栈存储了每个线程运行时的方法调用栈和局部变量表信息。每个方法在执行时都会创建一个栈帧,用于存储该方法的参数、局部变量和方法返回值等信息。当方法调用完成时,该方法对应的栈帧会从虚拟机栈中弹出。
  • 本地方法栈(Native Method Stack):本地方法栈类似于Java虚拟机栈,但是它是为Native方法服务的。
  • Java堆(Java Heap):Java堆是JVM管理的内存中最大的一部分,用于存储Java对象实例以及数组等数据结构。Java堆可以被所有线程共享,在堆中创建的对象都有一个标记,可以通过垃圾回收机制自动回收。
  • 方法区(Method Area):方法区用于存储已加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。与Java堆一样,方法区也是线程共享的。
  • 运行时常量池(Runtime Constant Pool):运行时常量池用于存储编译期生成的各种字面量和符号引用,在类加载完成后,将符号引用转化为直接引用。
  • 直接内存(Direct Memory):直接内存不是JVM内存中的一部分,但是它被JVM直接管理,是一种特殊的内存空间。直接内存与Java堆相似,但是它是通过Native函数库直接分配的内存空间,因此不会受到Java堆大小的限制。直接内存的使用可以提高I/O性能。
  1. Spring事务失效的几种常见
  • 注解失效
    没有通过代理对象调用
    方法没有使用public修饰,使用private、protect、default、static修饰均失效,因为无法生成重写该方法的代理对象
  • 事务失效
    数据库不支持事务、不再同一个线程中抛出异常、抛出的异常不属于事务捕获的异常或者错误