前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >快手二面,把握住了!

快手二面,把握住了!

作者头像
千羽
发布2024-01-04 12:21:51
1780
发布2024-01-04 12:21:51
举报
文章被收录于专栏:程序员千羽程序员千羽

哈喽,大家好,我是千羽。

嗯,前面呢,《快手一面》的时候也大部分都是Java常见的八股文,但是问的还是挺深的。

这次二面的话依旧还是八股文,这个还是挺常见的。比如Java的单例模式,说完单例模式肯定会问你下面的volatile是的底层原理.

然后还有一些是Spring的面试题,比如Spring的事务失效等等。回答的还可以吧,还有算法也还写出来了,所以这次二面也过了👏👏

  • 1、Java实现一个单例模式
  • 2、上面代码的 volatile 的作用是什么?底层原理说一下?
  • 3、说一下G1垃圾回收器
  • 4、Spring事务管理的方式有哪些
  • 5、Spring事务失效的场景,为什么会失效?
  • 6、Spring事务的底层原理说一下
  • 7、Spring Boot的Starter的底层原理
  • 8、HSF和Dubbo的区别
  • 9、HTTP1.1和2.0和websocket协议
  • 10、算法:一个集合找所有子集
  • 11、算法:找到两个字符串的最长公共子序列并输出

1、Java实现一个单例模式

直接上手单例模式,写完了之后给面试官看,然后可以诱导面试官说,这种存在xxx问题。应该对xxxx进行处理。可以加强xxx

“常见的问题可能包括线程安全性、序列化、反射攻击等。

代码语言:javascript
复制
public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

可能存在的问题:

  1. 线程安全性问题:在多线程环境下,以上的实现会导致多个线程可能同时进入 if (instance == null) 的判断条件中,导致创建多个实例,违反了单例模式的初衷。
  2. 不适合多线程环境:上述实现方式在多线程环境下不能保证单例对象的唯一性,可能会造成资源浪费或出现不可预料的行为。
  3. 序列化和反序列化问题:当一个单例类被序列化然后再反序列化时,会创建一个新的对象。要解决这个问题,可以通过实现 readResolve() 方法来防止这种情况下创建新的对象。
  4. 反射攻击:即使使用私有构造函数,通过反射仍然可以访问并创建新的对象,破坏了单例的限制。为了解决这个问题,可以在构造函数中添加逻辑,防止多次实例化。
  5. 内存泄漏:在某些情况下,如果实例被持续引用而不被释放,可能导致内存泄漏。

好了,忽悠完面试官,再讲解为了解决线程安全性问题,可以使用同步锁或者双重检查锁定(Double-Check Locking)来保证在多线程环境下单例的唯一性和正确性。同时,在实现单例模式时也要考虑其他潜在的问题,例如序列化、反射等。

代码语言:javascript
复制
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 的可见性,从而解决了懒汉式单例模式的线程安全问题。

单例模式应用场景

  1. 资源共享和独占控制:在需要共享某个资源(如数据库连接池、线程池、日志对象等)或控制某个资源的独占访问时,单例模式确保只有一个实例存在,全局可访问。
  2. 对象控制管理:有时需要全局管理某个类的实例,比如配置文件解析器、缓存管理器等,使用单例模式可以方便管理对象的生命周期。
  3. 应用程序配置:用于管理和提供全局访问的配置信息,例如系统配置信息、应用程序设置等,确保在应用程序中只有一个配置对象。
  4. 管理资源访问:例如线程池、线程管理等,确保全局仅有一个实例以避免资源竞争和冲突。
  5. 缓存管理:用于管理全局缓存对象,确保缓存的一致性和有效性,避免重复创建对象提升性能。
  6. 日志记录器:在应用程序中只需要一个日志记录器实例,可以被全局访问,方便统一管理日志输出。
  7. 对象工厂:在需要控制对象创建数量和全局访问时,单例模式可以用于对象工厂的实现。

下一步的套路,就问你 volatile的底层原理~~

2、上面代码的 volatile 的作用是什么?底层原理说一下?

volatile 的作用:

  1. 可见性: 当一个线程修改了 volatile 变量的值,其他线程能够立即看到这个变化,保证了变量的可见性。
  2. 禁止指令重排序: 在写 volatile 变量之前的指令不会被重排序到写之后,读 volatile 变量之后的指令不会被重排序到读之前。
  3. 有序性: volatile 关键字可以保证变量的读写操作是有序的,即保证了操作的有序性。

总结就是:volatile 关键字通过强制线程直接访问主内存中的变量值,而不是使用线程自己的缓存,确保了变量在多线程环境下的可见性和一致性,同时防止了编译器和处理器的优化对指令顺序的调整,从而保证了操作的有序性。


volatile 关键字在 Java 中用来声明变量,它确保了多线程环境下对该变量的可见性、禁止指令重排序以及保证了一定的有序性。其底层原理涉及到内存模型和处理器架构。

内存模型:

  • 在多线程环境下,每个线程都有自己的工作内存,而变量可能存储在主内存中。当一个线程对 volatile 变量进行写操作时,会直接将该变量的值刷新到主内存中,并且在读取该变量时会直接从主内存中获取最新值。

处理器架构:

  • 处理器(CPU)有多级缓存,线程对变量的操作可能先缓存在处理器的缓存中,而不是直接写入主内存。使用 volatile 关键字会告诉处理器,这个变量可能会被其他线程修改,因此需要直接从主内存读取和写入该变量的值,而不是依赖于缓存。

3、说一下G1垃圾回收器

(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相对于传统的垃圾回收器在大堆和低延迟场景下有着更好的性能表现。

4、Spring事务管理的方式有哪些

Spring框架提供了多种事务管理的方式,主要包括以下几种:

1.编程式事务管理(Programmatic Transaction Management):

在代码中显式地使用编程方式管理事务,通过编写代码来控制事务的开始、提交、回滚以及事务的属性设置。使用TransactionTemplate或者直接使用PlatformTransactionManager来进行事务管理。

代码语言:javascript
复制
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.execute(status -> {
    // 业务逻辑
    return result;
});

2.声明式事务管理(Declarative Transaction Management):

通过使用Spring的AOP机制,在方法或类上使用注解或XML配置声明事务的属性,将事务管理与业务逻辑解耦。常见的注解有@Transactional,它可以应用在方法级别或者类级别。

代码语言:javascript
复制
@Transactional
public void someTransactionalMethod() {
    // 业务逻辑
}

3.基于XML的事务管理:

通过在Spring的配置文件中使用XML配置事务管理器、事务属性等来定义事务管理的行为。在XML中配置<tx:advice><tx:attributes>等元素来声明事务的属性。

代码语言:javascript
复制
<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>开启基于注解的事务管理。

代码语言:javascript
复制
<tx:annotation-driven transaction-manager="transactionManager"/>

在实际应用中,通常会根据具体情况选择合适的事务管理方式。声明式事务管理是Spring中推荐的方式,能够更好地与业务逻辑解耦,提高代码的可读性和可维护性。

5、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>来启用基于注解的事务管理等。

6、Spring事务的底层原理说一下

Spring的事务管理是建立在AOP(Aspect-Oriented Programming)之上的,主要利用AOP实现对事务的控制。其底层原理主要涉及两个重要的组件:TransactionInterceptorPlatformTransactionManager

  1. PlatformTransactionManager(事务管理器):
  • PlatformTransactionManager 是Spring事务的核心接口,定义了一系列管理事务的方法。各种数据源(JDBC、Hibernate、JPA等)都有对应的事务管理器实现。
  1. TransactionInterceptor(事务拦截器):
  • TransactionInterceptor 是一个AOP拦截器,在方法调用前后拦截目标方法,负责事务的开启、提交、回滚等操作。

事务的生命周期:

  1. 方法调用触发: 当一个使用@Transactional注解修饰的方法被调用时,AOP会拦截这个方法的调用。
  2. TransactionInterceptor处理: TransactionInterceptor捕获到方法调用,查找对应的事务管理器(PlatformTransactionManager)。
  3. 事务开启: 通过事务管理器开启一个事务,开始一个数据库连接(或者获取一个连接对象)。
  4. 方法执行: 目标方法被执行,在方法执行期间可能进行数据库操作。
  5. 提交或回滚: 方法执行完成后,根据方法的执行情况(异常或正常执行),TransactionInterceptor决定提交事务或者回滚事务。
  6. 事务关闭: 提交或回滚操作完成后,事务被关闭,数据库连接(或连接对象)被释放或归还。

7、Spring Boot的Starter的底层原理

Spring Boot的Starter本质上是一种约定俗成的依赖聚合体系,它的底层原理主要包括以下几个方面:

  1. 自动配置(Auto-Configuration):
  • Starter通常包含自动配置类(auto-configuration),这些类是实现自动配置的核心。这些类基于条件化加载(Conditional Loading)机制,在启动过程中根据类路径上的情况自动判断是否需要启用特定的配置。
  1. Spring Boot的启动机制:
  • 在Spring Boot应用启动过程中,会扫描类路径上的所有Starter,Spring Boot Starter的命名遵循一定的约定,比如以spring-boot-starter-*为前缀。当引入这些Starter时,Spring Boot会自动装配相应的配置。
  1. 配置元数据和条件化配置:
  • Starter中通常包含META-INF/spring.factories文件,该文件指定了自动配置类。自动配置类使用条件化配置(例如@ConditionalOnClass@ConditionalOnProperty等注解)来根据特定的条件来决定是否应用这些配置。
  1. Starter依赖的传递性:
  • Starter可能依赖其他Starter,形成了一种依赖传递的链条。比如,spring-boot-starter-web可能依赖于spring-boot-starter-tomcat,这样在引入spring-boot-starter-web时会自动引入spring-boot-starter-tomcat
  1. 约定优于配置:
  • Spring Boot Starter遵循“约定优于配置”的原则,通过一系列的约定、自动配置和条件化加载,帮助开发者快速启动一个具备某些功能特性的Spring Boot应用程序,无需手动配置大量的依赖和属性。

8、HSF和Dubbo的区别

HSF(High-Speed Service Framework)和Dubbo都是阿里巴巴在分布式服务领域的开源框架,用于构建分布式服务架构。它们有一些相似之处,但也有一些明显的区别:

Dubbo:

  1. 通信协议: Dubbo采用自定义的RPC通信协议,默认使用基于Netty的NIO异步通信。
  2. 服务注册中心: Dubbo提供了多种服务注册中心的支持,包括Zookeeper、Redis等,用于服务的注册与发现。
  3. 协议支持: Dubbo支持多种RPC协议,如Dubbo自定义协议、HTTP、WebService等。
  4. 高性能: Dubbo强调高性能和高效率,在阿里巴巴内部得到广泛应用,具有良好的稳定性和可靠性。
  5. 社区生态: Dubbo拥有活跃的社区支持,有大量的扩展插件和开发者社区。

HSF:

  1. 基于SOFA框架: HSF基于阿里巴巴的SOFA框架,它是一种分布式服务框架,支持高性能RPC调用。
  2. 服务治理: HSF提供了丰富的服务治理功能,包括服务降级、熔断、路由、监控等,用于保障服务的稳定性和可靠性。
  3. 端到端的支持: HSF提供端到端的解决方案,包括服务端和客户端两方面的支持。
  4. 全链路追踪: HSF提供全链路的追踪和监控功能,能够实现服务调用链的可视化监控。
  5. 基于协议: HSF使用了Hessian协议和Netty作为底层通信组件。

区别:

  • 架构设计: Dubbo和HSF的架构设计有所不同,Dubbo更注重通信协议的扩展性和灵活性,而HSF更关注全链路的服务治理和端到端的服务支持。
  • 协议和底层通信: Dubbo提供了更多的通信协议选择,而HSF在协议上相对固定,更专注于Hessian协议和Netty。
  • 服务治理和监控: HSF在服务治理、监控和追踪方面提供了更全面的解决方案,而Dubbo相对较为简化。

9、HTTP1.1和2.0和websocket协议

HTTP/1.1、HTTP/2和WebSocket都是网络通信协议,它们有着不同的特点和应用场景。

HTTP/1.1:

  • 基于文本协议: HTTP/1.1是基于文本的协议,每次请求-响应都需要建立新的TCP连接,性能相对较低。
  • 无状态性: HTTP/1.1是无状态协议,每个请求都是独立的,服务器不会保留连接状态。
  • 头部阻塞: 请求和响应都是按照顺序进行,如果一个请求/响应出现延迟,会阻塞后续的请求/响应。

HTTP/2:

  • 二进制协议: HTTP/2采用二进制协议,使用二进制格式传输数据,而非HTTP/1.1的明文文本,降低了传输数据的大小。
  • 多路复用: 支持多路复用,即在同一个连接上同时进行多个请求和响应,避免了HTTP/1.1中的头部阻塞问题,提高了效率。
  • 头部压缩: 使用头部压缩技术,减少了头部大小,进一步提高了传输效率。

WebSocket:

  • 全双工通信: WebSocket是一种全双工通信协议,允许客户端和服务器之间进行双向实时通信。
  • 持久连接: 与HTTP不同,WebSocket在建立连接后保持持久连接,双方可以随时发送数据,避免了HTTP中频繁的建立和关闭连接的开销。

区别:

  • 性能: HTTP/2相比HTTP/1.1性能更好,具有多路复用、头部压缩等特性,提高了传输效率。
  • 通信方式: HTTP/1.1和HTTP/2是一种请求-响应式的通信方式,而WebSocket允许双向实时通信。
  • 持久连接: HTTP/1.1中每次请求-响应都需要重新建立连接,而WebSocket保持持久连接,可随时进行通信。

10、算法:一个集合找所有子集

生成一个集合的所有子集是一个经典的组合问题,可以使用递归或位运算的方法来实现。以下是两种常见的方法:

方法一:递归生成子集

代码语言:javascript
复制
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);
        }
    }
}

方法二:位运算生成子集

代码语言:javascript
复制
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);
        }
    }
}

11、算法:找到两个字符串的最长公共子序列并输出

可以使用动态规划来解决这个问题。找到两个字符串的最长公共子序列并输出:

代码语言:javascript
复制
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);
    }
}
  • 原文链接:https://github.com/warthecatalyst/What-to-in-Graduate-School/blob/main/%E7%A7%8B%E6%8B%9B%E7%9A%84%E9%9D%A2%E7%BB%8F/%E5%8D%8E%E7%A7%91%E8%AE%A1%E7%A7%91%E7%AC%AC%E4%BA%8C%E4%BA%BA%E7%9A%84%E7%A7%8B%E6%8B%9B%E6%8A%A5%E5%91%8A.md
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-12-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 千羽的编程时光 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、Java实现一个单例模式
  • 2、上面代码的 volatile 的作用是什么?底层原理说一下?
  • 3、说一下G1垃圾回收器
  • 4、Spring事务管理的方式有哪些
  • 5、Spring事务失效的场景,为什么会失效?
  • 6、Spring事务的底层原理说一下
  • 7、Spring Boot的Starter的底层原理
  • 8、HSF和Dubbo的区别
  • 9、HTTP1.1和2.0和websocket协议
  • 10、算法:一个集合找所有子集
  • 11、算法:找到两个字符串的最长公共子序列并输出
相关产品与服务
微服务引擎 TSE
微服务引擎(Tencent Cloud Service Engine)提供开箱即用的云上全场景微服务解决方案。支持开源增强的云原生注册配置中心(Zookeeper、Nacos 和 Apollo),北极星网格(腾讯自研并开源的 PolarisMesh)、云原生 API 网关(Kong)以及微服务应用托管的弹性微服务平台。微服务引擎完全兼容开源版本的使用方式,在功能、可用性和可运维性等多个方面进行增强。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档