Java 基础篇
# Java 语言有哪些特点
- 简单易学、有丰富的类库;
- 面向对象(Java 最重要的特性,让程序耦合度更低、内聚性更高);
- 与平台无关性(JVM 是 Java 跨平台使用的根本);
- 可靠安全;
- 支持多线程。
# 面向对象和面向过程的区别
面向过程:是分析解决问题的步骤,然后用函数把这些步骤一步步地实现,然后在使用的时候--调用即可。性能较高,所以单片机、嵌入式开发 一般采用面向过程开发;
面向对象:是把构成问题的事务分解成各个对象,而建立对象的目的也不是为了完成一个个步骤,而是为了描述某个事物在解决整个问题的过程 中所发生的行为。面向对象有封装、继承、多态的特性,所以易维护、易复用、易扩展。可以设计出低耦合的系统。但是性能上来说, 比面向过程要低。
# 八种基本数据类型的大小,以及它们的封装类
基本类型 | 大小(字节) | 默认值 | 封装类 |
---|---|---|---|
byte | 1 | (byte) 0 | Byte |
short | 2 | (short) 0 | Short |
int | 4 | 0 | Integer |
long | 8 | 0L | Long |
float | 4 | 0.0f | Float |
double | 8 | 0.0d | Double |
boolean | - | false | Boolean |
char | 2 | \u0000(null) | Character |
注意
- int 是基本数据类型,Integer 是 int 的封装类,是引用类型。int 默认值是 0, 而 Integer 默认值是 null,所以 Integer 能区分出 0 和 null 的情况。 一旦 java 看到 null,就知道这个引用还没有指向某个对象,再任何引用使用前,必须为其指定一个对象,否则会报错。
- 基本数据类型在声明时系统会自动给它分配空间,而引用类型声明时只是分配了引用空间,必须通过实例化开辟数据空间之后才可以赋值。数组 对象也是一个引用对象,将一个数组赋值给另一个数组时只是复制了一个引用,所以通过某一个数组所做的修改在另一个数组中也看的见。
虽然定义了 boolean 这种数据类型,但是只对它提供了非常有限的支持。在 java 虚拟机中没有任何供 boolean 值专用的字节码指令,java 语言表达式 所操作的 boolean 值,在编译之后都使用 java 虚拟机中的 int 数据类型来代替,而 boolean 数组将会被编码成 java 虚拟机的 byte 数组,每个元素 boolean 元素占 8 位。这样我们可以得出 boolean 类型单独使用占了 4 个字节,在数组中又是 1 个字节。使用 int 的原因是,对于当下 32 位的处理器(CPU) 来说,一次处理数据是 32 位(这里不是指的是 32/64 位系统,而是指 CPU 硬件层面),具有高效存取的特点。
# 标识符的命名规则
标识符的含义:
是指在程序中,我们自己定义的内容,譬如,类的名字,方法名称以及变量名称等等,都是标识符。
命名规则(硬性要求):
- 标识符可以包含英文字母,0-9 的数字,$以及_;
- 标识符不能以数字开头;
- 标识符不能是关键字。
命名规范(非硬性要求):
- 类命规范:首字符大写,后面每个单词首字母大写(大驼峰式);
- 变量名规范:首字母小写,后面每个单词首字母大写(小驼峰式);
- 方法名规范:同变量名。
# instanceof 关键字的作用
instanceof 严格来说是 Java 种的一个双目运算符,用来测试一个对象是否为一个类的实例,用法为:
boolean result = obj instanceof Class
其中 obj 为一个对象,Class 标识一个类或者一个接口,当 obj 为 Class 的对象,或者是其直接或间接子类,或者是其接口的实现类,结果 result 都返回 true,否则返回 false。
注意
编译器会检查 obj 是否能转换成右边的 class 类型,如果不能转换则直接报错,如果不能确定类型,则通过编译,具体看运行时定。
int i = 0;
System.out.println(i instanceof Integer); // 编译不通过,i必须是引用类型,不能是基本类型。
System.out.println(i instanceof Object); // 编译不通过
Integer integer = new Integer(1);
System.out.println(integer instanceof Integer); // true
System.out.println(null instanceof Object); // false, 在JavaSE规范种对instanceof运算符的规定就是:如果obj为null,那么返回false。
2
3
4
5
6
7
# Java 自动装箱与拆箱
装箱:就是自动将基本数据类型转换为包装器类型(int -> Integer);调用方法:Integer 的 valueOf(int)方法;
拆箱:就是自动将包装器类型转换为基本数据类型(Integer -> int);调用方法:Integer 的 intValue 方法。
在 Java SE5 之前,如果要生成一个数值为 10 的 Integer 对象,必须这样:
Integer i = new Integer(10);
而在从 Java SE5 开始就提供了自动装箱的特性,如果要生成一个数值为 10 的 Integer 对象,只需要:
Integer i = 10;
- 面试题:以下代码会输出什么?
public class Main {
public static void main(String[] args) {
Integer i1 = 100;
Integer i2 = 100;
Integer i3 = 200;
Integer i4 = 200;
System.out.println(i1 == i2);
System.out.println(i3 == i4);
}
}
2
3
4
5
6
7
8
9
10
11
12
运行结果
true
false
2
输出结果表明 i1 和 i2 指向的是同一个对象,而 i3 和 i4 指向的是不通的对象。看源码便知究竟,下面这段代码是 Integer 的 valueOf 方法的具体实现:
public static Integer valueOf(int i) {
if (i >= -128 && i <= IntegerCache.high)
return IntegerCache.cache[i + 128];
else
return new Integer(i);
}
2
3
4
5
6
其中 IntegerCache 类的实现为:
private static class IntegerCache {
static final int high;
static final Integer cache[];
static {
final int low = -128;
// high value may be configured by property
int h = 127;
if (integerCacheHighPropValue != null) {
// Use Long.decode here to avoid invoking methods that
// require Integer's autoboxing cache to be initialized
int i = Long.decode(integerCacheHighPropValue).intValue();
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - -low);
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for (int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
}
private IntegerCache() {}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
从上面代码可以看出,在通过 valueOf 方法创建 Integer 对象的时候,如果数值在[-128, 127]之间,便返回指向 IntegerCache.cache 中已经存在的 对象引用;否正创建一个新的 Integer 对象。
上面代码中 i1 和 i2 的数值为 100,因此会直接从 cache 中取已经存在的对象,所以 i1 和 i2 指向的是同一个对象,而 i3 和 i4 则是分别指向不同的对象。
- 面试题:以下代码输出什么?
public class Main {
public static void main(String[] args) {
Double i1 = 100.0;
Double i2 = 100.0;
Double i3 = 200.0;
Double i4 = 200.0;
System.out.println(i1 == i2);
System.out.println(i3 == i4);
}
}
2
3
4
5
6
7
8
9
10
11
运行结果
false
false
2
原因:在某个范围内的整型数值的个数是有限的,而浮点数却不是。
# 重载和重写的区别
# 重写(Override)
从字面上看,重写就是重写一遍的意思。其实就是子类中把父类本身有的方法重新写一遍。子类继承了父类原有的方法,但有时子类并不想原封不动 的继承父类中的某个方法,所以在方法名,参数列表,返回类型(除过子类中方法的返回值是父类中方法返回值的子类时)都相同的情况下,对方法 体进行修改或重写,这就是重写。但要注意子类函数的访问修饰权限不能少于父类的。
public class Father {
public static void main(String[] args) {
// TODO Auto-generated method stub
Son s = new Son();
s.sayHello();
}
public void sayHello() {
System.out.println("Hello");
}
}
class Son extends Father {
@Override
public void sayHello() {
// TODO Auto-generated method stub
System.out.println("hello by ");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
重写总结:
- 发生在父类与子类之间;
- 方法名、参数列表、返回类型(除过子类中方法的返回类型是父类中返回类型的子类)必须相同;
- 访问修饰符的限制一定要大于被重写方法的访问修饰符(
public > protected > default > private
); - 重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常。
# 重载(Overload)
在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同甚至是参数顺序不同)则视为重载。同时,重载对返回类型 没有要求,可以相同也可以不同,但不能通过返回类型是否相同来判断重载。
public class Father {
public static void main(String[] args) {
// TODO Auto-generated method stub
Father s = new Father();
s.sayHello();
s.sayHello("wintershii");
}
public void sayHello() {
System.out.println("Hello");
}
public void sayHello(String name) {
System.out.println("Hello" + " " + name);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
重载总结:
- 重载 Overload 是一个类中多态性的易中表现;
- 重载要求同名方法的参数列表不同(参数类型,参数个数甚至是参数顺序);
- 重载的时候,返回值类型可以相同也可以不同。无法以返回类型作为重载函数的区分标准。
# equals 与==的区别
# ==
==
比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的地址是否相同,即是否是指相同一个对象。比较的是真正意义上的
指针操作。
- 比较的是操作符两端的操作数是否是同一个对象;
- 两边的操作数必须是同一类型的(可以是父子类之间)才能编译通过;
- 比较的是地址,如果是具体的阿拉伯数字的比较,值相等则为 true,如:
int a = 10
与long b = 10L
与double c = 10.0
都是相同 的(true)。因为它们都指向地址为 10 的堆。
# equals
equals
用来比较的是两个对象的内容是否相等,由于所有的类都是继承自java.lang.Object
类的,所以适用于所有对象,如果没有对该方法
进行覆盖的话,调用的仍然是Object
类中的方法,而Object
中的equals
方法返回的却是==
的判断。
总结
所有比较是否相等时,都是用equals
并且在对常量相比较时,把常量写在前面,因为使用object
的equals
,object
可能为null
则空指针
在阿里的代码规范中只使用equals
,阿里插件默认会识别,并可以快速修改,推荐安装阿里插件来排查老代码使用==
,替换成equals
。
# Hashcode 的作用
Java 的集合有两类,一类是 List,还有一类是 Set。前者有序可重复,后者无序不重复。当我们在 set 中插入的时候怎么判断是否已经存在该元素呢? 可以通过 equals 方法。但是如果元素太多,用这样的方法就会比较慢。
于是有人发明了哈希算法来提高集合中查找元素的效率。这种方式将集合分成若干个存储区域,每个对象可以计算出一个哈希码,可以将哈希码分组, 每组分别对应某个存储区域,根据一个对象的哈希码就可以确定该对象应该存储的那个区域。
hashCode
方法可以这样理解:它返回的就是根据对象的内存地址换算出的一个值。这样一来,当集合要添加新的元素时,先调用这个元素的
hashCode
方法,就一下子能定位到它应该放置的物理位置上。如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;
如果这个位置上已经有元素了,就调用它的equals
方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址。这样一来实际调用
equals
方法的次数就大大降低了,几乎只需要一两次。
# String、StringBuffer 和 StringBuilder 的区别是什么?
String
是只读字符串,它并不是基本数据类型,而是一个对象。从底层源码来看是一个final
类型的字符数组,所引用的字符串不能被改变,一经
定义,无法再增删改。每次对String
的操作都会生成新的String
对象。
private final char value[];
每次+
操作:隐式在堆上new
了一个跟原字符串相同的StringBuilder
对象,再调用append
方法拼接+
后面的字符。
StringBuffer
和StringBuilder
它们两都继承了AbstractStringBuilder
抽象类,从AbstractStringBuilder
抽象类中我们可以看到
/**
* The value is used for character storage.
*/
char[] value;
2
3
4
它们的底层都是可变的字符数组,所以在进行频繁的字符串操作时,建议使用StringBuffer
和StringBuilder
来进行操作。另外StringBuffer
对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder
并没有对方法进行加同步锁,所以是非线程安全的。
# ArrayList 和 LinkedList 的区别
Array(数组)是基于索引(index)的数据结构,它使用索引在数组中搜索和读取数据是很快的。
Array 获取数据的时间复杂度是 O(1),但是要删除数据却是开销很大,因为这需要重排数组中的所有数据,(因为删除数据以后,需要把后面所有 的数据迁移)
缺点:数组初始化必须指定初始化的长度,否则报错。
int[] a = new int[4]; // 推介使用int[]这种方式初始化
int c[] = {23, 43, 56, 78}; // 长度:4,索引范围:[0, 3]
2
List -- 是一个有序的集合,可以包含重复的元素,提供了按索引访问的方式,它继承 Collection;
List 有两个重要的实现类:ArrayList 和 LinkedList;
ArrrayList:可以看作是能够自动增长容量的数组;
ArrayList 的 toArray 方法返回一个数组;
ArrayList 的 asList 方法返回一个列表;
ArrayList 的底层实现是 Array,数组扩容实现。
LinkedList 是一个双链表,在添加和删除元素时具有比 ArrayList 更好的性能。但在 get 与 set 方面弱于 ArrayList。当然,这些对比都是指数据量 很大或者操作很频繁。
# HashMap 和 HashTable 的区别
# 两者父类不同
HashMap 是继承自 AbstractMap 类,而 Hashtable 是继承自 Dictionary 类。不过它们都同时都实现了 Map、Cloneable(可复制)、Serializable(可序列化) 这三个接口。
# 对外提供的接口不同
Hashtable 比 HashMap 多提供了 elements()和 contains()两个方法。elements()方法继承自 Hashtable 的父类 Dictionary。elements()方法用于 返回此 Hashtable 中的 value 的枚举。contains()方法判断该 Hashtable 是否包含传入的 value。它的作用与 containsValue()一致。事实上, containsValue()就只是调用了一下 contains()方法。
# 对 null 的支持不同
Hashtable:key 和 value 都不能为 null。
HashMap:key 可以为 null,但是这样的 key 只能有一个,因为必须保证 key 的唯一性;可以有多个 key 值对应的 value 为 null。
# 安全性不同
HashMap 是线程不安全的,在多线程并发的环境下,可能会产生死锁等问题,因此需要开发人员自己处理多线程的安全问题。
Hashtable 是线程安全的,它的每个方法上都有 synchronized 关键字,因此可直接用于多线程中。
虽然 HashMap 是线程不安全的,但是它的效率远远高于 Hashtable,这样设计是合理的,因为大部分的使用场景都是单线程。当需要多线程操作的时候 可以使用线程安全的 ConcurrentHashMap。
ConcurrentHashMap 虽然也是线程安全的,但是它的效率比 Hashtable 要高好多倍。因为 ConcurrentHashMap 使用了分段锁,并不对整个数据进行 锁定。
# 初始容量大小和扩充容量大小不同
# 计算 hash 值的方法不同
# Collection 包结构,与 Collections 的区别
Collection 是集合类的上级接口,子接口有 Set、List、LinkedList、ArrayList、Vector、Stack、Set;
Collections 是集合类的一个帮助类,它包含有各种有关集合操作的静态多态方法,用于实现对各种集合的搜索、排序、线程安全化等操作。此类 不能实例化,就像一个工具类,服务于 Java 的 Collection 框架。
# Java 的四种引用,强弱软虚
# 强引用
强引用是平常中使用最多的引用,强引用在程序内存不足(OOM)的时候也不会被回收,使用方式:
String str = new String("str");
# 软引用
软引用在程序内存不足时,会被回收,使用方式:
/**
* 注意:wrf这个引用也是强引用,它是指向SoftReference这个对象的,这里的软引用指的是指向
* new String("str")的引用,也就是SoftReference类中的T
*/
SoftReference<String> wrf = new SoftReference<String>(new String("str"));
2
3
4
5
可用场景
创建缓存的时候,创建的对象放进缓存中,当内存不足时,JVM 就会回收早先创建的对象。
# 弱引用
弱引用就是只要 JVM 垃圾回收器发现了它,就会将之回收,使用方式:
WeakReference<String> wrf = new WeakReference<String>(str);
可用场景
Java 源码中的java.util.WeakHashMap
中的key
就是使用弱引用,一旦不需要某个引用,JVM 会自动处理它。
# 虚引用
虚引用的回收机制跟弱引用差不多,但是它被回收之前,会被放入ReferenceQueue
中。注意,其它引用是被 JVM 回收后才被传入ReferenceQueue
中的。由于这个机制,虚引用大多被用于引用销毁前的处理工作。还有,虚引用创建的时候,必须带有ReferenceQueue
,例子:
PhantomReference<String> prf = new PhantomReference<String>(new String("str"), new ReferenceQueue<>());
可用场景
对象销毁前的一些操作,比如说资源释放等。Object.finalize()
虽然也可以做这类动作,但是这个方式既不安全又低效。
上述所说的几类引用,都是指对象本身的引用,而不是指Reference
的四个子类的引用(SoftReference
等)。
# 泛型常用特点
泛型是 Java SE 1.5 之后的特性,《Java 核心技术》中对泛型的定义是:
“泛型”意味着编写的代码可以被不同类型的对象所重用。
“泛型”,顾名思义,“泛指的类型”。我们提供了泛指的概念,但具体执行的时候却可以有具体的规则来约束,比如我们用的非常多的 ArrayList 就是 个泛型类,ArrayList 作为集合可以存放各种元素,如 Integer、String,自定义的各种类型等,但在我们使用的时候通过具体的规则来约束,如我们 可以约束集合中只存放 Integer 类型的元素,如:
List<Integer> iniData = new ArrayList<>();
使用泛型的好处?
以结合来举例,使用泛型的好处是我们不必因为添加元素类型的不同而定义不同类型的集合,如整型集合类、浮点型集合类、字符串集合类,我们可以 定义一个集合来存放整型、浮点型、字符串型数据,而这并不是最重要的,因为我们只要把底层存储设置为 Object 即可,添加的数据全部都可向上 转型为 Object。更重要的是我们可以通过规则按照自己的想法控制存储的数据类型。
# Java 创建对象有几种方式?
Java 中提供了以下四种创建对象的方式:
- new 创建新对象
- 通过反射机制
- 采用 clone 机制
- 通过序列化机制
# 有没有可能两个不相等的对象有相同的 hashcode
有可能。在产生 hash 冲突时,两个不相等的对象就会有相同的hashcode
值。当 hash 冲突产生时,一般有以下几种方式来处理:
- 拉链法:每个哈希表节点都有一个 next 指针,多个哈希表节点可以用 next 指针构成一个单向链表,被分配到同一个索引上的多个节点可以用 这个单向链表进行存储。
- 开发定址法:一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
- 再哈希:又加双哈希法,有多个不同的 Hash 函数。当发生冲突时,使用第二个,第三个...等哈希函数计算地址,直到无冲突。
# 深拷贝和浅拷贝的区别是什么?
- 浅拷贝:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。换言之,浅拷贝不复制它所 引用的对象。
- 深拷贝:被复制对象的所有变量都含有与原来的对象相同的值。而那些引用其他对象的的变量将指向被复制过的新对象。而不再是原有的那些 被引用的对象。换言之,深拷贝把要复制的对象所引用的对象都复制了一遍。
# final 有哪些用法?
- 被 final 修饰的类不可以被继承
- 被 final 修饰的方法不可以被重写
- 被 final 修饰的变量不可以被改变,如果修饰引用,那么标识引用不可变,引用指向的内容可变。
- 被 final 修饰的方法,JVM 会尝试将其内联,以提高运行效率
- 被 final 修饰的常量,在编译阶段会存入常量池中
重排序规则
- 在构造函数内对一个 final 域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序;
- 初次读一个包含 final 域的对象的引用,与随后初次读这个 final 域,这两个操作之间不能重排序。
# Java 方法内联
# 什么是 Java 方法内联?
内联函数含义:
内联函数:在计算机科学中,内联函数(有时称作在线函数或编译时期展开函数)是一种编程语言结构,用来建议编译器对一些特殊函数进行 内联扩展(有时称作在线扩展);也就是说建议编译器将指定的函数插入并取代每一处调用该函数的地方(上下文),从而节省了每次调用函数带 来的额外时间开支。
# 为什么要设计内联函数?
其实也就两个字:性能。
内联扩展是一种特别的用于消除调用函数时所造成的固有时间消耗的方法。一般用于能够快速执行的函数,因为在这种情况下函数调用的时间消耗 显得更为突出。
# 方法调用开销
Java 中一个方法中调用不仅有执行方法逻辑的开销,还有一部分底层的开销。比如:方法栈帧的生成、局部变量的进栈与出栈、指令执行地址的跳转、 所以需要在一些特定的时候引入内联机制,减少底层的开销,从而提高运行性能。
- 举个例子
内联之前的代码:
public int fun1(int a, int b) {
return fun2(a, b);
}
public int fun2(int a, int b) {
return a + b;
}
2
3
4
5
6
7
内联之后的代码:
public int fun1(int a, int b) {
return a + b;
}
2
3
这样内联之后减少了 fun2 栈帧的生成、fun2 局部变量进栈出栈操作、以及从 fun1 到 fun2 指令执行的切换开销。那这样是不是就意味着,我们所有的方法 调用都可以使用方法内联来进行优化呢?很明显不可能。
# 方法内联的时机
首先说为什么不能时时都采用方法内联进行性能优化,原因有以下几点:
- 热点代码:即一个代码如果常常被调用,那么才有必要进行内联优化,如果一个方法就调用一次,就没有优化的必要;
- 方法体大小:JVM 中被内联的方法会被编译到机器码放在
code cache
中。如果方法体大,会影响缓存热点方法的个数,反而影响性能。
从上面可知,影响方法内联的因素就是方法调用频率,以及方法大小。当然我们也可以进行 JVM 配置,强制指定哪些方法可以被内联。下面列举部分 内联规则:
- 由
-XX:CompileCommand
中的inline
指令指定的方法,以及由@ForceInline
注解的方法(仅限于 JDK 内部方法),会被强制内联。而由-XX:CompileCommand
中的dontinline
指令或exclude
指令(标识不编译)指定的方法,以及由@DontInline
注解的方法(仅限 JDK 内部方法) 则始终不会被内联; - 如果调用字节码对应的符号引用未被解析、目标方法所在的类未被初始化,或者目标方法是
native
方法,都将导致无法内联; - JVM 决定一个方法是否是热点(例如,(频繁调用)基于内部计算;它不直接受任何可调参数的影响。如果一个方法由于频繁调用而符合内联条件,
那么只有当它的字节码大小小于 325 字节(或者指定为
-XX:MaxFreqInlineSize=N
标志)时,它才会内联。否则,只有当它很小:小于 35 字节 (或者指定为-XX:MaxInlineSize=N
标志的值时),它才有资格进行内联)。
当然方法内联还有很多限制,如下表:
java -XX:+PrintFlagsFinal
参数名 | 默认值 | 说明 |
---|---|---|
-XX:InlineSmallCode | 2000 | 如果目标方法已被编译,且其生成的机器码大小超过该值,则无法内联。 |
-XX:MaxTrivialSize | 6 | 如果方法的字节码大小少于该值,则直接内联 |
-XX:MinInliningThreshold | 250 | 如果目标方法的调用次数低于该值,则无法内联 |
-XX:InlineFrequencyCount | 100 | 如果方法调用指令执行次数超过该值,则认为是热点方法 |
-XX:MaxInlineSize | 35 | 如果非热点方法的字节码大小超过该值,则无法内联 |
-XX:FreqInlineSize | 325 | 如果热点方法的字节码大小超过该值,则无法内联 |
-XX:LiveNodeCountInliningCutoff | 40000 | 编译过程中 IR 节点树木上限 |
# Java 方法内联和 C++函数内联区别
- C++函数内联可通过 inline 进行声明,而 Java 方法内联则由 JVM 控制,开发者无法控制;
- C++函数内联为编译时进行,而 Java 方法内联则是由 JIT 在运行期进行
# static 都有哪些用法?
所有的人都知道 static 关键字这两个基本的用法:静态变量和静态方法。也就是被 static 所修饰的变量/方法都属于类的静态资源,类实例所共享。
除了静态变量和静态方法之外,static 也用于静态块,多用于初始化操作:
public class PreCache {
static {
// 执行相关操作
}
}
2
3
4
5
此外 static 也多用于修饰内部类,此时称之为静态内部类。
最后易中用法就是静态导包,即import static
。import static 是在 JDK 1.5 之后引入的新特性,可以用来指定导入某个类中的静态资源,并且
不需要使用类命,可以直接使用资源名,比如:
import static java.lang.Math.*;
public class Test {
public static void main(String[] args) {
// System.out.println(Mathi.sin(20)); 传统做法
System.out.println(sin(20));
}
}
2
3
4
5
6
7
8
# 3 * 0.1 == 0.3 返回值是什么
false
,因为有些浮点数不能完全精确的表示出来。
# a = a + b 与 a+=b 有什么区别吗?
+=
操作符会进行隐式自动类型转换,此处a+=b
隐式的将加操作的结果类型强制转换为持有结果的类型,而a = a + b
则不会自动进行类型转换。
如:
byte a = 127;
byte b = 127;
b = a + b; // 报编译错误:cannot convert from int to byte
b += a;
2
3
4
以下代码是否有错?
short s1 = 1;
s1 = s1 + 1;
2
有错误,short 类型在进行运算时会自动提升为 int 类型,也就是说s1 + 1
的运算结果是 int 类型,而 s1 是 short 类型,此时编译器会报错。
正确写法:
short s1 = 1;
s1 += 1;
2
+=
操作符会对右边的表达式结果强制匹配左边的数据类型,所以没错。
# try catch finally,try 里有 return,finally 还执行吗?
执行,并且 finally 的执行早于 try 里面的 return
结论
- 不管有没有出现异常,finally 块中的代码都会执行;
- 当 try 和 catch 中有 return 时,finally 仍然会执行;
- finally 是在 return 后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,不管 finally 中的代码怎么样, 返回的值都不会改变,仍然是之前保存的值),所以函数返回值是在 finally 执行前确定的;
- finally 中最好不要包含 return,否则程序会提前退出,返回值不是 try 或 catch 中保存的返回值。
# Exception 与 Error 包结构
Java 可抛出(Throwable)的结构分为三种类型:已检查异常(CheckedException),运行时异常(RuntimeException),错误(Error)。
# 运行时异常
定义:RuntimeException
及其子类都被称为运行时异常。
特点:Java 编译器不会检查它。也就是说,当程序中可能出现这类异常时,倘若既没有通过throws
声明抛出它,也没有用try-catch
捕获,
还是会编译通过。例如,除数为零时产生的ArithmeticException
异常,数组越界时产生的IndexOutOfBoundException
异常,fail-fast
机制产生的ConcurrentModificationException
异常(java.util
包下面的所有的集合类都是快速失败的,它是 Java 集合的一种错误检测机制)。
当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast
机制。**记住是有可能,而不是一定。**例如:假设存在两个线程(
线程 1、线程 2),线程 1 通过Iterator
在遍历集合 A 中的元素,在某个时候线程 2 修改了集合 A 的结构(是结构上面的修改,而不是修改集合元素的内容),
那么这个时候程序就会抛出ConcurrentModificationException
异常,从而产生fail-fast
机制,这个错叫并发修改异常。Fail-safe
,
java.util.concurrent
包下面的所有的类都是安全失败的,在遍历过程中,如果已经遍历的数组上的内容变化了,迭代器不会抛出
ConcurrentModificationException
异常。如果未遍历的数组上的内容发生了变化,则有可能反映到迭代过程中。这就是ConcurrentHashMap
迭代器弱一致的表现。ConcurrentHashMap
的弱一致性主要是为了提升效率,是一致性与效率之间的一种权衡。要成为强一致性,就得到处使用锁,
甚至是全局锁,这就与Hashtable
和同步的HashMap
一样了。
常见的五种运行时异常:
- ClassCastException(类转换异常)
- IndexOutOfBoundException(数组越界)
- NullPointerException(空指针异常)
- ArrayStoreException(数据存储异常,操作数组时类型不一致)
- BufferOverflowException
# 已检查异常
定义:Exception
类本身,以及Exception
的子类中除了运行时异常之外的其它子类都属于已检查异常。
特点:Java 编译器会检查它。此类异常,要么通过throws
进行声明抛出,要么通过try-catch
进行捕获处理,否则不能通过编译。例如,
CloneNotSupportedException
就属于已检查异常。当通过clone()
接口去克隆一个对象,而该对象对应的类没有实现Cloneable
接口,就会
抛出CloneNotSupportedException
异常。已检查异常通常都是可以恢复的。如:IOException
、FileNotFoundException
、SQLException
已检查的异常适用于那些不是因程序引起的错误情况,比如:读取文件时文件不存在引发的FileNotFoundException
。然而,不被检查的异常都是
由于糟糕的编程引起的,比如:在对象引用时没有确保对象非空而引起的NullPointerException
。
# 错误
定义:Error
类及其子类
特点:和运行时异常一样,编译器也不会对错误进行检查。
当资源不足、约束失败、或是其它程序无法继续运行的条件发生时,就产生错误。程序本身无法修复这些错误。例如,VirtualMachineError
就
属于错误,出现这种错误会导致程序终止运行:OutOfMemoryError、ThreadDeath。
# OOM 遇到过哪些情况,SOF 遇到过哪些情况
Java 虚拟机规范规定 JVM 的内存分为了好几块,比如堆、栈、程序计数器、方法区等。
# OOM
OutOfMemoryError
异常,除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(OOM)
异常的可能。
- Java Heap 溢出
一般的异常信息:java.lang.OutOfMemoryError: Java heap spaces
。
Java 堆用于存储对象实例,我们只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,就会在对象 数量达到最大堆容量限制后产生内存溢出异常。
出现这种异常,一般手段是先通过内存映像分析工具(如 Eclipse Memory Analyzer)对 dump 出来的堆转存快照进行分析,重点是确认内存中的对象 是否是必要的,先分清是因为内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。
如果是内存泄漏,可进一步通过工具查看泄漏对象到GCRoots
的引用链。于是就能找到泄漏对象是通过怎样的路径与GCRoots
相关联并导致垃圾
收集器无法自动回收。
如果不存在泄漏,就应该检查虚拟机的参数(-Xmx
与-Xms
)的设置是否适当。
- 虚拟机栈和本地方法栈溢出
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError
异常。
如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError
异常。
这里需要注意当栈越大可分配的线程数就越少。
- 运行时常量池溢出
异常信息:java.lang.OutOfMemoryError: PermGenspace
如果要向运行时常量池中添加内容,最简单的做法就是适用String.intern()
这个Native
方法。该方法的作用是:如果池中已经包含一个等于此
String 的字符串,则返回代表池中这个字符串的 String 对象;否则,将此 String 对象包含的字符串添加到常量池中,并且返回此 String 对象的引用。
由于常量池分配在方法区内,可以通过-XX:PermSize
和-XX:MaxPermSize
限制方法区的大小,从而间接限制其中常量池的容量。
- 方法区溢出
方法区用于存放 Class 的相关信息,如类命、访问修饰符、常量池、字段描述、方法描述等。也有可能是方法区中保存的 class 对象没有被及时回收掉 或者 class 信息占用的内存超过了我们配置。
异常信息:java.lang.OutOfMemoryError: PermGenspace
方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,判定条件是很苛刻的。在经常动态生成大量 Class 的引用中,要特别注意。
# SOF(堆栈溢出 StackOverflow)
StackOverflowError
的定义:当应用程序递归太深而发生堆栈溢出时,抛出该错误。
栈空间一般默认 1~2M,一旦出现死循环或者是大量的递归调用,在不断的压栈过程中,造成栈容量超过 1M 而导致溢出。
栈溢出的原因:递归调用,大量循环或死循环,全局变量是否过多,数组、List、Map 数据过大。
# Memory Leak (内存溢出)
当一个对象不用了,原本应该被垃圾回收机制回收。但是却被其他对象所持有造成无法回收的现象。几种常见的问题:
- 单例造成的内存泄漏
- Handler 造成的内存泄漏
- 线程造成的内存泄漏
它们几个都有一个相同点那就是引用问题(生命周期长的引用生命周期短的而导致无法被回收),解决方法:生命周期长的不要引用生命周期短的 context,可以考虑用 application context 替换 activity context,静态变量应该避免引用 activity context。
内存泄漏是造成内存溢出的主要原因。
# 简述线程、程序、进程的基本概念。以及它们之间的关系是什么?
线程:与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享 同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间切换工作时,负担要比进程小的多,也正因为如此,线程也被称为 轻量级进程。
程序:是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
进程:是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。 简单说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源和 CPU 时间、内存空间、 文件、输入输出设备的使用权等等。换句话说,当程序执行时,将会被操作系统载入内存中。线程是进程划分成的更小的运行单位。线程和进程最大的 不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一个角度来说,进程属于操作系统的范畴, 主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。
# 线程有哪些基本状态?
Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态
状态名称 | 说明 |
---|---|
NEW | 初始状态,线程被构建,但是还没有调用 start()方法 |
RUNNABLE | 运行状态,Java 线程将操作系统中的就绪和运行两种状态笼统地称作“运行中” |
BLOCKED | 阻塞状态,表示线程阻塞于锁 |
WAITING | 等待状态,标识线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断) |
TIME_WAITING | 超市等待状态,改状态不同于 WAITING,它是可以在指定的时间自行返回的 |
TERMINATED | 终止状态,表示当前线程已经执行完毕 |
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。
操作系统隐藏Java虚拟机(JVM)中的RUNNABLE和RUNNING状态,它只能看到RUNNABLE状态,Java系统一般将这两个状态称为RUNNABLE(运行中) 状态。
当线程执行wait()
方法之后,线程进入**WAITING(等待)**状态。进入等待状态的线程需要依靠其他线程的通知才能返回到运行状态,而
TIMED WAITING
(超时等待)状态相当于在等待的基础上增加了超时限制,比如通过sleep(long millis)
方法或wait(long millis)
方法
可以将Java线程置于TIMED WAITING
状态。当超时时间到达后Java线程将会返回到RUNNABLE
状态。当线程调用同步方法时,再没有获取到锁的
情况下,线程将会进入到BLOCKED
(阻塞)状态。线程在执行Runnalbe
的run()
方法之后将会进入到TERMINATED
(终止)状态。
# Java序列化中如果有些字段不想进行序列化,怎么办?
对于不想进行序列化的变量,使用transient
关键字修饰。
transient
关键字的作用是:阻止实例中那些用此关键字修饰的变量序列化;当对象被反序列化时,被transient
修饰的变量值不会被持久化和
恢复。transient
只能修饰变量,不能修饰类和方法。
# Java中IO流
Java中IO流种类:
- 按照流向分,可分为输入流和输出流
- 按照操作单元分,可分为字节流和字符流
- 按照流的角色划分为节点流和处理流
Java IO流共涉及40多个类,看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系,Java IO流的40多个类都是从如下4个抽象类 基类中派生出来的。
- InputStream/Reader:所有的输入流的基类,前者是字节输入流,后者是字符输入流
- OutputStream/Writer:所有输出流的基类,前者是字节输出流,后者是字符输出流
按操作方式分类结构图:
按操作对象分类结构图:
# Java IO与NIO
IO的全称是input output
,是Java程序跟外部世界交流的桥梁,IO指的是java.io
包中的所有类,它们是从Java1.0开始就存在的。NIO叫做
new IO
,是再Java1.4中引入的新一代IO。
# IO的本质
IO的作用就是从外部系统读取数据到Java程序中,或者把Java程序中输出的数据写回到外部系统。这里的外部系统可能是磁盘、网络流等。
因为对所有的外部数据的处理都是由操作系统内核来实现的,对于Java应用程序来说,只是调用操作系统中相应的接口方法,从而和外部数据进行交互。 所有IO的本质就是对Buffer的处理,我们把数据放入Buffer供系统写入外部数据,或者从系统Buffer中读取从外部系统中读取的数据。如下图:
用户空间也就是我们自己的Java程序有一个Buffer,系统空间也有一个Buffer。所以会出现系统空间缓存数据的情况,这种情况下系统空间将会直接 返回Buffer中的数据,提升读取速度。
# DMA和虚拟地址空间
我们先讲解两个操作系统中的基本概念:
DMA(Direct memory access)组件:一般来说对内存的读写都是要交给CPU完成,在没有DMA的情况下,如果程序进行IO操作,那么所有的CPU时间 都会被占用,CPU没法去响应其它的任务,只能等待IO执行完成。这在现代应用程序中是无法想象的。如果使用DMA,则CPU可以把IO操作转交给其它的 操作系统组件,比如数据管理器来操作,只有当数据管理器操作完毕之后,才会通知CPU该IO操作完成。
虚拟地址空间(Virtual address space):为了不同程序的互相隔离和保证程序中地址的确定性,现代计算机系统引入了虚拟地址空间的概念。 简单说可以看作是跟实际物理地址的映射,通过使用分段或者分页的技术,将实际的物理地址映射到虚拟地址空间。
对于上面的IO的基本流程图中,我们可以将系统空间的buffer和用户空间的buffer同时映射到虚拟地址空间的同一个地方。这样就省略了从系统空间 拷贝到用户空间的步骤,速度会更快。
同时为了解决虚拟空间比物理内存空间大的问题,现代计算机技术一般都采用了分页技术。分页技术就是将虚拟空间分为很多个page,只有在需要用到 的时候才为该page分配到物理内存的映射,这些物理内存实际上可以看作虚拟空间地址的缓存。
虚拟空间地址分页对IO的影响就在于,IO的操作也是基于page的。比较常用的page大小有:1024、2048和4096 bytes。
# IO和NIO的区别
Java1.0中的IO是流式IO,它只能一个字节一个字节的处理数据,所以IO也叫做Stream IO。而NIO是为了提升IO的效率而生的,它是以Block的方式 来读取数据的。
Stream IO中,input输入一个字节,output就输出一个字节,因为是Stream,所以可以加上过滤器或者过滤器链,可以想象一下web框架中的filter chain。在Stream IO中,数据只能处理一次,不能在Stream中回退数据。
在Block IO中,数据是以block的形式来被处理的,因此其处理速度要比Stream IO快,同时可以回退处理数据。但是要自己处理Buffer,因此复杂 度要比Stream IO高。
# 限流算法
大多数情况下,我们不需要自己实现一个限流系统,但限流在实际应用中是一个非常微妙、有很多细节的系统保护手段,尤其是高流量时,了解所使用 的限流系统的限流算法,将能很好地帮助我们充分利用该限流系统达到自己的商业需求和目的,并规避一些使用限流系统可能带来的大大小小的问题。
# 令牌桶算法
令牌桶(token bucket)算法,指的是涉及一个容器(即“桶”),由某个组件持续运行往该容器中添加令牌(token),令牌可以是简单的数字、字符或 组合,也可以仅仅是一个计数,然后每个请求进入系统时,需要从桶中领取一个令牌,所有请求都必须有令牌才能进入后端系统。当令牌桶空时,拒绝 请求;当令牌桶满时,不再往其中添加新的令牌。
令牌桶算法的架构如图所示:
令牌桶算法的实现逻辑如下:
首先会有一个定义的时间窗口的访问次数阈值,例如每天1000人,每秒5个请求之类,限流系统一般最小粒度是秒,再小就会因为实现和性能的原因而 变得不准确或不稳定,假设是T秒内允许N个请求,那么令牌桶算法则会使令牌添加组件每T秒往令牌桶中添加N个令牌。
其次、令牌桶需要一个最大值M,当令牌添加组件检测到令牌桶中已经有M个令牌时,剩余的令牌会被丢弃。反映到限流系统中,可以认为是当前系统 允许的瞬时最大流量,但不是持续最大流量。例如令牌桶中的令牌最大数量是100个,每秒会往其中添加10个新令牌,当令牌满的时候,突然出现100 TPS的流量,这时候是可以承受的,但是加入连续两秒的100 TPS流量就不行,因为令牌添加速度是一秒10个,添加速度跟不上使用速度。
因此,凡是使用令牌桶算法的限流系统,我们都会注意到它在配置时要求两个参数:
- 平均阈值(rate或average)
- 高峰阈值(burst或peak)
令牌桶算法的高峰阈值是有特指的,并不是指当前限流系统允许的最高流量。因为这一描述可能会使人认为只要低于该阈值的流量都可以,但事实上不 是这样,因为只要高于添加速度的流量持续一段时间都会出现问题。反过来说,令牌桶算法的限流系统不容易计算出它支持的最高流量,因为它能实时 支持的最高流量取决于时间段内的流量变化情况即令牌存量,而不是仅仅取决于一个瞬时的令牌量。
最后,当有组件请求令牌的时候,令牌桶会随机挑选一个令牌分发出去,然后将该令牌从桶中移除。注意,此时令牌桶不再做别的操作,令牌桶永远 不会主动要求令牌添加组件补充新的令牌。
# 漏桶算法
漏桶算法是一种采用令牌桶算法同一思想、方向相反的变种算法,它是令牌桶的一种改进,在商业应用中非常广泛。
漏桶算法的基本思想,是将请求看作水流,用一个底下有洞的桶盛装,底下的洞漏出水的速率是恒定的,所有请求进入系统的时候都会先进入这个桶, 并慢慢由洞流出交给后台服务。桶有一个固定大小,当水流量超过这个大小的时候,多余的请求都会被丢弃。
漏桶算法的架构如图:
漏桶算法的实现逻辑如下:
- 首先会有一个容器存放请求,该容器有一个固定大小M,所有请求都会被先存放到该容器中;
- 该容器会有一个转发逻辑,该转发以每T秒N个请求的速率循环发生;
- 当容器中请求数已经达到M个时,拒绝所有新的请求。
因此同样地,漏桶算法的配置也需要两个值:平均值(rate)和峰值(burst)。只是平均值这时候是用来表示漏出的请求数量,峰值则是表示桶中 可以存放的请求数量。
漏桶算法和缓冲的限流思想不是一回事!
同样是将请求放在一个容器中,漏桶算法和缓冲不是一个用途,切不可搞混,它们的区别如下:
- 漏桶算法中,存在桶中的请求会以恒定的速率被漏给后端业务服务器,而缓存思想中,放在缓冲区域的请求只有等到后端服务器空闲下来了,才会 被发出去;
- 漏桶算法中,存在桶中的请求是原本就应该被系统处理的,是系统对外界宣称的预期,不应该被丢弃,而缓冲思想中放在缓冲区域的请求仅仅是对 意外状况的尽量优化,并没有任何强制要求这些请求可以被处理。
漏桶算法和令牌桶算法在思想上非常接近,而实现方向恰好相反,它们的相同和不同之处如下:
- 令牌桶算法以固定速率补充可以转发的请求数量(令牌),而漏桶算法以固定速率转发请求;
- 令牌桶算法限制数量的是预算数,漏桶算法限制数量的是实际请求数;
- 令牌桶算法在有爆发式增长的流量时可以一定程度上接受,漏桶算法也是,但当流量爆发时,令牌桶算法会使业务服务器直接承担这种流量,而漏桶 算法的业务服务器感受到的是一样的速率变化。
因此,通过以上比较,漏桶算法略优于令牌桶算法,因为漏桶算法对流量控制更平滑,而令牌桶算法在设置的数值范围内,会将流量波动忠实地转嫁到 业务服务器上。
漏桶算法在Nginx和分布式的限流系统例如Redis的限流功能中都有广泛应用,是目前业界最流行的算法之一。
# 时间窗口算法
时间窗口算法是比较简单、基础的限流算法,由于它比较粗略,不适合大型、流量波动大或者有更精细的流量控制需求的网站。
时间窗口算法根据确定时间窗口的方式,可以分为两种:
- 固定时间窗口算法
- 滑动时间窗口算法
固定时间窗口算法最简单,这种算法即固定一个时间段内限定一个请求阈值,没有达到则让请求通过,达到数量阈值就拒绝请求。步骤如下:
- 先确定一个起始时间点,一般就是系统启动的时间;
- 从起始时间点开始,根据需求,设置一个最大值M,开始接受请求并从0开始为请求计数;
- 在时间段T内,请求计数超过M时,拒绝所有剩下的请求;
- 超过时间段T后,重置计数。
固定时间窗口算法的思路简单,但它的逻辑有问题,不适合流量波动大和有精细控制流量需求的服务。
假设我们的时间段是1秒,请求最大值是10,在第一秒内,请求数量分布是第500毫秒时有1个请求,第800毫秒时有9个请求,如图所示:
对于第一秒而言,这个请求分布是合理的。
此时第二秒的第200毫秒(即两秒中的第1200毫秒)内,又来了10个请求,如图:
单独看第二秒依然合理,但是两个时间段连在一起时,就有问题了,如图:
从500毫秒到1200毫秒,700毫秒内后端服务器就接收了20个请求,显然违背了一开始1秒最多10个请求的初衷。这种远远大于预期流量的流量加到 后端服务器上,会造成不可预料的后果。因此,人们改进了固定窗口的算法,将其改为检查任何一个时间段都不超过请求数量阈值的时间窗口算法: 滑动时间窗口算法。
滑动时间窗口算法要求当请求进入系统时,回溯过去的时间段T,找到其中的请求数量,然后决定是否接受当前请求,因此,滑动事件窗口算法需要 记录时间段T内请求到达的时间点,逻辑如图:
解释如下:
- 确定一个起始时间点,一般就是系统启动的时间,并记录该点为时间窗口的开始点。然后创建一个空的列表作为时间窗口内请求进入的时间戳记录
- 当请求到来时,使用当前时间戳比较它是否在时间窗口起始点加上T时间段(从开始点到开始点+T就是时间窗口)内。
- 如果在,则查看当前时间窗口内记录的所有请求的数量:
- 如果超过,则拒绝请求
- 如果没有,则将该请求加入到时间戳记录中,并将请求交给后端业务服务器
- 如果不在,则查看时间戳记录,将时间戳最久远的记录删除,然后将时间窗口的开始点更新为第二久远的记录时间,然后回到步骤2,再次检查 时间戳是否在时间窗口内
- 如果在,则查看当前时间窗口内记录的所有请求的数量:
滑动时间窗口尽管有所改进,但依然不能很好应对某个时间段内突发大量请求,而令牌桶和漏桶算法就由于允许制定平均请求率和最大瞬时请求率,它 比时间窗口算法控制更精确。
时间窗口算法可以通过多时间窗口来改进。例如,可以设置一个1秒10TPS的时间窗口限流和一个500毫秒5 TPS的时间窗口限流,二者同时运行,如此 就可以保证更精确的限流控制。
# 队列法
队列法与漏桶算法很类似,都是将请求放入到一个区域,然后业务服务器从中提取请求,但是队列法采用的时完全独立的外部系统,而不是依附于限流 系统。队列法的架构图:
与漏桶算法相比,队列法的优势如下:
- 由业务逻辑层决定请求收取的速度。限流系统即队列不需要再关注流量的设置(例如T是多少,N是多少,M又是多少等等),只需要专注保留发送的 请求,而业务服务器由于完全掌控消息的拉去,可以根据自身条件决定请求获取的速度,更加自由;
- 完全将业务逻辑层保护起来,并且可以增加服务消费这些请求。这一手段将业务服务器完全隐藏在了客户端后面,由队列去承担所有流量,也可以 更好地保护自身不受到恶意流量的攻击。
- 独立额可以使用更健壮、更成熟的服务,这些服务比限流系统更复杂,但能够承受大量的流量。
队列法最大的缺陷,就是服务器不能直接与客户端沟通,因此只适用于客户端令业务服务器执行任务且不要求响应的用例,所有客户端需要有实质性 响应的服务不能使用。
# Sentinel VS Hystrix
Sentinel是阿里中间件团队开源的,面向分布式服务架构的轻量级高可用流量控制组件,主要以流量为切入点,从流量控制、熔断降级、系统负载 保护等多个维度来帮助用户保护服务的稳定性。
Hystrix的官方介绍:
Hystrix is a library that helps you control the interactions between these distributed services by adding latency tolerance and fault tolerance logic. Hystrix does this by isolating points of access between the services, stopping cascading failures across them, and providing fallback options, all of which improve your system's overal resiliency.
Hystrix的关注点在于以隔离和熔断为主的容错机制,超时或被熔断的调用将会快速失败,并可以提供fallback
机制和。
Sentinel的侧重点在于:
- 多样化的流量控制
- 熔断降级
- 系统负载保护
- 实时监控和控制台
# 共同特性--资源模型和执行模型上的对比
Hystrix的资源模型设计上采用了命令模式,将对外部资源的调用和fallback
逻辑封装成了一个命令对象(HystrixCommand/HystrixObervableCommand
),
其底层的执行是基于RxJava
实现的。每个Command
创建时都要指定commandKey
和groupKey
(用于区分资源)以及对应的隔离策略(线程池隔离 or
信号量隔离)。
Sentinel的设计则更为简单。相比Hystrix Command
强依赖隔离规则,Sentinel的资源定义与规则配置的耦合度更低。Hystrix的Command
强
依赖于隔离规则的原因是隔离规则会直接影响Command
的执行。
Sentinel的原则会场简单:根据对应资源配置的规则来为资源执行响应的限流/降级/负载保护策略。在Sentinel中资源定义和规则配置是分离的。用户
先通过Sentinel API
给对应的业务逻辑定义资源(埋点),然后可以在需要的时候配置规则。
从0.1.1版本开始,Sentinel还支持基于注解的资源定义方式,可以通过注解参数指定异常处理函数和fallback
函数。
从0.2.0版本开始,Sentinel引入异步调用链路支持,可以方便地统计异步调用资源的数据,维护异步调用链路,同时具备了适配异步框架/库的能力。
Sentinel提供多样化的规则配置方式。除了直接通过loadRules API
将规则注册到内存态之外,用户还可以注册各种外部数据源来提供动态的规则。
用户可以根据系统当前的实时情况去动态地变更规则配置,数据源会将变更推送至Sentinel并即时生效。
# 隔离设计上的对比
实际情况下,线程池隔离并没有带来非常多的好处。首先就是过多的线程池会非常影响性能。考虑这样一个场景,在Tomcat之类的Servlet容器使用 Hystrix,本身Tomcat自身的线程数目就非常多(几十或上百),如果加上Hystrix为各个资源创建的线程池,总共线程数会非常多,这样上下文会 有非常大的损耗。
Hystrix的信号量隔离限制对某个资源调用的并发数。这样的隔离非常轻量级,仅限制对某个资源调用的并发数,而不是显式地创建线程池,所以
overhead
比较小,但效果不错,也支持超时失败。
# 熔断降级对比
Sentinel目前抽象出了Metric
指标统计接口,底层可以有不同的实现,目前默认的实现是基于LeapArray
的高性能滑动窗口,后续根据需要可能
会引入reactive stream
等实现。
- 轻量级、高性能:Sentinel作为一个功能完备的高可用流量管控组件,其核心
sentinel-core
没有任何多余的依赖,打包后不到200KB,非常轻量级。 开发者可以放心地引入sentinel-core
而不需担心依赖问题。同时,Sentinel提供了多种扩展点,用户可以很方便地根据需求进行扩展,并且无缝 地切合到Sentinel中。
引入Sentinel带来的性能损耗非常小。只有在业务单机量级超过25W QPS的时候才会有一些显著的影响(5% - 10%左右),单机QPS不太大的时候 损耗几乎可以忽略不记。
- 流量控制:Sentinel可以针对不同的调用关系,以不同的运行指标(如QPS、并发调用数、系统负载等)为基准,对资源调用进行流量控制,将 随机的请求调整成合适的形状。
Sentinel支持多样化的流量整形策略,在QPS过高的时候可以自动将流量调整成合适的形状。常用的有:
- 直接拒绝模式:即超出的请求直接拒绝;
- 慢启动预热模式:当流量激增的时候,控制流量通过的速率,让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间, 避免冷系统被压垮。
- 匀速器模式:利用
Leaky Bucket
算法实现的匀速模式,严格控制了请求通过的时间间隔,同时堆积的请求将会排队,超过超时时长的请求直接 被拒绝。
Sentinel还支持基于调用关系的限流,包括基于调用方限流、基于调用链入口限流、关联流量限流等,依托于Sentinel强大的调用链路统计信息, 可以提供精准的不同维度的限流。
Sentinel 0.2.0 开始支持热点参数限流,能够实时的统计热点参数并针对热点参数的资源调用进行流量控制。
# Spring AOP 底层原理
AOP底层是采用动态代理机制实现的:接口 + 实现类。
- 如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象。
- 没有实现接口的对象,就无法使用JDK Proxy进行代理,需要Spring AOP使用Cglib生成一个被代理对象的子类来作为代理。
就是由代理创建出一个和impl
实现类平级的一个对象,但是这个对象不是一个真正的对象,只是一个代理对象,但它可以实现和impl
相同的功能,
这个就是aop的横向机制原理,这样就不需要修改源代码。
- Java 语言有哪些特点
- 面向对象和面向过程的区别
- 八种基本数据类型的大小,以及它们的封装类
- 标识符的命名规则
- instanceof 关键字的作用
- Java 自动装箱与拆箱
- 重载和重写的区别
- 重写(Override)
- 重载(Overload)
- equals 与==的区别
- ==
- equals
- Hashcode 的作用
- String、StringBuffer 和 StringBuilder 的区别是什么?
- ArrayList 和 LinkedList 的区别
- HashMap 和 HashTable 的区别
- 两者父类不同
- 对外提供的接口不同
- 对 null 的支持不同
- 安全性不同
- 初始容量大小和扩充容量大小不同
- 计算 hash 值的方法不同
- Collection 包结构,与 Collections 的区别
- Java 的四种引用,强弱软虚
- 强引用
- 软引用
- 弱引用
- 虚引用
- 泛型常用特点
- Java 创建对象有几种方式?
- 有没有可能两个不相等的对象有相同的 hashcode
- 深拷贝和浅拷贝的区别是什么?
- final 有哪些用法?
- Java 方法内联
- 什么是 Java 方法内联?
- 为什么要设计内联函数?
- 方法调用开销
- 方法内联的时机
- Java 方法内联和 C++函数内联区别
- static 都有哪些用法?
- 3 * 0.1 == 0.3 返回值是什么
- a = a + b 与 a+=b 有什么区别吗?
- try catch finally,try 里有 return,finally 还执行吗?
- Exception 与 Error 包结构
- 运行时异常
- 已检查异常
- 错误
- OOM 遇到过哪些情况,SOF 遇到过哪些情况
- OOM
- SOF(堆栈溢出 StackOverflow)
- Memory Leak (内存溢出)
- 简述线程、程序、进程的基本概念。以及它们之间的关系是什么?
- 线程有哪些基本状态?
- Java序列化中如果有些字段不想进行序列化,怎么办?
- Java中IO流
- Java IO与NIO
- IO的本质
- DMA和虚拟地址空间
- IO和NIO的区别
- 限流算法
- 令牌桶算法
- 漏桶算法
- 时间窗口算法
- 队列法
- Sentinel VS Hystrix
- 共同特性--资源模型和执行模型上的对比
- 隔离设计上的对比
- 熔断降级对比
- Spring AOP 底层原理