优秀面试题
Java超高频面试题汇总!_笔经面经_牛客网
离职在家学习Java的第一天
1.JDK、JRE、JVM的关系
JVM: JVM是运行Java字节码的虚拟机
JDK: JAVA开发工具包,它能够创建和编译程序,JDK包含JRE
JRE: JAVA运行时环境,它是运行已编译Java程序的所需的所有内容的集合。包含JVM, JAVA类库,Java命令等一些基础构建,但是不能创建新程序。
2.hashCode() 与 equals() (已面)
你重写过 hashCode() 和 equals()么?为什么重写 equals() 时必须重写 hashCode() 方法?
在将对象加入哈希表中时,哈希表会判断对象的hashCode值是否有与加入对象的哈希值相同,若无,直接加入。
若有hashCode值相同的对象,继续使用equals判断是否相同,若equals相同,
不加入。
如果不重写hashCode()函数,那么在加入的时候会出现equals判断内容相同,但是hashCode值
不同的情况。将相同的对象加入。
3.Java语言有哪些特点 (已面)
(1)平台无关性( Java 虚拟机实现平台无关性)
(2)支持多线程
(3)面向对象(封装,继承,多态)
(4)支持网络编程并且很方便
(5)安全性和可靠性
(6)解释与编译并存
4.为什么说Java语言”解释与编译并存“
编译型 :编译型语言 (opens new window)会通过编译器 (opens new window)将源代码一次性翻译成可被该平台执行的机器码。
解释型 :解释型语言 (opens new window)会通过解释器 (opens new window)一句一句的将代码解释(interpret)为机器代码后再执行。
这是因为 Java 语言既具有编译型语言的特征,也具有解释型语言的特征。因为 Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(.class
文件),这种字节码必须由 Java 解释器来解释执行。
(加)为什么java语言“一次编译,到处执行”
对Java程序而言,只认识一种操作系统,这个系统就是JVM,字节码文件(扩展名为.class的文档)就是JVM的可执行文件。
Java程序理想上,并不理会真正执行哪个平台,只要知道如何执行于JVM就可以了,至于JVM实际上如何与底层平台沟通,那是JVM自己的事。
由于JVM实际上相当于Java程序的操作系统,JVM就负责了Java程序的各种资源管理。
我们要记住两点:
1. JVM就是Java程序的操作系统,JVM的可执行文件就是.class文件。
2. Java虚拟机屏蔽了操作系统之间的差异,但是不同的系统使用的虚拟机不同。
5.Java和C++的区别
虽然,Java 和 C++ 都是面向对象的语言,都支持封装、继承和多态,但是,它们还是有挺多不相同的地方:
6.字符常量和字符型常量的区别?
9.静态方法为什么不能调用非静态成员?
10.静态方法和实例方法有何不同?
java静态方法和非静态方法的区别是什么-Java基础-PHP中文网
11.重载和重写的区别(已面)
重载
发生在同一个类中(或者父类和子类之间),方法名必须相同,参数必须不同
方法的重写要遵循“两同两小一大”
12、== 和 equals() 的区别
==
对于基本类型和引用类型的作用效果是不同的:
==
比较的是值。==
比较的是对象的内存地址。equals()
不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。
equals()
方法存在两种使用情况:
equals()
方法 :通过equals()
比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是 Object
类equals()
方法。equals()
方法 :一般我们都重写 equals()
方法来比较两个对象中的属性是否相等;13.Java 中的几种基本数据类型了解么?(已面)
Java 中有 8 种基本数据类型,分别为:
整数型:byte
、short
、int
、long
浮点型:float
、double
字符型:char
布尔型:boolean
。
这 8 种基本数据类型的默认值以及所占空间的大小如下:
基本数据类型的取值范围
Java 里使用 long
类型的数据一定要在数值后面加上 L,否则将作为整型解析。
float直接赋值时必须在数字后加上f或F,否则将作为double解析。
double赋值时可以加d或D也可以不加。
14.包装类型的常量池技术了解么?
Java 基本类型的包装类的大部分都实现了常量池技术。
Byte
,Short
,Integer
,Long
这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character
创建了数值在 [0,127] 范围的缓存数据,Boolean
直接返回 True
or False
。
如果超出对应范围仍然会去创建新的对象。
两种浮点数类型的包装类 Float
,Double
并没有实现常量池技术。
15.自动装箱与拆箱了解吗?原理是什么?
什么是自动拆装箱?
装箱其实就是调用了 包装类的valueOf()
方法,拆箱其实就是调用了 xxxValue()
方法
因此,
Integer i = 10
等价于 Integer i = Integer.valueOf(10)
int n = i
等价于 int n = i.intValue()
;注意:如果频繁拆装箱的话,也会严重影响系统的性能。
17.成员变量和局部变量的区别?
public
,private
,static
等修饰符所修饰,而局部变量不能被访问控制 修饰符及 static
所修饰;但是,成员变量和局部变量都能被 final
所修饰。static
修饰,那么这个成员变量是属于类的,如果没有使用 static
修饰,这个成员变量是属于实例的。
18.创建一个对象用什么运算符?对象引用和对象实体有什么不同?
new 运算符,new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。
一个对象引用可以指向 0 个或 1 个对象;一个对象可以有 n 个引用指向它。
19.对象的相等与指向他们的引用相等,两者有什么不同?
21.构造方法有哪些特点,能否被override?
构造方法特点如下:
构造方法不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。
22.面向对象三大特征?(已面)
(封装/继承/多态)
封装:封装是指把一个对象的属性私有化,不允许外部对象直接访问这些私有属性,但是可以提供一些可以被外部访问的方法来操作这些属性。
继承:继承是子类继承父类的非私有属性和方法。子类可以拥有自己属性和方法,即子类可以对父类进行扩展。一个子类只能拥有一个父类,子类可以对父类的方法进行重写。
多态,顾名思义,表示一个对象具有多种的状态,具体表现为父类的引用指向子类的实例。
23.接口和抽象类有什么共同点和区别?
共同点 :
default
关键在接口中定义默认方法)。区别 :
public static final
类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。24.深拷贝和浅拷贝(不是强引用和弱引用!)的区别了解吗?什么是引用拷贝?
关于深拷贝和浅拷贝区别,我这里先给结论:
上面的结论没有完全理解的话也没关系,我们来看一个具体的案例!
浅拷贝
浅拷贝的示例代码如下,我们这里实现了 Cloneable
接口,并重写了 clone()
方法。
clone()
方法的实现很简单,直接调用的是父类 Object
的 clone()
方法。
public class Address implements Cloneable{
private String name;
// 省略构造函数、Getter&Setter方法
@Override
public Address clone() {
try {
return (Address) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
public class Person implements Cloneable {
private Address address;
// 省略构造函数、Getter&Setter方法
@Override
public Person clone() {
try {
Person person = (Person) super.clone();
return person;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
测试 :
Person person1 = new Person(new Address("武汉"));
Person person1Copy = person1.clone();
// true
System.out.println(person1.getAddress() == person1Copy.getAddress());
从输出结构就可以看出, person1
的克隆对象和 person1
使用的仍然是同一个 Address
对象。
深拷贝
这里我们简单对 Person
类的 clone()
方法进行修改,连带着要把 Person
对象内部的 Address
对象一起复制。
@Override
public Person clone() {
try {
Person person = (Person) super.clone();
person.setAddress(person.getAddress().clone());
return person;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
测试 :
Person person1 = new Person(new Address("武汉"));
Person person1Copy = person1.clone();
// false
System.out.println(person1.getAddress() == person1Copy.getAddress());
从输出结构就可以看出,虽然 person1
的克隆对象和 person1
包含的 Address
对象已经是不同的了。
那什么是引用拷贝呢? 简单来说,引用拷贝就是两个不同的引用指向同一个对象。
我专门画了一张图来描述浅拷贝、深拷贝、引用拷贝:
25.static的作用?
(差不多得了)
可以通过类名的方式来访问、所有对象共用一个(属于类不属于对象)、不能访问非静态变量和方法、类加载的时候就初始化,这几点来答
26.final的作用?可以修饰哪些东西?在什么情况下使用呢?(已面)
答:1、被final修饰的类不可以被继承;2、被final修饰的方法不可以被重写;3、被final修饰的变量不可以被改变。类,变量,方法。不希望当前修饰的元素被改动或重载的时候使用。
28.弱引用和强引用、软引用、虚引用
Java四种引用:强引用、软引用、弱引用和虚引用代码示例与应用_春风化作秋雨的博客-CSDN博客
30.Java创建对象的几种方式?(已面)
31.静态变量和实例变量的区别?
静态变量通过类名或者对象名访问。
32.
33.Integer和int的区别?
34.super
(看看差不多得了,不用背)
如果构造方法的第一行代码不是 this() 和 super(),则系统会默认添加 super()。
在继承关系中,由于在子类的构造方法中,第一条语句默认为调用父类的无参构造方法(即默认为 super(),一般这行代码省略了)。所以当在父类中定义了有参构造方法,但是没有定义无参构造方法时,编译器会强制要求我们定义一个相同参数类型的构造方法。
作用:
1.使用super调用父类构造方法
2.调用父类的成员变量或者方法
this 关键字的用法:
35.String为什么是不可变的?
为什么String要设计成不可变的?_铁锚的博客-CSDN博客_string为什么设计成不可变的
36.String str="i"和String str=new String("i")一样吗?
String str = “i” 与 String str = new String(“i”) 一样吗?-帅地玩编程
37.访问修饰符
38.final finally finalize区别(知道就行,别背)
39.float f=3.14能通过编译吗(知道就行,别背)
【Java面试题】用float型定义变量:float = 3.14;,是否正确?_Jock.Liu的博客-CSDN博客_用float定义变量
41.讲讲静态内部类
Java静态内部类
42.常量池
详解JVM常量池、Class常量池、运行时常量池、字符串常量池(心血总结)_祈祷ovo的博客-CSDN博客_jvm常量池和字符串常量池
图有毛病,其他挺好。
1.Object
Object 类的常见方法有哪些?
Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法:
public final native Class> getClass()//native方法,用于返回当前运行时对象的Class对象,使用了final关键字修饰,故不允许子类重写。
public native int hashCode() //native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的HashMap。
public boolean equals(Object obj)//用于比较2个对象的内存地址是否相等,String类对该方法进行了重写用户比较字符串的值是否相等。
protected native Object clone() throws CloneNotSupportedException//naitive方法,用于创建并返回当前对象的一份拷贝。一般情况下,对于任何对象 x,表达式 x.clone() != x 为true,x.clone().getClass() == x.getClass() 为true。Object本身没有实现Cloneable接口,所以不重写clone方法并且进行调用的话会发生CloneNotSupportedException异常。
public String toString()//返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方法。
public final native void notify()//native方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
public final native void notifyAll()//native方法,并且不能重写。跟notify一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
public final native void wait(long timeout) throws InterruptedException//native方法,并且不能重写。暂停线程的执行。注意:sleep方法没有释放锁,而wait方法释放了锁 。timeout是等待时间。
public final void wait(long timeout, int nanos) throws InterruptedException//多了nanos参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上nanos毫秒。
public final void wait() throws InterruptedException//跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
protected void finalize() throws Throwable { }//实例被垃圾回收器回收的时候触发的操作
2.String(已面)
String、StringBuffer、StringBuilder 的区别?String 为什么是不可变的?
可变性
String不可变,StringBuilder/StringBuffer可变
简单的来说:保存字符串的数组被 final
修饰且为私有的,并且String
类没有提供/暴露修改这个字符串的方法,所以String对象是不可变的.
public final class String implements java.io.Serializable, Comparable, CharSequence {
private final char value[];
//...
}
StringBuilder
与 StringBuffer
都继承自 AbstractStringBuilder
类,在 AbstractStringBuilder
中也是使用字符数组保存字符串,不过没有使用 final
和 private
关键字修饰,最关键的是这个 AbstractStringBuilder
类还提供了很多修改字符串的方法比如 append
方法。
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
//...
}
线程安全性
String
中的对象是不可变的,也就可以理解为常量,线程安全StringBuffer
对方法加了同步锁,所以是线程安全的。StringBuilder
并没有对方法加同步锁,所以是非线程安全的。
性能
StringBuilder>StringBuffer>String
每次对 String
类型进行改变的时候,都会生成一个新的 String
对象,然后将指针指向新的 String
对象。StringBuffer
每次都会对 StringBuffer
对象本身进行操作,而不是生成新的对象并改变对象引用, StringBuilder
相比使用 StringBuffer
仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险.
对于三者使用的总结:
String
StringBuilder
StringBuffer
3.字符串拼接用“+” 还是 StringBuilder?(知道就行)
Java 语言 “+”和“+=”是专门为 String 类重载过的运算符,也是 Java 中仅有的两个重载过的元素符。
对象引用和“+”的字符串拼接方式,实际上是通过 StringBuilder
调用 append()
方法实现的,拼接完成之后调用 toString()
得到一个 String
对象 。
String str1 = "he";
String str2 = "llo";
String str3 = "world";
String str4 = str1 + str2 + str3;
4.String#equals() 和 Object#equals() 有何区别?
String
中的 equals
方法是被重写过的,比较的是 String 字符串的值是否相等。 Object
的 equals
方法是比较的对象的内存地址。
Java基础知识&面试题总结(下) | JavaGuide
1. Java 泛型了解么?什么是类型擦除?介绍一下常用的通配符?
Java 泛型(generics) 是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,可以在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说将操作的数据类型指定为一个参数。
Java 在运行期间,所有的泛型信息都会被擦掉,这也就是通常所说类型擦除 。
泛型一般有三种使用方式: 泛型类、泛型接口、泛型方法。
2.常用的通配符有哪些?
常用的通配符为: T,E,K,V,?
反射的作用和机制(已面) 使用的地方 如何使用反射创建对象 介绍 优缺点
2020 最新Java反射面试题_一枚小小菜鸟的博客-CSDN博客_java反射面试题
fJava基础之—反射(非常重要)_敬业小码哥的博客-CSDN博客_jvav
...
1.Exception和Error有什么区别?
Exception和Error都有一个父类 Throwable
。
Exception
:程序可以处理的异常,可以通过 catch
来进行捕获。Exception
又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。Error
:Error
是程序无法处理的错误 ,我们没办法通过 catch
来进行捕获 。例如JVM运行错误(Virtual MachineError
)、栈溢出错误、类定义错误(NoClassDefFoundError
)等 。错误发生时,JVM一般会选择终止线程。2.Checked Exception 和 Unchecked Exception 有什么区别?
Checked Exception 即受检查异常,Java 代码在编译过程中,如果受检查异常没有被 catch
/throw
处理的话,就没办法通过编译 。
除了RuntimeException
及其子类以外,其他的Exception
类及其子类都属于受检查异常 。(常见的受检查异常有: IO 相关的异常、ClassNotFoundException
、SQLException
...。)
Unchecked Exception 即 不受检查异常 ,Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。
RuntimeException
及其子类都统称为非受检查异常,例如:NullPointerException
、NumberFormatException
(字符串转换为数字)、ArrayIndexOutOfBoundsException
(数组越界)、ClassCastException
(类型转换错误)、ArithmeticException
(算术错误)等。
3.Throwable类常用方法有哪些?
String getMessage()
: 返回异常发生时的简要描述String toString()
: 返回异常发生时的详细信息String getLocalizedMessage()
: 返回异常对象的本地化信息。使用 Throwable
的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()
返回的结果相同void printStackTrace()
: 在控制台上打印 Throwable
对象封装的异常信息4.try-catch-finally 如何使用?
try
块:执行可能产生异常的代码。其后可接零个或多个 catch
块,如果没有 catch
块,则必须跟一个 finally
块。catch
块: 用于捕获并处理try 语句块产生的异常。finally
块: 无论是否捕获或处理异常,finally
块里的语句都会被执行。当在 try
块或 catch
块中遇到 return
语句时,finally
语句块将在方法返回之前被执行。不要在 finally 语句块中使用 return! 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句不会被执行。
finally中的不一定会执行的!
就比如说 finally
之前虚拟机被终止运行的话,finally 中的代码就不会被执行。
在以下 3 种特殊情况下,finally
块的代码也不会被执行:
5.throw和throws的区别?
可以看看
Throw和Throws的区别_思远:的博客-CSDN博客_throw throws区别
1.如何读取文件,并写入到另外一个路径(已面)
Java基础知识&面试题总结(下) | JavaGuide
Java核心(五)深入理解BIO、NIO、AIO - 云+社区 - 腾讯云(知道是啥就行了,差不多得了)
1.为什么Java中只有值传递?
Java中有两种数据类型:基本数据类型和引用数据类型
Java 中将实参传递给方法时 :
补充(可以不背):也就是说,两种参数传递的都是值,基本数据传递的是值的拷贝,引用数据类型传递的是对象的地址值。
变量或者对象间的赋值也是同理,传递的是值的拷贝,或者内存地址值
原理、是什么,用在什么地方
静态代理 是什么
代理详解!静态代理+JDK/CGLIB 动态代理实战 | JavaGuide
JAVA动态代理 - 简书
1.集合概览(已问)
Java 集合, 也叫作容器,主要是由两大接口派生而来:一个是 Collection
接口,主要用于存放单一元素;另一个是 Map
接口,主要用于存放键值对。对于Collection
接口,下面又有三个主要的子接口:List
、Set
和 Queue
。
Java 集合框架如下图所示:
注:图中只列举了主要的继承派生关系,并没有列举所有关系
2.说说 List, Set, Queue, Map 四者的区别?
List
(对付顺序的好帮手): 存储的元素是有序的、可重复的。Set
(注重独一无二的性质): 存储的元素是不可重复的。Queue
(实现排队功能的叫号机): 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。Map
(用 key 来搜索的专家): 存储键值对(key-value),key 是无序的 不可重复的,value 是无序的、可重复的,每个键只能映射到一个值。3.集合框架底层的数据结构总结
List
Arraylist
: 底层使用Object[]
数组存储Vector
:底层使用Object[]
数组存储LinkedList
: 底层使用双向链表Set
HashSet
: 基于 HashMap
实现的,底层采用 HashMap
来保存元素LinkedHashSet
: LinkedHashSet
是 HashSet
的子类,并且其内部是通过 LinkedHashMap
来实现的。TreeSet
: 底层使用红黑树(自平衡的排序二叉树)Queue
PriorityQueue
: 底层使用Object[]
数组来实现二叉堆ArrayQueue(ArrayDeque)
: 底层使用Object[]
数组 + 双指针Map
HashMap
: JDK1.8 之前 HashMap
由数组+链表组成的,数组是 HashMap
的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8), 如果当前数组的长度小于 64,那么会选择进行数组扩容,否则,将链表转化为红黑树,以减少搜索时间LinkedHashMap
: LinkedHashMap
继承自 HashMap
,所以它的底层仍然是由数组和链表或红黑树组成。另外,LinkedHashMap
在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。Hashtable
: 数组+链表组成的,数组是 Hashtable
的主体,链表则是主要为了解决哈希冲突而存在的TreeMap
: 底层使用红黑树(自平衡的排序二叉树)HashMap的扩容机制详解!!!
4.为什么要使用集合?(集合的优点)
存储多个类型相同的数据时, 我们可以采用数组来保存数据,但是数组存在一些弊端
(1)数组的缺点是一旦声明之后,长度就不可变了;
(2)数组存储的数据是有序的、可重复的,特点单一;
但是集合提高了数据存储的灵活性,集合不仅可以用来存储不同类型不同数量的对象,还可以保存具有映射关系的数据。
5.Arraylist 和 Vector 的区别?
ArrayList
是 List
的主要实现类,底层使用 Object[ ]
存储,适用于频繁的查找工作,线程不安全 ;Vector
是 List
的古老实现类,底层使用Object[ ]
存储,线程安全的。6.ArrayList和LinkedList的区别?
ArrayList
和 LinkedList
都是线程不安全的;Arraylist
底层使用的是 Object
数组;LinkedList
底层使用的是 双向链表 数据结构ArrayList
采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)
方法的时候, ArrayList
会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element)
)时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。LinkedList
采用链表存储,所以,如果是在头尾插入或者删除元素不受元素位置的影响(add(E e)
、addFirst(E e)
、addLast(E e)
、removeFirst()
、 removeLast()
),近似 O(1),如果是要在指定位置 i
插入和删除元素的话(add(int index, E element)
,remove(Object o)
) 时间复杂度近似为 O(n) ,因为需要先移动到指定位置再插入。LinkedList
不支持高效的随机元素访问,而 ArrayList
支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)
方法)。7.ArrayList的扩容机制?
总的来说就是分两步:
1、扩容
把原来的数组复制到另一个内存空间更大的数组中
2、添加元素
把新元素添加到扩容以后的数组中
(1)ArrayList底层是使用Object[]数组存储数据的。
ArrayList定义了两个空数组,以及默认的初始容量10,以及实际存放数据的数组elementData
简单看看
//定义一个空数组以供使用
private static final Object[] EMPTY_ELEMENTDATA = {};
//也是一个空数组,跟上边的空数组不同之处在于,这个是在默认构造器时返回的,扩容时需要用到这个作判断,后面会讲到
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//存放数组中的元素,注意此变量是transient修饰的,不参与序列化
transient Object[] elementData;
//数组的长度,此参数是数组中实际的参数,区别于elementData.length,后边会说到
private int size;
/**
* 默认初始容量大小
*/
private static final int DEFAULT_CAPACITY = 10;
(2)ArrayList有三个构造函数,不同的构造函数会影响后边的扩容机制判断
1.默认的无参构造
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
可以看到,调用此构造函数,elementData赋值为一个空数组(DEFAULTCAPACITY_EMPTY_ELEMENTDATA),此数组长度为0.
2.给定初始容量的构造函数
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
逻辑很简单,就是构造一个具有指定长度的空数组,当initialCapacity为0时,elementData赋值为一个空数组
3.包含特定集合元素的构造函数
public ArrayList(Collection extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
把传入的集合转换为数组并赋值给elementData,然后判断elementData的大小,若为0,elementData赋值为一个空数组。
四.扩容机制
扩容开始于集合添加元素方法,添加元素有两种方法
一个方法是add方法传入对象,一个方法是add方法传入下标和对象。
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,size - index);
elementData[index] = element;
size++;
}
可以看到两个方法都调用了ensureCapacityInternal(size + 1)方法,把数组长度加1以确保能存下下一个数据。
//得到最小扩容量
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 获取默认的容量和传入参数的较大值
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
size+1作为参数传入,被当成最小容量(minCapacity)。
如果在添加的时候数组是空的,那么最小容量(minCapacity)赋值为默认的初始容量10和minCapacity两者中的最大值;
然后调用ensureExplicitCapacity(minCapacity)方法,传入minCapacity.
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
通过这个地方是真正的增加长度,当需要最小的长度(也就是minCapacity)大于原来数组长度的时候就需要扩容了,相反的则不需要扩容
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
扩容会调用grow方法,将创建一个int newCapacity,值为elementData长度的1.5倍,然后检查newCapacity是否大于minCapacity(新容量是否大于最小需要容量),
若newCapacity小于minCapacity,newCapacity=minCapacity,将原来elementData数组中的数据copy到新数组中,elementData=新数组,长度为newCapacity,也就是elementData=Arrays.copyOf(elementData,newCapacity)语句。
8.Comparable 和 Comparator 的区别
Comparable自然排序,在实体类中实现,在java.lang包下
Comparable 可以让实现它的类的对象进行比较,具体的比较规则是按照 compareTo 方法中的规则进行。
(选背:compareTo 方法的返回值有三种情况:
(选背:实现了Comparable接口的数组或者List集合就能调用Collections.sort()
或者 Arrays.sort()
实现了 Comparable 接口的对象能够直接被用作 SortedMap (SortedSet) 的 key)
// person对象没有实现Comparable接口,所以必须实现,这样才不会出错,才可以使treemap中的数据按顺序排列
// 前面一个例子的String类已经默认实现了Comparable接口,详细可以查看String类的API文档,另外其他
// 像Integer类等都已经实现了Comparable接口,所以不需要另外实现了
public class Person implements Comparable {
private String name;
private int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
/**
* T重写compareTo方法实现按年龄来排序
*/
@Override
public int compareTo(Person o) {
//下面的语句等价与
//return this.age-o.getAge(); 升序排序
if (this.age > o.getAge()) {
return 1;
}
if (this.age < o.getAge()) {
return -1;
}
return 0;
}
}
public static void main(String[] args) {
TreeMap pdata = new TreeMap();
pdata.put(new Person("张三", 30), "zhangsan");
pdata.put(new Person("李四", 20), "lisi");
pdata.put(new Person("王五", 10), "wangwu");
pdata.put(new Person("小红", 5), "xiaohong");
// 得到key的值的同时得到key所对应的值
Set keys = pdata.keySet();
for (Person key : keys) {
System.out.println(key.getAge() + "-" + key.getName());
}
}
5-小红
10-王五
20-李四
30-张三
Comparator定制排序
Comparator在Java.util包下面,Comparator则是在外部制定排序规则,然后作为排序策略参数传递给排序类的某个方法,
(选背:比如 Collections.sort(), Arrays.sort(), 或者一些内部有序的集合(比如 SortedSet,SortedMap 等)。)
使用方式主要分三步:
ArrayList arrayList = new ArrayList();
arrayList.add(-1);
arrayList.add(3);
arrayList.add(3);
arrayList.add(-5);
arrayList.add(7);
arrayList.add(4);
arrayList.add(-9);
arrayList.add(-7);
System.out.println("原始数组:");
System.out.println(arrayList);
// void reverse(List list):反转
Collections.reverse(arrayList);
System.out.println("Collections.reverse(arrayList):");
System.out.println(arrayList);
// void sort(List list),按自然排序的升序排序
Collections.sort(arrayList);
System.out.println("Collections.sort(arrayList):");
System.out.println(arrayList);
// 定制排序的用法
Collections.sort(arrayList, new Comparator() {
@Override
public int compare(Integer o1, Integer o2) {
//等价于o2-o1 降序排序
return o2.compareTo(o1);
//return o1-o2;升序排序
}
});
System.out.println("定制排序后:");
System.out.println(arrayList);
原始数组:
[-1, 3, 3, -5, 7, 4, -9, -7]
Collections.reverse(arrayList):
[-7, -9, 4, 7, -5, 3, 3, -1]
Collections.sort(arrayList):
[-9, -7, -5, -1, 3, 3, 4, 7]
定制排序后:
[7, 4, 3, 3, -1, -5, -7, -9]
9.(Set)无序性和不可重复性的含义是什么?
1、什么是无序性?无序性不等于随机性 ,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加 ,而是根据数据的哈希值决定的。
2、什么是不可重复性?不可重复性是指添加的元素按照 equals()判断时 ,返回 false,需要同时重写 equals()方法和 HashCode()方法。
10.比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同?
HashSet
、LinkedHashSet
和 TreeSet
都是 Set
接口的实现类,都能保证元素唯一,并且都不是线程安全的。HashSet
、LinkedHashSet
和 TreeSet
的主要区别在于底层数据结构不同。HashSet
的底层数据结构是哈希表(基于 HashMap
实现)。LinkedHashSet
的底层数据结构是链表和哈希表,元素的插入和取出顺序满足 FIFO。TreeSet
底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。HashSet
用于不需要保证元素插入和取出顺序的场景,LinkedHashSet
用于保证元素的插入和取出顺序满足 FIFO 的场景,TreeSet
用于支持对元素自定义排序规则的场景。Collection 子接口之 Queue
11.Queue 与 Deque 的区别
Queue
是单端队列,只能从一端插入元素,另一端删除元素,实现上一般遵循 先进先出(FIFO) 规则。
Queue
扩展了 Collection
的接口,根据 因为容量问题而导致操作失败后处理方式的不同 可以分为两类方法: 一种在操作失败后会抛出异常,另一种则会返回特殊值。
重要:
Deque
是双端队列,在队列的两端均可以插入或删除元素。
Deque
扩展了 Queue
的接口, 增加了在队首和队尾进行插入和删除的方法,同样根据失败后处理方式的不同分为两类:
稍微背背:
12.ArrayDeque 与 LinkedList 的区别(暂时没背,没找到对应面试题)
ArrayDeque
和 LinkedList
都实现了 Deque
接口,两者都具有队列的功能,但两者有什么区别呢?
ArrayDeque
是基于可变长的数组和双指针来实现,而 LinkedList
则通过链表来实现。
ArrayDeque
不支持存储 NULL
数据,但 LinkedList
支持。
ArrayDeque
是在 JDK1.6 才被引入的,而LinkedList
早在 JDK1.2 时就已经存在。
ArrayDeque
插入时可能存在扩容过程, 不过均摊后的插入操作依然为 O(1)。虽然 LinkedList
不需要扩容,但是每次插入数据时均需要申请新的堆空间,均摊性能相比更慢。
从性能的角度上,选用 ArrayDeque
来实现队列要比 LinkedList
更好。(我自己用LinkedList,一般不用ArrayDeque)
13.说一说 PriorityQueue
(PriorityQueue
是在 JDK1.5 中被引入的) ,其与 Queue
的区别在于元素出队顺序是与优先级相关的,即总是优先级最高的元素先出队。
这里列举其相关的一些要点:
PriorityQueue
利用了二叉堆的数据结构来实现的,底层使用可变长的数组来存储数据PriorityQueue
通过堆元素的上浮和下沉,实现了在 O(logn) 的时间复杂度内插入元素和删除元素。PriorityQueue
是非线程安全的,且不支持存储 NULL
和 non-comparable(未排序)
的对象。PriorityQueue
默认是小顶堆,但可以接收一个 Comparator
作为构造参数,从而来自定义元素优先级的先后。Map接口
14.HashMap 和 Hashtable 的区别
HashMap
是非线程安全的,Hashtable
是线程安全的(如果你要保证线程安全的话就使用 ConcurrentHashMap
吧!);HashMap
要比 Hashtable
效率高一点。另外,Hashtable
基本被淘汰,不要在代码中使用它;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 的幂次方。)HashMap
由数组+链表组成的,数组是 HashMap
的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8), 如果当前数组的长度小于 64,那么会选择进行数组扩容,否则,将链表转化为红黑树,以减少搜索时间。 Hashtable 没有这样的机制。Hashtable数组+链表组成的,数组是 Hashtable
的主体,链表则是主要为了解决哈希冲突而存在的15.HashMap 和 HashSet 区别
如果你看过 HashSet
源码的话就应该知道:HashSet
底层就是基于 HashMap
实现的。(不背:HashSet
的源码非常非常少,因为除了 clone()
、writeObject()
、readObject()
是 HashSet
自己不得不实现之外),HashSet
的源码非常少,除了少量方法之外,其他方法都是直接调用 HashMap
中的方法。
16.HashMap 和 TreeMap 区别
(1)HashMap通常比TreeMap快(树和哈希表的数据结构使然)
(2)一般使用HashMap,在需要排序的Map时候才用TreeMap.
(3)HashMap和TreeMap都继承自AbstractMap,但是TreeMap还实现了SortedMap接口,这个接口让TreeMap具有对集合中的元素根据键排序的能力。
(4)HashMap和TreeMap都是非线程安全
(5)HashMap底层数据结构是Object数组和链表或者红黑树,而TreeMap的底层数据结构是红黑树。
看看
HashMap夺命14问,你能坚持到第几问?
17.HashSet如何检查重复?
当你把对象加入HashSet
时,HashSet
会先计算对象的hashcode
值来判断对象加入的位置,同时也会与其他加入的对象的 hashcode
值作比较,如果没有相符的 hashcode
,HashSet
会假设对象没有重复出现,将对象加入。
但是如果发现有相同 hashcode
值的对象,这时会调用equals()
方法来检查 hashcode
相等的对象是否真的相同。如果两者相同,HashSet
就不会让加入操作成功。
18.HashMap 的长度为什么是 2 的幂次方
为了能让 HashMap 存取高效,尽量较少哈希碰撞,也就是要尽量把数据分配均匀。
Hash值是一个int值,范围从2^-31至2^31-1,
所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。也就是hash%length。
HashMap长度为2的幂次方时,hash%length等价于(length-1)&hash,采用二进制位操作 &,相对于%能够提高运算效率。
19.HashMap为什么是线程不安全的?
会问
为什么hashmap的加载因子是0.75?
会问
21.ConcurrentHashMap 线程安全的具体实现方式/底层具体实现 (如何实现线程安全)
了解ConcurrentHashMap吗?
JDK1.7(上面有示意图)
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
ConcurrentHashMap
是由 Segment
数组结构和 HashEntry
数组结构组成。
Segment 扮演锁的角色。HashEntry
用于存储键值对数据。
static class Segment extends ReentrantLock implements Serializable {
}
一个 ConcurrentHashMap
里包含一个 Segment
数组。Segment
的结构和 HashMap
(jdk1.8之前)类似,是一种数组和链表结构,一个 Segment
包含一个 HashEntry
数组,每个 HashEntry
是一个链表结构的元素,每个 Segment
守护着一个 HashEntry
数组里的元素,当对 HashEntry
数组的数据进行修改时,必须首先获得对应的 Segment
的锁。
JDK1.8 (上面有示意图)
ConcurrentHashMap
取消了 Segment
分段锁,采用 CAS 和 synchronized
来保证并发安全。数据结构跟 HashMap1.8 的结构类似,数组+链表/红黑二叉树。Java 8 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N)))
synchronized
只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍。
22.ConcurrentHashMap 和 Hashtable 的区别
ConcurrentHashMap
和 Hashtable
的区别主要体现在实现线程安全的方式上不同。
ConcurrentHashMap
底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟 HashMap1.8
的结构一样,数组+链表/红黑二叉树。Hashtable
和 JDK1.8 之前的 HashMap
的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;ConcurrentHashMap
(分段锁) 对整个桶数组进行了分割分段(Segment
),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 到了 JDK1.8 的时候已经摒弃了 Segment
的概念,而是直接用 Node
数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized
和 CAS 来操作。(JDK1.6 以后 对 synchronized
锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap
,虽然在 JDK1.8 中还能看到 Segment
的数据结构,但是已经简化了属性,只是为了兼容旧版本;② Hashtable
(同一把锁) :使用 synchronized
来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。ConcurrentHashMap 图在上面
Collections工具类
23.Collections工具类常用方法
排序操作
void reverse(List list)//反转
void shuffle(List list)//随机排序
void sort(List list)//按自然排序的升序排序
void sort(List list, Comparator c)//定制排序,由Comparator控制排序逻辑
void swap(List list, int i , int j)//交换两个索引位置的元素
void rotate(List list, int distance)//旋转。当distance为正数时,将list后distance个元素整体移到前面。当distance为负数时,将 list的前distance个元素整体移到后面
查找,替换操作:
int binarySearch(List list, Object key)//对List进行二分查找,返回索引,注意List必须是有序的
int max(Collection coll)//根据元素的自然顺序,返回最大的元素。 类比int min(Collection coll)
int max(Collection coll, Comparator c)//根据定制排序,返回最大元素,排序规则由Comparatator类控制。类比int min(Collection coll, Comparator c)
void fill(List list, Object obj)//用指定的元素代替指定list中的所有元素
int frequency(Collection c, Object o)//统计元素出现次数
int indexOfSubList(List list, List target)//统计target在list中第一次出现的索引,找不到则返回-1,类比int lastIndexOfSubList(List source, list target)
boolean replaceAll(List list, Object oldVal, Object newVal)//用新元素替换旧元素
24.集合使用注意事项(稍微看看)
Java集合使用注意事项总结 | JavaGuide
HashMap底层详解
之前有的介绍,还有
25.HashMap的loadFactor 加载因子
loadFactor 负载因子是控制数组存放数据的疏密程度,loadFactor 越趋近于 1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor 越小,也就是趋近于 0,数组中存放的数据(entry)也就越少,也就越稀疏。
loadFactor 太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 0.75f 是官方给出的一个比较好的临界值。
给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。
当size>loadFactor*Capacity时,就会考虑对数组进行扩容。
26.HashMap的put方法
HashMap 只提供了 put 用于添加元素,putVal 方法只是给 put 方法调用的一个方法,并没有提供给用户使用。
对 putVal 方法添加元素的分析如下:
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value)
将元素添加进入。如果不是就遍历链表插入(插入的是链表尾部)。说明:上图有两个小问题:
我们再来对比一下 JDK1.7 put 方法的代码
对于 put 方法的分析如下:
看我的有道云笔记里面的多线程内容 多线程类型的题目!
1.进程 线程 是什么 两者区别 协程
进程
进程是程序的一次执行过程,是系统运行程序的基本单位。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。
线程
进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。
与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
区别(随便背几条就行)
根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的
影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行
3.为什么要用多线程?
① 为了更好的利用cpu的资源,如果只有一个线程,则第二个任务必须等到第一个任务结束后才能进行,如果使用多线程则在主线程执行任务的同时可以执行其他任务,而不需要等待;
② 进程之间不能共享数据,线程可以;
③ 系统创建进程需要为该进程重新分配系统资源,创建线程代价比较小;
④ Java语言内置了多线程功能支持,简化了java多线程编程。
4.线程的生命周期?
5.说说并发和并行的区别?
并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。
并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
6.多线程的创建方式?
方式1:继承于Thread类
1.创建一个集成于Thread类的子类
2.重写Thread类的run()方法
3.创建Thread子类的对象
4.通过此对象调用start()方法
稍微看看:{
start与run方法的区别:
start方法的作用:1.启动当前线程 2.调用当前线程的重写的run方法(在主线程中生成子线程,有两条线程)
调用start方法以后,一条路径代表一个线程,同时执行两线程时,因为时间片的轮换,所以执行过程随机分配,且一个线程对象只能调用一次start方法。
run方法的作用:在主线程中start()调用以后,直接在主线程一条线程中执行了该线程中run的方法。(调用线程中的run方法,只调用run方法,并不新开线程)
总结:我们不能通过run方法来新开一个线程,只能调用线程中重写的run方法(可以在线程中不断的调用run方法,但是不能开启子线程,即不能同时干几件事),start是开启线程,再调用方法(即默认开启一次线程,调用一次run方法,可以同时执行几件事)
}
例子:火车站买票
package com.example.paoduantui.Thread;
import android.view.Window;
/**
*
* 创建三个窗口卖票,总票数为100张,使用继承自Thread方式
* 用静态变量保证三个线程的数据独一份
*
* 存在线程的安全问题,有待解决
*
* */
public class ThreadDemo extends Thread{
public static void main(String[] args){
window t1 = new window();
window t2 = new window();
window t3 = new window();
t1.setName("售票口1");
t2.setName("售票口2");
t3.setName("售票口3");
t1.start();
t2.start();
t3.start();
}
}
class window extends Thread{
private static int ticket = 100; //将其加载在类的静态区,所有线程共享该静态变量
@Override
public void run() {
while(true){
if(ticket>0){
// try {
// sleep(100);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
System.out.println(getName()+"当前售出第"+ticket+"张票");
ticket--;
}else{
break;
}
}
}
}
方式2:实现Runable接口方式
1.创建一个实现了Runable接口的类
2.实现类去实现Runnable中的抽象方法:run()
3.创建实现类的对象
4.将此对象作为参数传递到Thread类中的构造器中,创建Thread类的对象
5.通过Thread类的对象调用start()
package com.example.paoduantui.Thread;
public class ThreadDemo01 {
public static void main(String[] args){
window1 w = new window1();
//虽然有三个线程,但是只有一个窗口类实现的Runnable方法,由于三个线程共用一个window对象,所以自动共用100张票
Thread t1=new Thread(w);
Thread t2=new Thread(w);
Thread t3=new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class window1 implements Runnable{
private int ticket = 100;
@Override
public void run() {
while(true){
if(ticket>0){
// try {
// sleep(100);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
System.out.println(Thread.currentThread().getName()+"当前售出第"+ticket+"张票");
ticket--;
}else{
break;
}
}
}
}
新增的两种创建多线程方式
实现callable接口方式:
与使用runnable方式相比,callable功能更强大些:
runnable重写的run方法不如callable的call方法强大,call方法可以有返回值
方法可以抛出异常
支持泛型的返回值
需要借助FutureTask类,比如获取返回结果
package com.example.paoduantui.Thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* 创建线程的方式三:实现callable接口。---JDK 5.0新增
*是否多线程?否,就一个线程
*
* 比runable多一个FutureTask类,用来接收call方法的返回值。
* 适用于需要从线程中接收返回值的形式
*
* //callable实现新建线程的步骤:
* 1.创建一个实现callable的实现类
* 2.实现call方法,将此线程需要执行的操作声明在call()中
* 3.创建callable实现类的对象
* 4.将callable接口实现类的对象作为传递到FutureTask的构造器中,创建FutureTask的对象
* 5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start方法启动(通过FutureTask的对象调用方法get获取线程中的call的返回值)
*
* */
//实现callable接口的call方法
class NumThread implements Callable{
private int sum=0;//
//可以抛出异常
@Override
public Object call() throws Exception {
for(int i = 0;i<=100;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName()+":"+i);
sum += i;
}
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args){
//new一个实现callable接口的对象
NumThread numThread = new NumThread();
//通过futureTask对象的get方法来接收futureTask的值
FutureTask futureTask = new FutureTask(numThread);
Thread t1 = new Thread(futureTask);
t1.setName("线程1");
t1.start();
try {
//get返回值即为FutureTask构造器参数callable实现类重写的call的返回值
Object sum = futureTask.get();
System.out.println(Thread.currentThread().getName()+":"+sum);
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
线程池方式(可以不背)
代码如下:
package com.example.paoduantui.Thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 创建线程的方式四:使用线程池(批量使用线程)
*1.需要创建实现runnable或者callable接口方式的对象
* 2.创建executorservice线程池
* 3.将创建好的实现了runnable接口类的对象放入executorService对象的execute方法中执行。
* 4.关闭线程池
*
* */
class NumberThread implements Runnable{
@Override
public void run() {
for(int i = 0;i<=100;i++){
if (i % 2 ==0 )
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
class NumberThread1 implements Runnable{
@Override
public void run() {
for(int i = 0;i<100; i++){
if(i%2==1){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args){
//创建固定线程个数为十个的线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
//new一个Runnable接口的对象
NumberThread number = new NumberThread();
NumberThread1 number1 = new NumberThread1();
//执行线程,最多十个
executorService.execute(number1);
executorService.execute(number);//适合适用于Runnable
//executorService.submit();//适合使用于Callable
//关闭线程池
executorService.shutdown();
}
}
7.什么是上下文切换?
线程在执行过程中会有自己的运行状态和信息(也称上下文),比如上文所说到过的程序计数器,栈信息等。当出现如下情况的时候,线程会从占用 CPU 状态中退出。
sleep()
, wait()
等。这三种情况发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 上下文切换。
上下文频繁切换就会造成整体效率低下。
8.什么是线程死锁?如何预防和避免线程死锁?(差不多得了)
死锁,是指多个线程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。 如下图所示:如果此时有一个线程 A,已经持有了锁 A,但是试图获取锁 B,线程 B 持有锁 B,而试图获取锁 A,这种情况下就会产生死锁。
死锁是两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。
死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
public class DeadLockDemo {
private static Object resource1 = new Object();//资源 1
private static Object resource2 = new Object();//资源 2
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "线程 2").start();
}
}
学过操作系统的朋友都知道产生死锁必须具备以下四个条件:
如何预防死锁? 破坏死锁的产生的必要条件即可:
如何避免死锁?
避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。
安全状态 指的是系统能够按照某种线程推进顺序(P1、P2、P3.....Pn)来为每个线程分配所需资源,直到满足每个线程对资源的最大需求,使每个线程都可顺利完成。称
序列为安全序列。
我们对线程 2 的代码修改成下面这样就不会产生死锁了。
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 2").start();
9.说说 sleep() 方法和 wait() 方法区别和共同点?
sleep()
方法没有释放锁,而 wait()
方法释放了锁 。wait()
通常被用于线程间交互/通信,sleep()
通常被用于暂停执行。wait()
方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()
或者 notifyAll()
方法,用 wait(long timeout)
超时后线程会自动苏醒。sleep()
方法执行完成后,线程会自动苏醒。10.为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
(差不多得了,是这个意思然后巴拉巴拉一堆就行)
调用 start()
方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start()
会执行线程的相应准备工作,然后自动执行 run()
方法的内容,这是真正的多线程工作。 但是,直接执行 run()
方法,会把 run()
方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
CPU缓存模型,可以不背,不多
12.讲一下JMM(JAVA内存模型)
(可以不背:Java 内存模型主要目的是为了屏蔽系统和硬件的差异,避免一套代码在不同的平台下产生的效果不一致。)
(可以不背:在 JDK1.2 之前,Java 的内存模型实现总是从主存(即共享内存)读取变量,是不需要进行特别的注意的。而)
(答到黑体字的几点就行,差不多得了)
JMM规定了所有的变量都存储在主内存(Main Memory)中。每个线程还有自己的本地内存(Working Memory),线程的本地内存中保存了该线程使用到的变量的主内存的副本拷贝
线程对变量的所有操作(读取、赋值等)都必须在本地内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有本地内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)。
不同的线程之间也无法直接访问对方本地内存中的变量,线程之间值的传递都需要通过主内存来完成
这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致
(。。。不背)
要解决这个问题,就需要把变量声明为 volatile
,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
所以,volatile
关键字 除了防止 JVM 的指令重排 ,还有一个重要的作用就是保证变量的可见性。
13.并发编程的三个特性?(在 Java 程序中怎么保证多线程的运行安全?)
synchronized
可以保证代码片段的原子性。)volatile
关键字可以保证共享变量的可见性。)volatile
关键字可以禁止指令进行重排序优化。)14.volatile关键字
1.被volitale修饰的变量 保证可见性 和 防止重排序
2. 增加了volitale的时候,
2.1 写的时候, 会把当前线程的共享变量的值刷新的主内存中
2.2 读的时候,当前线程的共享变量的值失效,只能从主内存读取
3. (不会内存屏蔽就别背),通过内存屏蔽的方式防止重排序
JMM内存模型
加分(可以不背):
volatile用于保证内存的可见性,可以将其看做是轻量级的锁,它具有如下的内存语义:
volatile的底层是采用内存屏障来实现的,就是在编译器生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。内存屏障就是一段与平台相关的代码,Java中的内存屏障代码都在Unsafe类中定义,共包含三个方法:LoadFence()、storeFence()、fullFence()。
15.说说synchronized和volatile的区别?
synchronized
关键字和 volatile
关键字是两个互补的存在,而不是对立的存在!
volatile
关键字是线程同步的轻量级实现,所以 volatile
性能肯定比synchronized
关键字要好 。但是 volatile
关键字只能用于变量而 synchronized
关键字可以修饰方法以及代码块 。volatile
关键字能保证数据的可见性,但不能保证数据的原子性。synchronized
关键字两者都能保证。volatile
关键字主要用于解决变量在多个线程之间的可见性,而 synchronized
关键字解决的是多个线程之间访问资源的同步性。16.ThreadLocal简介
ThreadLocal(是Thread Local Variable,线程局部变量)类是Java为线程安全提供的一个工具类,代表一个线程局部变量。把数据放在ThreadLocal中可以让每个线程创建一个该变量的副本,线程间可以独立地改变自己的副本,而不会和其他线程产生副本冲突,从而避免并发访问的线程安全问题,就像每个线程都完全拥有该变量一样。
采用方法
17.ThreadLocal原理(底层结构)
this指当前ThreadLocal
在Thread类中
public class Thread implements Runnable {
//......
//与此线程有关的ThreadLocal值。由ThreadLocal类维护
ThreadLocal.ThreadLocalMap threadLocals = null;
}
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
}
可看出ThreadLocalMap是ThreadLocal的内部静态类,而它的构成主要是用Entry来保存数据 ,而且还是继承的弱引用。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value。详细内容要大家自己去跟。
set方法
public void set(T value) {
//1、获取当前线程
Thread t = Thread.currentThread();
//2、获取线程中的属性 threadLocalMap ,如果threadLocalMap 不为空,
//则直接更新要保存的变量值,否则创建threadLocalMap,并赋值
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
// 初始化thradLocalMap 并赋值
createMap(t, value);
}
从上面的代码可以看出,ThreadLocal set赋值的时候首先会获取当前线程thread,并获取thread线程中的ThreadLocalMap属性。如果map属性不为空,则直接更新value值,如果map为空,则实例化threadLocalMap,并将value值初始化。
get方法
在get方法的实现中,首先获取当前调用者线程,如果当前线程的threadLocals不为null,就直接返回threadLocals中的值,否则执行setInitialValue方法初始化threadLocals变量。在setInitialValue方法中,类似于set方法的实现,都是判断当前线程的threadLocals变量是否为null,是则设置值(这个时候由于是初始化,所以添加的值为null),否则创建threadLocals变量,同样添加的值为null。
public T get() {
//1、获取当前线程
Thread t = Thread.currentThread();
//2、获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
//3、如果map数据为空,
if (map != null) {
//3.1、获取threalLocalMap中存储的值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果是数据为null,则初始化,初始化的结果,TheralLocalMap中存放key值为threadLocal,值为null
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
remove方法
remove方法判断该当前线程对应的threadLocals变量是否为null,不为null就直接删除当前线程中指定的threadLocals变量
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
18.ThreadLocal内存泄漏问题?
ThreadLocalMap
中使用的 key 为 ThreadLocal
的弱引用,而 value 是强引用。所以,如果 ThreadLocal
没有被外部强引用的情况下,在垃圾回收的时候,key(ThreadLocal)被会被清理掉。这样一来,ThreadLocalMap
中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()
、get()
、remove()
方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal
方法后 最好手动调用remove()
方法。
(不背:弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。所以key会在垃圾回收的时候被回收掉, 而key对应的value则不会被回收, 这样会导致一种现象:key为null,value有值。)
(不背:内存泄露为程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光,
广义并通俗的说,就是:不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。)
19.ThreadLocal和Synchronized的区别
20.ThreadLocal使用场景(不背就算了)
扯一下16题,Session管理、数据库连接,处理数据库事务。
21.说一说自己对于 synchronized 关键字的了解?
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized 关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。它是一种悲观锁,是一种可重入锁。
synchronized void method() {
//业务代码
}
synchronized static void method() {
//业务代码
}
synchronized(this) {
//业务代码
}
24.synchronized和Lock的区别
来源:
lock是一个接口,而synchronized是java的一个关键字,synchronized是内置的语言实现;
异常是否释放锁:
synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。)
是否响应中断
lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断;
是否知道获取锁
Lock可以通过trylock来知道有没有获取锁,而synchronized不能;
25.什么是线程池?为什么要使用线程池,线程池的好处?
记住基本意思,差不多得了:
线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程,而是返回池中从而减少创建和销毁对象的开销
看看,no背:
线程池作用就是限制系统中执行线程的数量。
根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池中有等待的工作线程,就可以开始运行了;否则进入等待队列。
背:
使用线程池的好处:
25.线程池ThreadPoolExecutor
构造函数的构造参数?
/**
* 用给定的初始参数创建一个新的ThreadPoolExecutor。
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
ThreadPoolExecutor
参数:
int corePoolSize
: 核心线程数定义了最小可以同时运行的线程数量。int maximumPoolSize
: 最大线程数。long keepAliveTime
:当线程池中的线程数量大于 corePoolSize
的时候,如果这时没有新的任务提交,核心线程以外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime
才会被回收销毁;TimeUnit unit
: keepAliveTime
参数的时间单位。BlockingQueue workQueue
: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。ThreadFactory threadFactory
:就是创建线程的线程工厂。RejectExecutionHandler handler
:饱和策略。关于饱和策略下面单独介绍一下。26.如何创建线程池?
可以扯一下,多任务出来的放入队列中,等待有空闲线程取,或者,创建线程,放入线程池等待
可缓存、周期性、单个、定长
CachedThreadPool:可缓存的线程池,该线程池中没有核心线程,非核心线程的数量限制为Integer.max_value,就是无限大,当有需要时创建线程来执行任务,没有需要时回收空闲线程,适用于耗时少,任务量大的情况。
SecudleThreadPool:周期性执行任务的线程池,按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程的大小也为无限大。适用于执行周期性的任务。
SingleThreadPool:只有一条线程来执行任务,适用于有顺序的任务的应用场景。
FixedThreadPool:定长的线程池,有核心线程,核心线程的即为最大的线程数量,没有非核心线程
对应 Executors 工具类中的方法如图所示:
27.线程池的工作流程?
核心线程池->任务队列->最大线程池
(是这个意思就行,差不多得了)
线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
当调用 execute() 方法添加一个任务时,线程池会做如下判断
1、如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务(即使有空闲线程也会创建执行)
2、如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务,这个时候线程池中的线程数小于maximumPoolSize
3、如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理,这个时候缓冲队列不允许有新的任务了
4、如果线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,也就是allowCoreThreadTimeOut(true),那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止
29.execute()方法和submit()方法的区别?
都是用来提交任务的。
execute()
方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;submit()
方法用于提交需要返回值的任务。线程池会返回一个 Future
类型的对象,通过这个 Future
对象可以判断任务是否执行成功,并且可以通过 Future
的 get()
方法来获取返回值,get()
方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)
方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。 (1)可以接受的任务类型
execute只能接受Runnable类型的任务
submit不管是Runnable还是Callable类型的任务都可以接受
30.handler的拒绝(饱和)策略:
有四种:第一种AbortPolicy:不执行新任务,直接抛出异常,提示线程池已满(RejectedExecutionException)
第二种DisCardPolicy:不执行新任务,也不抛出异常
第三种DisCardOldSetPolicy:将消息队列中的第一个任务替换为当前新进来的任务执行
第四种CallerRunsPolicy:直接调用execute来执行当前任务,(调用执行自己的线程运行任务,也就是直接在调用execute
方法的线程中运行(run
)被拒绝的任务)
!!!!!!!!!并发编程,先看一圈看的差不多了再背!!!!!!!!
31.Atomic原子类
JUC 中的 Atomic 原子类总结
Atomic底层原理 具体如何加锁 volatile和Atomic区别 Atomic Integer和Integer区别 AtomicInteger原理 在高并发下有什么问题 理解介绍
31.简单说下对Java中原子类的理解?
这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
所以,所谓原子类说简单点就是具有原子/原子操作特征的类。
并发包 java.util.concurrent
的原子类都存放在java.util.concurrent.atomic
下,如下图所示。
(?背不背再说吧)
基本类型
使用原子的方式更新基本类型
AtomicInteger
:整形原子类AtomicLong
:长整型原子类AtomicBoolean
:布尔型原子类数组类型
使用原子的方式更新数组里的某个元素
AtomicIntegerArray
:整形数组原子类AtomicLongArray
:长整形数组原子类AtomicReferenceArray
:引用类型数组原子类引用类型
AtomicReference
:引用类型原子类AtomicStampedReference
:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。AtomicMarkableReference
:原子更新带有标记位的引用类型对象的属性修改类型
AtomicIntegerFieldUpdater
:原子更新整形字段的更新器AtomicLongFieldUpdater
:原子更新长整形字段的更新器AtomicReferenceFieldUpdater
:原子更新引用类型字段的更新器32.Atomic底层原理?
CAS锁,先跳过
33.AtomicInteger原理?
先跳过
34.AQS 介绍、知道吗? 底层原理 怎么实现公平锁和非公平锁
35.AQS原理、介绍,什么是AQS?
AQS内部维护了一个CLH队列来管理锁。线程会首先尝试获取锁,如果失败就将当前线程及等待状态等信息包装成一个node节点加入到同步队列sync queue里。 接着会不断的循环尝试获取锁,条件是当前节点为head的直接后继才会尝试。如果失败就会阻塞自己直到自己被唤醒。而当持有锁的线程释放锁的时候,会唤醒队列中的后继线程。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
看个 AQS(AbstractQueuedSynchronizer)原理图:
AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。
private volatile int state;//共享变量,使用volatile修饰保证线程可见性
状态信息通过 protected 类型的 getState,setState,compareAndSetState 进行操作
//返回同步状态的当前值
protected final int getState() {
return state;
}
//设置同步状态的值
protected final void setState(int newState) {
state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
36.AQS 对资源的共享方式
AQS 定义两种资源共享方式
ReentrantLock
。又可分为公平锁和非公平锁:
CountDownLatch
、Semaphore
、 CyclicBarrier
、ReadWriteLock
我们都会在后面讲到。37.AQS底层原理
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
private static final long serialVersionUID = 7373984972572414691L;
/**
* Creates a new {@code AbstractQueuedSynchronizer} instance
* with initial synchronization state of zero.
*/
protected AbstractQueuedSynchronizer() { }
static final class Node {
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
/** waitStatus value to indicate thread has cancelled. */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking. */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition. */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate.
*/
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
/**
* The thread that enqueued this node. Initialized on
* construction and nulled out after use.
*/
volatile Thread thread;
Node nextWaiter;
/**
* Returns true if node is waiting in shared mode.
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
/** Establishes initial head or SHARED marker. */
Node() {}
/** Constructor used by addWaiter. */
Node(Node nextWaiter) {
this.nextWaiter = nextWaiter;
THREAD.set(this, Thread.currentThread());
}
/** Constructor used by addConditionWaiter. */
Node(int waitStatus) {
WAITSTATUS.set(this, waitStatus);
THREAD.set(this, Thread.currentThread());
}
/** CASes waitStatus field. */
final boolean compareAndSetWaitStatus(int expect, int update) {
return WAITSTATUS.compareAndSet(this, expect, update);
}
/** CASes next field. */
final boolean compareAndSetNext(Node expect, Node update) {
return NEXT.compareAndSet(this, expect, update);
}
final void setPrevRelaxed(Node p) {
PREV.set(this, p);
}
// VarHandle mechanics
private static final VarHandle NEXT;
private static final VarHandle PREV;
private static final VarHandle THREAD;
private static final VarHandle WAITSTATUS;
static {
try {
MethodHandles.Lookup l = MethodHandles.lookup();
NEXT = l.findVarHandle(Node.class, "next", Node.class);
PREV = l.findVarHandle(Node.class, "prev", Node.class);
THREAD = l.findVarHandle(Node.class, "thread", Thread.class);
WAITSTATUS = l.findVarHandle(Node.class, "waitStatus", int.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
}
private transient volatile Node head;
/**
* Tail of the wait queue, lazily initialized. Modified only via
* method enq to add new wait node.
*/
private transient volatile Node tail;
/**
* The synchronization state.
*/
private volatile int state;
/**
* Returns the current value of synchronization state.
* This operation has memory semantics of a {@code volatile} read.
* @return current state value
*/
...其他方法
}
Java并发包基石-AQS详解 - dreamcatcher-cx - 博客园
39.ConcurrentHashMap介绍
线程安全的一个HashMap,底层
40.CopyOnWriteArrayList介绍,使用场景?
(差不多得了,从写和读两方面回答,加锁不加锁)
CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
适用场景:读多写少的场景
看一看,不背
41.CopyOnWriteArrayList底层具体怎么实现?(差不多得了,就这样,别再找更好的资料了)
CopyOnWriteArrayList的特点:
public class CopyOnWriteArrayList implements List, RandomAccess, Cloneable, java.io.Serializable {
//序列号
private static final long serialVersionUID = 8673264195747942595L;
/**
* 可重入锁,对数组增删改时,使用它加锁
*/
final transient ReentrantLock lock = new ReentrantLock();
/**
* 存放元素的数组,其实就是本体
*/
private transient volatile Object[] array;
}
构造方法内部都使用了setArray
在上述的构造方法中都使用了setArray()这个方法,不过这个方法只是将array引用指向对应的数组对象罢了,这个方法不是关键。
//设置数组引用指向的数组对象
final void setArray(Object[] a) {
array = a;
}
/**
* 默认构造方法,构建数组长度为0,
* 没错就是0,接下来会知道为什么这么做
*/
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
/**
* 通过集合类来构造
*/
public CopyOnWriteArrayList(Collection extends E> c) {
Object[] elements;
if (c.getClass() == CopyOnWriteArrayList.class)
elements = ((CopyOnWriteArrayList>)c).getArray();
else {
elements = c.toArray();
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
setArray(elements);
}
/**
* 通过数组来构造
*/
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
添加元素
//直接将元素添加到末尾
public boolean add(E e) {
//获取锁
final ReentrantLock lock = this.lock;
//加锁
lock.lock();
try {
//先获取原先的数组
Object[] elements = getArray();
int len = elements.length;
//构建一个新的数组,大小是原数组的大小 1
Object[] newElements = Arrays.copyOf(elements, len 1);
//将元素插入到数组末尾
newElements[len] = e;
//将array引用指向新的数组,原来的数组会被垃圾收集器回收
setArray(newElements);
return true;
} finally {
//释放锁
lock.unlock();
}
}
//在指定位置插入新元素
public void add(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//判断是否越界
if (index > len || index < 0)
throw new IndexOutOfBoundsException("Index: " index ", Size: " len);
Object[] newElements;
//计算插入位置与数组末尾下标的距离
int numMoved = len - index;
//若为0,则是添加到数组末尾
if (numMoved == 0)
newElements = Arrays.copyOf(elements, len 1);
else {
//不为0,则将原数组分开复制
newElements = new Object[len 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index, newElements, index 1, numMoved);
}
newElements[index] = element;
setArray(newElements);
} finally {
lock.unlock();
}
}
删除元素,很简单,就是判断要删除的元素是否最后一个,如果最后一个直接在复制副本数组的时候,复制长度为旧数组的 length-1 即可;
但是如果不是最后一个元素,就先复制旧的数组的index前面元素到新数组中,然后再复制旧数组中index后面的元素到数组中,最后再把新数组的引用赋值给旧数组的引用。最后在finally语句块中将锁释放。
public E remove(int index) {
//加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);
int numMoved = len - index - 1;
if (numMoved == 0)
//如果要删除的是列表末端数据,拷贝前len-1个数据到新副本上,再切换引用
setArray(Arrays.copyOf(elements, len - 1));
else {
//否则,将除要删除元素之外的其他元素拷贝到新副本中,并切换引用
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
setArray(newElements);
}
return oldValue;
} finally {
//解锁
lock.unlock();
}
}
获取元素
读的时候不需要加锁,直接返回数组中的数据。
//这个是真正用于查询的方法
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
/**
* 向外开放的方法
*/
public E get(int index) {
return get(getArray(), index);
}
final Object[] getArray() {
return array;
}
42.(别删)BlockingQueue 介绍 阻塞功能如何实现 写一个 底层 有哪些,具体有什么功能
42.BlockingQueue介绍
阻塞队列(BlockingQueue
)被广泛使用在“生产者-消费者”问题中,其原因是 BlockingQueue
提供了可阻塞的插入和移除的方法。当队列容器已满,生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止。
BlockingQueue
是一个接口,继承自 Queue
43.BlockingQueue有哪些?具体有什么功能
记1、2、4实现类就行了
可能实现类也需要考???找找
44.BlockingQueue方法(稍微看看,不背)
public interface BlockingQueue extends Queue {
/**
* 入队一个元素,如果有空间则直接插入,并返回true;
* 如果没有空间则抛出IllegalStateException
*/
boolean add(E e);
/**
* 入队一个元素,如果有空间则直接插入,并返回true;
* 如果没有空间返回false
*/
boolean offer(E e);
/**
* 入队一个元素,如果有空间则直接插入,如果没有空间则一直阻塞等待
*/
void put(E e) throws InterruptedException;
/**
* 入队一个元素,如果有空间则直接插入,并返回true;
* 如果没有空间则等待timeout时间,插入失败则返回false
*/
boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException;
/**
* 出队一个元素,如果存在则直接出队,如果没有空间则一直阻塞等待
*/
E take() throws InterruptedException;
/**
* 出队一个元素,如果存在则直接出队,如果没有空间则等待timeout时间,无元素则返回null
*/
E poll(long timeout, TimeUnit unit) throws InterruptedException;
/**
* 返回该队列剩余的容量(如果没有限制则返回Integer.MAX_VALUE)
*/
int remainingCapacity();
/**
* 如果元素o在队列中存在,则从队列中删除
*/
boolean remove(Object o);
/**
* 判断队列中是否存在元素o
*/
public boolean contains(Object o);
/**
* 将队列中的所有元素出队,并添加到给定的集合c中,返回出队的元素数量
*/
int drainTo(Collection super E> c);
/**
* 将队列中的元素出队,限制数量maxElements个,并添加到给定的集合c中,返回出队的元素数量
*/
int drainTo(Collection super E> c, int maxElements);
}
43.ConcurrentLinkedQueue介绍?(可以不背,考的很少)
44.ConcurrentSkipListMap介绍和底层实现 (很少考,不背了)
45.CountDownLatch和CyclicBarrier区别 CyclicBarrier介绍,实现原理 CountDownLatch介绍 countDownLatch底层原理、应用场景、常用方法
45.CountDownLatch介绍
CountDownLatch可以使一个获多个线程等待其他线程各自执行完毕后再执行。
CountDownLatch 定义了一个计数器,和一个阻塞队列, 当计数器的值递减为0之前,阻塞队列里面的线程处于挂起状态,当计数器递减到0时会唤醒阻塞队列所有线程,这里的计数器是一个标志,可以表示一个任务一个线程,也可以表示一个倒计时器,CountDownLatch可以解决那些一个或者多个线程在执行之前必须依赖于某些必要的前提业务先执行的场景。
常用方法
CountDownLatch(int count); //构造方法,创建一个值为count 的计数器。
await();//阻塞当前线程,将当前线程加入阻塞队列。
await(long timeout, TimeUnit unit);//在timeout的时间之内阻塞当前线程,时间一过则当前线程可以执行,
countDown();//对计数器进行递减1操作,当计数器递减至0时,当前线程会去唤醒阻塞队列里的所有线程。
46.CountDownLatch实现原理,底层?
public class CountDownLatch {
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
Sync(int count) {
setState(count);
}
int getCount() {
return getState();
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c - 1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
private final Sync sync;
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
public void countDown() {
sync.releaseShared(1);
}
public long getCount() {
return sync.getCount();
}
public String toString() {
return super.toString() + "[Count = " + sync.getCount() + "]";
}
}
CountDownLatch原理_以千的博客-CSDN博客
(差不多就行)主要背一下总过程
47.CountDownLatch应用场景?(差不多得了,细节随便扯扯就行)
1、线程在开始运行前等待其他任务执行完毕。
将 CountDownLatch
的计数器初始化为 n (new CountDownLatch(n)
),每当一个任务线程执行完毕,就将计数器减 1 (countdownlatch.countDown()
),当计数器的值变为 0 时,在 CountDownLatch 上 await()
的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。
2、实现多个线程在某时刻开始同时开始执行
注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的 CountDownLatch
对象,将其计数器初始化为 1 (new CountDownLatch(1)
),多个线程在开始执行任务前首先 coundownlatch.await()
,当主线程调用 countDown()
时,计数器变为 0,多个线程同时被唤醒。
47.CyclicBarrier介绍(差不多就行)
CyclicBarrier也叫同步屏障,可以让一组线程达到一个屏障时被阻塞,直到最后一个线程达到屏障时,所以被阻塞的线程才能继续执行。
CyclicBarrier好比一扇门,默认情况下关闭状态,堵住了线程执行的道路,直到所有线程都就位,门才打开,让所有线程一起通过。
48.CyclicBarrier原理/底层
JavaGuide写的可以
49.CountDownLatch和CyclicBarrier区别?
CountDownLatch
是计数器,只能使用一次,而 CyclicBarrier
可以多次使用。
CountDownLatch: 一个或者多个线程,等待其他多个线程完成某件事情之后才能执行;
CyclicBarrier : 多个线程互相等待,直到到达同一个同步点,再继续一起执行。
计数器递增/递减方面
50.ReentrantLock介绍 绑定多个Condition的原理 原理 如何实现公平
50.ReentrantLock介绍
51.ReentranLock底层原理
深入理解ReentrantLock的实现原理 - 掘金
47.CompletableFuture中get方法说一下,用了对当前线程有什么影响 原理 如何使用 超时处理 与线程池的区别 (考得好像不是很多,背一下介绍就行)
48.悲观锁和乐观锁 区别 是什么 使用场景 怎么实现
【BAT面试题系列】面试官:你了解乐观锁和悲观锁吗? - 编程迷思 - 博客园
49.CAS原理,公平锁和非公平锁区别 效率 CAS和synchronized区别
CAS ABA问题?啥玩意
优缺点
什么是CAS机制?_1*null的博客-CSDN博客_cas机制
50. CAS和synchronized区别
1、对于资源竞争较少的情况:性能cas>synchronized,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
2、对于资源竞争严重的情况:性能cas
乐观锁/悲观锁方面
背会的并发容器,不会的别背!
可重入锁:同一个线程可以多次获取该锁(就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁)
协程
多线程题目手写
(已面)
1.Interface
( interface 的设计初衷是面向抽象,提高扩展性。这也留有一点遗憾,Interface 修改的时候,实现它的类也必须跟着改。)
( 为了解决接口的修改与现有的实现不兼容的问题。)新 interface 的方法可以用default
或 static
修饰,这样就可以有方法体,实现类也不必重写此方法。
一个 interface 中可以有多个方法被它们修饰,这 2 个修饰符的区别主要也是普通方法和静态方法的区别。
default
修饰的方法,是普通实例方法,可以用this
调用,可以被实现类继承和重写。static
修饰的方法,使用上和一般类静态方法一样。但它不能被实现类继承,只能用Interface
调用。public interface InterfaceNew {
static void sm() {
System.out.println("interface提供的方式实现");
}
static void sm2() {
System.out.println("interface提供的方式实现");
}
default void def() {
System.out.println("interface default方法");
}
default void def2() {
System.out.println("interface default2方法");
}
//须要实现类重写
void f();
}
public interface InterfaceNew1 {
default void def() {
System.out.println("InterfaceNew1 default方法");
}
}
如果有一个类既实现了 InterfaceNew
接口又实现了 InterfaceNew1
接口,它们都有def()
,并且 InterfaceNew
接口和 InterfaceNew1
接口没有继承关系的话,这时就必须重写def()
。不然的话,编译的时候就会报错。
public class InterfaceNewImpl implements InterfaceNew , InterfaceNew1{
public static void main(String[] args) {
InterfaceNewImpl interfaceNew = new InterfaceNewImpl();
interfaceNew.def();
}
@Override
public void def() {
InterfaceNew1.super.def();
}
@Override
public void f() {
}
}
2.函数式接口
定义:有且只有一个抽象方法,但可以有多个非抽象方法的接口(接口默认方法的修饰符为public abstract)。
在 java 8 中专门有一个包放函数式接口java.util.function
,该包下的所有接口都有 @FunctionalInterface
注解,提供函数式编程。
在其他包中也有函数式接口,其中一些没有@FunctionalInterface
注解,但是只要符合函数式接口的定义就是函数式接口,与是否有
@FunctionalInterface
注解无关,注解只是在编译时起到强制规范定义的作用。其在 Lambda 表达式中有广泛的应用。
3.Lambda表达式(箭头函数)(需要能够大概介绍)
替代匿名内部类
Lambda 表达式是一个匿名函数,java 8 允许把函数作为参数传递进方法中。
过去给方法传动态参数的唯一方法是使用内部类。比如
1.Runnable
接口
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("The runable now is using!");
}
}).start();
//用lambda
new Thread(() -> System.out.println("It's a lambda function!")).start();
2.Comparator
接口
List strings = Arrays.asList(1, 2, 3);
Collections.sort(strings, new Comparator() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;}
});
//Lambda
Collections.sort(strings, (Integer o1, Integer o2) -> o1 - o2);
//分解开
Comparator comperator = (Integer o1, Integer o2) -> o1 - o2;
Collections.sort(strings, comperator);
3.自定义接口
上面的 3 个例子是我们在开发过程中最常见的,从中也能体会到 Lambda 带来的便捷与清爽。它只保留实际用到的代码,把无用代码全部省略。
那它对接口有没有要求呢?我们发现这些匿名内部类只重写了接口的一个方法,当然也只有一个方法必须要重写,其他的方法都已实现(default),(接口默认方法修饰符为public abstract)。
这就是我们上文提到的函数式接口,也就是说只要方法的参数是函数式接口都可以用 Lambda 表达式。
@FunctionalInterface
public interface Comparator{
...
}
@FunctionalInterface
public interface Runnable{
...
}
看看就行,不用背
```java
public static void main(String[] args) {
//完整的Lambda表达式
print((int x) -> {
return 2*x;
},10);
//省略类型声明,但必须所有参数都不写
print((x) -> {
return 2*x;
},10);
//如果只有一个参数,则可以省略参数小括号
print(x -> {
return 2*x;
},10);
//如果只有一条执行语句,且是返回语句,则可以省略大括号和return
Mymath m = x -> 2*x;
print(m,10);
}
public static void print(Mymath m,int x){
int num = m.multiplication(x);
System.out.println(num);
}
static interface Mymath{
int multiplication(int x);
}
```输出结果
20
20
20
20
方法的引用
Java 8 允许使用 ::
关键字来传递方法或者构造函数引用,无论如何,表达式返回的类型必须是 functional-interface。
public class LambdaClassSuper {
LambdaInterface sf(){
return null;
}
}
public class LambdaClass extends LambdaClassSuper {
public static LambdaInterface staticF() {
return null;
}
public LambdaInterface f() {
return null;
}
void show() {
//1.调用静态函数,返回类型必须是functional-interface
LambdaInterface t = LambdaClass::staticF;
//2.实例方法调用
LambdaClass lambdaClass = new LambdaClass();
LambdaInterface lambdaInterface = lambdaClass::f;
//3.超类上的方法调用
LambdaInterface superf = super::sf;
//4. 构造方法调用
LambdaInterface tt = LambdaClassSuper::new;
}
}
3.Optional类
背并看csdn收藏:JDK1.8新特。性值Optional_398701344努力加油!-CSDN博客_jdk1.8 optional
Optional.ofNullable(zoo).map(o -> o.getDog()).map(d -> d.getAge()).filter(v->v==1).orElse(3);
以下只稍微看看就行
在阿里巴巴开发手册关于 Optional 的介绍open in new window中这样写到:
防止 NPE,是程序员的基本修养,注意 NPE 产生的场景:
1) 返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE。
反例:public int f() { return Integer 对象}, 如果为 null,自动解箱抛 NPE。
2) 数据库的查询结果可能为 null。
3) 集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null。
4) 远程调用返回对象时,一律要求进行空指针判断,防止 NPE。
5) 对于 Session 中获取的数据,建议进行 NPE 检查,避免空指针。
6) 级联调用 obj.getA().getB().getC();一连串调用,易产生 NPE。
正例:使用 JDK8 的 Optional 类来防止 NPE 问题。
他建议使用 Optional
解决 NPE(java.lang.NullPointerException
)问题,它就是为 NPE 而生的,其中可以包含空值或非空值。下面我们通过源码逐步揭开 Optional
的红盖头。
假设有一个 Zoo
类,里面有个属性 Dog
,需求要获取 Dog
的 age
。
class Zoo {
private Dog dog;
}
class Dog {
private int age;
}
传统解决NPE方法:
Zoo zoo = getZoo();
if(zoo != null){
Dog dog = zoo.getDog();
if(dog != null){
int age = dog.getAge();
System.out.println(age);
}
}
Optional类解决方法:
Optional.ofNullable(zoo).map(o -> o.getDog()).map(d -> d.getAge()).ifPresent(age ->
System.out.println(age)
);
如何创建一个Options
上例中Optional.ofNullable
是其中一种创建 Optional 的方式。我们先看一下它的含义和其他创建 Optional 的源码方法。
/**
* Common instance for {@code empty()}. 全局EMPTY对象
*/
private static final Optional> EMPTY = new Optional<>();
/**
* Optional维护的值
*/
private final T value;
/**
* 如果value是null就返回EMPTY,否则就返回of(T)
*/
public static Optional ofNullable(T value) {
return value == null ? empty() : of(value);
}
/**
* 返回 EMPTY 对象
*/
public static Optional empty() {
Optional t = (Optional) EMPTY;
return t;
}
/**
* 返回Optional对象
*/
public static Optional of(T value) {
return new Optional<>(value);
}
/**
* 私有构造方法,给value赋值
*/
private Optional(T value) {
this.value = Objects.requireNonNull(value);
}
/**
* 所以如果of(T value) 的value是null,会抛出NullPointerException异常,这样貌似就没处理NPE问题
*/
public static T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
ofNullable
方法和of
方法唯一区别就是当 value 为 null 时,ofNullable
返回的是EMPTY
,of 会抛出 NullPointerException
异常。如果需要把 NullPointerException
暴漏出来就用 of
,否则就用 ofNullable
。
/**
* 如果value为null,返回EMPTY,否则返回Optional封装的参数值
*/
public Optional map(Function super T, ? extends U> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Optional.ofNullable(mapper.apply(value));
}
}
/**
* 如果value为null,返回EMPTY,否则返回Optional封装的参数值,如果参数值返回null会抛 NullPointerException
*/
public Optional flatMap(Function super T, Optional> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Objects.requireNonNull(mapper.apply(value));
}
}
map()
和 flatMap()
有什么区别的?
1.参数不一样,map
的参数上面看到过,flatMap
的参数是这样
class ZooFlat {
private DogFlat dog = new DogFlat();
public DogFlat getDog() {
return dog;
}
}
class DogFlat {
private int age = 1;
public Optional getAge() {
return Optional.ofNullable(age);
}
}
ZooFlat zooFlat = new ZooFlat();
Optional.ofNullable(zooFlat).map(o -> o.getDog()).flatMap(d -> d.getAge()).ifPresent(age ->
System.out.println(age)
);
2.flatMap()
参数返回值如果是 null 会抛 NullPointerException
,而 map()
返回EMPTY
。
/**
* value是否为null
*/
public boolean isPresent() {
return value != null;
}
/**
* 如果value不为null执行consumer.accept
*/
public void ifPresent(Consumer super T> consumer) {
if (value != null)
consumer.accept(value);
}
获取value
/**
* Return the value if present, otherwise invoke {@code other} and return
* the result of that invocation.
* 如果value != null 返回value,否则返回other的执行结果
*/
public T orElseGet(Supplier extends T> other) {
return value != null ? value : other.get();
}
/**
* 如果value != null 返回value,否则返回T
*/
public T orElse(T other) {
return value != null ? value : other;
}
/**
* 如果value != null 返回value,否则抛出参数返回的异常
*/
public T orElseThrow(Supplier extends X> exceptionSupplier) throws X {
if (value != null) {
return value;
} else {
throw exceptionSupplier.get();
}
}
/**
* value为null抛出NoSuchElementException,不为空返回value。
*/
public T get() {
if (value == null) {
throw new NoSuchElementException("No value present");
}
return value;
}
过滤值
/**
* 1. 如果是empty返回empty
* 2. predicate.test(value)==true 返回this,否则返回empty
*/
public Optional filter(Predicate super T> predicate) {
Objects.requireNonNull(predicate);
if (!isPresent())
return this;
else
return predicate.test(value) ? this : empty();
}
4.Stream流
java 新增了 java.util.stream
包,它和之前的流大同小异。之前接触最多的是资源流,比如java.io.FileInputStream
,通过流把文件从一个地方输入到另一个地方,它只是内容搬运工,对文件内容不做任何CRUD。
Stream
依然不存储数据,不同的是它可以检索(Retrieve)和逻辑处理集合数据、包括筛选、排序、统计、计数等。可以想象成是 Sql查询语句。
它的源数据可以是 Collection
、Array
等。由于它的方法参数都是函数式接口类型,所以一般和 Lambda 配合使用。
常用类型
接下来我们看java.util.stream.Stream
常用方法
/**
* 返回一个串行流
*/
default Stream stream()
/**
* 返回一个并行流
*/
default Stream parallelStream()
/**
* 返回T的流
*/
public static Stream of(T t)
/**
* 返回其元素是指定值的顺序流。
*/
public static Stream of(T... values) {
return Arrays.stream(values);
}
/**
* 过滤,返回由与给定predicate匹配的该流的元素组成的流
*/
Stream filter(Predicate super T> predicate);
/**
* 此流的所有元素是否与提供的predicate匹配。
*/
boolean allMatch(Predicate super T> predicate)
/**
* 此流任意元素是否有与提供的predicate匹配。
*/
boolean anyMatch(Predicate super T> predicate);
/**
* 返回一个 Stream的构建器。
*/
public static Builder builder();
/**
* 使用 Collector对此流的元素进行归纳
*/
R collect(Collector super T, A, R> collector);
/**
* 返回此流中的元素数。
*/
long count();
/**
* 返回由该流的不同元素(根据 Object.equals(Object) )组成的流。
*/
Stream distinct();
/**
* 遍历
*/
void forEach(Consumer super T> action);
/**
* 用于获取指定数量的流,截短长度不能超过 maxSize 。
*/
Stream limit(long maxSize);
/**
* 用于映射每个元素到对应的结果
*/
Stream map(Function super T, ? extends R> mapper);
/**
* 根据提供的 Comparator进行排序。
*/
Stream sorted(Comparator super T> comparator);
/**
* 在丢弃流的第一个 n元素后,返回由该流的 n元素组成的流。
*/
Stream skip(long n);
/**
* 返回一个包含此流的元素的数组。
*/
Object[] toArray();
/**
* 使用提供的 generator函数返回一个包含此流的元素的数组,以分配返回的数组,以及分区执行或调整大小可能需要的任何其他数组。
*/
A[] toArray(IntFunction generator);
/**
* 合并流
*/
public static Stream concat(Stream extends T> a, Stream extends T> b)
本文列出 Stream
具有代表性的方法之使用
@Test
public void test() {
List strings = Arrays.asList("abc", "def", "gkh", "abc");
//返回符合条件的stream
Stream stringStream = strings.stream().filter(s -> "abc".equals(s));
//计算流符合条件的流的数量
long count = stringStream.count();
//forEach遍历->打印元素
strings.stream().forEach(System.out::println);
//limit 获取到1个元素的stream
Stream limit = strings.stream().limit(1);
//toArray 比如我们想看这个limitStream里面是什么,比如转换成String[],比如循环
String[] array = limit.toArray(String[]::new);
//map 对每个元素进行操作返回新流
Stream map = strings.stream().map(s -> s + "22");
//sorted 排序并打印
strings.stream().sorted().forEach(System.out::println);
//Collectors collect 把abc放入容器中
List collect = strings.stream().filter(string -> "abc".equals(string)).collect(Collectors.toList());
//把list转为string,各元素用,号隔开
String mergedString = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.joining(","));
//对数组的统计,比如用
List number = Arrays.asList(1, 2, 5, 4);
IntSummaryStatistics statistics = number.stream().mapToInt((x) -> x).summaryStatistics();
System.out.println("列表中最大的数 : "+statistics.getMax());
System.out.println("列表中最小的数 : "+statistics.getMin());
System.out.println("平均数 : "+statistics.getAverage());
System.out.println("所有数之和 : "+statistics.getSum());
//concat 合并流
List strings2 = Arrays.asList("xyz", "jqx");
Stream.concat(strings2.stream(),strings.stream()).count();
//注意 一个Stream只能操作一次,不能断开,否则会报错。
Stream stream = strings.stream();
//第一次使用
stream.limit(2);
//第二次使用
stream.forEach(System.out::println);
//报错 java.lang.IllegalStateException: stream has already been operated upon or closed
//但是可以这样, 连续使用
stream.limit(2).forEach(System.out::println);
}
转换流操作 :例如filter和map方法,将一个Stream转换成另一个Stream,返回值都是Stream。
终结流操作 :例如count和collect方法,将一个Stream汇总为我们需要的结果,返回值都不是Stream。
Stream只在遇到终结操作
的时候才会执行,比如:
List.of(1, 2, 3).stream()
.filter(i -> i > 2)
.peek(System.out::println);
这么一段代码是不会执行的,peek方法可以看作是forEach,这里我用它来打印Stream中的元素。
因为filter方法和peek方法都是转换流方法,所以不会触发执行。
如果我们在后面加入一个count方法就能正常执行:
List.of(1, 2, 3).stream()
.filter(i -> i > 2)
.peek(System.out::println)
.count();
count方法是一个终结操作,用于计算出Stream中有多少个元素,它的返回值是一个long型。
Stream的这种没有终结操作就不会执行的特性被称为延迟执行
。
@Test
public void laziness(){
List strings = Arrays.asList("abc", "def", "gkh", "abc");
Stream stream = strings.stream().filter(new Predicate() {
@Override
public boolean test(Object o) {
System.out.println("Predicate.test 执行");
return true;
}
});
System.out.println("count 执行");
stream.count();
}
/*-------执行结果--------*/
count 执行
Predicate.test 执行
Predicate.test 执行
Predicate.test 执行
Predicate.test 执行
小结
从源码和实例中我们可以总结出一些 stream 的特点
5.Data-Time Api
来改进时间、日期的处理
直接看JavaGuide,(可以不背)
总结
我们梳理总结的 java 8 新特性有
重点面试题
1.OSI的七层模型,及其对应的功能
应用层:负责给应用程序提供统一的接口。
表示层:提供对应用层数据的编码和转换功能,确保应用层发送的数据能被接收方的应用层识别。
会话层:负责建立、管理和终止表示层实体之间的通信会话。
运输层:提供端到端的数据传输
网络层:负责数据的路由、转发、分片等
数据链路层:负责数据的封装成帧和差错检测,以及MAC寻址。
物理层:实际最终信号的传输是通过物理层实现的。通过物理介质传输比特流
2.三次握手(三报文握手)(已面)
TCP建立连接的过程叫做握手,握手需要在客户端和服务器之间三个TCP报文段。
1.第一次握手:客户端给服务器发送一个连接请求报文段,同步序号SYN=1(初始序号seq=x,报文段不能携带数据),客户端进入同步已发送状态。
2.第二次握手:服务端收到连接请求报文段之后,若同意建立连接,应答一个确认报文段,SYN=1,ACK=1(seq=y,ack(确认号)=x+1),服务端进入同步收到状态。
3.第三次握手:客户端收到确认报文后,回应一个确认报文段ACK=1(ack=y+1,seq=x+1),客户端进入ESTABLISHED(已建立连接)状态,双方就已建立连接。
3.为什么要三次握手,两次不行?
(1)确认双方的接受和发送能力正常,也就是客户端要考察服务端的发送和接收能力,服务端也要考察客户端的发送和接收能力。
(2)防止失效的请求再次传到服务端,造成错误;
如果只有两次握手。
失效的连接请求是指:客户端发出的连接请求没有收到服务端的确认,过一段时间后,客户端又向服务端发送连接请求,且建立成功,顺序完成数据传输。
客户端第一次发送的请求因网络延迟到达服务端(并没有丢失),服务端以为是客户端又发起的新请求,于是服务端同意连接,并向客户端发回确认,但此时客户端根本不理会,服务端确一直等待客户端发送数据,导致服务端的资源浪费。
其他问题:
如果最后一个ACK包丢失,服务器端收不到响应,会超时重传SYN+ACK包。
4.三次握手过程中可以携带数据吗?
其实第三次握手的时候,是可以携带数据的。也就是说,第一次、第二次握手不可以携带数据,而第三次握手是可以携带数据的。
对于第三次的话,此时客户端已经处于 established 状态,也就是说,对于客户端来说,他已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据页没啥毛病。
5.四次挥手(已面)
第一次挥手:客户端发送一个连接释放(FIN)报文段,FIN=1 (seq=u) ,客户端进入FIN-WAIT-1(终止等待1)状态。
第二次挥手:服务端收到连接释放(FIN)报文段后,发送确认(ACK)报文段ACK=1(seq=v,ack=u+1),服务端进入CLOSE-WAIT(停止等待状态),此时TCP处于半关闭状态,服务端仍可以向客户端发送数据。
客户端收到确认(ACK)报文段之后进入FIN-WAIT-2(终止等待2)状态。
第三次挥手:如果服务端没有要向客户端发送的数据了,服务端向客户端发送连接释放(FIN+ACK)报文段FIN=1,ACK=1,重复上次发送的确认号(ack=u+1,seq=w),服务端进入LAST-ACK(最后确认)状态。等待客户端的确认。
第四次挥手:客户端收到连接释放(FIN+ACK)报文段之后,发出确认(ACK)报文段,ACK=1(ack=w+1,seq=u+1),客户端进入TIME_WAIT(时间等待)状态,需要过一段时间(2MSL,MSL是最长报文段寿命)确保服务端收到自己的确认报文后,才进入CLOSED状态。
服务端收到确认(ACK)报文后,就关闭连接了,处于CLOSED状态。
6.为什么客户端在TIME_WAIT状态必须等待2MSL时间呢?
为了保证客户端发送的最后一个ACK报文段能够到达服务端,如果ACK报文段丢失,服务端收不到确认,会超时重传FIN+ACK报文段,而客户端就能在2MSL时间内收到重传的FIN+ACK报文段,接着客户端重传一次ACK报文段,重新启动2MSL计时器,最后,客户端和服务端都能进入CLOSED状态。
如果客户端不等待而直接进入CLOSED状态,那么就无法收到服务段重传的FIN+ACK报文,不会重传ACK报文段,服务端无法正常进入CLOSED状态。
7.TCP协议的主要特点(已面)
(1)TCP是面向连接的运输层协议;应用程序在使用TCP协议之前,必须先建立TCP连接,在传输完数据之后,必须释放已经建立的TCP连接。
(2)每一条TCP连接只能有两个端点,TCP只支持一对一通信(一对一)
(3)TCP提供可靠的传输服务。传输的数据无差错、不丢失、不重复、按序到达。
(4)TCP提供全双工通信,运行通信双方在任何时候都可以发送数据,因为两端都设有发送缓存和接收缓存。
(5)面向字节流,TCP把应用程序交下来的数据仅仅看成一连串无结构的字节流。
8.UDP协议特点(已面)
9.TCP和UDP的区别(已面)
UDP | TCP | |
---|---|---|
是否连接 | 无连接 | 面向连接 |
是否可靠 | 不可靠传输,不使用流量控制和拥塞控制 | 可靠传输,使用流量控制和拥塞控制 |
连接对象个数 | 支持一对一,一对多,多对一和多对多交互通信 | 只能是一对一通信 |
传输方式 | 面向报文 | 面向字节流 |
首部开销 | 首部开销小,仅8字节 | 首部最小20字节,最大60字节 |
适用场景 | 适用于实时应用(IP电话、视频会议、直播等) | 适用于要求可靠传输的应用,例如文件传输 |
10.TCP和UDP应用场景(需要背)
TCP应用场景:
效率要求相对低,但是要求可靠传输的场景。例子:文件传输、邮件传输
UDP应用场景:
效率要求相对高,不要求可靠传输的场景 。例子:视频会议、直播
12.Http1.0 1.1 2.0区别 3.0???(已面)
面试官:说说 HTTP1.0/1.1/2.0 的区别? | web前端面试 - 面试官系列
HTTP1.0,1.1,2.0,3.0 - 掘金
图解|为什么 HTTP3.0 使用 UDP 协议?
Http1.0:
短连接,HTTP1.0规定浏览器与服务器值保持短暂的连接,浏览器每次请求都需要与服务器建立一个新的TCP连接,服务器请求处理完成后立刻断开TCP连接。
Http1.1:
引入长连接(持久连接),TCP连接默认不关闭,可以被多个请求复用,客户端和服务器发现对方一段时间没有活动,就可以主动关闭连接。
引入管道机制,在同一个TCP连接里面,客户端可以同时发送多个请求。
有了持久连接和管道,大大的提升了HTTP的效率,但是服务端还是顺序执行的,效率还有提升的空间。
(额外:Host域:HTTP1.1的请求消息和响应消息都支持host域,且请求消息中如果没有host域会报告一个错误,不在以 IP 为请求方标志)
Http2.0:
为了解决Http1.1存在的效率问题,HTTP2.0采用了多路复用。即在一个连接里,客户端和服务端可以同时发送多个请求和响应,而且不用按照顺序一一对应,
能这样做的前提是,Http2.0采用了二进制分帧,Http2.0会将所有信息分割成更小的消息和帧,并对它们采用二进制格式的编码。
Header数据压缩
服务器推送,就是服务器可以对一个客户端请求发送多个响应。换句话说,除了对最初请求的响应外,服务器还可以额外向客户端推送资源,而无需客户端明确地请求。
13.get请求和post请求的区别
(1)post更安全(不会作为url的一部分,不会被缓存、不会保存在浏览器浏览记录中)
(2)post发送的数据更大(get有url长度限制)
(3)post能发送更多的数据类型(get只能发送ASCII字符)
(4)post比get慢
(5)post用于修改和写入数据,get一般用于搜索查询的操作(淘宝,支付宝的搜索查询都是get提交),目的是资源的获取,读取数据
为什么get比post更快?
1.最重要的一条,post在发送数据之前会先将请求头发送给服务器进行确认,然后才真正发送数据
post请求的过程:
(1)浏览器请求tcp连接(第一次握手)
(2)服务器答应进行tcp连接(第二次握手)
(3)浏览器确认,并发送post请求头(第三次握手,这个报文比较小,所以http会在此时进行第一次数据发送)
(4)服务器返回100 Continue响应
(5)浏览器发送数据
(6)服务器返回200 OK响应
get请求的过程:
(1)浏览器请求tcp连接(第一次握手)
(2)服务器答应进行tcp连接(第二次握手)
(3)浏览器确认,并发送get请求头和数据(第三次握手,这个报文比较小,所以http会在此时进行第一次数据发送)
(4)服务器返回200 OK响应
也就是说,目测get的总耗是post的2/3左右,这个口说无凭,网上已经有网友进行过测试。
2.get会将数据缓存起来,而post不会
可以做个简短的测试,使用ajax采用get方式请求静态数据(比如html页面,图片)的时候,如果两次传输的数据相同,第二次以后消耗的时间将会在10ms以内(chrome测试),而post每次消耗的时间都差不多。经测试,chrome和firefox下如果检测到get请求的是静态资源,则会缓存,如果是数据,则不会缓存,但是IE什么都会缓存起来,当然,应该没有人用post去获取静态数据吧,反正我是没见过。
14.HTTP常用状态码以及使用场景?
1xx:表示通知信息,服务器收到请求,需要请求者继续执行操作
2xx:表示请求成功
3xx:表示重定向,需要附加操作以完成请求
4xx:客户端错误
5xx:服务器端错误。
100:继续,客户端应继续其请求
101:切换请求协议
200:请求成功
301:永久重定向,会缓存
302:临时重定向,不会缓存
400:请求错误,如请求语法错误,服务端无法理解
403:服务器禁止访问
404:资源未找到
500:服务器内部错误
503:服务器繁忙
15.HTTP状态码301和302的区别,各自的用途?
http状态码301和302详解及区别_幽雨雨幽的博客-CSDN博客_301 302
状态码301和302的区别 - Wayne-Zhu - 博客园
16.HTTP如何实现长连接?在什么时候会超时?
如何用HTTP实现长连接?_@Block_Smile的博客-CSDN博客_http如何实现长连接
17.TCP如何保证可靠传输(可靠性原理)和拥塞控制原理?
TCP协议保证数据传输可靠性的方式主要有:
拥塞控制
拥塞控制就是为了防止过多的数据注入到网络中,使网络过载。
(1)发生网络拥塞
(2)收到三个重复确认,执行快重传算法
2.拥塞避免:当窗口大小达到阈值时,进入拥塞避免状态,每次确认,窗口大小+1
一旦发生网络发生拥塞导致超时重传,阈值设为当前窗口一半再重新进行慢启动过程
3.快重传:接收方在收到一个失序的报文段后就立即发出重复确认,M1接收确认,M2接收确认,M3没有接收到但是接收到了M4,必须立即发出对M2的重复确认,发送方一连收到3个重复确认立即进行重传,不会误认为网络拥塞,然后执行快恢复算法。
4.快恢复:将阈值和窗口大小都调整为现在窗口大小的一半并开始执行拥塞避免算法。
18. IP地址有哪些分类?
(背一下图4-5中内容就行)
A类地址(1~126):网络号占前8位,以0开头,主机号占后24位。
B类地址(128~191):网络号占前16位,以10开头,主机号占后16位。
C类地址(192~223):网络号占前24位,以110开头,主机号占后8位。
D类地址(224~339):以1110开头,保留位为多播地址。
E类地址(240~255):以1111开头,保留位今后使用。
网络号127保留作为换回测试
19.什么是SQL注入?
所谓的sql注入就是通过某种方式将恶意的sql代码添加到输入参数中,然后传递到sql服务器使其解析并执行的一种攻击手法
实例:
用户名: ‘or 1 = 1 #
密 码:
后台原来的语句:
' " + username + " '
String sql = “select * from user_table where username=’ “+userName+” ’ and password=’ “+password+” ‘”;
SQL注入后:
SELECT * FROM user_table WHERE username=’’or 1 = 1 # ' and password=’’
--注释
这条语句永远都能都能正确执行
预防方式:
使用预编译手段,进行参数绑定(差不多就行,不用背那么细)
使用预编译手段,绑定参数是最好的防SQL注入的方法。攻击者的恶意SQL会被当做SQL的参数而不是SQL命令被执行。
JDBC中,PrepareStatement,预编译进行参数绑定,?当成占位符。
在mybatis中,当使用#时,变量是占位符,就是一般我们使用javajdbc的PrepareStatement时的占位符,可以防止sql注入;当使用$
时,变量就是直接追加在sql中,一般会有sql注入问题。
21.网络五层模型
应用层:通过应用进程间的交互来完成特定网络应用。
运输层:提供端到端的数据传输
网络层:赋值数据的路由、转发、分片
数据链路层:负责数据的封装成帧和差错检测,以及MAC寻址。
物理层:实际最终信号的传输是通过物理层实现的。通过物理介质传输比特流
22.Https和Http的区别(已面)
1、https协议需要申请CA证书,一般免费证书较少,因而需要一定费用。
2、http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl/tls协议加密传输协议。
3、http和https使用的是完全不同的连接方式,用的默认端口号也不一样,前者是80,后者是443。
4、http的连接很简单,是无状态的;HTTPS协议是由SSL/TLS+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
(无状态的意思是其数据包的发送、传输和接收都是相互独立的。无连接的意思是指通信双方都不长久的维持对方的任何信息。)
23.对称加密和非对称加密的区别?(已面)
24.每一层的网络协议
应用层:
超文本传输协议HTTP
域名解析协议:DNS
远程终端协议TELNET
文件传输协议FTP
动态主机配置协议DHCP
运输层
传输控制协议TCP
用户数据报文协议UDP
网络层:
网际协议IP
地址解析协议ARP
网际控制报文协议ICMP
外部网关协议BGP
数据链路层:
自动请求重传协议ARQ
CSMA/CD协议
点对点协议PPP
25.ARP协议的工作原理?(差不多得了)
ARP协议原理 - 知乎
(MAC地址=物理地址=硬件地址)
1.首先,每个主机都会在自己的ARP缓冲区中建立一个ARP列表,以表示IP地址和MAC地址之间的对应关系。
2.当源主机要发送数据时,首先检查ARP列表中是否有对应IP地址的目的主机的MAC地址,如果有,则直接发送数据,如果没有,就向本网段的所有主机发送ARP数据包,该数据包包括的内容有:源主机 IP地址,源主机MAC地址,目的主机的IP 地址。
3.当本网络的所有主机收到该ARP数据包时,首先检查数据包中的IP地址是否是自己的IP地址,如果不是,则忽略该数据包,如果是,则首先从数据包中取出源主机的IP和MAC地址写入到ARP列表中,如果已经存在,则覆盖,然后将自己的MAC地址写入ARP响应包中,告诉源主机自己是它想要找的MAC地址。
4.源主机收到ARP响应包后。将目的主机的IP和MAC地址写入ARP列表,并利用此信息发送数据。如果源主机一直没有收到ARP响应数据包,表示ARP查询失败。
广播发送ARP请求,单播发送ARP响应。
(不同局域网的主机的MAC地址查找中间通过路由器实现)
27.谈谈你对ARQ协议的理解?
计算机网络常见知识点&面试题(补充) | JavaGuide
TCP可靠传输:ARQ协议(停止等待、超时重传、滑动窗口、回退N帧、选择重传)_啊a阿花的博客-CSDN博客_arq协议
自动重传请求ARQ 包括停止等待 ARQ 协议和连续 ARQ 协议。
停止等待协议
它的基本原理就是每发完一个分组就停止发送,等待对方确认(回复 ACK)。如果过了一段时间(超时时间后),还是没有收到 ACK 确认,说明没有发送成功,需要重新发送,直到收到确认后再发下一个分组。
在停止等待协议中,若接收方收到重复分组,就丢弃该分组,但同时还要发送确认。
连续ARQ协议
连续 ARQ 协议可提高信道利用率。发送方维持一个发送窗口,凡位于发送窗口内的分组可以连续发送出去,而不需要等待对方确认。接收方一般采用累计确认,对按序到达的最后一个分组发送确认,表明到这个分组为止的所有分组都已经正确收到了。
28.谈谈你对停止等待协议的理解?(是这个意思就行)
26-tcp可靠传输——停止等待协议_songly_的博客-CSDN博客_tcp 停止等待协议
1.在发送无差错的情况下,发送发每次向接收方发送一次分组,接收方就会向发送方发送一个确认分组,当发送方收到确认后即会发送下一个分组。
2.超时重传:发送方会内置一个超时计时器,时间略长于一个分组发送至接收方,接收方又将确认发送过来的时间,若发送方向接收方发送的数据丢失,计时器到时间则会使发送方重新发送分组直至收到确认。
3.确认丢失:若接收方发送的确认丢失了,发送方会超时重传同一个分组,接收方收到此重复的分组后将此丢弃并重新发送一次确认。
4.确认迟到:若接收方发送的确认迟到,而触发了发送方的超时重传后,接收方丢弃重复的分组再次发送确认,使得发送方收到了第二次的确认,但是此时第一次的确认又到了,发送方则会丢弃重复的确认。
29.TCP滑动窗口?(差不多得了,大概是这个意思就行)
TCP滑动窗口 - alifpga - 博客园
TCP会话的双方都各自维护一个发送窗口和一个接收窗口。
在等到确认应答返回之前,必须在缓冲区中保留已发送的 数据。
滑动窗口采用的是累计确认,收到了某个确认应答则意味着前面的数据全部收到了。
发送窗口大小则要求取决于的接收窗口大小。
TCP利用滑动窗口解决流量控制的的问题。
窗口的概念
发送方的发送缓存内的数据都可以被分为4类:
1. 已发送,已收到ACK
2. 已发送,未收到ACK
3. 未发送,但允许发送
4. 未发送,但不允许发送
其中类型2和3都属于发送窗口。
接收方的缓存数据分为3类:
1. 已接收
2. 未接收但准备接收
3. 未接收而且不准备接收
其中类型2属于接收窗口。
滑动机制
(收到32,33,但是31未收到,发送确认31,表示期望收到的序号)
发送窗口只有收到发送窗口内字节的ACK确认,才会移动发送窗口的左边界。
接收窗口只对按序收到的数据中的最高序号给出确认,然后移动窗口的左边界。
30.谈下你对流量控制的理解?(差不多得了,大概意思到位就行)
通俗易懂讲解TCP流量控制机制,了解一下 - 帅地 - 博客园
发送方的发送窗口不能超过接收方给出的接收窗口大小。
31.什么是粘包?
TCP粘包就是指发送方发送的若干包数据到达接收方时粘成了一包,从接收缓冲区来看,后一包数据的头紧接着前一包数据的尾,出现粘包的原因是多方面的,可能是来自发送方,也可能是来自接收方。
32.TCP粘包是怎么产生的?
接收方:
接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。
若下一包数据到达时上一包数据尚未被用户进程取走,则下一包数据放到接收缓冲区时就接到上一包数据之后,
而用户进程从接收缓冲区取数据,这样就一次取到了多包数据。
发送方:
TCP为提高传输效率,发送方可能会将多个数据包合在一起发送,这样接收方就收到了粘包数据。
(拆包)TCP协议规定有MSS(最大报文段长度),如果数据包过长就会被分开传输。这样接收方就收到了粘包数据。
33.怎么解决粘包和拆包?
1.发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。
2.发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就把每个数据包拆分开来。
3.可以在数据包之间设置边界,如添加特殊符号
34.forword和redirect的区别?
是servlet种的两种主要的跳转方式。forward又叫请求转发,redirect叫做重定向。
区别:
服务器浏览器、地址栏,数据共享,效率,次数
两者的区别总结:
(差不多得了)
1. 从地址栏显示来说:
1)forword在服务器内部进行,服务器直接访问目标地址的 url网址,把里面的东西读取出来,但是浏览器并不知道,因此用forward的话,客户端浏览器的网址是不会发生变化的。
2)redirect是服务器 根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址,所以地址栏显示的是新的地址。
2。 从数据共享来说:
1)由于在整个定向的过程中用的是同一个request,因此forward会将request的信息带到被重定向的jsp或者servlet中使用。即可以共享request作用域的数据
2)redirect不能共享
3. 从效率来说:
1)forword效率高,而redirect效率低
4. 从请求的次数来说:
forword只有一次请求;而redirect有两次请求,
35.Http方法有哪些?(已面)
36.在浏览器输入url到显示到主页的过程?(已面)
1.根据域名到DNS中找到IP
2.根据IP建立TCP连接(三次握手)
3.连接建立成功发起http请求
4.服务器响应http请求
5.浏览器解析HTML代码并请求html中的静态资源(js,css)
6.关闭TCP连接(四次挥手)
7.浏览器渲染页面
解析DNS域名
浏览器查找DNS缓存->操作系统查找DNS缓存->操作系统查找本地host文件->操作系统向本地域名服务器发起请求,查找本地DNS缓存->根域名服务器开始递归查询
1.浏览器查找自己的DNS缓存,如果有直接返回,如果没有进行步骤二
2.操作系统查找自己的DNS缓存,如果有直接返回给浏览器,如果没有进行步骤三
3.操作系统查找自己的本地host文件,如果有返回给浏览器,如果没有则进行步骤四
4.操作系统向本地域名服务器发起请求,查找本地DNS缓存,如果有,返回给操作系统,然后操作系统返回给浏览器,如果没有进行步骤五
5.如果本地域名服务器并未缓存该网址映射关系,那么它从根域名服务器开始递归查询。
37.DNS的域名解析过程?
通过域名访问网站时:
1.浏览器查找自己的DNS缓存(如果有直接返回,如果没有进行步骤二)
2.操作系统查找自己的DNS缓存(如果有直接返回给浏览器,如果没有进行步骤三)
3.操作系统查找自己的本地host文件,(如果有返回给浏览器,如果没有则进行步骤四)
4.操作系统向本地域名服务器发起请求,查找本地DNS缓存(如果有,返回给操作系统,然后操作系统返回给浏览器,如果没有进行步骤五)
5.如果本地域名服务器并未缓存该网址映射关系,那么它从根域名服务器开始递归查询。
(域名系统(英文:Domain Name System,缩写:DNS)是互联网的一项服务。它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。)
39.Https的工作过程?(已面)
在下面
HTTPS工作原理_morris131的博客-CSDN博客
https工作原理-实现过程(详细版)_M-artin.online的博客-CSDN博客_https工作流程
40.Https优缺点?
41.Cookie和session的区别
Cookie和Session的区别 - 简书
背1-6
1)、cookie存储在客户端,SESSION存储在服务器;
2)、cookie机制保持连接,通信时压力在客户端;SESSION机制保持连接,通信时压力在服务器;
3)、SESSION机制更安全,因为cookie存放在客户端,容易被窃取。但是session机制依赖于sessionID,而sessionID保存在cookie中,一旦客户端禁用了cookie,session也将失效;
4)、cookie是以文本的形式存储在客户端,存储量有限(<=4KB);
session可以无限量地往里面添加内容。
5)、Cookie支持跨域名访问,Session不支持跨域名访问;
6)、Cookie可以设置为长期有效,而Session会随会话窗口关闭而失效。
session用于存放登录信息等重要信息,cookie存放其他信息
42.tcp,http的keepalive(差不多得了)
KeepAlive详解 - 简书
43.长连接短连接(差不多得了)
长连接和短链接的区别 - 简书
44.DNS为什么用UDP?
DNS使用TCP还是UDP?_weixin_33989780的博客-CSDN博客
45.http为什么用tcp
tcp优点、udp缺点
46.url和uri之间的区别?(差不多得了,莫背)
uri和url的区别与联系(一看就理解)_sinat_38719275的博客-CSDN博客_uri和url的区别
47.syn洪水攻击
简单说下 SYN FLOOD 是什么-帅地玩编程
48.DDOS攻击
什么是 DDoS 攻击? - 知乎
49.ipv4 ipv6(知道啥是ipv4/ipv6地址、区别)
IPv4和IPv6有什么区别? - 知乎
50.数字签名、数字证书(知道是啥就行)
什么是数字签名和证书? - 简书
51.DNS劫持(差不多得了,没找到好资料,看不懂算了)
简单说下怎么实现 DNS 劫持-帅地玩编程
一个社招的金三银四——Java虚拟机面试题总结_技术交流_牛客网
1.JVM主要组成部分,及其作用?
Java 内存区域详解 | JavaGuide
说一下 Jvm 的主要组成部分?及其作用?-帅地玩编程
JVM运行时数据区域
看书
谈谈对运行时数据区的理解?-帅地玩编程
JVM运行时数据区(详解+面试)_计本张天扬的博客-CSDN博客_jvm运行时数据区
2.JVM堆和栈的区别?
堆和栈的区别是什么?-帅地玩编程
3.为什么要把堆和栈区分出来呢?栈不是也能存储数据吗?
在JVM中,为什么要把堆与栈分离?栈不是也可以存储数据吗?_神奇女侠666的博客-CSDN博客_jvm为什么要分堆和栈
4.Java参数传递时是传值呢?还是传引用?
值
Java中参数传递(值传递还是引用传递)_緈諨の約錠的博客-CSDN博客
5.Java对象的大小如何计算?
Java 对象的大小是怎么计算的?-帅地玩编程
6.如何判断垃圾可以回收?
JVM 垃圾回收详解 | JavaGuide
判断垃圾可以回收的方法有哪些?-帅地玩编程
7.被标记为垃圾的对象一定会回收吗?
不一定
任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行,因此下面第二段代码的自救行动失败了。
被标记为垃圾的对象一定会被回收吗?-帅地玩编程
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isAlive() {
System.out.println("yes,i am still alive:)");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize mehtod executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws Throwable {
SAVE_HOOK = new FinalizeEscapeGC();
// 对象第一次成功拯救自己
SAVE_HOOK = null;
System.gc();
// 因为finalize方法优先级很低,所以暂停0.5秒以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no,i am dead:(");
}
// 下面这段代码与上面的完全相同,但是这次自救却失败了
SAVE_HOOK = null;
System.gc();
// 因为finalize方法优先级很低,所以暂停0.5秒以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no,i am dead:(");
}
}
}
输出
finalize mehtod executed!
yes,i am still alive:)
no,i am dead:(
8.内存泄漏(重点)
(1)什么是内存泄漏、内存溢出?
不再会被使用的对象的内存不能被回收,就是内存泄露。
内存溢出:程序向系统申请的内存空间超出了系统能给的。比如内存只能分配一个int类型,我却要塞给他一个long类型,系统就出现内存溢出。
(2)内存泄漏根本原因:
如果长生命周期的对象持有短生命周期的引用,就很可能会出现内存泄露。尽管短生命周期对象已经不再需要,但是因为长生命周期持有它的引用而导致不能被回收,这就是 Java 中内存泄漏的发生场景。
public class Simple {
Object object;
void method () {
object = new Object();
}
}
就像上面的代码,严格意义上就是一种内存泄漏,因为object不再被使用了,但它不会被立即回收,而是得等到Simple对象被释放的时候。
可以这样写
public class Simple {
Object object;
void method () {
Object object = new Object();
//使用Object
object = null;
}
}
把Object定义为局部变量,并在最后赋值为null
(3)JVM内存泄漏常见的几种情况
JVM内存泄漏的几种常见情况分析_冲就完事了的博客-CSDN博客_jvm内存泄漏
(4)如何避免内存泄漏
尽量不要使用 static 成员变量,减少生命周期;
及时关闭(close)资源;
不用的对象,可以及时手动设置为 null。
9.垃圾回收算法(重点)
浅析JVM中常见的垃圾收集算法_果冻.Lee的技术博客_51CTO博客
10.为什么要采用分代垃圾收集算法?
为什么要采用分代收集算法?-帅地玩编程
11.老年代和新生代,永久代,什么时候触发垃圾回收算法?
书P129
java堆可以细分为新生代和老年代
新生代:几乎是所有对象出生的地方,较大对象除外等。
老年代:存活时间较长的对象。
MinorGC:新生代进行一次回收。
FullGC(Major):对整个堆进行整理,包括老年代和新生代。
JVM 垃圾回收详解 | JavaGuide
java堆内存详解 - 盛开的太阳 - 博客园
永久代:
指内存的永久保存区域,主要存放Class 和Meta (元数据)的信息,Class在被加载的时候被放入永久区域,它和和存放实例的区域不同,GC不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的Class 的增多而胀满,最终抛出OOM异常。
永久代不在堆中,永久代只是方法区的实现而已。
JAVA8与元数据 :
在Java8 中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。
元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
13.内存碎片
什么是内存碎片?如何解决?-帅地玩编程
14.常用的垃圾收集器?(再说,还没看懂)
常用的垃圾收集器有哪些?-帅地玩编程
15.CMS垃圾回收器
JVM 垃圾回收详解 | JavaGuide
谈谈你对 CMS 垃圾收集器的理解?-帅地玩编程
浮动垃圾,指的是并发清理阶段产生的垃圾。因为并发清理阶段用户程序也在运行,产生的垃圾在标记过程之后,所以本次清理过程不会被清理。
16.G1垃圾收集器(浅显的背一下书上内容,差不多得了)
看书
CMS、G1 垃圾回收器 - 掘金
17.谈谈你对内存分配的理解?大对象怎么分配?空间分配担保?
书P129
空间分配担保极端情况:内存回收后新生代所有对象都存活,需要将老年代进行分配担保,将Survivor无法容纳的对象直接送人老年代。
谈谈你对内存分配的理解?大对象怎么分配?空间分配担保?-帅地玩编程
18.JVM监控工具、故障处理工具?(再说)
jps:虚拟机进程状况工具
jstat:虚拟机统计信息监视工具
jinfo:Java配置信息工具
jmap:Java内存映像工具
jhat:虚拟机堆转储快照分析工具
jstack:Java堆栈跟踪工具
19.如何利用监控工具调优?
如何利用监控工具调优?-帅地玩编程
20.JVM参数,调优参数?(一定要自己用一下)
JVM 的一些参数?-帅地玩编程
21.JVM报错信息?
JVM常见报错信息_魔舞清华的博客-CSDN博客_jvm报错信息
22.类加载
类加载过程
类加载机制
各阶段作用
有哪些类加载器,分别有什么作用
双亲委派模型
谈谈你对双亲委派模型的理解?工作过程?为什么要使用-帅地玩编程
类加载方式
自定义类加载器
(基本上全部)
jvm系列(一):java类的加载机制 - 纯洁的微笑 - 博客园
23.怎么打破双亲委派模型?有哪些实际场景是需要打破双亲委派模型的?
怎么打破双亲委派模型?-帅地玩编程
双亲委派模型和破坏场景_u014753478的博客-CSDN博客_打破双亲委派机制的场景
24.类文件结构?(有空再看)
类文件结构详解 | JavaGuide
jvm调优经历,以后有空一定要自己用一下子jvm调优!
mysql题目做一做
MySQL夺命15问,你能坚持到第几问? 13、14、15不看
一个社招的金三银四——MySQL面试题总结_技术交流_牛客网
1.sql语句执行流程(差不多得了,是大概的意思就行)
背这个
Mysql SQL语句执行过程详解 - 简书
2.数据库三范式
数据库三大范式_凉_ting的博客-CSDN博客_数据库三范式
数据库设计三大范式 - Ruthless - 博客园
3.char和varchar区别
char 和 varchar 的区别?-帅地玩编程
4.索引理解
谈谈你对索引的理解?-帅地玩编程
5.索引底层用的是什么数据结构
索引的底层使用的是什么数据结构?-帅地玩编程
6.B树和B+树,是什么、区别
B树就是B-树 , 翻译问题!
一文彻底搞懂MySQL基础:B树和B+树的区别_公众号:码农富哥的博客-CSDN博客_mysqlbtree 与b+tree区别
7.Mysql为什么不用b树,而用b+树?
下面
MySQL为什么要使用B+树索引 - 雪山飞猪 - 博客园
8.聚簇索引和非聚簇索引
主键索引就是聚集索引?MySQL 索引类型大梳理_慕课手记
聚簇索引和非聚簇索引 - 扯不断得红尘 - 博客园
9.哈希索引
MySQL: Hash索引 - 简书
10.覆盖索引(差不多得了)
MySQL覆盖索引 - 简书
11.索引分类(差不多得了)
主键索引就是聚集索引?MySQL 索引类型大梳理_慕课手记
12. 组合索引(联合索引)(差不多得了,能扯一扯就行)
面试官:谈谈你对mysql联合索引的认识? - 知乎
13.联合索引的最左匹配原则
MYSQL | 最左匹配原则 - 一个人的孤独自白 - 博客园
14.索引什么时候不会使用?什么时候不推荐建索引?
索引失效的7种情况 - liehen2046 - 博客园
15.SQL查询优化(差不多得了,会一些得了)
SQL优化最干货总结 - MySQL(2020最新版) - 知乎
16.InnoDB和MyISAM区别?(差不多得了,背几点进行)
MyISAM与InnoDB 的区别(9个不同点)_Chackca的博客-CSDN博客_innodb和myisam的区别
17.水平切分 垂直切分(差不多得了)
mysql表的垂直拆分和水平拆分 - 知乎
数据库垂直拆分 水平拆分 - 有梦就能实现 - 博客园
18.主从复制(好多,不会就算了)
看完这篇还不懂 MySQL 主从复制,可以回家躺平了~ - 掘金
工作过程及之前内容
19.分库分表(差不多得了)
MySQL分库分表_小小渔夫的博客-CSDN博客
20.主从复制涉及到哪几个线程
mysql主从复制原理 - 知乎
21.主从复制的延迟原因以及解决办法?(差不多得了)
MySQL主从复制延迟原因及处理思路 - 云+社区 - 腾讯云
mysql的主从复制延迟问题--看这一篇就够了 - ityml - 博客园
一篇文带你彻底解决mysql的主从复制延迟问题 - 掘金
22.事务四大特性 四大隔离级别 脏读、不可重复读、幻读 MySQL默认隔离级别?
读未提交 读已提交 可重复读 串行化
MySQL默认隔离基本:可重复读
幻读指当用户操作某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户读取该范围的数据行时,会发现有新的“幻影” 行。
MySQL数据库事务的四大特性以及事务的隔离级别_哎呦、不错哦的博客-CSDN博客_数据库事务四大特性
23.MVCC
3.4的第三张图有毛病
看一遍就理解:MVCC原理详解 - 掘金
24.MySQL中的锁
MyISAM 和 InnoDB 存储引擎使用的锁:
表级锁和行级锁对比:
InnoDB 存储引擎的锁的算法有三种:
25.MySQL如何解决幻读?
1.串行化
2.
26.快照读和当前读?
数据库面试题:mysql当前读和快照读(MVCC)_我是方小磊的博客-CSDN博客_数据库当前读
当前读就是读的是当前时刻已提交的数据,快照读就是读的是快照生成时候的数据。
在读未提交隔离级别下,快照是什么时候生成的?
没有快照,因为不需要,怎么读都读到最新的。不管是否提交
在读已提交隔离级别下,快照是什么时候生成的?
SQL语句开始执行的时候。
在可重复读隔离级别下,快照是什么时候生成的?
事务开始的时候
select 语句加锁是当前读
# 共享锁
select a from t where id = 1 lock in share mode;
#排他锁
select a from t where id = 1 for update;
27.MySQL引擎
28.关系型数据库和非关系型数据库(差不多得了,随便看看)
非关系型数据库(NOSQL)和关系型数据库(SQL)区别详解 - 云+社区 - 腾讯云
30.Mysql占用cpu过高怎么办?(差不多得了)
mysql占用CPU过高解决 - 知乎
【64期】MySQL 服务占用cpu 100%,如何排查问题? (MySQL面试第七弹) - 知乎
31.慢查询如何排查?
select
1.1 1.2.1 慢查询分析只需要知道需要用工具 工具叫什么名字就行
慢查询 - SoyWang - 博客园
32.explain命令
获取 select 语句的执行计划
不用背太细,只需要记得explain的参数有几个,分别是做什么的 explain语句是什么 怎么用就行
mysql explain详解 - 云+社区 - 腾讯云
32.mysql问题排查手段?
MySQL 问题排查都有哪些手段?-帅地玩编程
1.Redis应用场景
社交网络
点赞、踩、关注/被关注、共同好友等是社交网站的基本功能,社交网站的访问量通常来说比较大,而且传统的关系数据库类型不适合存储这种类型的数据,Redis提供的哈希、集合等数据结构能很方便的的实现这些功能。
消息队列
消息队列是大型网站必用中间件,如ActiveMQ、RabbitMQ、Kafka等流行的消息队列中间件,主要用于业务解耦、流量削峰及异步处理实时性低的业务。Redis提供了发布/订阅及阻塞队列功能,能实现一个简单的消息队列系统。另外,这个不能和专业的消息中间件相比。
2.redis基本数据类型(redis通过key获取值。。。) 最少记前五种
基本数据类型:
1、String:最常用的一种数据类型,String类型的值可以是字符串、数字 或者二进制等,但值最大不能超过512MB。
2、Hash:Hash 是一个键值对集合,存储键值对。
3、Set:无序去重的String集合。Set 提供了交集、并集等方法,对于实现共同好友、共同关注等功能特别方便。
4、List:有序可重复的String集合,底层是依赖双向链表实现的。
5、SortedSet(Zset):sorted set 存储有序的元素。每个元素都有个 score,按照 score 从小到大排序。score 相同时,按照 key 的ASCII码排序。
应用场景
排行榜
特殊的数据类型:(不记就算了)
1、Bitmap:位图,可以认为是一个以位为单位数组,数组中的每个单元只能存0或者1,数组的下标在 Bitmap 中叫做偏移量。Bitmap的长度与集合中元素个数无关,而是与基数的上限有关。
2、Hyperloglog。HyperLogLog 是用来做基数统计的算法,其优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。典型的使用场景是统计独立访客。
3、Geospatial :主要用于存储地理位置信息,并对存储的信息进行操作,适用场景如定位、附近的人等。
3.Redis为什么这么快(重点)
Redis 为什么这么快?-帅地玩编程
4.缓存穿透、缓存击穿、缓存雪崩(重点)
什么是缓存雪崩、缓存击穿、缓存穿透? - 知乎
5.如何保证缓存与数据库的一致性?(重点)
面试官:Redis 缓存一致性问题怎么解决,这样回答简直完美。_墨眉无锋墨家代码的博客-CSDN博客_redis一致性问题怎么解决
redis缓存一致性 - huonan - 博客园
6.Redis的持久化方式(能介绍一下两种方式就行了)重点
Redis的两种持久化方式 - 知乎
7.Redis淘汰策略 重点
当Redis内存超出物理内存限制时,内存数据会开始和磁盘产生频繁的交换,使得性能急剧下降。为了限制内存的使用,Redis提供参数maxmemory
来限制最大内存,当内存超出后,会有以下策略(maxmemory-policy
)来淘汰key以腾出空间:
volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。LRU算法
volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰。
volatile-random:从已设置过期时间的数据集中任意选择数据淘汰。
volatile-lfu:从已设置过期时间的数据集挑选使用频率最低的数据淘汰。LFU算法
allkeys-lru:从数据集中挑选最近最少使用的数据淘汰 。LRU算法
allkeys-lfu:从数据集中挑选使用频率最低的数据淘汰。LFU算法
allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰。
no-enviction(驱逐):禁止驱逐数据,这也是默认策略。意思是当内存不足以容纳新入数据时,新写入操作就会报错,请求可以继续进行,线上任务也不能持续进行,采用no-enviction策略可以保证数据不被丢失。
8.Redis过期键的删除策略?(重点)
定时删除
创建一个定时器,当key设置有过期时间,且过期时间到达时,由定时器任务立即执行对键的删除操作。
当前时间和expires中对过期时间一致时,定时器触发删除。
优点:节省内存,到时间就删除,快速释放不必要的内存占用。
缺点:CPU压力较大,无论CPU此时负载情况如何,均占用CPU来执行删除,会影响Redis服务器的响应时间和指令吞吐量。
时间换空间
惰性删除
数据到达过期时间,不做处理,等下次访问该数据等时候执行删除。
在获取数据时,内部会调用expirelfNeeded()方法,来确定数据是否到期。
未到期,返回数据。
到期,删除,返回不存在。
优点:节约cpu性能,到了必须删除的时候才执行删除。
缺点:内存压力较大,会出现长期占用内存的数据。
空间换时间
定期删除
每隔一定的时间,会扫描一定数量的数据库数据,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
Redis中同时使用了惰性过期和定期过期两种过期策略。
9.Redis产生Hash冲突怎么办?(差不多得了)
redis面试:hash冲突怎么办_OceanStar的学习笔记的博客-CSDN博客_redis解决hash冲突
10.在生成RDB期间,Redis可以处理写请求吗?
在生成 RDB 期间,Redis 可以同时处理写请求么?-帅地玩编程
11.Redis(重启)如何兼顾性能又保证数据尽可能少丢失?
如何实现数据尽可能少丢失又能兼顾性能呢?-帅地玩编程
12.Redis线程模型?(不想看就算了)
Redis线程模型-帅地玩编程
Redis线程模型 - 简书
13.Redis分区 (是什么、好处、实现方式、缺点)
Redis 分区
14.Redis并发竞争key问题?
Redis 并发竞争key问题如何解决? - 云+社区 - 腾讯云
分布式锁、时间戳、消息队列这三种就行
15.Redis和Memcached区别?优势(背几个差不多得了)
Redis相比Memcached有哪些优势?-帅地玩编程
16.如何选择合适的持久化方式?
如何选择合适的持久化方式-帅地玩编程
17.Redis事务
没有回滚、不保证原子性、无隔离级别、一般用Lua脚本
跟我一起学Redis之Redis事务简单了解一下 - 云+社区 - 腾讯云
18.Redis缓存预热 重点
什么是缓存预热?-帅地玩编程
19.Redis缓存降级 重点
能说一说是什么就行了
redis面试:什么是缓存降级_OceanStar的学习笔记的博客-CSDN博客_redis缓存降级
17.Redis多线程?
Redis在6.0版本引入多线程机制
18.Redis为何引入多线程?
Redis 6.0为何引入多线程?-帅地玩编程
主要就背:
随着互联网的飞速发展,互联网业务系统所要处理的线上流量越来越大,Redis的单线程模式会导致系统消耗很多 CPU 时间在网络 I/O 上从而降低吞吐量,要提升 Redis的性能有两个方向:
Redis支持多线程主要就是两个原因:
Redis IO多线程模型只用来处理网络读写请求,对于 Redis 的读写命令,依然是单线程处理。
19.Redis多线程实现机制?主线程和IO多线程如何协作?(不想看就算了)
主要流程:
socket
放入全局等待读处理队列;socket
分配给 IO 线程;socket
完成;socket
完毕;该设计有如下特点:
20.主从模式、哨兵模式、集群模式 重点
(主从模式!=主从复制,哨兵、集群模式里面ye'y)
需要知道是什么、以及机制(原理)、优缺点以后再说
redis系列之——高可用(主从、哨兵、集群) - 知乎
21.Redis主从复制数据产生延迟怎么办?(可能看不懂,没找到好答案)
redis主从复制数据延迟解决方案 - 掘金
22.Redis主从复制数据丢失?
(数据复制、同步是什么,弄不懂)
Redis主从复制丢失数据的情况分析 - 简书
23.分布式锁
什么是分布式锁?为什么用分布式锁?-帅地玩编程(重点)
分布式锁特性:
不背 高可用性
记 可重入性:同一个线程可以多次获取该锁(就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁)
具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。
常见的分布式锁有哪些解决方案?-帅地玩编程(非重点,爱看不看)
Redis实现分布式锁-帅地玩编程(重点)
24.布隆过滤器
误判率别背:有毛病
布隆过滤器,这一篇给你讲的明明白白-阿里云开发者社区
(差不多得了,别背)误判:映射函数是哈希函数,可能产生哈希冲突(不同的数据映射位相同,都为1),当存入数据较多时,就可能出现,未存入数据经过多个映射之后,全部所对应位数为1,但是实际上不存在的情况。
25.IO多路复用(没看懂,再说吧,可能有毛病)(常问)
IO多路复用的三种机制Select,Poll,Epoll - 简书
26.Redis 宕机怎么办
持久化、主从、哨兵、集群模式 来回答
27.Redis如何存对象(差不多得了,知道怎么存就行了)
Redis 存储对象信息是用 Hash 还是 String - 又拍云 - 博客园
Redis存储对象的三种方式_kainx的博客-CSDN博客_redis存储对象
28.了解二叉平衡搜索树
(能讲个大概就行)
什么是平衡二叉树(AVL) - 知乎
1.介绍
动态Sql语句的持久层框架。Mybatis可以将Sql语句配置在XML文件中,避免将Sql语句硬编码在Java类中。与JDBC相比:
1)Mybatis通过参数映射方式,可以将参数灵活的配置在SQL语句中的配置文件中,避免在Java类中配置参数
2)Mybatis可以通过Xml配置文件对数据库连接进行管理
2.传参的几种方式
#{}(使用预处理【预编译】的处理,不存在sql注入的安全问题)或者${存在sql注入的安全问题}
1.常用命令
推送本地库到远程库
git push
拉取远程库到本地库
git pull
克隆远程库到本地
git clone
1.核心注解
@SpringBootApplication
通常用在启动类上,申明让spring boot自动给程序进行必要的配置,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解:
1. @SpringBootConfiguration
组合了 @Configuration 注解,实现配置文件的功能。
2. @EnableAutoConfiguration
打开自动配置的功能,也可以关闭某个自动配置的选项。
如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class });
3. @ComponentScan
Spring组件扫描功能,让spring Boot扫描到Configuration类并把它加入到程序上下文。
2.介绍
1.同步 异步 阻塞 非阻塞(差不多得了)
同步、异步、阻塞、非阻塞的概念-帅地玩编程
IO-同步、异步、阻塞、非阻塞 - 云+社区 - 腾讯云(可能有毛病,建议非要求别讲)
2.进程的状态
三种基本状态:就绪、运行、终止
进程的五种基本状态 - 操作系统_定雨的博客-CSDN博客_进程的状态
3.进程调度算法
看书上
先来先服务 短作业优先 优先级调度 高响应比优先调度算法 时间片轮转调度算法 多级反馈队列
4.进程通信方式?(记4、5个吧,差不多得了)
1 2 3 5 6 7
1. 管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
2. 命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
3. 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
4. 共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
5. 信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
6. 套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及机器的进程通信。
7. 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
5.死锁
(1)是什么?(差不多是这个意思就行了)
死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。 如下图所示:如果此时有一个线程 A,已经持有了锁 A,但是试图获取锁 B,线程 B 持有锁 B,而试图获取锁 A,这种情况下就会产生死锁。
死锁两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
(2)死锁产生的必备条件、如何避免、预防、解除
学过操作系统的朋友都知道产生死锁必须具备以下四个条件:
如何预防死锁? 破坏死锁的产生的必要条件即可:
如何避免死锁?
避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。
安全状态 指的是系统能够按照某种线程推进顺序(P1、P2、P3.....Pn)来为每个线程分配所需资源,直到满足每个线程对资源的最大需求,使每个线程都可顺利完成。称
序列为安全序列。
如何解除死锁?
怎么解除死锁?-帅地玩编程
6.缓冲区溢出
什么是缓冲区溢出?有什么危害?-帅地玩编程
7.页面置换算法
看书
最佳置换算法 先进先出 LRU LFU Clock
8.银行家算法
书121银行家算法
9.磁盘调度算法
书233
10.书第四章看看(没空就算了)
分页分段的区别
至少记得1 2 3
分页与分段的区别?-帅地玩编程
11.书第五章内容
有空去看看
物理地址、逻辑地址、虚拟内存的概念-帅地玩编程
12.典型的锁
几种常见的线程锁 - 简书
java中的各种锁详细介绍 - JYRoy - 博客园
Linux cp 命令详解-帅地玩编程
超级简历WonderCV - HdR推荐简历模板,智能简历制作工具,专业中英文简历模板免费下载
简历
请求大佬帮我看看简历存在那些问题?投了30多份简历都没过初审_我要提问_牛客网
【程序厨】一分命中率接近 100% 的校招简历_职业发展_牛客网
简历这样写,你的面试已经成功了99%_职业发展_牛客网
有没有公司招前端暑假实习勒,请大牛看看这简历怎么改,双非二本_招聘信息_牛客网
[经验贴]技术如何写一份脱俗的简历_技术交流_牛客网
1.对公司有什么了解?
3.没去实习在学校学了什么?
4.项目是自己写的吗?
5.自我介绍
7.评价一下自己
8.怎么证明自己能力好
9.职业规划,说了比较中长期的规划
10.短期的规划
11.最近看的书籍,有什么感想,书的作者是谁
12.平时怎么学习的?
13.最近打算学什么?
14.缺点
15.介绍一下你的大学
1.遇到了什么问题,怎么解决的?
2.在项目中收获了什么?
3.介绍一下这个项目
1.介绍Mybatis
2.介绍Springboot,Spring
3.SpringBoot怎么使用的
1.Java反射的作用
2.get和post的区别
3.springboot核心注解SpringBootApplication
4.git常用命令,提交、拉代码
5.mybatis传参的几种方式
6.abstract和接口的区别,有构造方法吗?起什么作用?不能和哪些一起用?
7.ArrayList和LinkedList的区别?
8.Hash和TreeSet的异同?
9.等号和equals的区别?
10.super
11.io流常见的类?
1.编辑距离
微软又考了这道题
dp[i][j]表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]。
初始化:dp[i][0] = i;
同理dp[0][j] = j;
两层for循环从1开始遍历。
递推公式:
if (word1[i - 1] == word2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
}
else {
dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1;
}
2.最长公共子距离
dp[i][j]:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]
初始化:
dp[i][0] = 0;
同理dp[0][j]也是0。
if (text1[i - 1] == text2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
1.堆排序手撕
堆排序
堆排序_guanlovean的博客-CSDN博客_堆排序
剑指 Offer II 076. 数组中的第 k 大的数字
手写大根堆,在数组中操作
构建大顶堆
然后堆顶和末尾互换,并调用判断交换函数。
时刻注意len,尚未排序的元素个数,交换判断函数可能有递归调整子节点。
第1个叶节点下标 len/2
最后一个非叶节点len/2-1
第i个节点的左子节点2i+1
右子节点2i+2
注意 发生交换时,需要递归调整子节点
函数:
main(){
构建大顶堆函数
交换堆顶0和末尾节点(从len-1到1),判断交换函数
}
构建大顶堆(){
从最后一个非叶节点到第一个,使用判断交换函数
}
判断交换函数(){
判断节点交换
是否递归判断子节点
}
2.LRU手撕
3.LFU手撕
4.快排手撕
5.各种排序
6.十大排序
十大排序算法简介-帅地玩编程
1.哈希表
一般哈希表都是用来快速判断一个元素是否出现集合里
题目:
160相交链表
1. 两数之和
2.二分法
有序或者貌似有序的数组查找某个元素,可以使用二分
题目: 33. 搜索旋转排序数组
注意边界 判断条件是while(left<=right)
3.链表
为了更加方便,我们可以虚拟头节点,即在原来的头节点前面加上一个节点,方便操作。
4.滑动窗口
遍历并维持一个窗口
题目:
3. 无重复字符的最长子串
5.动态规划
当前的操作是否和之前的操作有关
对单单只有一个数组:
需要动态规划时,如果选用二维dp,dp[i][j]一般表示,从字符串i到j,左下角一般不用。
题目:5. 最长回文子串 只有一个数组需要我们判断其中最长回文子串
两个数组
一般的话二维dp数组
dp[i][j],数组1从0到i 和数组2从0到j
剑指 Offer II 095. 最长公共子序列
6.深度优先搜索
使用栈,函数就是天然栈
200. 岛屿数量