300+ Java常见面试题总结【JavaPub版】

点赞再看,养成习惯

答案解析见文末

我是JavaPub,专注于面试、副业,技术人的成长记录。

这份【Java常见面试题总结】我想准备很久了,前面做面试官,后来自己也面了很多一线二线互联网公司,希望通过当下真实经历做一份面试题总结,助力金九银十,喜提心仪 Offer。

pub哥刚开始找工作时,觉得看面试题再找是作弊行为,不屑于刷题,干看技术教程。其实不然:

  1. 经过俩年多的工作看,面试中问到的,大多都是我们在实际工作中用到的一些硬知识或软知识;
  2. 每一次面试都是技术人之间的技术切磋,就和练武一样,都有助于技术和认知的提升;
  3. 知道的知识更多肯定不是一件坏事;
  4. 注意:最重要一点事,网络上大多数答案属于ctrlCV,有一些错误答案也被大量传播,致力于纠正一些错误。

适合人群

  • 准备有面试/晋升的程序员;
  • 希望夯实Java基础的程序员;
  • 团队Leader;
  • 准面试官。

面试题模块

目前打算从如下几个模块开始,包含 Java 基础,JVM,容器,注解,多线程,反射,异常,网络,对象拷贝,JavaWeb,设计模式,Spring-Spring MVC,Spring Boot / Spring Cloud,Mybatis / Hibernate,Kafka,RocketMQ,Zookeeper,MySQL,Redis,Elasticsearch,Lucene

面试题

下面一起来看看有哪些具体内容

文章目录

Java基础

http://www.bjpowernode.com/tutorial_baseinterviewquestions/243.html

1. instanceof 关键字的作用

instanceof 是 Java 的保留关键字。它的作用是测试它左边的对象是否是它右边的类的实例,返回 boolean 的数据类型。

boolean result = obj instanceof class 

当 obj 为 Class 的对象,或者是其直接或间接子类,或者是其接口的实现类,结果result 都返回 true,否则返回false。

注意一点:编译器会检查 obj 是否能转换成右边的class类型,如果不能转换则直接报错,如果不能确定类型,则通过编译,具体看运行时定。

obj 必须为引用类型,只能作为对象的判断,不能是基本类型。

int i = 0;
System.out.println(i instanceof Integer);//编译不通过
System.out.println(i instanceof Object);//编译不通过

源码参考:JavaSE 8 instanceof 的实现算法:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.instanceof

2. Java自动装箱和拆箱

什么是装箱拆箱,这里不做源码层面解读,源码解读在JavaPub公众号发出。这里通过讲解 int 和 Interger 区别,解答Java自动装箱和拆箱。

自动装箱 ----- 基本类型的值 → 包装类的实例

自动拆箱 ----- 基本类型的值 ← 包装类的实例

  1. Integer变量必须实例化后才能使用,而int变量不需要
  2. Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值 。
  3. Integer的默认值是null,int的默认值是0

Java中8种基本数据类型。左边基本类型,右边包装类型。
在这里插入图片描述

在面试中:

下面这段代码的输出结果是什么?

public class Main {
    public static void main(String[] args) {
         
        Integer i1 = 100;
        Integer i2 = 100;
        Integer i3 = 200;
        Integer i4 = 200;
         
        System.out.println(i1==i2);
        System.out.println(i3==i4);
    }
}
 
//true
//false

输出结果表明i1和i2指向的是同一个对象,而i3和i4指向的是不同的对象。此时只需一看源码便知究竟,下面这段代码是Integer的valueOf方法的具体实现:

public static Integer valueOf(int i) {
        if(i >= -128 && i <= IntegerCache.high)
            return IntegerCache.cache[i + 128];
        else
            return new Integer(i);
}
private static class IntegerCache {
        static final int high;
        static final Integer cache[];
 
        static {
            final int low = -128;
 
            // high value may be configured by property
            int h = 127;
            if (integerCacheHighPropValue != null) {
                // Use Long.decode here to avoid invoking methods that
                // require Integer's autoboxing cache to be initialized
                int i = Long.decode(integerCacheHighPropValue).intValue();
                i = Math.max(i, 127);
                // Maximum array size is Integer.MAX_VALUE
                h = Math.min(i, Integer.MAX_VALUE - -low);
            }
            high = h;
 
            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);
        }
 
        private IntegerCache() {}
    }

从这2段代码可以看出,在通过valueOf方法创建Integer对象的时候,如果数值在[-128,127]之间,便返回指向IntegerCache.cache中已经存在的对象的引用;否则创建一个新的Integer对象。

上面的代码中i1和i2的数值为100,因此会直接从cache中取已经存在的对象,所以i1和i2指向的是同一个对象,而i3和i4则是分别指向不同的对象。

注意,Integer、Short、Byte、Character、Long这几个类的valueOf方法的实现是类似的。 Double、Float的valueOf方法的实现是类似的(没有缓存数值,这里的数值想想都有很多,不适合缓存)。


3. 重载和重写区别

重载和重写是一个特别好理解的概念,这里说一个通俗的解答方式

重载(Overload):首先是位于一个类之中或者其子类中,具有相同的方法名,但是方法的参数不同,返回值类型可以相同也可以不同。

  1. 方法名必须相同
  2. 方法的参数列表一定不一样。
  3. 访问修饰符和返回值类型可以相同也可以不同。

其实简单而言:重载就是对于不同的情况写不同的方法。 比如,同一个类中,写不同的构造函数用于初始化不同的参数。

public class JavaPubTest {
    public void out(){
        System.out.println("参数"+null);
    }
    //参数数目不同
    public void out(Integer n){
        System.out.println("参数"+n.getClass().getName());
    }
 
    //参数类型不同
    public void out(String string){
        System.out.println("参数"+string.getClass().getName());
    }
 
    public void out(Integer n ,String string){
        System.out.println("参数"+n.getClass().getName()+","+string.getClass().getName());
    }
    //参数顺序不同
    public void out(String string,Integer n){
        System.out.println("参数"+string.getClass().getName()+","+n.getClass().getName());
    }
    
    public static void main(String[] args) {
        JavaPubTest javaPubTest = new JavaPubTest();
        javaPubTest.out();
        javaPubTest.out(1);
        javaPubTest.out("string");
        javaPubTest.out(1,"string");
        javaPubTest.out("string",1);
    }
}

**重写(Overriding)**发生在父类子类之间,比如所有类都是继承与Object类的,Object类中本身就有equals、hashcode、toString方法等。在任意子类中定义了重名和同样的参数列表就构成方法重写。

  1. 方法名必须相同,返回值类型必须相同。
  2. 参数列表必须相同。
  3. 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为public,那么在子类中重写该方法就不能声明为protected。
  4. 子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为private和final的方法。
  5. 构造方法不能被重写。

4. equals与==区别

"=="是判断两个变量或实例是不是指向同一个内存空间。
"equals"是判断两个变量或实例所指向的内存空间的值是不是相同。

除了这俩点,这个问题大概率会引出以下问题:

为什么重写equals还要重写hashcode?

通过上面俩条我们知道 "equals"是判断两个变量或实例所指向的内存空间的值是不是相同。 但是一些特殊场景,我们需要对比俩个对象是否相等,例如:User user1 = new User(); User user2 = new User(); user1 和 user2 对比。这是我们就需要重写 equals 方法。

所以可以通过重写equals()方法来判断对象的值是否相等,但是有一个要求:equals()方法实现了等价关系,即:

  • 自反性:对于任何非空引用x,x.equals(x)应该返回true;
  • 对称性:对于任何引用x和y,如果x.equals(y)返回true,那么y.equals(x)也应该返回true;
  • 传递性:对于任何引用x、y和z,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也应该返回true;
  • 一致性:如果x和y引用的对象没有发生变化,那么反复调用x.equals(y)应该返回同样的结果;
  • 非空性:对于任意非空引用x,x.equals(null)应该返回false;

到这里也是一个很正常的操作,但是当我们要用到 HashSet 等集合时。存储的对象我们需要用 hashcode 判断对象是否存在,如果使用 Object 默认的hashcode方法,那我们同样属性的俩个用户一定是不相等的(例如下面user3、user4),因为内存地址不同,这并不符合我们的业务,所以决定了重写 hashcode 的必要性。

User user3 = new User("JavaPub", "man", "1996-08-28")
User user4 = new User("JavaPub", "man", "1996-08-28")

5. 谈谈NIO和BIO区别

致力于大白话说清楚。NIO和BIO是一个相对有点抽象的概念,如果你对网络有点了解,理解起来可能会更顺畅。首先说一下基本

BIO:同步阻塞IO,每一个客户端连接,服务端都会对应一个处理线程,对于没有分配到处理线程的连接就会被阻塞或者拒绝。相当于是一个连接一个线程

NIO:同步非阻塞IO,基于Reactor模型,客户端和channel进行通信,channel可以进行读写操作,通过多路复用器selector来轮询注册在其上的channel,而后再进行IO操作。这样的话,在进行IO操作的时候再用一个线程去处理就可以了,也就是一个请求一个线程

Reactor模型是什么?

  1. 基于池化思想,避免为每个连接创建线程,连接完成后将业务处理交给线程池处理
  2. 基于IO复用模型,多个连接共用同一个阻塞对象,不用等待所有的连接。遍历到有新数据可以处理时,操作系统会通知程序,线程跳出阻塞状态,进行业务逻辑处理
    .

简单来说:Reactor线程模型的思想就是基于IO复用和线程池的结合。

AIO:(一般都会把AIO和NIO、BIO放一块比较,这里简单提一下。)异步非阻塞IO,相比NIO更进一步,完全由操作系统来完成请求的处理,然后通知服务端开启线程去进行处理,因此是一个有效请求一个线程

那么怎么理解同步和阻塞?

首先,可以认为一个IO操作包含两个部分:

  1. 发起IO请求
  2. 实际的IO读写操作

同步和异步在于第二个,实际的IO读写操作,如果操作系统帮你完成了再通知你,那就是异步,否则都叫做同步。

阻塞和非阻塞在于第一个,发起IO请求,对于NIO来说通过channel发起IO操作请求后,其实就返回了,所以是非阻塞。

NIO和BIO是非常重要的计算机知识,学习后会对整个计算机的理解更近一步,一次学会终身受益。JavaPub会单独写一篇深入图解NIO和BIO。

网上看到一个例子(一定要看,会对你有所帮助):

一辆从 A 开往 B 的公共汽车上,路上有很多点可能会有人下车。司机不知道哪些点会有哪些人会下车,对于需要下车的人,如何处理更好?   

1. 司机过程中定时询问每个乘客是否到达目的地,若有人说到了,那么司机停车,乘客下车。 ( 类似阻塞式 )   

2. 每个人告诉售票员自己的目的地,然后睡觉,司机只和售票员交互,到了某个点由售票员通知乘客下车。 ( 类似非阻塞 )     

很显然,每个人要到达某个目的地可以认为是一个线程,司机可以认为是 CPU 。在阻塞式里面,每个线程需要不断的轮询,上下文切换,以达到找到目的地的结果。而在非阻塞方式里,每个乘客 ( 线程 ) 都在睡觉 ( 休眠 ) ,只在真正外部环境准备好了才唤醒,这样的唤醒肯定不会阻塞。

建议阅读:
https://www.cnblogs.com/aspirant/p/6877350.html
https://www.cnblogs.com/shoshana-kong/p/11228555.html


6. String、StringBuffer、StringBuilder 的区别是什么?

String是Immutable类的典型实现,被声明为 final class,除了hash这个属性其它属性都声明为final。它的不可变性,所以例如拼接字符串时候会产生很多无用的中间对象,如果频繁的进行这样的操作对性能有所影响。

StringBuffer、StringBuilder就是解决String的这个性能问题。

StringBuffer 是线程安全的,本质是一个线程安全的可修改的字符序列,把所有修改数据的方法都加上synchronized。

StringBuffer 线程不安全,但是性能更好。


7. 泛型是什么,有什么特点

泛型在编码中有非常广泛的使用(jdk5引入),你一定经常能见到类似这种写法 <T>

泛型提供了编译时类型安全检测机制,允许在编译时检测到非法的类型。本质是参数化类型。

  1. 把类型当作是参数一样传递
  2. <数据类型>只能是引用类型

泛型:就是一种不确定的数据类型。

泛型的好处:

  1. 省略了强转的代码。
  2. 可以把运行时的问题提前到编译时期。

引入泛型主要想实现一个通用的、可以处理不同类型的方法

泛型擦除:

泛型时提供给javac编译器使用的,用于限定集合的输入类型,让编译器在源代码级别上,避免向集合中插入非法数据。但编译器编译完带有泛型的java程序后,生成的class文件中不再带有泛型信息,以此使程序运行效率不受影响,这个过程称为擦除。

JVM并不知道泛型的存在,因为泛型在编译阶段就已经被处理成普通的类和方法; 处理机制是通过类型擦除,擦除规则:

  • 若泛型类型没有指定具体类型,用Object作为原始类型;

  • 若有限定类型< T exnteds XClass >,使用XClass作为原始类型;

  • 若有多个限定< T exnteds XClass1 & XClass2 >,使用第一个边界类型XClass1作为原始类型;


8. final 有哪些用法

final关键字有四个常见用法。

final修饰一个类

当 final 关键字用来修饰一个类的时候,表明这个类不能有任何的子类,也就是说这个类不能被继承。

final类中的所有成员方法都会被隐式地指定为final方法,也就是说一个类如果是final的,那么其中所有的成员方法都无法进行覆盖重写。

     public final class 类名称 {

        // ...

      }

final修饰一个方法

当 final 关键字用来修饰一个方法的时候,这个方法就是最终方法,也就是不能被覆盖重写。

    修饰符 final 返回值类型 方法名称(参数列表) {

       // 方法体

     }

注意:对于类、方法来说,abstract 关键字和 final 关键字不能同时使用,因为矛盾。

final修饰一个局部变量

一旦使用 final 用来修饰局部变量,那么这个变量就不能进行更改「一次赋值,终生不变」。

  1. 对于基本类型来说,不可变说的是变量当中的数据不可改变;
  2. 对于引用类型来说,不可变说的是变量当中的地址值不可改变。

final修饰一个成员变量

对于成员变量来说,如果使用 final 关键字修饰,那么这个变量也照样是不可变。

  1. 由于成员变量具有默认值,所以用了 final 之后必须手动赋值,不会再给默认值了;
  2. 对于 final 的成员变量,要么使用直接赋值,要么通过构造方法赋值,必须二者选其一;
  3. 必须保证类当中所有重载的构造方法都最终会对 final 的成员变量进行赋值。

9. 说一下Java注解

在Java编程中,注解非常常见,注解的本质是什么?

注解大致分为以下三种:

  1. Java原生注解 如@Override,@Deprecated 等。大多用于 [标记] 和 [检查] 。

  2. 第三方注解,如 Spring、Mybatis等定义的注解(@Controller,@Data)。

  3. 自定义注解。

Java原生除了提供基本注解,还提供了 meta-annotation(元注解)。这些类型和它们所支持的类在java.lang.annotation包中可以找到。

  1. @Target
  2. @Retention
  3. @Documented
  4. @Inherited

在这里插入图片描述

一般比较常用的有 @Target,@Retention。@Target表示这个注解可以修饰那些地方(比如类、方法、成员变量),@Retention 主要是设置注解的生命周期。

这是你一定会被问,

  1. 有使用过注解吗?
  2. 你是怎么使用的?

注解有一个非常常见的使用场景,大家可以用这个来理解学习。

场景一:自定义注解+拦截器 实现登录校验

实现功能:

接下来,我们使用springboot拦截器实现这样一个功能,如果方法上加了@LoginRequired,则提示用户该接口需要登录才能访问,否则不需要登录。

首先定义一个LoginRequired注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
	public @interface LoginRequired {
    
}

然后写两个简单的接口,访问sourceA,sourceB资源

@RestController
public class IndexController {

    @GetMapping("/sourceA")
    public String sourceA(){
        return "你正在访问sourceA资源";
    }

    @GetMapping("/sourceB")
    public String sourceB(){
        return "你正在访问sourceB资源";
    }

}

很简单的俩个接口,没添加拦截器之前成功访问

在这里插入图片描述

实现 spring 的 HandlerInterceptor 类先实现拦截器,但不拦截,只是简单打印日志,如下:

public class SourceAccessInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("进入拦截器了");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

实现spring类 WebMvcConfigurer,创建配置类把拦截器添加到拦截器链中

@Configuration
public class InterceptorTrainConfigurer implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new SourceAccessInterceptor()).addPathPatterns("/**");
    }
}

拦截成功如下

在这里插入图片描述
在 sourceB 方法上添加我们的登录注解 @LoginRequired

@RestController
public class IndexController {

    @GetMapping("/sourceA")
    public String sourceA(){
        return "你正在访问sourceA资源";
    }

    @LoginRequired
    @GetMapping("/sourceB")
    public String sourceB(){
        return "你正在访问sourceB资源";
    }

}

简单实现登录拦截逻辑

	@Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("进入拦截器了");

        // 反射获取方法上的LoginRequred注解
        HandlerMethod handlerMethod = (HandlerMethod)handler;
        LoginRequired loginRequired = handlerMethod.getMethod().getAnnotation(LoginRequired.class);
        if(loginRequired == null){
            return true;
        }

        // 有LoginRequired注解说明需要登录,提示用户登录
        response.setContentType("application/json; charset=utf-8");
        response.getWriter().print("你访问的资源需要登录");
        return false;
    }

运行成功,访问sourceB时需要登录了,访问sourceA则不用登录。
在这里插入图片描述
在这里插入图片描述

场景二:自定义注解+AOP 实现日志打印

先导入切面需要的依赖包

<dependency>
      <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

定义一个注解@MyLog

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
    
}

定义一个切面类,见如下代码注释理解:

@Aspect // 1.表明这是一个切面类
@Component
public class MyLogAspect {

    // 2. PointCut表示这是一个切点,@annotation表示这个切点切到一个注解上,后面带该注解的全类名
    // 切面最主要的就是切点,所有的故事都围绕切点发生
    // logPointCut()代表切点名称
    @Pointcut("@annotation(com.javapub.blog.MyLog)")
    public void logPointCut(){};

    // 3. 环绕通知
    @Around("logPointCut()")
    public void logAround(ProceedingJoinPoint joinPoint){
        // 获取方法名称
        String methodName = joinPoint.getSignature().getName();
        // 获取入参
        Object[] param = joinPoint.getArgs();

        StringBuilder sb = new StringBuilder();
        for(Object o : param){
            sb.append(o + "; ");
        }
        System.out.println("进入[" + methodName + "]方法,参数为:" + sb.toString());

        // 继续执行方法
        try {
            joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        System.out.println(methodName + "方法执行结束");

    }
}

在步骤二中的IndexController写一个sourceC进行测试,加上我们的自定义注解:

    @MyLog
    @GetMapping("/sourceC/{source_name}")
    public String sourceC(@PathVariable("source_name") String sourceName){
        return "你正在访问sourceC资源";
    }

启动springboot web项目,输入访问地址

在这里插入图片描述
在这里插入图片描述

有些面试官喜欢问,注解三要素是哪些:

  1. 注解声明、
  2. 使用注解的元素、
  3. 操作注解使其起作用(注解处理器)

10. Java创建对象有几种方式

Java中有5种创建对象的方式,下面给出它们的例子

在这里插入图片描述
使用new关键字

User user = new User();

使用Class类的newInstance方法

我们也可以使用Class类的newInstance方法创建对象。这个newInstance方法调用无参的构造函数创建对象。

Employee emp = (Employee) Class.forName("org.javapub.blog.Employee").newInstance();
或者
Employee emp2 = Employee.class.newInstance();

使用Constructor类的newInstance方法

和Class类的newInstance方法很像, java.lang.reflect.Constructor类里也有一个newInstance方法可以创建对象。我们可以通过这个newInstance方法调用有参数的和私有的构造函数。

Constructor<Employee> constructor = Employee.class.getConstructor();
Employee emp3 = constructor.newInstance();

使用clone方法

无论何时我们调用一个对象的clone方法,jvm就会创建一个新的对象,将前面对象的内容全部拷贝进去。用clone方法创建对象并不会调用任何构造函数。

要使用clone方法,我们需要先实现Cloneable接口并实现其定义的clone方法。

Employee emp4 = (Employee) emp3.clone();

使用反序列化

当我们序列化和反序列化一个对象,jvm会给我们创建一个单独的对象。在反序列化时,jvm创建对象并不会调用任何构造函数。

为了反序列化一个对象,我们需要让我们的类实现Serializable接口

ObjectInputStream in = new ObjectInputStream(new FileInputStream("data.obj"));
Employee emp5 = (Employee) in.readObject();

JVM

https://blog.csdn.net/weixin_45081813/article/details/115312135

基于JDK8

1. 说一说JVM的主要组成部分

点击放大看,一图胜千文
jvm

  • 方法区和堆是所有线程共享的内存区域;而虚拟机栈、本地方法栈和程序计数器的运行是线程私有的内存区域,运行时数据区域就是我们常说的JVM的内存。

  • 类加载子系统:根据给定的全限定名类名(如:java.lang.Object)来装载class文件到运行时数据区中的方法区中。

  • Java堆是Java虚拟机所管理的内存中最大的一块,也是垃圾回收的主要区域。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

  • 方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

  • 程序计数器是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器,用来指示执行引擎下一条执行指令的地址。

  • Java虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、返回方法地址等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

  • 本地方法栈(Native Method Stacks),本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。

  • 执行引擎:根据程序计数器中存储的指令地址执行classes中的指令。

  • 本地接口:与本地方法库交互,是其它编程语言交互的接口。

2. 说一下 JVM 的作用?

首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。

3. 说一下堆栈的区别?

物理地址

堆的物理地址分配对象是不连续的。因此性能慢些。在GC的时候也要考虑到不连续的分配,所以有各种算法。比如,标记-消除,复制,标记-压缩,分代(即新生代使用复制算法,老年代使用标记——压缩)

栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。

内存分别

堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。一般堆大小远远大于栈。

栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。

存放的内容

堆存放的是对象的实例和数组。因此该区更关注的是数据的存储

栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。

PS:

静态变量放在方法区
静态的对象还是放在堆。
程序的可见度

堆对于整个应用程序都是共享、可见的。

栈只对于线程是可见的。所以也是线程私有。他的生命周期和线程相同。

4. Java内存泄漏

内存泄漏是指不再被使用的对象或者变量一直被占据在内存中。

严格来说,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏。

理论上来说,Java是有GC垃圾回收机制的,也就是说,不再被使用的对象,会被GC自动回收掉,自动从内存中清除。

但是,即使这样,Java也还是存在着内存泄漏的情况,java导致内存泄露的原因很明确:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景。

5. JVM 有哪些垃圾回收算法?

  • 标记-清除算法:标记有用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。
  • 复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半,消耗内存。
  • 标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
  • 分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。

6. 说一下 JVM 有哪些垃圾回收器?

在这里插入图片描述
在这里插入图片描述

7. 说一下类加载的执行过程?

  • 加载:根据查找路径找到相应的 class 文件然后装载入内存中;

在这里插入图片描述

  • 验证:检查加载的 class 文件的正确性;
  • 准备:给类中的静态变量分配内存空间;
  • 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
  • 初始化:对静态变量和静态代码块执行初始化工作。

8. 什么是双亲委派模型?为什么要使用双亲委派模型?

什么是双亲委派模型

  • 当需要加载一个类的时候,子类加载器并不会马上去加载,而是依次去请求父类加载器加载
  • 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
  • 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。

为什么要使用双亲委派模型

可以防止内存中出现多份同样的字节码,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,多个类加载器都去加载这个类到内存中,系统中将会出现多个不同的Object类,那么类之间的比较结果及类的唯一性将无法保证,而且如果不使用这种双亲委派模型将会给虚拟机的安全带来隐患。所以,要让类对象进行比较有意义,前提是他们要被同一个类加载器加载。

9. CMS垃圾清理的过程

在这里插入图片描述
CMS整个过程比之前的收集器要复杂,整个过程分为4个主要阶段,即初始标记阶段、并发标记阶段、重新标记阶段和并发清除阶段。(涉及STW的阶段主要是:初始标记 和 重新标记 stop-the-world)

  • 初始标记(Initial-Mark)阶段:在这个阶段中,程序中所有的工作线程都将会因为“stop-the-world”机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出 GC Roots 能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快。

  • 并发标记(Concurrent-Mark)阶段:从 Gc Roots 的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。

  • 重新标记(Remark)阶段:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。

  • 并发清除(Concurrent-Sweep)阶段:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的

10. 常用的 JVM 调优的参数都有哪些?

  • -XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
  • -XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
  • –XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
  • -XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
  • -XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
  • -XX:+PrintGC:开启打印 gc 信息;
  • -XX:+PrintGCDetails:打印 gc 详细信息。

JVM 调优-这个问题单独一篇


容器

  1. Java 容器都有哪些?
  2. Collection 和 Collections 有什么区别?
  3. List、Set、Map 之间的区别是什么?
  4. HashMap 和 Hashtable 有什么区别?
  5. 如何决定使用 HashMap 还是 TreeMap?
  6. 说一下 HashMap 的实现原理?
  7. 说一下 HashSet 的实现原理?
  8. ArrayList 和 LinkedList 的区别是什么?
  9. 如何实现数组和 List 之间的转换?
  10. ArrayList 和 Vector 的区别是什么?
  11. Array 和 ArrayList 有何区别?
  12. 在 Queue 中 poll()和 remove()有什么区别?
  13. 哪些集合类是线程安全的?
  14. 迭代器 Iterator 是什么?
  15. Iterator 怎么使用?有什么特点?
  16. Iterator 和 ListIterator 有什么区别?
  17. 怎么确保一个集合不能被修改?

异常

  1. 什么是Java中的异常?

  2. Java中的异常处理关键字是什么?

  3. Java中的检查型异常和非检查型异常有什么区别?

  4. error和exception有什么区别?

  5. final, finally, finalize的区别?

  6. Java 中什么是异常链?

  7. .try-catch-finally-return执行顺序?

  8. 列出常见的几种RunException?

  9. Java异常类的重要方法是什么?

  10. 你曾经自定义实现过异常吗?怎么写的?

  11. 你遇到过 OutOfMemoryError 错误嘛?你是怎么搞定的?

  12. 如果执行finally代码块之前方法返回了结果,或者JVM退出了,finally块中的代码还会执行吗?

注解

  1. 什么是注解?
  2. 他们的典型用例是什么(注解有哪些应用场景)?
  3. 你怎么能创建一个注解?
  4. 你用过哪些常用注解?
  5. 可以从注解方法声明返回哪些对象类型?
  6. 注解可以应用在什么程序位置?
  7. 有没有办法限制可以应用注解的元素?
  8. 什么是元注解?
  9. 什么是重复注解??
  10. 你怎么能检索注解???这与保留政策有何关系?
  11. 是否可以扩展注解?
  12. 如何获取注解属性?

多线程

  1. 什么是线程?

  2. 线程和进程有什么区别?

  3. 如何在Java中实现线程?

  4. 用Runnable还是Thread?

  5. Thread 类中的start() 和 run() 方法有什么区别?

  6. Java中Runnable和Callable有什么不同?

  7. Java中CyclicBarrier 和 CountDownLatch有什么不同?

  8. Java内存模型是什么?

  9. Java中的volatile 变量是什么?

  10. 什么是线程安全?Vector是一个线程安全类吗? (详见这里)

  11. Java中什么是竞态条件? 举个例子说明。

  12. Java中如何停止一个线程?

  13. 一个线程运行时发生异常会怎样?

  14. 如何在两个线程间共享数据?

  15. Java中notify 和 notifyAll有什么区别?

  16. 为什么wait, notify 和 notifyAll这些方法不在thread类里面?

  17. 什么是ThreadLocal变量?

  18. 什么是FutureTask?

  19. Java中interrupted 和 isInterruptedd方法的区别?

  20. 为什么wait和notify方法要在同步块中调用?

  21. 为什么你应该在循环中检查等待条件?

  22. Java中的同步集合与并发集合有什么区别?

  23. Java中堆和栈有什么不同?

  24. 什么是线程池? 为什么要使用它?

  25. 如何写代码来解决生产者消费者问题?

  26. 如何避免死锁?

  27. Java中活锁和死锁有什么区别?

  28. 怎么检测一个线程是否拥有锁?

  29. 你如何在Java中获取线程堆栈?

  30. JVM中哪个参数是用来控制线程的栈堆栈小的

  31. Java中synchronized 和 ReentrantLock 有什么不同?

  32. 有三个线程T1,T2,T3,怎么确保它们按顺序执行?

  33. Thread类中的yield方法有什么作用?

  34. Java中ConcurrentHashMap的并发度是什么?

  35. Java中Semaphore是什么?

  36. 如果你提交任务时,线程池队列已满。会时发会生什么?

  37. Java线程池中submit() 和 execute()方法有什么区别?

  38. 什么是阻塞式方法?

  39. Swing是线程安全的吗? 为什么?

  40. Java中invokeAndWait 和 invokeLater有什么区别?

  41. Swing API中那些方法是线程安全的?

  42. 如何在Java中创建Immutable对象?

  43. Java中的ReadWriteLock是什么?

  44. 多线程中的忙循环是什么?

  45. volatile 变量和 atomic 变量有什么不同?

  46. 如果同步块内的线程抛出异常会发生什么?

  47. 单例模式的双检锁是什么?

  48. 如何在Java中创建线程安全的Singleton?

  49. 如何强制启动一个线程?

  50. Java中的fork join框架是什么?

  51. Java多线程中调用wait() 和 sleep()方法有什么不同?

  52. 在实际开发中用过多线程吗,具体怎么用的,解决什么问题?

  53. 你对java的内存模型有了解吗?

  54. 线程之间怎么通信?

  55. 什么是线程安全,怎么理解的?

  56. 线程之间如何共享数据?

  57. 线程的start()和run()方法的区别?

  58. 实现线程的方式分别是什么?

  59. 怎么获取线程的返回值?

  60. volatile关键字的作用是什么?

  61. CyclicBarrier(篱栅)和CountDownLatch(倒计时锁存器)的区别是什么?

  62. volatile(不稳定的)和synchronized(同步的)对比?

  63. 怎么唤醒一个阻塞的线程?

  64. Java中如何获取到线程dump文件?

  65. sleep方法和wait方法的相同点和不同点?

  66. 生产者和消费者模型的作用是什么?

  67. ThreadLocal有什么作用?

  68. wait方法和notify/notifyAll方法在放弃对象监视器时有什么区别?

  69. Lock和synchronized对比?

  70. ConcurrentHashMap的并发度是什么?

  71. ReadWriteLock是什么?

  72. 不可变对象对多线程有什么帮助

  73. FutureTask是什么?

  74. Java中用到的线程调度算法是什么?

  75. 怎么得到一个线程安全的单例模式?

  76. 什么是乐观锁和悲观锁?

  77. Java编写一个会导致死锁的程序?

  78. 如何确保N个线程可以同时访问N个资源又不导致死锁?

  79. Thread.sleep(0)的作用是什么?

  80. 高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?

  81. 同步方法和同步块,哪个是更好的选择,你怎么看?

  82. 线程类的构造方法、静态块是被哪个线程调用的

  83. Hashtable的size()方法中明明只有一条语句”return count”,为什么还要做同步?

  84. 多线程有什么用?

  85. Semaphore有什么作用,信号量是什么?

  86. 一个线程如果出现了运行时异常会怎么样?

  87. 为什么使用线程池?

  88. synchronized和ReentrantLock的区别

  89. 什么是多线程的上下文切换

  90. 如果你提交任务时,线程池队列已满,这时会发生什么

  91. 什么是CAS?

  92. 什么是AQS?

  93. notify和notifyAll的区别?

  94. 你是如何合理配置线程池大小的?

  95. 线程池具体配置参数?

  96. 线程池按以下行为执行任务:

1. start()方法和run()方法的区别

如果只是调用 run() 方法,那么代码还是同步执行的,必须等待一个线程的run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其run()方法里面的代码。

只有调用了start()方法,才会表现出多线程的特性,不同线程的run()方法里面的代码交替执行。

参考图:

在这里插入图片描述


2. volatile关键字的作用

volatile 英 [ˈvɒlətaɪl] ,第一个想到的一定是保证内存可见性(Memory Visibility)。可见性是性对于线程而言。

在这里插入图片描述
上图是Java内存模型,所有线程的共享变量都放在主内存中,每一个线程都有一个独有的工作内存,每个线程不直接操作在主内存中的变量,而是将主内存上变量的副本放进自己的工作内存中,只操作工作内存中的数据。当修改完毕后,再把修改后的结果放回到主内存中。每个线程都只操作自己工作内存中的变量,无法直接访问对方工作内存中的变量,线程间变量值的传递需要通过主内存来完成。

很明显,在并发环境下一定会发生脏数据问题。

使用volatile变量能够保证:

  1. 每次读取前必须先从主内存刷新最新的值。
  2. 每次写入后必须立即同步回主内存当中。

也就是说,volatile关键字修饰的变量看到的随时是自己的最新值。

防止指令重排

在基于偏序关系的Happens-Before内存模型中,指令重排技术大大提高了程序执行效率。但是也引入一个新问题:被部分初始化的对象

例子:

创建一个对象
instance = new Singleton();

它并不是一个原子操作。事实上,它可以”抽象“为下面几条JVM指令:

memory = allocate();    //1:分配对象的内存空间
initInstance(memory);   //2:初始化对象
instance = memory;      //3:设置instance指向刚分配的内存地址

上面操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM可以以“优化”为目的对它们进行重排序,经过重排序后如下:

memory = allocate();    //1:分配对象的内存空间
instance = memory;      //3:设置instance指向刚分配的内存地址(此时对象还未初始化)
initInstance(memory);   //2:初始化对象

可以看到指令重排之后,操作 3 排在了操作 2 之前,即引用instance指向内存memory时,这段崭新的内存还没有初始化。由于instance已经指向了一块内存空间,从而返回 instance!=null,用户得到了没有完成初始化的“半个”单例。

但是有一点:volatile不保证原子性。

这里有一篇生产环境使用volatile的例子:https://mp.weixin.qq.com/s/s1cwut9WvUSrMYw_6UK3sg


3. sleep方法和wait方法有什么区别

要了解sleep和wait,首先需要了解Java线程的6种状态。

#下面是Java线程的6种状态
1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
2. 运行(RUNNABLE)Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
3. 阻塞(BLOCKED):表示线程阻塞于锁。
4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
6. 终止(TERMINATED):表示该线程已经执行完毕。

sleep 休眠方法

static void sleep(long ms)

该方法会使当前线程进入阻塞状态指定毫秒,当阻塞指定毫秒后,当前线程会重新进入Runnable状态,等待划分时间片。

sleep方法属于Thread类中方法,表示让一个线程进入睡眠状态,等待一定的时间之后,自动醒来进入到可运行状态,不会马上进入运行状态,因为线程调度机制恢复线程的运行也需要时间,一个线程对象调用了sleep方法之后,并不会释放他所持有的所有对象锁,所以也就不会影响其他进程对象的运行。

wait 方法一般是跟notify方法连用的

多线程之间需要协调工作。如果条件不满足则等待。当条件满足时,等待该条件的线程将被唤醒。在Java中,这个机制实现依赖于wait/notify或wait/notifyAll。

object.wait()让当前线程进入不可运行状态,如sleep()一样,但不同的是wait方法从一个对象调用,而不是从一个线程调用;我们称这个对象为“锁定对象(lockObj)”。在lockObj.wait()被调用之前,当前线程必须在lockObj上同步(synchronize);然后调用wait()后释放这个锁,并将线程增加到与lockObj相关的“等待名单(wait list)”。然后,另一个在同一个lockObj锁定(synchronize)的方法可以调用lockObj.nofity()。这会唤醒原来等待的线程。基本上,wait() / notify()就像sleep() / interrupt(),只是活动线程不需要直接指向一个睡眠线程,他们只需要共享锁对象(lockObj)。

到这里你是否明白这个问题,如果不明白来JavaPub,后续一篇代码分析,马上安排。


4. 如何停止一个正在运行的线程?

最直观的一定是 Thread.stop,但是它是不推荐的,并且已经废弃。看一下官方说明
https://docs.oracle.com/javase/8/docs/technotes/guides/concurrency/threadPrimitiveDeprecation.html

*   	This method is inherently unsafe.   Stopping a thread with 
*       Thread. stop causes it to unlock all of the monitors that it 
*       has locked (as a natural consequence of the unchecked 
*       <code>ThreadDeath</code> exception propagating up the stack).   If 
*       any of the objects previously protected by these monitors were in 
*       an inconsistent state, the damaged objects become visible to 
*       other threads, potentially resulting in arbitrary behavior.   Many 
*       uses of <code>stop</code> should be replaced by code that simply 
*       modifies some variable to indicate that the target thread should 
*       stop running.   The target thread should check this variable 
*       regularly, and return from its run method in an orderly fashion 
*       if the variable indicates that it is to stop running.   If the 
*       target thread waits for long periods (on a condition variable, 
*       for example), the <code>interrupt</code> method should be used to 
*       interrupt the wait.

小结:
简单来说,Thread.stop()不安全,已不再建议使用。

方法一:

使用 interrupt 方法中断线程。

interrupt()方法的使用效果并不像for+break语句那样,马上就停止循环。调用interrupt方法是在当前线程中打了一个停止标志,并不是真的停止线程。

需要 this.isInterrupted(): 测试线程是否真的已经中断。

方法二:

最好的一种方法,使用标志位停止。

run() 方法中做标识符,保证优雅的停止服务。


5. java如何实现多线程之间的通讯和协作?(如何在两个线程间共享数据?)

volatile关键字方式

volatile有两大特性,一是可见性,二是有序性,禁止指令重排序,其中可见性就是可以让线程之间进行通信。

等待/通知机制

等待通知机制是基于wait和notify方法来实现的,在一个线程内调用该线程锁对象的wait方法,线程将进入等待队列进行等待直到被通知或者被唤醒。

也就是通过等待/通知机制 让多个线程协作

join方式

join其实合理理解成是线程合并,当在一个线程调用另一个线程的join方法时,当前线程阻塞等待被调用join方法的线程执行完毕才能继续执行,所以join的好处能够保证线程的执行顺序,但是如果调用线程的join方法其实已经失去了并行的意义,虽然存在多个线程,但是本质上还是串行的,最后join的实现其实是基于等待通知机制的。

threadLocal方式

threadLocal方式的线程通信,不像以上三种方式是多个线程之间的通信,它更像是一个线程内部的通信,将当前线程和一个map绑定,在当前线程内可以任意存取数据,减省了方法调用间参数的传递。


6. 什么是ThreadLocal?

定义:线程局部变量是局限于线程内的变量,属于线程自身所有,不在多个线程间共享。java提供 ThreadLocal类 来支持线程局部变量,是一个实现线程安全的方式。

作用:ThreadLocal 是一种以空间换时间的做法,在每一个 Thread 里面维护了一个 ThreadLocal.ThreadLocalMap 把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了。


7. Java 中 CountDownLatch 和 CyclicBarrier 有什么不同?

概念:

CountDownLatch 是一个同步的辅助类,允许一个或多个线程,等待其他一组线程完成操作,再继续执行。简单来说:CountDownLatch 是一个计数器,可以保证线程之间的顺序执行把线程从并发状态调整为串行状态保证了线程的执行顺序。(只可以使用一次)

CyclicBarrier 是一个同步的辅助类,允许一组线程相互之间等待,达到一个共同点,再继续执行。典型场景:可以用于多线程计算数据,最后合并计算结果。(可以多次使用)

分享一个直观的代码:

package com.javapub.test;

import java.util.concurrent.CountDownLatch;

/**
 * @Author: JavaPub
 * @License: https://github.com/Rodert/
 * @Contact: https://javapub.blog.csdn.net/
 * @Date: 2022/1/1 16:50
 * @Version: 1.0
 * @Description: countDownLatch 可以保证线程之间的顺序执行把线程从并发状态调整为串行状态保证了线程的执行顺序。
 * demo效果:当打印完B,再打印C。
 */

class ThreadA extends Thread {

    private CountDownLatch down;

    public ThreadA(CountDownLatch down) {
        this.down = down;
    }

    @Override
    public void run() {
        System.out.println("A");
        try {
            down.await();//相当于wait(),调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("C");
    }
}

class ThreadB extends Thread {
    private CountDownLatch down;

    public ThreadB(CountDownLatch down) {
        this.down = down;
    }

    @Override
    public void run() {
        System.out.println("B");
        System.out.println(down.getCount());
        down.countDown();//将count值减1
    }
}

public class Test {
    public static void main(String[] args) {
        CountDownLatch down = new CountDownLatch(1);//创建1个计数器
        new ThreadA(down).start();
        new ThreadB(down).start();
    }
}

/*输出
A
B
C
 */
package com.roundyuan.fanggateway.test;

import java.util.concurrent.CyclicBarrier;

/**
 * @Author: JavaPub
 * @License: https://github.com/Rodert/
 * @Contact: https://javapub.blog.csdn.net/
 * @Date: 2022/1/2 13:42
 * @Version: 1.0
 * @Description: CyclicBarrier
 */

public class CyclicBarrierDemo {

    static class TaskThread extends Thread {

        CyclicBarrier barrier;

        public TaskThread(CyclicBarrier barrier) {
            this.barrier = barrier;
        }

        @Override
        public void run() {
            try {
                Thread.sleep(1000);
                System.out.println(getName() + " 到达栅栏 A");
                barrier.await();
                System.out.println(getName() + " 冲破栅栏 A");

                Thread.sleep(2000);
                System.out.println(getName() + " 到达栅栏 B");
                barrier.await();
                System.out.println(getName() + " 冲破栅栏 B");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        int threadNum = 5;
        CyclicBarrier barrier = new CyclicBarrier(threadNum, new Runnable() {

            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " 完成最后任务");
            }
        });

        for (int i = 0; i < threadNum; i++) {
            new TaskThread(barrier).start();
        }
    }
}

/*
Thread-3 到达栅栏 A
Thread-1 到达栅栏 A
Thread-4 到达栅栏 A
Thread-2 到达栅栏 A
Thread-0 到达栅栏 A
Thread-2 完成最后任务
Thread-2 冲破栅栏 A
Thread-0 冲破栅栏 A
Thread-4 冲破栅栏 A
Thread-3 冲破栅栏 A
Thread-1 冲破栅栏 A
Thread-4 到达栅栏 B
Thread-0 到达栅栏 B
Thread-2 到达栅栏 B
Thread-1 到达栅栏 B
Thread-3 到达栅栏 B
Thread-3 完成最后任务
Thread-3 冲破栅栏 B
Thread-0 冲破栅栏 B
Thread-4 冲破栅栏 B
Thread-1 冲破栅栏 B
Thread-2 冲破栅栏 B
 */

网上看到一个比较形象一个例子:

CountDownLatch:
宿管阿姨,晚上关宿舍大门睡觉,需要等到所有学生回寝,才能关门睡觉,学生之间不用相互等待,回寝就能睡觉。	(学生就是各个线程,宿管阿姨就是监听CountDownLatch为0后要执行的。)

CyclicBarrier:
家庭聚餐,等待家庭成员到齐才能开饭,家庭成员之间需要相互等待,直到最后一个到达,才能同时开饭。

8. 如何避免死锁?


从上图我们就可以看出,产生死锁就是俩个或多个线程在申请资源时,自己需要的资源别别人持有、并阻塞。所以导致死锁。

如何解决:

  1. 减小锁的范围,尽量保证之锁定自己需要的资源,减小交叉持有资源情况
  2. 但是有些时候不得不持有多个资源,比如银行转账,我们必须同时获得两个账户上的锁,才能进行操作,两个锁的申请必须发生交叉。这时我们也可以打破死锁的那个闭环,在涉及到要同时申请两个锁的方法中,总是以相同的顺序来申请锁,比如总是先申请 id 大的账户上的锁 ,然后再申请 id 小的账户上的锁,这样就无法形成导致死锁的那个闭环。
  3. 我们知道导致死锁有一个因素是阻塞,所以如果我们不使用默认阻塞的锁,也是可以避免死锁的。我们可以使用 ReentrantLock.tryLock() 方法,在一个循环中,如果 tryLock() 返回失败,那么就释放以及获得的锁,并睡眠一小段时间。这样就打破了死锁的闭环。
package com.roundyuan.fanggateway.test;

import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author: JavaPub
 * @License: https://github.com/Rodert/
 * @Contact: https://javapub.blog.csdn.net/
 * @Date: 2022/1/2 14:38
 * @Version: 1.0
 * @Description: ReentrantLock
 */

public class DeadLock {

    private static Lock lock1 = new ReentrantLock();
    private static Lock lock2 = new ReentrantLock();

    public static void deathLock() {
        new Thread() {
            @Override
            public void run() {
                while (true) {
                    if (lock1.tryLock()) {
                        try {
                            //如果获取成功则执行业务逻辑,如果获取失败,则释放lock1的锁,自旋重新尝试获得锁
                            if (lock2.tryLock()) {
                                try {
                                    System.out.println("Thread1:已成功获取 lock1 and lock2 ...");
                                    break;
                                } finally {
                                    lock2.unlock();
                                }
                            }
                        } finally {
                            lock1.unlock();
                        }
                    }
                    System.out.println("Thread1:获取锁失败,重新获取---");
                    try {
                        //防止发生活锁
                        TimeUnit.NANOSECONDS.sleep(new Random().nextInt(100));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();

        new Thread() {
            @Override
            public void run() {
                while (true) {
                    if (lock2.tryLock()) {
                        try {
                            //如果获取成功则执行业务逻辑,如果获取失败,则释放lock2的锁,自旋重新尝试获得锁
                            if (lock1.tryLock()) {
                                try {
                                    System.out.println("Thread2:已成功获取 lock2 and lock1 ...");
                                    break;
                                } finally {
                                    lock1.unlock();
                                }
                            }
                        } finally {
                            lock2.unlock();
                        }
                    }
                    System.out.println("Thread2:获取锁失败,重新获取---");
                    try {
                        //防止发生活锁
                        TimeUnit.NANOSECONDS.sleep(new Random().nextInt(100));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            deathLock();
        }
    }
}

说起死锁,银行家算法非常有必要了解:

银行家算法(Banker’s Algorithm)是一个避免死锁(Deadlock)的著名算法,是由艾兹格·迪杰斯特拉在1965年为T.H.E系统设计的一种避免死锁产生的算法。它以银行借贷系统的分配策略为基础,判断并保证系统的安全运行。
1、操作系统按照银行家指定的规则为进程分配资源,当进程首次申请资源时,需要测试该进程对资源的最大需求量,如果系统现存的资源可以满足它的最大需求量则按当前的申请资源分配资源,否则就推迟分配;
2、当进程在执行中继续申请资源时,先测试该进程本次申请的资源数,是否超过了该资源剩余的总量,若超过则拒绝分配资源,若能满足则按当前的申请量分配资源,否则也要推迟分配。

参考阅读:

如何快速排查死锁?如何避免死锁?
https://zhuanlan.zhihu.com/p/136294283


9. Java 中 synchronized 和 ReentrantLock 有什么不同?

等待可中断:

使用synchronized,不能被中断。synchronized 也可以说是Java提供的原子性内置锁机制。内部锁扮演了互斥锁(mutual exclusion lock ,mutex)的角色,一个线程引用锁的时候,别的线程阻塞等待。

使用ReentrantLock。等待了很长时间以后,可以中断等待,转而去做别的事情。

公平锁:

公平锁是指多个线程在等待同一个锁时,必须按照申请的时间顺序来依次获得锁;而非公平锁则不能保证这一点。非公平锁在锁被释放时,任何一个等待锁的线程都有机会获得锁。 synchronized的锁是非公平锁,ReentrantLock默认情况下也是非公平锁,但可以通过带布尔值的构造函数要求使用公平锁。

还有大家已知的俩点:

  1. synchronized是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活。ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。

  2. synchronized可重入,因为加锁和解锁自动进行,不必担心最后是否释放锁;ReentrantLock也可重入,但加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁。


10. 有三个线程 T1,T2,T3,怎么确保它们按顺序执行?

方法1:

线程内部顺序调用,T1、T2、T3。这个可能不是要考察的点,但也是一个方案。

方法2:

join()方法用于将线程由 ”并行“变成”串行“,它用于等待其他线程的终止,在当前线程掉用了join()方法,那么当前线程将进入阻塞状态,等到另一个线程结束,当前线程再由阻塞状态转变成就绪状态,等待CPU的使用权。

package com.javapub.test;

/**
 * @Author: JavaPub
 * @License: https://github.com/Rodert/
 * @Contact: https://javapub.blog.csdn.net/
 * @Date: 2022/1/2 15:20
 * @Version: 1.0
 * @Description:
 */


public class Test1 {

    public static void main(String[] args) {
        ThreadA threadA = new ThreadA();
        ThreadB threadB = new ThreadB(threadA);
        ThreadC threadC = new ThreadC(threadB);

        threadA.start();
        threadB.start();
        threadC.start();

    }

}

class ThreadA extends Thread {
    @Override
    public void run() {
        System.out.println("线程A");
    }
}

class ThreadB extends Thread {
    Thread threadA;

    public ThreadB() {
        // TODO Auto-generated constructor stub
    }

    public ThreadB(Thread threadA) {
        this.threadA = threadA;
    }

    @Override
    public void run() {
        try {
            threadA.join();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println("线程B");
    }
}

class ThreadC extends Thread {
    Thread threadB;

    public ThreadC(Thread threadB) {
        this.threadB = threadB;
    }

    @Override
    public void run() {
        try {
            threadB.join();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println("线程C");
    }
}

信号量方式 java.util.concurrent.Semaphore 英 [ˈseməfɔː(r)]
待研究

JavaWeb

面对javaweb,如果是工作经验较年轻的工程师,必定会问到jsp、cookie、session、servlet

1. Cookie,Session的区别

  1. 当你的浏览器地址栏输入地址 并回车的一瞬间到页面能够展示出来,经历了什么?

3. 什么是 jsp,什么是 Servlet? jsp 和 Servlet 有什么区别?

jsp 本质上就是一个 Servlet,它是 Servlet 的一种特殊形式(由 SUN 公司推出),每个 jsp 页面都是一个 servlet
实例。
Servlet 是由 Java 提供用于开发 web 服务器应用程序的一个组件,运行在服务端,由 servlet 容器管理,用来生成动态内容。一个 servlet 实例是实现了特殊接口 Servlet 的 Java 类,所有自定义的 servlet 均必须实现 Servlet 接口。
区别:
jsp 是 html 页面中内嵌的 Java 代码,侧重页面显示;
Servlet 是 html 代码和 Java 代码分离,侧重逻辑控制, mvc 设计思想中 jsp 位于视图层, servlet 位 于控制层
Jsp 运行机制:如下图:

在这里插入图片描述

JVM 只能识别 Java 类,并不能识别 jsp 代码! web 容器收到以.jsp 为扩展名的 url 请求时,会将访问 请求交给 tomcat 中 jsp 引擎处理,每个 jsp 页面第一次被访问时, jsp 引擎将 jsp 代码解释为一个 servlet 源程 序,接着编译servlet 源程序生成.class 文件,再有 web 容器 servlet 引擎去装载执行 servlet 程序,实 现页面交互。

Spring

1. 什么是 Spring 框架?Spring 框架有哪些主要模块?

Spring是针对bean的生命周期进行管理的轻量级容器,一个控制反转和面向切面的容器框架

Spring有七大功能模块:

1、Core

Core模块是Spring的核心类库,Core实现了IOC功能。

2、AOP

Apring AOP模块是Spring的AOP库,提供了AOP(拦截器)机制,并提供常见的拦截器,供用户自定义和配置。

3、orm

提供对常用ORM框架的管理和支持,hibernate、mybatis等。

4、Dao

Spring提供对JDBC的支持,对JDBC进行封装。

5、Web

对Struts2的支持。

6、Context

Context模块提供框架式的Bean的访问方式,其它程序可以通过Context访问Spring的Bean资源,相当于资源注入。

7、MVC

MVC模块为spring提供了一套轻量级的MVC实现,即Spring MVC。

2. Spring IOC、AOP举例说明

这是一个基础问题,如果理解有难度、建议先读五遍。

1、IOC理论的背景

我们都知道,在采用面向对象方法设计的软件系统中,它的底层实现都是由N个对象组成的,所有的对象通过彼此的合作,最终实现系统的业务逻辑。

图1:软件系统中耦合的对象

如果我们打开机械式手表的后盖,就会看到与上面类似的情形,各个齿轮分别带动时针、分针和秒 针顺时针旋转,从而在表盘上产生正确的时间。图1中描述的就是这样的一个齿轮组,它拥有多个独立的齿轮,这些齿轮相互啮合在一起,协同工作,共同完成某项 任务。我们可以看到,在这样的齿轮组中,如果有一个齿轮出了问题,就可能会影响到整个齿轮组的正常运转。
齿轮组中齿轮之间的啮合关系,与软件系统中对象之间的耦合关系非常相似。对象之间的耦合关系是无法避免的,也是必要的,这是协同工作的基础。现在,伴随着 工业级应用的规模越来越庞大,对象之间的依赖关系也越来越复杂,经常会出现对象之间的多重依赖性关系,因此,架构师和设计师对于系统的分析和设计,将面临 更大的挑战。对象之间耦合度过高的系统,必然会出现牵一发而动全身的情形。

图2:对象之间复杂的依赖关系

耦合关系不仅会出现在对象与对象之间,也会出现在软件系统的各模块之间,以及软件系统和硬件系统之间。如何降低系统之间、模块之间和对象之间的耦合度,是软件工程永远追求的目标之一。为了解决对象之间的耦合度过高的问题,软件专家Michael Mattson提出了IOC理论,用来实现对象之间的“解耦”,目前这个理论已经被成功地应用到实践当中,很多的J2EE项目均采用了IOC框架产品Spring。

2、什么是控制反转(IoC)

IOC是Inversion of Control的缩写,多数书籍翻译成“控制反转”,还有些书籍翻译成为“控制反向”或者“控制倒置”。
1996年,Michael Mattson在一篇有关探讨面向对象框架的文章中,首先提出了IOC 这个概念。对于面向对象设计及编程的基本思想,前面我们已经讲了很多了,不再赘述,简单来说就是把复杂系统分解成相互合作的对象,这些对象类通过封装以 后,内部实现对外部是透明的,从而降低了解决问题的复杂度,而且可以灵活地被重用和扩展。IOC理论提出的观点大体是这样的:借助于“第三方”实现具有依 赖关系的对象之间的解耦,如下图:

图3:IOC解耦过程

大家看到了吧,由于引进了中间位置的“第三方”,也就是IOC容器,使得A、B、C、D这4个对象没有了耦合关系,齿轮之间的传动全部依靠“第三 方”了,全部对象的控制权全部上缴给“第三方”IOC容器,所以,IOC容器成了整个系统的关键核心,它起到了一种类似“粘合剂”的作用,把系统中的所有 对象粘合在一起发挥作用,如果没有这个“粘合剂”,对象与对象之间会彼此失去联系,这就是有人把IOC容器比喻成“粘合剂”的由来。
我们再来做个试验:把上图中间的IOC容器拿掉,然后再来看看这套系统:

图4:拿掉IoC容器后的系统

我们现在看到的画面,就是我们要实现整个系统所需要完成的全部内容。这时候,A、B、C、D这4个对象之间已经没有了耦合关系,彼此毫无联系,这样 的话,当你在实现A的时候,根本无须再去考虑B、C和D了,对象之间的依赖关系已经降低到了最低程度。所以,如果真能实现IOC容器,对于系统开发而言, 这将是一件多么美好的事情,参与开发的每一成员只要实现自己的类就可以了,跟别人没有任何关系!
我们再来看看,控制反转(IOC)到底为什么要起这么个名字?我们来对比一下:
软件系统在没有引入IOC容器之前,如图1所示,对象A依赖于对象B,那么对象A在初始化或者运行到某一点的时候,自己必须主动去创建对象B或者使用已经创建的对象B。无论是创建还是使用对象B,控制权都在自己手上。
软件系统在引入IOC容器之后,这种情形就完全改变了,如图3所示,由于IOC容器的加入,对象A与对象B之间失去了直接联系,所以,当对象A运行到需要对象B的时候,IOC容器会主动创建一个对象B注入到对象A需要的地方。
通过前后的对比,我们不难看出来:对象A获得依赖对象B的过程,由主动行为变为了被动行为,控制权颠倒过来了,这就是“控制反转”这个名称的由来。

参考资料:https://www.cnblogs.com/jianmang/articles/4947615.html

3. 什么是控制反转(IOC)?什么是依赖注入(DI)?

IoC(Inversion of Control) – 控制反转。它不是一种技术,而是一种思想。

IOC:就是对象之间的依赖关系由容器来创建,对象之间的关系本来是由我们开发者自己创建和维护的,在我们使用Spring框架后,对象之间的关系由容器来创建和维护,将开发者做的事让容器做,这就是控制反转。BeanFactory接口是Spring Ioc容器的核心接口。

DI:我们在使用Spring容器的时候,容器通过调用set方法或者是构造器来建立对象之间的依赖关系。
控制反转是目标,依赖注入是我们实现控制反转的一种手段。

4. 描述一下 Spring Bean 的生命周期?

这道题是spring一道标准题目

按照阶段理解Spring中的bean的生命周期主要包含四个阶段:实例化Bean --> Bean属性填充 --> 初始化Bean -->销毁Bean

Spring Bean 的生命周期

  1. Spring中的bean的生命周期主要包含四个阶段:实例化Bean --> Bean属性填充 --> 初始化Bean -->销毁Bean

  2. 首先是实例化Bean,当客户向容器请求一个尚未初始化的bean时,或初始化bean的时候需要注入另一个尚末初始化的依赖时,容器就会调用doCreateBean()方法进行实例化,实际上就是通过反射的方式创建出一个bean对象

  3. Bean实例创建出来后,接着就是给这个Bean对象进行属性填充,也就是注入这个Bean依赖的其它bean对象

  4. 属性填充完成后,进行初始化Bean操作,初始化阶段又可以分为几个步骤:

a. 执行Aware接口的方法

Spring会检测该对象是否实现了xxxAware接口,通过Aware类型的接口,可以让我们拿到Spring容器的些资源。如实现BeanNameAware接口可以获取到BeanName,实现BeanFactoryAware接口可以获取到工厂对象BeanFactory等

b. 执行BeanPostProcessor的前置处理方法postProcessBeforelnitialization(),对Bean进行一些自定义的前置处理

c. 判断Bean是否实现了InitializingBean接口,如果实现了,将会执行lnitializingBean的afeterPropertiesSet()初始化方法;

d. 执行用户自定义的初始化方法,如init-method等;

e. 执行BeanPostProcessor的后置处理方法postProcessAfterinitialization()

  1. 初始化完成后,Bean就成功创建了,之后就可以使用这个Bean, 当Bean不再需要时,会进行销毁操作,

a. 首先判断Bean是否实现了DestructionAwareBeanPostProcessor接口,如果实现了,则会执行DestructionAwareBeanPostProcessor后置处理器的销毁回调方法
b. 其次会判断Bean是否实现了DisposableBean接口,如果实现了将会调用其实现的destroy()方法
c. 最后判断这个Bean是否配置了dlestroy-method等自定义的销毁方法,如果有的话,则会自动调用其配置的销毁方法;

5. Spring Bean 的作用域之间有什么区别?

Spring器中的bean可以分为5个范围:

  1. singleton:这种bean范围是默认的,这种范围确保不管接受多少请求,每个容器中只有一个bean的实例,单例模式;
  2. prototype:为每一个bean提供一个实例;
  3. request:在请求bean范围内为每一个来自客户端的网络请求创建一个实例,在请求完毕后,bean会失效并被垃圾回收器回收;
  4. session:为每个session创建一个实例,session过期后,bean会随之消失;
  5. global-session:global-session和Portlet应用相关。当你的应用部署在Portlet容器中工作时,它包含很多portlet。如果你想要声明让所有的portlet公用全局的存储变量的话,那么全局变量需要存储在global-session中。

6. Spring中都应用了哪些设计模式

1、简单工厂模式

简单工厂模式的本质就是一个工厂类根据传入的参数,动态的决定实例化哪个类。

Spring中的BeanFactory就是简单工厂模式的体现,根据传入一个唯一的标识来获得bean对象。

2、工厂方法模式

应用程序将对象的创建及初始化职责交给工厂对象,工厂Bean。

定义工厂方法,然后通过config.xml配置文件,将其纳入Spring容器来管理,需要通过factory-method指定静态方法名称。

3、单例模式

Spring用的是双重判断加锁的单例模式,通过getSingleton方法从singletonObjects中获取bean。

     /**
     * Return the (raw) singleton object registered under the given name.
     * <p>Checks already instantiated singletons and also allows for an early
     * reference to a currently created singleton (resolving a circular reference).
     * @param beanName the name of the bean to look for
     * @param allowEarlyReference whether early references should be created or not
     * @return the registered singleton object, or {@code null} if none found
     */
    protected Object getSingleton(String beanName, boolean allowEarlyReference) {
        Object singletonObject = this.singletonObjects.get(beanName);
        if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
            synchronized (this.singletonObjects) {
                singletonObject = this.earlySingletonObjects.get(beanName);
                if (singletonObject == null && allowEarlyReference) {
                    ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                    if (singletonFactory != null) {
                        singletonObject = singletonFactory.getObject();
                        this.earlySingletonObjects.put(beanName, singletonObject);
                        this.singletonFactories.remove(beanName);
                    }
                }
            }
        }
        return (singletonObject != NULL_OBJECT ? singletonObject : null);
    }

4、代理模式

Spring的AOP中,使用的Advice(通知)来增强被代理类的功能。Spring实现AOP功能的原理就是代理模式(① JDK动态代理,② CGLIB字节码生成技术代理。)对类进行方法级别的切面增强。

5、装饰器模式

装饰器模式:动态的给一个对象添加一些额外的功能。

Spring的ApplicationContext中配置所有的DataSource。这些DataSource可能是不同的数据库,然后SessionFactory根据用户的每次请求,将DataSource设置成不同的数据源,以达到切换数据源的目的。

在Spring中有两种表现:

一种是类名中含有Wrapper,另一种是类名中含有Decorator。

6、观察者模式

定义对象间的一对多的关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。

Spring中观察者模式一般用在listener的实现。

7、策略模式

策略模式是行为性模式,调用不同的方法,适应行为的变化 ,强调父类的调用子类的特性 。

getHandler是HandlerMapping接口中的唯一方法,用于根据请求找到匹配的处理器。

8、模板方法模式

Spring JdbcTemplate的query方法总体结构是一个模板方法+回调函数,query方法中调用的execute()是一个模板方法,而预期的回调doInStatement(Statement state)方法也是一个模板方法。

7. Spring AOP里面的几个名词的概念

(1)连接点(Join point):指程序运行过程中所执行的方法。在Spring AOP中,一个连接点总代表一个方法的执行。

(2)切面(Aspect):被抽取出来的公共模块,可以用来会横切多个对象。Aspect切面可以看成 Pointcut切点 和 Advice通知 的结合,一个切面可以由多个切点和通知组成。

在Spring AOP中,切面可以在类上使用 @AspectJ 注解来实现。

(3)切点(Pointcut):切点用于定义 要对哪些Join point进行拦截。

切点分为execution方式和annotation方式。execution方式可以用路径表达式指定对哪些方法拦截,比如指定拦截add*、search*。annotation方式可以指定被哪些注解修饰的代码进行拦截。

(4)通知(Advice):指要在连接点(Join Point)上执行的动作,即增强的逻辑,比如权限校验和、日志记录等。通知有各种类型,包括Around、Before、After、After returning、After throwing。

(5)目标对象(Target):包含连接点的对象,也称作被通知(Advice)的对象。 由于Spring AOP是通过动态代理实现的,所以这个对象永远是一个代理对象。

(6)织入(Weaving):通过动态代理,在目标对象(Target)的方法(即连接点Join point)中执行增强逻辑(Advice)的过程。

(7)引入(Introduction):添加额外的方法或者字段到被通知的类。Spring允许引入新的接口(以及对应的实现)到任何被代理的对象。例如,你可以使用一个引入来使bean实现 IsModified 接口,以便简化缓存机制。

几个概念的关系图可以参考下图:

网上有张非常形象的图,描述了各个概念所处的场景和作用,贴在这里供大家理解:

8. BeanFactory和ApplicationContext有什么区别?

BeanFactory和ApplicationContext是Spring的两大核心接口,都可以当做Spring的容器。

  1. BeanFactory是Spring里面最底层的接口,是IoC的核心,定义了IoC的基本功能,包含了各种Bean的定义、加载、实例化,依赖注入和生命周期管理。ApplicationContext接口作为BeanFactory的子类,除了提供BeanFactory所具有的功能外,还提供了更完整的框架功能:
  • 继承MessageSource,因此支持国际化。
  • 资源文件访问,如URL和文件(ResourceLoader)。
  • 载入多个(有继承关系)上下文(即同时加载多个配置文件) ,使得每一个上下文都专注于一个特定的层次,比如应用的web层。
  • 提供在监听器中注册bean的事件。

a. BeanFactroy采用的是延迟加载形式来注入Bean的,只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化。这样,我们就不能提前发现一些存在的Spring的配置问题。如果Bean的某一个属性没有注入,BeanFacotry加载后,直至第一次使用调用getBean方法才会抛出异常。

b. ApplicationContext,它是在容器启动时,一次性创建了所有的Bean。这样,在容器启动时,我们就可以发现Spring中存在的配置错误,这样有利于检查所依赖属性是否注入。

c. ApplicationContext启动后预载入所有的单实例Bean,所以在运行的时候速度比较快,因为它们已经创建好了。相对于BeanFactory,ApplicationContext 唯一的不足是占用内存空间,当应用程序配置Bean较多时,程序启动较慢。

  1. BeanFactory和ApplicationContext都支持BeanPostProcessor、BeanFactoryPostProcessor的使用,但两者之间的区别是:BeanFactory需要手动注册,而ApplicationContext则是自动注册。

  2. BeanFactory通常以编程的方式被创建,ApplicationContext还能以声明的方式创建,如使用ContextLoader。

9. Spring如何解决循环依赖问题:

见:https://javapub.blog.csdn.net/

循环依赖问题在Spring中主要有三种情况:

  • (1)通过构造方法进行依赖注入时产生的循环依赖问题。
  • (2)通过setter方法进行依赖注入且是在多例(原型)模式下产生的循环依赖问题。
  • (3)通过setter方法进行依赖注入且是在单例模式下产生的循环依赖问题。

在Spring中,只有第(3)种方式的循环依赖问题被解决了,其他两种方式在遇到循环依赖问题时都会产生异常。这是因为:

  • 第一种构造方法注入的情况下,在new对象的时候就会堵塞住了,其实也就是”先有鸡还是先有蛋“的历史难题。
  • 第二种setter方法(多例)的情况下,每一次getBean()时,都会产生一个新的Bean,如此反复下去就会有无穷无尽的Bean产生了,最终就会导致OOM问题的出现。

Spring在单例模式下的setter方法依赖注入引起的循环依赖问题,主要是通过二级缓存和三级缓存来解决的,其中三级缓存是主要功臣。解决的核心原理就是:在对象实例化之后,依赖注入之前,Spring提前暴露的Bean实例的引用在第三级缓存中进行存储。

第一种构造方法注入的情况:

例如:类A通过构造函数注入需要类B的实例,而类B通过构造函数注入需要类A的实例。如果将A类和B类的bean配置为相互注入,则Spring IoC容器会在运行时检测此循环引用,并抛出a BeanCurrentlyInCreationException
一种可能的解决方案是编辑由setter而不是构造函数配置的某些类的源代码。或者,避免构造函数注入并仅使用setter注入。换句话说,尽管不推荐使用,但您可以使用setter注入配置循环依赖关系。
与典型情况(没有循环依赖)不同,bean A和bean B之间的循环依赖强制其中一个bean在完全初始化之前被注入另一个bean(经典的鸡与鸡蛋场景)。

    <bean id="person" class="pojo.Person">
        <constructor-arg index="0" value="小明"/>
        <constructor-arg index="1" value="12"/>
        <constructor-arg index="2" value="student"/>
    </bean>

    <bean id="student" class="pojo.Student">
        <constructor-arg index="0" value="小王"/>
        <constructor-arg index="1" value="13"/>
        <constructor-arg index="2" value="person"/>
    </bean>

10. Spring事务的实现方式和实现原理:

Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。Spring只提供统一事务管理接口,具体实现都是由各数据库自己实现,数据库事务的提交和回滚是通过 redo log 和 undo log实现的。Spring会在事务开始时,根据当前环境中设置的隔离级别,调整数据库隔离级别,由此保持一致。

1. Spring事务的种类:

spring支持编程式事务管理和声明式事务管理两种方式:

a. 编程式事务管理使用 TransactionTemplate。

b. 声明式事务管理建立在AOP之上的。其本质是通过AOP功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前启动一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。

声明式事务最大的优点就是不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明或通过@Transactional注解的方式,便可以将事务规则应用到业务逻辑中,减少业务代码的污染。唯一不足地方是,最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。

2. spring的事务传播机制:

spring事务的传播机制说的是,当多个事务同时存在的时候,spring如何处理这些事务的行为。事务传播机制实际上是使用简单的ThreadLocal实现的,所以,如果调用的方法是在新线程调用的,事务传播实际上是会失效的。

① PROPAGATION_REQUIRED:(默认传播行为)如果当前没有事务,就创建一个新事务;如果当前存在事务,就加入该事务。

② PROPAGATION_REQUIRES_NEW:无论当前存不存在事务,都创建新事务进行执行。

③ PROPAGATION_SUPPORTS:如果当前存在事务,就加入该事务;如果当前不存在事务,就以非事务执行。‘

④ PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

⑤ PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则按REQUIRED属性执行。

⑥ PROPAGATION_MANDATORY:如果当前存在事务,就加入该事务;如果当前不存在事务,就抛出异常。

⑦ PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。

事务不只限于脏读、幻读等名词。

3. Spring中的隔离级别:

① ISOLATION_DEFAULT:这是个 PlatfromTransactionManager 默认的隔离级别,使用数据库默认的事务隔离级别。

② ISOLATION_READ_UNCOMMITTED:读未提交,允许事务在执行过程中,读取其他事务未提交的数据。

③ ISOLATION_READ_COMMITTED:读已提交,允许事务在执行过程中,读取其他事务已经提交的数据。

④ ISOLATION_REPEATABLE_READ:可重复读,在同一个事务内,任意时刻的查询结果都是一致的。

⑤ ISOLATION_SERIALIZABLE:所有事务逐个依次执行。

MySql

1. mysql有哪几种log

重做日志(redo log)、回滚日志(undo log)、二进制日志(binlog)、错误日志(errorlog)、慢查询日志(slow query log)、一般查询日志(general log),中继日志(relay log)

错误日志:记录出错信息,也记录一些警告信息或者正确的信息。

查询日志:记录所有对数据库请求的信息,不论这些请求是否得到了正确的执行。

慢查询日志:设置一个阈值,将运行时间超过该值的所有SQL语句都记录到慢查询的日志文件中。

二进制日志:记录对数据库执行更改的所有操作。

中继日志:中继日志也是二进制日志,用来给slave 库恢复

事务日志:重做日志redo和回滚日志undo


2. MySQL的复制原理以及流程

  1. 主:binlog线程——记录下所有改变了数据库数据的语句,放进master上的binlog中。
  2. 从:io线程——在使用start slave 之后,负责从master上拉取 binlog 内容,放进 自己的relay log中。
  3. 从:sql执行线程——执行relay log中的语句。

3. 事物的4种隔离级别

隔离强度逐渐增强,性能逐渐变差。

  • 读未提交(RU) READ UNCOMMITTED
  • 读已提交(RC) READ COMMITT
  • 可重复读(RR) REPEATABLE READ
  • 串行化 SERIALIZABLE

事务具有原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)四个特性,简称 ACID,缺一不可。


4. 相关概念

脏读

脏读指的是读到了其他事务未提交的数据,未提交意味着这些数据可能会回滚,也就是可能最终不会存到数据库中,也就是不存在的数据。读到了并一定最终存在的数据,这就是脏读。

可重复读

可重复读指的是在一个事务内,最开始读到的数据和事务结束前的任意时刻读到的同一批数据都是一致的。通常针对数据更新(UPDATE)操作。

不可重复读

对比可重复读,不可重复读指的是在同一事务内,不同的时刻读到的同一批数据可能是不一样的,可能会受到其他事务的影响,比如其他事务改了这批数据并提交了。通常针对数据更新(UPDATE)操作。

幻读

幻读是针对数据插入(INSERT)操作来说的。假设事务 A 对某些行的内容作了更改,但是还未提交,此时事务 B 插入了与事务 A 更改前的记录相同的记录行,并且在事务 A 提交之前先提交了,而这时,在事务 A 中查询,会发现好像刚刚的更改对于某些数据未起作用,但其实是事务 B 刚插入进来的,让用户感觉很魔幻,感觉出现了幻觉,这就叫幻读。


5. MySQL数据库几个基本的索引类型

普通索引、唯一索引、主键索引、全文索引


6. drop、delete与truncate的区

SQL中的drop、delete、truncate都表示删除,但是三者有一些差别

1、delete和truncate只删除表的数据不删除表的结构
2、速度,一般来说: drop> truncate >delete
3、delete语句是dml,这个操作会放到rollback segement中,事务提交之后才生效;
4、如果有相应的trigger,执行的时候将被触发. truncate,drop是ddl, 操作立即生效,原数据不放到rollback segment中,不能回滚. 操作不触发trigger.


7. 数据库的乐观锁和悲观锁是什么?

悲观锁的特点是先获取锁,再进行业务操作,即 “悲观” 的认为获取锁是非常有可能失败的,因此要先确保获取锁成功再进行业务操作。通常所说的 “一锁二查三更新” 即指的是使用悲观锁。

通常来讲在数据库上的悲观锁需要数据库本身提供支持,即通过常用的 select … for update 操作来实现悲观锁。 当数据库执行 select for update 时会获取被 select 中的数据行的行锁,因此其他并发执行的 select for update 如果试图选中同一行则会发生排斥(需要等待行锁被释放),因此达到锁的效果。select for update 获取的行锁会在当前事务结束时自动释放,因此必须在事务中使用。

mysql 还有个问题是 select… for update 语句执行中,如果数据表没有添加索引或主键,所有扫描过的行都会被锁上,这一点很容易造成问题。因此如果在 mysql 中用悲观锁务必要确定走了索引,而不是全表扫描。

乐观锁的特点先进行业务操作,不到万不得已不去拿锁。即“乐观”的认为拿锁多半是会成功的,因此在进行完业务操作需要实际更新数据的最后一步再去拿一下锁就好。

乐观锁在数据库上的实现完全是逻辑的,不需要数据库提供特殊的支持。一般的做法是在需要锁的数据上增加一个版本号,或者时间戳。

乐观锁的两种实现方式:

  1. 使用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将 version 字段的值一同读出,数据每更新一次,对此 version 值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的 version 值进行比对,如果数据库表当前版本号与第一次取出来的 version 值相等,则予以更新,否则认为是过期数据。

  2. 乐观锁定的第二种实现方式和第一种差不多,同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp),和上面的 version 类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则 OK,否则就是版本冲突。


8. SQL优化方式

  1. 对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。
  2. 应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如果索引是整形,那么可以在索引上设置默认值 0,确保表中列没有 null 值。
  3. 应尽量避免在 where 子句中使用 != 或 <> 操作符,否则将引擎放弃使用索引而进行全表扫描。
  4. 应尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描。
  5. in 和 not in 也要慎用,否则会导致全表扫描。
  6. like ‘%abc%’ 也会导致全表扫描。
  7. 应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。
  8. 应尽量避免在 where 子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描。
  9. 在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使用,并且应尽可能的让字段顺序与索引顺序相一致。
  10. 很多时候用 exists 代替 in 是一个好的选择。

9. 从锁的类别上分MySQL都有哪些锁呢?

从锁的类别上来讲,有共享锁和排他锁。

共享锁: 又叫做读锁。 当用户要进行数据的读取时,对数据加上共享锁。共享锁可以同时加上多个。

排他锁: 又叫做写锁。 当用户要进行数据的写入时,对数据加上排他锁。排他锁只可以加一个,他和其他的排他锁,共享锁都相斥。


参考:

1. https://haicoder.net/note/mysql-interview/mysql-interview-optimistic-pessimism-lock.html

Docker

Lucene

看到这里相信你一定已经知道什么是Lucene:
1.Lucene是一套用于全文检索和搜寻的开源程式库;
2.是一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文两种西方语言);
3.是apache软件基金会4 jakarta项目组的一个子项目;
4.是一个成熟的免费开源工具;

1. Lucene 的数据结构是什么样的?

逻辑上来说,由大到小依次是:

index > segment > doc > field > term

倒排索引、正排信息、

  1. Lucene如何计算文档的相关性? ( vector 空间模型, boolean 模型)
  2. 什么是网段? (索引的一部分)
  3. 如何索引文本? (分析器, token 生成器)
  4. 什么是文件? (字段集合)
  5. Lucene查询语法是什么样的? ( boolean 查询,提升,模糊搜索)
  6. 它与关系数据库有何不同?何时在另一个数据库上使用它?
  7. lucene 搜索方式有哪些?
  8. lucene 常用字典数据结构有哪些?
  9. 谈谈FST原理简析
  10. lucene join解决父子关系索引
  11. lucene如何通过docId快速查找field字段以及最近距离等信息?

http://www.mianshigee.com/question/search/?q=lucene&p=2

Elasticsearch

关于es的面试题有很多解答的比较官方,但是很多名词用官方语言描述会更准确。

1. 说说你们公司 es 的集群架构,索引数据大小,分片有多少,以及一些调优手段 。

节点数、分片数、副本数,尽量根据自己公司使用情况回答,当然适当放大也可行。

调优手段是现在很常见的面试题,下面这几种调优手段一定要了解懂。当然,下面的每一条都可以当做调优的一部分。

设计调优

参考:
https://www.cnblogs.com/sanduzxcvbnm/p/12084012.html

a. 根据业务增量需求,采取基于日期模板创建索引,通过 rollover API 滚动索引;(rollover API我会单独写一个代码案例做讲解,公众号:JavaPub)
b. 使用别名进行索引管理;(es的索引名不能改变,提供的别名机制使用非常广泛。)
c. 每天凌晨定时对索引做force_merge操作,以释放空间;
d. 采取冷热分离机制,热数据存储到SSD,提高检索效率;冷数据定期进行shrink操作,以缩减存储;
e. 采取curator进行索引的生命周期管理;
f. 仅针对需要分词的字段,合理的设置分词器;
g. Mapping阶段充分结合各个字段的属性,是否需要检索、是否需要存储等。

进100+原创文章:https://gitee.com/rodert/JavaPub

写入调优

  1. 写入前副本数设置为0;
  2. 写入前关闭refresh_interval设置为-1,禁用刷新机制;
  3. 写入过程中:采取bulk批量写入;
  4. 写入后恢复副本数和刷新间隔;
  5. 尽量使用自动生成的id。

查询调优

  1. 禁用wildcard;(通配符模式,类似于%like%)
  2. 禁用批量terms(成百上千的场景);
  3. 充分利用倒排索引机制,能keyword类型尽量keyword;
  4. 数据量大时候,可以先基于时间敲定索引再检索;
  5. 设置合理的路由机制。
2. elasticsearch 的倒排索引是什么

倒排索引也就是单词到文档的映射,当然不只是存里文档id这么简单。还包括:词频(TF,Term Frequency)、偏移量(offset)、位置(Posting)。

3. elasticsearch 是如何实现 master 选举的

ElasticSearch 的选主是 ZenDiscovery 模块负责,源码分析将首发在。 https://gitee.com/rodert/JavaPub

  1. 对所有可以成为 Master 的节点(node.master: true)根据 nodeId 排序,每次选举每个节点都把自己所知道节点排一次序,然后选出第一个(第0位)节点,暂且认为它是 Master 节点。
  2. 如果对某个节点的投票数达到一定的值(可以成为master节点数n/2+1)并且该节点自己也选举自己,那这个节点就是master。否则重新选举。
    (当然也可以自己设定一个值,最小值设定为超过能成为Master节点的n/2+1,否则会出现脑裂问题。discovery.zen.minimum_master_nodes)
5. 描述一下 Elasticsearch 索引文档的过程

在这里插入图片描述

  1. 客户端向 Node 1 发送新建、索引或者删除请求。
  2. 节点使用文档的 _id 确定文档属于分片 0 。请求会被转发到 Node 3,因为分片 0 的主分片目前被分配在 Node 3 上。
  3. Node 3 在主分片上面执行请求。如果成功了,它将请求并行转发到 Node 1 和 Node 2 的副本分片上。一旦所有的副本分片都报告成功, Node 3 将向协调节点报告成功,协调节点向客户端报告成功。

一图胜千文,记住这幅图,上面是文档在节点间分发的过程,接着说一下文档从接收到写入磁盘过程。
协调节点默认使用文档 ID 参与计算(也支持通过 routing),以便为路由提供合适的分片。

shard = hash(document_id) % (num_of_primary_shards)

  1. 当分片所在的节点接收到来自协调节点的请求后,会将请求写入到 MemoryBuffer,然后定时(默认是每隔 1 秒)写入到 Filesystem Cache,这个从 MomeryBuffer 到 Filesystem Cache 的过程就叫做 refresh;
  2. 当然在某些情况下,存在 Momery Buffer 和 Filesystem Cache 的数据可能会丢失,ES 是通过 translog 的机制来保证数据的可靠性的。其实现机制是接收到请求后,同时也会写入到 translog 中,当 Filesystem cache 中的数据写入到磁盘中时,才会清除掉,这个过程叫做 flush;
  3. 在 flush 过程中,内存中的缓冲将被清除,内容被写入一个新段,段的 fsync将创建一个新的提交点,并将内容刷新到磁盘,旧的 translog 将被删除并开始一个新的 translog。
  4. flush 触发的时机是定时触发(默认 30 分钟)或者 translog 变得太大(默认为 512M)时;
1. translog 可以理解为就是一个文件,一直追加。
2. MemoryBuffer 应用缓存。
3. Filesystem Cache 系统缓冲区。

延伸阅读:Lucene 的 Segement:

  1. Lucene 索引是由多个段组成,段本身是一个功能齐全的倒排索引。
  2. 段是不可变的,允许 Lucene 将新的文档增量地添加到索引中,而不用从头重建索引。
  3. 对于每一个搜索请求而言,索引中的所有段都会被搜索,并且每个段会消耗CPU 的时钟周、文件句柄和内存。这意味着段的数量越多,搜索性能会越低。
  4. 为了解决这个问题,Elasticsearch 会合并小段到一个较大的段,提交新的合并段到磁盘,并删除那些旧的小段。
4. 详细描述一下 Elasticsearch 搜索的过程?

es作为一个分布式的存储和检索系统,每个文档根据 _id 字段做路由分发被转发到对应的shard上。

搜索执行阶段过程分俩个部分,我们称之为 Query Then Fetch。

4.1 query-查询阶段

当一个search请求发出的时候,这个query会被广播到索引里面的每一个shard(主shard或副本shard),每个shard会在本地执行查询请求后会生成一个命中文档的优先级队列。

这个队列是一个排序好的top N数据的列表,它的size等于from+size的和,也就是说如果你的from是10,size是10,那么这个队列的size就是20,所以这也是为什么深度分页不能用from+size这种方式,因为from越大,性能就越低。

es里面分布式search的查询流程如下:
es里面分布式search的查询流程
查询阶段包含以下三个步骤:

  1. 客户端发送一个 search 请求到 Node 3 , Node 3 会创建一个大小为 from + size 的空优先队列。
  2. Node 3 将查询请求转发到索引的每个主分片或副本分片中。每个分片在本地执行查询并添加结果到大小为 from + size 的本地有序优先队列中。
  3. 每个分片返回各自优先队列中所有文档的 ID 和排序值给协调节点,也就是 Node 3 ,它合并这些值到自己的优先队列中来产生一个全局排序后的结果列表。

4.2 fetch - 读取阶段 / 取回阶段

在这里插入图片描述
分布式阶段由以下步骤构成:

  1. 协调节点辨别出哪些文档需要被取回并向相关的分片提交多个 GET 请求。
  2. 每个分片加载并 丰富 文档,如果有需要的话,接着返回文档给协调节点。
  3. 一旦所有的文档都被取回了,协调节点返回结果给客户端。

协调节点首先决定哪些文档 确实 需要被取回。例如,如果我们的查询指定了 { “from”: 90, “size”: 10 } ,最初的90个结果会被丢弃,只有从第91个开始的10个结果需要被取回。这些文档可能来自和最初搜索请求有关的一个、多个甚至全部分片。

协调节点给持有相关文档的每个分片创建一个 multi-get request ,并发送请求给同样处理查询阶段的分片副本。

分片加载文档体-- _source 字段—​如果有需要,用元数据和 search snippet highlighting 丰富结果文档。 一旦协调节点接收到所有的结果文档,它就组装这些结果为单个响应返回给客户端。

拓展阅读:
深翻页(Deep Pagination)
---
先查后取的过程支持用 from 和 size 参数分页,但是这是 有限制的 。 要记住需要传递信息给协调节点的每个分片必须先创建一个 from + size 长度的队列,协调节点需要根据 number_of_shards * (from + size) 排序文档,来找到被包含在 size 里的文档。

取决于你的文档的大小,分片的数量和你使用的硬件,给 10,00050,000 的结果文档深分页( 1,0005,000 页)是完全可行的。但是使用足够大的 from 值,排序过程可能会变得非常沉重,使用大量的CPU、内存和带宽。因为这个原因,我们强烈建议你不要使用深分页。

实际上, “深分页” 很少符合人的行为。当2到3页过去以后,人会停止翻页,并且改变搜索标准。会不知疲倦地一页一页的获取网页直到你的服务崩溃的罪魁祸首一般是机器人或者web spider。

如果你 确实 需要从你的集群取回大量的文档,你可以通过用 scroll 查询禁用排序使这个取回行为更有效率,我们会在 later in this chapter 进行讨论。
注:https://www.elastic.co/guide/cn/elasticsearch/guide/current/scroll.html
5. Elasticsearch 在部署时,对 Linux 的设置有哪些优化方法
  1. 关闭缓存swap;

原因:大多数操作系统会将内存使用到文件系统缓存,会将应用程序未用到的内存交换出去。会导致jvm的堆内存交换到磁盘上。交换会导致性能问题。会导致内存垃圾回收延长。会导致集群节点响应时间变慢,或者从集群中断开。

  1. 堆内存设置为:Min(节点内存/2, 32GB);

  2. 设置最大文件句柄数;

后俩点不懂可以先说有一定了解,关注JavaPub会做详细讲解。

  1. 调整线程池和队列大小

  2. 磁盘存储 raid 方式——存储有条件使用 RAID6,增加单节点性能以及避免单节点存储故障。

https://www.elastic.co/cn/blog/how-to-design-your-elasticsearch-data-storage-architecture-for-scale#raid56

6. Elasticsearch 中的节点(比如共 20 个),其中的 10 个选了一个 master,另外 10 个选了另一个 master,怎么办?
  1. 当集群 master 候选数量不小于 3 个时,可以通过设置最少投票通过数量(discovery.zen.minimum_master_nodes)超过所有候选节点一半以上来解决脑裂问题;

  2. 当候选数量为两个时,只能修改为唯一的一个 master 候选,其他作为 data节点,避免脑裂问题。

7. 客户端在和集群连接时,如何选择特定的节点执行请求的?

client 远程连接连接一个 elasticsearch 集群。它并不加入到集群中,只是获得一个或者多个初始化的地址,并以轮询的方式与这些地址进行通信。

8. 详细描述一下 Elasticsearch 更新和删除文档的过程。
  1. 删除和更新也都是写操作,但是 Elasticsearch 中的文档是不可变的,因此不能被删除或者改动以展示其变更;(根本原因是底层lucene的segment段文件不可更新删除)
  2. 磁盘上的每个段都有一个相应的 .del 文件。当删除请求发送后,文档并没有真 的被删除,而是在 .del 文件中被标记为删除。该文档依然能匹配查询,但是会在 结果中被过滤掉。当段合并时,在.del 文件中被标记为删除的文档将不会被写入 新段。
  3. 在新的文档被创建时,Elasticsearch 会为该文档指定一个版本号,当执行更新 时,旧版本的文档在.del 文件中被标记为删除,新版本的文档被索引到一个新段。

旧版本的文档依然能匹配查询,但是会在结果中被过滤掉。

9. Elasticsearch 对于大数据量(上亿量级)的聚合如何实现?

这道题目较难,相信大家看到很多类似这种回答

Elasticsearch 提供的首个近似聚合是cardinality 度量。它提供一个字段的基数,即该字段的distinct或者unique值的数目。它是基于HLL算法的。HLL 会先对我们的输入作哈希运算,然后根据哈希运算的结果中的 bits 做概率估算从而得到基数。其特点是:可配置的精度,用来控制内存的使用(更精确 = 更多内存);小的数据集精度是非常高的;我们可以通过配置参数,来设置去重需要的固定内存使用量。无论数千还是数十亿的唯一值,内存使用量只与你配置的精确度相关。

科普&拓展

HyperLogLog:
下面简称为HLL,它是 LogLog 算法的升级版,作用是能够提供不精确的去重计数。存在以下的特点:
1. 能够使用极少的内存来统计巨量的数据,在 Redis 中实现的 HyperLogLog,只需要12K内存就能统计2^64个数据。
2. 计数存在一定的误差,误差率整体较低。标准误差为 0.81% 。
3. 误差可以被设置辅助计算因子进行降低。
---
应用场景:
1. 基数不大,数据量不大就用不上,会有点大材小用浪费空间
2. 有局限性,就是只能统计基数数量,而没办法去知道具体的内容是什么
3. 和bitmap相比,属于两种特定统计情况,简单来说,HyperLogLog 去重比 bitmap 方便很多
4. 一般可以bitmap和hyperloglog配合使用,bitmap标识哪些用户活跃,hyperloglog计数
---
应用场景:
1. 基数不大,数据量不大就用不上,会有点大材小用浪费空间
2. 有局限性,就是只能统计基数数量,而没办法去知道具体的内容是什么
3. 和bitmap相比,属于两种特定统计情况,简单来说,HyperLogLog 去重比 bitmap 方便很多
4. 一般可以bitmap和hyperloglog配合使用,bitmap标识哪些用户活跃,hyperloglog计数
来源:刷刷面试
10. 在并发情况下,Elasticsearch 如果保证读写一致?

首先要了解什么是一致性,在分布式系统中,我们一般通过CPA理论分析。

分布式系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容忍性(P:Partition Tolerance),最多只能同时满足其中两项。
分布式系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容忍性(P:Partition Tolerance),最多只能同时满足其中两项。

  1. 可以通过版本号使用乐观并发控制,以确保新版本不会被旧版本覆盖,由应用层来处理具体的冲突;
  2. 另外对于写操作,一致性级别支持 quorum/one/all,默认为 quorum,即只有当大多数分片可用时才允许写操作。但即使大多数可用,也可能存在因为网络等原因导致写入副本失败,这样该副本被认为故障,分片将会在一个不同的节点上重建。
  3. 对于读操作,可以设置 replication 为 sync(默认),这使得操作在主分片和副本分片都完成后才会返回;如果设置 replication 为 async 时,也可以通过设置搜索请求参数_preference 为 primary 来查询主分片,确保文档是最新版本。
11. 介绍一下你们的个性化搜索方案?

如果你没有很多实战经验,可以基于 word2vec 做一些练习,我的博客提供了 word2vec Java版的一些Demo。

基于 word2vec 和 Elasticsearch 实现个性化搜索,它有以下优点:

  1. 基于word2vec的商品向量还有一个可用之处,就是可以用来实现相似商品的推荐;

Redis

1. Redis是什么?

一般问这个问题你最少要答出以下几点

Redis 是一个基于内存的 key-value 存储系统,数据结构包括字符串、list、set、zset(sorted set --有序集合)和hash,bitmap,GeoHash(坐标),HyperLogLog,Streams(5.x版本以后)

2. 你在哪些场景使用redis

你有实战经验,那就直接表演。如果没有,选几个下面的经典场景

  1. 作为队列使用,(因为是基于内存、一般不会作为消费队列、作为循环队列必要适用);
  2. 模拟类似于token这种需要设置过期时间的场景,登录失效;
  3. 分布式缓存,避免大量请求底层关系型数据库,大大降低数据库压力;
  4. 分布式锁;
  5. 基于 bitmap 实现布隆过滤器;
  6. 排行榜-基于zset(有序集合数据类型);
  7. 计数器-对于浏览量、播放量等并发较高,使用 redis incr 实现计数器功能;
  8. 分布式会话;
  9. 消息系统;

3. 为什么Redis是单线程的?

这个问题给一个官方答案

因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了。

4. Redis持久化有几种方式?

redis 提供了两种持久化的方式,分别是快照方式(RDB Redis DataBase)和文件追加(AOF Append Only File)

显而易见,快照方式重启恢复快、但是数据更容易丢失,文件追加数据更完整、重启恢复慢。

混合持久化方式,Redis 4.0之后新增的方式,混合持久化是结合RDB和AOF的优点,在写入的时候先把当前的数据以RDB的形式写入到文件的开头,再将后续的操作以AOF的格式存入文件当中,这样既能保证重启时的速度,又能降低数据丢失的风险。

在恢复时,先恢复快照方式保存的文件,然后再恢复追加文件中的增量数据。

5. 什么是缓存穿透?怎么解决?

缓存穿透是指用户请求的数据在缓存中不存在即没有命中,同时在数据库中也不存在,导致用户每次请求该数据都要去数据库中查询一遍,然后返回空。

如果有恶意攻击者不断请求系统中不存在的数据,会导致短时间大量请求落在数据库上,造成数据库压力过大,甚至击垮数据库系统。

这就叫做缓存穿透。

怎么解决?

  • 对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该key对应的数据insert之后清理缓存。

  • 对一定不存在的key进行过滤。可以把所有的可能存在的key放到一个大的Bitmap中,查询时通过该Bitmap过滤。(也就是布隆过滤器的原理:大白话讲解布隆过滤器)

6. 什么是缓存雪崩?

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,请求直接落到数据库上,引起数据库压力过大甚至宕机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

怎么解决?

常用的解决方案有:

  • 均匀过期
  • 加互斥锁
  • 缓存永不过期
  • 双层缓存策略

均匀过期:设置不同的过期时间,让缓存失效的时间点尽量均匀。通常可以为有效期增加随机值或者统一规划有效期。

加互斥锁:跟缓存击穿解决思路一致,同一时间只让一个线程构建缓存,其他线程阻塞排队。

缓存永不过期:跟缓存击穿解决思路一致,缓存在物理上永远不过期,用一个异步的线程更新缓存。

双层缓存策略:使用主备两层缓存:

主缓存:有效期按照经验值设置,设置为主读取的缓存,主缓存失效后从数据库加载最新值。

备份缓存:有效期长,获取锁失败时读取的缓存,主缓存更新时需要同步更新备份缓存。

7. Redis使用上如何做内存优化?

  1. 缩短键值的长度
  • 缩短值的长度才是关键,如果值是一个大的业务对象,可以将对象序列化成二进制数组;
  • 首先应该在业务上进行精简,去掉不必要的属性,避免存储一些没用的数据;
  • 其次是序列化的工具选择上,应该选择更高效的序列化工具来降低字节数组大小;
  • 以JAVA为例,内置的序列化方式无论从速度还是压缩比都不尽如人意,这时可以选择更高效的序列化工具,如: protostuff,kryo等
  1. 共享对象池

对象共享池指Redis内部维护[0-9999]的整数对象池。创建大量的整数类型redisObject存在内存开销,每个redisObject内部结构至少占16字节,甚至超过了整数自身空间消耗。所以Redis内存维护一个[0-9999]的整数对象池,用于节约内存。 除了整数值对象,其他类型如list,hash,set,zset内部元素也可以使用整数对象池。因此开发中在满足需求的前提下,尽量使用整数对象以节省内存。

  1. 字符串优化

因为redis的惰性删除机制,字符串缩减后的空间不释放,作为预分配空间保留。尽量做新增不做更新。

  1. 编码优化

所谓编码就是具体使用哪种底层数据结构来实现。编码不同将直接影响数据的内存占用和读写效率。

这个需要掌握redis底层的数据结构。下图作为参考:

在这里插入图片描述

  1. 控制key的数量

8. 你们redis使用哪种部署方式?

redis部署分为单节点、主从部署(master-slave)、哨兵部署(Sentinel)、集群部署(cluster)。

单节点:也就是单机部署;

主从部署:分为一主一从或一主多从,主从之间同步分为全量或增量。量同步:master 节点通过 BGSAVE 生成对应的RDB文件,然后发送给slave节点,slave节点接收到写入命令后将master发送过来的文件加载并写入;增量同步:即在 master-slave 关系建立开始,master每执行一次数据变更的命令就会同步至slave节点。一般会将写请求转发到master,读请求转发到slave。提高了redis的性能。

哨兵部署:分别有哨兵集群与Redis的主从集群,哨兵作为操作系统中的一个监控进程,对应监控每一个Redis实例,如果master服务异常(ping pong其中节点没有回复且超过了一定时间),就会多个哨兵之间进行确认,如果超过一半确认服务异常,则对master服务进行下线处理,并且选举出当前一个slave节点来转换成master节点;如果slave节点服务异常,也是经过多个哨兵确认后,进行下线处理。提高了redis集群高可用的特性,及横向扩展能力的增强。

在这里插入图片描述

集群部署:属于**“去中心化”**的一种方式,多个 master 节点保存整个集群中的全部数据,而数据根据 key 进行 crc-16 校验算法进行散列,将 key 散列成对应 16383 个 slot,而 Redis cluster 集群中每个 master 节点负责不同的slot范围。每个 master 节点下还可以配置多个 slave 节点,同时也可以在集群中再使用 sentinel 哨兵提升整个集群的高可用性。

在这里插入图片描述

9. redis实现分布式锁要注意什么?

  1. 加锁过程要保证原子性;
  2. 保证谁加的锁只能被谁解锁,即Redis加锁的value,解锁时需要传入相同的value才能成功,保证value唯一性;
  3. 设置锁超时时间,防止加锁方异常无法释放锁时其他客户端无法获取锁,同时,超时时间要大于业务处理时间;

使用Redis命令 SET lock_key unique_value NX EX seconds 进行加锁,单命令操作,Redis是串行执行命令,所以能保证只有一个能加锁成功。


kafka

image

在面试kafka中,一定要了解为什么要用kafka、及kafka的架构等基本概念,才能对面试中的问题得心应手。

1. kafka 是什么?有什么作用?

Kafka 是一个分布式的流式处理平台,它以高吞吐、可持久化、可水平扩展、支持流数据处理等多种特性而被广泛使用

主要功能体现于三点:

  • 消息系统:kafka与传统的消息中间件都具备系统解耦、冗余存储、流量削峰、缓冲、异步通信、扩展性、可恢复性等功能。与此同时,kafka还提供了大多数消息系统难以实现的消息顺序性保障及回溯性消费的功能。

  • 存储系统:kafka把消息持久化到磁盘,相比于其他基于内存存储的系统而言,有效的降低了消息丢失的风险。这得益于其消息持久化和多副本机制。也可以将kafka作为长期的存储系统来使用,只需要把对应的数据保留策略设置为“永久”或启用主题日志压缩功能。

  • 流式处理平台:kafka为流行的流式处理框架提供了可靠的数据来源,还提供了一个完整的流式处理框架,比如窗口、连接、变换和聚合等各类操作。

2. kafka 的架构是怎么样的?

这是一个基本概念的题目,一定要掌握。

一个典型的 kafka 体系架构包括若干 Producer、若干 Consumer、以及一个 Zookeeper 集群(在2.8.0版本中移,除了 Zookeeper,通过 KRaft 进行自己的集群管理)

Producer 将消息发送到 Broker,Broker 负责将受到的消息存储到磁盘中,而 Consumer 负责从 Broker 订阅并消费消息。

Kafka 基本概念:

  • Producer :生产者,负责将消息发送到 Broker

  • Consumer :消费者,从 Broker 接收消息

  • Consumer Group :消费者组,由多个 Consumer 组成。消费者组内每个消费者负责消费不同分区的数据,一个分区只能由一个组内消费者消费,消费者组之间互不影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。

  • Broker :可以看做一个独立的 Kafka 服务节点或 Kafka 服务实例。如果一台服务器上只部署了一个 Kafka 实例,那么我们也可以将 Broker 看做一台 Kafka 服务器。

  • Topic :一个逻辑上的概念,包含很多 Partition,同一个 Topic 下的 Partiton 的消息内容是不相同的

  • Partition :为了实现扩展性,一个非常大的 topic 可以分布到多个 broker 上,一个 topic 可以分为多个 partition,每个 partition 是一个有序的队列。

  • Replica :副本,同一分区的不同副本保存的是相同的消息,为保证集群中的某个节点发生故障时,该节点上的 partition 数据不丢失,且 kafka 仍然能够继续工作,- kafka 提供了副本机制,一个 topic 的每个分区都有若干个副本,一个 leader 和若干个 follower。

  • Leader :每个分区的多个副本中的"主副本",生产者以及消费者只与 Leader 交互

  • Follower :每个分区的多个副本中的"从副本",负责实时从 Leader 中同步数据,保持和 Leader 数据的同步。Leader 发生故障时,从 Follower 副本中重新选举新的 Leader 副本对外提供服务。

3. Kafka Replicas是怎么管理的?

  • AR:分区中的所有 Replica 统称为 AR
  • ISR:所有与 Leader 副本保持一定程度同步的Replica(包括 Leader 副本在内)组成 ISR
  • OSR:与 Leader 副本同步滞后过多的 Replica 组成了 OSR

Leader 负责维护和跟踪 ISR 集合中所有 Follower 副本的滞后状态,当 Follower 副本落后过多时,就会将其放入 OSR 集合,当 Follower 副本追上了 Leader 的进度时,就会将其放入 ISR 集合。

默认情况下,只有 ISR 中的副本才有资格晋升为 Leader

4. 如何确定当前能读到哪一条消息?

这个问题要先了解上一个问题的概念

分区相当于一个日志文件,我们先简单介绍几个概念

如上图是一个分区日志文件

  • 标识共有7条消息,offset (消息偏移量)分别是0~6
  • 0 代表这个日志文件的开始
  • HW(High Watermark) 为4,0~3 代表这个日志文件可以消费的区间,消费者只能消费到这四条消息
  • LEO 代表即将要写入消息的偏移量 offset

分区 ISR 集合中的每个副本都会维护自己的 LEO,而 ISR 集合中最小的LEO 即为分区的 HW

如上图: 三个分区副本都是 ISR集合当中的,最小的 LEO 为 3,就代表分区的 HW 为3,所以当前分区只能消费到 0~2 之间的三条数据,如下图

5. 发送消息的分区策略有哪些?

  • 1.轮询:依次将消息发送该topic下的所有分区,如果在创建消息的时候 key 为 null,Kafka 默认采用这种策略。

  • 2.key 指定分区:在创建消息是 key 不为空,并且使用默认分区器,Kafka 会将 key 进行 hash,然后根据hash值映射到指定的分区上。这样的好处是 key 相同的消息会在一个分区下,Kafka 并不能保证全局有序,但是在每个分区下的消息是有序的,按照顺序存储,按照顺序消费。在保证同一个 key 的消息是有序的,这样基本能满足消息的顺序性的需求。但是如果 partation 数量发生变化,那就很难保证 key 与分区之间的映射关系了

  • 3.自定义策略:实现 Partitioner 接口就能自定义分区策略。

  • 4.指定 Partiton 发送

6. Kafka 的可靠性是怎么保证的?

1.acks

这个参数用来指定分区中有多少个副本收到这条消息,生产者才认为这条消息是写入成功的,这个参数有三个值:

  • 1.acks = 1,默认为1。生产者发送消息,只要 leader 副本成功写入消息,就代表成功。这种方案的问题在于,当返回成功后,如果 leader 副本和 follower 副本还没有来得及同步,leader 就崩溃了,那么在选举后新的 leader 就没有这条消息,也就丢失了
  • 2.acks = 0。生产者发送消息后直接算写入成功,不需要等待响应。这个方案的问题很明显,只要服务端写消息时出现任何问题,都会导致消息丢失
  • 3.acks = -1 或 acks = all。生产者发送消息后,需要等待 ISR 中的所有副本都成功写入消息后才能收到服务端的响应。毫无疑问这种方案的可靠性是最高的,但是如果 ISR 中只有leader 副本,那么就和 acks = 1 毫无差别了。

2.消息发送的方式

第6问中我们提到了生产者发送消息有三种方式,发完即忘,同步和异步。我们可以通过同步或者异步获取响应结果,失败做重试来保证消息的可靠性。

3.手动提交位移

默认情况下,当消费者消费到消息后,就会自动提交位移。但是如果消费者消费出错,没有进入真正的业务处理,那么就可能会导致这条消息消费失败,从而丢失。我们可以开启手动提交位移,等待业务正常处理完成后,再提交offset。

4.通过副本 LEO 来确定分区 HW

可参考第四问

7. 分区再分配是做什么的?解决了什么问题?

分区再分配主要是用来维护 kafka 集群的负载均衡

既然是分区再分配,那么 kafka 分区有什么问题呢?

问题1:当集群中的一个节点下线了

  • 如果该节点的分区是单副本的,那么分区将会变得不可用
  • 如果是多副本的,就会进行 leader 选举,在其他机器上选举出新的 leader

kafka 并不会将这些失效的分区迁移到其他可用的 broker 上,这样就会影响集群的负载均衡,甚至也会影响服务的可靠性和可用性

问题2:集群新增 broker 时,只有新的主题分区会分配在该 broker 上,而老的主题分区不会分配在该 broker 上,就造成了老节点和新节点之间的负载不均衡

为了解决该问题就出现了分区再分配,它可以在集群扩容,broker 失效的场景下进行分区迁移。

分区再分配的原理就是通化控制器给分区新增新的副本,然后通过网络把旧的副本数据复制到新的副本上,在复制完成后,将旧副本清除。 当然,为了不影响集群正常的性能,在此复制期间还会有一系列保证性能的操作,比如复制限流

8. Kafka Partition 副本 leader 是怎么选举的?

这个问题设计的点比较多,拓展的也更多一点,建议耐心阅读。

常用选主机制的缺点:

split-brain (脑裂):
​这是由ZooKeeper的特性引起的,虽然ZooKeeper能保证所有Watch按顺序触发,但是网络延迟,并不能保证同一时刻所有Replica“看”到的状态是一样的,这就可能造成不同Replica的响应不一致,可能选出多个领导“大脑”,导致“脑裂”。

herd effect (羊群效应):
​如果宕机的那个Broker上的Partition比较多, 会造成多个Watch被触发,造成集群内大量的调整,导致大量网络阻塞。

ZooKeeper负载过重:
​每个Replica都要为此在ZooKeeper上注册一个Watch,当集群规模增加到几千个Partition时ZooKeeper负载会过重。

优势:

​Kafka的Leader Election方案解决了上述问题,它在所有broker中选出一个controller,所有Partition的Leader选举都由controller决定。 controller会将Leader的改变直接通过RPC的方式(比ZooKeeper Queue的方式更高效)通知需为此作为响应的Broker。

​没有使用 zk,所以无 2.3 问题;也没有注册 watch无 2.2 问题 leader 失败了,就通过 controller 继续重新选举即可,所以克服所有问题。

Kafka partition leader的选举:

由 controller 执行:

  • 从Zookeeper中读取当前分区的所有ISR(in-sync replicas)集合
  • 调用配置的分区选择算法选择分区的leader

分区选择算法

上面五种分区算法都是选择PreferredReplica(优先副本选举)作为当前Partition的leader。区别仅仅是选择leader之后的操作有所不同。

9. 分区数越多越好吗?吞吐量就会越高吗?

般类似于这种问题的答案,都是持否定态度的。

但是可以说,在一定条件下,分区数的数量是和吞吐量成正比的,分区数和性能也是成正比的

那么为什么说超过了一定限度,就会对性能造成影响呢?原因如下:

1.客户端/服务器端需要使用的内存就越多

服务端在很多组件中都维护了分区级别的缓存,分区数越大,缓存成本也就越大。
消费端的消费线程数是和分区数挂钩的,分区数越大消费线程数也就越多,线程的开销成本也就越大
生产者发送消息有缓存的概念,会为每个分区缓存消息,当积累到一定程度或者时间时会将消息发送到分区,分区越多,这部分的缓存也就越大

2.文件句柄的开销

每个 partition 都会对应磁盘文件系统的一个目录。在 Kafka 的数据日志文件目录中,每个日志数据段都会分配两个文件,一个索引文件和一个数据文件。每个 broker 会为每个日志段文件打开一个 index 文件句柄和一个数据文件句柄。因此,随着 partition 的增多,所需要保持打开状态的文件句柄数也就越多,最终可能超过底层操作系统配置的文件句柄数量限制。

3.越多的分区可能增加端对端的延迟

Kafka 会将分区 HW 之前的消息暴露给消费者。分区越多则副本之间的同步数量就越多,在默认情况下,每个 broker 从其他 broker 节点进行数据副本复制时,该 broker 节点只会为此工作分配一个线程,该线程需要完成该 broker 所有 partition 数据的复制。

4.降低高可用性

在第 7 问我们提到了分区再分配,会将数据复制到另一份副本当中,分区数量越多,那么恢复时间也就越长,而如果发生宕机的 broker 恰好是 controller 节点时:在这种情况下,新 leader 节点的选举过程在 controller 节点恢复到新的 broker 之前不会启动。controller 节点的错误恢复将会自动地进行,但是新的 controller 节点需要从 zookeeper 中读取每一个 partition 的元数据信息用于初始化数据。例如,假设一个Kafka 集群存在 10000个partition,从 zookeeper 中恢复元数据时每个 partition 大约花费 2 ms,则 controller 的恢复将会增加约 20 秒的不可用时间窗口。

10. kafka 为什么这么快?

  • 1.顺序读写磁盘分为顺序读写与随机读写,基于磁盘的随机读写确实很慢,但磁盘的顺序读写性能却很高,kafka 这里采用的就是顺序读写。

  • 2.Page Cache为了优化读写性能,Kafka 利用了操作系统本身的 Page Cache,就是利用操作系统自身的内存而不是JVM空间内存。

  • 3.零拷贝Kafka使用了零拷贝技术,也就是直接将数据从内核空间的读缓冲区直接拷贝到内核空间的 socket 缓冲区,然后再写入到 NIC 缓冲区,避免了在内核空间和用户空间之间穿梭。

  • 4.分区分段+索引Kafka 的 message 是按 topic分 类存储的,topic 中的数据又是按照一个一个的 partition 即分区存储到不同 broker 节点。每个 partition 对应了操作系统上的一个文件夹,partition 实际上又是按照segment分段存储的。通过这种分区分段的设计,Kafka 的 message 消息实际上是分布式存储在一个一个小的 segment 中的,每次文件操作也是直接操作的 segment。为了进一步的查询优化,Kafka 又默认为分段后的数据文件建立了索引文件,就是文件系统上的.index文件。这种分区分段+索引的设计,不仅提升了数据读取的效率,同时也提高了数据操作的并行度。

  • 5.批量读写Kafka 数据读写也是批量的而不是单条的,这样可以避免在网络上频繁传输单个消息带来的延迟和带宽开销。假设网络带宽为10MB/S,一次性传输10MB的消息比传输1KB的消息10000万次显然要快得多。

  • 6.批量压缩Kafka 把所有的消息都变成一个批量的文件,并且进行合理的批量压缩,减少网络 IO 损耗,通过 mmap 提高 I/O 速度,写入数据的时候由于单个Partion是末尾添加所以速度最优;读取数据的时候配合 sendfile 进行直接读取。


算法题

1. 最长公共子串

这位大佬写的太好了,通俗易懂good

描述

有两个字符串(可能包含空格),请找出其中最长的公共连续子串,输出其长度。(长度在1000以内)

例如:
输入:abcde bcd
输出:3

解析

1、把两个字符串分别以行和列组成一个二维矩阵。
2、比较二维矩阵中每个点对应行列字符中否相等,相等的话值设置为1,否则设置为0。
3、通过查找出值为1的最长对角线就能找到最长公共子串。

比如:str=acbcbcef,str2=abcbced,则str和str2的最长公共子串为bcbce,最长公共子串长度为5。

针对于上面的两个字符串我们可以得到的二维矩阵如下:

在这里插入图片描述
从上图可以看到,str1 和 str2 共有 5 个公共子串,但最长的公共子串长度为 5。

为了进一步优化算法的效率,我们可以再计算某个二维矩阵的值的时候顺便计算出来当前最长的公共子串的长度,即某个二维矩阵元素的值由 record[i][j]=1 演变为 record[i][j]=1 +record[i-1][j-1] ,这样就避免了后续查找对角线长度的操作了。修改后的二维矩阵如下:
在这里插入图片描述
递推公式为:
当A[i] != B[j],dp[i][j] = 0
当A[i] == B[j],
若i = 0 || j == 0,dp[i][j] = 1
否则 dp[i][j] = dp[i - 1][j - 1] + 1

代码

仔细看看,一定可以掌握

暴力法

public int getLCS(String s, String s2) {
        if (s == null || t == null) {
            return 0;
        }
        int l1 = s.length();
        int l2 = t.length();
        int res = 0;
        for (int i = 0; i < l1; i++) {
            for (int j = 0; j < l2; j++) {
                int m = i;
                int k = j;
                int len = 0;
                while (m < l1 && k < l2 && s.charAt(m) == t.charAt(k)) {
                    len++;
                    m++;
                    k++;
                }
                res = Math.max(res, len);
            }
        }
        return res;
    }

动态规划

public int getLCS(String s, String t) {
        if (s == null || t == null) {
            return 0;
        }
        int result = 0;
        int sLength = s.length();
        int tLength = t.length();
        int[][] dp = new int[sLength][tLength];
        for (int i = 0; i < sLength; i++) {
            for (int k = 0; k < tLength; k++) {
                if (s.charAt(i) == t.charAt(k)) {
                    if (i == 0 || k == 0) {
                        dp[i][k] = 1;
                    } else {
                        dp[i][k] = dp[i - 1][k - 1] + 1;
                    }
                    result = Math.max(dp[i][k], result);
                } else {
                    dp[i][k] = 0;
                }
            }
        }
        return result;
    }

简化-优化

简化一下递推公式:
当A[i] != B[j],dp[i][j] = 0
否则 dp[i][j] = dp[i - 1][j - 1] + 1
全部都归结为一个公式即可,二维数组默认值为0

public int getLCS(String s, String t) {
        if (s == null || t == null) {
            return 0;
        }
        int result = 0;
        int sLength = s.length();
        int tLength = t.length();
        int[][] dp = new int[sLength + 1][tLength + 1];
        for (int i = 1; i <= sLength; i++) {
            for (int k = 1; k <= tLength; k++) {
                if (s.charAt(i - 1) == t.charAt(k - 1)) {
                    dp[i][k] = dp[i - 1][k - 1] + 1;
                    result = Math.max(dp[i][k], result);
                }
            }
        }
//        for (int i = 1; i <= sLength + 1; i++) {
//            for (int k = 1; k <= tLength + 1; k++) {
//                System.out.print(dp[i - 1][k - 1] + " ");
//            }
//            System.out.println();
//        }
        return result;
    }

行、列都多一行,更适应公式。

2. 反转链表

  1. 动态规划-迷宫

为了方便随时阅读,下载PDF版本

查看更多面试题及答案,微信搜:JavaPub。PDF下载(回复:pdf)

关注公众号,回复1024,获取Java学习路线思维导图、加入万粉计划学习交流群
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JavaPub-rodert

谢谢老板

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值