面试题
基础篇
1
、
Java
语言有哪些特点
1
、简单易学、有丰富的类库
2
、面向对象(
Java
最重要的特性,让程序耦合度更低,内聚性更高)
2
、面向对象和面向过程的区别
面向过程
:是分析解决问题的步骤,然后用函数把这些步骤一步一步地实现,然后在使用的时候一
一调用则可。性能较高,所以单片机、嵌入式开发等一般采用面向过程开发
面向对象
:是把构成问题的事务分解成各个对象,而建立对象的目的也不是为了完成一个个步骤,
而是为了描述某个事物在解决整个问题的过程中所发生的行为。面向对象有
封装、继承、多态
的特
性,所以易维护、易复用、易扩展。可以设计出低耦合的系统。 但是性能上来说,比面向过程要
低。
3
、八种基本数据类型的大小,以及他们的封装类
注:
1.int
是基本数据类型,
Integer
是
int
的封装类,是引用类型。
int
默认值是
0
,而
Integer
默认值
是
null
,所以
Integer
能区分出
0
和
null
的情况。一旦
java
看到
null
,就知道这个引用还没有指向某个
对象,再任何引用使用前,必须为其指定一个对象,否则会报错。
2.
基本数据类型在声明时系统会自动给它分配空间,而引用类型声明时只是分配了引用空间,
必须通过实例化开辟数据空间之后才可以赋值。数组对象也是一个引用对象,将一个数组赋值给另
一个数组时只是复制了一个引用,所以通过某一个数组所做的修改在另一个数组中也看的见。
阿里内部资料
虽然定义了
boolean
这种数据类型,但是只对它提供了非常有限的支持。在
Java
虚拟机中没有
任何供
boolean
值专用的字节码指令,
Java
语言表达式所操作的
boolean
值,在编译之后都使用
Java
虚拟机中的
int
数据类型来代替,而
boolean
数组将会被编码成
Java
虚拟机的
byte
数组,每个元素
boolean
元素占
8
位。这样我们可以得出
boolean
类型占了单独使用是
4
个字节,在数组中又是
1
个字
节。使用
int
的原因是,对于当下
32
位的处理器(
CPU
)来说,一次处理数据是
32
位(这里不是指的
是
32/64
位系统,而是指
CPU
硬件层面),具有高效存取的特点。
4
、标识符的命名规则。
标识符的含义:
是指在程序中,我们自己定义的内容,譬如,类的名字,方法名称以及变量名称等
等,都是标识符。
命名规则:(硬性要求)
标识符可以包含英文字母,
0-9
的数字,
$
以及
_
标识符不能以数字开头 标
识符不是关键字
命名规范:(非硬性要求)
类名规范:首字符大写,后面每个单词首字母大写(大驼峰式)。 变量
名规范:首字母小写,后面每个单词首字母大写(小驼峰式)。 方法名规范:同变量名。
5
、
instanceof
关键字的作用
instanceof
严格来说是
Java
中的一个双目运算符,用来测试一个对象是否为一个类的实例,用法
为:
其中
obj
为一个对象,
Class
表示一个类或者一个接口,当
obj
为
Class
的对象,或者是其直接
或间接子类,或者是其接口的实现类,结果
result
都返回
true
,否则返回
false
。
注意:编译器会检查
obj
是否能转换成右边的
class
类型,如果不能转换则直接报错,如果不能
确定类型,则通过编译,具体看运行时定。
boolean
result
=
obj
instanceof
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
//false ,
在
JavaSE
规范 中对
instanceof
运算符的规定就是:如果
obj
为
null
,那么将返
回
false
。
System
.
out
.
println
(
null
instanceof
Object
);
阿里内部资料
6
、
Java
自动装箱与拆箱
装箱就是自动将基本数据类型转换为包装器类型(
int-->Integer
);调用方法:
Integer
的
valueOf(int)
方法
拆箱就是自动将包装器类型转换为基本数据类型(
Integer-->int
)。调用方法:
Integer
的
intValue
方法
在
Java SE5
之前,如果要生成一个数值为
10
的
Integer
对象,必须这样进行:
而在从
Java SE5
开始就提供了自动装箱的特性,如果要生成一个数值为
10
的
Integer
对象,只需要
这样就可以了:
面试题
1
:
以下代码会输出什么?
运行结果:
为什么会出现这样的结果?输出结果表明
i1
和
i2
指向的是同一个对象,而
i3
和
i4
指向的是不同的对
象。此时只需一看源码便知究竟,下面这段代码是
Integer
的
valueOf
方法的具体实现:
Integer
i
=
new
Integer
(
10
);
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
);
}
}
true
false
阿里内部资料
其中
IntegerCache
类的实现为:
从这
2
段代码可以看出,在通过
valueOf
方法创建
Integer
对象的时候,如果数值在
[-128,127]
之间,
便返回指向
IntegerCache.cache
中已经存在的对象的引用;否则创建一个新的
Integer
对象。
上面的代码中
i1
和
i2
的数值为
100
,因此会直接从
cache
中取已经存在的对象,所以
i1
和
i2
指向的是
同一个对象,而
i3
和
i4
则是分别指向不同的对象。
面试题
2
:以下代码输出什么
public static Integer valueOf(int i) {
if(i >= -128 && i <= IntegerCache.high)
return IntegerCache.cache[i + 128];
else
return new Integer(i);
}
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
() {}
}
阿里内部资料
运行结果:
原因:
在某个范围内的整型数值的个数是有限的,而浮点数却不是。
7
、 重载和重写的区别
重写
(Override)
从字面上看,重写就是 重新写一遍的意思。其实就是在子类中把父类本身有的方法重新写一遍。子
类继承了父类原有的方法,但有时子类并不想原封不动的继承父类中的某个方法,所以在方法名,
参数列表,返回类型
(
除过子类中方法的返回值是父类中方法返回值的子类时
)
都相同的情况下,
对
方法体进行修改或重写,这就是重写。但要注意子类函数的访问修饰权限不能少于父类的。
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
);
}
}
false
false
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
{
重写 总结:
1.
发生在父类与子类之间
2.
方法名,参数列表,返回类型(除过子类中方法的返回类型
是父类中返回类型的子类)必须相同
3.
访问修饰符的限制一定要大于被重写方法的访问修饰符
(
public>protected>default>private) 4.
重写方法一定不能抛出新的检查异常或者比被重写方法申
明更加宽泛的检查型异常
重载(
Overload
)
在一个类中,同名的方法如果有不同的参数列表(
参数类型不同、参数个数不同甚至是参数顺序不
同
)则视为重载。同时,重载对返回类型没有要求,可以相同也可以不同,但
不能通过返回类型是
否相同来判断重载
。
重载 总结:
1.
重载
Overload
是一个类中多态性的一种表现
2.
重载要求同名方法的参数列表不同
(
参
数类型,参数个数甚至是参数顺序
) 3.
重载的时候,返回值类型可以相同也可以不相同。无法以返回
型别作为重载函数的区分标准
8
、
equals
与
==
的区别
@Override
public
void
sayHello
() {
// TODO Auto-generated method stub
System
.
out
.
println
(
"hello by "
);
}
}
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
);
}
}
阿里内部资料
==
:
==
比较的是变量
(
栈
)
内存中存放的对象的
(
堆
)
内存地址,用来判断两个对象的地址是否相同,即是
否是指相同一个对象。比较的是真正意义上的指针操作。
1
、比较的是操作符两端的操作数是否是同一个对象。
2
、两边的操作数必须是同一类型的(可以是
父子类之间)才能编译通过。
3
、比较的是地址,如果是具体的阿拉伯数字的比较,值相等则为
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
9
、
Hashcode
的作用
java
的集合有两类,一类是
List
,还有一类是
Set
。前者有序可重复,后者无序不重复。当我们在
set
中插入的时候怎么判断是否已经存在该元素呢,可以通过
equals
方法。但是如果元素太多,用这样
的方法就会比较满。
于是有人发明了哈希算法来提高集合中查找元素的效率。 这种方式将集合分成若干个存储区域,每
个对象可以计算出一个哈希码,可以将哈希码分组,每组分别对应某个存储区域,根据一个对象的
哈希码就可以确定该对象应该存储的那个区域。
hashCode
方法可以这样理解:它返回的就是根据对象的内存地址换算出的一个值。这样一来,当
集合要添加新的元素时,先调用这个元素的
hashCode
方法,就一下子能定位到它应该放置的物理
位置上。如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如
果这个位置上已经有元素了,就调用它的
equals
方法与新元素进行比较,相同的话就不存了,不相
同就散列其它的地址。这样一来实际调用
equals
方法的次数就大大降低了,几乎只需要一两次。
10
、
String
、
String StringBuffffer
和
StringBuilder
的区别是什
么
?
阿里内部资料
String
是只读字符串,它并不是基本数据类型,而是一个对象。从底层源码来看是一个
fifinal
类型的
字符数组,所引用的字符串不能被改变,一经定义,无法再增删改。每次对
String
的操作都会生成
新的
String
对象。
每次
+
操作 :
隐式在堆上
new
了一个跟原字符串相同的
StringBuilder
对象,再调用
append
方法 拼
接
+
后面的字符。
StringBuffffer
和
StringBuilder
他们两都继承了
AbstractStringBuilder
抽象类,从
AbstractStringBuilder
抽象类中我们可以看到
他们的底层都是可变的字符数组,所以在进行频繁的字符串操作时,建议使用
StringBuffffer
和
StringBuilder
来进行操作。 另外
StringBuffffer
对方法加了同步锁或者对调用的方法加了同步锁,所
以是线程安全的。
StringBuilder
并没有对方法进行加同步锁,所以是非线程安全的。
11
、
ArrayList
和
linkedList
的区别
Array
(数组)是基于索引
(index)
的数据结构,它使用索引在数组中搜索和读取数据是很快的。
Array
获取数据的时间复杂度是
O(1),
但是要删除数据却是开销很大,因为这需要重排数组中的所有
数据
, (
因为删除数据以后
,
需要把后面所有的数据前移
)
缺点
:
数组初始化必须指定初始化的长度
,
否则报错
例如
:
List—
是一个有序的集合,可以包含重复的元素,提供了按索引访问的方式,它继承
Collection
。
List
有两个重要的实现类:
ArrayList
和
LinkedList
ArrayList:
可以看作是能够自动增长容量的数组
ArrayList
的
toArray
方法返回一个数组
ArrayList
的
asList
方法返回一个列表
private final
char
value
[];
/**
* The value is used for character storage.
*/
char
[]
value
;
int
[]
a
=
new
int
[
4
];
//
推介使用
int[]
这种方式初始化
int
c
[]
=
{
23
,
43
,
56
,
78
};
//
长度:
4
,索引范围:
[0,3]
阿里内部资料
ArrayList
底层的实现是
Array,
数组扩容实现
LinkList
是一个双链表
,
在添加和删除元素时具有比
ArrayList
更好的性能
.
但在
get
与
set
方面弱于
ArrayList.
当然
,
这些对比都是指数据量很大或者操作很频繁。
12
、
HashMap
和
HashTable
的区别
1
、两者父类不同
HashMap
是继承自
AbstractMap
类,而
Hashtable
是继承自
Dictionary
类。不过它们都实现了同时
实现了
map
、
Cloneable
(可复制)、
Serializable
(可序列化)这三个接口。
2
、对外提供的接口不同
Hashtable
比
HashMap
多提供了
elments()
和
contains()
两个方法。
elments()
方法继承自
Hashtable
的父类
Dictionnary
。
elements()
方法用于返回此
Hashtable
中的
value
的枚举。
contains()
方法判断该
Hashtable
是否包含传入的
value
。它的作用与
containsValue()
一致。事实
上,
contansValue()
就只是调用了一下
contains()
方法。
3
、对
null
的支持不同
Hashtable
:
key
和
value
都不能为
null
。
HashMap
:
key
可以为
null
,但是这样的
key
只能有一个,因为必须保证
key
的唯一性;可以有多个
key
值对应的
value
为
null
。
4
、安全性不同
HashMap
是线程不安全的,在多线程并发的环境下,可能会产生死锁等问题,因此需要开发人员自
己处理多线程的安全问题。
Hashtable
是线程安全的,它的每个方法上都有
synchronized
关键字,因此可直接用于多线程中。
虽然
HashMap
是线程不安全的,但是它的效率远远高于
Hashtable
,这样设计是合理的,因为大部
分的使用场景都是单线程。当需要多线程操作的时候可以使用线程安全的
ConcurrentHashMap
。
ConcurrentHashMap
虽然也是线程安全的,但是它的效率比
Hashtable
要高好多倍。因为
ConcurrentHashMap
使用了分段锁,并不对整个数据进行锁定。
5
、初始容量大小和每次扩充容量大小不同
6
、计算
hash
值的方法不同
13
、
Collection
包结构,与
Collections
的区别
Collection
是集合类的上级接口,子接口有
Set
、
List
、
LinkedList
、
ArrayList
、
Vector
、
Stack
、
Set
;
阿里内部资料
Collections
是集合类的一个帮助类,
它包含有各种有关集合操作的静态多态方法,用于实现对各种
集合的搜索、排序、线程安全化等操作。此类不能实例化,就像一个工具类,服务于
Java
的
Collection
框架。
14
、
Java
的四种引用,强弱软虚
强引用
强引用是平常中使用最多的引用,强引用在程序内存不足(
OOM
)的时候也不会被回收,使用
方式:
软引用
软引用在程序内存不足时,会被回收,使用方式:
可用场景:
创建缓存的时候,创建的对象放进缓存中,当内存不足时,
JVM
就会回收早先创建
的对象。
弱引用
弱引用就是只要
JVM
垃圾回收器发现了它,就会将之回收,使用方式:
可用场景:
Java
源码中的
java.util.WeakHashMap
中的
key
就是使用弱引用,我的理解就是,
一旦我不需要某个引用,
JVM
会自动帮我处理它,这样我就不需要做其它操作。
虚引用
虚引用的回收机制跟弱引用差不多,但是它被回收之前,会被放入
ReferenceQueue
中。注意
哦,其它引用是被
JVM
回收后才被传入
ReferenceQueue
中的。由于这个机制,所以虚引用大多
被用于引用销毁前的处理工作。还有就是,虚引用创建的时候,必须带有
ReferenceQueue
,
使用例子:
String
str
=
new
String
(
"str"
);
System
.
out
.
println
(
str
);
//
注意:
wrf
这个引用也是强引用,它是指向
SoftReference
这个对象的,
//
这里的软引用指的是指向
new String("str")
的引用,也就是
SoftReference
类中
T
SoftReference
<
String
>
wrf
=
new
SoftReference
<
String
>
(
new
String
(
"str"
));
WeakReference
<
String
>
wrf
=
new
WeakReference
<
String
>
(
str
);
PhantomReference
<
String
>
prf
=
new
PhantomReference
<
String
>
(
new
String
(
"str"
),
new
ReferenceQueue
<>
());
阿里内部资料
可用场景:
对象销毁前的一些操作,比如说资源释放等。
Object.finalize()
虽然也可以做这
类动作,但是这个方式即不安全又低效
上诉所说的几类引用,都是指对象本身的引用,而不是指
Reference
的四个子类的引用
(SoftReference
等
)
。
15
、 泛型常用特点
泛型是
Java SE 1.5
之后的特性,
《
Java
核心技术》中对泛型的定义是:
“
泛型
”
意味着编写的代码可以被不同类型的对象所重用。
“
泛型
”
,顾名思义,
“
泛指的类型
”
。我们提供了泛指的概念,但具体执行的时候却可以有具体的规则
来约束,比如我们用的非常多的
ArrayList
就是个泛型类,
ArrayList
作为集合可以存放各种元素,如
Integer, String
,自定义的各种类型等,但在我们使用的时候通过具体的规则来约束,如我们可以约
束集合中只存放
Integer
类型的元素,如
使用泛型的好处?
以集合来举例,使用泛型的好处是我们不必因为添加元素类型的不同而定义不同类型的集合,如整
型集合类,浮点型集合类,字符串集合类,我们可以定义一个集合来存放整型、浮点型,字符串型
数据,而这并不是最重要的,因为我们只要把底层存储设置了
Object
即可,添加的数据全部都可向
上转型为
Object
。 更重要的是我们可以通过规则按照自己的想法控制存储的数据类型。
16
、
Java
创建对象有几种方式?
java
中提供了以下四种创建对象的方式
:
new
创建新对象
通过反射机制
采用
clone
机制
通过序列化机制
17
、有没有可能两个不相等的对象有相同的
hashcode
有可能
.
在产生
hash
冲突时
,
两个不相等的对象就会有相同的
hashcode
值
.
当
hash
冲突产生时
,
一般
有以下几种方式来处理
:
拉链法
:
每个哈希表节点都有一个
next
指针
,
多个哈希表节点可以用
next
指针构成一个单向链
表,被分配到同一个索引上的多个节点可以用这个单向链表进行存储
.
开放定址法
:
一旦发生了冲突
,
就去寻找下一个空的散列地址
,
只要散列表足够大
,
空的散列地址总
能找到
,
并将记录存入
List
<
Integer
>
iniData
=
new
ArrayList
<>
()
阿里内部资料
再哈希
:
又叫双哈希法
,
有多个不同的
Hash
函数
.
当发生冲突时
,
使用第二个
,
第三个
….
等哈希函数
计算地址
,
直到无冲突
.
18
、深拷贝和浅拷贝的区别是什么
?
浅拷贝
:
被复制对象的所有变量都含有与原来的对象相同的值
,
而所有的对其他对象的引用仍然指
向原来的对象
.
换言之
,
浅拷贝仅仅复制所考虑的对象
,
而不复制它所引用的对象
.
深拷贝
:
被复制对象的所有变量都含有与原来的对象相同的值
.
而那些引用其他对象的变量将指向
被复制过的新对象
.
而不再是原有的那些被引用的对象
.
换言之
.
深拷贝把要复制的对象所引用的
对象都复制了一遍
.
19
、
fifinal
有哪些用法
?
fifinal
也是很多面试喜欢问的地方
,
但我觉得这个问题很无聊
,
通常能回答下以下
5
点就不错了
:
被
fifinal
修饰的类不可以被继承
被
fifinal
修饰的方法不可以被重写
被
fifinal
修饰的变量不可以被改变
.
如果修饰引用
,
那么表示引用不可变
,
引用指向的内容可变
.
被
fifinal
修饰的方法
,JVM
会尝试将其内联
,
以提高运行效率
被
fifinal
修饰的常量
,
在编译阶段会存入常量池中
.
除此之外
,
编译器对
fifinal
域要遵守的两个重排序规则更好
:
在构造函数内对一个
fifinal
域的写入
,
与随后把这个被构造对象的引用赋值给一个引用变量
,
这两个操作
之间不能重排序 初次读一个包含
fifinal
域的对象的引用
,
与随后初次读这个
fifinal
域
,
这两个操作之间不
能重排序
.
20
、
static
都有哪些用法
?
所有的人都知道
static
关键字这两个基本的用法
:
静态变量和静态方法
.
也就是被
static
所修饰的变量
/
方法都属于类的静态资源
,
类实例所共享
.
除了静态变量和静态方法之外
,static
也用于静态块
,
多用于初始化操作
:
此外
static
也多用于修饰内部类
,
此时称之为静态内部类
.
最后一种用法就是静态导包
,
即
import static
.import static
是在
JDK 1.5
之后引入的新特性
,
可以用
来指定导入某个类中的静态资源
,
并且不需要使用类名
,
可以直接使用资源名
,
比如
:
public
calss PreCache
{
static
{
//
执行相关操作
}
}
阿里内部资料
21
、
3*0.1
==
0.3
返回值是什么
false,
因为有些浮点数不能完全精确的表示出来
.
22
、
a=a+b
与
a+=b
有什么区别吗
?
+=
操作符会进行隐式自动类型转换
,
此处
a+=b
隐式的将加操作的结果类型强制转换为持有结果的类
型
,
而
a=a+b
则不会自动进行类型转换
.
如:
以下代码是否有错
,
有的话怎么改?
有错误
.short
类型在进行运算时会自动提升为
int
类型
,
也就是说
s1+1
的运算结果是
int
类型
,
而
s1
是
short
类型
,
此时编译器会报错
.
正确写法:
+=
操作符会对右边的表达式结果强转匹配左边的数据类型
,
所以没错
.
23
、
try catch fifinally
,
try
里有
return
,
fifinally
还执行么?
执行,并且
fifinally
的执行早于
try
里面的
return
import static
java
.
lang
.
Math
.
*
;
public class
Test
{
public static
void
main
(
String
[]
args
){
//System.out.println(Math.sin(20));
传统做法
System
.
out
.
println
(
sin
(
20
));
}
}
byte
a
=
127
;
byte
b
=
127
;
b
=
a
+
b
;
//
报编译错误
:cannot convert from int to byte
b
+=
a
;
short
s1
=
1
;
s1
=
s1
+
1
;
short
s1
=
1
;
s1
+=
1
;
阿里内部资料
结论:
1
、不管有木有出现异常,
fifinally
块中代码都会执行;
2
、当
try
和
catch
中有
return
时,
fifinally
仍然会执行;
3
、
fifinally
是在
return
后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的
值保存起来,管
fifinally
中的代码怎么样,返回的值都不会改变,任然是之前保存的值),所以函数
返回值是在
fifinally
执行前确定的;
4
、
fifinally
中最好不要包含
return
,否则程序会提前退出,返回值不是
try
或
catch
中保存的返回值。
24
、
Excption
与
Error
包结构
Java
可抛出
(Throwable)
的结构分为三种类型:被检查的异常
(CheckedException)
,运行时异常
(RuntimeException)
,错误
(Error)
。
1
、运行时异常
定义
:RuntimeException
及其子类都被称为运行时异常。
特点
:Java
编译器不会检查它。也就是说,当程序中可能出现这类异常时,倘若既
"
没有通过
throws
声明抛出它
"
,也
"
没有用
try-catch
语句捕获它
"
,还是会编译通过。例如,除数为零时产生的
ArithmeticException
异常,数组越界时产生的
IndexOutOfBoundsException
异常,
fail-fast
机制产
生的
ConcurrentModifificationException
异常(
java.util
包下面的所有的集合类都是快速失败
的,
“
快速失败
”
也就是
fail-fast
,它是
Java
集合的一种错误检测机制。当多个线程对集合进行结构上
的改变的操作时,有可能会产生
fail-fast
机制。记住是有可能,而不是一定。例如:假设存在两个线
程(线程
1
、线程
2
),线程
1
通过
Iterator
在遍历集合
A
中的元素,在某个时候线程
2
修改了集合
A
的
结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出
ConcurrentModifificationException
异常,从而产生
fail-fast
机制,这个错叫并发修改异常。
Fail
safe
,
java.util.concurrent
包下面的所有的类都是安全失败的,在遍历过程中,如果已经遍历的数
组上的内容变化了,迭代器不会抛出
ConcurrentModifificationException
异常。如果未遍历的数组
上的内容发生了变化,则有可能反映到迭代过程中。这就是
ConcurrentHashMap
迭代器弱一致的
表现。
ConcurrentHashMap
的弱一致性主要是为了提升效率,是一致性与效率之间的一种权衡。
要成为强一致性,就得到处使用锁,甚至是全局锁,这就与
Hashtable
和同步的
HashMap
一样
了。)等,都属于运行时异常。
常见的五种运行时异常:
ClassCastException
(类转换异常)
IndexOutOfBoundsException
(数组越界)
NullPointerException
(空指针异常)
ArrayStoreException
(数据存储异常,操作数组是类型不一致)
阿里内部资料
BufffferOverflflowException
2
、被检查异常
定义
:Exception
类本身,以及
Exception
的子类中除了
"
运行时异常
"
之外的其它子类都属于被检查异
常。
特点
: Java
编译器会检查它。 此类异常,要么通过
throws
进行声明抛出,要么通过
try-catch
进行捕
获处理,否则不能通过编译。例如,
CloneNotSupportedException
就属于被检查异常。当通过
clone()
接口去克隆一个对象,而该对象对应的类没有实现
Cloneable
接口,就会抛出
CloneNotSupportedException
异常。被检查异常通常都是可以恢复的。 如:
IOException
FileNotFoundException
SQLException
被检查的异常适用于那些不是因程序引起的错误情况,比如:读取文件时文件不存在引发的
FileNotFoundException
。然而,不被检查的异常通常都是由于糟糕的编程引起的,比如:在对象
引用时没有确保对象非空而引起的
NullPointerException
。
3
、错误
定义
: Error
类及其子类。
特点
:
和运行时异常一样,编译器也不会对错误进行检查。
当资源不足、约束失败、或是其它程序无法继续运行的条件发生时,就产生错误。程序本身无法修
复这些错误的。例如,
VirtualMachineError
就属于错误。出现这种错误会导致程序终止运行。
OutOfMemoryError
、
ThreadDeath
。
Java
虚拟机规范规定
JVM
的内存分为了好几块,比如堆,栈,程序计数器,方法区等
25
、
OOM
你遇到过哪些情况,
SOF
你遇到过哪些情况
OOM
:
1
,
OutOfMemoryError
异常
除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生
OutOfMemoryError(OOM)
异常的
可能。
Java Heap
溢出:
一般的异常信息:
java.lang.OutOfMemoryError:Java heap spacess
。
java
堆用于存储对象实例,我们只要不断的创建对象,并且保证
GC Roots
到对象之间有可达路径来
避免垃圾回收机制清除这些对象,就会在对象数量达到最大堆容量限制后产生内存溢出异常。
阿里内部资料
出现这种异常,一般手段是先通过内存映像分析工具
(
如
Eclipse Memory Analyzer)
对
dump
出来的
堆转存快照进行分析,重点是确认内存中的对象是否是必要的,先分清是因为内存泄漏
(Memory
Leak)
还是内存溢出
(Memory Overflflow)
。
如果是内存泄漏,可进一步通过工具查看泄漏对象到
GCRoots
的引用链。于是就能找到泄漏对象是
通过怎样的路径与
GC Roots
相关联并导致垃圾收集器无法自动回收。
如果不存在泄漏,那就应该检查虚拟机的参数
(-Xmx
与
-Xms)
的设置是否适当。
2
,虚拟机栈和本地方法栈溢出
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出
StackOverflflowError
异常。
如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出
OutOfMemoryError
异常
这里需要注意当栈的大小越大可分配的线程数就越少。
3
,运行时常量池溢出
异常信息:
java.lang.OutOfMemoryError:PermGenspace
如果要向运行时常量池中添加内容,最简单的做法就是使用
String.intern()
这个
Native
方法。该方法
的作用是:如果池中已经包含一个等于此
String
的字符串,则返回代表池中这个字符串的
String
对
象;否则,将此
String
对象包含的字符串添加到常量池中,并且返回此
String
对象的引用。由于常量
池分配在方法区内,我们可以通过
-XX:PermSize
和
-XX:MaxPermSize
限制方法区的大小,从而间接
限制其中常量池的容量。
4
,方法区溢出
方法区用于存放
Class
的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。也有可
能是方法区中保存的
class
对象没有被及时回收掉或者
class
信息占用的内存超过了我们配置。
异常信息:
java.lang.OutOfMemoryError:PermGenspace
方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,判定条件是很苛刻
的。在经常动态生成大量
Class
的应用中,要特别注意这点。
SOF
(堆栈溢出
StackOverflflow
):
StackOverflflowError
的定义:当应用程序递归太深而发生堆栈溢出时,抛出该错误。
因为栈一般默认为
1-2m
,一旦出现死循环或者是大量的递归调用,在不断的压栈过程中,造成栈容
量超过
1m
而导致溢出。
栈溢出的原因:递归调用,大量循环或死循环,全局变量是否过多,数组、
List
、
map
数据过大。
26
、 简述线程、程序、进程的基本概念。以及他们之间关系是什
么
?
阿里内部资料
线程
与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个
线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个
线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻
量级进程。
程序
是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代
码。
进程
是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序
即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算
机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如
CPU
时间,内存空
间,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存
中。 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而
各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系
统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同
时执行一个以上的程序段。
27
、
Java
序列化中如果有些字段不想进行序列化,怎么办?
对于不想进行序列化的变量,使用
transient
关键字修饰。
transient
关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化
时,被
transient
修饰的变量值不会被持久化和恢复。
transient
只能修饰变量,不能修饰类和方
法。
28
、说说
Java
中
IO
流
Java
中
IO
流分为几种
?
按照流的流向分,可以分为输入流和输出流;
按照操作单元划分,可以划分为字节流和字符流;
按照流的角色划分为节点流和处理流。
Java Io
流共涉及
40
多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧
密的联系,
Java I0
流的
40
多个类都是从如下
4
个抽象类基类中派生出来的。
InputStream/Reader:
所有的输入流的基类,前者是字节输入流,后者是字符输入流。
OutputStream/Writer:
所有输出流的基类,前者是字节输出流,后者是字符输出流。
按操作方式分类结构图:
阿里内部资料
按操作对象分类结构图:
阿里内部资料
29
、
Java IO
与
NIO
的区别(补充)
NIO
即
New IO
,这个库是在
JDK1.4
中才引入的。
NIO
和
IO
有相同的作用和目的,但实现方式不同,
NIO
主要用到的是块,所以
NIO
的效率要比
IO
高很多。在
Java API
中提供了两套
NIO
,一套是针对标
准输入输出
NIO
,另一套就是网络编程
NIO
。
30
、
java
反射的作用于原理
1
、定义:
反射机制是在运行时,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意个对象,
都能够调用它的任意一个方法。在
java
中,只要给定类的名字,就可以通过反射机制来获得类的所
有信息。
这种动态获取的信息以及动态调用对象的方法的功能称为
Java
语言的反射机制。
2
、哪里会用到反射机制?
jdbc
就是典型的反射
Class
.
forName
(
'com.mysql.jdbc.Driver.class'
);
//
加载
MySQL
的驱动类
阿里内部资料
这就是反射。如
hibernate
,
struts
等框架使用反射实现的。
3
、反射的实现方式:
第一步:获取
Class
对象,有
4
中方法:
1
)
Class.forName(“
类的路径
”)
;
2
)类名
.class 3
)对象
名
.getClass() 4
)基本类型的包装类,可以调用包装类的
Type
属性来获得该包装类的
Class
对象
4
、实现
Java
反射的类:
1
)
Class
:表示正在运行的
Java
应用程序中的类和接口 注意:
所有获取对象的信息都需要
Class
类
来实现。
2
)
Field
:提供有关类和接口的属性信息,以及对它的动态访问权限。
3
)
Constructor
:
提供关于类的单个构造方法的信息以及它的访问权限
4
)
Method
:提供类或接口中某个方法的信息
5
、反射机制的优缺点:
优点:
1
)能够运行时动态获取类的实例,提高灵活性;
2
)与动态编译结合
缺点:
1
)使用反射
性能较低,需要解析字节码,将内存中的对象进行解析。 解决方案:
1
、通过
setAccessible(true)
关闭
JDK
的安全检查来提升反射速度;
2
、多次创建一个类的实例时,有缓存会快很多
3
、
ReflflectASM
工具类,通过字节码生成的方式加快反射速度
2
)相对不安全,破坏了封装性(因为通
过反射可以获得私有方法和属性)
31
、说说
List,Set,Map
三者的区别?
List(
对付顺序的好帮手
)
:
List
接口存储一组不唯一(可以有多个元素引用相同的对象),有序
的对象
Set(
注重独一无二的性质
):
不允许重复的集合。不会有多个元素引用相同的对象。
Map(
用
Key
来搜索的专家
):
使用键值对存储。
Map
会维护与
Key
有关联的值。两个
Key
可以引
用相同的对象,但
Key
不能重复,典型的
Key
是
String
类型,但也可以是任何对象。
32.
、
Object
有哪些常用方法?大致说一下每个方法的含义
java.lang.Object
阿里内部资料
下面是对应方法的含义。
clone
方法
保护方法,实现对象的浅复制,只有实现了
Cloneable
接口才可以调用该方法,否则抛出
CloneNotSupportedException
异常,深拷贝也需要实现
Cloneable
,同时其成员变量为引用类型
的也需要实现
Cloneable
,然后重写
clone
方法。
fifinalize
方法
该方法和垃圾收集器有关系,判断一个对象是否可以被回收的最后一步就是判断是否重写了此方
法。
equals
方法
该方法使用频率非常高。一般
equals
和
==
是不一样的,但是在
Object
中两者是一样的。子类一
般都要重写这个方法。
hashCode
方法
该方法用于哈希查找,重写了
equals
方法一般都要重写
hashCode
方法,这个方法在一些具有哈
希功能的
Collection
中用到。
一般必须满足
obj1.equals(obj2)==true
。可以推出
obj1.hashCode()==obj2.hashCode()
,但是
hashCode
相等不一定就满足
equals
。不过为了提高效率,应该尽量使上面两个条件接近等价。
JDK 1.6
、
1.7
默认是返回随机数;
JDK 1.8
默认是通过和当前线程有关的一个随机数
+
三个确定值,运用
Marsaglia’s xorshift
scheme
随机数算法得到的一个随机数。
wait
方法
阿里内部资料
配合
synchronized
使用,
wait
方法就是使当前线程等待该对象的锁,当前线程必须是该对象的拥
有者,也就是具有该对象的锁。
wait()
方法一直等待,直到获得锁或者被中断。
wait(long timeout)
设定一个超时间隔,如果在规定时间内没有获得锁就返回。
调用该方法后当前线程进入睡眠状态,直到以下事件发生。
1.
其他线程调用了该对象的
notify
方法;
2.
其他线程调用了该对象的
notifyAll
方法;
3.
其他线程调用了
interrupt
中断该线程;
4.
时间间隔到了。
此时该线程就可以被调度了,如果是被中断的话就抛出一个
InterruptedException
异常。
notify
方法
配合
synchronized
使用,该方法唤醒在该对象上
等待队列
中的某个线程(同步队列中的线程是给
抢占
CPU
的线程,等待队列中的线程指的是等待唤醒的线程)。
notifyAll
方法
配合
synchronized
使用,该方法唤醒在该对象上等待队列中的所有线程。
总结
只要把上面几个方法熟悉就可以了,
toString
和
getClass
方法可以不用去讨论它们。该题目考察的
是对
Object
的熟悉程度,平时用的很多方法并没看其定义但是也在用,比如说:
wait()
方法,
equals()
方法等。
大致意思:
Object
是所有类的根,是所有类的父类,所有对象包括数组都实现了
Object
的方法。
33
、
Java
创建对象有几种方式?
这题目看似简单,要好好回答起来还是有点小复杂的,我们来看看,到底有哪些方式可以创建对
象?
使用
new
关键字
,这也是我们平时使用的最多的创建对象的方式,示例:
使用反射方式创建对象
,使用
newInstance()
,但是得处理两个异常
InstantiationException
、
IllegalAccessException
:
Class Object is the root of the class hierarchy.Every class has Object as a
superclass. All objects, including arrays, implement the methods of this class.
User user=new User();
阿里内部资料
User user=User.class.newInstance();
Object object=(Object)Class.forName("java.lang.Object").newInstance()
使用
clone
方法
,前面题目中
clone
是
Object
的方法,所以所有对象都有这个方法。
使用反序列化创建对象
,调用
ObjectInputStream
类的
readObject()
方法。
我们反序列化一个对象,
JVM
会给我们创建一个单独的对象。
JVM
创建对象并不会调用任何构造函
数。一个对象实现了
Serializable
接口,就可以把对象写入到文件中,并通过读取文件来创建对
象。
总结
创建对象的方式关键字:
new
、反射、
clone
拷贝、反序列化。
34
、获取一个类
Class
对象的方式有哪些?
搞清楚类对象和实例对象,但都是对象。
第一种:通过类对象的
getClass()
方法获取,细心点的都知道,这个
getClass
是
Object
类里面的
方法。
第二种:通过类的静态成员表示,每个类都有隐含的静态成员
class
。
第三种:通过
Class
类的静态方法
forName()
方法获取。
35
、
ArrayList
和
LinkedList
的区别有哪些?
ArrayList
优点
:
ArrayList
是实现了基于动态数组的数据结构,因为地址连续,一旦数据存储好了,查询
操作效率会比较高(在内存里是连着放的)。
缺点
:因为地址连续,
ArrayList
要移动数据,所以插入和删除操作效率比较低。
LinkedList
User user
=
new
User
();
//clazz
就是一个
User
的类对象
Class
<?>
clazz
=
user
.
getClass
();
//clazz
就是一个
User
的类对象
Class
<?>
clazz
=
User
.
class
;
Class
<?>
clazz
=
Class
.
forName
(
"com.tian.User"
);
阿里内部资料
优点
:
LinkedList
基于链表的数据结构,地址是任意的,所以在开辟内存空间的时候不需要等
一个连续的地址。对于新增和删除操作,
LinkedList
比较占优势。
LinkedList
适用于要头尾操
作或插入指定位置的场景。
缺点
:因为
LinkedList
要移动指针,所以查询操作性能比较低。
适用场景分析
当需要对数据进行对随机访问的时候,选用
ArrayList
。
当需要对数据进行多次增加删除修改时,采用
LinkedList
。
如果容量固定,并且只会添加到尾部,不会引起扩容,优先采用
ArrayList
。
当然,绝大数业务的场景下,使用
ArrayList
就够了,但需要注意避免
ArrayList
的扩容,以及非顺
序的插入。
36
、用过
ArrayList
吗?说一下它有什么特点?
只要是搞
Java
的肯定都会回答
“
用过
”
。所以,回答题目的后半部分
——ArrayList
的特点。可以从这
几个方面去回答:
Java
集合框架中的一种存放相同类型的元素数据,是一种变长的集合类,基于定长数组实现,当加
入数据达到一定程度后,会实行自动扩容,即扩大数组大小。
底层是使用数组实现,添加元素。
如果
add(o)
,添加到的是数组的尾部,如果要增加的数据量很大,应该使用
ensureCapacity()
方法,该方法的作用是预先设置
ArrayList
的大小,这样可以大大提高初始化速度。
如果使用
add(int,o)
,添加到某个位置,那么可能会挪动大量的数组元素,并且可能会触发扩
容机制。
高并发的情况下,线程不安全。多个线程同时操作
ArrayList
,会引发不可预知的异常或错误。
ArrayList
实现了
Cloneable
接口,标识着它可以被复制。注意:
ArrayList
里面的
clone()
复制其实
是浅复制。
37
、有数组了为什么还要搞个
ArrayList
呢?
通常我们在使用的时候,如果在不明确要插入多少数据的情况下,普通数组就很尴尬了,因为你不
知道需要初始化数组大小为多少,而
ArrayList
可以使用默认的大小,当元素个数到达一定程度
后,会自动扩容。
可以这么来理解:我们常说的数组是定死的数组,
ArrayList
却是动态数组。
38
、说说什么是
fail-fast
?
阿里内部资料
fail-fast
机制是
Java
集合(
Collection
)中的一种错误机制。当多个线程对同一个集合的内容进行
操作时,就可能会产生
fail-fast
事件。
例如:当某一个线程
A
通过
iterator
去遍历某集合的过程中,若该集合的内容被其他线程所改变
了,那么线程
A
访问集合时,就会抛出
ConcurrentModifificationException
异常,产生
fail-fast
事
件。这里的操作主要是指
add
、
remove
和
clear
,对集合元素个数进行修改。
解决办法:建议使用
“java.util.concurrent
包下的类
”
去取代
“java.util
包下的类
”
。
可以这么理解:在遍历之前,把
modCount
记下来
expectModCount
,后面
expectModCount
去
和
modCount
进行比较,如果不相等了,证明已并发了,被修改了,于是抛出
ConcurrentModifificationException
异常。
39
、说说
Hashtable
与
HashMap
的区别
本来不想这么写标题的,但是无奈,面试官都喜欢这么问
HashMap
。
1.
出生的版本不一样,
Hashtable
出生于
Java
发布的第一版本
JDK 1.0
,
HashMap
出生于
JDK
1.2
。
2.
都实现了
Map
、
Cloneable
、
Serializable
(当前
JDK
版本
1.8
)。
3. HashMap
继承的是
AbstractMap
,并且
AbstractMap
也实现了
Map
接口。
Hashtable
继承
Dictionary
。
4. Hashtable
中大部分
public
修饰普通方法都是
synchronized
字段修饰的,是线程安全的,
HashMap
是非线程安全的。
5. Hashtable
的
key
不能为
null
,
value
也不能为
null
,这个可以从
Hashtable
源码中的
put
方
法看到,判断如果
value
为
null
就直接抛出空指针异常,在
put
方法中计算
key
的
hash
值之
前并没有判断
key
为
null
的情况,那说明,这时候如果
key
为空,照样会抛出空指针异常。
6. HashMap
的
key
和
value
都可以为
null
。在计算
hash
值的时候,有判断,如果
key==null
,则其
hash=0
;至于
value
是否为
null
,根本没有判断过。
7. Hashtable
直接使用对象的
hash
值。
hash
值是
JDK
根据对象的地址或者字符串或者数字算出
来的
int
类型的数值。然后再使用除留余数法来获得最终的位置。然而除法运算是非常耗费时
间的,效率很低。
HashMap
为了提高计算效率,将哈希表的大小固定为了
2
的幂,这样在取
模预算时,不需要做除法,只需要做位运算。位运算比除法的效率要高很多。
8. Hashtable
、
HashMap
都使用了
Iterator
。而由于历史原因,
Hashtable
还使用了
Enumeration
的方式。
9.
默认情况下,初始容量不同,
Hashtable
的初始长度是
11
,之后每次扩充容量变为之前的
2n+1
(
n
为上一次的长度)而
HashMap
的初始长度为
16
,之后每次扩充变为原来的两倍。
另外在
Hashtable
源码注释中有这么一句话:
阿里内部资料
Hashtable is synchronized. If a thread-safe implementation is not needed, it is
recommended to use HashMap in place of Hashtable . If a thread-safe highly
concurrent implementation is desired, then it is recommended to use
ConcurrentHashMap in place of Hashtable.
大致意思:
Hashtable
是线程安全,推荐使用
HashMap
代替
Hashtable
;如果需要线程安全高并
发的话,推荐使用
ConcurrentHashMap
代替
Hashtable
。
这个回答完了,面试官可能会继续问:
HashMap
是线程不安全的,那么在需要线程安全的情况下
还要考虑性能,有什么解决方式?
这里最好的选择就是
ConcurrentHashMap
了,但面试官肯定会叫你继续说一下
ConcurrentHashMap
数据结构以及底层原理等。
40
、
HashMap
中的
key
我们可以使用任何类作为
key
吗?
平时可能大家使用的最多的就是使用
String
作为
HashMap
的
key
,但是现在我们想使用某个自定
义类作为
HashMap
的
key
,那就需要注意以下几点:
如果类重写了
equals
方法,它也应该重写
hashCode
方法。
类的所有实例需要遵循与
equals
和
hashCode
相关的规则。
如果一个类没有使用
equals
,你不应该在
hashCode
中使用它。
咱们自定义
key
类的最佳实践是使之为不可变的,这样,
hashCode
值可以被缓存起来,拥有
更好的性能。不可变的类也可以确保
hashCode
和
equals
在未来不会改变,这样就会解决与
可变相关的问题了。
41
、
HashMap
的长度为什么是
2
的
N
次方呢?
为了能让
HashMap
存数据和取数据的效率高,尽可能地减少
hash
值的碰撞,也就是说尽量把数
据能均匀的分配,每个链表或者红黑树长度尽量相等。
我们首先可能会想到
%
取模的操作来实现。
下面是回答的重点哟:
取余(
%
)操作中如果除数是
2
的幂次,则等价于与其除数减一的与(
&
)操作(也就是说
hash % length == hash &(length
-
1)
的前提是
length
是
2
的
n
次方)。并且,采用二进
制位操作
&
,相对于
%
能够提高运算效率。
这就是为什么
HashMap
的长度需要
2
的
N
次方了。
42
、
HashMap
与
ConcurrentHashMap
的异同
1.
都是
key-value
形式的存储数据;
2. HashMap
是线程不安全的,
ConcurrentHashMap
是
JUC
下的线程安全的;
阿里内部资料
3. HashMap
底层数据结构是数组
+
链表(
JDK 1.8
之前)。
JDK 1.8
之后是数组
+
链表
+
红黑
树。当链表中元素个数达到
8
的时候,链表的查询速度不如红黑树快,链表会转为红黑树,红
黑树查询速度快;
4. HashMap
初始数组大小为
16
(默认),当出现扩容的时候,以
0.75 *
数组大小的方式进行扩
容;
5. ConcurrentHashMap
在
JDK 1.8
之前是采用分段锁来现实的
Segment + HashEntry
,
Segment
数组大小默认是
16
,
2
的
n
次方;
JDK 1.8
之后,采用
Node + CAS + Synchronized
来保证并发安全进行实现。
43
、红黑树有哪几个特征?
紧接上个问题,面试官很有可能会问红黑树,下面把红黑树的几个特征列出来:
44
、说说你平时是怎么处理
Java
异常的
try-catch-fifinally
try
块负责监控可能出现异常的代码
catch
块负责捕获可能出现的异常,并进行处理
fifinally
块负责清理各种资源,不管是否出现异常都会执行
其中
try
块是必须的,
catch
和
fifinally
至少存在一个标准异常处理流程
阿里内部资料
抛出异常
→
捕获异常
→
捕获成功(当
catch
的异常类型与抛出的异常类型匹配时,捕获成功)
→
异常被处理,程序继续运行 抛出异常
→
捕获异常
→
捕获失败(当
catch
的异常类型与抛出异
常类型不匹配时,捕获失败)
→
异常未被处理,程序中断运行
在开发过程中会使用到自定义异常,在通常情况下,程序很少会自己抛出异常,因为异常的类名通
常也包含了该异常的有用信息,所以在选择抛出异常的时候,应该选择合适的异常类,从而可以明
确地描述该异常情况,所以这时候往往都是自定义异常。
自定义异常通常是通过继承
java.lang.Exception
类,如果想自定义
Runtime
异常的话,可以继承
java.lang.RuntimeException
类,实现一个无参构造和一个带字符串参数的有参构造方法。
在业务代码里,可以针对性的使用自定义异常。比如说:该用户不具备某某权限、余额不足等。
45
、说说深拷贝和浅拷贝?
浅拷贝(
shallowCopy
)只是增加了一个指针指向已存在的内存地址,
深拷贝(
deepCopy
)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新
的内存,
使用深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同一个内存的错误。
最好是结合克隆已经原型模式联系在一起哈,记得复习的时候,把这几个联系起来的。
欢迎关注微信公众号:
Java
后端技术全栈
JVM
篇
1
、知识点汇总
阿里内部资料
JVM
是
Java
运行基础
,
面试时一定会遇到
JVM
的有关问题
,
内容相对集中
,
但对只是深度要求较高
.
其中内存模型
,
类加载机制
,GC
是重点方面
.
性能调优部分更偏向应用
,
重点突出实践能力
.
编译器优化
和执行模式部分偏向于理论基础
,
重点掌握知识点
.
需了解
内存模型
各部分作用
,
保存哪些数据
.
类加载
双亲委派加载机制
,
常用加载器分别加载哪种类型的类
.
GC
分代回收的思想和依据以及不同垃圾回收算法的回收思路和适合场景
.
性能调优
常有
JVM
优化参数作用
,
参数调优的依据
,
常用的
JVM
分析工具能分析哪些问题以及使用方法
.
执行模式
解释
/
编译
/
混合模式的优缺点
,Java7
提供的分层编译技术
,JIT
即时编译技术
,OSR
栈上替
换
,C1/C2
编译器针对的场景
,C2
针对的是
server
模式
,
优化更激进
.
新技术方面
Java10
的
graal
编译器
编译器优化
j
avac
的编译过程
,ast
抽象语法树
,
编译器优化和运行器优化
.
2
、知识点详解:
1
、
JVM
内存模型:
阿里内部资料
线程独占
:
栈
,
本地方法栈
,
程序计数器 线程共享
:
堆
,
方法区
2
、栈:
又称方法栈
,
线程私有的
,
线程执行方法是都会创建一个栈阵
,
用来存储局部变量表
,
操作栈
,
动态链接
,
方
法出口等信息
.
调用方法时执行入栈
,
方法返回式执行出栈
.
3
、本地方法栈
与栈类似
,
也是用来保存执行方法的信息
.
执行
Java
方法是使用栈
,
执行
Native
方法时使用本地方法栈
.
4
、程序计数器
保存着当前线程执行的字节码位置
,
每个线程工作时都有独立的计数器
,
只为执行
Java
方法服务
,
执行
Native
方法时
,
程序计数器为空
.
5
、堆
JVM
内存管理最大的一块
,
对被线程共享
,
目的是存放对象的实例
,
几乎所欲的对象实例都会放在这里
,
当堆没有可用空间时
,
会抛出
OOM
异常
.
根据对象的存活周期不同
,JVM
把对象进行分代管理
,
由垃圾回
收器进行垃圾的回收管理
6
、方法区:
又称非堆区
,
用于存储已被虚拟机加载的类信息
,
常量
,
静态变量
,
即时编译器优化后的代码等数据
.1.7
的永久代和
1.8
的元空间都是方法区的一种实现
7
、
JVM
内存可见性
阿里内部资料
JMM
是定义程序中变量的访问规则
,
线程对于变量的操作只能在自己的工作内存中进行
,
而不能直接对
主内存操作
.
由于指令重排序
,
读写的顺序会被打乱
,
因此
JMM
需要提供原子性
,
可见性
,
有序性保证
.
3
、说说类加载与卸载
加载过程
其中
验证
,
准备
,
解析
合称链接
加载
通过类的完全限定名
,
查找此类字节码文件
,
利用字节码文件创建
Class
对象
.
验证
确保
Class
文件符合当前虚拟机的要求
,
不会危害到虚拟机自身安全
.
准备
进行内存分配
,
为
static
修饰的类变量分配内存
,
并设置初始值
(0
或
null).
不包含
fifinal
修饰的静态变
量
,
因为
fifinal
变量在编译时分配
.
解析
将常量池中的符号引用替换为直接引用的过程
.
直接引用为直接指向目标的指针或者相对偏移量
等
.
初始化
主要完成静态块执行以及静态变量的赋值
.
先初始化父类
,
再初始化当前类
.
只有对类主动使用
时才会初始化
.
触发条件包括
,
创建类的实例时
,
访问类的静态方法或静态变量的时候
,
使用
Class.forName
反射类的时
候
,
或者某个子类初始化的时候
.
Java
自带的加载器加载的类
,
在虚拟机的生命周期中是不会被卸载的
,
只有用户自定义的加载器加载的
类才可以被卸
.
阿里内部资料
1
、加载机制
-
双亲委派模式
双亲委派模式
,
即加载器加载类时先把请求委托给自己的父类加载器执行
,
直到顶层的启动类加载器
.
父类加载器能够完成加载则成功返回
,
不能则子类加载器才自己尝试加载
.*
优点
:
1.
避免类的重复加载
2.
避免
Java
的核心
API
被篡改
2
、分代回收
分代回收基于两个事实
:
大部分对象很快就不使用了
,
还有一部分不会立即无用
,
但也不会持续很长时
间
.
年轻代
->
标记
-
复制 老年代
->
标记
-
清除
3
、回收算法
阿里内部资料
a
、
G1
算法
1.9
后默认的垃圾回收算法
,
特点保持高回收率的同时减少停顿
.
采用每次只清理一部分
,
而不是清理全
部的增量式清理
,
以保证停顿时间不会过长
其取消了年轻代与老年代的物理划分
,
但仍属于分代收集器
,
算法将堆分为若干个逻辑区域
(region),
一
部分用作年轻代
,
一部分用作老年代
,
还有用来存储巨型对象的分区
.
同
CMS
相同
,
会遍历所有对象
,
标记引用情况
,
清除对象后会对区域进行复制移动
,
以整合碎片空间
.
年轻代回收
:
并行复制采用复制算法
,
并行收集
,
会
StopTheWorld.
老年代回收
:
会对年轻代一并回收
初始标记完成堆
root
对象的标记
,
会
StopTheWorld.
并发标记
GC
线程和应用线程并发执行
.
最终标记
完成三色标记周期
,
会
StopTheWorld.
复制
/
清楚会优先对可回收空间加大的区域进行回收
b
、
ZGC
算法
前面提供的高效垃圾回收算法
,
针对大堆内存设计
,
可以处理
TB
级别的堆
,
可以做到
10ms
以下的回收停
顿时间
.
着色指针
读屏障
并发处理
基于
region
内存压缩
(
整理
)
roots
标记:标记
root
对象
,
会
StopTheWorld.
并发标记:利用读屏障与应用线程一起运行标记
,
可能
会发生
StopTheWorld.
清除会清理标记为不可用的对象
. roots
重定位:是对存活的对象进行移动
,
以
腾出大块内存空间
,
减少碎片产生
.
重定位最开始会
StopTheWorld,
却决于重定位集与对象总活动集的
比例
.
并发重定位与并发标记类似
.
4
、简述一下
JVM
的内存模型
阿里内部资料
1.JVM
内存模型简介
JVM
定义了不同运行时数据区,他们是用来执行应用程序的。某些区域随着
JVM
启动及销毁,另外一
些区域的数据是线程性独立的,随着线程创建和销毁。
jvm
内存模型总体架构图如下:(摘自
oracle
官方网站
)
JVM
在执行
Java
程序时,会把它管理的内存划分为若干个的区域,每个区域都有自己的用途和创建
销毁时间。如下图所示,可以分为两大部分,线程私有区和共享区。下图是根据自己理解画的一个
JVM
内存模型架构图:
阿里内部资料
JVM
内存分为线程私有区和线程共享区
线程私有区
1
、程序计数器
当同时进行的线程数超过
CPU
数或其内核数时,就要通过时间片轮询分派
CPU
的时间资源,不免发
生线程切换。这时,每个线程就需要一个属于自己的计数器来记录下一条要运行的指令。如果执行
的是
JAVA
方法,计数器记录正在执行的
java
字节码地址,如果执行的是
native
方法,则计数器为
空。
2
、虚拟机栈
线程私有的,与线程在同一时间创建。管理
JAVA
方法执行的内存模型。每个方法执行时都会创建一
个桢栈来存储方法的的变量表、操作数栈、动态链接方法、返回值、返回地址等信息。栈的大小决
定了方法调用的可达深度(递归多少层次,或嵌套调用多少层其他方法,
-Xss
参数可以设置虚拟机
栈大小)。栈的大小可以是固定的,或者是动态扩展的。如果请求的栈深度大于最大可用深度,则
抛出
stackOverflflowError
;如果栈是可动态扩展的,但没有内存空间支持扩展,则抛出
OutofMemoryError
。 使用
jclasslib
工具可以查看
class
类文件的结构。下图为栈帧结构图:
阿里内部资料
3
、本地方法栈
与虚拟机栈作用相似。但它不是为
Java
方法服务的,而是本地方法(
C
语言)。由于规范对这块没有
强制要求,不同虚拟机实现方法不同。
线程共享区
1
、方法区
线程共享的,用于存放被虚拟机加载的类的元数据信息,如常量、静态变量和即时编译器编译后的
代码。若要分代,算是永久代(老年代),以前类大多
“static”
的,很少被卸载或收集,现回收废弃
常量和无用的类。其中运行时常量池存放编译生成的各种常量。(如果
hotspot
虚拟机确定一个类
的定义信息不会被使用,也会将其回收。回收的基本条件至少有:所有该类的实例被回收,而且装
载该类的
ClassLoader
被回收)
2
、堆
存放对象实例和数组,是垃圾回收的主要区域,分为新生代和老年代。刚创建的对象在新生代的
Eden
区中,经过
GC
后进入新生代的
S0
区中,再经过
GC
进入新生代的
S1
区中,
15
次
GC
后仍存在就
进入老年代。这是按照一种回收机制进行划分的,不是固定的。若堆的空间不够实例分配,则
OutOfMemoryError
。
阿里内部资料
Young Generation
即图中的
Eden + From Space
(
s0
)
+ To Space(s1)
Eden
存放新生的对象
Survivor Space
有两个,存放每次垃圾回收后存活的对象
(s0+s1)
Old Generation Tenured Generation
即图中的
Old Space
主要存放应用程序中生命周期长的存活对象
5
、说说堆和栈的区别
栈是运行时单位,代表着逻辑,内含基本数据类型和堆中对象引用,所在区域连续,没有碎片;堆
是存储单位,代表着数据,可被多个栈共享(包括成员中基本数据类型、引用和引用对象),所在
区域不连续,会有碎片。
1
、功能不同
栈内存用来存储局部变量和方法调用,而堆内存用来存储
Java
中的对象。无论是成员变量,局部变
量,还是类变量,它们指向的对象都存储在堆内存中。
2
、共享性不同
栈内存是线程私有的。 堆内存是所有线程共有的。
3
、异常错误不同
如果栈内存或者堆内存不足都会抛出异常。 栈空间不足:
java.lang.StackOverFlowError
。 堆空间
不足:
java.lang.OutOfMemoryError
。
4
、空间大小
栈的空间大小远远小于堆的。
6
、 什么时候会触发
FullGC
除直接调用
System.gc
外,触发
Full GC
执行的情况有如下四种。
1.
旧生代空间不足
旧生代空间只有
在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行
Full GC
后空间仍然不
足,则抛出如下错误:
java.lang.OutOfMemoryError: Java heap space
为避免以上两种状况引起
的
FullGC
,调优时应尽量做到让对象在
Minor GC
阶段被回收、让对象在新生代多存活一段时间及不
要创建过大的对象及数组。
2. Permanet Generation
空间满
PermanetGeneration
中存放的为一些
class
的信息等,当系统中
要加载的类、反射的类和调用的方法较多时,
Permanet Generation
可能会被占满,在未配置为采
用
CMS GC
的情况下会执行
Full GC
。如果经过
Full GC
仍然回收不了,那么
JVM
会抛出如下错误信
息:
java.lang.OutOfMemoryError: PermGen space
为避免
Perm Gen
占满造成
Full GC
现象,可
采用的方法为增大
Perm Gen
空间或转为使用
CMS GC
。
阿里内部资料
3. CMS GC
时出现
promotion failed
和
concurrent mode failure
对于采用
CMS
进行旧生代
GC
的
程序而言,尤其要注意
GC
日志中是否有
promotion failed
和
concurrent mode failure
两种状况,当
这两种状况出现时可能会触发
Full GC
。
promotionfailed
是在进行
Minor GC
时,
survivor space
放
不下、对象只能放入旧生代,而此时旧生代也放不下造成的;
concurrent mode failure
是在执行
CMS GC
的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。 应对措施为:增大
survivorspace
、旧生代空间或调低触发并发
GC
的比率,但在
JDK 5.0+
、
6.0+
的版本中有可能会由
于
JDK
的
bug29
导致
CMS
在
remark
完毕后很久才触发
sweeping
动作。对于这种状况,可通过设置
-
XX:CMSMaxAbortablePrecleanTime=5
(单位为
ms
)来避免。
4.
统计得到的
Minor GC
晋升到旧生代的平均大小大于旧生代的剩余空间
这是一个较为复杂的触发
情况,
Hotspot
为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行
Minor
GC
时,做了一个判断,如果之前统计所得到的
Minor GC
晋升到旧生代的平均大小大于旧生代的剩
余空间,那么就直接触发
Full GC
。 例如程序第一次触发
MinorGC
后,有
6MB
的对象晋升到旧生
代,那么当下一次
Minor GC
发生时,首先检查旧生代的剩余空间是否大于
6MB
,如果小于
6MB
,
则执行
Full GC
。 当新生代采用
PSGC
时,方式稍有不同,
PS GC
是在
Minor GC
后也会检查,例如上
面的例子中第一次
Minor GC
后,
PS GC
会检查此时旧生代的剩余空间是否大于
6MB
,如小于,则触
发对旧生代的回收。 除了以上
4
种状况外,对于使用
RMI
来进行
RPC
或管理的
Sun JDK
应用而言,默
认情况下会一小时执行一次
Full GC
。可通过在启动时通过
- java
Dsun.rmi.dgc.client.gcInterval=3600000
来设置
Full GC
执行的间隔时间或通过
-XX:+
DisableExplicitGC
来禁止
RMI
调用
System.gc
。
7
、什么是
Java
虚拟机?为什么
Java
被称作是
“
平台无关的编程语
言
”
?
Java
虚拟机是一个可以执行
Java
字节码的虚拟机进程。
Java
源文件被编译成能被
Java
虚拟机执行的字
节码文件。
Java
被设计成允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独
重写或者是重新编译。
Java
虚拟机让这个变为可能,因为它知道底层硬件平台的指令长度和其他特
性。
8
、
Java
内存结构
阿里内部资料
方法区和对是所有线程共享的内存区域;而
java
栈、本地方法栈和程序员计数器是运行是线程私有
的内存区域。
Java
堆(
Heap
)
,
是
Java
虚拟机所管理的内存中最大的一块。
Java
堆是被所有线程共享的一块内
存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实
例都在这里分配内存。
方法区(
Method Area
)
,
方法区(
Method Area
)与
Java
堆一样,是各个线程共享的内存区
域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数
据。
程序计数器(
Program Counter Register
)
,
程序计数器(
Program Counter Register
)是一块
较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。
JVM
栈(
JVM Stacks
)
,
与程序计数器一样,
Java
虚拟机栈(
Java Virtual Machine Stacks
)也是
线程私有的,它的生命周期与线程相同。虚拟机栈描述的是
Java
方法执行的内存模型:每个方
法被执行的时候都会同时创建一个栈帧(
Stack Frame
)用于存储局部变量表、操作栈、动态
阿里内部资料
链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机
栈中从入栈到出栈的过程。
本地方法栈(
Native Method Stacks
)
,
本地方法栈(
Native Method Stacks
)与虚拟机栈所发
挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行
Java
方法(也就是字节码)服
务,而本地方法栈则是为虚拟机使用到的
Native
方法服务。
9
、说说对象分配规则
对象优先分配在
Eden
区,如果
Eden
区没有足够的空间时,虚拟机执行一次
Minor GC
。
大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在
Eden
区和两个
Survivor
区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了
1
次
Minor GC
那么对象会进入
Survivor
区,之后每经过一次
Minor GC
那么对象的年龄加
1
,知道达
到阀值对象进入老年区。
动态判断对象的年龄。如果
Survivor
区中相同年龄的所有对象大小的总和大于
Survivor
空间的
一半,年龄大于或等于该年龄的对象可以直接进入老年代。
空间分配担保。每次进行
Minor GC
时,
JVM
会计算
Survivor
区移至老年区的对象的平均大小,
如果这个值大于老年区的剩余值大小则进行一次
Full GC
,如果小于检查
HandlePromotionFailure
设置,如果
true
则只进行
Monitor GC,
如果
false
则进行
Full GC
。
10
、描述一下
JVM
加载
class
文件的原理机制?
JVM
中类的装载是由类加载器(
ClassLoader
)和它的子类来实现的,
Java
中的类加载器是一个重要
的
Java
运行时系统组件,它负责在运行时查找和装入类文件中的类。 由于
Java
的跨平台性,经过编
译的
Java
源程序并不是一个可执行程序,而是一个或多个类文件。当
Java
程序需要使用某个类时,
JVM
会确保这个类已经被加载、连接(验证、准备和解析)和初始化。类的加载是指把类的
.class
文
件中的数据读入到内存中,通常是创建一个字节数组读入
.class
文件,然后产生与所加载类对应的
Class
对象。加载完成后,
Class
对象还不完整,所以此时的类还不可用。当类被加载后就进入连接
阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用
替换为直接引用)三个步骤。最后
JVM
对类进行初始化,包括:
1)
如果类存在直接的父类并且这个
类还没有被初始化,那么就先初始化父类;
2)
如果类中存在初始化语句,就依次执行这些初始化语
句。 类的加载是由类加载器完成的,类加载器包括:根加载器(
BootStrap
)、扩展加载器
(
Extension
)、系统加载器(
System
)和用户自定义类加载器(
java.lang.ClassLoader
的子
类)。从
Java 2
(
JDK 1.2
)开始,类加载过程采取了父亲委托机制(
PDM
)。
PDM
更好的保证了
Java
平台的安全性,在该机制中,
JVM
自带的
Bootstrap
是根加载器,其他的加载器都有且仅有一个
父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加
载。
JVM
不会向
Java
程序提供对
Bootstrap
的引用。下面是关于几个类加载器的说明:
Bootstrap
:一般用本地代码实现,负责加载
JVM
基础核心类库(
rt.jar
);
Extension
:从
java.ext.dirs
系统属性所指定的目录中加载类库,它的父加载器是
Bootstrap
;
阿里内部资料
System
:又叫应用类加载器,其父类是
Extension
。它是应用最广泛的类加载器。它从环境变
量
classpath
或者系统属性
java.class.path
所指定的目录中记载类,是用户自定义加载器的默认
父加载器。
11
、说说
Java
对象创建过程
1.JVM
遇到一条新建对象的指令时首先去检查这个指令的参数是否能在常量池中定义到一个类的符
号引用。然后加载这个类(类加载过程在后边讲)
2.
为对象分配内存。一种办法
“
指针碰撞
”
、一种办法
“
空闲列表
”
,最终常用的办法
“
本地线程缓冲分
配
(TLAB)”
3.
将除对象头外的对象内存空间初始化为
0
4.
对对象头进行必要设置
12
、知道类的生命周期吗?
类的生命周期包括这几个部分,加载、连接、初始化、使用和卸载,其中前三部是类的加载的过程
,
如下图;
加载,查找并加载类的二进制数据,在
Java
堆中也创建一个
java.lang.Class
类的对象
连接,连接又包含三块内容:验证、准备、初始化。
1
)验证,文件格式、元数据、字节码、
符号引用验证;
2
)准备,为类的静态变量分配内存,并将其初始化为默认值;
3
)解析,把
类中的符号引用转换为直接引用
初始化,为类的静态变量赋予正确的初始值
使用,
new
出对象程序中使用
卸载,执行垃圾回收
13
、简述
Java
的对象结构
Java
对象由三个部分组成:对象头、实例数据、对齐填充。
阿里内部资料
对象头由两部分组成,第一部分存储对象自身的运行时数据:哈希码、
GC
分代年龄、锁标识状态、
线程持有的锁、偏向线程
ID
(一般占
32/64 bit
)。第二部分是指针类型,指向对象的类元数据类型
(即对象代表哪个类)。如果是数组对象,则对象头中还有一部分用来记录数组长度。
实例数据用来存储对象真正的有效信息(包括父类继承下来的和自己定义的)
对齐填充:
JVM
要求对象起始地址必须是
8
字节的整数倍(
8
字节对齐)
14
、如何判断对象可以被回收?
判断对象是否存活一般有两种方式:
引用计数:每个对象有一个引用计数属性,新增一个引用时计数加
1
,引用释放时计数减
1
,计
数为
0
时可以回收。此方法简单,无法解决对象相互循环引用的问题。
可达性分析(
Reachability Analysis
):从
GC Roots
开始向下搜索,搜索所走过的路径称为引
用链。当一个对象到
GC Roots
没有任何引用链相连时,则证明此对象是不可用的,不可达对
象。
15
、
JVM
的永久代中会发生垃圾回收么?
垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收
(Full
GC)
。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确
的永久代大小对避免
Full GC
是非常重要的原因。请参考下
Java8
:从永久代到元数据区
(
注:
Java8
中已经移除了永久代,新加了一个叫做元数据区的
native
内存区
)
16
、你知道哪些垃圾收集算法
GC
最基础的算法有三种:
标记
-
清除算法、复制算法、标记
-
压缩算法,我们常用的垃圾回收器一般
都采用分代收集算法。
标记
-
清除算法,
“
标记
-
清除
”
(
Mark-Sweep
)算法,如它的名字一样,算法分为
“
标记
”
和
“
清
除
”
两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
复制算法,
“
复制
”
(
Copying
)的收集算法,它将可用内存按容量划分为大小相等的两块,每次
只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后
再把已使用过的内存空间一次清理掉。
标记
-
压缩算法,标记过程仍然与
“
标记
-
清除
”
算法一样,但后续步骤不是直接对可回收对象进行
清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
分代收集算法,
“
分代收集
”
(
Generational Collection
)算法,把
Java
堆分为新生代和老年代,
这样就可以根据各个年代的特点采用最适当的收集算法。
17
、调优命令有哪些?
Sun JDK
监控和故障处理命令有
jps jstat jmap jhat jstack jinfo
阿里内部资料
jps
,
JVM Process Status Tool,
显示指定系统内所有的
HotSpot
虚拟机进程。
jstat
,
JVM statistics Monitoring
是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟
机进程中的类装载、内存、垃圾收集、
JIT
编译等运行数据。
jmap
,
JVM Memory Map
命令用于生成
heap dump
文件
jhat
,
JVM Heap Analysis Tool
命令是与
jmap
搭配使用,用来分析
jmap
生成的
dump
,
jhat
内
置了一个微型的
HTTP/HTML
服务器,生成
dump
的分析结果后,可以在浏览器中查看
jstack
,用于生成
java
虚拟机当前时刻的线程快照。
jinfo
,
JVM Confifiguration info
这个命令作用是实时查看和调整虚拟机运行参数。
18
、常见调优工具有哪些
常用调优工具分为两类
,jdk
自带监控工具:
jconsole
和
jvisualvm
,第三方有:
MAT(Memory
Analyzer Tool)
、
GChisto
。
jconsole
,
Java Monitoring and Management Console
是从
java5
开始,在
JDK
中自带的
java
监
控和管理控制台,用于对
JVM
中内存,线程和类等的监控
jvisualvm
,
jdk
自带全能工具,可以分析内存快照、线程快照;监控内存变化、
GC
变化等。
MAT
,
Memory Analyzer Tool
,一个基于
Eclipse
的内存分析工具,是一个快速、功能丰富的
Java heap
分析工具,它可以帮助我们查找内存泄漏和减少内存消耗
GChisto
,一款专业分析
gc
日志的工具
19
、
Minor GC
与
Full GC
分别在什么时候发生?
新生代内存不够用时候发生
MGC
也叫
YGC
,
JVM
内存不够的时候发生
FGC
20
、你知道哪些
JVM
性能调优参数?(简单版回答)
设定堆内存大小
-Xmx
:堆内存最大限制。
设定新生代大小。 新生代不宜太小,否则会有大量对象涌入老年代
-XX:NewSize
:新生代大小
-XX:NewRatio
新生代和老生代占比
-XX:SurvivorRatio
:伊甸园空间和幸存者空间的占比
设定垃圾回收器 年轻代用
-XX:+UseParNewGC
年老代用
-XX:+UseConcMarkSweepGC
21
、 对象一定分配在堆中吗?有没有了解逃逸分析技术?
「对象一定分配在堆中吗?」
不一定的,
JVM
通过
「逃逸分析」
,那些逃不出方法的对象会在栈上
分配。
阿里内部资料
「什么是逃逸分析?」
逃逸分析
(Escape Analysis)
,是一种可以有效减少
Java
程序中同步负载和内存堆分配压力的跨函数
全局数据流分析算法。通过逃逸分析,
Java Hotspot
编译器能够分析出一个新的对象的引用的使用
范围,从而决定是否要将这个对象分配到堆上。
逃逸分析
是指分析指针动态范围的方法,它同编译器优化原理的指针分析和外形分析相关联。当变
量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他方法或者
线程所引用,这种现象称作指针(或者引用)的逃逸
(Escape)
。通俗点讲,如果一个对象的指针被
多个方法或者线程引用时,那么我们就称这个对象的指针发生了逃逸。
「逃逸分析的好处」
栈上分配,可以降低垃圾收集器运行的频率。
同步消除,如果发现某个对象只能从一个线程可访问,那么在这个对象上的操作可以不需要同
步。
标量替换,把对象分解成一个个基本类型,并且内存分配不再是分配在堆上,而是分配在栈
上。这样的好处有,一、减少内存使用,因为不用生成对象头。二、程序内存回收效率高,并
且
GC
频率也会减少。
22
、虚拟机为什么使用元空间替换了永久代?
「什么是元空间?什么是永久代?为什么用元空间代替永久代?」
我们先回顾一下
「方法区」
吧
,
看
看虚拟机运行时数据内存图,如下
:
阿里内部资料
方法区和堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、
静态变量、即时编译后的代码等数据。
「什么是永久代?它和方法区有什么关系呢?」
如果在
HotSpot
虚拟机上开发、部署,很多程序员都把方法区称作永久代。可以说方法区是规
范,永久代是
Hotspot
针对该规范进行的实现。在
Java7
及以前的版本,方法区都是永久代实现
的。
「什么是元空间?它和方法区有什么关系呢?」
对于
Java8
,
HotSpots
取消了永久代,取而代之的是元空间
(Metaspace)
。换句话说,就是方
法区还是在的,只是实现变了,从永久代变为元空间了。
「为什么使用元空间替换了永久代?」
永久代的方法区,和堆使用的物理内存是连续的。
阿里内部资料
「永久代」
是通过以下这两个参数配置大小的
~
-XX:PremSize
:设置永久代的初始大小
-XX:MaxPermSize:
设置永久代的最大值,默认是
64M
对于
「永久代」
,如果动态生成很多
class
的话,就很可能出现
「
java.lang.OutOfMemoryError:
PermGen space
错误」
,因为永久代空间配置有限嘛。最典型的场景是,在
web
开发比较多
jsp
页
面的时候。
JDK8
之后,方法区存在于元空间
(Metaspace)
。物理内存不再与堆连续,而是直接存在于本地
内存中,理论上机器
「内存有多大,元空间就有多大」
。
阿里内部资料
可以通过以下的参数来设置元空间的大小:
-XX:MetaspaceSize
,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时
GC
会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,
那么在不超过
MaxMetaspaceSize
时,适当提高该值。
-XX:MaxMetaspaceSize
,最大空间,默认是没有限制的。
-XX:MinMetaspaceFreeRatio
,在
GC
之后,最小的
Metaspace
剩余空间容量的百分比,
减少为分配空间所导致的垃圾收集
-XX:MaxMetaspaceFreeRatio
,在
GC
之后,最大的
Metaspace
剩余空间容量的百分比,
减少为释放空间所导致的垃圾收集
「所以,为什么使用元空间替换永久代?」
表面上看是为了避免
OOM
异常。因为通常使用
PermSize
和
MaxPermSize
设置永久代的大小就
决定了永久代的上限,但是不是总能知道应该设置为多大合适
,
如果使用默认值很容易遇到
OOM
错误。当使用元空间时,可以加载多少类的元数据就不再由
MaxPermSize
控制
,
而由系统
的实际可用空间来控制啦。
23
、什么是
Stop The World ?
什么是
OopMap
?什么是安全
点?
阿里内部资料
进行垃圾回收的过程中,会涉及对象的移动。为了保证对象引用更新的正确性,必须暂停所有的用
户线程,像这样的停顿,虚拟机设计者形象描述为
「
Stop The World
」
。也简称为
STW
。
在
HotSpot
中,有个数据结构(映射表)称为
「
OopMap
」
。一旦类加载动作完成的时候,
HotSpot
就会把对象内什么偏移量上是什么类型的数据计算出来,记录到
OopMap
。在即时编译过
程中,也会在
「特定的位置」
生成
OopMap
,记录下栈上和寄存器里哪些位置是引用。
这些特定的位置主要在:
1.
循环的末尾(非
counted
循环)
2.
方法临返回前
/
调用方法的
call
指令后
3.
可能抛异常的位置
这些位置就叫作
「安全点
(safepoint)
。」
用户程序执行时并非在代码指令流的任意位置都能够在
停顿下来开始垃圾收集,而是必须是执行到安全点才能够暂停。
24
、说一下
JVM
的主要组成部分及其作用?
JVM
包含两个子系统和两个组件,分别为
Class loader(
类装载子系统
)
Execution engine(
执行引擎子系统
)
;
Runtime data area(
运行时数据区组件
)
Native Interface(
本地接口组件
)
。
「
Class loader(
类装载
)
:」
根据给定的全限定名类名
(
如:
java.lang.Object)
来装载
class
文件
到运行时数据区的方法区中。
阿里内部资料
「
Execution engine
(执行引擎)」
:执行
class
的指令。
「
Native Interface(
本地接口
)
:」
与
native lib
交互,是其它编程语言交互的接口。
「
Runtime data area(
运行时数据区域
)
」
:即我们常说的
JVM
的内存。
首先通过编译器把
Java
源代码转换成字节码,
Class loader(
类装载
)
再把字节码加载到内存
中,将其放在运行时数据区的方法区内,而字节码文件只是
JVM
的一套指令集规范,并不能直
接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(
Execution Engine
),将
字节码翻译成底层系统指令,再交由
CPU
去执行,而这个过程中需要调用其他语言的本地库
接口(
Native Interface
)来实现整个程序的功能。
25
、什么是指针碰撞?
一般情况下,
JVM
的对象都放在堆内存中(发生逃逸分析除外)。当类加载检查通过后,
Java
虚拟
机开始为新生对象分配内存。如果
Java
堆中内存是绝对规整的,所有被使用过的的内存都被放到一
边,空闲的内存放到另外一边,中间放着一个指针作为分界点的指示器,所分配内存仅仅是把那个
指针向空闲空间方向挪动一段与对象大小相等的实例,这种分配方式就是 指针碰撞。
26
,什么是空闲列表?
如果
Java
堆内存中的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,不可以进
行指针碰撞啦,虚拟机必须维护一个列表,记录哪些内存是可用的,在分配的时候从列表找到一块
大的空间分配给对象实例,并更新列表上的记录,这种分配方式就是空闲列表。
27
,什么是
TLAB
?
可以把内存分配的动作按照线程划分在不同的空间之中进行,每个线程在
Java
堆中预先分配一小块
内存
,
这就是
TLAB
(
Thread Local Allocation Buffffer
,本地线程分配缓存)
。虚拟机通过
-
XX:UseTLAB
设定它的。
28
、对象头具体都包含哪些内容?
在我们常用的
Hotspot
虚拟机中,对象在内存中布局实际包含
3
个部分:
1.
对象头
阿里内部资料
2.
实例数据
3.
对齐填充
而对象头包含两部分内容,
Mark Word
中的内容会随着锁标志位而发生变化,所以只说存储结构就
好了。
1.
对象自身运行时所需的数据,也被称为
Mark Word
,也就是用于轻量级锁和偏向锁的关键点。
具体的内容包含对象的
hashcode
、分代年龄、轻量级锁指针、重量级锁指针、
GC
标记、偏向
锁线程
ID
、偏向锁时间戳。
2.
存储类型指针,也就是指向类的元数据的指针,通过这个指针才能确定对象是属于哪个类的实
例。
如果是数组的话,则还包含了数组的长度。
29
、你知道哪些
JVM
调优参数?
「堆栈内存相关」
-Xms
设置初始堆的大小
-Xmx
设置最大堆的大小
-Xmn
设置年轻代大小,相当于同时配置
-XX:NewSize
和
-XX:MaxNewSize
为一样的值
-Xss
每个线程的堆栈大小
-XX:NewSize
设置年轻代大小
(for 1.3/1.4)
-XX:MaxNewSize
年轻代最大值
(for 1.3/1.4)
-XX:NewRatio
年轻代与年老代的比值
(
除去持久代
)
-XX:SurvivorRatio Eden
区与
Survivor
区的的比值
-XX:PretenureSizeThreshold
当创建的对象超过指定大小时,直接把对象分配在老年代。
-XX:MaxTenuringThreshold
设定对象在
Survivor
复制的最大年龄阈值,超过阈值转移到
老年代
阿里内部资料
「垃圾收集器相关」
-XX:+UseParallelGC
:选择垃圾收集器为并行收集器。
-XX:ParallelGCThreads=20
:配置并行收集器的线程数
-XX:+UseConcMarkSweepGC
:设置年老代为并发收集。
-XX:CMSFullGCsBeforeCompaction=5
由于并发收集器不对内存空间进行压缩、整理,
所以运行一段时间以后会产生
“
碎片
”
,使得运行效率降低。此值设置运行
5
次
GC
以后对内
存空间进行压缩、整理。
-XX:+UseCMSCompactAtFullCollection
:打开对年老代的压缩。可能会影响性能,但是
可以消除碎片
「辅助信息相关」
-XX:+PrintGCDetails
打印
GC
详细信息
-XX:+HeapDumpOnOutOfMemoryError
让
JVM
在发生内存溢出的时候自动生成内存快照
,
排查问题用
-XX:+DisableExplicitGC
禁止系统
System.gc()
,防止手动误触发
FGC
造成问题
.
-XX:+PrintTLAB
查看
TLAB
空间的使用情况
30
、说一下
JVM
有哪些垃圾回收器?
如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。下图展示了
7
种作用于不同分代的收集器,其中用于回收新生代的收集器包括
Serial
、
PraNew
、
Parallel
Scavenge
,回收老年代的收集器包括
Serial Old
、
Parallel Old
、
CMS
,还有用于回收整个
Java
堆的
G1
收集器。不同收集器之间的连线表示它们可以搭配使用。
阿里内部资料
Serial
收集器(复制算法
):
新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
ParNew
收集器
(
复制算法
):
新生代收并行集器,实际上是
Serial
收集器的多线程版本,在多核
CPU
环境下有着比
Serial
更好的表现;
Parallel Scavenge
收集器
(
复制算法
):
新生代并行收集器,追求高吞吐量,高效利用
CPU
。吞
吐量
=
用户线程时间
/(
用户线程时间
+GC
线程时间
)
,高吞吐量可以高效率的利用
CPU
时间,尽
快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
Serial Old
收集器
(
标记
-
整理算法
):
老年代单线程收集器,
Serial
收集器的老年代版本;
Parallel Old
收集器
(
标记
-
整理算法
)
:
老年代并行收集器,吞吐量优先,
Parallel Scavenge
收
集器的老年代版本;
CMS(Concurrent Mark Sweep)
收集器(标记
-
清除算法):
老年代并行收集器,以获取最短回
收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短
GC
回收停顿时间。
G1(Garbage First)
收集器
(
标记
-
整理算法
)
:
Java
堆并行收集器,
G1
收集器是
JDK1.7
提供的一
个新收集器,
G1
收集器基于
“
标记
-
整理
”
算法实现,也就是说不会产生内存碎片。此外,
G1
收
集器不同于之前的收集器的一个重要特点是:
G1
回收的范围是整个
Java
堆
(
包括新生代,老年
代
)
,而前六种收集器回收的范围仅限于新生代或老年代。
ZGC
(
Z Garbage Collector
)是一款由
Oracle
公司研发的,以低延迟为首要目标的一款垃圾收
集器。它是基于动态
Region
内存布局,(暂时)不设年龄分代,使用了读屏障、染色指针和内
存多重映射等技术来实现可并发的标记
-
整理算法的收集器。在
JDK 11
新加入,还在实验阶
段,主要特点是:回收
TB
级内存(最大
4T
),停顿时间不超过
10ms
。
优点
:低停顿,高吞吐
量,
ZGC
收集过程中额外耗费的内存小。
缺点
:浮动垃圾
目前使用的非常少,真正普及还是需要写时间的。
新生代收集器
:
Serial
、
ParNew
、
Parallel Scavenge
老年代收集器
:
CMS
、
Serial Old
、
Parallel Old
整堆收集器
:
G1
,
ZGC
(
因为不涉年代不在图中
)
。
31
、如何选择垃圾收集器?
1.
如果你的堆大小不是很大(比如
100MB
),选择串行收集器一般是效率最高的。
参数:
-
XX:+UseSerialGC
。
2.
如果你的应用运行在单核的机器上,或者你的虚拟机核数只有单核,选择串行收集器依然是合
适的,这时候启用一些并行收集器没有任何收益。
参数:
-
XX:+UseSerialGC
。
3.
如果你的应用是
“
吞吐量
”
优先的,并且对较长时间的停顿没有什么特别的要求。选择并行收集
器是比较好的。
参数:
-
XX:+UseParallelGC
。
阿里内部资料
4.
如果你的应用对响应时间要求较高,想要较少的停顿。甚至
1
秒的停顿都会引起大量的请求失
败,那么选择
G1
、
ZGC
、
CMS
都是合理的。虽然这些收集器的
GC
停顿通常都比较短,但它
需要一些额外的资源去处理这些工作,通常吞吐量会低一些。
参数:
-
XX:+UseConcMarkSweepGC
、
-
XX:+UseG1GC
、
-
XX:+UseZGC
等。
从上面这些出发点来看,我们平常的
Web
服务器,都是对响应性要求非常高的。选择性其实就集
中在
CMS
、
G1
、
ZGC
上。而对于某些定时任务,使用并行收集器,是一个比较好的选择。
32
、 什么是类加载器?
类加载器是一个用来加载类文件的类。
Java
源代码通过
javac
编译器编译成类 文件。然后
JVM
来执
行类文件中的字节码来执行程序。类加载器负责加载文件 系统、网络或其他来源的类文件。
33
、什么是
tomcat
类加载机制?
在
tomcat
中类的加载稍有不同,如下图:
阿里内部资料
当
tomcat
启动时,会创建几种类加载器:
Bootstrap
引导类加载器
加载
JVM
启动所需的类,以及
标准扩展类(位于
jre/lib/ext
下)
System
系统类加载器
加载
tomcat
启动的类,比如
bootstrap.jar
,通常在
catalina.bat
或者
catalina.sh
中指定。位于
CATALINA_HOME/bin
下。
Common
通用类加载器
欢迎关注微信公众号:
Java
后端技术全栈
多线程
&
并发篇
1
、说说
Java
中实现多线程有几种方法
创建线程的常用三种方式:
1.
继承
Thread
类
2.
实现
Runnable
接口
3.
实现
Callable
接口(
JDK1.5>=
)
4.
线程池方式创建
通过继承
Thread
类或者实现
Runnable
接口、
Callable
接口都可以实现多线程,不过实现
Runnable
接口与实现
Callable
接口的方式基本相同,只是
Callable
接口里定义的方法返回值,可以声明抛出异
常而已。因此将实现
Runnable
接口和实现
Callable
接口归为一种方式。这种方式与继承
Thread
方式
之间的主要差别如下。
采用实现
Runnable
、
Callable
接口的方式创建线程的优缺点
优点
:线程类只是实现了
Runnable
或者
Callable
接口,还可以继承其他类。这种方式下,多个线程
可以共享一个
target
对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将
CPU
、代码和数据分开,形成清晰的模型,较好的体现了面向对象的思想。
缺点
:编程稍微复杂一些,如果需要访问当前线程,则必须使用
Thread.currentThread()
方法
采用继承
Thread
类的方式创建线程的优缺点
阿里内部资料
优点
:编写简单,如果需要访问当前线程,则无需使用
Thread.currentThread()
方法,直接使用
this
即可获取当前线程
缺点
:因为线程类已经继承了
Thread
类,
Java
语言是单继承的,所以就不能再继承其他父类了。
2
、如何停止一个正在运行的线程
1
、使用退出标志,使线程正常退出,也就是当
run
方法完成后线程终止。
2
、使用
stop
方法强行终止,但是不推荐这个方法,因为
stop
和
suspend
及
resume
一样都是过期作
废的方法。
3
、使用
interrupt
方法中断线程。
class MyThread extends Thread {
volatile boolean stop = false;
public void run() {
while (!stop) {
System.out.println(getName() + " is running");
try {
sleep(1000);
} catch (InterruptedException e) {
System.out.println("week up from blcok...");
stop = true; //
在异常处理代码中修改共享变量的状态
}
}
System.out.println(getName() + " is exiting...");
}
}
class InterruptThreadDemo3 {
public static void main(String[] args) throws InterruptedException {
MyThread m1 = new MyThread();
System.out.println("Starting thread...");
m1.start();
Thread.sleep(3000);
System.out.println("Interrupt thread...: " + m1.getName());
m1.stop = true; //
设置共享变量为
true
m1.interrupt(); //
阻塞时退出阻塞状态
Thread.sleep(3000); //
主线程休眠
3
秒以便观察线程
m1
的中断情况
System.out.println("Stopping application...");
}
}
阿里内部资料
3
、
notify()
和
notifyAll()
有什么区别?
notify
可能会导致死锁,而
notifyAll
则不会
任何时候只有一个线程可以获得锁,也就是说只有一个线程可以运行
synchronized
中的代码
使用
notifyall,
可以唤醒 所有处于
wait
状态的线程,使其重新进入锁的争夺队列中,而
notify
只能唤
醒一个。
wait()
应配合
while
循环使用,不应使用
if
,务必在
wait()
调用前后都检查条件,如果不满足,必须调
用
notify()
唤醒另外的线程来处理,自己继续
wait()
直至条件满足再往下执行。
notify()
是对
notifyAll()
的一个优化,但它有很精确的应用场景,并且要求正确使用。不然可能导致
死锁。正确的场景应该是
WaitSet
中等待的是相同的条件,唤醒任一个都能正确处理接下来的事
项,如果唤醒的线程无法正确处理,务必确保继续
notify()
下一个线程,并且自身需要重新回到
WaitSet
中
.
4
、
sleep()
和
wait()
有什么区别?
对于
sleep()
方法,我们首先要知道该方法是属于
Thread
类中的。而
wait()
方法,则是属于
Object
类
中的。
sleep()
方法导致了程序暂停执行指定的时间,让出
cpu
该其他线程,但是他的监控状态依然保持
者,当指定的时间到了又会自动恢复运行状态。在调用
sleep()
方法的过程中,线程不会释放对象
锁。
当调用
wait()
方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用
notify()
方法后本线程才进入对象锁定池准备,获取对象锁进入运行状态。
5
、
volatile
是什么
?
可以保证有序性吗
?
一旦一个共享变量(类的成员变量、类的静态成员变量)被
volatile
修饰之后,那么就具备了两层语
义:
1
)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对
其他线程来说是立即可见的
,volatile
关键字会强制将修改的值立即写入主存。
2
)禁止进行指令重排序。
volatile
不是原子性操作
什么叫保证部分有序性
?
当程序执行到
volatile
变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结
果已经对后面的操作可见;在其后面的操作肯定还没有进行;
阿里内部资料
x = 2; //
语句
1
y = 0; //
语句
2
flag = true; //
语句
3
x = 4; //
语句
4
y = -1; //
语句
5
由于
flflag
变量为
volatile
变量,那么在进行指令重排序的过程的时候,不会将语句
3
放到语句
1
、语句
2
前面,也不会讲语句
3
放到语句
4
、语句
5
后面。但是要注意语句
1
和语句
2
的顺序、语句
4
和语句
5
的顺序是不作任何保证的。
使用
volatile
一般用于 状态标记量 和 单例模式的双检锁。
6
、
Thread
类中的
start()
和
run()
方法有什么区别?
start()
方法被用来启动新创建的线程,而且
start()
内部调用了
run()
方法,这和直接调用
run()
方法的
效果不一样。当你调用
run()
方法的时候,只会是在原来的线程中调用,没有新的线程启动,
start()
方法才会启动新线程。
7
、为什么
wait, notify
和
notifyAll
这些方法不在
thread
类里
面?
明显的原因是
JAVA
提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线
程需要等待某些锁那么调用对象中的
wait()
方法就有意义了。如果
wait()
方法定义在
Thread
类中,线
程正在等待的是哪个锁就不明显了。简单的说,由于
wait
,
notify
和
notifyAll
都是锁级别的操作,所
以把他们定义在
Object
类中因为锁属于对象。
8
、为什么
wait
和
notify
方法要在同步块中调用?
1.
只有在调用线程拥有某个对象的独占锁时,才能够调用该对象的
wait(),notify()
和
notifyAll()
方
法。
2.
如果你不这么做,你的代码会抛出
IllegalMonitorStateException
异常。
3.
还有一个原因是为了避免
wait
和
notify
之间产生竞态条件。
wait()
方法强制当前线程释放对象锁。这意味着在调用某对象的
wait()
方法之前,当前线程必须已经
获得该对象的锁。因此,线程必须在某个对象的同步方法或同步代码块中才能调用该对象的
wait()
方
法。
在调用对象的
notify()
和
notifyAll()
方法之前,调用线程必须已经得到该对象的锁。因此,必须在某
个对象的同步方法或同步代码块中才能调用该对象的
notify()
或
notifyAll()
方法。
调用
wait()
方法的原因通常是,调用线程希望某个特殊的状态
(
或变量
)
被设置之后再继续执行。调用
notify()
或
notifyAll()
方法的原因通常是,调用线程希望告诉其他等待中的线程
:"
特殊状态已经被设
置
"
。这个状态作为线程间通信的通道,它必须是一个可变的共享状态
(
或变量
)
。
阿里内部资料
9
、
Java
中
interrupted
和
isInterruptedd
方法的区别?
interrupted()
和
isInterrupted()
的主要区别是前者会将中断状态清除而后者不会。
Java
多线程的中
断机制是用内部标识来实现的,调用
Thread.interrupt()
来中断一个线程就会设置中断标识为
true
。
当中断线程调用静态方法
Thread.interrupted()
来检查中断状态时,中断状态会被清零。而非静态方
法
isInterrupted()
用来查询其它线程的中断状态且不会改变中断状态标识。简单的说就是任何抛出
InterruptedException
异常的方法都会将中断状态清零。无论如何,一个线程的中断状态有有可能
被其它线程调用中断来改变。
10
、
Java
中
synchronized
和
ReentrantLock
有什么不同?
相似点:
这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如
果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等
待,而进行线程阻塞和唤醒的代价是比较高的
.
区别:
这两种方式最大区别就是对于
Synchronized
来说,它是
java
语言的关键字,是原生语法层面的互
斥,需要
jvm
实现。而
ReentrantLock
它是
JDK 1.5
之后提供的
API
层面的互斥锁,需要
lock()
和
unlock()
方法配合
try/fifinally
语句块来完成。
Synchronized
进过编译,会在同步块的前后分别形成
monitorenter
和
monitorexit
这个两个字节码
指令。在执行
monitorenter
指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线
程已经拥有了那个对象锁,把锁的计算器加
1
,相应的,在执行
monitorexit
指令时会将锁计算器就
减
1
,当计算器为
0
时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被
另一个线程释放为止。
由于
ReentrantLock
是
java.util.concurrent
包下提供的一套互斥锁,相比
Synchronized
,
ReentrantLock
类提供了一些高级功能,主要有以下
3
项:
1.
等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于
Synchronized
来说可以避免出现死锁的情况。
2.
公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,
Synchronized
锁非公平
锁,
ReentrantLock
默认的构造函数是创建的非公平锁,可以通过参数
true
设为公平锁,但公平锁
表现的性能不是很好。
3.
锁绑定多个条件,一个
ReentrantLock
对象可以同时绑定对个对象。
11
、有三个线程
T1,T2,T3,
如何保证顺序执行?
阿里内部资料
在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的
join()
方法在一个线程中启动另一
个线程,另外一个线程完成该线程继续执行。为了确保三个线程的顺序你应该先启动最后一个
(T3
调
用
T2
,
T2
调用
T1)
,这样
T1
就会先完成而
T3
最后完成。
实际上先启动三个线程中哪一个都行,
因为在每个线程的
run
方法中用
join
方法限定了三个线程的
执行顺序。
public class JoinTest2 {
// 1.
现在有
T1
、
T2
、
T3
三个线程,你怎样保证
T2
在
T1
执行完后执行,
T3
在
T2
执行完后执行
public static void main(String[] args) {
final Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t1");
}
});
final Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
//
引用
t1
线程,等待
t1
线程执行完
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2");
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
try {
//
引用
t2
线程,等待
t2
线程执行完
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t3");
}
阿里内部资料
});
t3.start();//
这里三个线程的启动顺序可以任意,大家可以试下!
t2.start();
t1.start();
}
}
12
、
SynchronizedMap
和
ConcurrentHashMap
有什么区别?
SynchronizedMap()
和
Hashtable
一样,实现上在调用
map
所有方法时,都对整个
map
进行同步。
而
ConcurrentHashMap
的实现却更加精细,它对
map
中的所有桶加了锁。所以,只要有一个线程
访问
map
,其他线程就无法进入
map
,而如果一个线程在访问
ConcurrentHashMap
某个桶时,其
他线程,仍然可以对
map
执行某些操作。
所以,
ConcurrentHashMap
在性能以及安全性方面,明显比
Collections.synchronizedMap()
更加
有优势。同时,同步操作精确控制到桶,这样,即使在遍历
map
时,如果其他线程试图对
map
进行
数据修改,也不会抛出
ConcurrentModifificationException
。
13
、什么是线程安全
线程安全就是说多线程访问同一段代码,不会产生不确定的结果。
又是一个理论的问题,各式各样的答案有很多,我给出一个个人认为解释地最好的:
如果你的代码
在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的
。
这个问题有值得一提的地方,就是线程安全也是有几个级别的:
(
1
)不可变
像
String
、
Integer
、
Long
这些,都是
fifinal
类型的类,任何一个线程都改变不了它们的值,要改变除
非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用
(
2
)绝对线程安全
不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代
价,
Java
中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的
类,
Java
中也有,比方说
CopyOnWriteArrayList
、
CopyOnWriteArraySet
(
3
)相对线程安全
相对线程安全也就是我们通常意义上所说的线程安全,像
Vector
这种,
add
、
remove
方法都是原子
操作,不会被打断,但也仅限于此,如果有个线程在遍历某个
Vector
、有个线程同时在
add
这个
Vector
,
99%
的情况下都会出现
ConcurrentModifificationException
,也就是
fail-fast
机制
。
(
4
)线程非安全
这个就没什么好说的了,
ArrayList
、
LinkedList
、
HashMap
等都是线程非安全的类
阿里内部资料
14
、
Thread
类中的
yield
方法有什么作用?
Yield
方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法
而且只保证当前线程放弃
CPU
占用而不能保证使其它线程一定能占用
CPU
,执行
yield()
的线程有可
能在进入到暂停状态后马上又被执行。
15
、
Java
线程池中
submit()
和
execute()
方法有什么区别?
两个方法都可以向线程池提交任务,
execute()
方法的返回类型是
void
,它定义在
Executor
接口中
,
而
submit()
方法可以返回持有计算结果的
Future
对象,它定义在
ExecutorService
接口中,它扩展了
Executor
接口,其它线程池类像
ThreadPoolExecutor
和
ScheduledThreadPoolExecutor
都有这些
方法。
16
、说一说自己对于
synchronized
关键字的了解
synchronized
关键字解决的是多个线程之间访问资源的同步性,
synchronized
关键字可以保证被它
修饰的方法或者代码块在任意时刻只能有一个线程执行。 另外,在
Java
早期版本中,
synchronized
属于重量级锁,效率低下,因为监视器锁(
monitor
)是依赖于底层的操作系统的
Mutex Lock
来实现的,
Java
的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一
个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核
态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的
synchronized
效率低的原因。庆幸的是在
Java 6
之后
Java
官方对从
JVM
层面对
synchronized
较
大优化,所以现在的
synchronized
锁效率也优化得很不错了。
JDK1.6
对锁的实现引入了大量的优
化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
17
、说说自己是怎么使用
synchronized
关键字?
修饰实例方法
:
作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
修饰静态方法
:
也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类
成员(
static
表明这是该类的一个静态资源,不管
new
了多少个对象,只有一份)。所以如果一个
线程
A
调用一个实例对象的非静态
synchronized
方法,而线程
B
需要调用这个实例对象所属类的静
态
synchronized
方法,是允许的,不会发生互斥现象,
因为访问静态
synchronized
方法占用的
锁是当前类的锁,而访问非静态
synchronized
方法占用的锁是当前实例对象锁。 修饰代码块
:
指
定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
总结
:
synchronized
关
键字加到
static
静态方法和
synchronized(class)
代码块上都是是给
Class
类上锁。
synchronized
关键字加到实例方法上是给对象实例上锁。尽量不要使用
synchronized(String a)
因为
JVM
中,字
符串常量池具有缓存功能!
18
、什么是线程安全?
Vector
是一个线程安全类吗?
阿里内部资料
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每
次运行结果和单线程运行的结果是一样的,而且其他的变量 的值也和预期的是一样的,就是线程安
全的。一个线程安全的计数器类的同一个实例对象在被多个线程使用的情况下也不会出现计算失
误。很显然你可以将集合类分 成两组,线程安全和非线程安全的。
Vector
是用同步方法来实现线程
安全的
,
而和它相似的
ArrayList
不是线程安全的。
19
、
volatile
关键字的作用?
一旦一个共享变量(类的成员变量、类的静态成员变量)被
volatile
修饰之后,那么就具备了两层语
义:
保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对
其他线程来说是立即可见的。
禁止进行指令重排序。
volatile
本质是在告诉
jvm
当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读
取;
synchronized
则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
volatile
仅能使用在变量级别;
synchronized
则可以使用在变量、方法、和类级别的。
volatile
仅能实现变量的修改可见性,并不能保证原子性;
synchronized
则可以保证变量的修改
可见性和原子性。
volatile
不会造成线程的阻塞;
synchronized
可能会造成线程的阻塞。
volatile
标记的变量不会被编译器优化;
synchronized
标记的变量可以被编译器优化。
20
、常用的线程池有哪些?
newSingleThreadExecutor
:创建一个单线程的线程池,此线程池保证所有任务的执行顺序按
照任务的提交顺序执行。
newFixedThreadPool
:创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线
程达到线程池的最大大小。
newCachedThreadPool
:创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线
程池大小完全依赖于操作系统(或者说
JVM
)能够创建的最大线程大小。
newScheduledThreadPool
:创建一个大小无限的线程池,此线程池支持定时以及周期性执行
任务的需求。
newSingleThreadExecutor
:创建一个单线程的线程池。此线程池支持定时以及周期性执行任
务的需求。
21
、简述一下你对线程池的理解
(如果问到了这样的问题,可以展开的说一下线程池如何用、线程池的好处、线程池的启动策略)
合理利用线程池能够带来三个好处。
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
阿里内部资料
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降
低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
22
、
Java
程序是如何执行的
我们日常的工作中都使用开发工具(
IntelliJ IDEA
或
Eclipse
等)可以很方便的调试程序,或者是通
过打包工具把项目打包成
jar
包或者
war
包,放入
Tomcat
等
Web
容器中就可以正常运行了,但你
有没有想过
Java
程序内部是如何执行的?其实不论是在开发工具中运行还是在
Tomcat
中运行,
Java
程序的执行流程基本都是相同的,它的执行流程如下:
先把
Java
代码编译成字节码,也就是把
.java
类型的文件编译成
.class
类型的文件。这个过程
的大致执行流程:
Java
源代码
->
词法分析器
->
语法分析器
->
语义分析器
->
字符码生成器
->
最终生成字节码,其中任何一个节点执行失败就会造成编译失败;
把
class
文件放置到
Java
虚拟机,这个虚拟机通常指的是
Oracle
官方自带的
Hotspot JVM
;
Java
虚拟机使用类加载器(
Class Loader
)装载
class
文件;
类加载完成之后,会进行字节码效验,字节码效验通过之后
JVM
解释器会把字节码翻译成机器
码交由操作系统执行。但不是所有代码都是解释执行的,
JVM
对此做了优化,比如,以
Hotspot
虚拟机来说,它本身提供了
JIT
(
Just In Time
)也就是我们通常所说的动态编译器,
它能够在运行时将热点代码编译为机器码,这个时候字节码就变成了编译执行。
Java
程序执行
流程图如下:
阿里内部资料
23
、锁的优化机制了解吗?
从
JDK1.6
版本之后,
synchronized
本身也在不断优化锁的机制,有些情况下他并不会是一个很重量
级的锁了。优化机制包括自适应锁、自旋锁、锁消除、锁粗化、轻量级锁和偏向锁。
锁的状态从低到高依次为
无锁
->
偏向锁
->
轻量级锁
->
重量级锁
,升级的过程就是从低到高,降级在
一定条件也是有可能发生的。
自旋锁
:由于大部分时候,锁被占用的时间很短,共享变量的锁定时间也很短,所有没有必要挂起
线程,用户态和内核态的来回上下文切换严重影响性能。自旋的概念就是让线程执行一个忙循环,
可以理解为就是啥也不干,防止从用户态转入内核态,自旋锁可以通过设置
-XX:+UseSpining
来开
启,自旋的默认次数是
10
次,可以使用
-XX:PreBlockSpin
设置。
自适应锁
:自适应锁就是自适应的自旋锁,自旋的时间不是固定时间,而是由前一次在同一个锁上
的自旋时间和锁的持有者状态来决定。
锁消除
:锁消除指的是
JVM
检测到一些同步的代码块,完全不存在数据竞争的场景,也就是不需要
加锁,就会进行锁消除。
锁粗化
:锁粗化指的是有很多操作都是对同一个对象进行加锁,就会把锁的同步范围扩展到整个操
作序列之外。
阿里内部资料
偏向锁
:当线程访问同步块获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程
ID
,之后
这个线程再次进入同步块时都不需要
CAS
来加锁和解锁了,偏向锁会永远偏向第一个获得锁的线
程,如果后续没有其他线程获得过这个锁,持有锁的线程就永远不需要进行同步,反之,当有其他
线程竞争偏向锁时,持有偏向锁的线程就会释放偏向锁。可以用过设置
-XX:+UseBiasedLocking
开
启偏向锁。
轻量级锁
:
JVM
的对象的对象头中包含有一些锁的标志位,代码进入同步块的时候,
JVM
将会使用
CAS
方式来尝试获取锁,如果更新成功则会把对象头中的状态位标记为轻量级锁,如果更新失败,
当前线程就尝试自旋来获得锁。
整个锁升级的过程非常复杂,我尽力去除一些无用的环节,简单来描述整个升级的机制。
简单点说,偏向锁就是通过对象头的偏向线程
ID
来对比,甚至都不需要
CAS
了,而轻量级锁主要就
是通过
CAS
修改对象头锁记录和自旋来实现,重量级锁则是除了拥有锁的线程其他全部阻塞。
24
、说说进程和线程的区别?
1.
进程是一个
“
执行中的程序
”
,是系统进行资源分配和调度的一个独立单位。
2.
线程是进程的一个实体,一个进程中拥有多个线程,线程之间共享地址空间和其它资源(所以
通信和同步等操作线程比进程更加容易)
3.
线程上下文的切换比进程上下文切换要快很多。
(
1
)进程切换时,涉及到当前进程的
CPU
环境的保存和新被调度运行进程的
CPU
环境的设置。
(
2
)线程切换仅需要保存和设置少量的寄存器内容,不涉及存储管理方面的操作。
阿里内部资料
25
,产生死锁的四个必要条件?
1.
互斥条件:一个资源每次只能被一个线程使用
2.
请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放
3.
不剥夺条件:进程已经获得的资源,在未使用完之前,不能强行剥夺
4.
循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系
26
、如何避免死锁?
指定获取锁的顺序,举例如下:
1.
比如某个线程只有获得
A
锁和
B
锁才能对某资源进行操作,在多线程条件下,如何避免死锁?
2.
获得锁的顺序是一定的,比如规定,只有获得
A
锁的线程才有资格获取
B
锁,按顺序获取锁就可
以避免死锁!!!
27
,线程池核心线程数怎么设置呢?
分为
CPU
密集型和
IO
密集型
CPU
这种任务消耗的主要是
CPU
资源,可以将线程数设置为
N
(
CPU
核心数)
+1
,比
CPU
核心数多出
来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦
任务暂停,
CPU
就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用
CPU
的空
闲时间。
IO
密集型
这种任务应用起来,系统会用大部分的时间来处理
I/O
交互,而线程在处理
I/O
的时间段内不会占
用
CPU
来处理,这时就可以将
CPU
交出给其它线程使用。因此在
I/O
密集型任务的应用中,我们
可以多配置一些线程,具体的计算方法是 :
核心线程数
=CPU
核心数量
*2
。
28
,
Java
线程池中队列常用类型有哪些?
ArrayBlockingQueue
是一个基于数组结构的
有界阻塞队列
,此队列按
FIFO
(先进先出)原则
对元素进行排序。
LinkedBlockingQueue
一个基于链表结构的
阻塞队列
,此队列按
FIFO
(先进先出)
排序元
素,吞吐量通常要高于
ArrayBlockingQueue
。
SynchronousQueue
一个不存储元素的
阻塞队列
。
PriorityBlockingQueue
一个具有优先级的
无限阻塞队列
。
PriorityBlockingQueue
也是
基于
最小二叉堆实现
DelayQueue
阿里内部资料
只有当其指定的延迟时间到了,才能够从队列中获取到该元素。
DelayQueue
是一个没有大小限制的队列,
因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费
者)才会被阻塞。
这里能说出前三种也就差不多了,如果能说全那是最好。
29
,线程安全需要保证几个基本特征?
原子性
,简单说就是相关操作不会中途被其他线程干扰,一般通过同步机制实现。
可见性
,是一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为将
线程本地状态反映到主内存上,
volatile
就是负责保证可见性的。
有序性
,是保证线程内串行语义,避免指令重排等。
30
,说一下线程之间是如何通信的?
线程之间的通信有两种方式:共享内存和消息传递。
共享内存
在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写
-
读内存中的公共状态来
隐式进行通信。典型的共享内存通信方式,就是通过共享对象进行通信。
例如上图线程
A
与 线程
B
之间如果要通信的话,那么就必须经历下面两个步骤:
1.
线程
A
把本地内存
A
更新过得共享变量刷新到主内存中去。
2.
线程
B
到主内存中去读取线程
A
之前更新过的共享变量。
消息传递
在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行
通信。在
Java
中典型的消息传递方式,就是
wait()
和
notify()
,或者
BlockingQueue
。
31
、
CAS
的原理呢?
CAS
叫做
CompareAndSwap
,比较并交换,主要是通过处理器的指令来保证操作的原子性,它包含
三个操作数:
1.
变量内存地址,
V
表示
2.
旧的预期值,
A
表示
3.
准备设置的新值,
B
表示
当执行
CAS
指令时,只有当
V
等于
A
时,才会用
B
去更新
V
的值,否则就不会执行更新操作。
32
、
CAS
有什么缺点吗?
阿里内部资料
CAS
的缺点主要有
3
点:
ABA
问题
:
ABA
的问题指的是在
CAS
更新的过程中,当读取到的值是
A
,然后准备赋值的时候仍然是
A
,但是实际上有可能
A
的值被改成了
B
,然后又被改回了
A
,这个
CAS
更新的漏洞就叫做
ABA
。只是
ABA
的问题大部分场景下都不影响并发的最终效果。
Java
中有
AtomicStampedReference
来解决这个问题,他加入了预期标志和更新后标志两个字段,
更新时不光检查值,还要检查当前的标志是否等于预期标志,全部相等的话才会更新。
循环时间长开销大
:自旋
CAS
的方式如果长时间不成功,会给
CPU
带来很大的开销。
只能保证一个共享变量的原子操作
:只对一个共享变量操作可以保证原子性,但是多个则不行,多
个可以通过
AtomicReference
来处理或者使用锁
synchronized
实现。
33
、引用类型有哪些?有什么区别?
引用类型主要分为强软弱虚四种:
1.
强引用指的就是代码中普遍存在的赋值方式,比如
A a = new A()
这种。强引用关联的对象,永
远不会被
GC
回收。
2.
软引用可以用
SoftReference
来描述,指的是那些有用但是不是必须要的对象。系统在发生内存
溢出前会对这类引用的对象进行回收。
3.
弱引用可以用
WeakReference
来描述,他的强度比软引用更低一点,弱引用的对象下一次
GC
的时候一定会被回收,而不管内存是否足够。
4.
虚引用也被称作幻影引用,是最弱的引用关系,可以用
PhantomReference
来描述,他必须和
ReferenceQueue
一起使用,同样的当发生
GC
的时候,虚引用也会被回收。可以用虚引用来管
理堆外内存。
34
、说说
ThreadLocal
原理?
hreadLocal
可以理解为线程本地变量,他会在每个线程都创建一个副本,那么在线程之间访问内部
副本变量就行了,做到了线程之间互相隔离,相比于
synchronized
的做法是用空间来换时间。
ThreadLocal
有一个静态内部类
ThreadLocalMap
,
ThreadLocalMap
又包含了一个
Entry
数组,
Entry
本身是一个弱引用,他的
key
是指向
ThreadLocal
的弱引用,
Entry
具备了保存
key value
键值对
的能力。
弱引用的目的是为了防止内存泄露,如果是强引用那么
ThreadLocal
对象除非线程结束否则始终无
法被回收,弱引用则会在下一次
GC
的时候被回收。
但是这样还是会存在内存泄露的问题,假如
key
和
ThreadLocal
对象被回收之后,
entry
中就存在
key
为
null
,但是
value
有值的
entry
对象,但是永远没办法被访问到,同样除非线程结束运行。
但是只要
ThreadLocal
使用恰当,在使用完之后调用
remove
方法删除
Entry
对象,实际上是不会出
现这个问题的。
阿里内部资料
35
、线程池原理知道吗?以及核心参数
首先线程池有几个核心的参数概念:
1.
最大线程数
maximumPoolSize
2.
核心线程数
corePoolSize
3.
活跃时间
keepAliveTime
4.
阻塞队列
workQueue
5.
拒绝策略
RejectedExecutionHandler
当提交一个新任务到线程池时,具体的执行流程如下:
1.
当我们提交任务,线程池会根据
corePoolSize
大小创建若干任务数量线程执行任务
2.
当任务的数量超过
corePoolSize
数量,后续的任务将会进入阻塞队列阻塞排队
3.
当阻塞队列也满了之后,那么将会继续创建
(maximumPoolSize-corePoolSize)
个数量的线程来
执行任务,如果任务处理完成,
maximumPoolSize-corePoolSize
额外创建的线程等待
keepAliveTime
之后被自动销毁
4.
如果达到
maximumPoolSize
,阻塞队列还是满的状态,那么将根据不同的拒绝策略对应处理
阿里内部资料
36
、 线程池的拒绝策略有哪些?
主要有
4
种拒绝策略:
1. AbortPolicy
:直接丢弃任务,抛出异常,这是默认策略
2. CallerRunsPolicy
:只用调用者所在的线程来处理任务
3. DiscardOldestPolicy
:丢弃等待队列中最旧的任务,并执行当前任务
4. DiscardPolicy
:直接丢弃任务,也不抛出异常
37
、说说你对
JMM
内存模型的理解?为什么需要
JMM
?
随着
CPU
和内存的发展速度差异的问题,导致
CPU
的速度远快于内存,所以现在的
CPU
加入了高速
缓存,高速缓存一般可以分为
L1
、
L2
、
L3
三级缓存。基于上面的例子我们知道了这导致了缓存一致
性的问题,所以加入了缓存一致性协议,同时导致了内存可见性的问题,而编译器和
CPU
的重排序
导致了原子性和有序性的问题,
JMM
内存模型正是对多线程操作下的一系列规范约束,因为不可能
让陈雇员的代码去兼容所有的
CPU
,通过
JMM
我们才屏蔽了不同硬件和操作系统内存的访问差异,
这样保证了
Java
程序在不同的平台下达到一致的内存访问效果,同时也是保证在高效并发的时候程
序能够正确执行。
原子性
:
Java
内存模型通过
read
、
load
、
assign
、
use
、
store
、
write
来保证原子性操作,此外还有
lock
和
unlock
,直接对应着
synchronized
关键字的
monitorenter
和
monitorexit
字节码指令。
阿里内部资料
可见性
:可见性的问题在上面的回答已经说过,
Java
保证可见性可以认为通过
volatile
、
synchronized
、
fifinal
来实现。
有序性
:由于处理器和编译器的重排序导致的有序性问题,
Java
通过
volatile
、
synchronized
来保
证。
happen-before
规则
虽然指令重排提高了并发的性能,但是
Java
虚拟机会对指令重排做出一些规则限制,并不能让所有
的指令都随意的改变执行位置,主要有以下几点:
1.
单线程每个操作,
happen-before
于该线程中任意后续操作
2. volatile
写
happen-before
与后续对这个变量的读
3. synchronized
解锁
happen-before
后续对这个锁的加锁
4. fifinal
变量的写
happen-before
于
fifinal
域对象的读,
happen-before
后续对
fifinal
变量的读
5.
传递性规则,
A
先于
B
,
B
先于
C
,那么
A
一定先于
C
发生
说了半天,到底工作内存和主内存是什么?
主内存可以认为就是物理内存,
Java
内存模型中实际就是虚拟机内存的一部分。而工作内存就是
CPU
缓存,他有可能是寄存器也有可能是
L1\L2\L3
缓存,都是有可能的。
38
、多线程有什么用?
一个可能在很多人看来很扯淡的一个问题:我会用多线程就好了,还管它有什么用?在我看来,这
个回答更扯淡。所谓
"
知其然知其所以然
"
,
"
会用
"
只是
"
知其然
"
,
"
为什么用
"
才是
"
知其所以然
"
,只
有达到
"
知其然知其所以然
"
的程度才可以说是把一个知识点运用自如。
OK
,下面说说我对这个问题
的看法:
(
1
)发挥多核
CPU
的优势
随着工业的进步,现在的笔记本、台式机乃至商用的应用服务器至少也都是双核的,
4
核、
8
核甚至
16
核的也都不少见,如果是单线程的程序,那么在双核
CPU
上就浪费了
50%
,在
4
核
CPU
上就浪费
了
75%
。
单核
CPU
上所谓的
"
多线程
"
那是假的多线程,同一时间处理器只会处理一段逻辑,只不过
线程之间切换得比较快,看着像多个线程
"
同时
"
运行罢了
。多核
CPU
上的多线程才是真正的多线
程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核
CPU
的优势来,达到充分利用
CPU
的目的。
(
2
)防止阻塞
从程序运行效率的角度来看,单核
CPU
不但不会发挥出多线程的优势,反而会因为在单核
CPU
上运
行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核
CPU
我们还是要应用多线程,
就是为了防止阻塞。试想,如果单核
CPU
使用单线程,那么只要这个线程阻塞了,比方说远程读取
某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止
阿里内部资料
运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,
也不会影响其它任务的执行。
(
3
)便于建模
这是另外一个没有这么明显的优点了。假设有一个大的任务
A
,单线程编程,那么就要考虑很多,
建立整个程序模型比较麻烦。但是如果把这个大的任务
A
分解成几个小任务,任务
B
、任务
C
、任务
D
,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。
39
、说说
CyclicBarrier
和
CountDownLatch
的区别?
两个看上去有点像的类,都在
java.util.concurrent
下,都可以用来表示代码运行到某个点上,二者
的区别在于:
(
1
)
CyclicBarrier
的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了
这个点,所有线程才重新运行;
CountDownLatch
则不是,某线程运行到某个点上之后,只是给某
个数值
-1
而已,该线程继续运行
(
2
)
CyclicBarrier
只能唤起一个任务,
CountDownLatch
可以唤起多个任务
(
3
)
CyclicBarrier
可重用,
CountDownLatch
不可重用,计数值为
0
该
CountDownLatch
就不可再
用了
40
、什么是
AQS
?
简单说一下
AQS
,
AQS
全称为
AbstractQueuedSychronizer
,翻译过来应该是抽象队列同步器。
如果说
java.util.concurrent
的基础是
CAS
的话,那么
AQS
就是整个
Java
并发包的核心了,
ReentrantLock
、
CountDownLatch
、
Semaphore
等等都用到了它。
AQS
实际上以双向队列的形式
连接所有的
Entry
,比方说
ReentrantLock
,所有等待的线程都被放在一个
Entry
中并连成双向队
列,前面一个线程使用
ReentrantLock
好了,则双向队列实际上的第一个
Entry
开始运行。
AQS
定义了对双向队列所有的操作,而只开放了
tryLock
和
tryRelease
方法给开发者使用,开发者可
以根据自己的实现重写
tryLock
和
tryRelease
方法,以实现自己的并发功能。
41
、了解
Semaphore
吗?
emaphore
就是一个信号量,它的作用是
限制某段代码块的并发数
。
Semaphore
有一个构造函数,
可以传入一个
int
型整数
n
,表示某段代码最多只有
n
个线程可以访问,如果超出了
n
,那么请等待,
等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果
Semaphore
构造函数中
传入的
int
型整数
n=1
,相当于变成了一个
synchronized
了。
42
、什么是
Callable
和
Future?
阿里内部资料
Callable
接口类似于
Runnable
,从名字就可以看出来了,但是
Runnable
不会返回结果,并且无法抛
出返回结果的异常,而
Callable
功能更强大一些,被线程执行后,可以返回值,这个返回值可以被
Future
拿到,也就是说,
Future
可以拿到异步执行任务的返回值。可以认为是带有回调的
Runnable
。
Future
接口表示异步任务,是还没有完成的任务给出的未来结果。所以说
Callable
用于产生结果,
Future
用于获取结果。
43
、什么是阻塞队列?阻塞队列的实现原理是什么?如何使用阻
塞队列来实现生产者
-
消费者模型?
阻塞队列(
BlockingQueue
)是一个支持两个附加操作的队列。
这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元
素的线程会等待队列可用。
阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿
元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
JDK7
提供了
7
个阻塞队列。分别是:
ArrayBlockingQueue
:一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue
:一个由链表结构组成的有界阻塞队列。
PriorityBlockingQueue
:一个支持优先级排序的无界阻塞队列。
DelayQueue
:一个使用优先级队列实现的无界阻塞队列。
SynchronousQueue
:一个不存储元素的阻塞队列。
LinkedTransferQueue
:一个由链表结构组成的无界阻塞队列。
LinkedBlockingDeque
:一个由链表结构组成的双向阻塞队列。
Java 5
之前实现同步存取时,可以使用普通的一个集合,然后在使用线程的协作和线程同步可以实
现生产者,消费者模式,主要的技术就是用好,
wait ,notify,notifyAll,sychronized
这些关键字。而
在
java 5
之后,可以使用阻塞队列来实现,此方式大大简少了代码量,使得多线程编程更加容易,
安全方面也有保障。
BlockingQueue
接口是
Queue
的子接口,它的主要用途并不是作为容器,而是作为线程同步的的工
具,因此他具有一个很明显的特性,当生产者线程试图向
BlockingQueue
放入元素时,如果队列已
满,则线程被阻塞,当消费者线程试图从中取出一个元素时,如果队列为空,则该线程会被阻塞,
正是因为它所具有这个特性,所以在程序中多个线程交替向
BlockingQueue
中放入元素,取出元
素,它可以很好的控制线程之间的通信。
阻塞队列使用最经典的场景就是
socket
客户端数据的读取和解析,读取数据的线程不断将数据放入
队列,然后解析线程不断从队列取数据解析。
44
、什么是多线程中的上下文切换?
阿里内部资料
在上下文切换过程中,
CPU
会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后
继续运行。从这个角度来看,上下文切换有点像我们同时阅读几本书,在来回切换书本的同时我们
需要记住每本书当前读到的页码。
在程序中,上下文切换过程中的
“
页码
”
信息是保存在进程控制块(
PCB
)中的。
PCB
还经常被称
作
“
切换桢
”
(
switchframe
)。
“
页码
”
信息会一直保存到
CPU
的内存中,直到他们被再次使用。
上下文切换是存储和恢复
CPU
状态的过程,它使得线程执行能够从中断点恢复执行。上下文切换是
多任务操作系统和多线程环境的基本特征。
45
、什么是
Daemon
线程?它有什么意义?
所谓后台
(daemon)
线程,也叫守护线程,是指在程序运行的时候在后台提供一种通用服务的线程,
并且这个线程并不属于程序中不可或缺的部分。
因此,当所有的非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。反过来
说,
只要有任何非后台线程还在运行,程序就不会终止。
必须在线程启动之前调用
setDaemon()
方法,才能把它设置为后台线程。注意:后台进程在不执行
fifinally
子句的情况下就会终止其
run()
方法。
比如:
JVM
的垃圾回收线程就是
Daemon
线程,
Finalizer
也是守护线程。
46
、乐观锁和悲观锁的理解及如何实现,有哪些实现方式?
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候
都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做
操作之前先上锁。再比如
Java
里面的同步原语
synchronized
关键字的实现也是悲观锁。
乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是
在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。
乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于
write_condition
机
制,其实都是提供的乐观锁。
在
Java
中
java.util.concurrent.atomic
包下面的原子变量类就是使用了乐观锁的一种实现方式
CAS
实
现的。
乐观锁的实现方式:
1
、使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可
以采取丢弃和再次尝试的策略。
阿里内部资料
2
、
java
中的
Compare and Swap
即
CAS
,当多个线程尝试使用
CAS
同时更新同一个变量时,只有其
中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争
中失败,并可以再次尝试。
CAS
操作中包含三个操作数
——
需要读写的内存位置(
V
)、进行比
较的预期原值(
A
)和拟写入的新值
(B)
。如果内存位置
V
的值与预期原值
A
相匹配,那么处理器会自
动将该位置值更新为新值
B
。否则处理器不做任何操作。
CAS
缺点:
1.
ABA
问题:
比如说一个线程
one
从内存位置
V
中取出
A
,这时候另一个线程
two
也从内存中取出
A
,并且
two
进行了一些操作变成了
B
,然后
two
又将
V
位置的数据变成
A
,这时候线程
one
进行
CAS
操作发现内存中仍然是
A
,然后
one
操作成功。尽管线程
one
的
CAS
操作成功,但可能存在
潜藏的问题。从
Java1.5
开始
JDK
的
atomic
包里提供了一个类
AtomicStampedReference
来解决
ABA
问题。
2.
循环时间长开销大:
对于资源竞争严重(线程冲突严重)的情况,
CAS
自旋的概率会比较大,
从而浪费更多的
CPU
资源,效率低于
synchronized
。
3.
只能保证一个共享变量的原子操作:
当对一个共享变量执行操作时,我们可以使用循环
CAS
的
方式来保证原子操作,但是对多个共享变量操作时,循环
CAS
就无法保证操作的原子性,这个
时候就可以用锁。
欢迎关注微信公众号:
Java
后端技术全栈
Spring
篇
1
、什么是
spring?
Spring
是个
java
企业级应用的开源开发框架。
Spring
主要用来开发
Java
应用,但是有些扩展是针对
构建
J2EE
平台的
web
应用。
Spring
框架目标是简化
Java
企业级应用开发,并通过
POJO
为基础的编程
模型促进良好的编程习惯。
2
、你们项目中为什么使用
Spring
框架?
这么问的话,就直接说
Spring
框架的好处就可以了。比如说
Spring
有以下特点:
轻量:
Spring
是轻量的,基本的版本大约
2MB
。
控制反转:
Spring
通过控制反转实现了松散耦合,对象们给出它们的依赖,而不是创建或查找
依赖的对象们。
面向切面的编程
(AOP)
:
Spring
支持面向切面的编程,并且把应用业务逻辑和系统服务分开。
容器:
Spring
包含并管理应用中对象的生命周期和配置。
MVC
框架
:
Spring
的
WEB
框架是个精心设计的框架,是
Web
框架的一个很好的替代品。
事务管理:
Spring
提供一个持续的事务管理接口,可以扩展到上至本地事务下至全局事务
(
JTA
)。
阿里内部资料
异常处理:
Spring
提供方便的
API
把具体技术相关的异常(比如由
JDBC
,
Hibernate or JDO
抛
出的)转化为一致的
unchecked
异常。
3
、
Autowired
和
Resource
关键字的区别?
@Resource
和
@Autowired
都是做
bean
的注入时使用,其实
@Resource
并不是
Spring
的注解,它的
包是
javax.annotation.Resource
,需要导入,但是
Spring
支持该注解的注入。
1
、共同点
两者都可以写在字段和
setter
方法上。两者如果都写在字段上,那么就不需要再写
setter
方法。
2
、不同点
(
1
)
@Autowired
@Autowired
为
Spring
提供的注解,需要导入包
org.springframework.beans.factory.annotation.Autowired;
只按照
byType
注入。
@Autowired
注解是按照类型(
byType
)装配依赖对象,默认情况下它要求依赖对象必须存在,如
果允许
null
值,可以设置它的
required
属性为
false
。如果我们想使用按照名称(
byName
)来装
配,可以结合
@Qualififier
注解一起使用。如下:
(
2
)
@Resource
public class
TestServiceImpl
{
//
下面两种
@Autowired
只要使用一种即可
@Autowired
private
UserDao userDao
;
//
用于字段上
@Autowired
public
void
setUserDao
(
UserDao userDao
) {
//
用于属性的方法上
this
.
userDao
=
userDao
;
}
}
public class
TestServiceImpl
{
@Autowired
@Qualifier
(
"userDao"
)
private
UserDao userDao
;
}
阿里内部资料
@Resource
默认按照
ByName
自动注入,由
J2EE
提供,需要导入包
javax.annotation.Resource
。
@Resource
有两个重要的属性:
name
和
type
,而
Spring
将
@Resource
注解的
name
属性解析为
bean
的名字,而
type
属性则解析为
bean
的类型。所以,如果使用
name
属性,则使用
byName
的自
动注入策略,而使用
type
属性时则使用
byType
自动注入策略。如果既不制定
name
也不制定
type
属
性,这时将通过反射机制使用
byName
自动注入策略。
注:最好是将
@Resource
放在
setter
方法上,因为这样更符合面向对象的思想,通过
set
、
get
去操
作属性,而不是直接去操作属性。
@Resource
装配顺序:
①如果同时指定了
name
和
type
,则从
Spring
上下文中找到唯一匹配的
bean
进行装配,找不到则抛
出异常。
②如果指定了
name
,则从上下文中查找名称(
id
)匹配的
bean
进行装配,找不到则抛出异常。
③如果指定了
type
,则从上下文中找到类似匹配的唯一
bean
进行装配,找不到或是找到多个,都会
抛出异常。
④如果既没有指定
name
,又没有指定
type
,则自动按照
byName
方式进行装配;如果没有匹配,
则回退为一个原始类型进行匹配,如果匹配则自动装配。
@Resource
的作用相当于
@Autowired
,只不过
@Autowired
按照
byType
自动注入。
4
、依赖注入的方式有几种,各是什么
?
一、构造器注入
将被依赖对象通过构造函数的参数注入给依赖对象,并且在初始化对象的时候注
入。
优点:
对象初始化完成后便可获得可使用的对象。
缺点:
当需要注入的对象很多时,构造器参数列表将会很长;
不够灵活。若有多种注入方式,每种
方式只需注入指定几个依赖,那么就需要提供多个重载的构造函数,麻烦。
public class
TestServiceImpl
{
//
下面两种
@Resource
只要使用一种即可
@Resource
(
name
=
"userDao"
)
private
UserDao userDao
;
//
用于字段上
@Resource
(
name
=
"userDao"
)
public
void
setUserDao
(
UserDao userDao
) {
//
用于属性的
setter
方法上
this
.
userDao
=
userDao
;
}
}
阿里内部资料
二、
setter
方法注入
IoC Service Provider
通过调用成员变量提供的
setter
函数将被依赖对象注入给
依赖类。
优点:
灵活。可以选择性地注入需要的对象。
缺点:
依赖对象初始化完成后由于尚未注入被依赖对象,因此还不能使用。
三、接口注入
依赖类必须要实现指定的接口,然后实现该接口中的一个函数,该函数就是用于依赖
注入。该函数的参数就是要注入的对象。
优点 接口注入中,接口的名字、函数的名字都不重要,只要保证函数的参数是要注入的对象类型即
可。
缺点:
侵入行太强,不建议使用。
PS
:什么是侵入行?
如果类
A
要使用别人提供的一个功能,若为了使用这功能,需要在自己的类中
增加额外的代码,这就是侵入性。
5
、讲一下什么是
Spring
Spring
是一个轻量级的
IoC
和
AOP
容器框架。是为
Java
应用程序提供基础性服务的一套框架,目的是
用于简化企业应用程序的开发,它使得开发者只需要关心业务需求。常见的配置方式有三种:基于
XML
的配置、基于注解的配置、基于
Java
的配置。
主要由以下几个模块组成:
Spring Core
:核心类库,提供
IOC
服务;
Spring Context
:提供框架式的
Bean
访问方式,以及企业级功能(
JNDI
、定时任务等);
Spring AOP
:
AOP
服务;
Spring DAO
:对
JDBC
的抽象,简化了数据访问异常的处理;
Spring ORM
:对现有的
ORM
框架的支持;
Spring Web
:提供了基本的面向
Web
的综合特性,例如多方文件上传;
Spring MVC
:提供面向
Web
应用的
Model-View-Controller
实现。
6
、说说你对
Spring MVC
的理解
什么是
MVC
模式
MVC
:
MVC
是一种设计模式
MVC
的原理图:
阿里内部资料
分析:
M-Model
模型(完成业务逻辑:有
javaBean
构成,
service+dao+entity
)
V-View
视图(做界面的展示
jsp
,
html……
)
C-Controller
控制器(接收请求
—>
调用模型
—>
根据结果派发页面)
springMVC
是一个
MVC
的开源框架,
springMVC=struts2+spring
,
springMVC
就相当于是
Struts2
加上
sring
的整合,但是这里有一个疑惑就是,
springMVC
和
spring
是什么样的关系呢?这个在百度
百科上有一个很好的解释:意思是说,
springMVC
是
spring
的一个后续产品,其实就是
spring
在原
有基础上,又提供了
web
应用的
MVC
模块,可以简单的把
springMVC
理解为是
spring
的一个模块
(类似
AOP
,
IOC
这样的模块),网络上经常会说
springMVC
和
spring
无缝集成,其实
springMVC
就是
spring
的一个子模块,所以根本不需要同
spring
进行整合。
工作原理:
阿里内部资料
1
、 用户发送请求至前端控制器
DispatcherServlet
。
2
、
DispatcherServlet
收到请求调用
HandlerMapping
处理器映射器。
3
、 处理器映射器找到具体的处理器
(
可以根据
xml
配置、注解进行查找
)
,生成处理器对象及处理器
拦截器
(
如果有则生成
)
一并返回给
DispatcherServlet
。
4
、
DispatcherServlet
调用
HandlerAdapter
处理器适配器。
5
、
HandlerAdapter
经过适配调用具体的处理器
(Controller
,也叫后端控制器
)
。
6
、
Controller
执行完成返回
ModelAndView
。
7
、
HandlerAdapter
将
controller
执行结果
ModelAndView
返回给
DispatcherServlet
。
8
、
DispatcherServlet
将
ModelAndView
传给
ViewReslover
视图解析器。
9
、
ViewReslover
解析后返回具体
View
。
10
、
DispatcherServlet
根据
View
进行渲染视图(即将模型数据填充至视图中)。
11
、
DispatcherServlet
响应用户。
组件说明:
以下组件通常使用框架提供实现:
DispatcherServlet
:作为前端控制器,整个流程控制的中心,控制其它组件执行,统一调度,降低
组件之间的耦合性,提高每个组件的扩展性。
阿里内部资料
HandlerMapping
:通过扩展处理器映射器实现不同的映射方式,例如:配置文件方式,实现接口
方式,注解方式等。
HandlAdapter
:通过扩展处理器适配器,支持更多类型的处理器。
ViewResolver
:通过扩展视图解析器,支持更多类型的视图解析,例如:
jsp
、
freemarker
、
pdf
、
excel
等。
组件:
1
、前端控制器
DispatcherServlet
(不需要工程师开发)
,
由框架提供
作用:接收请求,响
应结果,相当于转发器,中央处理器。有了
dispatcherServlet
减少了其它组件之间的耦合度。 用户
请求到达前端控制器,它就相当于
mvc
模式中的
c
,
dispatcherServlet
是整个流程控制的中心,由它
调用其它组件处理用户的请求,
dispatcherServlet
的存在降低了组件之间的耦合性。
2
、处理器映射器
HandlerMapping(
不需要工程师开发
),
由框架提供
作用:根据请求的
url
查找
Handler HandlerMapping
负责根据用户请求找到
Handler
即处理器,
springmvc
提供了不同的映射
器实现不同的映射方式,例如:配置文件方式,实现接口方式,注解方式等。
3
、处理器适配器
HandlerAdapter
作用:按照特定规则(
HandlerAdapter
要求的规则)去执行
Handler
通过
HandlerAdapter
对处理器进行执行,这是适配器模式的应用,通过扩展适配器可以对
更多类型的处理器进行执行。
4
、处理器
Handler(
需要工程师开发
)
注意:编写
Handler
时按照
HandlerAdapter
的要求去做,
这样适配器才可以去正确执行
Handler
Handler
是继
DispatcherServlet
前端控制器的后端控制器,
在
DispatcherServlet
的控制下
Handler
对具体的用户请求进行处理。 由于
Handler
涉及到具体的用
户业务请求,所以一般情况需要工程师根据业务需求开发
Handler
。
5
、视图解析器
View resolver(
不需要工程师开发
),
由框架提供
作用:进行视图解析,根据逻辑视图
名解析成真正的视图(
view
)
View Resolver
负责将处理结果生成
View
视图,
View Resolver
首先根
据逻辑视图名解析成物理视图名即具体的页面地址,再生成
View
视图对象,最后对
View
进行渲染将
处理结果通过页面展示给用户。
springmvc
框架提供了很多的
View
视图类型,包括:
jstlView
、
freemarkerView
、
pdfView
等。 一般情况下需要通过页面标签或页面模版技术将模型数据通过页面
展示给用户,需要由工程师根据业务需求开发具体的页面。
6
、视图
View(
需要工程师开发
jsp...)
View
是一个接口,实现类支持不同的
View
类型(
jsp
、
freemarker
、
pdf...
)
核心架构的具体流程步骤如下:
1
、首先用户发送请求
——>DispatcherServlet
,前端控制器收到
请求后自己不进行处理,而是委托给其他的解析器进行处理,作为统一访问点,进行全局的流程控
制;
2
、
DispatcherServlet——>HandlerMapping
,
HandlerMapping
将会把请求映射为
HandlerExecutionChain
对象(包含一个
Handler
处理器(页面控制器)对象、多个
HandlerInterceptor
拦截器)对象,通过这种策略模式,很容易添加新的映射策略;
3
、
DispatcherServlet——>HandlerAdapter
,
HandlerAdapter
将会把处理器包装为适配器,从而支
持多种类型的处理器,即适配器设计模式的应用,从而很容易支持很多类型的处理器;
4
、
HandlerAdapter——>
处理器功能处理方法的调用,
HandlerAdapter
将会根据适配的结果调用真
阿里内部资料
正的处理器的功能处理方法,完成功能处理;并返回一个
ModelAndView
对象(包含模型数据、逻
辑视图名);
5
、
ModelAndView
的逻辑视图名
——> ViewResolver
,
ViewResolver
将把逻辑视图
名解析为具体的
View
,通过这种策略模式,很容易更换其他视图技术;
6
、
View——>
渲染,
View
会根据传进来的
Model
模型数据进行渲染,此处的
Model
实际是一个
Map
数据结构,因此很容易支
持其他视图技术;
7
、返回控制权给
DispatcherServlet
,由
DispatcherServlet
返回响应给用户,到
此一个流程结束。
看到这些步骤我相信大家很感觉非常的乱,这是正常的,但是这里主要是要大家理解
springMVC
中
的几个组件:
前端控制器(
DispatcherServlet
):接收请求,响应结果,相当于电脑的
CPU
。
处理器映射器(
HandlerMapping
):根据
URL
去查找处理器。
处理器(
Handler
):需要程序员去写代码处理逻辑的。
处理器适配器(
HandlerAdapter
):会把处理器包装成适配器,这样就可以支持多种类型的处理
器,类比笔记本的适配器(适配器模式的应用)。
视图解析器(
ViewResovler
):进行视图解析,多返回的字符串,进行处理,可以解析成对应的页
面。
7
、
SpringMVC
常用的注解有哪些?
@RequestMapping
:用于处理请求
url
映射的注解,可用于类或方法上。用于类上,则表示类中
的所有响应请求的方法都是以该地址作为父路径。
@RequestBody
:注解实现接收
http
请求的
json
数据,将
json
转换为
java
对象。
@ResponseBody
:注解实现将
conreoller
方法返回对象转化为
json
对象响应给客户。
8
、 谈谈你对
Spring
的
AOP
理解
AOP
(
Aspect-Oriented Programming
,面向切面编程)能够将那些与业务无关,却为业务模块所
共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复
代码,降低模块间的耦合度,并有利于未来的可扩展性和可维护性。
Spring AOP
是基于动态代理的,如果要代理的对象实现了某个接口,那么
Spring AOP
就会使用
JDK
动态代理去创建代理对象;而对于没有实现接口的对象,就无法使用
JDK
动态代理,转而使用
CGlib
动态代理生成一个被代理对象的子类来作为代理。
阿里内部资料
注意:图中的
implements
和
extend
。即一个是接口,一个是实现类。
当然也可以使用
AspectJ
,
Spring AOP
中已经集成了
AspectJ
,
AspectJ
应该算得上是
Java
生态系统中
最完整的
AOP
框架了。使用
AOP
之后我们可以把一些通用功能抽象出来,在需要用到的地方直接使
用即可,这样可以大大简化代码量。我们需要增加新功能也方便,提高了系统的扩展性。日志功
能、事务管理和权限管理等场景都用到了
AOP
。
这里只要你提到了
AspectJ
,那么面试官很有可能会继续问:
9
、
Spring AOP
和
AspectJ AOP
有什么区别?
Spring AOP
是属于运行时增强,而
AspectJ
是编译时增强。
Spring AOP
基于代理(
Proxying
),而
AspectJ
基于字节码操作(
Bytecode Manipulation
)。
Spring AOP
已经集成了
AspectJ
,
AspectJ
应该算得上是
Java
生态系统中最完整的
AOP
框架了。
AspectJ
相比于
Spring AOP
功能更加强大,但是
Spring AOP
相对来说更简单。
如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择
AspectJ
,它比
SpringAOP
快很多。
可能还会继续问:
在
Spring AOP
中,关注点和横切关注的区别是什么?
关注点是应用中一个模块的行为,一个关注点可能会被定义成一个我们想实现的一个功能。 横切关
注点是一个关注点,此关注点是整个应用都会使用的功能,并影响整个应用,比如日志,安全和数
据传输,几乎应用的每个模块都需要的功能。因此这些都属于横切关注点。
那什么是连接点呢?连接点代表一个应用程序的某个位置,在这个位置我们可以插入一个
AOP
切
面,它实际上是个应用程序执行
Spring AOP
的位置。
阿里内部资料
切入点是什么?切入点是一个或一组连接点,通知将在这些位置执行。可以通过表达式或匹配的方
式指明切入点。
什么是通知呢?有哪些类型呢?
通知是个在方法执行前或执行后要做的动作,实际上是程序执行时要通过
SpringAOP
框架触发的代
码段。
Spring
切面可以应用五种类型的通知:
before
:前置通知,在一个方法执行前被调用。
after:
在方法执行之后调用的通知,无论方法执行是否成功。
after-returning:
仅当方法成功完成后执行的通知。
after-throwing:
在方法抛出异常退出时执行的通知。
around:
在方法执行之前和之后调用的通知。
10
、说说你对
Spring
的
IOC
是怎么理解的?
(
1
)
IOC
就是控制反转,是指创建对象的控制权的转移。以前创建对象的主动权和时机是由自己把
控的,而现在这种权力转移到
Spring
容器中,并由容器根据配置文件去创建实例和管理各个实例之
间的依赖关系。对象与对象之间松散耦合,也利于功能的复用。
DI
依赖注入,和控制反转是同一个
概念的不同角度的描述,即 应用程序在运行时依赖
IoC
容器来动态注入对象需要的外部资源。
(
2
)最直观的表达就是,
IOC
让对象的创建不用去
new
了,可以由
spring
自动生产,使用
java
的反
射机制,根据配置文件在运行时动态的去创建对象以及管理对象,并调用对象的方法的。
(
3
)
Spring
的
IOC
有三种注入方式 :构造器注入、
setter
方法注入、根据注解注入。
IoC
让相互协作的组件保持松散的耦合,而
AOP
编程允许你把遍布于应用各层的功能分离出来
形成可重用的功能组件。
11
、解释一下
spring bean
的生命周期
首先说一下
Servlet
的生命周期:实例化,初始
init
,接收请求
service
,销毁
destroy
;
Spring
上下文中的
Bean
生命周期也类似,如下:
(
1
)实例化
Bean
:
对于
BeanFactory
容器,当客户向容器请求一个尚未初始化的
bean
时,或初始化
bean
的时候需要注
入另一个尚未初始化的依赖时,容器就会调用
createBean
进行实例化。对于
ApplicationContext
容
器,当容器启动结束后,通过获取
BeanDefifinition
对象中的信息,实例化所有的
bean
。
阿里内部资料
(
2
)设置对象属性(依赖注入):
实例化后的对象被封装在
BeanWrapper
对象中,紧接着,
Spring
根据
BeanDefifinition
中的信息 以
及 通过
BeanWrapper
提供的设置属性的接口完成依赖注入。
(
3
)处理
Aware
接口:
接着,
Spring
会检测该对象是否实现了
xxxAware
接口,并将相关的
xxxAware
实例注入给
Bean
:
①如果这个
Bean
已经实现了
BeanNameAware
接口,会调用它实现的
setBeanName(String
beanId)
方法,此处传递的就是
Spring
配置文件中
Bean
的
id
值;
②如果这个
Bean
已经实现了
BeanFactoryAware
接口,会调用它实现的
setBeanFactory()
方法,传
递的是
Spring
工厂自身。
③如果这个
Bean
已经实现了
ApplicationContextAware
接口,会调用
setApplicationContext(ApplicationContext)
方法,传入
Spring
上下文;
(
4
)
BeanPostProcessor
:
如果想对
Bean
进行一些自定义的处理,那么可以让
Bean
实现了
BeanPostProcessor
接口,那将会
调用
postProcessBeforeInitialization(Object obj, String s)
方法。
(
5
)
InitializingBean
与
init-method
:
如果
Bean
在
Spring
配置文件中配置了
init-method
属性,则会自动调用其配置的初始化方法。
(
6
)如果这个
Bean
实现了
BeanPostProcessor
接口,将会调用
postProcessAfterInitialization(Object obj, String s)
方法;由于这个方法是在
Bean
初始化结束时调
用的,所以可以被应用于内存或缓存技术;
以上几个步骤完成后,
Bean
就已经被正确创建了,之后就可以使用这个
Bean
了。
(
7
)
DisposableBean
:
当
Bean
不再需要时,会经过清理阶段,如果
Bean
实现了
DisposableBean
这个接口,会调用其实现
的
destroy()
方法;
(
8
)
destroy-method
:
最后,如果这个
Bean
的
Spring
配置中配置了
destroy-method
属性,会自动调用其配置的销毁方
法。
阿里内部资料
12
、解释
Spring
支持的几种
bean
的作用域?
Spring
容器中的
bean
可以分为
5
个范围:
(
1
)
singleton
:默认,每个容器中只有一个
bean
的实例,单例的模式由
BeanFactory
自身来维
护。
(
2
)
prototype
:为每一个
bean
请求提供一个实例。
(
3
)
request
:为每一个网络请求创建一个实例,在请求完成以后,
bean
会失效并被垃圾回收器回
收。
(
4
)
session
:与
request
范围类似,确保每个
session
中有一个
bean
的实例,在
session
过期后,
bean
会随之失效。
(
5
)
global-session
:全局作用域,
global-session
和
Portlet
应用相关。当你的应用部署在
Portlet
容器中工作时,它包含很多
portlet
。如果你想要声明让所有的
portlet
共用全局的存储变量的话,那
么这全局变量需要存储在
global-session
中。全局作用域与
Servlet
中的
session
作用域效果相同。
13
、
Spring
基于
xml
注入
bean
的几种方式
?
(
1
)
Set
方法注入;
(
2
)构造器注入:①通过
index
设置参数的位置;②通过
type
设置参数类型;
阿里内部资料
(
3
)静态工厂注入;
(
4
)实例工厂;
通常回答前面两种即可,因为后面两种很多人都不太会,不会的就不要说出来,不然问到你不会就
尴尬了。
14
、
Spring
框架中都用到了哪些设计模式?
这是一道相对有难度的题目,你不仅要回设计模式,还要知道每个设计模式在
Spring
中是如何使用
的。
简单工厂模式
:
Spring
中的
BeanFactory
就是简单工厂模式的体现。根据传入一个唯一的标识来获
得
Bean
对象,但是在传入参数后创建还是传入参数前创建,要根据具体情况来定。
工厂模式
:
Spring
中的
FactoryBean
就是典型的工厂方法模式,实现了
FactoryBean
接口的
bean
是一类叫做
factory
的
bean
。其特点是,
spring
在使用
getBean()
调用获得该
bean
时,会自动调
用该
bean
的
getObject()
方法,所以返回的不是
factory
这个
bean
,而是这个
bean.getOjbect()
方法的返回值。
单例模式
:在
spring
中用到的单例模式有:
scope="singleton"
,注册式单例模式,
bean
存放于
Map
中。
bean name
当做
key
,
bean
当做
value
。
原型模式
:在
spring
中用到的原型模式有:
scope="prototype"
,每次获取的是通过克隆生成的新
实例,对其进行修改时对原有实例对象不造成任何影响。
迭代器模式
:在
Spring
中有个
CompositeIterator
实现了
Iterator
,
Iterable
接口和
Iterator
接
口,这两个都是迭代相关的接口。可以这么认为,实现了
Iterable
接口,则表示某个对象是可被迭
代的。
Iterator
接口相当于是一个迭代器,实现了
Iterator
接口,等于具体定义了这个可被迭代的
对象时如何进行迭代的。
代理模式
:
Spring
中经典的
AOP
,就是使用动态代理实现的,分
JDK
和
CGlib
动态代理。
适配器模式
:
Spring
中的
AOP
中
AdvisorAdapter
类,它有三个实现:
MethodBeforAdviceAdapter
、
AfterReturnningAdviceAdapter
、
ThrowsAdviceAdapter
。
Spring
会根据不同的
AOP
配置来使用对应的
Advice
,与策略模式不同的是,一个方法可以同时拥有多个
Advice
。
Spring
存在很多以
Adapter
结尾的,大多数都是适配器模式。
观察者模式
:
Spring
中的
Event
和
Listener
。
spring
事件:
ApplicationEvent
,该抽象类继承了
EventObject
类,
JDK
建议所有的事件都应该继承自
EventObject
。
spring
事件监听器:
ApplicationListener
,该接口继承了
EventListener
接口,
JDK
建议所有的事件监听器都应该继承
EventListener
。
模板模式
:
Spring
中的
org.springframework.jdbc.core.JdbcTemplate
就是非常经典的模板模式
的应用,里面的
execute
方法,把整个算法步骤都定义好了。
阿里内部资料
责任链模式
:
DispatcherServlet
中的
doDispatch()
方法中获取与请求匹配的处理器
HandlerExecutionChain
,
this.getHandler()
方法的处理使用到了责任链模式。
注意
:这里只是列举了部分设计模式,其实里面用到了还有享元模式、建造者模式等。可选择性的
回答,主要是怕你回答了迭代器模式,然后继续问你,结果你一问三不知,那就尴了尬了。
15
、说说
Spring
中
ApplicationContext
和
BeanFactory
的区
别
类图
包目录不同
spring-beans.jar
中
org.springframework.beans.factory.BeanFactory
spring-context.jar
中
org.springframework.context.ApplicationContext
国际化
BeanFactory
是不支持国际化功能的,因为
BeanFactory
没有扩展
Spring
中
MessageResource
接口。相反,由于
ApplicationContext
扩展了
MessageResource
接口,因而具有消息处理的能力
(
i18N
)。
强大的事件机制(
Event
)
基本上牵涉到事件(
Event
)方面的设计,就离不开观察者模式,
ApplicationContext
的事件机制
主要通过
ApplicationEvent
和
ApplicationListener
这两个接口来提供的,和
Java swing
中的事件
机制一样。即当
ApplicationContext
中发布一个事件时,所有扩展了
ApplicationListener
的
Bean
都将接受到这个事件,并进行相应的处理。
底层资源的访问
ApplicationContext
扩展了
ResourceLoader
(资源加载器)接口,从而可以用来加载多个
Resource
,而
BeanFactory
是没有扩展
ResourceLoader
。
对
Web
应用的支持
与
BeanFactory
通常以编程的方式被创建,
ApplicationContext
能以声明的方式创建,如使用
ContextLoader
。
阿里内部资料
当然你也可以使用
ApplicationContext
的实现方式之一,以编程的方式创建
ApplicationContext
实例。
延迟加载
1. BeanFactroy
采用的是延迟加载形式来注入
Bean
的,即只有在使用到某个
Bean
时
(
调用
getBean())
,才对该
Bean
进行加载实例化。这样,我们就不能发现一些存在的
spring
的配置
问题。而
ApplicationContext
则相反,它是在容器启动时,一次性创建了所有的
Bean
。这
样,在容器启动时,我们就可以发现
Spring
中存在的配置错误。
2. BeanFactory
和
ApplicationContext
都支持
BeanPostProcessor
、
BeanFactoryPostProcessor
的使用。两者之间的区别是:
BeanFactory
需要手动注册,而
ApplicationContext
则是自动注册。
可以看到,
ApplicationContext
继承了
BeanFactory
,
BeanFactory
是
Spring
中比较原始的
Factory
,它不支持
AOP
、
Web
等
Spring
插件。而
ApplicationContext
不仅包含了
BeanFactory
的所有功能,还支持
Spring
的各种插件,还以一种面向框架的方式工作以及对上下文进行分层和实
现继承。
BeanFactory
是
Spring
框架的基础设施,面向
Spring
本身;而
ApplicationContext
面向使用
Spring
的开发者,相比
BeanFactory
提供了更多面向实际应用的功能,几乎所有场合都可以直接使
用
ApplicationContext
,而不是底层的
BeanFactory
。
常用容器
BeanFactory
类型的有
XmlBeanFactory
,它可以根据
XML
文件中定义的内容,创建相应的
Bean
。
ApplicationContext
类型的常用容器有:
1. ClassPathXmlApplicationContext
:从
ClassPath
的
XML
配置文件中读取上下文,并生成上
下文定义。应用程序上下文从程序环境变量中取得。
2. FileSystemXmlApplicationContext
:由文件系统中的
XML
配置文件读取上下文。
3. XmlWebApplicationContext
:由
Web
应用的
XML
文件读取上下文。例如我们在
Spring MVC
使用的情况。
16
、
Spring
框架中的单例
Bean
是线程安全的么?
Spring
框架并没有对单例
Bean
进行任何多线程的封装处理。
关于单例
Bean
的线程安全和并发问题,需要开发者自行去搞定。
单例的线程安全问题,并不是
Spring
应该去关心的。
Spring
应该做的是,提供根据配置,创
建单例
Bean
或多例
Bean
的功能。
阿里内部资料
当然,但实际上,大部分的
Spring Bean
并没有可变的状态,所以在某种程度上说
Spring
的单例
Bean
是线程安全的。如果你的
Bean
有多种状态的话,就需要自行保证线程安全。最浅显的解决办
法,就是将多态
Bean
的作用域(
Scope
)由
Singleton
变更为
Prototype
。
17
、
Spring
是怎么解决循环依赖的?
整个流程大致如下:
1.
首先
A
完成初始化第一步并将自己
提前曝光
出来(通过
ObjectFactory
将自己提前曝光),在
初始化的时候,发现自己依赖对象
B
,此时就会去尝试
get(B)
,这个时候发现
B
还没有被创建
出来;
2.
然后
B
就走创建流程,在
B
初始化的时候,同样发现自己依赖
C
,
C
也没有被创建出来;
3.
这个时候
C
又开始初始化进程,但是在初始化的过程中发现自己依赖
A
,于是尝试
get(A)
。这
个时候由于
A
已经添加至缓存中(一般都是添加至三级缓存
singletonFactories
),通过
ObjectFactory
提前曝光,所以可以通过
ObjectFactory#getObject()
方法来拿到
A
对象。
C
拿
到
A
对象后顺利完成初始化,然后将自己添加到一级缓存中;
4.
回到
B
,
B
也可以拿到
C
对象,完成初始化,
A
可以顺利拿到
B
完成初始化。到这里整个链路
就已经完成了初始化过程了。
关键字:三级缓存,提前曝光。
18
、说说事务的隔离级别
未提交读
(Read Uncommitted)
:允许脏读,也就是可能读取到其他会话中未提交事务修改的数据
提交读
(Read Committed)
:只能读取到已经提交的数据。
Oracle
等多数数据库默认都是该级别
(
不
重复读
)
阿里内部资料
可重复读
(Repeated Read)
:在同一个事务内的查询都是事务开始时刻一致的,
Mysql
的
InnoDB
默
认级别。在
SQL
标准中,该隔离级别消除了不可重复读,但是还存在幻读(多个事务同时修改同一
条记录,事务之间不知道彼此存在,当事务提交之后,后面的事务修改的数据将会覆盖前事务,前
一个事务就像发生幻觉一样)
可串行化
(Serializable)
:完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞。
不可重复读和幻读的区别主要是:解决不可重复读需要锁定了当前满足条件的记录,而解决幻读需
要锁定当前满足条件的记录及相近的记录。比如查询某个商品的信息,可重复读事务隔离级别可以
保证当前商品信息被锁定,解决不可重复读;但是如果统计商品个数,中途有记录插入,可重复读
事务隔离级别就不能保证两个事务统计的个数相同。
19
、说说事务的传播级别
Spring
事务定义了
7
种传播机制:
1. PROPAGATION_REQUIRED:
默认的
Spring
事物传播级别,若当前存在事务,则加入该事务,若
不存在事务,则新建一个事务。
2. PAOPAGATION_REQUIRE_NEW:
若当前没有事务,则新建一个事务。若当前存在事务,则新建
一个事务,新老事务相互独立。外部事务抛出异常回滚不会影响内部事务的正常提交。
3. PROPAGATION_NESTED:
如果当前存在事务,则嵌套在当前事务中执行。如果当前没有事务,
则新建一个事务,类似于
REQUIRE_NEW
。
4. PROPAGATION_SUPPORTS:
支持当前事务,若当前不存在事务,以非事务的方式执行。
5. PROPAGATION_NOT_SUPPORTED:
以非事务的方式执行,若当前存在事务,则把当前事务挂
起。
6. PROPAGATION_MANDATORY:
强制事务执行,若当前不存在事务,则抛出异常
.
7. PROPAGATION_NEVER:
以非事务的方式执行,如果当前存在事务,则抛出异常。
Spring
事务传播级别一般不需要定义,默认就是
PROPAGATION_REQUIRED
,除非在嵌套事务的情
况下需要重点了解。
20
、
Spring
事务实现方式
编程式事务管理:这意味着你可以通过编程的方式管理事务,这种方式带来了很大的灵活性,但很
难维护。
声明式事务管理:这种方式意味着你可以将事务管理和业务代码分离。你只需要通过注解或者
XML
配置管理事务。
阿里内部资料
21
、
Spring
框架的事务管理有哪些优点
它为不同的事务
API(
如
JTA, JDBC, Hibernate, JPA,
和
JDO)
提供了统一的编程模型。它为编程式事务
管理提供了一个简单的
API
而非一系列复杂的事务
API(
如
JTA).
它支持声明式事务管理。它可以和
Spring
的多种数据访问技术很好的融合。
它为不同的事务
API(
如
JTA, JDBC, Hibernate, JPA,
和
JDO)
提供了统一的编程模型。它为编程式事务
管理提供了一个简单的
API
而非一系列复杂的事务
API(
如
JTA).
它支持声明式事务管理。它可以和
Spring
的多种数据访问技术很好的融合。
它为不同的事务
API(
如
JTA, JDBC, Hibernate, JPA,
和
JDO)
提供了统一的编程模型。它为编程式事务
管理提供了一个简单的
API
而非一系列复杂的事务
API(
如
JTA).
它支持声明式事务管理。它可以和
Spring
的多种数据访问技术很好的融合。
22
、事务三要素是什么?
数据源
:表示具体的事务性资源,是事务的真正处理者,如
MySQL
等。
事务管理器
:像一个大管家,从整体上管理事务的处理过程,如打开、提交、回滚等。
事务应用和属性配置
:像一个标识符,表明哪些方法要参与事务,如何参与事务,以及一些相关属
性如隔离级别、超时时间等。
23
、 事务注解的本质是什么?
@Transactional
这个注解仅仅是一些(和事务相关的)元数据,在运行时被事务基础设施读取消
费,并
使用这些元数据来配置
bean
的事务行为
。 大致来说具有两方面功能,
一是表明该方法要参
与事务,二是配置相关属性来定制事务的参与方式和运行行为
声明式事务主要是得益于
Spring AOP
。使用一个事务拦截器,在方法调用的前后
/
周围进行事务性
增强(
advice
),来驱动事务完成。
@Transactional
注解既可以标注在类上,也可以标注在方法上。当在类上时,默认应用到类里的所
有方法。如果此时方法上也标注了,则方法上的优先级高。 另外注意方法一定要是
public
的。
MyBatis
篇
1
、什么是
MyBatis
(
1
)
Mybatis
是一个半
ORM
(对象关系映射)框架,它内部封装了
JDBC
,开发时只需要关注
SQL
语句本身,不需要花费精力去处理加载驱动、创建连接、创建
statement
等繁杂的过程。程序员直
接编写原生态
sql
,可以严格控制
sql
执行性能,灵活度高。
阿里内部资料
(
2
)
MyBatis
可以使用
XML
或注解来配置和映射原生信息,将
POJO
映射成数据库中的记录,避
免了几乎所有的
JDBC
代码和手动设置参数以及获取结果集。
(
3
)通过
xml
文件或注解的方式将要执行的各种
statement
配置起来,并通过
java
对象和
statement
中
sql
的动态参数进行映射生成最终执行的
sql
语句,最后由
mybatis
框架执行
sql
并将结果
映射为
java
对象并返回。(从执行
sql
到返回
result
的过程)。
2
、说说
MyBatis
的优点和缺点
优点:
(
1
)基于
SQL
语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,
SQL
写
在
XML
里,解除
sql
与程序代码的耦合,便于统一管理;提供
XML
标签,支持编写动态
SQL
语句,并
可重用。
(
2
)与
JDBC
相比,减少了
50%
以上的代码量,消除了
JDBC
大量冗余的代码,不需要手动开关连
接;
(
3
)很好的与各种数据库兼容(因为
MyBatis
使用
JDBC
来连接数据库,所以只要
JDBC
支持的数据
库
MyBatis
都支持)。
(
4
)能够与
Spring
很好的集成;
(
5
)提供映射标签,支持对象与数据库的
ORM
字段关系映射;提供对象关系映射标签,支持对象
关系组件维护。
缺点
(
1
)
SQL
语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写
SQL
语句的功底有
一定要求。
(
2
)
SQL
语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。
3
、
#{}
和
${}
的区别是什么?
#{}
是预编译处理,
${}
是字符串替换。
Mybatis
在处理
#{}
时,会将
sql
中的
#{}
替换为
?
号,调用
PreparedStatement
的
set
方法来赋值;
Mybatis
在处理
${}
时,就是把
${}
替换成变量的值。
使用
#{}
可以有效的防止
SQL
注入,提高系统安全性。
4
、当实体类中的属性名和表中的字段名不一样 ,怎么办 ?
第
1
种:
通过在查询的
sql
语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。
阿里内部资料
<select id=”selectorder” parametertype=”int” resultetype=”me.gacl.domain.order”>
select order_id id, order_no orderno ,order_price price form orders where
order_id=#{id};
</select>
第
2
种:
通过来映射字段名和实体类属性名的一一对应的关系。
<select id="getOrder" parameterType="int" resultMap="orderresultmap">
select * from orders where order_id=#{id}
</select>
<resultMap type=”me.gacl.domain.order” id=”orderresultmap”>
<!–
用
id
属性来映射主键字段
–>
<id property=”id” column=”order_id”>
<!–
用
result
属性来映射非主键字段,
property
为实体类属性名,
column
为数据表中的属性
–>
<result property = “orderno” column =”order_no”/>
<result property=”price” column=”order_price” />
</reslutMap>
5
、
Mybatis
是如何进行分页的?分页插件的原理是什么?
Mybatis
使用
RowBounds
对象进行分页,它是针对
ResultSet
结果集执行的内存分页,而非物理分
页。可以在
sql
内直接拼写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物
理分页,比如:
MySQL
数据的时候,在原有
SQL
后面拼写
limit
。
分页插件的基本原理是使用
Mybatis
提供的插件接口,实现自定义插件,在插件的拦截方法内拦截
待执行的
sql
,然后重写
sql
,根据
dialect
方言,添加对应的物理分页语句和物理分页参数。
6
、
Mybatis
是如何将
sql
执行结果封装为目标对象并返回的?都有
哪些映射形式?
第一种是使用标签,逐一定义数据库列名和对象属性名之间的映射关系。
第二种是使用
sql
列的别名功能,将列的别名书写为对象属性名。
有了列名与属性名的映射关系后,
Mybatis
通过反射创建对象,同时使用反射给对象的属性逐一赋
值并返回,那些找不到映射关系的属性,是无法完成赋值的。
7
、 如何执行批量插入?
首先
,
创建一个简单的
insert
语句
:
阿里内部资料
<insert id=”insertname”>
insert into names (name) values (#{value})
</insert>
然后在
java
代码中像下面这样执行批处理插入
:
list<string> names = new arraylist();
names.add(“fred”);
names.add(“barney”);
names.add(“betty”);
names.add(“wilma”);
//
注意这里
executortype.batch
sqlsession sqlsession = sqlsessionfactory.opensession(executortype.batch);
try {
namemapper mapper = sqlsession.getmapper(namemapper.class);
for (string name : names) {
mapper.insertname(name);
}
sqlsession.commit();
}catch(Exception e){
e.printStackTrace();
sqlSession.rollback();
throw e;
}
finally {
sqlsession.close();
}
8
、
Xml
映射文件中,除了常见的
select|insert|updae|delete
标签之外,还有哪些标签?
加上动态
sql
的
9
个标签,其中为
sql
片段标签,通过标签引入
sql
片段,为不支持自增的主键生成策
略标签。
9
、
MyBatis
实现一对一有几种方式
?
具体怎么操作的?
有联合查询和嵌套查询
,
联合查询是几个表联合查询
,
只查询一次
,
通过在
resultMap
里面配置
association
节点配置一对一的类就可以完成;
嵌套查询是先查一个表,根据这个表里面的结果的 外键
id
,去再另外一个表里面查询数据
,
也是通过
association
配置,但另外一个表的查询通过
select
属性配置。
阿里内部资料
10
、
Mybatis
是否支持延迟加载?如果支持,它的实现原理是什
么?
Mybatis
仅支持
association
关联对象和
collection
关联集合对象的延迟加载,
association
指的就是一
对一,
collection
指的就是一对多查询。在
Mybatis
配置文件中,可以配置是否启用延迟加载
lazyLoadingEnabled=true|false
。
它的原理是,使用
CGLIB
创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调
用
a.getB().getName()
,拦截器
invoke()
方法发现
a.getB()
是
null
值,那么就会单独发送事先保存好的
查询关联
B
对象的
sql
,把
B
查询上来,然后调用
a.setB(b)
,于是
a
的对象
b
属性就有值了,接着完成
a.getB().getName()
方法的调用。这就是延迟加载的基本原理。
当然了,不光是
Mybatis
,几乎所有的包括
Hibernate
,支持延迟加载的原理都是一样的。
11
、说说
Mybatis
的缓存机制
:
Mybatis
整体:
一级缓存
localCache
在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的
SQL
,
MyBatis
提供了一级缓存的方案优化这部分场景,如果是相同的
SQL
语句,会优先命中一级缓存,
避免直接对数据库进行查询,提高性能。
阿里内部资料
每个
SqlSession
中持有了
Executor
,每个
Executor
中有一个
LocalCache
。当用户发起查询时,
MyBatis
根据当前执行的语句生成
MappedStatement
,在
Local Cache
进行查询,如果缓存命中
的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入
Local Cache
,最后
返回结果给用户。具体实现类的类关系图如下图所示:
1. MyBatis
一级缓存的生命周期和
SqlSession
一致。
2. MyBatis
一级缓存内部设计简单,只是一个没有容量限定的
HashMap
,在缓存的功能性上有
所欠缺。
3. MyBatis
的一级缓存最大范围是
SqlSession
内部,有多个
SqlSession
或者分布式的环境下,
数据库写操作会引起脏数据,建议设定缓存级别为
Statement
。
二级缓存
在上文中提到的一级缓存中,其最大的共享范围就是一个
SqlSession
内部,如果多个
SqlSession
之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用
CachingExecutor
装饰
Executor
,进入一级缓存的查询流程前,先在
CachingExecutor
进行二级缓存的查询,具体的工作
流程如下所示。
阿里内部资料
二级缓存开启后,同一个
namespace
下的所有操作语句,都影响着同一个
Cache
,即二级缓存被
多个
SqlSession
共享,是一个全局的变量。
当开启缓存后,数据的查询执行的流程为:
二级缓存
->
一级缓存
->
数据库
1. MyBatis
的二级缓存相对于一级缓存来说,实现了
SqlSession
之间缓存数据的共享,同时粒度
更加细,能够到
namespace
级别,通过
Cache
接口实现类不同的组合,对
Cache
的可控性
也更强。
2. MyBatis
在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件
比较苛刻。
3.
在分布式环境下,由于默认的
MyBatis Cache
实现都是基于本地的,分布式环境下必然会出现
读取到脏数据,需要使用集中式缓存将
MyBatis
的
Cache
接口实现,有一定的开发成本,直
接使用
Redis
、
Memcached
等分布式缓存可能成本更低,安全性也更高。
12
、
JDBC
编程有哪些步骤?
1.
装载相应的数据库的
JDBC
驱动并进行初始化:
2.
建立
JDBC
和数据库之间的
Connection
连接:
Class
.
forName
(
"com.mysql.jdbc.Driver"
);
Connection c
=
DriverManager
.
getConnection
(
"jdbc:mysql://127.0.0.1:3306/test?
characterEncoding=UTF-8"
,
"root"
,
"123456"
);
阿里内部资料
3.
创建
Statement
或者
PreparedStatement
接口,执行
SQL
语句。
4.
处理和显示结果。
5.
释放资源。
13
、
MyBatis
中见过什么设计模式?
14
、
MyBatis
中比如
UserMapper.java
是接口,为什么没有实
现类还能调用?
使用
JDK
动态代理
+MapperProxy
。本质上调用的是
MapperProxy
的
invoke
方法。
欢迎关注微信公众号:
Java
后端技术全栈
SpringBoot
篇
1
、为什么要用
SpringBoot
Spring Boot
优点非常多,如:
一、独立运行
Spring Boot
而且内嵌了各种
servlet
容器,
Tomcat
、
Jetty
等,现在不再需要打成
war
包部署到容器
中,
Spring Boot
只要打成一个可执行的
jar
包就能独立运行,所有的依赖包都在一个
jar
包内。
二、简化配置
spring-boot-starter-web
启动器自动依赖其他组件,简少了
maven
的配置。 三、自动配置
阿里内部资料
Spring Boot
能根据当前类路径下的类、
jar
包来自动配置
bean
,如添加一个
spring-boot-starter
web
启动器就能拥有
web
的功能,无需其他配置。
四、无代码生成和
XML
配置
Spring Boot
配置过程中无代码生成,也无需
XML
配置文件就能完成所有配置工作,这一切都是借助
于条件注解完成的,这也是
Spring4.x
的核心功能之一。
五、应用监控
Spring Boot
提供一系列端点可以监控服务及应用,做健康检测。
2
、
Spring Boot
的核心注解是哪个?它主要由哪几个注解组成
的?
启动类上面的注解是
@SpringBootApplication
,它也是
Spring Boot
的核心注解,主要组合包含了
以下
3
个注解:
@SpringBootConfifiguration
:组合了
@Confifiguration
注解,实现配置文件的功能。
@EnableAutoConfifiguration
:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数
据源自动配置功能:
@SpringBootApplication(exclude = { DataSourceAutoConfifiguration.class
})
。
@ComponentScan
:
Spring
组件扫描。
3
、运行
Spring Boot
有哪几种方式?
1
)打包用命令或者放到容器中运行
2
)用
Maven/Gradle
插件运行
3
)直接执行
main
方法运行
4
、如何理解
Spring Boot
中的
Starters
?
Starters
是什么:
Starters
可以理解为启动器,它包含了一系列可以集成到应用里面的依赖包,你可以一站式集成
Spring
及其他技术,而不需要到处找示例代码和依赖包。如你想使用
Spring JPA
访问数据库,只要
加入
spring-boot-starter-data-jpa
启动器依赖就能使用了。
Starters
包含了许多项目中需要用到的
依赖,它们能快速持续的运行,都是一系列得到支持的管理传递性依赖。
Starters
命名:
阿里内部资料
Spring Boot
官方的启动器都是以
spring-boot-starter-
命名的,代表了一个特定的应用类型。第三
方的启动器不能以
spring-boot
开头命名,它们都被
Spring Boot
官方保留。一般一个第三方的应该
这样命名,像
mybatis
的
mybatis-spring-boot-starter
。
Starters
分类:
1. Spring Boot
应用类启动器
1. Spring Boot
生产启动器
1. Spring Boot
技术类启动器
1.
其他第三方启动器
5
、 如何在
Spring Boot
启动的时候运行一些特定的代码?
如果你想在
Spring Boot
启动的时候运行一些特定的代码,你可以实现接口
ApplicationRunner
或
者
CommandLineRunner
,这两个接口实现方式一样,它们都只提供了一个
run
方法。
CommandLineRunner
:启动获取命令行参数
6
、
Spring Boot
需要独立的容器运行吗?
阿里内部资料
可以不需要,内置了
Tomcat/ Jetty
等容器。
7
、
Spring Boot
中的监视器是什么?
Spring boot actuator
是
spring
启动框架中的重要功能之一。
Spring boot
监视器可帮助您访问生产
环境中正在运行的应用程序的当前状态。有几个指标必须在生产环境中进行检查和监控。即使一些
外部应用程序可能正在使用这些服务来向相关人员触发警报消息。监视器模块公开了一组可直接作
为
HTTP URL
访问的
REST
端点来检查状态。
8
、 如何使用
Spring Boot
实现异常处理?
Spring
提供了一种使用
ControllerAdvice
处理异常的非常有用的方法。 我们通过实现一个
ControlerAdvice
类,来处理控制器类抛出的所有异常。
9
、 你如何理解
Spring Boot
中的
Starters
?
Starters
可以理解为启动器,它包含了一系列可以集成到应用里面的依赖包,你可以一站式集成
Spring
及其他技术,而不需要到处找示例代码和依赖包。如你想使用
Spring JPA
访问数据库,只要
加入
spring-boot-starter-data-jpa
启动器依赖就能使用了。
10
、
springboot
常用的
starter
有哪些
spring-boot-starter-web
嵌入
tomcat
和
web
开发需要
servlet
与
jsp
支持
spring-boot-starter-data-jpa
数据库支持
spring-boot-starter-data-redis redis
数据库支持
spring-boot-starter-data-solr solr
支持
mybatis-spring-boot-starter
第三方的
mybatis
集成
starter
11
、
SpringBoot
实现热部署有哪几种方式?
主要有两种方式:
Spring Loaded
Spring-boot-devtools
12
、 如何理解
Spring Boot
配置加载顺序?
在
Spring Boot
里面,可以使用以下几种方式来加载配置。
1
)
properties
文件;
2
)
YAML
文件;
阿里内部资料
3
)系统环境变量;
4
)命令行参数;
等等
……
13
、
Spring Boot
的核心配置文件有哪几个?它们的区别是什
么?
pring Boot
的核心配置文件是
application
和
bootstrap
配置文件。
application
配置文件这个容易理解,主要用于
Spring Boot
项目的自动化配置。
bootstrap
配置文件有以下几个应用场景。
使用
Spring Cloud Confifig
配置中心时,这时需要在
bootstrap
配置文件中添加连接到配置中
心的配置属性来加载外部配置中心的配置信息;
一些固定的不能被覆盖的属性;
一些加密
/
解密的场景;
14
、如何集成
Spring Boot
和
ActiveMQ
?
对于集成
Spring Boot
和
ActiveMQ
,我们使用
spring-boot-starter-activemq
依赖关系。 它只需
要很少的配置,并且不需要样板代码。
MySQL
篇
1
、数据库的三范式是什么
第一范式:列不可再分 第二范式:行可以唯一区分,主键约束 第三范式:表的非主属性不能依赖与
其他表的非主属性 外键约束 且三大范式是一级一级依赖的,第二范式建立在第一范式上,第三范式
建立第一第二范式上。
2
、
MySQL
数据库引擎有哪些
如何查看
mysql
提供的所有存储引擎
mysql> show engines;
阿里内部资料
mysql
常用引擎包括:
MYISAM
、
Innodb
、
Memory
、
MERGE
MYISAM
:全表锁,拥有较高的执行速度,不支持事务,不支持外键,并发性能差,占用空间
相对较小,对事务完整性没有要求,以
select
、
insert
为主的应用基本上可以使用这引擎
Innodb:
行级锁,提供了具有提交、回滚和崩溃回复能力的事务安全,支持自动增长列,支持
外键约束,并发能力强,占用空间是
MYISAM
的
2.5
倍,处理效率相对会差一些
Memory:
全表锁,存储在内容中,速度快,但会占用和数据量成正比的内存空间且数据在
mysql
重启时会丢失,默认使用
HASH
索引,检索效率非常高,但不适用于精确查找,主要用于
那些内容变化不频繁的代码表
MERGE
:是一组
MYISAM
表的组合
3
、说说
InnoDB
与
MyISAM
的区别
1. InnoDB
支持事务,
MyISAM
不支持,对于
InnoDB
每一条
SQL
语言都默认封装成事务,自动提
交,这样会影响速度,所以最好把多条
SQL
语言放在
begin
和
commit
之间,组成一个事务;
2. InnoDB
支持外键,而
MyISAM
不支持。对一个包含外键的
InnoDB
表转为
MYISAM
会失败;
3. InnoDB
是聚集索引,数据文件是和索引绑在一起的,必须要有主键,通过主键索引效率很高。
但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该
过大,因为主键太大,其他索引也都会很大。而
MyISAM
是非聚集索引,数据文件是分离的,
索引保存的是数据文件的指针。主键索引和辅助索引是独立的。
4. InnoDB
不保存表的具体行数,执行
select count(*) from table
时需要全表扫描。而
MyISAM
用
一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快;
5. Innodb
不支持全文索引,而
MyISAM
支持全文索引,查询效率上
MyISAM
要高;
4
、数据库的事务
什么是事务?:
多条
sql
语句,要么全部成功,要么全部失败。
阿里内部资料
事务的特性:
数据库事务特性:原子性
(Atomic)
、一致性
(Consistency)
、隔离性
(Isolation)
、持久性
(Durabiliy)
。简称
ACID
。
原子性:组成一个事务的多个数据库操作是一个不可分割的原子单元,只有所有操作都成功,
整个事务才会提交。任何一个操作失败,已经执行的任何操作都必须撤销,让数据库返回初始
状态。
一致性:事务操作成功后,数据库所处的状态和它的业务规则是一致的。即数据不会被破坏。
如
A
转账
100
元给
B
,不管操作是否成功,
A
和
B
的账户总额是不变的。
隔离性:在并发数据操作时,不同的事务拥有各自的数据空间,它们的操作不会对彼此产生干
扰
持久性:一旦事务提交成功,事务中的所有操作都必须持久化到数据库中。
5
、索引是什么
官方介绍索引是帮助
MySQL
高效获取数据
的
数据结构
。更通俗的说,数据库索引好比是一本书
前面的目录,能
加快数据库的查询速度
。
一般来说索引本身也很大,不可能全部存储在内存中,因此
索引往往是存储在磁盘上的文件中
的
(可能存储在单独的索引文件中,也可能和数据一起存储在数据文件中)。
我们通常所说的索引,包括聚集索引、覆盖索引、组合索引、前缀索引、唯一索引等,没有特
别说明,默认都是使用
B+
树结构组织(多路搜索树,并不一定是二叉的)的索引。
6
、
SQL
优化手段有哪些
1
、查询语句中不要使用
select *
2
、尽量减少子查询,使用关联查询(
left join,right join,inner join
)替代
3
、减少使用
IN
或者
NOT IN ,
使用
exists
,
not exists
或者关联查询语句替代
4
、
or
的查询尽量用
union
或者
union all
代替
(
在确认没有重复数据或者不用剔除重复数据时,
union all
会更好
)
5
、应尽量避免在
where
子句中使用
!=
或
<>
操作符,否则将引擎放弃使用索引而进行全表扫描。
6
、应尽量避免在
where
子句中对字段进行
null
值判断,否则将导致引擎放弃使用索引而进行全表
扫描,如:
select id from t where num is null
可以在
num
上设置默认值
0
,确保表中
num
列没有
null
值,然后这样查询:
select id from t where num=0
7
、简单说一说
drop
、
delete
与
truncate
的区别
SQL
中的
drop
、
delete
、
truncate
都表示删除,但是三者有一些差别
阿里内部资料
delete
和
truncate
只删除表的数据不删除表的结构 速度
,
一般来说
: drop> truncate >delete delete
语句是
dml,
这个操作会放到
rollback segement
中
,
事务提交之后才生效
;
如果有相应的
trigger,
执行
的时候将被触发
. truncate,drop
是
ddl,
操作立即生效
,
原数据不放到
rollback segment
中
,
不能回滚
.
操作不触发
trigger.
8
、什么是视图
视图是一种虚拟的表,具有和物理表相同的功能。可以对视图进行增,改,查,操作,试图通常是
有一个表或者多个表的行或列的子集。对视图的修改不影响基本表。它使得我们获取数据更容易,
相比多表查询。
9
、 什么是内联接、左外联接、右外联接?
内联接(
Inner Join
):匹配
2
张表中相关联的记录。
左外联接(
Left Outer Join
):除了匹配
2
张表中相关联的记录外,还会匹配左表中剩余的记
录,右表中未匹配到的字段用
NULL
表示。
右外联接(
Right Outer Join
):除了匹配
2
张表中相关联的记录外,还会匹配右表中剩余的记
录,左表中未匹配到的字段用
NULL
表示。在判定左表和右表时,要根据表名出现在
Outer Join
的左右位置关系。
10
、并发事务带来哪些问题
?
在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对
同一数据进行操作)。并发虽然是必须的,但可能会导致以下的问题。
脏读(
Dirty read
)
:
当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提
交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是
还没有提交的数据,那么另外一个事务读到的这个数据是
“
脏数据
”
,依据
“
脏数据
”
所做的操作可
能是不正确的。
丢失修改(
Lost to modify
)
:
指在一个事务读取一个数据时,另外一个事务也访问了该数
据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务
内的修改结果就被丢失,因此称为丢失修改。 例如:事务
1
读取某表中的数据
A=20
,事务
2
也
读取
A=20
,事务
1
修改
A=A-1
,事务
2
也修改
A=A-1
,最终结果
A=19
,事务
1
的修改被丢失。
不可重复读(
Unrepeatableread
)
:
指在一个事务内多次读同一数据。在这个事务还没有结
束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务
的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数
据是不一样的情况,因此称为不可重复读。
幻读(
Phantom read
)
:
幻读与不可重复读类似。它发生在一个事务(
T1
)读取了几行数
据,接着另一个并发事务(
T2
)插入了一些数据时。在随后的查询中,第一个事务(
T1
)就会
发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
不可重复读和幻读区别:
阿里内部资料
隔离级别
脏读
不可重复读
幻影读
READ-UNCOMMITTED
√
√
√
READ-COMMITTED
×
√
√
REPEATABLE-READ
×
×
√
SERIALIZABLE
×
×
×
不可重复读的重点是修改比如多次读取一条记录发现其中某些列的值被修改,幻读的重点在于新增
或者删除比如多次读取一条记录发现记录增多或减少了。
11
,事务隔离级别有哪些
?MySQL
的默认隔离级别是
?
SQL
标准定义了四个隔离级别:
READ-UNCOMMITTED(
读取未提交
)
:
最低的隔离级别,允许读取尚未提交的数据变更,
可
能会导致脏读、幻读或不可重复读
。
READ-COMMITTED(
读取已提交
)
:
允许读取并发事务已经提交的数据,
可以阻止脏读,但是
幻读或不可重复读仍有可能发生
。
REPEATABLE-READ(
可重复读
)
:
对同一字段的多次读取结果都是一致的,除非数据是被本身
事务自己所修改,
可以阻止脏读和不可重复读,但幻读仍有可能发生
。
SERIALIZABLE(
可串行化
)
:
最高的隔离级别,完全服从
ACID
的隔离级别。所有的事务依次逐
个执行,这样事务之间就完全不可能产生干扰,也就是说,
该级别可以防止脏读、不可重复读
以及幻读
。
MySQL InnoDB
存储引擎的默认支持的隔离级别是
REPEATABLE-READ
(可重读)
。我们可以通
过
SELECT @@tx_isolation
;
命令来查看
mysql> SELECT @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
这里需要注意的是:与
SQL
标准不同的地方在于
InnoDB
存储引擎在
REPEATABLE-READ
(可重
读)
事务隔离级别下使用的是
Next-Key Lock
锁算法,因此可以避免幻读的产生,这与其他数据库
系统
(
如
SQL Server)
是不同的。所以说
InnoDB
存储引擎的默认支持的隔离级别是
REPEATABLE
READ
(可重读)
已经可以完全保证事务的隔离性要求,即达到了
SQL
标准的
SERIALIZABLE(
可串
行化
)
隔离级别。因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是
阿里内部资料
READ-COMMITTED(
读取提交内容
)
,但是你要知道的是
InnoDB
存储引擎默认使用
REPEAaTABLE-READ
(可重读)
并不会有任何性能损失。
InnoDB
存储引擎在
分布式事务
的情况下一般会用到
SERIALIZABLE(
可串行化
)
隔离级别。
12
,大表如何优化?
当
MySQL
单表记录数过大时,数据库的
CRUD
性能会明显下降,一些常见的优化措施如下:
1.
限定数据的范围
务必禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时候,我们
可以控制在一个月的范围内;
2.
读
/
写分离
经典的数据库拆分方案,主库负责写,从库负责读;
3.
垂直分区
根据数据库里面数据表的相关性进行拆分。
例如,用户表中既有用户的登录信息又有用户的基本信
息,可以将用户表拆分成两个单独的表,甚至放到单独的库做分库。
简单来说垂直拆分是指数据表列的拆分,把一张列比较多的表拆分为多张表。
如下图所示,这样来
说大家应该就更容易理解了。
1583307481617
垂直拆分的优点:
可以使得列数据变小,在查询时减少读取的
Block
数,减少
I/O
次数。此外,
垂直分区可以简化表的结构,易于维护。
垂直拆分的缺点:
主键会出现冗余,需要管理冗余列,并会引起
Join
操作,可以通过在应用层
进行
Join
来解决。此外,垂直分区会让事务变得更加复杂;
4.
水平分区
保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表或者库中,达
到了分布式的目的。 水平拆分可以支撑非常大的数据量。
水平拆分是指数据表行的拆分,表的行数超过
200
万行时,就会变慢,这时可以把一张的表的数据
拆成多张表来存放。举个例子:我们可以将用户信息表拆分成多个用户信息表,这样就可以避免单
一表数据量过大对性能造成影响。
1583308353521
阿里内部资料
水平拆分可以支持非常大的数据量。需要注意的一点是:分表仅仅是解决了单一表数据过大的问
题,但由于表的数据还是在同一台机器上,其实对于提升
MySQL
并发能力没有什么意义,所以
水平
拆分最好分库
。
水平拆分能够
支持非常大的数据量存储,应用端改造也少
,但
分片事务难以解决
,跨节点
Join
性能
较差,逻辑复杂。《
Java
工程师修炼之道》的作者推荐
尽量不要对数据进行分片,因为拆分会带来
逻辑、部署、运维的各种复杂度
,一般的数据表在优化得当的情况下支撑千万以下的数据量是没有
太大问题的。如果实在要分片,尽量选择客户端分片架构,这样可以减少一次和中间件的网络
I/O
。
下面补充一下数据库分片的两种常见方案:
客户端代理:
分片逻辑在应用端,封装在
jar
包中,通过修改或者封装
JDBC
层来实现。
当当网
的
Sharding-JDBC
、阿里的
TDDL
是两种比较常用的实现。
中间件代理:
在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中。
我们现
在谈的
Mycat
、
360
的
Atlas
、网易的
DDB
等等都是这种架构的实现。
详细内容可以参考:
MySQL
大表优化方案
:
https://segmentfault.com/a/1190000006158186
13
、分库分表之后
,id
主键如何处理?
因为要是分成多个表之后,每个表都是从
1
开始累加,这样是不对的,我们需要一个全局唯一的
id
来支持。
生成全局
id
有下面这几种方式:
UUID
:不适合作为主键,因为太长了,并且无序不可读,查询效率低。比较适合用于生成唯
一的名字的标示比如文件的名字。
数据库自增
id
:
两台数据库分别设置不同步长,生成不重复
ID
的策略来实现高可用。这种方式
生成的
id
有序,但是需要独立部署数据库实例,成本高,还会有性能瓶颈。
利用
redis
生成
id :
性能比较好,灵活方便,不依赖于数据库。但是,引入了新的组件造成系
统更加复杂,可用性降低,编码更加复杂,增加了系统成本。
Twitter
的
snowflflake
算法
:
Github
地址:
https://github.com/twitter-archive/snowflflake
。
美团的
Leaf
分布式
ID
生成系统
:
Leaf
是美团开源的分布式
ID
生成器,能保证全局唯一性、趋
势递增、单调递增、信息安全,里面也提到了几种分布式方案的对比,但也需要依赖关系数据
库、
Zookeeper
等中间件。感觉还不错。美团技术团队的一篇文章:
https://tech.meituan.co
m/2017/04/21/mt-leaf.html
。
14
、 说说在
MySQL
中一条查询
SQL
是如何执行的?
比如下面这条
SQL
语句:
1.
取得链接
,使用使用到
MySQL
中的连接器。
select
name
from
t_user
where
id=
1
阿里内部资料
2.
查询缓存
,
key
为
SQL
语句,
value
为查询结果,如果查到就直接返回。不建议使用次缓存,
在
MySQL 8.0
版本已经将查询缓存删除,也就是说
MySQL 8.0
版本后不存在此功能。
3.
分析器
,分为词法分析和语法分析。此阶段只是做一些
SQL
解析,语法校验。所以一般语法错
误在此阶段。
4.
优化器
,是在表里有多个索引的时候,决定使用哪个索引;或者一个语句中存在多表关联的时
候(
join
),决定各个表的连接顺序。
5.
执行器
,通过分析器让
SQL
知道你要干啥,通过优化器知道该怎么做,于是开始执行语句。执
行语句的时候还要判断是否具备此权限,没有权限就直接返回提示没有权限的错误;有权限则
打开表,根据表的引擎定义,去使用这个引擎提供的接口,获取这个表的第一行,判断
id
是都
等于
1
。如果是,直接返回;如果不是继续调用引擎接口去下一行,重复相同的判断,直到取
到这个表的最后一行,最后返回。
15
、索引有什么优缺点?
16
、
MySQL
中
varchar
与
char
的区别?
varchar(30)
中的
30
代表的涵义?
varchar
与
char
的区别,
char
是一种固定长度的类型,
varchar
则是一种可变长度的类型。
varchar(30)
中
30
的涵义最多存放
30
个字符。
varchar(30)
和
(130)
存储
hello
所占空间一
样,但后者在排序时会消耗更多内存,因为
ORDER BY col
采用
fifixed_length
计算
col
长度
(
memory
引擎也一样)。
对效率要求高用
char
,对空间使用要求高用
varchar
。
17
、
int(11)
中的
11
代表什么涵义?
int(11)
中的
11
,不影响字段存储的范围,只影响展示效果。
18
、 为什么
SELECT COUNT(*) FROM table
在
InnoDB
比
MyISAM
慢?
阿里内部资料
对于
SELECT COUNT(*) FROM table
语句,在没有
WHERE
条件的情况下,
InnoDB
比
MyISAM
可
能会慢很多,尤其在大表的情况下。因为,
InnoDB
是去实时统计结果,会
全表扫描
;而
MyISAM
内部维持了一个计数器,预存了结果,所以直接返回即可。
19.
说说
InnoDB
与
MyISAM
有什么区别?
在
MySQL 5.1
及之前的版本中,
MyISAM
是默认的存储引擎,而在
MySQL 5.5
版本以后,默
认使用
InnoDB
存储引擎。
MyISAM
不支持行级锁,换句话说,
MyISAM
会对整张表加锁,而不是针对行。同时,
MyISAM
不支持事务和外键。
MyISAM
可被压缩,存储空间较小,而且
MyISAM
在筛选大量数
据时非常快。
InnoDB
是事务型引擎,当事务异常提交时,会被回滚。同时,
InnoDB
支持行锁。此外,
InnoDB
需要更多存储空间,会在内存中建立其专用的缓冲池用于高速缓冲数据和索引。
InnoDB
支持自动奔溃恢复特性。
建议:一般情况下,个人建议优先选择
InnoDB
存储引擎,并且尽量不要将
InnoDB
与
MyISAM
混
合使用。
20
、
MySQL
索引类型有哪些?
主键索引
索引列中的值必须是唯一的,不允许有空值。
普通索引
MySQL
中基本索引类型,没有什么限制,允许在定义索引的列中插入重复值和空值。
唯一索引
索引列中的值必须是唯一的,但是允许为空值。
全文索引
只能在文本类型
CHAR,VARCHAR,TEXT
类型字段上创建全文索引。字段长度比较大时,如果创建普
通索引,在进行
like
模糊查询时效率比较低,这时可以创建全文索引。
MyISAM
和
InnoDB
中都可以
使用全文索引。
空间索引
MySQL
在
5.7
之后的版本支持了空间索引,而且支持
OpenGIS
几何数据模型。
MySQL
在空间索引这
方面遵循
OpenGIS
几何数据模型规则。
前缀索引
在文本类型如
CHAR,VARCHAR,TEXT
类列上创建索引时,可以指定索引列的长度,但是数值类型不
能指定。
按照索引列数量分类)
1.
单列索引
2.
组合索引
组合索引的使用,需要遵循
最左前缀匹配原则(最左匹配原则)
。一般情况下在条件允许的情
况下使用组合索引替代多个单列索引使用。