Java面试题:面向造火箭看面经

本文章面试题大部分参考:

《面试题》:

【Java】:

一个类的构造方法的作用是什么? 若一个类没有声明构造方法,该程序能正确执行吗? 为什么?

主要作用是完成对类对象的初始化工作。可以执行。因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。如果我们自己添加了类的构造方法(无论是否有参),Java 就不会再添加默认的无参数的构造方法了,这时候,就不能直接 new 一个对象而不传递参数了,所以我们一直在不知不觉地使用构造方法,这也是为什么我们在创建对象的时候后面要加一个括号(因为要调用无参的构造方法)。如果我们重载了有参的构造方法,记得都要把无参的构造方法也写出来(无论是否用到),因为这可以帮助我们在创建对象的时候少踩坑。

简述 BIO, NIO, AIO 的区别?

Java 中的 BIO、NIO和 AIO 理解为是 Java 语言对操作系统的各种 IO 模型的封装。

在讲 BIO,NIO,AIO 之前先来回顾一下这样几个概念:同步与异步,阻塞与非阻塞。

同步和异步:

当你同步执行某项任务时,你需要等待其完成才能继续执行其他任务。当你异步执行某些操作时,你可以在完成另一个任务之前继续进行。

  • 同步 :两个同步任务相互依赖,并且一个任务必须以依赖于另一任务的某种方式执行。 比如在A->B事件模型中,你需要先完成 A 才能执行B。再换句话说,同步调用中被调用者未处理完请求之前,调用不返回,调用者会一直等待结果的返回。
  • 异步:两个异步的任务完全独立的,一方的执行不需要等待另外一方的执行。再换句话说,异步调用中一调用就返回结果不需要等待结果返回,当结果返回的时候通过回调函数或者其他方式拿着结果再做相关事情;

阻塞和非阻塞:

  • 阻塞: 阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。
  • 非阻塞: 非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。

如何区分 “同步/异步 ”和 “阻塞/非阻塞” 呢?

同步/异步是从行为角度描述事物的,而阻塞和非阻塞描述的当前事物的状态(等待调用结果时的状态)。

BIO (Blocking I/O): 同步阻塞 I/O 模式:

数据的读取写入必须阻塞在一个线程内等待其完成。 在活动连接数不是特别高(小于单机 1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。

NIO (Non-blocking/New I/O): 同步非阻塞的 I/O 模型:

在 Java 1.4 中引入了 NIO 框架,对应 java.nio 包,提供了 Channel , Selector,Buffer 等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它支持面向缓冲的,基于通道的 I/O 操作方法。 NIO 提供了与传统 BIO 模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞 I/O 来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发

IO流是阻塞的,NIO流是不阻塞的。

  • Java IO的各种流是阻塞的。这意味着,当一个线程调用 read() 或 write() 时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了.
  • Java NIO使我们可以进行非阻塞IO操作。比如说,单线程中从通道读取数据到 buffer(缓冲区),同时可以继续做别的事情,当数据读取到 buffer(缓冲区) 中后,线程再继续处理数据。写数据也是一样的。另外,非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。

IO 面向流(Stream oriented),而 NIO 面向缓冲区(Buffer oriented)。

  • Buffer是一个对象,它包含一些要写入或者要读出的数据。
  • 在NIO厍中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。

NIO 通过Channel(通道) 进行读写。

  • 通道是双向的,可读也可写,而流的读写是单向的。无论读写,通道只能和Buffer交互。因为 Buffer,通道可以异步地读写。

AIO (Asynchronous I/O): AIO 也就是 NIO 2,异步非阻塞的 IO 模型:

在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的 IO 模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步 IO 的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO 操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。

== 和 equals() 的区别?

== : 它的作用是判断两个对象的地址是不是相等。即判断两个对象是不是同一个对象。
(基本数据类型 == 比较的是值,引用数据类型==比较的是内存地址)

  • 因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。

equals() : 它的作用也是判断两个对象是否相等,它不能用于比较基本数据类型的变量。equals()方法存在于Object类中,而Object类是所有类的直接或间接父类。

equals() 方法存在两种使用情况:

  • 情况 1:类没有覆盖 equals()方法。则通过 equals()比较该类的两个对象时,等价于通过“==”比较这两个对象。使用的默认是 Object类equals()方法。
  • 情况 2:类覆盖了 equals()方法。一般,我们都覆盖 equals()方法来两个对象的内容相等;若它们的内容相等,则返回 true(即,认为这两个对象相等)。

举个例子:

public class test1 {
    public static void main(String[] args) {
        String a = new String("ab"); // a 为一个引用
        String b = new String("ab"); // b为另一个引用,对象的内容一样
        String aa = "ab"; // 放在常量池中
        String bb = "ab"; // 从常量池中查找
        if (aa == bb) // true
            System.out.println("aa==bb");
        if (a == b) // false,非同一对象
            System.out.println("a==b");
        if (a.equals(b)) // true
            System.out.println("aEQb");
        if (42 == 42.0) { // true
            System.out.println("true");
        }
    }
}

说明:

  • String 中的 equals 方法是被重写过的,因为 Object 的 equals 方法是比较的对象的内存地址,而String 的equals 方法比较的是对象的值。
  • 当创建 String类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个String 对象。

简述 Spring AOP 的原理?

Spring AOP采用的是动态代理,在运行期间对业务方法进行增强,所以不会生成新类,对于动态代理技术,Spring AOP提供了对JDK动态代理的支持以及CGLib的支持。
JDK动态代理只能为接口创建动态代理实例,而不能对类创建动态代理。需要获得被目标类的接口信息
(应用Java的反射技术),生成一个实现了代理接口的动态代理类(字节码),再通过反射机制获得
动态代理类的构造函数,利用构造函数生成动态代理类的实例对象,在调用具体方法前调用invokeHandler方法来处理。
CGLib动态代理需要依赖asm包,把被代理对象类的class文件加载进来,修改其字节码生成子类。
但是Spring AOP基于注解配置的情况下,需要依赖于AspectJ包的标准注解,但是不需要额外的编译
以及AspectJ的织入器,而基于XML配置不需要。

Spring Bean的生命周期?

可以简述为以下几步:

  • 实例化bean对象(通过构造方法或者工厂方法)
  • 设置对象属性(setter等)(依赖注入)
  • 如果Bean实现了BeanNameAware接口,工厂调用Bean的setBeanName()方法传递Bean的ID。(和下面的一条均属于检查Aware接口)
  • 如果Bean实现了BeanFactoryAware接口,工厂调用setBeanFactory()方法传入工厂自身
  • 将Bean实例传递给Bean的前置处理器的postProcessBeforeInitialization(Object bean, String beanname)方法
  • 调用Bean的初始化方法
  • 将Bean实例传递给Bean的后置处理器的postProcessAfterInitialization(Object bean, String beanname)方法
  • 使用Bean
  • 容器关闭之前,调用Bean的销毁方法

参考内容:
Spring 了解Bean的一生(生命周期)
关于Spring Bean的生命周期
Spring Bean的生命周期(非常详细)

Java 类的加载流程是怎样的?知道new一个对象的过程吗?

在这里插入图片描述

  1. 类加载过程:
  1. 首先校验当前类是否被加载,如果没有加载,执行类加载机制 。
  2. 加载:就是从字节码加载成二进制流的过程。
  3. 验证:当然加载完成之后,当然需要校验Class文件是否符合虚拟机规范,跟我们接口请求一样,第一件事情当然是先做个参数校验了。
  4. 准备:为静态变量、常量赋默认值 。
  5. 解析:把常量池中符号引用(以符号描述引用的目标)替换为直接引用(指向目标的指针或者句柄等)的过程。
  6. 初始化:执行static代码块(cinit)进行初始化,如果存在父类,先对父类进行初始化。
  • Ps:静态代码块是绝对线程安全的,只能隐式被java虚拟机在类加载过程中初始化调用!
  1. 当虚拟机遇见new关键字时候,首先判断当前类是否已经加载,如果类没有加载,首先执行类的加载机制,加载完成后,紧接着就是对象分配内存空间和初始化的过程:
  1. 首先为对象分配合适大小的内存空间
  2. 接着为实例变量赋默认值
  3. 设置对象的头信息,对象hash码、GC分代年龄、元数据信息等
  4. 执行构造函数(init)初始化

Java 有几种基本数据类型,分别占多少字节?

Java中有8种基本数据类型,分别为:

  • 6种数字类型 :byte、short、int、long、float、double
  • 1种字符类型:char
  • 1种布尔型:boolean。
基本类型		位数		字节		默认值		包装类
int			32		4		0			Integer
short		16		2		0			Short
long		64		8		0L			Long
byte		8		1		0			Byte
char		16		2		'u0000'		Character
float		32		4		0f			Float
double		64		8		0d			Double
boolean		1				false		Boolean

简述封装、继承、多态的特性及使用场景?

封装:
封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。

继承:
继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。

关于继承如下 3 点请记住:

  • 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。
  • 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
  • 子类可以用自己的方式实现父类的方法。(以后介绍)。

多态:
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。

  • 在 Java 中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。

Java 中接口和抽象类的区别?

  • 接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),均为抽象方法,而抽象类可以有非抽象的方法。
  • 接口中除了 static、final 变量,不能有其他变量,而抽象类中则不一定。
  • 一个类可以实现多个接口,但只能实现一个抽象类。接口自己本身可以通过 extends 关键字扩展多个接口。
  • 接口方法默认修饰符是 public,抽象方法可以有 public、protected 和 default 这些修饰符(抽象方法就是为了被重写所以不能使用 private 关键字修饰!)。
  • 从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范。

总结一下 jdk7~jdk9 Java 中接口概念的变化:

  • 在 jdk 7 或更早版本中,接口里面只能有常量变量和抽象方法。这些接口方法必须由选择实现接口的类实现。
  • jdk8 的时候接口可以有默认方法和静态方法功能。
  • Jdk 9 在接口中引入了私有方法和私有静态方法。

简述 Java 的反射机制?

JAVA 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 java 语言的反射机制。

反射的应用场景:

  • 反射是框架设计的灵魂。
  • 在我们平时的项目开发过程中,基本上很少会直接使用到反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架也大量使用到了反射机制。

举例:

  • 我们在使用 JDBC 连接数据库时使用 Class.forName()通过反射加载数据库的驱动程序;
  • Spring 框架的 IOC(动态加载管理 Bean)创建对象以及 AOP(动态代理)功能都和反射有联系;
  • 动态配置实例的属性;

简述 Java 中 final 关键字的作用?

final 关键字主要用在三个地方:变量、方法、类。

  • 对于一个 final 变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。
  • 当用 final 修饰一个时,表明这个类不能被继承。final 类中的所有成员方法都会被隐式地指定为 final 方法。
  • 使用 final 方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的 Java 实现版本中,会将 final 方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的 Java 版本已经不需要使用 final 方法进行这些优化了)。类中所有的 private 方法都隐式地指定为 final。

Java 怎么防止内存溢出?

引起内存溢出的原因有很多种,小编列举一下常见的有以下几种:
1.内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
2.集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
3.代码中存在死循环或循环产生过多重复的对象实体;
4.使用的第三方软件中的BUG;
5.启动参数内存值设定的过小

内存溢出的解决方案:

  • 第一步,修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。)
  • 第二步,检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。
  • 第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。

重点排查以下几点:

  • 1.检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
  • 2.检查代码中是否有死循环或递归调用。
  • 3.检查是否有大循环重复产生新对象实体。
  • 4.检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
  • 5.检查List、MAP等集合对象是否有使用完后,未清除的问题。List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。
  • 第四步,使用内存查看工具动态查看内存使用情况

转载于:https://www.cnblogs.com/flywang/p/5518423.html

什么是重写和重载?

重载:同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理。

  • 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。

重写:重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写,外部样子不能改变,内部逻辑可以改变。

  • 返回值类型、方法名、参数列表必须相同,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。
  • 如果父类方法访问修饰符为 private/final/static 则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。
  • 构造方法无法被重写
区别点:		重载方法:		重写方法:
发生范围		同一个类		子类
参数列表		必须修改		一定不能修改
返回类型		可修改		子类方法返回值类型应比父类方法返回值类型更小或相等
异常			可修改		子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;
访问修饰符	可修改		一定不能做更严格的限制(可以降低限制)
发生阶段		编译期		运行期

Java 编译后的 .class 文件包含了什么内容?

参考内容:JVM系列文章(三):Class文件内容解析

成员变量与局部变量的区别有哪些?

  • 从语法形式上看:成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
  • 从变量在内存中的存储方式来看:如果成员变量是使用static修饰的,那么这个成员变量是属于类的,如果没有使用static修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
  • 从变量在内存中的生存时间上看:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。
  • 成员变量如果没有被赋初值,则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。

Spring事务传播行为?

参考文章:
Spring事务传播行为详解
spring的两种常用事务传播属性说明

简述 Spring 的初始化流程?

初始化环境—>加载配置文件—>实例化Bean—>调用Bean显示信息

SpringMVC 的工作原理?

在这里插入图片描述
上图的一个笔误的小问题:Spring MVC 的入口函数也就是前端控制器 DispatcherServlet 的作用是接收请求,响应结果。

流程说明(重要):

  1. 客户端(浏览器)发送请求,直接请求到 DispatcherServlet
  2. DispatcherServlet 根据请求信息调用 HandlerMapping,解析请求对应的 Handler
  3. 解析到对应的 Handler(也就是我们平常说的 Controller 控制器)后,开始由 HandlerAdapter 适配器处理。
  4. HandlerAdapter 会根据 Handler 来调用真正的处理器来处理请求,并处理相应的业务逻辑。
  5. 处理器处理完业务后,会返回一个 ModelAndView 对象,Model 是返回的数据对象,View 是个逻辑上的 View
  6. ViewResolver 会根据逻辑 View 查找实际的 View
  7. DispaterServlet 把返回的 Model 传给 View(视图渲染)。
  8. View 返回给请求者(浏览器)

Java 缓冲流 buffer 的用途和原理是什么?

缓冲区就是内存里的一块区域,把数据先存内存里,然后一次性写入,类似于数据库的批量操作,这样大大提高高了数据的读写速率。

  • 不带缓冲的流的工作原理:
    它读取到一个字节/字符,就向用户指定的路径写出去,读一个写一个,所以就慢了。
  • 带缓冲的流的工作原理:
    读取到一个字节/字符,先不输出,等凑足了缓冲的最大容量后一次性写出去,从而提高了工作效率
    优点:减少对硬盘的读取次数,降低对硬盘的损耗。

Java 如何高效进行数组拷贝?

  1. for循环逐一复制
  2. System.arraycopy
  3. System.copyof
  4. 使用clone方法

效率:System.arraycopy > clone > Arrays.copyOf > for循环

Java - 数组拷贝的几种方式

hashcode 和 equals 方法的联系?

hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在 JDK 的 Object 类中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。另外需要注意的是: Object 的 hashcode 方法是本地方法,也就是用 c 语言或 c++ 实现的,该方法通常用来将对象的 内存地址 转换为整数之后返回。

  • 散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)

hashCode()与 equals()的相关规定:

  • 如果两个对象相等,则 hashcode 一定也是相同的
  • 两个对象相等,对两个对象分别调用 equals 方法都返回 true
  • 两个对象有相同的 hashcode 值,它们也不一定是相等的
  • 因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖
  • hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)

SpringBoot 是如何进行自动配置的?

@SpringBootApplication:这个注解是 Spring Boot 项目的基石,创建 SpringBoot 项目之后会默认在主类加上。

可以把 @SpringBootApplication看作是 @Configuration、@EnableAutoConfiguration、@ComponentScan 注解的集合。

根据 SpringBoot 官网,这三个注解的作用分别是:

  • @EnableAutoConfiguration:启用 SpringBoot 的自动配置机制
  • @ComponentScan: 扫描被@Component (@Service,@Controller)注解的 bean,注解默认会扫描该类所在的包下所有的类。
  • @Configuration:允许在 Spring 上下文中注册额外的 bean 或导入其他配置类

简单的说 springboot 的自动装载机制就是通过配置@EnableAutoConfiguration 将配置为@Configuration 下的@Bean 方法加载到 spring 容器中,这个过程就是 spring 自动装载机制。
首先 springboot 自动装配功能是为了满足啥?是为了满足其他的插件进行扩展,因为有很多外部的 bean 我们没法管理,也不知道具体包的路径,这个时候 springboot 提供了自动装配功能,让我们外部的类能够注入到 spring 项目中。
第二,如果说这个是 springboot 的自动装配功能不如说是 spring 的自动装配功能。因为 springboot 使用了 spring3.1 出来的 ImportSelector 动态 bean 的装载实现的自动装载机制,同时使用了 META-INF/spring.factories 中的 SPI 机制实现了 spring 自动扫描到自动装载的 bean 的机制。
后面再说几点,spring 发展是由 XML 文件到注解方式的一个循序渐进的过程,比如@Component 以及它的派生注解@Controller 等,最后 spring 直接把 XML 文件变成了@Configuration 注解,这样理解就可以理解成 springboot 自动装载机制是把外部的 xml 文件的 Bean 配置导入到了自己的项目中,让 Bean 在自己的项目中运行。而起到关键作用的@EnableAutoConfiguration 只是作为了一个中介者的作用。

Spring Boot 的自动配置,是如何实现的?

String 类能不能被继承?为什么?

String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[]。
所以不能被继承。

注:在 Java 9 之后,String 类的实现改用 byte 数组存储字符串 private final byte[] value;

常用的排序方式有哪些,时间复杂度是多少?

1. 冒泡排序:

/**
 * 冒泡排序
 */
@Test
public void test1(){
   int[] arr = {4,7,6,5,3,2,8,1};
    int temp = 0;
    for (int i = 0; i < arr.length; i++) {
        for (int j = 0; j < arr.length-1; j++) {
            if (arr[j] > arr[j+1]){
                temp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1]=temp;
            }
        }
    }

    for (int i : arr) {
        System.out.println(i);
    }
}

2. 快速排序:

/**
 * 快速排序
 */
@Test
public void test2() {
    int[] arr = {4,7,6,5,3,2,8,1};

    System.out.println("排序前:"+Arrays.toString(arr));

    int startIndex = 0;
    int endIndex = arr.length-1;
    quickSort(arr, startIndex, endIndex);  //快速排序

    System.out.println("排序后:"+Arrays.toString(arr));

}

private void quickSort(int [] arr,int startIndex,int endIndex) {
    //startIndex 大于等于 endIndex的时候 递归结束;
    if(startIndex>=endIndex) {
        return;
    }

    //得到基准元素位置
    int pivotIndex = partition(arr, startIndex, endIndex);

    //根据基准元素,分成两部分递归排序
    quickSort(arr, startIndex,pivotIndex - 1);
    quickSort(arr,pivotIndex + 1, endIndex);
}

private int partition(int[] arr,int startIndex,int endIndex) {
    int temp =arr[startIndex];    //取最左边的第一个元素为基准元素

    int left = startIndex;
    int right = endIndex;

    while(left!=right) {    //当left、right 两个指针重合了,循环结束

        //从最右边right指针开始,把指针所指向的元素和基准元素做比较。
        // 如果大于等于基准值,则指针向左移动;
        // 直到小于基准值了,则right指针停止移动,切换到left指针。
        while(left<right && arr[right]>temp) {
            right--;
        }

        //当轮到left指针行动,把指针所指向的元素和基准元素做比较。
        // 如果小于等于基准值,则指针向右移动;
        // 直到大于基准值了,则left指针停止移动。
        while(left<right && arr[left]<=temp) {
            left++;
        }

        //如果left指针的值大于基准值,让left和right指向的元素进行交换。
        if (left<right){
            int t = arr[left];
            arr[left] = arr[right];
            arr[right] = t;
        }
    }

    //当left和right指针重合之时,我们让基准值和left与right重合点的元素进行交换。
    int t2 = arr[left];
    arr[left] = arr[startIndex];
    arr[startIndex] = t2;

    return left;
}

ConcurrentHashMap 的实现原理是怎样的?ConcurrentHashMap 是如何保证线程安全的?

JDK1.8 ConcurrentHashMap 取消了 Segment 分段锁,采用 CAS 和 synchronized 来保证并发安全。数据结构跟 HashMap1.8 的结构类似,数组+链表/红黑二叉树。Java 8 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N)))

(分段锁),synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍。

HashMap 和 HashTable 的区别?

  • 线程是否安全: HashMap 是非线程安全的,HashTable 是线程安全的,因为 HashTable 内部的方法基本都经过synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);
  • 效率:因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;
  • 对 Null key 和 Null value 的支持: HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;HashTable 不允许有 null 键和 null 值,否则会抛出 NullPointerException。
  • 初始容量大小和每次扩充容量大小的不同 :

– ① 创建时如果不指定容量初始值,Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。
– ② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2的幂次方大小(HashMap 中的tableSizeFor()方法保证,下面给出了源代码)。也就是说 HashMap 总是使用 2的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。

  • 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。

HashMap 和 TreeMap 区别?

TreeMap 和HashMap 都继承自AbstractMap ,但是需要注意的是TreeMap它还实现了NavigableMap接口和SortedMap 接口。

  • 实现 NavigableMap 接口让 TreeMap 有了对集合内元素的搜索的能力。
  • 实现SortMap接口让 TreeMap 有了对集合中的元素根据键排序的能力。默认是按 key 的升序排序,不过我们也可以指定排序的比较器。

HashMap 实现原理,为什么使用红黑树?

JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。

TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。

所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。

HashMap 1.7 / 1.8 的实现区别?

  • JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。
  • JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。

比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同?

  • HashSet 是 Set 接口的主要实现类 ,HashSet 的底层是 HashMap,线程不安全的,可以存储 null 值;HashSet 使用成员对象来计算 hashcode 值,对于两个对象来说 hashcode 可能相同,所以 equals()方法用来判断对象的相等性。
  • LinkedHashSet 是 HashSet 的子类,能够按照添加的顺序遍历;
  • TreeSet 底层使用红黑树,能够按照添加元素的顺序进行遍历,排序的方式有自然排序和定制排序。

简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度?

Arraylist 底层使用的是 Object 数组;不保证线程安全;

  • ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element))时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。
  • 支持高效的随机元素访问。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。

LinkedList 底层使用的是 双向链表 数据结构。不保证线程安全;

  • LinkedList 采用链表存储,所以对于add(E e)方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置i插入和删除元素的话((add(int index, E element)) 时间复杂度近似为o(n))因为需要先移动到指定位置再插入。

集合类中的 List 和 Map 的线程安全版本是什么,如何保证线程安全的?

JUC包(java.util.concurrent)中提供了很多并发容器供你使用:

  • ConcurrentHashMap: 可以看作是线程安全的 HashMap
  • CopyOnWriteArrayList:可以看作是线程安全的 ArrayList,在读多写少的场合性能非常好,远远好于 Vector.
  • ConcurrentLinkedQueue:高效的并发队列,使用链表实现。可以看做一个线程安全的 LinkedList,这是一个非阻塞队列。
  • BlockingQueue: 这是一个接口,JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道。
  • ConcurrentSkipListMap :跳表的实现。这是一个Map,使用跳表的数据结构进行快速查找。

Java 中垃圾回收机制中如何判断对象需要回收?常见的 GC 回收算法有哪些?

堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断那些对象已经死亡(即不能再被任何途径使用的对象)。

  • 引用计数法:给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。
    (这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。所谓对象之间的相互引用问题,就是除了对象 objA 和 objB 相互引用着对方之外,这两个对象之外再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 GC 回收器回收他们。)
  • 可达性分析算法:这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。

在这里插入图片描述

可作为GC Roots的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 本地方法栈(Native方法)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象

垃圾收集算法:

  • 标记-清除算法
  • 复制算法
  • 标记-整理算法
  • 分代收集算法

详细内容:JVM 垃圾回收

简述 JVM 的内存模型 JVM 内存是如何对应到操作系统内存的?

JVM 中内存模型是怎样的,简述新生代与年老代的区别?

JVM 是怎么去调优的?简述过程和调优的结果?

Java 线程和操作系统的线程是怎么对应的?Java线程是怎样进行调度的?

Java 常见锁有哪些,ReetrantLock 是怎么实现的,谈谈 synchronized 和 ReentrantLock 的区别?

synchronized:

synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

  • JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
  • 锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

“可重入锁” 指的是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。

  • 两者都是可重入锁;
  • synchronized 依赖于 JVM 实现的。 ReentrantLock是 JDK 层面实现的(也就是 API 层面,需要 lock()unlock() 方法配合 try/finally 语句块来完成)。
  • ReentrantLocksynchronized 增加了一些高级功能:
  • 等待可中断 : ReentrantLock提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly() 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
  • 可实现公平锁 : ReentrantLock可以指定是公平锁还是非公平锁。而 synchronized只能是非公平锁 。所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的 ReentrantLock(boolean fair) 构造方法来制定是否是公平的。
  • 可实现选择性通知(锁可以绑定多个条件): synchronized 关键字与 wait()notify() / notifyAll() 方法相结合可以实现等待 / 通知机制。ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition()方法。
  • Condition是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是 Condition 接口默认提供的。而synchronized关键字就相当于整个 Lock 对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。

synchronized 关键字和 volatile 关键字的区别?

volatile :

  • 在 JDK1.2 之前,Java 的内存模型实现总是从主存(即共享内存)读取变量,是不需要进行特别的注意的。
    而在当前的 Java 内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致
  • 要解决这个问题,就需要把变量声明为 volatile,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
    所以,volatile 关键字 除了防止 JVM 的指令重排 ,还有一个重要的作用就是保证变量的可见性。

并发编程的三个重要特性:

  • 原子性:是指一个操作是不可中断的。即使是多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。提供互斥访问,同一时刻只能有一个线程对数据进行操作;(atomicsynchronized 保证代码片段的原子性)
  • 比如,对于一个静态全局变量int i,两个线程同时对它赋值,线程A给他赋值为 1线程B给他赋值为-1。那么不管这两个线程以何种方式。何种步调工作,i 的值要么是1,要么是 -1。线程A和线程B之间是没有干扰的。这就是原子性的一个特点,不可被中断。
  • 可见性 :当一个变量对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。显然,对于串行来说,可见性问题是不存在的。 (synchronizedvolatile 关键字可以保证共享变量的可见性)
  • 有序性 :代码在执行的过程中的先后顺序,Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。volatile 关键字可以禁止指令进行重排序优化。

区别:

synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在!

  • volatile 关键字是线程同步的轻量级实现,所以volatile 性能肯定比 synchronized 关键字要好。
    但是 volatile 关键字只能用于变量synchronized 关键字可以修饰方法以及代码块
  • volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
  • volatile 关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。

什么是公平锁?什么是非公平锁?

  • 公平锁:就是先等待的线程先获得锁。过来加锁的线程全部是按照先来后到的顺序,依次进入等待队列中排队的,不会盲目的胡乱抢占加锁,非常的公平。
  • 非公平锁:在非公平锁策略之下,不一定说先来排队的线程就先会得到机会加锁,而是出现各种线程随意抢占的情况。

Java 的线程有哪些状态,转换关系是怎么样的?

在这里插入图片描述

线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。

在这里插入图片描述

由上图可以看出:

  • 线程创建之后它将处于 NEW 【初始状态】
  • 调用 start() 方法后开始运行,线程这时候处于 READY 【可运行状态】。可运行状态的线程获得了 cpu 时间片(timeslice)后就处于 RUNNING 【运行中状态】

注: 操作系统隐藏 Java 虚拟机(JVM)中的 READY 和 RUNNING 状态,它只能看到 RUNNABLE 状态,所以 Java 系统一般将这两个状态统称为 RUNNABLE 【运行状态】

  • 当线程执行 wait()方法之后,线程进入 WAITING 【等待状态】;进入等待状态的线程需要依靠其他线程的通知(notify()、notifyAll())才能够返回到运行状态;
  • TIME_WAITING 【超时等待状态】 相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)方法或 wait(long millis)方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 【运行状态】
  • 当线程调用同步方法 synchronized 时,在没有获取到锁的情况下,线程将会进入到 BLOCKED 【阻塞状态】
  • 线程在执行完毕 Runnable 的 run() 方法之后将会进入到 TERMINATED 【终止状态】

什么情况下会发生死锁,如何解决死锁?

线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

产生死锁必须具备以下四个条件:

  • 互斥条件:该资源任意一个时刻只由一个线程占用。
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

避免死锁,只要破坏产生死锁的四个条件中的其中一个就可以了:

  • 破坏互斥条件 :这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
  • 破坏请求与保持条件 :一次性申请所有的资源。
  • 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  • 破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

ThreadLocal 实现原理是什么?

通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢? JDK 中提供的ThreadLocal类正是为了解决这样的问题。 ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。

如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使用get()set() 方法来获取默认值将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。

import java.text.SimpleDateFormat;
import java.util.Random;

public class ThreadLocalExample implements Runnable{

     // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本
    private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));

    public static void main(String[] args) throws InterruptedException {
        ThreadLocalExample obj = new ThreadLocalExample();
        for(int i=0 ; i<10; i++){
            Thread t = new Thread(obj, ""+i);
            Thread.sleep(new Random().nextInt(1000));
            t.start();
        }
    }

    @Override
    public void run() {
        System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //formatter pattern is changed here by thread, but it won't reflect to other threads
        formatter.set(new SimpleDateFormat());

        System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());
    }

}

Output:

Thread Name= 0 default Formatter = yyyyMMdd HHmm
Thread Name= 0 formatter = yy-M-d ah:mm
Thread Name= 1 default Formatter = yyyyMMdd HHmm
Thread Name= 2 default Formatter = yyyyMMdd HHmm
Thread Name= 1 formatter = yy-M-d ah:mm
Thread Name= 3 default Formatter = yyyyMMdd HHmm
Thread Name= 2 formatter = yy-M-d ah:mm
Thread Name= 4 default Formatter = yyyyMMdd HHmm
Thread Name= 3 formatter = yy-M-d ah:mm
Thread Name= 4 formatter = yy-M-d ah:mm
Thread Name= 5 default Formatter = yyyyMMdd HHmm
Thread Name= 5 formatter = yy-M-d ah:mm
Thread Name= 6 default Formatter = yyyyMMdd HHmm
Thread Name= 6 formatter = yy-M-d ah:mm
Thread Name= 7 default Formatter = yyyyMMdd HHmm
Thread Name= 7 formatter = yy-M-d ah:mm
Thread Name= 8 default Formatter = yyyyMMdd HHmm
Thread Name= 9 default Formatter = yyyyMMdd HHmm
Thread Name= 8 formatter = yy-M-d ah:mm
Thread Name= 9 formatter = yy-M-d ah:mm

从输出中可以看出,Thread-0 已经改变了 formatter 的值,但仍然是 thread-2 默认格式化程序与初始化值相同,其他线程也一样。

ThreadLocal 原理:

查看源码:

public class Thread implements Runnable {
 ......
//与此线程有关的ThreadLocal值。由ThreadLocal类维护
ThreadLocal.ThreadLocalMap threadLocals = null;

//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
 ......
}

从上面 Thread类 源代码可以看出Thread 类中有一个 threadLocals 和 一个 inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量,我们可以把 ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap。默认情况下这两个变量都是 null,只有当前线程调用 ThreadLocal 类的 setget方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap类对应的 get()set()方法。

ThreadLocal类的set()方法:

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

通过上面这些内容,我们足以通过猜测得出结论:最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。 ThrealLocal 类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象。

每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocalkeyObject 对象为 value 的键值对。

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
 ......
}

比如我们在同一个线程中声明了两个 ThreadLocal 对象的话,Thread内部都是使用仅有的那个ThreadLocalMap 存放数据的,ThreadLocalMapkey 就是 ThreadLocal对象,value 就是 ThreadLocal 对象调用set方法设置的值。

Java 线程间有多少通信方式?

线程间通信的几种实现方式

Java 是如何实现线程安全的?

线程安全在三个方面体现:

  • 原子性:是指一个操作是不可中断的。即使是多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。提供互斥访问,同一时刻只能有一个线程对数据进行操作;(atomicsynchronized 保证代码片段的原子性)
  • 比如,对于一个静态全局变量int i,两个线程同时对它赋值,线程A给他赋值为 1线程B给他赋值为-1。那么不管这两个线程以何种方式。何种步调工作,i 的值要么是1,要么是 -1。线程A和线程B之间是没有干扰的。这就是原子性的一个特点,不可被中断。
  • 可见性 :当一个变量对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。显然,对于串行来说,可见性问题是不存在的。 (synchronizedvolatile 关键字可以保证共享变量的可见性)
  • 有序性 :代码在执行的过程中的先后顺序,Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。(volatile 关键字可以禁止指令进行重排序优化。(happens-before原则))

Java 中 sleep() 与 wait() 的区别?

  • 两者最主要的区别在于:sleep 方法没有释放锁,而 wait 方法释放了锁 。
  • 两者都可以暂停线程的执行。
  • Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
  • wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒。

Java 线程池里的 arrayblockingqueue 与 linkedblockingqueue 的使用场景和区别?

Arrayblockingqueue 与 Linkedblockingqueue,SynchronousQueue 都是 线程池 队列 的类型;

参考文章:ThreadPoolExecutor使用和思考(上)-线程池大小设置与BlockingQueue的三种实现区别

简述并实现工厂模式,工厂模式有什么常见问题?

参考文章:
漫画:设计模式之 “工厂模式” ?
漫画:什么是 “抽象工厂模式” ?

其他设计模式相关文章:
Web开发应了解的5种设计模式
Java开源框架中的设计模式以及应用场景
GoF 的 23 种设计模式
大神详解,这么详细的Java设计模式不收藏可惜了
设计模式目录:https://refactoringguru.cn/design-patterns/catalog

实现单例设计模式(懒汉,饿汉)?

/**
 * 双重校验锁实现对象单例(线程安全)
 */
public class Singleton {
    private Singleton() {}  //私有构造函数
    private volatile static Singleton instance = null;  //单例对象
    //静态工厂方法
    public static Singleton getInstance() {
          if (instance == null) {      //双重检测机制
         synchronized (Singleton.class){  //同步锁
           if (instance == null) {     //双重检测机制
             instance = new Singleton();
                }
             }
          }
          return instance;
      }
}

如果单例初始值是null,还未构建,则构建单例对象并返回。这个写法属于单例模式当中的懒汉模式
如果单例对象一开始就被new Singleton()主动构建,则不再需要判空操作,这种写法属于饿汉模式

参考内容:漫画:什么是单例模式?

CAS 实现原理是什么?

CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。
CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

参考答案:漫画:什么是 CAS 机制?

简述 Java 的 happen before 原则?

参考文章:java 8大happen-before原则超全面详解

什么是 CAP ?什么是最终一致性?

CAP原理:

  • C:Consistency,一致性。在分布式系统中的所有数据备份,在同一时刻具有同样的值,所有节点在同一时刻读取的数据都是最新的数据副本(all nodes see the same data at the same time)。
  • A:Availability ,可用性,好的响应性能。完全的可用性指的是在任何故障模型下,服务都会在有限的时间内处理完成并进行响应(Reads and writes always succeed)。
  • P:Partition Tolerance ,分区容错性,即分布式系统在遇到某些节点或网络分区故障的时候,仍然能够对外提供满足一致性或可用性的服务。分区容错性要求一个分布式系统中有某一个或者几个节点故障时,其他剩下的节点还能够正常运转并对外提供服务,对于用户而言并没有什么体验上的影响。
  • 任何分布式系统只可同时满足CAP三个指标中的两个,无法三者兼顾,这个结论就叫做 CAP 定理。

参考文章:

  1. CAP定理的含义
  2. https://blog.csdn.net/wangshuminjava/article/details/93613709
  3. 什么是CAP
  4. 采用最终一致性解决微服务一致性问题
  5. cap与一致性(强一致性、弱一致性、最终一致性)

什么是幂等操作? 如何设计 API 接口使其实现幂等性?

参考文章:
深入理解 幂等性
java幂等性的解决方案


【数据库】:

简述事务的四大特性(ACID)?

  • 原子性(Atomicity): 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
    (原子性由undo log日志保证,它记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的sql)
  • 一致性(Consistency): 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
    (一致性一般由代码层面来保证)
  • 隔离性(Isolation): 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
    (隔离性由MVCC来保证)
  • 持久性(Durability): 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
    (持久性由内存+redo log来保证,mysql修改数据同时在内存和redo log记录这次操作,事务提交的时候通过redo log刷盘,宕机的时候可以从redo log恢复)

并发事务带来哪些问题?

  • 脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
  • 丢失修改(Lost to modify): 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=18,事务1的修改被丢失。
  • 不可重复读(Unrepeatableread): 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
  • 幻读(Phantom read): 幻读与不可重复读类似。指在一个事务内多次读同一数据。一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

不可重复读和幻读区别:
不可重复读的重点是修改比如多次读取一条记录发现其中某些列的值被修改,幻读的重点在于新增或者删除比如多次读取一条记录发现记录增多或减少了。

数据库的事务隔离级别有哪些?各有哪些优缺点?

  • READ-UNCOMMITTED(读取未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
  • READ-COMMITTED(读取已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
  • REPEATABLE-READ(可重复读)【InnoDB默认】: 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
  • SERIALIZABLE(可串行化): 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
隔离级别				脏读    不可重复读      幻影读
READ-UNCOMMITTED	 √			√			√
READ-COMMITTED		 ×			√			√
REPEATABLE-READ		 ×			×			√
SERIALIZABLE		 ×			×			×

这里需要注意的是: 与 SQL 标准不同的地方在于 InnoDB 存储引擎在 REPEATABLE-READ(可重读) 事务隔离级别下使用的是Next-Key Lock 锁算法,因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server) 是不同的。
所以说InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读) 已经可以完全保证事务的隔离性要求,即达到了 SQL标准的 SERIALIZABLE(可串行化) 隔离级别。因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是 READ-COMMITTED(读取提交内容) ,但是你要知道的是InnoDB 存储引擎默认使用REPEAaTABLE-READ(可重读) 并不会有任何性能损失。
InnoDB 存储引擎在 分布式事务 的情况下一般会用到 SERIALIZABLE(可串行化) 隔离级别。

什么情况下会发生死锁,如何解决死锁?

死锁条件:互斥性、一个线程拿到资源不放、线程会一直等待拿锁、

线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

产生死锁必须具备以下四个条件:

  • 互斥条件:该资源任意一个时刻只由一个线程占用。
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

避免死锁,我们只要破坏产生死锁的四个条件中的其中一个就可以了:

  • 破坏互斥条件 :这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
  • 破坏请求与保持条件 :一次性申请所有的资源。
  • 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  • 破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

简述乐观锁以及悲观锁的区别以及使用场景、实现方式?

乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制(MVCC)和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁 (共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程) 。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁(共享锁),写锁(排他锁) 等,都是在做操作之前先上锁。Java中 synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

两种锁的使用场景:

从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种。
乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。
但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适

推荐文章:

  1. 面试必备之乐观锁与悲观锁
  2. 悲观锁与乐观锁的实现(详情图解)

InnoDB 是如何解决幻读的?

间隙锁是可重复读级别下才会有的锁,结合MVCC和间隙锁可以解决幻读的问题。
参考文章:幻读在 InnoDB 中是被如何解决的?

MySQL 有哪些常见的存储引擎,以及其区别?

MyISAM 和 InnoDB 区别:

MyISAM是MySQL的默认数据库引擎(5.5版之前)。
虽然性能极佳,而且提供了大量的特性,包括全文索引、压缩、空间函数等,
但MyISAM不支持事务和行级锁,而且最大的缺陷就是崩溃后无法安全恢复。

不过,5.5版本之后,MySQL引入了InnoDB(事务性数据库引擎),MySQL 5.5版本后默认的存储引擎为InnoDB。

大多数时候我们使用的都是 InnoDB 存储引擎,但是在某些情况下使用 MyISAM 也是合适的比如读密集的情况下。
(如果你不介意 MyISAM 崩溃恢复问题的话)。

两者的对比:

  • 1、是否支持行级锁 :MyISAM 只有表级锁(table-level locking),
    而InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。
  • 2、是否支持事务和崩溃后的安全恢复: MyISAM 强调的是性能,每次查询具有原子性,其执行速度比InnoDB类型更快,但是不提供事务支持。
    但是InnoDB 提供事务支持事务,外部键等高级数据库功能。 具有事务(commit)、回滚(rollback) 和 崩溃修复能力(crash recovery capabilities) 的事务安全(transaction-safe (ACID compliant))型表。
  • 3、是否支持外键: MyISAM不支持,而InnoDB支持。
  • 4、是否支持MVCC(多版本并发控制) :MVCC叫做多版本并发控制,实际上就是保存了数据在某个时间节点的快照。
    仅 InnoDB 支持。应对高并发事务, MVCC比单纯的加锁更高效;MVCC只在 READ COMMITTED(读取已提交)REPEATABLE READ(可重复读) 两个隔离级别下工作;
    MVCC可以使用 乐观(optimistic)锁 和 悲观(pessimistic)锁来实现;
    各数据库中MVCC实现并不统一。推荐阅读:MySQL-InnoDB-MVCC多版本并发控制
  • 5、InnoDB是聚集索引,数据文件是和索引绑在一起,必须要有主键,通过主键索引效率高。

《MySQL高性能》上面有一句话这样写到:

不要轻易相信“MyISAM比InnoDB快”之类的经验之谈,这个结论往往不是绝对的。
在很多我们已知场景中,InnoDB的速度都可以让MyISAM望尘莫及,尤其是用到了聚簇索引,或者需要访问的数据都可以放入内存的应用。

一般情况下我们选择 InnoDB 都是没有问题的,但是某些情况下你并不在乎可扩展能力和并发能力,也不需要事务支持,也不在乎崩溃后的安全恢复问题的话,选择MyISAM也是一个不错的选择。但是一般情况下,我们都是需要考虑到这些问题的。

联合索引的存储结构是什么?

聚簇索引和非聚簇索引有什么区别?什么情况用聚集索引?

MySQL索引使用的数据结构主要有 BTree索引 和 哈希索引 。对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择 BTree索引。

MySQL的BTree索引使用的是B树中的B+树,但对于主要的两种存储引擎的实现方式是不同的。

  • MyISAM:B+树 叶节点的data域存放的是 数据记录的地址。在索引检索的时候,首先按照 B+树 搜索算法搜索索引,如果指定的Key存在,则取出其 data 域的值,然后以 data 域的值为地址读取相应的数据记录。这被称为“非聚簇索引”。
  • InnoDB: 其数据文件本身就是索引文件。相比MyISAM的索引文件和数据文件是分离的,InnoDB的表数据文件本身就是按 B+树 组织的一个索引结构,树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。这被称为“聚簇索引(或聚集索引) ”。而其余的索引都作为辅助索引,辅助索引的data域存储相应记录主键的值而不是地址,这也是和MyISAM不同的地方。在根据主索引搜索时,直接找到key所在的节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,再走一遍主索引。 因此,在设计表的时候,不建议使用过长的字段作为主键,也不建议使用非单调的字段作为主键,这样会造成主索引频繁分裂。

主键索引、唯一索引与普通索引的区别是什么?为什么建议使用主键自增的索引?什么是回表?使用索引会有哪些优缺点?

  • 主键索引:主键索引是一种特殊的唯一索引,不允许有空值;主键索引的叶子节点存放的是整行的数据,主键索引也被称为聚簇索引
  • 普通索引:非主键索引的叶子节点存放的是主键字段的值,非主键索引也被称为二级索引。

参考文章:主键索引和非主键索引的区别,为什么建议使用主键自增的索引?

MySQL 为什么使用 B+ 树来作索引,对比 B 树它的优点和缺点是什么?

B+树:是有序的,支持范围查询;

MySQL 的索引什么情况下会失效?

假设建立联合索引 (a, b, c) 如果对字段 a 和 c 查询,会用到这个联合索引吗?

数据库索引的实现原理是什么?

数据库有哪些常见索引?数据库设计的范式是什么?

简述 MySQL 常见索引数据,介绍一下覆盖索引?

简述什么是最左匹配原则?

简述一致性哈希算法的实现方式及原理?

SQL优化的方案有哪些,如何定位问题并解决问题?

简述数据库中什么情况下进行分库,什么情况下进行分表?

简述 MySQL 的主从同步机制,如果同步失败会怎么样?

数据库的读写分离的作用是什么?如何实现?

并发事务会引发哪些问题?如何解决?

简述 SQL 中左连接和右连接的区别?

数据库查询中左外连接和内连接的区别是什么?

MySQL 中 join 与 left join 的区别是什么?

简述 MySQL 三种日志的使用场景?

模糊查询是如何实现的?

Cookie和Session的关系和区别是什么?

Kafka 发送消息是如何保证可靠性的?

为什么 Redis 在单线程下能如此快?

Redis 中 key 的过期策略有哪些?

简述 Redis 中常见类型的底层数据结构?

简述 Redis 的线程模型以及底层架构设计?

简述 Redis 的过期机制和内存淘汰策略?

简述 Redis 如何处理热点 key 访问?

Redis 有几种数据结构?Zset 是如何实现的?

简述 SortedSet 实现原理?

Redis 中,sentine l和 cluster 的区别和适用场景是什么?

Redis 序列化有哪些方式?

简述 Redis 的哨兵机制?

Redis 如何实现分布式锁?

Redis的缓存淘汰策略有哪些?

简述 Redis 中跳表的应用以及优缺点?

Redis 如何实现延时队列,分布式锁的实现原理?

简述 Redis 中如何防止缓存雪崩和缓存击穿?

简述 Redis 持久化中 rdb 以及 aof 方案的优缺点?

假设Redis 的 master 节点宕机了,你会怎么进行数据恢复?

【操作系统】:

进程和线程之间有什么区别?

进程间有哪些通信方式?

多线程和多进程的区别是什么?

为什么进程切换慢,线程切换快?

Linux 进程调度中有哪些常见算法以及策略?

简单介绍进程调度的算法?

线程有多少种状态,状态之间如何转换?

进程有多少种状态?

什么情况下,进程会进行切换?

进程通信中的管道实现原理是什么?

进程空间从高位到低位都有些什么?

两个线程交替打印一个共享变量?

线程间有哪些通信方式?

简述 Linux 虚拟内存的页面置换算法?

Linux 如何查看实时的滚动日志?

简述 Linux 零拷贝的原理?

Linux 中虚拟内存和物理内存有什么区别?有什么优点?

Linux 下如何查看 CPU 荷载,正在运行的进程,某个端口对应的进程?

Linux 下如何排查 CPU 以及 内存占用过多?

简述 Linux 系统态与用户态,什么时候会进入系统态?

简述几个常用的 Linux 命令以及他们的功能?

简述 LRU 算法及其实现方式?

简述同步与异步的区别,阻塞与非阻塞的区别?

简述操作系统如何进行内存管理?

简述操作系统中的缺页中断?

简述操作系统中 malloc 的实现原理?

操作系统如何申请以及管理内存的?

BIO、NIO 有什么区别?怎么判断写文件时 Buffer 已经写满?简述 Linux 的 IO模型?

操作系统中,虚拟地址与物理地址之间如何映射?

I/O多路复用中 select, poll, epoll之间有什么区别,各自支持的最大描述符上限以及原因是什么?

简述 mmap 的使用场景以及原理?

什么时候会由用户态陷入内核态?

简述 traceroute 命令的原理?

LVS 的 NAT、TUN、DR 原理及区别?

系统调用的过程是怎样的?操作系统是通过什么机制触发系统调用的?

简述 socket 中 select 与 epoll 的使用场景以及区别,epoll 中水平触发以及边缘触发有什么不同?

如何调试服务器内存占用过高的问题?

简述自旋锁与互斥锁的使用场景?

【网络协议】:

简述 TCP 三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手?

RestFul 与 RPC 的区别是什么?RestFul 的优点在哪里?

HTTP 与 HTTPS 有哪些区别?

RestFul 是什么?RestFul 请求的 URL 有什么特点?

一次 HTTP 的请求过程中发生了什么?

TCP 与 UDP 在网络协议中的哪一层,他们之间有什么区别?

TCP 中常见的拥塞控制算法有哪些?

TCP 怎么保证可靠传输?

从系统层面上,UDP如何保证尽量可靠?

TCP 的 keepalive 了解吗?说一说它和 http 的 keepalive 的区别?

简述 TCP 滑动窗口以及重传机制?

简述 HTTP 1.0,1.1,2.0 的主要区别?

简述 TCP 的 TIME_WAIT?

HTTP 的方法有哪些?

简述 TCP 协议的延迟 ACK 和累计应答?

简述 TCP 的报文头部结构?

简述 TCP 半连接发生场景?

什么是 SYN flood,如何防止这类攻击?

DNS 查询服务器的基本流程是什么?DNS 劫持是什么?

Cookie和Session的关系和区别是什么?

TCP 中 SYN 攻击是什么?如何防止?

TCP 四次挥手的时候 CLOSE_WAIT 的话怎么处理?

简述 WebSocket 是如何进行传输的?

简述常见的 HTTP 状态码的含义(301,304,401,403)?

TCP的拥塞控制具体是怎么实现的?UDP有拥塞控制吗?

简述 HTTPS 的加密与认证过程?

什么是跨域,什么情况下会发生跨域请求?

简述对称与非对称加密的概念?

简述 OSI 七层模型,TCP,IP 属于哪一层?

TCP四次挥手过程以及所处状态,为什么还需要有 time_wait?

TCP 在什么情况下服务端会出现大量 CLOSE_WAIT ?

简述什么是 XSS 攻击以及 CSRF 攻击?

简述 TCP 中的拥塞控制与滑动窗口机制?

SSL握手流程为什么要使用对称秘钥?

简述 JWT 的原理和校验机制?

TCP长连接和短连接有那么不同的使用场景?

HTTP 中 GET 和 POST 区别?

5 种常见的请求类型:

  • GET :请求从服务器获取特定资源。举个例子:GET /users(获取所有学生)
  • POST :在服务器上创建一个新的资源。举个例子:POST /users(创建学生)
  • PUT :更新服务器上的资源(客户端提供更新后的整个资源)。举个例子:PUT /users/12(更新编号为 12 的学生)
  • DELETE :从服务器删除特定的资源。举个例子:DELETE /users/12(删除编号为 12 的学生)
  • PATCH :更新服务器上的资源(客户端提供更改的属性,可以看做作是部分更新),使用的比较少,这里就不举例子了。

什么是 ARP 协议?

面试题来源: https://github.com/resumejob/interview-questions

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值