哈喽,大家好,我是千羽。
嗯,前面呢,《快手一面》的时候也大部分都是Java常见的八股文,但是问的还是挺深的。
这次二面的话依旧还是八股文,这个还是挺常见的。比如Java的单例模式,说完单例模式肯定会问你下面的volatile是的底层原理.
然后还有一些是Spring的面试题,比如Spring的事务失效等等。回答的还可以吧,还有算法也还写出来了,所以这次二面也过了👏👏
直接上手单例模式,写完了之后给面试官看,然后可以诱导面试官说,这种存在xxx问题。应该对xxxx进行处理。可以加强xxx
“常见的问题可能包括线程安全性、序列化、反射攻击等。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
可能存在的问题:
if (instance == null)
的判断条件中,导致创建多个实例,违反了单例模式的初衷。readResolve()
方法来防止这种情况下创建新的对象。好了,忽悠完面试官,再讲解为了解决线程安全性问题,可以使用同步锁或者双重检查锁定(Double-Check Locking)来保证在多线程环境下单例的唯一性和正确性。同时,在实现单例模式时也要考虑其他潜在的问题,例如序列化、反射等。
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
的可见性,从而解决了懒汉式单例模式的线程安全问题。
单例模式应用场景
下一步的套路,就问你 volatile
的底层原理~~
volatile
的作用:
volatile
变量的值,其他线程能够立即看到这个变化,保证了变量的可见性。volatile
变量之前的指令不会被重排序到写之后,读 volatile
变量之后的指令不会被重排序到读之前。volatile
关键字可以保证变量的读写操作是有序的,即保证了操作的有序性。总结就是:volatile
关键字通过强制线程直接访问主内存中的变量值,而不是使用线程自己的缓存,确保了变量在多线程环境下的可见性和一致性,同时防止了编译器和处理器的优化对指令顺序的调整,从而保证了操作的有序性。
volatile
关键字在 Java 中用来声明变量,它确保了多线程环境下对该变量的可见性、禁止指令重排序以及保证了一定的有序性。其底层原理涉及到内存模型和处理器架构。
内存模型:
volatile
变量进行写操作时,会直接将该变量的值刷新到主内存中,并且在读取该变量时会直接从主内存中获取最新值。处理器架构:
volatile
关键字会告诉处理器,这个变量可能会被其他线程修改,因此需要直接从主内存读取和写入该变量的值,而不是依赖于缓存。(1)初始标记(Initial Marking):标记根对象: G1从GC Root根对象(如线程栈、静态变量等)开始,标记所有存活的对象,这个过程是短暂的暂停。
(2)并发标记(Concurrent Marking):并发标记阶段: G1启动并发标记过程,与应用程序并发运行。它扫描所有对象,标记出所有存活的对象,并标记出可能被回收的区域。
(3)最终标记(Final Marking):再次标记: 在并发标记过程中,应用程序继续运行,可能会产生新的存活对象。因此,G1进行最终标记,找出在并发标记过程中被新生成的存活对象,并更新标记状态。
(4)混合回收(Mixed Collection):区域回收: G1根据垃圾最多的区域(Garbage-First),选择优先回收的区域。它会选择垃圾较多的小块区域进行回收,这些区域中包含大量垃圾对象。
(5)清理(Cleanup):回收空闲区域: G1完成回收后,会释放掉被标记为垃圾的对象所占用的空间,并将空闲的区域加入到空闲列表中。
(6)重复迭代:循环迭代: G1会根据堆的状态,不断重复上述过程。它动态地根据垃圾量和区域情况选择回收的目标,以提高效率和减少暂停时间。
G1回收器通过区域化管理内存,采用并发标记和混合回收等策略,在保证垃圾回收的效率的同时,尽量减少长时间的停顿。这种处理方式使得G1相对于传统的垃圾回收器在大堆和低延迟场景下有着更好的性能表现。
Spring框架提供了多种事务管理的方式,主要包括以下几种:
1.编程式事务管理(Programmatic Transaction Management):
在代码中显式地使用编程方式管理事务,通过编写代码来控制事务的开始、提交、回滚以及事务的属性设置。使用TransactionTemplate
或者直接使用PlatformTransactionManager
来进行事务管理。
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.execute(status -> {
// 业务逻辑
return result;
});
2.声明式事务管理(Declarative Transaction Management):
通过使用Spring的AOP机制,在方法或类上使用注解或XML配置声明事务的属性,将事务管理与业务逻辑解耦。常见的注解有@Transactional
,它可以应用在方法级别或者类级别。
@Transactional
public void someTransactionalMethod() {
// 业务逻辑
}
3.基于XML的事务管理:
通过在Spring的配置文件中使用XML配置事务管理器、事务属性等来定义事务管理的行为。在XML中配置<tx:advice>
、<tx:attributes>
等元素来声明事务的属性。
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*Operation" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="serviceOperation" expression="execution(* com.example.Service.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="serviceOperation"/>
</aop:config>
4.注解和XML混合使用:
也可以将注解和XML混合使用来管理事务,比如在XML配置中启用注解驱动,使用<tx:annotation-driven>
开启基于注解的事务管理。
<tx:annotation-driven transaction-manager="transactionManager"/>
在实际应用中,通常会根据具体情况选择合适的事务管理方式。声明式事务管理是Spring中推荐的方式,能够更好地与业务逻辑解耦,提高代码的可读性和可维护性。
1.未开启代理:如果在使用基于注解的事务管理时,Spring的AOP代理未被正确开启或应用到目标对象上,导致注解不生效。通常需要确保被注解修饰的方法在代理对象上执行,这样事务才能被AOP拦截并进行管理。
2.异常被捕获并未重新抛出:当事务方法内部捕获了异常并未重新抛出,Spring无法感知到异常发生,从而无法触发事务的回滚操作。确保捕获到的异常能够在需要的情况下重新抛出,以便Spring捕获到并进行事务处理。
3.方法没有被public修饰:基于注解的事务通常会被Spring的AOP机制拦截,如果方法未被public
修饰,AOP无法正确拦截方法调用,导致事务注解失效。确保被事务管理的方法是public
的。
4.嵌套方法调用问题:Spring的事务是通过代理实现的,嵌套方法调用可能导致事务失效。如果在同一个类中一个public
方法调用另一个public
方法,事务注解可能不会生效。可以使用AspectJ
模式或者将方法放在不同的Bean中以确保事务生效。
5.事务作用域问题:事务的传播行为可能会导致事务失效。如果一个方法内部调用了另一个被@Transactional
修饰的方法,但是这个方法的事务传播行为与当前事务不匹配,可能会导致内部方法的事务失效。
6.配置问题:不正确的配置可能导致事务失效,如错误地配置了事务管理器、忘记添加<tx:annotation-driven>
来启用基于注解的事务管理等。
Spring的事务管理是建立在AOP(Aspect-Oriented Programming)之上的,主要利用AOP实现对事务的控制。其底层原理主要涉及两个重要的组件:TransactionInterceptor
和 PlatformTransactionManager
。
PlatformTransactionManager
是Spring事务的核心接口,定义了一系列管理事务的方法。各种数据源(JDBC、Hibernate、JPA等)都有对应的事务管理器实现。TransactionInterceptor
是一个AOP拦截器,在方法调用前后拦截目标方法,负责事务的开启、提交、回滚等操作。事务的生命周期:
@Transactional
注解修饰的方法被调用时,AOP会拦截这个方法的调用。TransactionInterceptor
捕获到方法调用,查找对应的事务管理器(PlatformTransactionManager
)。TransactionInterceptor
决定提交事务或者回滚事务。Spring Boot的Starter本质上是一种约定俗成的依赖聚合体系,它的底层原理主要包括以下几个方面:
spring-boot-starter-*
为前缀。当引入这些Starter时,Spring Boot会自动装配相应的配置。META-INF/spring.factories
文件,该文件指定了自动配置类。自动配置类使用条件化配置(例如@ConditionalOnClass
、@ConditionalOnProperty
等注解)来根据特定的条件来决定是否应用这些配置。spring-boot-starter-web
可能依赖于spring-boot-starter-tomcat
,这样在引入spring-boot-starter-web
时会自动引入spring-boot-starter-tomcat
。HSF(High-Speed Service Framework)和Dubbo都是阿里巴巴在分布式服务领域的开源框架,用于构建分布式服务架构。它们有一些相似之处,但也有一些明显的区别:
Dubbo:
HSF:
区别:
HTTP/1.1、HTTP/2和WebSocket都是网络通信协议,它们有着不同的特点和应用场景。
HTTP/1.1:
HTTP/2:
WebSocket:
区别:
生成一个集合的所有子集是一个经典的组合问题,可以使用递归或位运算的方法来实现。以下是两种常见的方法:
方法一:递归生成子集
import java.util.ArrayList;
import java.util.List;
public class SubsetGenerator {
public List<List<Integer>> generateSubsets(List<Integer> nums) {
List<List<Integer>> result = new ArrayList<>();
backtrack(result, new ArrayList<>(), nums, 0);
return result;
}
private void backtrack(List<List<Integer>> result, List<Integer> tempList, List<Integer> nums, int start) {
result.add(new ArrayList<>(tempList));
for (int i = start; i < nums.size(); i++) {
tempList.add(nums.get(i));
backtrack(result, tempList, nums, i + 1);
tempList.remove(tempList.size() - 1);
}
}
public static void main(String[] args) {
SubsetGenerator generator = new SubsetGenerator();
List<Integer> nums = List.of(1, 2, 3);
List<List<Integer>> subsets = generator.generateSubsets(nums);
for (List<Integer> subset : subsets) {
System.out.println(subset);
}
}
}
方法二:位运算生成子集
import java.util.ArrayList;
import java.util.List;
public class SubsetGenerator {
public List<List<Integer>> generateSubsets(List<Integer> nums) {
List<List<Integer>> result = new ArrayList<>();
int n = nums.size();
for (int i = 0; i < (1 << n); i++) {
List<Integer> subset = new ArrayList<>();
for (int j = 0; j < n; j++) {
if ((i & (1 << j)) > 0) {
subset.add(nums.get(j));
}
}
result.add(subset);
}
return result;
}
public static void main(String[] args) {
SubsetGenerator generator = new SubsetGenerator();
List<Integer> nums = List.of(1, 2, 3);
List<List<Integer>> subsets = generator.generateSubsets(nums);
for (List<Integer> subset : subsets) {
System.out.println(subset);
}
}
}
可以使用动态规划来解决这个问题。找到两个字符串的最长公共子序列并输出:
public class LongestCommonSubsequence {
public static String longestCommonSubsequence(String text1, String text2) {
int m = text1.length();
int n = text2.length();
int[][] dp = new int[m + 1][n + 1];
// 构建动态规划表
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
// 回溯构造最长公共子序列
int len = dp[m][n];
char[] result = new char[len];
int i = m, j = n;
while (i > 0 && j > 0) {
if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
result[--len] = text1.charAt(i - 1);
i--;
j--;
} else if (dp[i - 1][j] > dp[i][j - 1]) {
i--;
} else {
j--;
}
}
return new String(result);
}
public static void main(String[] args) {
String text1 = "abcdefg";
String text2 = "acbcf";
String longestCommonSeq = longestCommonSubsequence(text1, text2);
System.out.println("最长公共子序列为: " + longestCommonSeq);
}
}