2022Java后端面试题整理

        因为这两年互联网整体环境不好,所在公司也在大批量裁员,故花很多时间在看面试题。看了好久,决定自己整理百家面试题,如果能帮助正在找工作的程序员们,那就更好了。会一直更新,直到找到满意工作为止...

借鉴以下原文:

Java面试题全集(上)_骆昊的博客-CSDN博客

Java后端面试八股文汇总_hutc_Alan的博客-CSDN博客

一、简单知识点

1、面向对象的特征有哪些方面?

抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。
继承:继承是从已有类得到继承信息创建新类的过程。提供继承信息的类被称为父类(超类、基类);得到继承信息的类被称为子类(派生类)。继承让变化中的软件系统有了一定的延续性,同时继承也是封装程序中可变因素的重要手段(如果不能理解请阅读阎宏博士的《Java与模式》或《设计模式精解》中关于桥梁模式的部分)。
封装:通常认为封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。面向对象的本质就是将现实世界描绘成一系列完全自治、封闭的对象。我们在类中编写的方法就是对实现细节的一种封装;我们编写一个类就是对数据和数据操作的封装。可以说,封装就是隐藏一切可隐藏的东西,只向外界提供最简单的编程接口(可以想想普通洗衣机和全自动洗衣机的差别,明显全自动洗衣机封装更好因此操作起来更简单;我们现在使用的智能手机也是封装得足够好的,因为几个按键就搞定了所有的事情)。
多态性:多态性是指允许不同子类型的对象对同一消息作出不同的响应。简单的说就是用同样的对象引用调用同样的方法但是做了不同的事情。多态性分为编译时的多态性和运行时的多态性。如果将对象的方法视为对象向外界提供的服务,那么运行时的多态性可以解释为:当A系统访问B系统提供的服务时,B系统有多种提供服务的方式,但一切对A系统来说都是透明的(就像电动剃须刀是A系统,它的供电系统是B系统,B系统可以使用电池供电或者用交流电,甚至还有可能是太阳能,A系统只会通过B类对象调用供电的方法,但并不知道供电系统的底层实现是什么,究竟通过何种方式获得了动力)。方法重载(overload)实现的是编译时的多态性(也称为前绑定),而方法重写(override)实现的是运行时的多态性(也称为后绑定)。运行时的多态是面向对象最精髓的东西,要实现多态需要做两件事:1). 方法重写(子类继承父类并重写父类中已有的或抽象的方法);2). 对象造型(用父类型引用引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为)。

2、访问修饰符public,private,protected,以及不写(默认)时的区别? 

修饰符 当前类 同 包 子 类 其他包
public
protected ×
default × ×
private × × ×

类的成员不写访问修饰时默认为default。默认对于同一个包中的其他类相当于公开(public),对于不是同一个包中的其他类相当于私有(private)。受保护(protected)对子类相当于公开,对不是同一包中的没有父子关系的类相当于私有。Java中,外部类的修饰符只能是public或默认,类的成员(包括内部类)的修饰符可以是以上四种。

3.构造方法、成员变量初始化以及静态成员变量三者的初始化顺序?

先后顺序:静态成员变量、成员变量、构造方法
详细的先后顺序:父类静态变量、父类静态代码块、子类静态变量、子类静态代码块、父类非静态变量、父类非静态代码块、父类构造函数、子类非静态变量、子类非静态代码块、子类构造函数

4.接口和抽象类的相同点和区别?

相同点:
1.都不能被实例化
2.接口的实现类或抽象类的子类需实现接口或抽象类中相应的方法才能被实例化

不同点:
1.接口只能有方法定义,不能有方法的实现,而抽象类可以有方法的定义与实现
2.实现接口的关键字为implements,继承抽象类的关键字为extends。一个类可以实现多个接口,但只能继承一个抽象类
3.当子类和父类之间存在逻辑上的层次结构,推荐使用抽象类,有利于功能的累积。当功能不需要,希望支持差别较大的两个或更多对象间的特定交互行为,推荐使用接口。使用接口能降低软件系统的耦合度,便于日后维护或添加删除方法。


5.为什么Java语言不支持多重继承?

  •  为了程序的结构能够更加清晰从而便于维护。假设Java语言支持多重继承,类C继承自类A和类B,如果类A和B都有自定义的成员方法f(),那么当代码中调用类C的f()会产生二义性。Java语言通过实现多个接口间接支持多重继承,接口由于只包含方法定义,不能有方法的实现,类C继承接口A与接口B时即使他们都有方法f(),也不能直接调用方法,需实现具体的f()方法才能调用,不会产生二义性。

  • 多重继承会使类型转换、构造方法的调用顺序变得复杂,会影响到性能。

9.Java提供的多态机制?

Java提供了两种用于多态的机制,分别是重载与重写(覆盖)

  1. 重载:重载是指同一个类中有多个同名的方法,但这些方法有着不同的参数,因此在编译时就可以确定到底调用哪个方法,它是一种编译时多态。重载可以被看作一个类中的方法多态性。

  2. 重写(覆盖):子类可以覆盖父类的方法,因此同样的方法会在父类与子类中有着不同的表现形式。在 Java 语言中,基类的引用变量不仅可以指向基类的实例对象,也可以指向其子类的实例对象。同样,接口的引用变量也可以指向其实现类的实例对象,而程序调用的方法在运行期才动态绑定(绑定指的是将一个方法调用和一个方法主体连接到一起),就是引用变量所指向的具体实例对象的方法,也就是内存里正在运行的那个对象的方法,而不是引用变量的类型中定义的方法。通过这种动态绑定的方法实现了多态。由于只有在运行时才能确定调用哪个方法,因此通过方法覆盖实现的多态也可以被称为运行时多态。

10.重载与重写(覆盖)的区别?

  • 重写是父类与子类之间的关系,是垂直关系;重载是同一类中方法之间的关系,是水平关系

  • 重写只能由一个方法或一对方法产生关系;重载是多个方法之间的关系

  • 重写要求参数列表相同;重载要求参数列表不同

  • 覆盖中,调用方法体是根据对象的类型来决定的,而重载是根据调用时实参表与形参表来对应选择方法体

  • 重载方法可以改变返回值的类型;重写方法不能改变返回值的类型

11.final、finally和finalize的区别是什么?

  • final用于声明属性、方法和类,分别表示属性不可变、方法不可覆盖、类不可继承。

  • finally作为异常处理的一部分,只能在try/catch语句中使用,finally附带一个语句块用来表示这个语句最终一定被执行,经常被用在需要释放资源的情况下。

  • finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的finalize()方法。当垃圾回收器准备好释放对象占用空间时,首先会调用finalize()方法,并在下一次垃圾回收动作发生时真正回收对象占用的内存。

12.出现在Java程序中的finally代码块是否一定会执行?
当遇到下面情况不会执行。

  1. 当程序在进入try语句块之前就出现异常时会直接结束。

  2. 当程序在try块中强制退出时,如使用System.exit(0),也不会执行finally块中的代码。

  3. 其它情况下,在try/catch/finally语句执行的时候,try块先执行,当有异常发生,catch和finally进行处理后程序就结束了,当没有异常发生,在执行完finally中的代码后,后面代码会继续执行。值得注意的是,当try/catch语句块中有return时,finally语句块中的代码会在return之前执行。如果try/catch/finally块中都有return语句,finally块中的return语句会覆盖try/catch模块中的return语句。

13.Java语言中关键字static的作用是什么?
static的主要作用有两个:
1.为某种特定数据类型或对象分配与创建对象个数无关的单一的存储空间。
2.使得某个方法或属性与类关联在一起而不是对象,在不创建对象的情况下可通过类直接调用方法或使用类的属性。

具体而言static又可分为4种使用方式:
1.修饰成员变量。用static关键字修饰的静态变量在内存中只有一个副本。只要静态变量所在的类被加载,这个静态变量就会被分配空间,可以使用’‘类.静态变量’‘和’‘对象.静态变量’'的方法使用。
2.修饰成员方法。static修饰的方法无需创建对象就可以被调用。static方法中不能使用this和super关键字,不能调用非static方法,只能访问所属类的静态成员变量和静态成员方法。
3.修饰代码块。JVM在加载类的时候会执行static代码块。static代码块常用于初始化静态变量。static代码块只会被执行一次。
4.修饰内部类。static内部类可以不依赖外部类实例对象而被实例化。静态内部类不能与外部类有相同的名字,不能访问普通成员变量,只能访问外部类中的静态成员和静态成员方法。

14.判等运算符==与equals的区别?
== 比较的是引用,equals比较的是内容。

        1.如果变量是基础数据类型,== 用于比较其对应值是否相等。如果变量指向的是对象,== 用于比较两个对象是否指向同一块存储空间。
        2.equals是Object类提供的方法之一,每个Java类都继承自Object类,所以每个对象都具有equals这个方法。Object类中定义的equals方法内部是直接调用 == 比较对象的。但通过覆盖的方法可以让它不是比较引用而是比较数据内容。需保证equals方法相同对应的对象hashCode也相同。

        3.(1)如果两个对象相同(equals方法返回true),那么它们的hashCode值一定要相同;(2)如果两个对象的hashCode相同,它们并不一定相同

15.为什么要把String设计为不变量?

  1. 节省空间:字符串常量存储在JVM的字符串池中可以被用户共享。
  2. 提高效率:   String会被不同线程共享,是线程安全的。在涉及多线程操作中不需要同步操作。
  3. 安全:String常被用于用户名、密码、文件名等使用,由于其不可变,可避免黑客行为对其恶意修改。

16.序列化是什么?

序列化是一种将对象转换成字节序列的过程,用于解决在对对象流进行读写操作时所引发的问题。序列化可以将对象的状态写在流里进行网络传输,或者保存到文件、数据库等系统里,并在需要的时候把该流读取出来重新构造成一个相同的对象。

17.Java反射机制是什么?
Java反射机制是指在程序的运行过程中可以构造任意一个类的对象、获取任意一个类的成员变量和成员方法、获取任意一个对象所属的类信息、调用任意一个对象的属性和方法。反射机制使得Java具有动态获取程序信息和动态调用对象方法的能力。可以通过以下类调用反射API。

18.简述注解

Java 注解用于为 Java 代码提供元数据。作为元数据,注解不直接影响你的代码执行,但也有一些类型的注解实际上可以用于这一目的。其可以用于提供信息给编译器,在编译阶段时给软件提供信息进行相关的处理,在运行时处理相应代码,做对应操作。

一般常用的注解可以分为三类:

  1. 一类是Java自带的标准注解,包括@Override(标明重写某个方法)、@Deprecated(标明某个类或方法过时)和@SuppressWarnings(标明要忽略的警告),使用这些注解后编译器就会进行检查。
  2. 一类为元注解,元注解是用于定义注解的注解,包括@Retention(标明注解被保留的阶段)、@Target(标明注解使用的范围)、@Inherited(标明注解可继承)、@Documented(标明是否生成javadoc文档)
  3. 一类为自定义注解,可以根据自己的需求定义注解
     

19.简述元注解
元注解可以理解为注解的注解,即在注解中使用,实现想要的功能。其具体分为:

  • @Retention: 表示注解存在阶段是保留在源码,还是在字节码(类加载)或者运行期(JVM中运行)。
  • @Target:表示注解作用的范围。
  • @Documented:将注解中的元素包含到 Javadoc 中去。
  • @Inherited:一个被@Inherited注解了的注解修饰了一个父类,如果他的子类没有被其他注解修饰,则它的子类也继承了父类的注解。
  • @Repeatable:被这个元注解修饰的注解可以同时作用一个对象多次,但是每次作用注解又可以代表不同的含
     

20.事务的ACID是指什么?

  • 原子性(Atomic):事务中各项操作,要么全做要么全不做,任何一项操作的失败都会导致整个事务的失败;
  • 一致性(Consistent):事务结束后系统状态是一致的;
  • 隔离性(Isolated):并发执行的事务彼此无法看到对方的中间状态;
  • 持久性(Durable):事务完成后所做的改动都会被持久化,即使发生灾难性的失败。通过日志和同步备份可以在故障发生后重建数据。

21.转发(forward)和重定向(redirect)的区别?
答:forward是容器中控制权的转向,是服务器请求资源,服务器直接访问目标地址的URL,把那个URL 的响应内容读取过来,然后把这些内容再发给浏览器,浏览器根本不知道服务器发送的内容是从哪儿来的,所以它的地址栏中还是原来的地址。redirect就是服务器端根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址,因此从浏览器的地址栏中可以看到跳转后的链接地址,很明显redirect无法访问到服务器保护起来资源,但是可以从一个网站redirect到其他网站。forward更加高效,所以在满足需要时尽量使用forward(通过调用RequestDispatcher对象的forward()方法,该对象可以通过ServletRequest对象的getRequestDispatcher()方法获得),并且这样也有助于隐藏实际的链接;在有些情况下,比如需要访问一个其它服务器上的资源,则必须使用重定向(通过HttpServletResponse对象调用其sendRedirect()方法实现)。

22.什么是ORM?
答:对象关系映射(Object-Relational Mapping,简称ORM)是一种为了解决程序的面向对象模型与数据库的关系模型互不匹配问题的技术;简单的说,ORM是通过使用描述对象和数据库之间映射的元数据(在Java中可以用XML或者是注解),将程序中的对象自动持久化到关系数据库中或者将关系数据库表中的行转换成Java对象,其本质上就是将数据从一种形式转换到另外一种形式。

23.解释一下什么叫AOP(面向切面编程)?
答:AOP(Aspect-Oriented Programming)指一种程序设计范型,该范型以一种称为切面(aspect)的语言构造为基础,切面是一种新的模块化机制,用来描述分散在对象、类或方法中的横切关注点(crosscutting concern)。
 

24.深拷贝和浅拷贝的区别?

浅拷贝:在拷贝一个对象时,对对象的基本数据类型的成员变量进行拷贝,但对引用类型的成员变量只进行引用的传递,并没有创建一个新的对象,当对引用类型的内容修改会影响被拷贝的对象。
深拷贝:在拷贝一个对象时,除了对基本数据类型的成员变量进行拷贝,对引用类型的成员变量进行拷贝时,创建一个新的对象来保存引用类型的成员变量。
实现深拷贝的两种方法:

1.序列化该对象,然后反序列化回来,就能得到一个新的对象了。

序列化:将对象写入到IO流中; 反序列化:从IO流中恢复对象 序列化机制允许将实现序列化的java对象转化为字节序列,这些字节序列可以保存到磁盘或者网络传输上,以达到以后恢复成原来的对象,序列化机制使得对象可以脱离程序的运行而独立存在

2. 继续利用clone()方法,对该对象的引用类型变量再实现一次clone()方法。

25.数组和链表的区别?

数组:

  • 数组是具有相同的数据类型且按一定次序排列的一组变量的集合体,数组在内存中的地址是连续的。
  • 数组是一种引用数据类型,数组元素类似对象的成员变量;
  • 随机访问性强(通过下标进行快速定位);查找速度快
  • 插入和删除效率低(插入和删除需要移动数据)
  • 内存空间要求高,必须有足够的连续内存空间。
  • 数组大小固定,不能动态拓展

链表:

  • 链表内存地址是散列、不连续的;
  • 链表通过指针连接元素,分为单向链表、双向链表和循环链表
  • 插入删除速度快(因为有next指针指向其下一个节点,通过改变指针的指向可以方便的增加删除元素)
    内存利用率高,不会浪费内存
  • 没有固定,拓展很灵活。
  • 不能随机查找,必须从第一个开始遍历,查找效率低

26.Hashmap和treemap的区别?

1)HashMap无序,TreeMap有序。

2)HashMap覆盖了equals()方法和hashcode()方法,这使得HashMap中两个相等的映射返回相同的哈希值;

TreeMap则是实现了SortedMap接口,使其有序。

3)HashMap的工作效率更高,而TreeMap则是基于树的增删查改。更推荐使用HashMap。

4)HashMap基于数组+链表+红黑树(jdk1.8之后)实现,TreeMap是基于红黑树实现。(如果单链表元素超过8个,则将单链表转变为红黑树;如果红黑树节点小于6时,再将红黑树变为单链表)

5)两者都不是线性安全的。

27.Mysql执行计划的type性能从高到低依次是?

null>system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL

28.接口的幂等性是啥?

答:是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变

实现方案:

1)数据库唯一标识

2)乐观锁

3)悲观锁

4)Token机制

5)分布式锁

29.Nacos作为注册中心的原理?

  • 1.使用http发送注册
  • 2.查询服务提供方案
  • 3.定时拉取服务列表(每10秒)
  • 4.检查服务提供者异常,基于UDP协议推送更新
  • 5.定时心跳(5秒),检测服务状态
  • 6.定时心跳任务检查
  • 7.集群数据同步任务使用Distro

30.https协议与http的区别

1)https协议需要到CA(Certificate Authority,数字证书认证机构)申请证书,一般免费证书较少,因而需要一定费用。
2)http是明文传输,数据未加密,安全性差;https则是具有安全性的ssl加密传输协议,安全性好。
3)http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
4)http页面响应速度比https快,因为http使用TCP三次握手建立连接,客户端和服务器需要交换 3 个包,而https除了TCP的三个包,还要加上ssl握手需要的 9 个包,所以一共是 12 个包。
5)https=SSL/TLS+http,所以,https比http要更耗费服务器资源。

31.TCP和UDP的区别?

2022Java后端面试题整理_第1张图片

32.简述MVC工作原理?

MVC模式(Model-view-controller)是软件工程中的一种软件架构模式,把软件系统分为三个基本部分:模型(Model)、视图(View)和控制器(Controller)

1、由客户端发起请求;2、服务端接收请求,并解析请求;3、根据解析出来的请求,找到对应的控制器,并执行控制器;4、控制器调用模型获取数据,并将数据传给视图;5、视图将数据渲染出来

33.autowired和resource注解的区别?

相同点:

  1. @Resource注解和@Autowired注解都可以用作bean的注入.
  2. 在接口只有一个实现类的时候,两个注解可以互相替换,效果相同.

不同点:

  1. @Resource注解是Java自身的注解,@Autowired注解是Spring的注解.

      2.@Resource注解有两个重要的属性,分别是name和type,如果name属性有值,则使用byName的自动注入策略,将值作为需要注入bean的名字,如果type有值,则使用byType自动注入策略,将值作为需要注入bean的类型.如果既不指定name也不指定type属性,这时将通过反射机制使用byName自动注入策略。即@Resource注解默认按照名称进行匹配,名称可以通过name属性进行指定,如果没有指定name属性,当注解写在字段上时,默认取字段名,按照名称查找,当找不到与名称匹配的bean时才按照类型进行装配。但是需要注意的是,如果name属性一旦指定,就只会按照名称进行装配。
        3.@Autowired注解是spring的注解,此注解只根据type进行注入,不会去匹配name.但是如果只根据type无法辨别注入对象时,就需要配合使用@Qualifier注解或者@Primary注解使用. 

34.spring中常见的设计模式?

(1)工厂模式:Spring使用工厂模式,通过BeanFactory和ApplicationContext来创建对象。通过唯一标识来获取bean对象。

(2)单例模式:Bean默认为单例模式

(3)策略模式:例如Resource的实现类,针对不同的资源文件,实现了不同方式的资源获取策略

(4)代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术

(5)模板方法:可以将相同部分的代码放在父类中,而将不同的代码放入不同的子类中,用来解决代码重复的问题。比如RestTemplate, JmsTemplate, JpaTemplate

(6)适配器模式:Spring AOP的增强或通知(Advice)使用到了适配器模式,Spring MVC中也是用到了适配器模式适配Controller

(7)观察者模式:Spring事件驱动模型就是观察者模式的一个经典应用。

(8)桥接模式:可以根据客户的需求能够动态切换不同的数据源。比如我们的项目需要连接多个数据库,客户在每次访问中根据需要会去访问不同的数据库
 

35.如何保持redis和数据库的一致性?

缓存延时双删:

  1. 先删除缓存
  2. 再更新数据库
  3. 休眠一会(比如500ms),再次删除缓存

异步更新缓存(最终一致性):

        可以使用阿里的canal将binlog日志采集发送到MQ队列里面然后通过ACK机制确认处理这条更新消息,删除缓存,保证数据缓存一致性

二、线程、并发相关

1、线程的生命周期?线程有几种状态

1).线程通常有五种状态,创建,就绪,运行、阻塞和死亡状态。

2). 阻塞的情况又分为三种:
        (1)、等待阻塞:运行的线程执行 wait 方法,该线程会释放占用的所有资源, JVM 会把该线程放入 等待
中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用 notify notifyAll 方法才能被唤
醒, wait object 类的方法
        (2)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放
锁池 中。
        (3)、其他阻塞:运行的线程执行 sleep join 方法,或者发出了 I/O 请求时, JVM 会把该线程置为阻塞状·
态。当 sleep 状态超时、 join 等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。
sleepThread类的方法
        1.新建状态 New ):新创建了一个线程对象。
        2.就绪状态 Runnable ):线程对象创建后,其他线程调用了该对象的 start 方法。该状态的线程位于
可运行线程池中,变得可运行,等待获取 CPU 的使用权。
        3.运行状态 Running ):就绪状态的线程获取了 CPU ,执行程序代码。
        4.阻塞状态 Blocked ):阻塞状态是线程因为某种原因放弃 CPU 使用权,暂时停止运行。直到线程进
入就绪状态,才有机会转到运行状态。
        5.死亡状态 Dead ):线程执行完了或者因异常退出了 run 方法,该线程结束生命周期。

2、sleep()wait()join()yield()的区别

1).锁池
所有需要竞争同步锁的线程都会放在锁池当中,比如当前对象的锁已经被其中一个线程得到,则其他线
程需要在这个锁池进行等待,当前面的线程释放同步锁后锁池中的线程去竞争同步锁,当某个线程得到
后会进入就绪队列进行等待 cpu 资源分配。
2).等待池
当我们调用 wait ()方法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁。只有调用了
notify ()或 notifyAll() 后等待池的线程才会开始去竞争锁, notify ()是随机从等待池选出一个线程放
到锁池,而 notifyAll() 是将等待池的所有线程放到锁池当中
  • (1)sleep Thread 类的静态本地方法,wait 则是 Object 类的本地方法。
  • (2)sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。
sleep就是把cpu的执行资格和执行权释放出去,不再运行此线程,当定时时间结束再取回cpu资源,参与cpu
的调度,获取到cpu资源后就可以继续运行了。而如果sleep时该线程有锁,那么sleep不会释放这个锁,而
是把锁带着进入了冻结状态,也就是说其他需要这个锁的线程根本不可能获取到这个锁。也就是说无法执行程
序。如果在睡眠期间其他线程调用了这个线程的interrupt方法,那么这个线程也会抛出
interruptexception异常返回,这点和wait是一样的。
  • (3)sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。
  • (4)sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)。
  • (5)sleep 一般用于当前线程休眠,或者轮循暂停操作,wait 则多用于多线程之间的通信。
  • (6)sleep 会让出 CPU 执行时间且强制上下文切换,而 wait 则不一定,wait 后可能还是有机会重新竞 争到锁继续执行的。
yield ()执行后线程直接进入就绪状态,马上释放了 cpu 的执行权,但是依然保留了 cpu 的执行资格, 所以有可能cpu 下次进行线程调度还会让这个线程获取到执行权继续执行
join ()执行后线程进入阻塞状态,例如在线程 B 中调用线程 A join (),那线程 B 会进入到阻塞队 列,直到线程A 结束或中断线程
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {

            e.printStackTrace();
        }
        System.out.println("22222222");
    }
    });
    t1.start();
    t1.join();
    // 这行代码必须要等t1全部执行完毕,才会执行
    System.out.println("1111");
}
22222222
1111

3、对线程安全的理解

不是线程安全、应该是内存安全,堆是共享内存,可以被所有线程访问
当多个线程访问一个对象时,如果不用进行额外的同步控制或其他的协调操作,调用这个对象的行为都可以获
得正确的结果,我们就说这个对象是线程安全的
是进程和线程共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分
配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完了
要还给操作系统,要不然就是内存泄漏。
Java 中,堆是 Java 虚拟机所管理的内存中最大的一块,是所有线程共享的一块内存区域,在虚
拟机启动时创建。堆所存在的内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及
数组都在这里分配内存。
是每个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈
互相独立,因此,栈是线程安全的。操作系统在切换线程的时候会自动切换栈。栈空间不需要在高级语
言里面显式的分配和释放。
目前主流操作系统都是多任务的,即多个进程同时运行。为了保证安全,每个进程只能访问分配给自己 的内存空间,而不能访问别的进程的,这是由操作系统保障的。
在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存)。进程内的所有线程都可以 访问到该区域,这就是造成问题的潜在原因。

4、ThreadRunable的区别

Thread Runnable 的实质是继承关系,没有可比性。无论使用 Runnable 还是 Thread ,都会 new
Thread ,然后执行 run 方法。用法上,如果有复杂的线程操作需求,那就选择继承 Thread ,如果只是简 单的执行一个任务,那就实现runnable
//会卖出多一倍的票
public class Test {
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        new MyThread().start();
        new MyThread().start();
    }
    static class MyThread extends Thread{
        private int ticket = 5;
        public void run(){
            while(true){
                System.out.println("Thread ticket = " + ticket--);
                if(ticket < 0){
                break;
                }
            }
        }
    }
}
//正常卖出
public class Test2 {
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        MyThread2 mt=new MyThread2();
        new Thread(mt).start();
        new Thread(mt).start();
    }
    static class MyThread2 implements Runnable{
        private int ticket = 5;
        public void run(){
            while(true){
                System.out.println("Runnable ticket = " + ticket--);
                if(ticket < 0){
                    break;
                }
            }
        }
    }
}
原因是: MyThread 创建了两个实例,自然会卖出两倍,属于用法错误

5、对守护线程的理解

守护线程:为所有非守护线程提供服务的线程;任何一个守护线程都是整个 JVM 中所有非守护线程的保 姆;
守护线程类似于整个进程的一个默默无闻的小喽喽;它的生死无关重要,它却依赖整个进程而运行;哪 天其他线程结束了,没有要执行的了,程序就结束了,理都没理守护线程,就把它中了;
注意: 由于守护线程的终止是自身无法控制的,因此千万不要把 IO File 等重要操作逻辑分配给它;因 为它不靠谱;
守护线程的作用是什么?
举例, GC 垃圾回收线程:就是一个经典的守护线程,当我们的程序中不再有任何运行的 Thread, 程序就 不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM 上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
应用场景:(1)来为其它线程提供服务支持的情况;(2) 或者在任何情况下,程序结束时,这个线 程必须正常且立刻关闭,就可以作为守护线程来使用;反之,如果一个正在执行某个操作的线程必须要 正确地关闭掉否则就会出现不好的后果的话,那么这个线程就不能是守护线程,而是用户线程。通常都 是些关键的事务,比方说,数据库录入或者更新,这些操作都是不能中断的。
thread.setDaemon(true) 必须在 thread.start() 之前设置,否则会跑出一个
IllegalThreadStateException 异常。你不能把正在运行的常规线程设置为守护线程。
Daemon 线程中产生的新线程也是 Daemon 的。
守护线程不能用于去访问固有资源,比如读写操作或者计算逻辑。因为它会在任何时候甚至在一个操作 的中间发生中断。
Java 自带的多线程框架,比如 ExecutorService ,会将守护线程转换为用户线程,所以如果要使用后台线 程就不能用Java 的线程池。

6、ThreadLocal的原理和使用场景

每一个 Thread 对象均含有一个 ThreadLocalMap 类型的成员变量 threadLocals ,它存储本线程中所 有ThreadLocal 对象及其对应的值 ThreadLocalMap 由一个个 Entry 对象构成
Entry 继承自 WeakReference> ,一个 Entry ThreadLocal 对象和 Object
成。由此可见, Entry key ThreadLocal 对象,并且是一个弱引用。当没指向 key 的强引用后,该 key就会被垃圾收集器回收
当执行 set 方法时, ThreadLocal 首先会获取当前线程对象,然后获取当前线程的 ThreadLocalMap 对 象。再以当前ThreadLocal 对象为 key ,将值存储进 ThreadLocalMap 对象中。
get 方法执行过程类似。 ThreadLocal 首先会获取当前线程对象,然后获取当前线程的 ThreadLocalMap 对象。再以当前ThreadLocal 对象为 key ,获取对应的 value 由于每一条线程均含有各自 私有的 ThreadLocalMap 容器,这些容器相互独立互不影响,因此不会存在
线程安全性问题,从而也无需使用同步机制来保证多条线程访问容器的互斥性。
使用场景:
  • 1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
  • 2、线程间数据隔离
  • 3、进行事务操作,用于存储线程事务信息。
  • 4、数据库连接,Session会话管理。
Spring 框架在事务开始时会给当前线程绑定一个 Jdbc Connection, 在整个事务过程都是使用该线程绑定的
connection 来执行数据库操作,实现了事务的隔离性。 Spring 框架里面就是用的 ThreadLocal 来实现这种
隔离

2022Java后端面试题整理_第2张图片

7、ThreadLocal内存泄露原因,如何避免

内存泄露为程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露 堆积后果很严重,无论多少内存, 迟早会被占光,
不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。
强引用:使用最普遍的引用 (new) ,一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足, Java虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不回收这种对象。
Spring 框架在事务开始时会给当前线程绑定一个 Jdbc Connection, 在整个事务过程都是使用该线程绑定的 connection来执行数据库操作,实现了事务的隔离性。 Spring 框架里面就是用的 ThreadLocal 来实现这种隔离如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为 null ,这样可以使 JVM 在合适的时   间就会回收该对象。
弱引用 JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在 java 中,用
java.lang.ref.WeakReference 类来表示。可以在缓存中使用弱引用。
ThreadLocal 的实现原理,每一个 Thread 维护一个 ThreadLocalMap key 为使用 弱引用 ThreadLocal 实例,value 为线程变量的副本
2022Java后端面试题整理_第3张图片
hreadLocalMap 使用 ThreadLocal 的弱引用作为 key ,如果一个 ThreadLocal 不存在外部 强引用 时,
Key(ThreadLocal) 势必会被 GC 回收,这样就会导致 ThreadLocalMap key null , 而 value 还存在着强引用,只有thead 线程退出以后 ,value 的强引用链条才会断掉,但如果当前线程再迟迟不结束的话,这些key null Entry value 就会一直存在一条强引用链(红色链条)

key 使用强引用

当hreadLocalMap key 为强引用回收 ThreadLocal 时,因为 ThreadLocalMap 还持有 ThreadLocal 的强引用,如果没有手动删除,ThreadLocal 不会被回收,导致 Entry 内存泄漏。

key 使用弱引用

当ThreadLocalMap key 为弱引用回收 ThreadLocal 时,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 也会被回收。当 key null ,在下一次 ThreadLocalMap 调用set(),get(), remove() 方法的时候会被清除 value 值。
因此, ThreadLocal 内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
ThreadLocal 正确的使用方法
  • 每次使用完ThreadLocal都调用它的remove()方法清除数据
  • ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entryvalue值,进而清除掉 。

8、并发、并行、串行的区别

  • 串行在时间上不可能发生重叠,前一个任务没搞定,下一个任务就只能等着
  • 并行在时间上是重叠的,两个任务在同一时刻互不干扰的同时执行。
  • 并发允许两个任务彼此干扰。统一时间点、只有一个任务运行,交替执行

9、并发的三大特性

1)原子性

原子性是指在一个操作中 cpu 不可以在中途暂停然后再调度,即不被中断操作,要不全部执行完成,要 不都不执行。就好比转账,从账户A 向账户 B 1000 元,那么必然包括 2 个操作:从账户 A 减去 1000 元, 往账户B 加上 1000 元。 2 个操作必须全部完成。
private long count = 0;
public void calc() {
     count++;
}
  1. count 从主存读到工作内存中的副本中
  2. +1的运算
  3. 将结果写入工作内存
  4. 将工作内存的值刷回主存(什么时候刷入由操作系统决定,不确定的)

那程序中原子性指的是最小的操作单元,比如自增操作,它本身其实并不是原子性操作,分了 3 步的, 包括读取变量的原始值、进行加1 操作、写入工作内存。所以在多线程中,有可能一个线程还没自增 完,可能才执行到第二部,另一个线程就已经读取了值,导致结果错误。那如果我们能保证自增操作是 一个原子性的操作,那么就能保证其他线程读取到的一定是自增后的数据。
关键字: synchronized

2)可见性

当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
若两个线程在不同的 cpu ,那么线程 1 改变了 i 的值还没刷新到主存,线程 2 又使用了 i ,那么这个 i 值肯定 还是之前的,线程1 对变量的修改线程没看到这就是可见性问题。
//线程1
boolean stop = false;
while(!stop){
    doSomething();
}
//线程2
stop = true;
如果线程 2 改变了 stop 的值,线程 1 一定会停止吗?不一定。当线程 2 更改了 stop 变量的值之后,但是还 没来得及写入主存当中,线程2 转去做其他事情了,那么线程 1 由于不知道线程 2 stop 变量的更改,因 此还会一直循环下去。
关键字: volatile synchronized final

3)有序性 

虚拟机在进行代码编译时,对于那些改变顺序之后不会对最终结果造成影响的代码,虚拟机不一定会按 照我们写的代码的顺序来执行,有可能将他们重排序。实际上,对于有些代码进行重排序之后,虽然对 变量的值没有造成影响,但有可能会出现线程安全问题。
int a = 0;
bool flag = false;
public void write() {
    a = 2; //1
    flag = true; //2
}
public void multiply() {
    if (flag) { //3
        int ret = a * a;//4
    }
}
write 方法里的 1 2 做了重排序,线程 1 先对 flag 赋值为 true ,随后执行到线程 2 ret 直接计算出结果, 再到线程1 ,这时候 a 才赋值为 2, 很明显迟了一步
关键字: volatile synchronized
volatile 本身就包含了禁止指令重排序的语义,而 synchronized 关键字是由 一个变量在同一时刻只允许 一条线程对其进行lock 操作 这条规则明确的。
synchronized 关键字同时满足以上三种特性,但是 volatile 关键字不满足原子性。
在某些情况下, volatile 的同步机制的性能确实要优于锁 ( 使用 synchronized 关键字或
java.util.concurrent 包里面的锁 ) ,因为 volatile 的总开销要比锁低。
我们判断使用 volatile 还是加锁的唯一依据就是 volatile 的语义能否满足使用的场景 ( 原子性 )

10、volatile

1. 保证被volatile修饰的共享变量对所有线程总是可见的,也就是当一个线程修改了一个被volatile修 饰共享变量的值,新值总是可以被其他线程立即得知。

//线程1
boolean stop = false;
while(!stop){
    doSomething();
}
//线程2
stop = true;
如果线程 2 改变了 stop 的值,线程 1 一定会停止吗?不一定。当线程 2 更改了 stop 变量的值之后,但
是还没来得及写入主存当中,线程 2 转去做其他事情了,那么线程 1 由于不知道线程 2 stop 变量的
更改,因此还会一直循环下去。

2. 禁止指令重排序优化。

int a = 0;
bool flag = false;
public void write() {
    a = 2; //1
    flag = true; //2
}
public void multiply() {
    if (flag) { //3
        int ret = a * a;//4
    }
}
write 方法里的 1 2 做了重排序,线程 1 先对 flag 赋值为 true ,随后执行到线程 2 ret 直接计算出结果, 再到线程1 ,这时候 a 才赋值为 2, 很明显迟了一步。
但是用 volatile 修饰之后就变得不一样了
第一:使用 volatile 关键字会强制将修改的值立即写入主存;
第二:使用 volatile 关键字的话,当线程 2 进行修改时,会导致线程 1 的工作内存中缓存变量 stop 的缓存
行无效(反映到硬件层的话,就是 CPU L1 或者 L2 缓存中对应的缓存行无效);
第三:由于线程 1 的工作内存中缓存变量 stop 的缓存行无效,所以线程 1 再次读取变量 stop 的值时会去主
存读取。
inc++ ; 其实是两个步骤,先加加,然后再赋值。不是原子性操作,所以 volatile 不能保证线程安全。

11、为什么用线程池?解释下线程池参数? 

  1. )降低资源消耗;提高线程利用率,降低创建和销毁线程的消耗。
  2. )提高响应速度;任务来了,直接有线程可用可执行,而不是先创建线程,再执行。
  3. )提高线程的可管理性;线程是稀缺资源,使用线程池可以统一分配调优监控。
  • corePoolSize 代表核心线程数,也就是正常情况下创建工作的线程数,这些线程创建后并不会消除,而是一种常驻线程
  • maxinumPoolSize 代表的是最大线程数,它与核心线程数相对应,表示最大允许被创建的线程 数,比如当前任务较多,将核心线程数都用完了,还无法满足需求时,此时就会创建新的线程,但是线程池内线程总数不会超过最大线程数
  • keepAliveTime unit 表示超出核心线程数之外的线程的空闲存活时间,也就是核心线程不会 消除,但是超出核心线程数的部分线程如果空闲一定的时间则会被消除,我们可以通过
  • setKeepAliveTime 来设置空闲时间
  • workQueue 用来存放待执行的任务,假设我们现在核心线程都已被使用,还有任务进来则全部放 入队列,直到整个队列被放满但任务还再持续进入则会开始创建新的线程
  • ThreadFactory 实际上是一个线程工厂,用来生产线程执行任务。我们可以选择使用默认的创建 工厂,产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以选择 自定义线程工厂,一般我们会根据业务来制定不同的线程工厂
  • Handler 任务拒绝策略,有两种情况,第一种是当我们调用 shutdown 等方法关闭线程池后,这 时候即使线程池内部还有没执行完的任务正在执行,但是由于线程池已经关闭,我们再继续想线程 池提交任务就会遭到拒绝。另一种情况就是当达到最大线程数,线程池已经没有能力继续处理新提交的任务时,这是也就拒绝

12、简述线程池处理流程 

2022Java后端面试题整理_第4张图片

13、线程池中阻塞队列的作用?为什么是先添加列队而不是先 创建最大线程

1 、一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,阻塞队列通过阻塞可以保留住当前想要继续入队的任务。
阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入 wait 状态,释放 cpu 资源。
阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时 , 线程池利用阻塞队列的 take 方法挂起,从而维持核心线程的存活、不至于一直占用cpu 资源
2 、在创建新线程的时候,是要获取全局锁的,这个时候其它的就得阻塞,影响了整体效率。
就好比一个企业里面有 10 个( core )正式工的名额,最多招 10 个正式工,要是任务超过正式工人数 (task > core )的情况下,工厂领导(线程池)不是首先扩招工人,还是这 10 人,但是任务可以稍微积 压一下,即先放到队列去(代价低)。10 个正式工慢慢干,迟早会干完的,要是任务还在继续增加,超过正式工的加班忍耐极限了(队列满了),就的招外包帮忙了(注意是临时工)要是正式工加上外包还 是不能完成任务,那新来的任务就会被领导拒绝了(线程池的拒绝策略)

14、线程池中线程复用原理

        线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过 Thread 创建线程时的一个线程必须对应一个任务的限制。
        在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对 Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去执行一个“ 循环任务 ,在这个 循环任务 中不停检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的 run 方法,将 run 方法当成一个普通的方法执行,通过这种方式只使用固定的线程就将所有任务的 run 方法串联起来。

15创建线程的几种方式?

1)继承Thread类创建线程类,重写run()方法,

2)实现Runnable接口创建线程,实现run()方法

3)实现Callable接口,实现call()方法,通过FutureTask包装器来创建Thread线程(有返回值)

4)从线程池获取

三、Sring相关

1.如何实现一个IOC容器

  1. 配置文件配置包扫描路径
  2. 递归包扫描获取.class文件
  3. 反射、确定需要交给IOC管理的类
  4. 对需要注入的类进行依赖注入
  • 配置文件中指定需要扫描的包路径
  • 定义一些注解,分别表示访问控制层、业务服务层、数据持久层、依赖注入注解、获取配置文件注
  • 从配置文件中获取需要扫描的包路径,获取到当前路径下的文件信息及文件夹信息,我们将当前路径下所有以.class结尾的文件添加到一个Set集合中进行存储
  • 遍历这个set集合,获取在类上有指定注解的类,并将其交给IOC容器,定义一个安全的Map用来 存储这些对象
  • 遍历这个IOC容器,获取到每一个类的实例,判断里面是有有依赖其他的类的实例,然后进行递归注入

2.Spring事务的分类?

1)编程式事务: 如果系统需要明确的事务,并且需要细粒度的控制各个事务的边界,此时建议使用编程式事务
2)声明式事务: 如果系统对于事务的控制粒度较为粗糙,则建议使用声明式事务,可以通过注解@Transational实现,原理是利用Spring框架通过AOP代理自动完成开启事务,提交事务,回滚事务。回滚的异常默认是运行时异常,可以通过rollbackFor属性制定回滚的异常类型

 

四、springCloud

1.springcloud五大组件:

        1)、注册中心组件(服务治理):Netflix Eureka;

        2)、负载均衡组件:Netflix Ribbon,各个微服务进行分摊,提高性能;

        3)、熔断器组件(断路器):Netflix Hystrix,Resilience4j ;保护系统,控制故障范围;

        4)、网关服务组件:Zuul,Spring Cloud Gateway;api网关,路由,负载均衡等多种作用;

        5)、配置中心:Spring Cloud Config,将配置文件组合起来,放在远程仓库,便于管理;

你可能感兴趣的:(面试,java,面试,jvm)