Java基础面试题

JVM、JRE、JDK之间的关系

  1. JDK 的全称(Java Development Kit Java 开发工具包)
    JDK = JRE + java 的开发工具 [java, javac,javadoc,javap 等]
  2. JRE(Java Runtime Environment Java 运行环境)
    JRE = JVM + Java 的核心类库[类]
  3. JVM 是一个虚拟的计算机,负责执行指令,管理数据、内存、寄存器,包含在JDK 中

public、protected、default、private的区别

java提供四种访问控制修饰符号,用于控制方法和属性(成员变量)的访问权(范围):

  1. 公开级别:用 public 修饰,对外公开
  2. 受保护级别:用 protected 修饰,对子类和同一个包中的类公开
  3. 默认级别:没有修饰符号,向同一个包的类公开.
  4. 私有级别:用 private 修饰,只有类本身可以访问,不对外公开

final、finally、finalize的区别

3.1 final是不可变得的意思,可以修饰变量,方法,类和局部变量。
使用的场景:
1.当不希望类被继承的时候
2.当不希望父类的某个方法被子类重写的时候
3.当不希望类的某个属性的值被修改的时候
4.当不希望某个局部变量被修改的时候
注意事项:
1.final修饰的属性在定义时,必须被赋值,其位置可以是:
(1)定义时
(2)构造器中
(3)代码块中
如果修饰的属性是静态的,则只能在定义时、静态代码块
2.final修饰的类,不可以被继承,但是可以实例化
3.非final类如果包括final修饰的方法,则该方法不可以被重写,但是可以被继承
4.final不能修饰构造方法
5.final和static搭配使用,效率更高,不会导致类的加载
6.包装类都是final类
3.2 finally
是异常处理的一部分,只能用在try/catch中,这段语句一定会执行,不管是否有异常
3.3 finalize

  1. 当对象被回收时,系统自动调用该对象的 finalize 方法。子类可以重写该方法,做一些释放资源的操作
  2. 什么时候被回收:当某个对象没有任何引用时,则 jvm 就认为这个对象是一个垃圾对象,就会使用垃圾回收机制来销毁该对象,在销毁该对象前,会先调用 finalize 方法。
  3. 垃圾回收机制的调用,是由系统来决定(即有自己的 GC 算法), 也可以通过 System.gc() 主动触发垃圾回收机制

⾯向对象、⾯向过程

面向过程的性能比较高,因为没有实例化等操作,开销比较小、
面向对象因为有了封装继承多态的特性,可以设计出低耦合的系统,使得系统更灵活,容易维护。
(1)封装:把抽象出来的数据(属性)和对数据的操作(方法)封装在一起。数据被保护在内部,程序的其他部分只有通过被授权的操作方法才能对数据进行操作。----隐藏细节,可以对数据进行验证
(2)继承:当多个类存在相同的属性(变量)和方法时,可以从这些类中抽象出父类,在父类中定义这些相同的属性和方法,所有的子类不需要重新定义这些属性和方法,只需要通过 extends 来声明继承父类即可。–提高复用性,提高维护性

继承的细节讨论:

  1. 子类继承了所有的属性和方法,非私有的属性和方法可以在子类直接访问, 但是私有属性和方法不能在子类直接访问,要通过父类提供公共的方法去访问
  2. 子类必须调用父类的构造器, 完成父类的初始化
  3. 当创建子类对象时,不管使用子类的哪个构造器,默认情况下总会去调用父类的无参构造器,如果父类没有提供无参构造器,则必须在子类的构造器中用 super 去指定使用父类的哪个构造器完成对父类的初始化工作,否则,编译不会通过
  4. 如果希望指定去调用父类的某个构造器,则显式的调用一下 : super(参数列表)
  5. super 在使用时,必须放在构造器第一行(super 只能在构造器中使用)
  6. super() 和 this() 都只能放在构造器第一行,因此这两个方法不能共存在一个构造器
  7. java 所有类都是 Object 类的子类, Object 是所有类的基类.
  8. 父类构造器的调用不限于直接父类!将一直往上追溯直到 Object 类(顶级父类)
  9. 子类最多只能继承一个父类(指直接继承),即 java 中是单继承机制。
  10. 不能滥用继承,子类和父类之间必须满足 is-a 的逻辑关系

(3)多态:多态的前提是:两个对象(类)存在继承关系。简单的说就是用同样的对象引用调用同样的方法但是做了不同的事情。重写和重载就是多态的方式,区别在于重写是运行时多态,重载是编译时多态

多态的向上转型

本质是父类的引用指向子类的对象
(1)可以调用父类中的所有成员
(2)不能调用子类种特有的成员
(3)最终运行结果看子类具体实现,运行的时候关注的是运行类型
多态向下转型
(1)只能强转父类的引用,不能强转父类的对象
(2)要求父类的引用必须指向当前目标类型的对象
(3)当向下转型后,可以调用子类类型中的所有成员

动态绑定机制:

(1)当调用对象方法时,该方法会和该对象的运行类型绑定
(2)当调用属性时,没有动态绑定机制,哪里声明,哪里使用

Switch

Switch参数:整数、枚举、字符、字符串
不能作为参数的:long、float、double、boolean

自动装箱与拆箱

装箱:基本包装类类型
拆箱:包装类基本
Integer a= 127 与 Integer b = 127相等吗
如果整型字面量的值在-128到127之间,那么自动装箱时不会new新的Integer对象,而是直接引用常量池中的 Integer对象,超过范围 则会新建对象

什么是反射机制?

在加载完类之后,在堆中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象中包括了类的完整的结构信息,通过这个对象就可以得到类的结构,这种机制成为反射。
静态加载:编译时加载相关的类,如果没有则报错,依赖性太强
动态加载:如果运行时不用该类,即使不存在该类,则不报错,降低了依赖性。

反射机制优缺点

优点:可以动态的创建和使用对象,使用灵活,没有反射机制,框架技术就失去了底层支撑
缺点,使用反射基本是解释执行,对执行的速度有影响

反射机制的应用场景有哪些?

(1)反射是框架设计的灵魂。
举例:①我们在使用JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序;
②Spring框架也用到很多反射机制,最经典的就是xml的配置模式。Spring 通过 XML 配置模式装
载 Bean 的过程:1) 将程序内所有 XML 或 Properties 配置文件加载入内存中; 2)Java类里面解析
xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息; 3)使用反射机
制,根据这个字符串获得某个类的Class实例; 4)动态配置实例的属性。

== 和 equals 的区别是什么?

1.==
(1)是一个比较运算符,即可以判断基本类型,又可以判断引用类型
(2)如果判断基本类型,判断的是值是否相等
(3)如果判断引用类型,判断的是地址是否相等,即判定是否为同一个对象
2.equals
(1)是Object类中的方法,只能判断引用类型
(2)默认判断的是地址是否相等,子类中往往重写该方法,可用于判断内容是否相等
重写equals方法

什么是网络编程

网络编程的本质是多台计算机之间的数据交换。在网络编程中,发起连接程序,也就是发送第一次请求的程序,被称作客户端(Client),等待其他程序连接的程序被称作服务器(Server)。客户端程序可以在需要的时候启动,而服务器为了能够时刻相应连接,则需要一直启动。

什么是TCP/IP和UDP

TCP/IP:传输控制协议/因特网互联协议,简单的说就是由网络层协议ip协议与传输层的tcp协议组成的。是面向连接的协议,发送数据前要先建立连接(发送方和接收方的成对的两个之间必须建立连接),TCP提供可靠的服务。UDP提供的是无连接的协议,发送数据前不需要建立连接,是不可靠性的协议。
TCP协议:传输控制协议
1.使用TCP协议前,须先建立TCP连接,形成传输数据通道
2.传输前,采用三次握手方式,是可靠的
3.进行通信的两个应用进程时客户端和服务端
4.在连接中可进行大数据量的传输
5.传输完毕,需要释放已建立的链接,效率低
UDP协议:用户数据协议
1.将数据封装成数据包,不需要建立链接
2.每个数据包的大小限制在64k内,不适合传输大量数据
3.因无需链接,因此是不可靠的
4.发送数据结束时无需释放资源,速度快

字符型常量和字符串常量的区别

  1. 形式上: 字符常量是单引号引起的一个字符 字符串常量是双引号引起的若干个字符
  2. 含义上: 字符常量相当于一个整形值(ASCII值),可以参加表达式运算 字符串常量代表一个地址值(该字符串在内存中存放位置)
  3. 占内存大小 字符常量只占一个字节 字符串常量占若干个字节(至少一个字符结束标志)

什么是字符串常量池?

字符串常量池位于堆(方法区)内存中,专门用来存储字符串常量,可以提高内存的使用率,避免开辟多块空间存储相同的字符串,在创建字符串时 JVM 会首先检查字符串常量池,如果该字符串已经存在池中,则返回它的引用,如果不存在,则实例化一个字符串放到池中,并返回其引用。

调⽤另⼀个构造器:this

Java虚拟机会给每个对象分配一个this

  1. this 关键字可以用来访问本类的属性、方法、构造器
  2. this 用于区分当前类的属性和局部变量
  3. 访问成员方法的语法:this.方法名(参数列表);
  4. 访问构造器语法:this(参数列表); 注意只能在构造器中使用(即只能在构造器中访问另外一个构造器, 必须放在第一条语句)
  5. this 不能在类定义的外部使用,只能在类定义的方法中使用

synchronized关键字

线程同步机制:
在多线程编程中,一些敏感数据不允许被多个线程同时访问,此时就是用同步访问技术,保证数据在任意同一时刻,最多有一个线程访问,以保证数据的完整性。
同步的具体方法:
1.同步代码块
2.同步在方法声明中
互斥锁:
(1)每个对象都对应于一个k可称为“互斥锁”的标记,这个标记用来保证在任意时刻只能有一个线程访问对象
(2)关键字synchronized来与对象的互斥锁联系
(3)同步方法(非静态的)的锁可以是this,也可以是其他对象(要求是同一个对象)
(4)同步方法(静态的)的锁为当前类本身
释放锁:
(1)当前线程的同步方法,同步代码块执行结束
(2)当前线程的同步方法,同步代码块中遇到break、return
(3)出现为处理的error或excetion,导致异常结束
(4)执行了线程对象的wait方法,当前线程暂停
不释放锁:
(1)程序调用Thread.sleep()方法、Thread.yield()方法暂停当前线程的执行
(2)线程执行同步代码块时,其他线程调用了suspend()方法将该线程挂起

Super:

super 代表父类的引用,用于访问父类的属性、方法、构造器
super和this的区别:
在这里插入图片描述

重载(Overload)和重写(Override)的区别

  1. 方法的重载和重写都是实现多态的方式。
  2. 重载发生在一个类中同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者参数顺序)则视为重载。重载对返回类型没有特殊的要求。
  3. 重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,不能比父类被重写方法声明更多的异常(里氏代换原则)。

抽象类(abstract class)和接口(interface)有什么异同?

  1. 抽象类
    当父类的某些方法需要声明,但不确定如何实现的时候,可以将其声明为抽象方法,当一个类中存在抽象方法时,需要将该类声明为 abstract 类。
    (1)抽象类不能被实例化
    (2)抽象类不一定要包括abstract方法,即抽象类可以没有abstract方法
    (3)一旦包含了abstract方法,则这个类必须声明为abstract
    (4)abstrat只能修饰类和方法,不能修饰属性和其他
    (5)抽象类可以有任意成员,比如构造器,静态属性、非抽象方法
    (6)抽象方法不能有主体,即不能实现
    (7)如果一个类继承了抽象类,则他必须实现抽象类的所有抽象方法,除非他自己也声明为abstract类
    (8)抽象方法不能使用private、final和static来修饰,因为这些关键字都是和重写相违背的

  2. 接口
    接口是更加抽象的抽象类(里面的抽象方法可以没有abstract),抽象类里面可以有方法体,接口里面所有的方法都没有方法体。接口体现了程序设计的多态和高内聚低耦合的设计思想。Jdk1.8或接口可以有静态方法,默认方法,即接口中可以有方法的具体实现。
    (1)接口不能被实例化,因为接口的功能就是需要其他类实现的
    (2)接口中的所有方法都是public方法,接口中抽象方法可以不用abstract修饰
    (3)普通类实现接口就必须将该接口的所有方法都实现
    (4)抽象类实现接口,可以不用实现接口的方法
    (5)一个类可以实现多个接口
    (6)接口中的属性,只能是final,而且是public static final,因此必须初始化
    (7)接口可以当成一个接口类,属性的访问形式是:接口名.属性名
    (8)接口不能继承其他的类(实现其他的接口),但是可以继承多个别的接口(继承多个也可以)
    (9)接口的修饰符只能是public和默认,这点和类的修饰符是一样的

  3. 接口和抽象类的异同
    (1)抽象类和接口都不能够实例化。
    (2)一个类如果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实现,否则该类仍然需要被声明为抽象类。
    (3)接口比抽象类更加抽象,因为抽象类中可以定义构造器,可以有抽象方法和具体方法,而接口中不能定义构造器而且其中的方法只能是抽象方法、静态方法和默认方法
    (4)抽象类中的成员可以是 private、默认、protected、public 的,而接口中的成员全都是 public(默认) 的
    (5)抽象类中可以定义成员变量,而接口中定义的成员变量实际上都是常量。
    (6)有抽象方法的类必须被声明为抽象类,而抽象类未必要有抽象方法。

final、finally、finalize的区别

final

final是不可变得的意思,可以修饰变量,方法,类和局部变量
使用的场景:
1.当不希望类被继承的时候
2.当不希望父类的某个方法被子类重写的时候
3.当不希望类的某个属性的值被修改的时候
4.当不希望某个局部变量被修改的时候
注意事项:
1.final修饰的属性在定义时,必须被赋值,其位置可以是:
(1)定义时
(2)构造器中
(3)代码块中
如果修饰的属性是静态的,则只能在(1)(3)
2.final修饰的类,不可以被继承,但是可以实例化
3.非final类如果包括final修饰的方法,则该方法不可以被重写,但是可以被继承
4.final不能修饰构造方法
5.final和static搭配使用,效率更高,不会导致类的加载
6.包装类都是final类

finally

是异常处理的一部分,只能用在try/catch中,这段语句一定会执行,不管是否有异常

finalize

1)当对象被回收时,系统自动调用该对象的 finalize 方法。子类可以重写该方法,做一些释放资源的操作。
2) 什么时候被回收:当某个对象没有任何引用时,则 jvm 就认为这个对象是一个垃圾对象,就会使用垃圾回收机制来销毁该对象,在销毁该对象前,会先调用 finalize 方法。
3) 垃圾回收机制的调用,是由系统来决定(即有自己的 GC 算法), 也可以通过 System.gc() 主动触发垃圾回收机制。

什么是集合,集合和数组的区别

(1)集合就是一个放数据的容器
(2)集合类存放的都是对象的引用,而不是对象的本身
(3)集合类型主要有3种:set(集)、list(列表)和map(映射)。
区别:
(1)数组是固定长度的;集合可变长度的。
(2)数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。
(3)数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。

List,Set,Map三者的区别?

Java 容器分为 Collection 和 Map 两大类,Collection集合的子接口有Set、List、Queue三种子接口。我们比较常用的是Set、List,Map接口不是collection的子接口。
Collection集合主要有List和Set两大接口

  1. List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多 个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。
  2. Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素只允许存入一 个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。
  3. Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、 ConcurrentHashMap

List—> Arraylist

(1)是由数组来实现数据存储的,维护了一个Object数组,并且可以加入多null
(2)ArrayList如果使用无参构造器,初始容量为0,当第一次添加数据时,扩容为10,满了以1.5倍扩容
(3)使用指定大小构造器,初始容量为指定大小,1.5倍扩容。
优点:查询效率较高
缺点:无法存储大数据量,增删效率比较低

LinkedList

(1)底层维护了一个双向链表,因此添加和删除的效率较高
(2)可以添加任何数据,包括null
(3)线程不安全,没有实现同步

Set–>HashSet

(1)HashSet底层实际上是HashMap(数组+链表+红黑树)
(2)可以存放null,但只有一个
(3)不保证元素有序,取决于hash(与hashcode有关)后,在确定索引的位置
(4)不能有重复元素
添加元素机制:
(1)当添加元素时,先得到hash值–>转成索引值
(2)找到存储数据表table,看这个索引位置是否已经存放元素,如果没有直接加入
(3)如果有则调用equals比较,相同放弃添加,不同添加到最后
(4)jdk8中,如果一条链表的元素个数达到8,并且table达到64,就会进行树化(红黑树)
扩容机制:
(1)第一次添加元素时,table数组扩容到16,临界值16*0.75=12
(2)如果table数组使用到了临界值(包括链表上面的值),就会进行2倍扩容
(3)jdk8中,如果一条链表的元素个数达到8,并且table达到64,就会进行树化(红黑树)

TreeSet—最大特点可以排序

  1. 当我们使用无参构造器,创建 TreeSet 时,仍然是无序的
  2. 使用 TreeSet 提供的一个构造器,可以传入一个比较器(匿名内部类)
  3. 底层是TreeMap
    (1)构造器把传入的比较器对象,赋给了 TreeSet 的底层的 TreeMap 的属性 this.comparator
    (2)在 调用 treeSet.add(“tom”), 在底层会执行到一个cpr比较器,该比较器会调用cpr.compare()方法并且动态绑定到我们传入的比较器,进行比较,如果相等返回0,这个数据加不进去。

Map–>HashMap

(1)k不可重复,v可以重复
(2)如果添加相同的k,会覆盖原来的k-v
(3)底层是数组+链表+红黑树,不保证映射顺序
(4)线程不安全
(5)键和值都可以为null
扩容机制:
(1)底层维护了Node类型的数组table,默认为null
(2)当创建对象时,将加载因子初始化为0.75
(3)当添加元素时,通过key的哈希值得到table索引。如果没有元素直接添加,否则判断该元素的key是否相等,如果相等直接替换val;如果不相等需要判断是树结构还是链表结构并作出相应的处理。
(4)第一次添加的初始容量为16,临界值为0.75*16
(5)扩容以2倍扩容
(6)java8中如果一条链表的元素超过8,并且table大小超过64就会树化

HashTable

(1)存放的元素是键值对
(2)键和值都不能为null
(3)是线程安全的,hashMap是线程不安全的
(4)底层的table存储的是Hashtable$Entry

TreeMap—底层是一个entry

使用默认的构造器,创建 TreeMap, 是无序的(也没有排序),不可重复

  1. 构造器. 把传入的实现了 Comparator 接口的匿名内部类(对象),传给给 TreeMap 的 comparator
  2. 调用 put 方法
    2.1 第一次添加, 把 k-v 封装到 Entry 对象,放入 root
    2.2 以后添加在底层会执行到一个cpr比较器进行比较

迭代器 Iterator 是什么?

(1)Iterator对象成为迭代器,主要用于遍历Collection集合中的元素。
(2)所有实现了Collection接口的集合类都有一个iteractor()方法,用于返回一个实现Iterator接口的对象,即返回一个迭代器
(3)Iteractor仅用于遍历集合,本身并不存放对象
(4)再调用iteractor.next()方法之前必须要调用iteractor.hasnext()方法进行检测。若不调用,且下一条记录无效,会出现nosuchelementexception异常

ArrayList 和 LinkedList 的区别是什么?

底层实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。
随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。
增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。
内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储 了两个引用,一个指向前一个元素,一个指向后一个元素。
线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。

ArrayList 和 Vector 的区别是什么?

这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序集合
(1)线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。
(2)扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,当调用无参构造器的时候,初始容量都为10,只不过在 Vector 2倍扩容,ArrayList 只会1.5倍扩容。

List 和 Set 的区别

(1)List , Set 都是继承自Collection 接口
(2)List 特点:
1)元素有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,
2)每个元素都有其对应的索引,支持索引。
3)Lsit容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号取出容器中的元素。
4)常用的实现类有 ArrayList、LinkedList 和 Vector,Stack。
(3)Set 特点:
1)一个无序(存入和取出顺序有可能不一致)容器,没有索引。
2)不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。
3)Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。
另外 List 支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。
Set和List对比
Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变

Collection 和 Collections 的区别?

  1. Collection 是一个接口,它是 Set、List 等容器的父接口;
  2. Collections 是个一个工具类,提供了一系列的静态方法来辅助容器操作,这些方法包括对容器的搜索、排序、线程安全化等等。

sleep 与 wait 区别

  1. 对于 sleep()方法,我们首先要知道该方法是属于 Thread 类中的。而 wait()方法,则是属于Object 类中的。
  2. sleep()方法导致了程序暂停执行指定的时间,让出 cpu 该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。
  3. 在调用 sleep()方法的过程中,线程不会释放对象锁。
  4. 而当调用 wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态,继续争夺cup资源
  5. wait 必须搭配 synchronize 一起使用,而 sleep 不需要;
  6. 在调用 wait 方法之后,线程会变为 WATING 状态,而调用 sleep 方法之后,线程会变为 TIMED_WAITING 状态。

start 与 run 区别

  1. start()方法来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,可以直接继续执行下面的代码。
  2. 通过调用 Thread 类的 start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。
  3. 方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行 run 函数当中的代码。 Run 方法运行结束, 此线程终止。然后 CPU 再调度其它线程。

线程的 sleep()方法和 yield()方法有什么区别?

  1. sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的
    线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的
    机会;
  2. 线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转
    入就绪(ready)状态;也就是说yield方法让出线程后有可能又执行了。
  3. sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异
    常;
  4. sleep()方法比 yield()方法(跟操作系统 CPU 调度相关)具有更好的可移植性

请说出与线程同步以及线程调度相关的方法。

  1. wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
  2. sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用
    此方法要处理 InterruptedException 异常;
  3. notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并
    不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且
    与优先级无关;
  4. notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;

什么是线程池(thread pool)?

在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内
存资源或者其它更多资源。在 Java 中更是如此,虚拟机将试图跟踪每一个对象,
以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽
可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就
是”池化资源”技术产生的原因。线程池顾名思义就是事先创建若干个可执行的
线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完
毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。

Java 中,throw 和 throws 有什么区别

  1. throws 用在方法声明出,后面跟的是异常类型,可以跟多个;而 throw 用在方法体重,后面跟的是异常对象。
  2. throws是异常处理的一种方式;throw是手动生成异常对象的关键字。
  3. throws 表示出现异常的一种可能性,并不一定会发生这些异常;throw 则是抛出了异常,执行 throw 则一定抛出了某种异常对象。

String 和 StringBuilder、StringBuffer 的区别?

(1)String对象实现了serializable和comparable接口,可以串行化和比较
(2)对象用于保存字符串,即一组字符序列
(3)类是final类不可以被其他类继承,一个字符串一旦被分配,其内容是不可变的,添加内容会重新开辟空间。
(4)类中有属性是由final修饰的char数组value,用于存放字符串内容
注意:value是final类行,不可以修改地址,但是里面的单个字符内容是可以修改的
两种创建String对象的区别:
(1)先从常量池查看是否有字符串的数据空间,如果有直接指向;如果没有则重新创建,然后指向。最后指向的是常量池的空间地址
(2)先在堆中创建空间,里面维护了一个value属性,指向常量池的空间。最终指向的是堆中的空间地址。
String类是保存字符串常量,每次更新需要重新开辟空间,效率较低,因此设计来增强String的功能,并提升效率。
StringBuffer
(1)直接父类是AbstractStringBuilder,实现了sreializable接口,既可以串行化
(2)在父类AbstractStringBuilder有属性char[]value,不是final,该数组存放字符串内容,引用存放在堆中
(3)是一个final类,不能被继承
STRINGBUILDER
(1)直接父类是AbstractStringBuilder,实现了sreializable接口,既可以串行化
(2)在父类AbstractStringBuilder有属性char[]value,不是final,该数组存放字符串内容,引用存放在堆中
(3)是一个final类,不能被继承
(4)方法没有做互斥处理,即没有synchronized关键字,因此在单线程的情况下使用
效率测试:StringBuilder > StringBuffer > String

区别

String和StringBuffffer、StringBuilder的区别是什么?String为什么是不可
变的
(1)可变性
String类中使用字符数组保存字符串,private final char value[],所以string对象是不可变的。StringBuilder与StringBuffffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,char[] value,这两种对象都是可变的。
(2)线程安全性
String中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder是 StringBuilder与StringBuffffer的公共父类,定义了一些字符串的基本操作,StringBuffffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。
(3)性能
每次对String 类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String 对象。StringBuffffer每次都会对StringBuffffer对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用StirngBuilder 相比使用StringBuffffer 仅能获得10%~15% 左右的性能提升,但却要冒多线程不安全的风险。

抽象的(abstract)方法是否可同时是静态的(static),是否可同时是本地方法(native),是否可同时被 synchronized修饰?

都不能。抽象方法需要子类重写,而静态的方法是无法被重写的,因此二者是矛
盾的。本地方法是由本地代码(如 C 代码)实现的方法,而抽象方法是没有实现
,也是矛盾的。synchronized 和方法的实现细节有关,抽象方法不涉及实现细
,因此也是相互矛盾的。

static关键字啥作⽤?

static是类修饰符,用于修饰变量、方法、代码块、内部类
1.static可以修饰变量
(1)当需要某个类的所有对象都共享一个变量时,可以使用
(2)类变量可以通过类名.类变量名
(3)类变量在类的加载时就初始化了,随着类的加载开始
2.static修饰方法
(1)普通方法和类方法都随着类的加载而加载,但普通方法在加载后没有分配内存。类方法中不允许使用任何对象相关得关键字,this和super
(2)类方法中只能访问静态变量/方法,因为静态方法的初始化优于普通方法
3. static代码块
作用就是对类进行初始化,而且它随着类的加载而执行(因此可用静态代码块是否被执行来判断类的加载),只会执行一次。如果是普通代码块,每创建一个对象就执行。
创建一个对象时在,在一个类的调用顺序是:
(1)调用静态代码块和静态属性初始化(这个初始化赋值是程序员给定的值,而不是系统默认初始化),注意:静态代码块和静态属性优先级一样,他们按照顺序执行。
(2)调用普通代码块和普通属性初始化(这个初始化也是程序员给的),这个优先级顺序也是按照顺序执行。
(3)调用构造方法
(4)构造器1前面隐含了super()和调用普通代码块的过程,静态代码块和属性初始化,在类加载时,就执行完毕,因此优于构造器和普通代码块。
4.static修饰内部类—静态内部类
(1)静态内部类是定义在外部类的成员位置,并且有static修饰
(2)可以添加任意访问修饰符,因为它的地位就相当于一个成员变量
(3)作用域为整个类体
(4)静态内部类访问外部类的静态成员可以直接访问外部类的所有静态成员
(5)外部类访问静态内部类的成员两种方式:1.构建方法,创建对象,返回对象2.通过类名访问。
(6)如果外部类和静态内部类的成员重名时,静态内部类访问时,默认遵循就近原则,如果想访问外部类的成员,可以使用外部类.成员去访问

类的加载机制

1.Java程序在计算机有哪三个阶段?
在这里插入图片描述

2.获取class类对象的方法
(1)已知一个类的全类名,可通过class类的静态方法Class.forName,多用于配置文件(编译阶段)。
(2)已知具体的类,通过.class获取,多用于参数传递,比如通过反射得到对应的构造器对象(类加载阶段)。
(3)已知某个类的实例,通过getClass()方法获取Class对象(运行阶段)。
(4)通过类加载器classLoader,然后通过classLoader.loadClass(classAllPath)获取Class对象。
(5)基本数据类型通过;Class integerClass = int.class;
(6)基本数据类型对应的包装类: Class type1 = Integer.TYPE;
3.类加载
(1)静态加载:编译时加载相关的类,如果没有这个类则报错,依赖性太强
(2)动态加载:运行时加载相关的类,如果运行时不用该类,即使该类不存在也不会报错,降低了依赖性。
4.类加载的时机
(1)当创建对象的时候(new)–静态加载
(2)当子类被加载的时候,父类也会被加载。原因是继承的本质,当存在父类的时候,父类会被优先加载—静态加载
(3)当调用类中的静态成员时—静态加载
(4)通过反射的时候—动态加载
5.类加载的过程图
在这里插入图片描述

(1)类加载阶段:JVM在该阶段主要是将字节码从不同的数据源(class字节码文件、或者jar包)通过类类加载器转换为二进制字节流加载到内存中,同时生成一个代表类的Class对象。注意此时生成了两个文件class对象和二进制字节码文件,其中class对象要依赖于二进制字节字节流文件
(2)连接阶段-验证:目的是为了确保.class文件的字节流文件中包含的信息是否符合当前虚拟机的要求,且不会危害虚拟机自身的安全,包括文件格式的验证(.class文件是否以cafebabe开头)、元数据验证、符号引用验证等。
(3)连接阶段-准备:JVM在该阶段对静态变量分批内存,并默认初始化。
(4)连接阶段-解析:简单来说就是在编译阶段,此时还没有真正进入内存,两个类是靠符号进行引用的。等到进入类加载阶段就会给两个类分配内存,此时是靠地址进行引用的。
通过以上阶段,将二进制文件和并到JRE中,此时就成了一个可运行的状态。
(5)初始化initialization:到这个阶段才开始真正运行java中的程序代码,此阶段执行的是clinit()方法的过程,该方法是由编译器按照语句在源文件中出现的顺序,依次自动收集类中的所有静态变量的赋值动作和静态代码块中的语句,并进行合并。虚拟机会保证一个类的<clinit>() 方法在多线程环境中被正确的加锁,同步,如果多个线程同时去初始化一个类,那么只有一个线程去执行这个类的<clinit>() 方法。 正因为有这个机制,才能保证某个类在内存中, 只有一份 Class 对象。

双亲委派机制

双亲委派机制是当类加载器需要加载某一个.class字节码文件时,则首先会把这个任务委托给他的上级类加载器,递归这个操作,如果上级没有加载该.class文件,自己才会去加载这个.class。这是一种任务委派模式。
原理:

  1. 如果一个类加载器收到了要加载某个类的请求,它要做的首要事情不是加载,而是将这个请求委托给父类的加载器去执行。
  2. 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
  3. 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
    在这里插入图片描述
    双亲委派机制的作用:
    (1)安全性:确保java核心类库提供的类不会被自定义的类所破坏
    (2)防止重复加载同一个.class文件。
    (3)不同的类加载器可以为相同名称(binary name)的类创建额外的命名空间(通过自定义类)。相同名称的类可以并存在Java虚拟机中,只需要用不同的类加载器来加他们即可,不同类加载器所加载的类是不兼容的,这就相当于在Java虚拟机内部创建了一个又一个相互隔离的Java类空间。

如何实现对象克隆?

有两种方式:
1). 实现 Cloneable 接口并重写 Object 类中的 clone()方法(这个方法是Object里面的,是一个native方法,是由C实现的);
1.1 深拷贝:如果里面有一个对象,拷贝后这个成员的属性是会重新开辟
1.2 浅拷贝:如果里面有一个对象,拷贝后这个成员的属性是和之前的共享的空间
2). 实现 Serializable 接口(通过io流技术),通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆;
序列化和反序列化:
1.序列化就是在保存数据时,保存数据的值和数据类型
2.反序列化就是在恢复数据时,恢复数据的值和数据类型
2.1 读写顺序要一致
2.2 要求序列化对象和反序列化对象,需要实现Serializable
2.3 序列化对象时,默认将里面的所有属性都进行序列化,除了static或Transient修饰的成员(为null)
2.4 序列化对象时,要求里面属性的类型也需要实现序列化接口
2.5 序列化具备可继承性,也就是如果某个类已经实现实现了序列化,他的所有子类也已经默认实现了序列化

线程的基本状态以及状态之间的关系?

在这里插入图片描述

反射相关

获得一个类的类对象有哪些方式?

  1. 已知一个类的全类名,可通过class类的静态方法Class.forName,多用于配置文件(编译阶段)。
  2. 已知具体的类,通过.class获取,多用于参数传递,比如通过反射得到对应的构造器对象(类加载阶段)。
  3. 已知某个类的实例,通过getClass()方法获取Class对象(运行阶段)。
  4. 通过类加载器classLoader,然后通过classLoader.loadClass(classAllPath)获取Class对象。
  5. 基本数据类型通过;Class integerClass = int.class;
  6. 基本数据类型对应的包装类: Class type1 = Integer.TYPE;

如何通过反射创建对象?

  1. 调用类中的public修饰的无参构造器。
		// 先获取到 User 类的 Class 对象
        Class<?> userClass = Class.forName("com.hspedu.reflection.User");
        //1.通过 public 的无参构造器创建实例
        Object o = userClass.newInstance();
  1. 调用类中的指定构造器。
    2.1 调用public修饰的构造器
		// 先得到对应构造器
        Constructor<?> constructor = userClass.getConstructor(String.class);
        //3.2 创建实例,并传入实参
        Object hsp = constructor.newInstance("hsp");

2.2 通过非 public 的有参构造器创建实例

		//得到 private 的构造器对象
        Constructor<?> constructor1 = userClass.getDeclaredConstructor(int.class, String.class);
        //创建实例
        //暴破【暴力破解】 , 使用反射可以访问 private 构造器/方法/属性, 反射面前,都是纸老虎
        constructor1.setAccessible(true);
        Object user2 = constructor1.newInstance(100, "张三丰");

如何通过反射获取和设置对象私有字段的值?

  		//1. 得到 Boss 类对应的 Class 对象
        Class<?> bossCls = Class.forName("com.hspedu.reflection.Boss");
        //2. 创建对象
        Object o = bossCls.newInstance();
        //3. 调用 public 的 hi 方法
        //Method hi = bossCls.getMethod("hi", String.class);//OK
        //3.1 得到 hi 方法对象
        Method hi = bossCls.getDeclaredMethod("hi", String.class);//OK
        //3.2 调用
        hi.invoke(o, "韩顺平教育~");


        //4. 调用 private static 方法
        //4.1 得到 say 方法对象
        Method say = bossCls.getDeclaredMethod("say", int.class, String.class, char.class);
        //4.2 因为 say 方法是 private, 所以需要暴破,原理和前面讲的构造器和属性一样
        say.setAccessible(true);
        System.out.println(say.invoke(o, 100, "张三", '男'));

简述一下你了解的设计模式(案例)。

所谓设计模式,就是一套被反复使用的代码设计经验的总结(情境中一个问题经
过证实的一个解决方案)。使用设计模式是为了可重用代码、让代码更容易被他
人理解、保证代码可靠性。设计模式使人们可以更加简单方便的复用成功的设计
和体系结构。将已证实的技术表述成设计模式也会使新系统开发者更加容易理解
其设计思路。

  1. 工厂模式:工厂类可以根据条件生成不同的子类实例,这些子类有一个公
    共的抽象父类并且实现了相同的方法,但是这些方法针对不同的数据进行了不同的操作(多态方法)。当得到子类的实例后,开发人员可以调用基类中的方法而不必考虑到底返回的是哪一个子类的实例。
  2. 代理模式:给一个对象提供一个代理对象,并由代理对象控制原对象的引
    用。实际开发中,按照使用目的的不同,代理可以分为:远程代理、虚拟代理、保护代理、Cache 代理、防火墙代理、同步化代理、智能引用代理(线程代理类 , 模拟了一个极简的 Thread 类)。
  3. 适配器模式:把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起使用的类能够一起工作。
  4. 模板方法模式:提供一个抽象类,将部分逻辑以具体方法或构造器的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法(多态实现),从而实现不同的业务逻辑。模板方法模式,一般是为了统一子类的算法实现步骤,所使用的一种手段或者说是方式。它在父类中定义一系列算法的步骤,而将具体的实现都推迟到子类(抽象类的最佳实践)。
  5. 修饰器模式:模拟io流的处理流

volatile关键字

一旦一个共享变量类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  1. 可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
//线程1
 
boolean stop = false; 
while(!stop){ 
    doSomething();
}
//线程2 
stop = true;

下面解释一下这段代码:
每个线程在运行过程中都有自己的工作内存(cpu的高速缓存中),那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。
那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。
但是用volatile修饰之后就变得不一样了:
(1) 第一:使用volatile关键字会强制将修改的值立即写入主存;
(2)使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效
(3)在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。
2. 有序性
一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的(因为会考虑指令之间的数据依赖性),这样可能会会影响到多线程并发执行的正确性。
volatile能在一定程度上保证有序性。

x = 2;        //语句1
y = 0;        //语句2
flag = true;  //语句3
x = 4;         //语句4
y = -1;       //语句5

flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。
3. 原子性
在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了;然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,所以线程2会直接去主存读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作内存。

volatile 修饰符的有过什么实践?

一种实践是用 volatile 修饰 long 和 double 变量,使其能按原子类型来读写。double 和 long 都是 64 位宽,因此对这两种类型的读是分为两部分的,第一次读取第一个 32 位,然后再读剩下的 32 位,这个过程不是原子的,但 Java 中volatile 型的 long 或 double 变量的读写是原子的。

a = a + b 与 a += b 的区别

+= 隐式的将加操作的结果类型强制转换为持有结果的类型。

byte a = 127;
byte b = 127; b = a + b; // error : cannot convert from int to byte
b += a; // ok

集合和数组的区别

(1)数组是固定长度的;集合可变长度的。
(2)数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。
(3)数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。

常用的集合类有哪些?

Map接口和Collection接口是所有集合框架的父接口:

  1. Collection接口的子接口包括:Set接口和List接口
  2. Map接口的实现类主要有:HashMap、TreeMap、Hashtable以及
    Properties等
  3. Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等
  4. List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等

Collection集合类

1 Set
(1)TreeSet 基于红黑树实现,支持有序性操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。
(2)HashSet 基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。
(3)LinkedHashSet 具有 HashSet 的查找效率,使用哈希值进行插入,同时且内部使用双向链表维护元素的插入顺序,使得元素看起来是按照循序插入的。
2 List
(1)ArrayList 基于动态数组实现,支持随机访问。
(2)Vector 和 ArrayList 类似,但它是线程安全的。
(3)LinkedList 基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。遍历是通过迭代器的方式。

List,Set,Map三者的区别?

Java 容器分为 Collection 和 Map 两大类,Collection集合的子接口有Set、List、Queue三种子接口。我们比较常用的是Set、List,Map接口不是collection的子接口。
Collection集合主要有List和Set两大接口
List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多
个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。
Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一
个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及
TreeSet。
Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、
ConcurrentHashMap

集合框架底层数据结构

7.1 Collection
7.1.1. List—> Arraylist
(1)是由数组来实现数据存储的,维护了一个Object数组,并且可以加入多null
(2)ArrayList如果使用无参构造器,初始容量为0,当第一次添加数据时,扩容为10,满了以1.5倍扩容
(3)使用指定大小构造器,初始容量为指定大小,1.5倍扩容
优点:查询效率较高
缺点:无法存储大数据量,增删效率比较低—链表,线程不安全
7.1.1 LinkedList:
(1)底层维护了一个双向链表,因此添加和删除的效率较高
(2)可以添加任何数据,包括多个null
(3)线程不安全,没有实现同步
7.2. Set
7.2.1HashSet
(1)HashSet底层实际上是HashMap(数组+链表+红黑树)
(2)可以存放null,但只有一个
(3)不保证元素有序,取决于hash(与hashcode有关)后,在确定索引的位置
(4)不能有重复元素
添加元素机制:
(1)当添加元素时,先得到hash值转成索引值
(2)找到存储数据表table,看这个索引位置是否已经存放元素,如果没有直接加入
(3)如果有则调用equals比较,相同放弃添加,不同添加到最后
(4)jdk8中,如果一条链表的元素个数达到8,并且table达到64,就会进行树化(红黑树)
扩容机制:
(1)第一次添加元素时,table数组扩容到16,临界值16*0.75=12
(2)如果table数组使用到了临界值(包括链表上面的值),就会进行2倍扩容
(3)jdk8中,如果一条链表的元素个数达到8,并且table达到64,就会进行树化(红黑树)
7.2.2 TreeSet—最大特点可以排序

  1. 当我们使用无参构造器,创建 TreeSet 时,仍然是无序的
  2. 使用 TreeSet 提供的一个构造器,可以传入一个比较器(匿名内部类)
  3. 底层是TreeMap
    (1)构造器把传入的比较器对象,赋给了 TreeSet 的底层的 TreeMap 的属性 this.comparator
    (2) 在 调用 treeSet.add(“tom”), 在底层会执行到一个cpr(将comparator赋值给他)比较器进行比较,如果相等返回0,这个数据加不进去
    7.2.3 LinkedHashSet
    (1)是HashSet的子类
    (2)底层是一个LIinkedHashMap,维护了一个数组+双向链表
    (3)根据hashcode值来决定元素的存储位置,同时用链表维护元素的次序,使得元素看起来是以插入顺序保存的。
    7.3 Map
    7.3.1 HashMap
    (1)k不可重复,v可以重复
    (2)如果添加相同的k,会覆盖原来的k-v
    (3)底层是数组+链表+红黑树,不保证映射顺序
    (4)线程不安全
    扩容机制:
    (1)底层维护了Node类型的数组table,默认为null
    (2)当创建对象时,将加载因子初始化为0.75
    (3)当添加元素时,通过key的哈希值得到table索引。如果没有元素直接添加,否则判断该元素的key是否相等,如果相等直接替换val;如果不相等需要判断是树结构还是链表结构并作出相应的处理。
    (4)第一次添加的初始容量为16,临界值为0.75*16
    (5)扩容以2倍扩容
    (6)java8中如果一条链表的元素超过8,并且table大小超过64就会树化
    7.3.2 HashTable
    (1)存放的元素是键值对
    (2)建和值都不能为null
    (3)是线程安全的,hashMap是线程不安全的
    (4)底层的table存储的是Hashtable$Entry
    7.3.3TreeMap—底层是一个entry,是红黑数
    使用默认的构造器,创建 TreeMap, 是无序的(也没有排序),不可重复
  4. 构造器. 把传入的实现了 Comparator 接口的匿名内部类(对象),传给给 TreeMap 的 comparator
  5. 调用 put 方法
    2.1 第一次添加, 把 k-v 封装到 Entry 对象,放入 root
    2.2 以后添加在底层会执行到一个cpr比较器进行比较

说一下 ArrayList 的优缺点

(1)是由数组来实现数据存储的,维护了一个Object数组,并且可以加入多null
(2)ArrayList如果使用无参构造器,初始容量为0,当第一次添加数据时,扩容为10,满了以1.5倍扩容
(3)使用指定大小构造器,初始容量为指定大小,1.5倍扩容
优点:查询效率较高,线程不安全
缺点:无法存储大数据量,增删效率比较低—链表

ArrayList 和 LinkedList 的区别是什么?

底层实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。
随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。
增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。
内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储 了两个引用,一个指向前一个元素,一个指向后一个元素。
线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。

ArrayList 和 Vector 的区别是什么?

这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序集合
(1)线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。
(2)扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,当调用无参构造器的时候,初始容量都为10,只不过在 Vector 2倍扩容,ArrayList 只会1.5倍扩容。

List 和 Set 的区别

(1)List , Set 都是继承自Collection 接口
(2)List 特点:
1)元素有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,
2)每个元素都有其对应的索引,支持索引。
3)Lsit容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号取出容器中的元素。
4)常用的实现类有 ArrayList、LinkedList 和 Vector,Stack。
(3)Set 特点:
1)一个无序(存入和取出顺序有可能不一致)容器,没有索引。
2)不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。3)Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。
另外 List 支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。
Set和List对比
Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变

说一下HashMap的实现原理?

(1)HashMap实现了Map接口
(2)以k-v键值对的方式存储数据(HashMap$Node类型)
(3)key不能重复,但是值可以重复,允许null值和null键
(4)如果添加相同的key,则会覆盖原来的k-v等同于修改
(5)不保证映射顺序,底层是通过hash表的方法存储的(数组+链表+红黑树)
(6)hashmap没有实现同步,没有synchronized,因此是线程不安全的

HashMap的底层实现

扩容机制:
(1)底层维护了Node类型的数组table,默认为null
(2)当创建对象时,将加载因子初始化为0.75
(3)当添加元素时,通过key的哈希值得到table索引。如果没有元素直接添加,否则判断该元素的key是否相等,如果相等直接替换val;如果不相等需要判断是树结构还是链表结构并作出相应的处理。
(4)第一次添加的初始容量为16,临界值为0.75*16
(5)扩容以2倍扩容
(6)java8中如果一条链表的元素超过8,并且table大小超过64就会树化

HashMap 与 HashTable 有什么区别?

  1. 线程安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过 synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap );
  2. 效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它
  3. 对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛NullPointerException。
  4. 创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。
  5. 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为16,然后以2倍扩容。
  6. 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,并且总的table长度大于64,将链表转化为红黑树,以减少搜索时间。

HashMap 和 ConcurrentHashMap 的区别

  1. ConcurrentHashMap有synchronized锁发性能更好,是线程安全的。
  2. HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。
    ConcurrentHashMap 结合了 HashMap 和 HashTable 二者的优势。HashMap 没有考虑同步,HashTable 考虑了同步的问题使用了synchronized 关键字,所以 HashTable 在每次同步执行时都要锁住整个结构。 ConcurrentHashMap 锁的方式是稍微细粒度的。

JAVA 四中引用类型

  1. 强引用
    把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。
  2. 软引用
    软引用需要用 SoftReference 类来实现。当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。
  3. 弱引用
    弱引用需要用 WeakReference 类来实现。只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。
  4. 虚引用
    虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。

JVM是什么?

JVM全称是Java Virtual Machine(Java虚拟机),正因为这个虚拟机,Java的运行环境实现跨平台。

JVM架构图分析

VM被分为三个主要的子系统
(1)类加载器子系统
(2)运行时数据区
(3)执行引擎

运行时数据区

1. 方法区(Method Area)

方法区中那个主要存放的是字符串常量static、所有的类加载信息(元数据)成员方法

2.堆区

所有的对象class类和它们相应的实例变量以及数组将被存储在这里。每个JVM同样只有一个堆区。由于方法区和堆区的内存由多个线程共享,所以存储的数据不是线程安全的

3. 栈区

函数的方法在栈中开辟空间,所有的局部变量对象的引用将在栈内存中创建。栈区是线程安全的,因为它不是一个共享资源。

4.PC寄存器

每个线程都有一个单独的PC寄存器来保存当前执行指令的地址,一旦该指令被执行,pc寄存器会被更新至下条指令的地址。

5 本地方法栈

本地方法栈保存本地方法信息(Native修饰)。
包括解释器、及时编译器垃圾回收器

垃圾回收器

收集并删除未引用的对象。可以通过调用"System.gc()"来触发垃圾回收,但并不保证会确实进行垃圾回收。JVM的垃圾回收只收集哪些由new关键字创建的对象。所以,如果不是用new创建的对象,你可以使用finalize函数来执行清理。

垃圾回收机制

运行时自身会运行相应的垃圾回收机制,程序员只需要申请内存,而不需要关注内存的释放。垃圾回收器(GC)会在适当的时候将已经终止生命周期的变量的内存给释放掉。

垃圾回收机制的优缺点

优点:

  1. 它大大简化了应用层开发的复杂度(不需要开发者再去手动跟踪内存)
  2. 降低了内存泄露(申请内存之后,忘记释放了 导致 可用的内容越来越少,最终无内存可用)的风险。

缺点:
3. 消耗额外的开销(消耗的资源更多了)
4. 会影响程序的流畅运行

哪些内存需要回收

在这里插入图片描述

垃圾回收具体是如何回收的

分为两个阶段:

  1. 找垃圾/判定垃圾
  2. 回收垃圾(释放内存)

找垃圾/判定垃圾

基于引用计数
Java主要使用的是基于可达性分析

基于引用计数

什么是基于引用计数:简单来说,针对每个对象,都会额外引入一小块内存,保存这个对象有多少个引用指向他
举个例子
1.Test t = new Test();,此时 new 了一个对象,那么我们就会额外引入一小块内存,此时 t 指向这个对象的引用,因此 引用计数 加 1
在这里插入图片描述
2.Test t2 = t; 此时 t 和 t2 都是指向这个对象的引用,此时引用计数 从1 变为 2
在这里插入图片描述

引用计数的优缺

引用技术,简单可靠高效,但是有个两个致命缺陷!!

(1) 空间利用率比较低,每个 new 的对象都得搭配个 计数器,计数器假设 4个字节,如果对象本身很大(几百个字节),多出来4个字节,就不算什么,但是如果本身对象很小(自己才4个字节),多出4个字节,相当于空间被浪费了一半
(2)会有循环引用的问题

基于可达性分析

简单的来说,通过额外的线程,定期的针对整个内存空间的对象进行扫描,有一些起始位置(称为 GCRoots),会类似于 深度优先遍历一样,把可以访问到的对象都标记一遍(带有标记的对象就是可达对象),没有被标记的对象,就是不可达,也就是垃圾!
那问题来了,什么才可以充当GCRoots?

  1. 虚拟机栈中引用的对象 Test a = new Test();
  2. 方法区中类静态属性引用的对象 public static Test s = new Test;
  3. 方法区中常量引用的对象 public static final Test s = new Test();
  4. 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
可达性分析的优缺点

优点:
克服了引用计数的两个缺点:

  1. 空间利用率低
  2. 循环引用

缺点:
如果内存中的对象特别多,这个遍历就会很慢,因此 GC 还是比较消耗时间和系统资源的。

回收垃圾

  1. 标记 - 清除
    首先通过可达性分析标记出哪些是垃圾,然后把垃圾直接释放。
    不足之处是虽然内存还是还给了系统,但是被释放的内存是离散的(不是连续的),会带来“内存碎片”。内存碎片:比如,空闲的内存,有很多,假设一共是 1G,如果要申请 500M 内存,也是可能申请失败的,因为要申请 500M 的内存 必须是连续的,每次申请,都是申请的连续的内存空间,而这里的 1G 可能是多个 碎片加在一起 才 1G,可用的并不多。
    为了解决内存碎片因此我们引入了复制算法!
  2. 复制算法
    在这里插入图片描述
    一块内存,分成两半,左边一半有很多对象,打钩的标记为垃圾,右边为 左边不是垃圾的,拷贝过来。然后再将左边全部标记为垃圾(灰色),全部释放掉,我们就能保证,左右两侧空间都是整体连续的。
    在这里插入图片描述

复制算法的问题有如下几点:
(1)内存空间利用率低(只能用一半的空间)
(2)如果要保留的的对象多,要释放的对象少,此时复制开销就很大
针对复制算法我们进行改进!–》 标记 - 整理
4. 标记 - 整理
类似于顺序表删除中间元素,有一个搬运操作,我们将 3 搬运到 2 ,再将 5 搬运到 3 ,再把 7 搬运到 4 然后再把后面的部分整体的释放掉
在这里插入图片描述
这个方案空间利用率是高了,但是仍然没有解决复制/搬运元素开销大的问题~
5. 分代回收
上述的三个方案,虽然能解决回收垃圾的问题,但是都有缺陷,实际 JVM 中的实现,会把多种方案结合起来一起使用,这个思路我们称为 “分代回收”。
在这里插入图片描述
(1)刚创建出来的对象,就放在伊甸区
(2)如果伊甸区的对象熬过一轮 GC 扫描,就会被拷贝到 幸存区(伊甸区 到 幸存区 应用了复制算法)
(3)在后续的几轮 GC 中,幸存区的对象就在两个幸存区之间来回拷贝(复制算法),每一轮都会淘汰一波幸存者
(4)在持续若干轮之后,对象终于,进入老年代,老年代有个特点,里面的对象都是比较老的(年级大的),因此老年代的 GC 扫描频率大大低于新生代。老年代中使用标记整理的方式进行回收!
注意!!!
分代回收中,还有一个特殊情况,有一类对象可以直接进入老年代(大对象,占有内存多的对象),大对象拷贝开销比较大,不适合使用复制算法!

补充

  1. 新生代(Young Generation):几乎所有新生成的对象首先都是放在年轻代的。新生代内存按照 8:1:1 的比例分为一个 Eden 区和两个 Survivor(Survivor0,Survivor1)区。大部分对象在 Eden 区中生成。当新对象生成,Eden 空间申请失败(因为空间不足等),则会发起一次 GC(Scavenge GC)。回收时先将 Eden 区存活对象复制到一个 Survivor0 区,然后清空 Eden 区,当这个 Survivor0 区也存放满了时,则将 Eden 区和 Survivor0 区存活对象复制到另一个 Survivor1 区,然后清空 Eden 和这个 Survivor0 区,此时 Survivor0 区是空的,然后将 Survivor0 区和 Survivor1 区交换,即保持 Survivor1 区为空, 如此往复。当 Survivor1 区不足以存放 Eden 和 Survivor0 的存活对象时,就将存活对象直接存放到老年代。当对象在 Survivor 区躲过一次 GC 的话,其对象年龄便会加 1,默认情况下,如果对象年龄达到 15 岁,就会移动到老年代中。若是老年代也满了就会触发一次** Full GC**,也就是新生代、老年代都进行回收。新生代大小可以由-Xmn来控制,也可以用-XX:SurvivorRatio来控制 Eden 和 Survivor 的比例。
  2. 老年代(Old Generation):在新生代中经历了 N 次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。内存比新生代也大很多(大概比例是 1:2),当老年代内存满时触发 Major GC 即 Full GC,Full GC 发生频率比较低,老年代对象存活时间比较长,存活率高。一般来说,大对象会被直接分配到老年代。所谓的大对象是指需要大量连续存储空间的对象,最常见的一种大对象就是大数组。当然分配的规则并不是百分之百固定的,这要取决于当前使用的是哪种垃圾收集器组合和 JVM 的相关参数。
  3. 永久代(Permanent Generation):用于存放静态文件(class类、方法)和常量等。永久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如 Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。对永久代的回收主要回收两部分内容:废弃常量和无用的类。永久代在 Java SE8 特性中已经被移除了,取而代之的是元空间(MetaSpace),因此也不会再出现java.lang.OutOfMemoryError: PermGen error的错误了。

网络相关面试题

网络 7 层架构

(1)物理层:主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。它的主要作用是传输比特流。这一层的数据叫做比特。
(2)数据链路层:主要将从物理层接收的数据进行 MAC 地址(网卡的地址)的封装与解封装。常把这一层的数据叫做帧。在这一层工作的设备是交换机,数据通过交换机来传输。
(3)网络层:主要将从下层接收到的数据进行 IP 地址(例 192.168.0.1)的封装与解封装。在这一层工作的设备是路由器,常把这一层的数据叫做数据包。
(4)传输层:定义了一些传输数据的协议和端口号(WWW 端口 80 等),如:TCP,UDP。 主要是将从下层接收的数据进行分段进行传输,到达目的地址后在进行重组。
常常把这一层数据叫做段。
(5)会话层:通过传输层(端口号:传输端口与接收端口)建立数据传输的通路。主要在你的系统之间发起会话或或者接受会话请求(设备之间需要互相认识可以是 IP 也可以是 MAC 或者是主机名)
(6)表示层:主要是进行对接收的数据进行解释、加密与解密、压缩与解压缩等(也就是把计算机能够识别的东西转换成人能够能识别的东西(如图片、声音等))
(7)应用层 主要是一些终端的应用(你就把它理解成我们在电脑屏幕上可以看到的东西.就是终端应用)。

TCP/IP 原理

TCP/IP 协议不是 TCP 和 IP 这两个协议的合称,而是指因特网整个 TCP/IP 协议族。从协议分层模型方面来讲,TCP/IP 由四个层次组成:网络接口层、网络层、传输层、应用层。

网络层
其功能是使主机可以把分组发往任何网络,并使分组独立地传向目标。互联网层使用因特网协议(IP,Internet Protocol)。

传输层(Tramsport Layer-TCP/UDP)
在这一层定义了两个端到端的协议:传输控制协议(TCP,Transmission Control Protocol)和用户数据报协议(UDP,User Datagram Protocol)。TCP 是面向连接的协议,它提供可靠的字节流传输,用于传输可靠性要求高,数据量大的数据,此外它还有流量控制、多路复用、优先权和安全性控制等功能。UDP 是面向无连接的不可靠传输的协议,用于传输可靠性要求不高,数据量小的数据。

应用层(Application Layer)
超文本传送协议(HTTP,HyperText Transfer Protocol)

TCP 三次握手/四次挥手

三次握手其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备。

三次握手

预备知识:
TCP的序号和确认号:
seq:某一个传输方向上的字节流的每个字节的序号,通过这个来确认发送的数据有序.
个人理解这个是用来确认发送数据的顺序的。
ack:CP对上一次seq序号做出的确认号,用来响应TCP报文段,给收到的TCP报文段的序号seq加1。
个人理解这个是对发送的数据进行确认,并告诉发送发下一次发送的序列号seq的位置
TCP的标志位:
SYN:简写为S,同步标志位,用于建立会话连接,同步序列号;
ACK: 简写为.,确认标志位,对已接收的数据包进行确认;
FIN: 简写为F,完成标志位,表示我已经没有数据要发送了,即将关闭连接

第一次握手:客户端将TCP报文标志位SYN置为1(通知对方我要建立连接了),随机产生一个序号值seq=J(发送数据的序列排队),并将该数据包发送给服务器端,发送完毕后,客户端进入SYN_SENT状态,等待服务器端确认。
第二次握手:服务器端收到数据包后由标志位SYN=1知道客户端请求建立连接,并添加ACK标志位(用以确认接收消息),随机产生一个序号值seq=y,并且发送一个ack = j + 1,(即告诉另一个应该发送的消息的序列),并将该数据包发送给客户端以确认连接请求,服务器端进入SYN_RCVD状态。
第三次握手:客户端收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1(表示确认接收到消息了),ack=y+1(接下来发送数据的位置),并将该数据包发送给服务器端,服务器端检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,客户端和服务器端进入ESTABLISHED状态,完成三次握手,随后客户端与服务器端之间可以开始传输数据了。
在这里插入图片描述

四次挥手

四次挥手即终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。在socket编程中,这一过程由客户端或服务端任一方执行close来触发。
由于TCP连接是全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭。
(1)第一次挥手: Client端发起挥手请求,向Server端发送标志位是FIN报文段,设置序列号seq,此时,Client端进入FIN_WAIT_1状态,这表示Client端没有数据要发送给Server端了。
(2)第二次挥手:Server端收到了Client端发送的FIN报文段,向Client端返回一个标志位是ACK的报文段(确认收到报文段),ack设为seq加1(这个消息接收了,发下一个吧),Client端进入FIN_WAIT_2状态,Server端告诉Client端,我确认并同意你的关闭请求。
(3)第三次挥手: Server端向Client端发送标志位是FIN的报文段,请求关闭连接,同时Client端进入LAST_ACK状态。
(4)第四次挥手 : Client端收到Server端发送的FIN报文段,向Server端发送标志位是ACK的报文段(确认收到报文段),然后Client端进入TIME_WAIT状态。Server端收到Client端的ACK报文段以后,就关闭连接。此时,Client端等待2MSL(MSL报文最大生存时间,一般为2分钟)的时间后依然没有收到回复,则证明Server端已正常关闭,那好,Client端也可以关闭连接了。
在这里插入图片描述
在这里插入图片描述

TCP协议为什么是三次握手而不是两次呢?

原因1:如果使用的是两次握手建立连接,假设有这样一种场景,客户端发送的第一个请求连接并且没有丢失,只是因为在网络中滞留的时间太长了,由于TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。此时之前滞留的那一次请求连接,因为网络通畅了, 到达了服务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的浪费。

如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接。
原因2 :两次握手只能保证单向连接是畅通的。因为TCP是一个双向传输协议,只有经过第三次握手,才能确保双向都可以接收到对方的发送的数据。

为何要四次挥手

那四次分手又是为何呢?TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议。TCP是全双工模式,这就意味着,当主机1发出FIN报文段时,只是表示主机1已经没有数据要发送了,主机1告诉主机2,它的数据已经全部发送完毕了;但是,这个时候主机1还是可以接受来自主机2的数据;当主机2返回ACK报文段时,表示它已经知道主机1没有数据发送了,但是主机2还是可以发送数据到主机1的;当主机2也发送了FIN报文段时,这个时候就表示主机2也没有数据要发送了,就会告诉主机1,我也没有数据要发送了,之后彼此就会愉快的中断这次TCP连接。

为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?

虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假定网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。在Client发送出最后的ACK回复,但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态,并且会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。所谓的2MSL是两倍的MSL(Maximum Segment Lifetime)。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。

如果已经建立了连接,但是客户端突然出现故障了怎么办?

TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。

SYN攻击是什么?

SYN攻击就是Client在短时间内伪造大量不存在的IP地址,并向Server不断地发送SYN包,Server则回复确认包,并等待Client确认,由于源地址不存在,因此Server需要不断重发直至超时,这些伪造的SYN包将长时间占用未连接队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网络拥塞甚至系统瘫痪。
检测 SYN 攻击非常的方便,当你在服务器上看到大量的半连接状态时,特别是源IP地址是随机的,基本上可以断定这是一次SYN攻击。在 Linux/Unix 上可以使用系统自带的 **netstat **命令来检测 SYN 攻击。

常见的防御 SYN 攻击的方法有如下几种:

缩短超时(SYN Timeout)
时间增加最大半连接数
过滤网关防护SYN
cookies技术

TCP 如何保证可靠性

(1)序列号和确认号机制:
TCP 发送端发送数据包的时候会选择一个 seq 序列号,接收端收到数据包后会检测数据包的完整性,如果检测通过会响应一个 ack 确认号表示收到了数据包。
(2)超时重发机制:
TCP 发送端发送了数据包后会启动一个定时器,如果一定时间没有收到接受端的确认后,将会重新发送该数据包。
(3)对乱序数据包重新排序:
从 IP 网络层传输到 TCP 层的数据包可能会乱序,TCP 层会对数据包重新排序再发给应用层。
(4)丢弃重复数据:
从 IP 网络层传输到 TCP 层的数据包可能会重复,TCP 层会丢弃重复的数据包。
(5)流量控制:
TCP 发送端和接收端都有一个固定大小的缓冲空间,为了防止发送端发送数据的速度太快导致接收端缓冲区溢出,发送端只能发送接收端可以接纳的数据,为了达到这种控制效果,TCP 用了流量控制协议(可变大小的滑动窗口协议)来实现。

TCP 如何实现流量控制?

TCP 利用滑动窗口实现流量控制。流量控制是为了控制发送方发送速率,保证接收方来得及接收。
为什么需要流量控制? 这是因为双方在通信的时候,发送方的速率与接收方的速率是不一定相等,如果发送方的发送速率太快,会导致接收方处理不过来。如果接收方处理不过来的话,就只能把处理不过来的数据存在 接收缓冲区(Receiving Buffers) 里(失序的数据包也会被存放在缓存区里)。如果缓存区满了发送方还在狂发数据的话,接收方只能把收到的数据包丢掉。出现丢包问题的同时又疯狂浪费着珍贵的网络资源。因此,我们需要控制发送方的发送速率,让接收方与发送方处于一种动态平衡才好。

HTTP 原理

HTTP既是超文本传输协议,是客户端向服务器发送请求,服务器向客户端发送响应数据的一个过程。HTTP 是一个无状态的协议。是指客户机(Web 浏览器)和服务器之间不需要建立持久的连接,这意味着当一个客户端向服务器端发出请求,然后服务器返回响应(response),连接就被关闭了,在服务器端不保留连接的有关信息.HTTP 遵循请求(Request)/应答(Response)模型。客户机(浏览器)向服务器发送请求,服务器处理请求并返回适当的应答。

请简述HTTP和HTTPS,并说明它们的区别

HTTP既是超文本传输协议,用于从网络传输超文本数据到本地浏览器的协议。HTTPS是以安全为目标的HTTP通道,在其基础上加入了证书机制进行加密。区别在于,一个有证书加密一个没有证书,在安全上具有差异;端口上也不同,HTTP是80端口,HTTPS是443端口。消耗资源:和HTTP相比,HTTPS通信会因为加解密的处理消耗更多的CPU和内存资源。

HTTP 响应码有哪些?分别代表什么含义?

200:成功,Web 服务器成功处理了客户端的请求。
301:永久重定向,当客户端请求一个网址的时候,Web 服务器会将当前请求重定向到另一个网址,搜索引擎会抓取重定向后网页的内容并且将旧的网址替换为重定向后的网址。
400:客户端请求错误,多为参数不合法导致 Web 服务器验参失败。
404:未找到,Web 服务器找不到资源。
500:Web 服务器错误,服务器处理客户端请求的时候发生错误。
503:服务不可用,服务器停机。

说一下HTTP的长连接与短连接的区别

短连接
在HTTP/1.0中默认使用短链接,也就是说,浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。
长连接
从HTTP/1.1起,默认使用长连接,用以保持连接特性。在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的 TCP连接不会关闭。如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。

如何理解HTTP协议是无状态的?

HTTP协议是无状态的,指的是协议对于事务处理没有记忆能力,服务器不知道客户端是什么状态。也就是说,打开一个服务器上的网页和上一次打开这个服务器上的网页之间没有任何联系。HTTP是一个无状态的面向连接的协议,无状态不代表HTTP不能保持TCP连接,更不能代表HTTP使用的是UDP协议(无连接)。

Forward 和 Redirect 的区别?

Forward 是服务器内部的重定向,服务器内部请求某个 servlet,然后获取响应的内容,浏览器的 URL 地址是不会变化的;Redirect 是客户端请求服务器,然后服务器给客户端返回了一个 302 状态码和新的 location,客户端重新发起 HTTP 请求,服务器给客户端响应 location 对应的 URL 地址,浏览器的 URL 地址发生了变化。
数据的共享:Forward 是服务器内部的重定向,request 在整个重定向过程中是不变的,
request 中的信息在 servlet 间是共享的。Redirect 发起了两次 HTTP 请求分别使用不同的request。
请求的次数:Forward 只有一次请求;Redirect 有两次请求。

Get 和 Post 请求有哪些区别?

表单的提交方式:
(1)get 请求直接将表单数据以 name1=value1&name2=value2 的形式拼接到 URL 上(http://www.baidu.com/action?name1=value1&name2=value2),多个参数参数值需要用 & 连接起来并且用 ? 拼接到 action 后面;
(2)post 请求将表单数据放到请求体中。
传输数据的大小限制:

get 请求传输的数据受到 URL 长度的限制,而 URL 长度是由浏览器决定的;
post 请求传输数据的大小理论上来说是没有限制的

浏览器访问 Web 服务过程详解(很重要!!!)

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

HTTP1.0、HTTP1.1、HTTP2.0的关系和区别

HTTP1.0:
浏览器的每次请求都需要与服务器建立一个TCP连接,服务器处理完成后立即断开TCP连接(无连接),服务器不跟踪每个客户端也不记录过去的请求(无状态)。
HTTP1.1:
HTTP/1.0中默认使用Connection: close。在HTTP/1.1中已经默认使用Connection: keep-alive,避免了连接建立和释放的开销,但服务器必须按照客户端请求的先后顺序依次回送相应的结果,以保证客户端能够区分出每次请求的响应内容。通过Content-Length字段来判断当前请求的数据是否已经全部接收。不允许同时存在两个并行的响应。
HTTP2.0:
HTTP/2引入二进制数据帧和流的概念,其中帧对数据进行顺序标识,这样浏览器收到数据之后,就可以按照序列对数据进行合并,而不会出现合并后数据错乱的情况。同样是因为有了序列,服务器就可以并行的传输数据。

NIO、AIO、BIO

Java共支持3种网络编程模型I/O模式:BIO、NIO、AIO

BIO(同步阻塞)

客户端有连接请求时服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,一个线程停留在一个水壶那,直到这个水壶烧开,才去处理下一个水壶。

NIO(同步非阻塞)

客户端发送的连接请求都会注册到多路复用器上(Selector),多路复用轮询到连接有I/O请求就进行处理。一个线程不停的循环观察每一个水壶,根据每个水壶当前的状态去处理。

AIO(异步非阻塞)

特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。每个水壶上装一个开关,当水开了以后会提醒对应的线程去处理。

线程生命周期(状态)

线程生命周期(状态)

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)就绪(Runnable)运行(Running)阻塞(Blocked)和死亡 5 种状态。当线程启动以后,线程状态也会多次在 运行、阻塞之间切换。

新建状态(NEW)

使用new关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配内存,并初始化其成员变量的值。

就绪状态(RUNNABLE)

当线程对象调用了 start()之后,该线程处于就绪状态。Java 虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。

运行状态(RUNNING)

处于就绪状态的线程获得了 CPU,开始执行run()方法的线程执行体,则该线程处于运行状态。

阻塞状态(BLOCKED)

阻塞状态是指线程因为某种原因放弃了 cpu 使用权,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得 cpu 转到运行(running)状态。阻塞的情况分三种:

等待阻塞(o.wait->等待对列)

运行(running)的线程执行 o.wait()方法,JVM 会把该线程放入等待队列(waitting queue)中。

同步阻塞(lock->锁池)

若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池(lock pool)中。

其他阻塞(sleep/join)

运行(running)的线程执行 Thread.sleep(long ms)t.join() 方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。

线程死亡(DEAD)

线程会以下面三种方式结束,结束后就是死亡状态。

  1. 正常结束
    run()方法执行完成,线程正常结束。
  2. 异常结束
    线程抛出一个未捕获的 Exception 或 Error。
  3. 调用 stop
    直接调用该线程的stop() 方法来结束该线程.
    在这里插入图片描述

sleep 与 wait 区别

  1. 对于 sleep()方法,我们首先要知道该方法是属于 Thread 类中的。而 wait()方法,则是属于Object 类中的。
  2. sleep()方法导致了程序暂停执行指定的时间,让出 cpu 该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。
  3. 在调用 sleep()方法的过程中,线程不会释放对象锁。
  4. 而当调用 wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态,继续争夺cup资源
  5. wait 必须搭配 synchronize 一起使用,而 sleep 不需要;
  6. 在调用 wait 方法之后,线程会变为 WATING 状态,而调用 sleep 方法之后,线程会变为 TIMED_WAITING 状态。

JAVA 锁

乐观锁

乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作。适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升

悲观锁

悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确

公平锁 VS 非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁,队列中的第一个线程才能获得锁,其他线程直接进入队列中排队(等待唤醒)。公平锁的优点是等待锁的线程不会饿死。缺点是整体效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。
非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

可重入锁 VS 非可重入锁

可重入锁是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁

Synchronized 同步锁

synchronized 它可以把任意一个非 NULL 的对象当作锁。他属于独占式的悲观锁,同时属于可重入锁。

Synchronized 作用范围

  1. 作用于方法时,锁住的是对象的实例(this);即同一时间只能有一个线程进入该对象的这个方法
  2. 当作用于静态方法时,锁住的是Class实例,是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;即是这个类的所有对象
  3. synchronized 作用于一个代码块对象实例时,锁住的是所有以该对象为锁的代码块;即当前对象。

ReentrantLock 与 synchronized

  1. ReentrantLock 通过方法 lock()与 unlock()来进行加锁与解锁操作,与 synchronized 会 被 JVM 自动解锁机制不同,ReentrantLock 加锁后需要手动进行解锁。为了避免程序出现异常而无法正常解锁的情况,使用 ReentrantLock 必须在 finally 控制块中进行解锁操作。
  2. ReentrantLock 相比 synchronized 的优势是可中断、公平锁。这种情况下需要使用 ReentrantLock。

线程池原理

线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后提交任务队列后,如果线程数量超过了最大数量,超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。他的主要特点为:线程复用;控制最大并发数;管理线程。

线程复用

每一个 Thread 的类都有一个 start 方法。 当调用 start 启动线程时 Java 虚拟机会调用该类的 run 方法。 那么该类的 run() 方法中就是调用了 Runnable 对象的 run() 方法。 我们可以继承重写Thread 类,在其 start 方法中添加不断循环调用传递过来的 Runnable 对象。 这就是线程池的实现原理。循环方法中不断获取 Runnable 是用 Queue 实现的,在获取下一个 Runnable 之前可以是阻塞的。

线程池的组成

  1. 线程池管理器:用于创建并管理线程池
  2. 工作线程:线程池中的线程
  3. 任务接口:每个任务必须实现的接口,用于工作线程调度其运行
  4. 任务队列:用于存放待处理的任务,提供一种缓冲机制

拒绝策略

线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。

Java 线程池工作过程

  1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
  2. 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
    a) 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
    b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
    c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
    d) 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池
    会抛出异常 RejectExecutionException。
  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  4. 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

JAVA 阻塞队列原理

  1. 当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列。
  2. 当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒。

volatile关键字

一旦一个共享变量类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  1. 可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
//线程1
 
boolean stop = false; 
while(!stop){ 
    doSomething();
}
//线程2 
stop = true;

下面解释一下这段代码:
每个线程在运行过程中都有自己的工作内存(cpu的告诉缓存中),那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。
那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。
但是用volatile修饰之后就变得不一样了:
(1) 第一:使用volatile关键字会强制将修改的值立即写入主存;
(2)使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效
(3)在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。
2. 有序性
一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的(因为会考虑指令之间的数据依赖性),这样可能会会影响到多线程并发执行的正确性。
volatile能在一定程度上保证有序性。

x = 2;        //语句1
y = 0;        //语句2
flag = true;  //语句3
x = 4;         //语句4
y = -1;       //语句5

flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。
3. 不保证原子性,synchronized 可以保证
在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了;然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,所以线程2会直接去主存读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作内存。

volatile 修饰符的有过什么实践?

一种实践是用 volatile 修饰 long 和 double 变量,使其能按原子类型来读写。double 和 long 都是 64 位宽,因此对这两种类型的读是分为两部分的,第一次读取第一个 32 位,然后再读剩下的 32 位,这个过程不是原子的,但 Java 中volatile 型的 long 或 double 变量的读写是原子的。

ThreadLocal 作用

ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

每个Thread中都有一个threadlocal属性和一个threadlocalmap属性,当创建threadlocal对象的时候,会将该threadlocal对象的弱引用放入到threadlocalmap中充当k,并把threadlocal.set()方法的值放入threadlocalmap充当value。当把threadlocal置为null的时候,由于弱引用k的值就会被垃圾回收,但是value是强引用,会被遗留,此时threadlocalmap只有一个value值,无法被垃圾回收,导致内存泄漏。

ThreadLocal 内存泄漏的根源是:

由于ThreadLocalMap 的生命周期跟 Thread 一样长,对于重复利用的线程来说,如果没有手动删除(remove()方法)对应 key 就会导致entry(null,value)的对象越来越多,从而导致内存泄漏。

那么为什么 key 要用弱引用

事实上,在 ThreadLocalMap 中的set/getEntry 方法中,会对 key 为 null(也即是 ThreadLocal 为 null )进行判断,如果为 null 的话,那么会把 value 置为 null 的.这就意味着使用threadLocal , CurrentThread 依然运行的前提下.就算忘记调用 remove 方法,弱引用比强引用可以多一层保障:弱引用的 ThreadLocal 会被回收.对应value在下一次 ThreadLocaI 调用 get()/set()/remove() 中的任一方法的时候会被清除,从而避免内存泄漏.

进程调度

所谓进程调度方式,是指当某个进程正在处理机上执行时,若有某个更为重要或紧迫的进程需要处理,即有优先权更高的进程进入就绪队列,此时应如何分配处理机。通常有以下两种进程调度方式:

非剥夺调度方式 —抢占方式。

非剥夺调度方式是指当一个进程正在处理机上执行时,即使有某个更为重要或紧迫的进程进入就绪队列,仍然让正在执行的进程继续执行,直到该进程完成或发生某种事件而进入阻塞态时,才把处理机分配给更为重要或紧迫的进程。
这种方式的优点是实现简单、系统开销小,适用于大多数的批处理系统,但它不能用于分时系统和大多数的实时系统。

剥夺调度方式,又称抢占方式。

剥夺调度方式是指当一个进程正在处理机上执行时,若有某个更为重要或紧迫的进程需要使用处理机,则立即暂停正在执行的进程,将处理机分配给这个更为重要或紧迫的进程。
采用剥夺式的调度,对提高系统吞吐率和响应效率都有明显的好处。但“剥夺”不是一种任意的行为,必须遵循一定的原则,主要有优先权、段进程优先和时间片原则等

MySQL 中有哪几种锁?

  1. 行级锁:行级锁是一种排他锁,防止其他事务修改此行。开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
  2. 表级锁:表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分 MySQL 引擎支持。最常使
    用的 MYISAM 与 INNODB 都支持表级锁定。开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
  3. 页面锁:页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。开销和加锁时间界于表锁和行锁之间;会出现死锁;并发度一般。

简述在MySQL 数据库中 MyISAM 和InnoDB 的区别:

  1. MyISAM: 不支持事务,也不支持外键; 支持表级锁, 即每次操作是对整个表加锁; 存储表的总行数; 但是访问速度快,对事物的完整性没有要求。
  2. InnoDb: 存储引擎提供了具有提交、回滚和崩溃恢复能力的事务安全。支持外键、支持行锁。但是比起MyISAM存储引擎,写的处理效率差一些并且会暂用更多的磁盘空间以保留数据和索引。

MySQL 中 InnoDB 支持的四种事务隔离级别名称,以及逐级之间的区别?

脏读:当一个事务读取到另一个事务尚未提交的改变(update、insert、dalete)时,产生脏读。
不可重复读:当一个事务在查询中,由于其他提交事务所做的修改或删除,每次查询到的结果会随着提交事务方的修改而变化,此时会出现不可重复读。
幻读:当一个事务在查询中,由于其他提交事务所做的插入操作,每次查询到的结果会随着提交事务方的修改而变化,此时会出现幻读。
1、read uncommited : 读未提交
会出现脏读、不可重复读、幻读。不加锁
2、read committed:读已提交
会出现不可重复读和幻读。不加锁
3、repeatable read: 可重复读
不会出现脏读、不可重复读、幻读。不加锁
4、serializable : 串行事物
不会出现脏读、不可重复读、幻读。加锁

CHAR 和VARCHAR 的区别?

char (0-255)/varchar (0-65535)(字符数)
1.char(4):这个4表示字符数(最大时255),不管是中文还是英文都是放四个。
2.varchar(4);4也表示字符数,不管是中文还是英文都是放四个。
3.char(4)是定长,即使插入‘aa’两个字符,也会暂用4个字符空间。
4.varchar(4)是变长,按照实际占用空间来分配,注意:本身还需要占用1-3个字节来记录存放内容的长度。

简单描述 MySQL 中,索引,主键,唯一索引,联合索引的区别,对数据库的性能有什么影响。

1.索引的作用在不用加内存,不用改程序,不用调sql,提高数据库的性能。
2.索引的应用场景:
(1) 较频繁的作为查询寻字段应该创建索引。
(2) 唯一性太差的字段不适合单独创建索引。即使频繁作为查询条件,比如性别
(3)更新非常频繁的字段不适合创建索引,比如登录次数。
(4)不会出现在where子句中字段不该创建索引,即不作为查询条件。
3.索引的原理:
(1)没有索引为什么会慢?因为是全表扫描
(2)使用索引为为什么会快?因为形成一个索引的数据结构,比如二叉树
(3)索引的代价?
(3.1) 会多占用磁盘
(3.2) 对dml(delete update insert)语句形成影响、但是没事,因为在我们的项目中,select会占用90%。
4.索引的类型:
(1) 主键索引,主键自动的为索引(PRIMARY KEY (id))
(2) 唯一索引(UNIQUE),unique自动为索引(CREATE UNIQUE INDEX id_index ON t25 (id);)
(3)普通索引(INDEX)(CREATE INDEX id_index ON t25 (id);)
(4)全文索引(FULLTEXT)(不常用)

数据库中的事务是什么?

事务( transaction) :用于保证数据的一致性,它由一组相关的dml(增删改查)语句组成,该组语句要么全部成功,要么全部失败。如:转账
事务特性:
1、原子性: 即不可分割性, 事务要么全部被执行, 要么就全部不被执行。
2、一致性。事务必须使得数据库从一个一致性状态转换成另一个一致性状态。
3、隔离性。当多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。(可重复读)
4、持久性。事务正确提交后, 其结果将永久保存在数据库中, 即使在事务提交后有了其他故障,也不应该有任何影响(回滚也回滚不了了)

解释 MySQL 外连接、内连接与自连接的区别

  1. 内连接,表示以两个表的交集为主,查出来是两个表有交集的部分,其余没有关联就不额外显示出来
  2. 外连接:
    (1) 左外连接(left join):左表为主表,左侧的表完全显示,我们就说是左外连接,即左边的表如果没有跟右面的表完全匹配,也会全部显示
    (2) 右外连接(right join):右表为主表,右侧的表完全显示,我们就说是右外连接,里没有对应的用null填充。
  3. 自连接:是指在一张表上链接查询,即将一张表看作两张表。

Myql 中的事务回滚机制概述

事务回滚:设置一个保存点(savepoint),保存点是事务中的点,用于取消部分事务,当结束事务时(commit),会自动删除事务所定义的所有保存点。当执行回退事务时(rollback),通过指定保存点可以退回到指定对的点。

主键、外键和索引的区别?

  1. 主键:用来保证数据完整性
    (1) 主键列的值是不可以重复且不能为null
    (2) 一张表最多只能有一个主键, 但可以是复合主键(比如 id+name)
  2. 外键:用来和其他表建立联系用的,一个表可以有多个外键
    (1) 用于定义主表和从表之间的关系:外键约束要定义在从表上,主表则必须具有主键约束或unique约束
    (2) 外键的列数据必须在主表的主键列存在
    (3) 也可为空
    (4) 一旦建立主外键的关系,数据不能随意删除了
  3. 索引:是提高查询排序的速度,一个表可以有多个唯一索引

说说对 SQL 语句优化有哪些方法?

  1. 用 EXISTS 替代 IN、用 NOT EXISTS 替代 NOT IN。
  2. 避免在索引列上使用计算
  3. 对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。
  4. 应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描
  5. 应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描
    =======================================

基础补充

Callable和Runnable的区别

相同点:
1、两者都是接口
2、两者都需要调用Thread.start启动线程
不同点:
1、如上面代码所示,callable的核心是call()方法,允许返回值,runnable的核心是run()方法,没有返回值
2、call方法可以抛出异常,但是run方法不行(只能通过try()catch()方法来执行)
3、callable需要实现一个FutureTask接口,Runnable不需要

BIO NIO AIO

BIO

同步阻塞模型(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。传统的socket网络编程就是用的这个技术,其阻塞具体体现在accept()/read()/write()方法上。
不足之处:
(1)当多连接的时候,如果一个线程阻塞,会造成其他链接只能进行等待而无法连接。
(2)可以通过多线程解决上面的问题,但是在java中创建线程和销毁是很费资源的。
(3)也可以通过线程池来解决,如果单个消息处理的缓慢,或者服务器线程池中的全部线程都被阻塞,那么后续Socket的I/O消息都将在队列中排队。新的Socket请求将被拒绝,客户端会发生大量连接超时

NIO

同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理。
(1)单线程的情况:简单理解就是当客户端发送请求,服务端通过selector进行轮询,当有连接请求的时候就会处理,在此基础上再次轮询,如果发现读写操作,就会再次处理,在这个过程中,如果在读写方面出现了阻塞,会造成其他连接无法连接的问题。
(2)多线程的情况,NIO-reactor模式,简单理解为当客户端发送连接请求,如果发现连接请求,则由selector进行连接处理,具体的读写操作会交给一个线程池来操作。
NIO有三大核心部分:Channel(通道-双向发送数据原因是缓冲区)、Buffer(缓冲区)、Selector(选择器)

AIO

异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由操作系统先完成了再通知服务器应用去启动线程进行处理,一般适用于连接数较多且连接时间较长的应用。简单理解为当客户端发送连接请求,先交给操作系统,当操作系统认为你想要建立连接的时候,再通知服务端启动线程进行处理,然后具体的内部读写操作依然是由线程池来处理。

synchronized补充

(1)重量级锁:当JVM启动一些线程的时候,操作系统会进行对应的生成相应的线程,JVM不会管理这些线程,而是由操作系统负责管理线程加锁解锁的过程。
(2)synchronized是线程同步的意思,在JDK1.6 之前属于重量级锁,JDK1.6后 对锁的实现引入了大量的优化,如自旋锁(比如集合,偏向锁就是贴标签线程id),因此synchronized可以根据不同的条件在重量级锁和轻量级锁之间进行转换。
(3)使用synchronized进行加锁的应用主要有Stringbuffer、vector、Hashtable
(4)synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。
(5)synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。

CAS和ABA问题

(1)CAS是乐观锁的一种实现,同时也是轻量级锁,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B并返回true,否则返回false。
(2)针对ABA问题,解决的办法就是加版本号就可以了
(3)CAS本身必须符合原子性,因为如果不是原子性,在修改值的瞬间有可能被其他线程修改数据。

对象在内存中的布局

普通对象,当new一个对象的时候,内存是在这样。
其中markword中存放的信息最重要的就是锁的信息。
在这里插入图片描述
在这里插入图片描述

synchronized 和 volatile 的区别?

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

synchronized 和 ReentrantLock 的区别

(1)两者都是可重入锁
(2)synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API(lock和unlock)
(3)ReentrantLock 比 synchronized 增加了一些高级功能
(3.1)等待可中断 : ReentrantLock提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly() 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
(3.2)可实现公平锁 : ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
(3.3)可实现选择性通知(锁可以绑定多个条件): synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制。ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition()方法。例如可以将生产者的队列和消费者的队列区分开来,需要唤醒哪个队列就唤醒哪个队列(condition,方法为await()和signal())区分了不同条件的等待队列,ABC ABC问题。

线程池

执行 execute()方法和 submit()方法的区别是什么呢?
(1)execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
(2)submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Future 的 get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

如何创建线程池

ThreadPoolExecutor 类分析
(1)corePoolSize : 核心线程数定义了最小可以同时运行的线程数量。
(2)maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
(3)workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
(4)keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
(5)unit : keepAliveTime 参数的时间单位。
(6)threadFactory :executor 创建新线程的时候会用到。
(7)handler :饱和策略。关于饱和策略下面单独介绍一下。

AQS

AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出大量应用广泛的同步器,比如我们提到的 ReentrantLock。我们自己也能利用 AQS 非常轻松容易地构造出符合我们自己需求的同步器。

原理
AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
例如:以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程 lock() 时,会调用 tryAcquire() 独占该锁并将 state+1 。此后,其他线程再 tryAcquire() 时就会失败,直到 A 线程 unlock() 到 state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多少次,这样才能保证 state 是能回到零态的。

注解

可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。

注解的解析方法有哪几种?

注解只有被解析之后才会生效,常见的解析方法有两种:
(1)编译期直接扫描 :编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用@Override 注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。
(2)运行期通过反射处理 :像框架中自带的注解(比如 Spring 框架的 @Value 、@Component)都是通过反射来进行处理的

HashMap 的长度为什么是 2 的幂次方

为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀((n - 1) & hash)
理解,按位与的操作可以等价为取余的操作,但是效率更高,例如:
在这里插入图片描述
在这里插入图片描述

ConcurrentHashMap 和 Hashtable 的区别

  1. 底层数据结构: JDK1.8 采用的数据结构跟 HashMap1.8 的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
  2. 实现线程安全的方式(重要):
    到了 JDK1.8 的时候,ConcurrentHashMap 直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized (头节点,也就是当在链表上要连接而数据的时候)和 CAS(当为null的时候) 来操作。
    Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。

产生死锁的四个必要条件

1、 互斥条件:
在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
2、不可剥夺条件:
进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。
3、 请求与保持条件:
进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
4、循环等待条件:
可重入锁的过程

Redis和传统的关系型数据库有什么不同?

Redis是一种基于键值对的NoSQL数据库,Redis的数据都存储于内存中,因此它的速度惊人。
关系型数据库是基于二维数据表来存储数据的,它的数据格式更为严谨,并支持关系查询。关系型数据库的数据存储于磁盘上,可以存放海量的数据,但性能远不如Redis。

Redis有哪些数据类型?

Redis支持5种核心的数据类型,分别是字符串、哈希、列表、集合、有序集合;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值