Java面试笔记
基础
面向对象
是一种思想,可以让我们从执行者变成指挥者,是复杂的问题简单化
-
封装:将事物封装成一个类,减少耦合,隐藏细节。保留特定接口和外部联系
-
继承:从已知的类中派生出一个新的类,可以通过覆盖/重写增强功能
- Java中类的初始化顺序:
- 父类静态成员变量、静态代码块;子类静态成员变量、静态代码块
- 父类普通成员变量和代码块,再执行父类构造方法
- 子类普通成员变量和代码块,再执行父类构造方法
- 子类特点:
- 父类非private的属性和方法
- 添加自己的方法和属性,对父类进行扩展
- 重新定义父类的方法=方法的覆盖/重写
- Java中类的初始化顺序:
-
多态:本质是一个程序中存在多个同名的不同方法
-
子类的覆盖实现
-
方法的重载实现
-
子类作为父类对象使用
// 转型指从右往左转 // 向上转型:可以直接转 Father f =new Son(); // 向下转型:需要强制类型转换 Son s = (Son)f;
-
什么方法重载、方法重写?
重载(overload):一个类中存在多个同名的不同方法,这些方法的参数个数、顺序和类型不同均可以构成方法重载
重写(override):子类对父类非私有方法的重新编写,涉及写的就会有子父类
如果只有方法返回值不同,可以构成重载吗?
不可以,因为我们调用某个方法,并不关心返回值。编译器根据方法名和参数无法确定我们调用的是那个方法。
抽象类与接口
主要区别:
- 方法:抽象类中可以没有抽象方法,也可以非抽象方法、抽象方法并存;接口中的方法在JDK8以前只能是抽象的,8以后提供了接口方法的default实现
- 为什么会出现接口中的默认方法?
- 为了给已存在的接口提供新的实现,不需要修改所有实例,指定重写就好
- 继承和实现:抽象类和类一样只能是单继承的;接口可以多实现
- 成员变量:抽象类中可以存在普通的成员变量;接口中成员变量必须是static final类型,必须被初始化,只能有常量
接口和抽象类该如何选择?
当我们仅仅只需要定义一些抽象方法而不需要额外的具体方法/变量的时候,用接口;反之,考虑抽象类
接口的普通方法、default修饰方法:
public interface MyInterface {
// 接口的普通方法只能等待实现类实现,不能具体定义
void test();
// 但是JDK8以后,接口可以default声明,然后具体定义
default void say(String message) {
System.out.println("hello:"+message);
}
}
public class MyInterfaceImpl implements MyInterface {
// 实现接口里的抽象方法
@Override
public void test() {
System.out.println("test被执行");
}
// 当然也可以重写say方法
public static void main(String[] args) {
MyInterfaceImpl client = new MyInterfaceImpl();
client.test();
client.say("World");
}
}
执行结果:
test被执行
hello:World
如果实现类实现了两个接口,两个接口都有相同的(default)默认方法名,那么该方法重写会报错
解决办法:
- 实现类重写多个多个接口的默认方法
- 手动指定重写哪个接口的默认方法
public interface MyInterface {
void test();
default void say(String message) {
System.out.println("hello:"+message);
}
}
public interface MyInterface1 {
default void say(String message) {
System.out.println("hello1:" + message);
}
}
public class MyInterfaceImpl1 implements MyInterface, MyInterface1 {
@Override
public void test() {
System.out.println("test是普通方法被重写");
}
@Override
public void say(String message) {
// 方法一:System.out.println("实现类重写多个接口相同的默认方法:" + message);
// 方法二:指定重写哪个接口的默认方法
MyInterface.super.say(message);
}
public static void main(String[] args) {
MyInterfaceImpl1 client = new MyInterfaceImpl1();
client.say("好的");
}
}
执行结果:
实现类重写多个接口相同的默认方法:好的
基本数据类型
- byte:1字节
- short:2字节
- int:4字节
- long:8字节
- float:4字节
- double:8字节
- char:2字节(和c不一样,c是1字节)
- boolean:Java中没有规定字节数
元注解
Java中使用返回值类型@interface表示该类是一个注解配置类,注解配置不能使用class、interface、abstract修饰
// 自定义注解,以下只是简单的演示。实际开发注解还需要一个注解处理器,自行百度学习
public @interface MyAnn {
// 注解类的成员变量=被注解的class的成员属性
// 看起来是变量,其实是方法
int id();
String username();
}
// 使用自定义注解
@MyAnn(id = 1, username = "张三")
public class MyAnnTest {
}
四大元注解:
- @Target:说明注解对象修饰范围
- @Retention:该注解保留时长
- @Documented:表示被Javadoc文档工具化,只是一个标记注解,没有成员
- @Inherited:表示该类型是被继承的,表名该注解类作用于一个class的子类
反射机制
概念:反射机制是指在运行中,动态获取信息和动态调用对象方法的功能。
- 对于任意一个类,都能够知道这个类的所有属性和方法。
- 对于一个对象,都能够调用它的任意一个方法和属性
作用:
- 在运行时判断任意一个类对象所属的类
- 在运行时构造一个类的对象
- 在运行时判断任意一个类所具有的成员变量和方法
- 在运行时调用任意一个对象的方法,生成动态代理
获取Class三种方法:
package 基础.反射;
public class User {
String username;
String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}
public class ThreeClassGetDemo {
public static void main(String[] args) throws ClassNotFoundException {
// 方式一:类.class
Class<Integer> intClass = int.class;
// 方式二:对象.getClass()
User user = new User();
Class<? extends User> userClass = user.getClass();
// 方式三:Class.forName(类名)
String ClassName = "基础.反射.User";
Class<?> userClass1 = Class.forName(ClassName);
}
}
public class UserClassDemo {
public static void main(String[] args) {
String className = "基础.反射.User";
try {
// 通过反射获取userClass
Class<?> userClassByForName = Class.forName(className);
// 获取构造器
Constructor<?> constructor = userClassByForName.getConstructor();
// 生成user实例
User user = (User) constructor.newInstance();
user.setUsername("张三");
user.setPassword("123");
System.out.println(user);
// 反射来修改成员变量
Class<? extends User> userClassByuser = user.getClass();
userClassByuser.getDeclaredField("username").set(user, "张三1");
userClassByuser.getDeclaredField("password").set(user, "456");
// 反射修改方法
Method setUsername = userClassByuser.getMethod("setUsername", String.class);
setUsername.invoke(user, "张三2");
System.out.println(user);
} catch (Exception e) {
e.printStackTrace();
}
}
}
打印结果:
User{username='张三', password='123'}
User{username='张三2', password='456'}
Exception和Error区别
- 异常是Java运行时候预料到出现的错误,是可以被捕获并进行相关处理的,是一种异常现象
- 错误是正常情况下不可能发生的,会导致JVM处于不可恢复的状态,不需要被捕获,如:OutOfMemory
Exception分类:运行时异常和编译时异常
- 编译时异常:编译时候出现的异常,表示调用方法内部抛出了一个异常,可以被捕获或者抛给上一层调用方
- 运行时异常:运行时出现的异常,常见:空指针异常NullPointerException、类发现异常ClassNotFoundException 数组越界异常IndexOutOfBoundsException、数字转换异常NumberFormatException、算法异常Arithmetic Exception、并发修改异常ConcurrentModificationException
NoClassDefFoundError和NoClassFoundException区别:
- 前者是错误,表示JVM/ClassLoader尝试加载时,找不到类定义,但是类编译时候却正常的,通常是打包的时候漏掉了该类或Jar包被篡改、损坏了
- 后者是异常,通常是ClassPath/ClassName不对造成的,检查路径/类名是否正确
JIT及时编译器
概念:JVM层面的概念,把经常运行的代码作为热点代码进行优化。JIT除了具有缓存的功能外,可以对代码做出各种优化,比如逃逸分析、锁消除、锁膨胀、方法内联等。该知识点请结合JVM笔记学习比对,记忆
逃逸分析:
分析对象的动态作用域,当一个方法被定义后,它可能会被外部方法调用,比如作为参数传递到其他方法中,该过程就叫方法逃逸。JIT对此有2种优化:
- 锁消除:JIT判断不会发生并发问题,就会把同步Synchronized去掉
- 标量替换:JIT分析发现一个对象不会被外界访问,就会把该对象分解成若干个其中标量进行替换。好处是不用再堆内存中分配,为栈上分配提供了良好的基础。
- 标量:无法再分解成更小的数据,Java中原始数据类型就是标量
- 聚合量:还可以分配的数据。Java中对象就是聚合量
值传递和引用传递
值传递:传递是一个对象副本,即使副本改变,也不会影响原对象
引用传递:传递不是实际的对象,而是对象的引用。因此,外部对引用对象的改变会反映到所有的对象上
public class Test {
public static void main(String[] args) {
int a = 1;
// 基本数据类型:值传递,原值不会变
change(a);
System.out.println(a);
}
private static void change(int num) {
num++;
}
}
public class Test1 {
public static void main(String[] args) {
// 以下2个是引用传递,会改变原值
StringBuilder hello1 = new StringBuilder("hello1");
StringBuffer hello2 = new StringBuffer("hello2");
// String存放在常量池,虽然是引用传递,但不会改变原值
String hello3 = new String("hello3");
change1(hello1);
change2(hello2);
change3(hello3);
System.out.println(hello1);
System.out.println(hello2);
System.out.println(hello3);
}
private static void change3(String str) {
str += " world";
}
private static void change1(StringBuilder str) {
str.append(" world");
}
private static void change2(StringBuffer str) {
str.append(" world");
}
}
public class Test3 {
public static void main(String[] args) {
StringBuffer hello = new StringBuffer("hello");
System.out.println("before:" + hello);
changeData(hello);
// 前后值:都没有发生改变
// 因为changeData中str形参重新执行了str1,与原值hello无关了
System.out.println("after:" + hello);
}
private static void changeData(StringBuffer str) {
StringBuffer str1 = new StringBuffer("Hi");
str = str1;
str1.append("world");
}
}
public class PassTest {
public static void main(String[] args) {
int i = 1;
String str = "hello";
Integer num = 200;
int[] arr = {1, 2, 3, 4, 5};
MyData my = new MyData();
change(i, str, num, arr, my);
/*
结果:传值还是传引用?
i = 1 传值。基本数据类型不会变
str = hello 传常量池地址。字符串不变
num = 200,传堆中的地址。原包装类不变,和字符串一样
arr = [2, 2, 3, 4, 5] 传堆中数组的首地址。发生了改变
my.a = 11,传堆中地址,资源类变量发生改变。资源类中的变量,会在堆中生成一个实例
*/
System.out.println("i = " + i);
System.out.println("str = " + str);
System.out.println("num = " + num);
System.out.println("arr = " + Arrays.toString(arr));
System.out.println("my.a = " + my.a);
}
public static void change(int j, String s, Integer num, int[] arr, MyData myData) {
j += 1;
s += "world";
num += 1;
arr[0] += 1;
myData.a += 1;
}
}
class MyData {
int a = 10;
}
结果:
- 基本数据类型是值传递,不会改变原值
- String和包装类是引用传递,但不会改变原值,因为形参指向了另一个新生成的对象,原值不变
- 数组和自定义类时引用传递,会改变原值,因为数组是连续地址空间,没有在堆中新生成实例;自定义类中的成员变量分配在堆中,也没有重新生成实例
i = 1 // 传值。基本数据类型不会变
str = hello // 传常量池地址。字符串不变
num = 200 // 传堆中的地址。原包装类不变,和字符串一样
arr = [2, 2, 3, 4, 5]// 传堆中数组的首地址。发生了改变
my.a = 11// 传堆中地址,资源类变量发生改变。资源类中的变量,会在堆中生成一个实例
集合
集合中的上层接口只有2类:Map和Collection,我们平常使用的hashMap、ArrayList都是间接实现了这2个接口
常见集合:
- Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等
- Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等
- List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等
HashMap和Hashtable
- HashMap没有考虑同步,是线程不安全的;Hashtable使用了synchronized关键字,是线程安全的;
- HashMap允许null作为Key;Hashtable不允许null作为Key,Hashtable的value也不可以为null
HashMap线程不安全是在哪儿?
- HashMap是线程不安全的,无论JDK7还是8,都是多线程环境下进行扩容可能会出现HashMap死循环
- Hashtable单个线程是安全的,因为其内部实现在put和remove等方法上使用synchronized进行了同步。但是对多个方法进行复合操作时,线程安全性无法保证。 比如一个线程在进行get然后put更新的操作,这就是两个复合操作,在两个操作之间,可能别的线程已经对这个key做了改动,所以,你接下来的put操作可能会不符合预期
快速失败机制fast-fail?
- 迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedModCount值,是的话就返回遍历;否则抛出异常,终止遍历
HashMap的长度为什么是2的幂次方?
2的幂次方可以减少冲突(碰撞)的次数,提高HashMap查询效率
-
(length - 1) & hash //源码:算出put进的数组下标,使用的&位运算
-
如果length为2的幂次方,则length-1 转化为二进制必定是11111……的形式,在与h的二进制与操作效率会非常的快,而且空间不浪费
-
如果length不是2的幂次方,比如length为15,则length-1为14,对应的二进制为1110,在与h与操作,最后一位都为0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,并且数组可以使用的位置比数组长度小了很多,进一步增加了碰撞的几率,减慢了查询的效率!
哪些类适合作为HashMap的键?
- String和Interger这样的包装类很适合做为HashMap的键,因为他们是final类型的类,而且重写了equals和hashCode方法,避免了键值对改写,有效提高HashMap性能。
- 为了计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashCode的话,那么就不能从HashMap中找到你想要的对象。
ConcurrentHashMap
JDK7加锁:Segment和HashEntry
JDK8:分段锁+CAS保证线程安全,分段锁基于synchronized关键字实现
TreeMap
treeMap底层使用红黑树,会按照Key来排序
- 如果是字符串,就会按照字典序来排序
- 如果是自定义类,就要使用2种方法指定比较规则
- 实现Compareable接口,但是需要重新定义比较规则就要修改源码,麻烦
- 创建实例时候,传入一个比较器Comparator,重新定义规则不需要修改源码,推荐使用
public class TreeMapDemo {
public static void main(String[] args) {
// treeMap中自定义类需要指定比较器
// 方式一:自定义类实现Comparable接口
TreeMap<User, User> treeMap1 = new TreeMap<>();
// 方式二:创建实例指定比较器Comparator
TreeMap<User, User> treeMap2 = new TreeMap<>(new Comparator<User>() {
@Override
public int compare(User o1, User o2) {
// 定义比较规则
return 0;
}
});
}
}
public class User implements Comparable {
private String id;
private String username;
@Override
public int compareTo(Object obj) {
// 这里定义比较规则
return 0;
}
}
ArrayList和LinkedList区别
- ArrayList底层是动态数组,随机存取效率高;必须预留一定空间,空间不足时候,会进行扩容
- LinkedList底层是双向链表,可以当做堆栈、队列、双端队列使用,增删方法效率高;开销是需要存储节点信息
- 两者都线程不安全,Vector是线程安全的ArrayList,但是已经被废弃。线程安全推荐使用CopyOnWriteArrayList
LinkedHashMap和LinkedHashSet有了解吗?
答:LinkedHashMap可以记录下元素的插入顺序和访问顺序
- LinkedHashMap内部的Entry继承于HashMap.Node,这两个类都实现了Map.Entry<K,V>
- 底层链表是双向链表,Node不光有value,next,还有before和after属性,保证了各个元素的插入顺序
- 通过构造方法public LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder), accessOrder传入true可以实现LRU缓存算法(访问顺序)
什么是LRU算法?LinkedHashMap如何实现LRU算法?
最近最少使用算法(Least Recently Used): 根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
public class LRUTest {
private static int size = 5;
public static void main(String[] args) {
// 指定构造器 实现LRU算法
Map<String, String> map = new LinkedHashMap<String, String>(size, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
return size() > size;
}
};
map.put("1", "1");
map.put("2", "2");
map.put("3", "3");
map.put("4", "4");
map.put("5", "5");
// map满了
System.out.println(map.toString());
// map满了,再put就会移除表头第一个元素
map.put("6", "6");
System.out.println(map.toString());
// 取出的元素,重新放回到表尾
map.get("3");
System.out.println(map.toString());
}
}
执行结果:
{1=1, 2=2, 3=3, 4=4, 5=5}
{2=2, 3=3, 4=4, 5=5, 6=6}
{2=2, 4=4, 5=5, 6=6, 3=3}
List和Set的区别?
- List是有序的并且元素是可以重复的
- Set是无序(LinkedHashSet除外)的,元素不可以重复(有序和无序是指放入顺序和取出顺序是否保持一致)
Iterator和ListIterator的区别是什么?
ListIterator其实就是实现了前者,并且增加了一些新的功能。
- Iterator可以遍历list和set集合;ListIterator只能用来遍历list集合
- Iterator前者只能前向遍历集合;ListIterator可以前向和后向遍历集合
public class IteratorDemo {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
数组和集合List之间的转换
数组转为集合: Arrays.asList
通过Arrays.asList方法搞定,转换之后不可以使用add/remove等修改集合的相关方法,因为该方法返回的其实是一个Arrays的内部私有的一个类ArrayList,该类继承于Abstractlist,并没有实现这些操作方法,调用将会直接抛出UnsupportOperationException异常。这种转换体现的是一种适配器模式,只是转换接口,本质上还是一个数组。
集合转换数组:list.toArray
List.toArray方法搞定了集合转换成数组,这里最好传入一个类型一样的数组,大小就是list.size()。因为如果入参分配的数组空间不够大时,toArray方法内部将重新分配内存空间,并返回新数组地址;如果数组元素个数大于实际所需,下标为list.size()及其之后的数组元素将被置为null,其它数组元素保持原值。
若是直接使用toArray无参方法,此方法返回值只能是Object[ ]类,若强转其它类型数组将出现ClassCastException错误
public class ConverTest {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
// 链表转换成数组,最好指定数组长度,否则可能会浪费空间效率
String[] listToArr = list.toArray(new String[3]);
// 如使用无参强转,直接异常:ClassCastException异常
// String[] listToArrNoCon = (String[]) list.toArray();
for (String s : listToArr) {
System.out.println(s);
}
System.out.println("==========");
List<String> list1 = Arrays.asList(listToArr);
for (String s1 : list1) {
System.out.println(s1);
}
}
}
并发
进程和线程
- 进程是一个“执行中的程序”,是系统进行资源分配和调度的一个独立单位
- 线程是进程的一个实体,一个进程中一般拥有多个线程。线程之间共享地址空间和其它资源(所以通信和同步等操作,线程比进程更加容易)
- 线程一般不拥有系统资源,但是也有一些必不可少的资源(使用ThreadLocal存储)
- 线程上下文的切换比进程上下文切换要快很多。
多线程与单线程的关系
- 多线程是指在一个进程中,并发执行了多个线程,每个线程都实现了不同的功能
- 在单核CPU中,将CPU分为很小的时间片,在每一时刻只能有一个线程在执行,是一种微观上轮流占用CPU的机制。由于CPU轮询的速度非常快,所以看起来像是“同时”在执行一样
- 多线程会存在线程上下文切换,会导致程序执行速度变慢
- 多线程不会提高程序的执行速度,反而会降低速度。但是对于用户来说,可以减少用户的等待响应时间,提高了资源的利用效率
线程状态
- NEW:属于一个已经创建的线程,但是还没有调用start方法启动的线程所处的状态。
- RUNNABLE:状态包含两种可能。有可能正在运行,或者正在等待CPU资源。总体上就是当我们创建线程并且启动之后,就属于Runnable状态。
- BLOCKED:当线程准备进入synchronized同步块或同步方法的时候,需要申请一个监视器锁而进行的等待,会使线程进入BLOCKED状态。
- WAITING:调用了Object.wait()或者Thread.join()或者LockSupport.park()。处于该状态下的线程在等待另一个线程 执行一些其余action来将其唤醒。
- TIMED_WAITING:该状态和上一个状态其实是一样的,是不过其等待的时间是明确的
- TERMINATED:消亡状态比较容易理解,那就是线程执行结束了,run方法执行结束表示线程处于消亡状态了。
多线程编程常用函数
sleep 和 wait 的区别:
- **sleep方法:**是Thread类的静态方法,当前线程将睡眠n毫秒,线程进入阻塞状态。当睡眠时间到了,会解除阻塞,进入可运行状态,等待CPU的到来。睡眠不释放锁(如果有的话)。
- wait方法:是Object的方法,必须与synchronized关键字一起使用,线程进入阻塞状态,当notify或者notifyall被调用后,会解除阻塞。但是,只有重新占用互斥锁之后才会进入可运行状态。睡眠时,会释放互斥锁。
**join 方法:**当前线程调用,则其它线程全部停止,等待当前线程执行完毕,接着执行。
**yield 方法:**该方法使得线程放弃当前分得的 CPU 时间。但是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。
线程活性故障有哪些?
线程一直被阻塞,线程该执行的任务一直得不到执行。常见的线程活性故障包括死锁,锁死,活锁与线程饥饿。
死锁
多个线程之间相互等待对方而被永远暂停(处于非Runnable)。死锁的产生必须满足如下四个必要条件:
- 资源互斥:资源A只能被线程一使用
- 请求与保持条件:线程一因请求资源A而阻塞时,线程一不释放资源
- 不剥夺条件:线程一已经获得的资源,在未使用完之前,不能强行剥夺
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系
避免死锁的发生?
- **粗锁法:**使用一个粒度粗的锁来消除“请求与保持条件”,缺点是会明显降低程序的并发性能并且会导致资源的浪费。
- 显式锁:ReentrantLock.try(long,TimeUnit)来申请锁
-
锁排序法:(必须回答出来的点)
- 指定获取锁的顺序,比如某个线程只有获得A锁和B锁,才能对某资源进行操作,比如规定,只有获得A锁的线程才有资格获取B锁,按顺序获取锁就可以避免死锁。思考高并发下的单例设计模式
锁死
线程锁死是指等待线程由于唤醒其所需的条件永远无法成立,或者其他线程无法唤醒这个线程而一直处于非运行状态(线程并未终止)导致其任务 一直无法进展。线程死锁和线程锁死的外部表现是一致的,但是锁死的产生条件和线程死锁不一样,即使产生死锁的4个必要条件都没有发生,线程锁死仍然可能已经发生。
锁死的两种情况:
-
**信号丢失锁死:**没有对应的通知线程来将等待线程唤醒,导致等待线程一直处于等待状态。
- 例子:等待线程在执行Object.wait( )/Condition.await( )前没有对保护条件进行判断,而此时保护条件实际上可能已经成立,此后可能并无其他线程更新相应保护条件涉及的共享变量使其成立并通知等待线程,这就使得等待线程一直处于等待状态,从而使其任务一直无法进展。
-
**嵌套监视器锁死:**由于嵌套锁导致等待线程永远无法被唤醒的一种故障。
- **例子:**比如一个线程,只释放了内层锁Y.wait(),但是没有释放外层锁X; 但是通知线程必须先获得外层锁X,才可以通过 Y.notifyAll()来唤醒等待线程,这就导致出现了嵌套等待现象
活锁
处于运行状态下,线程所执行的任务却没有任何进展称为活锁。
- 比如,一个线程一直在申请其所需要的资源,但是却无法申请成功。
线程饥饿
无法运行状态下,线程饥饿是指线程一直无法获得其所需的资源导致任务一直无法运行的情况。
- 线程调度模式有公平调度和非公平调度两种模式。在线程的非公平调度模式下,就可能出现线程饥饿的情况。
- 线程饥饿发生时,如果线程处于可运行状态,也就是其一直在申请资源,那么就会转变为活锁
- 只要存在一个或多个线程因为获取不到其所需的资源而无法进展就是线程饥饿,所以线程死锁其实也算是线程饥饿
原子性,可见性与有序性
原子性
共享变量访问的操作,若该操作从执行线程以外的任意线程来看是不可分割的,那么该操作就是原子操作,该操作具有原子性。即,其它线程不会“看到”该操作执行了部分的中间结果。
-
例子:银行转账流程中,A账户减少了100元,那么B账户就会多100元,这两个动作是一个原子操作。我们不会看到A减少了100元,但是B余额保持不变的中间结果
-
原子性针对的是多个线程的共享变量,所以对于局部变量来说不存在共享问题,也就无所谓是否是原子操作
-
volatile关键字仅仅能保证变量写操作的原子性,不保证复合操作,比如说读写操作的原子性
可见性的实现:
- 利用锁的排他性,保证同一时刻只有一个线程在操作一个共享变量
- 利用**CAS(Compare And Swap)**保证
- Java语言规范中,保证了除long和double型以外的任何变量的写操作都是原子操作
- Java语言规范中又规定,volatile关键字修饰的变量可以保证其写操作的原子性
可见性
可见性是指一个线程对于共享变量的更新,对于后续访问该变量的线程是否可见的问题。
可见性的保证:
- 当前处理器需要刷新处理器缓存,使得其余处理器对变量所做的更新可以同步到当前的处理器缓存中
- 当前处理器对共享变量更新之后,需要冲刷处理器缓存,使得该更新可以被写入处理器缓存中
有序性
有序性是指一个处理器上运行的线程所执行的内存访问操作在另外一个处理器上运行的线程来看是否有序的问题。
**重排序举例:**JVM可能会对源代码顺序进行一定的调整,导致程序运行顺序与源代码顺序不一致。
/*
1 分配对象的内存空间(堆上)
2 初始化对象
3 设置instance指向刚分配的内存地址
第二步和第三步可能会发生重排序,导致引用型变量指向了一个不为null但是也不完整的对象。(在多线程下的单例模式中,我们必须通过volatile来禁止指令重排序)
*/
Instance instance = new Instance()
synchronized理解
synchronized是关键只,也是一个内部锁。内部锁可以使用在方法上和代码块上,被内部锁修饰的区域又叫做临界区。
内部锁底层实现:
- 进入时,执行monitorenter,将计数器+1,释放锁monitorexit时,计数器-1
- 当一个线程判断到计数器为0时,则当前锁空闲,可以占用;反之,当前线程进入等待状态
syn分为同步方法和同步代码块
public class SynTest {
public static void main(String[] args) {
synchronized (new SynTest()) {
System.out.println("syn同步的代码块");
}
}
public synchronized void synMethod() {
System.out.println("syn在方法上,就是同步方法");
}
}
**非公平调度策略:**资源的持有线程释放该资源的时候,等待队列中一个线程会被唤醒,而该线程从被唤醒到其继续执行可能需要一段时间。在该段时间内,**新来的线程(活跃线程)**可以先被授予该资源的独占权。如果新来的线程占用该资源的时间不长,那么它完全有可能在被唤醒的线程继续执行前释放相应的资源,从而不影响该被唤醒的线程申请资源。
- 优点:吞吐率较高,单位时间内可以为更多的申请者调配资源
- 缺点:资源申请者申请资源所需的时间偏差可能较大,并可能出现线程饥饿的现象
公平调度策略:按照申请的先后顺序授予资源的独占权。
- 优点:线程申请资源所需的时间偏差较小;不会出现线程饥饿的现象;适合在资源的持有线程占用资源的时间相对长或者资源的平均申请时间间隔相对长的情况下,或者对资源申请所需的时间偏差有所要求的情况下使用;
- 缺点:吞吐率较小
JVM对synchronized内部锁的调度:
JVM对内部锁的调度是一种非公平的调度方式,JVM会给每个内部锁分配一个入口集(Entry Set),用于记录等待获得相应内部锁的线程。当锁被持有的线程释放的时候,该锁的入口集中的任意一个线程将会被唤醒,从而得到再次申请锁的机会。被唤醒的线程等待占用处理器运行时可能还有其他新的活跃线程与该线程抢占这个被释放的锁.
ReentrantLock和synchronized的区别:
-
ReentrantLock是显示锁,显式锁支持公平和非公平的调度方式,默认采用非公平调度。
-
synchronized 内部锁简单,但是不灵活。显示锁支持在一个方法内申请锁,并且在另一个方法里释放锁。显示锁定义了一个
tryLock()
方法,尝试去获取锁,成功返回true,失败并不会导致其执行的线程被暂停而是直接返回false,即可以避免死锁。
volatile
volatile关键字是一个轻量级的锁,可以保证可见性和有序性,但是不保证原子性
- volatile 可以保证主内存和工作内存直接产生交互,进行读写操作,保证可见性
- volatile 仅能保证变量写操作的原子性,不能保证读写操作的原子性。
- volatile可以禁止指令重排序(通过插入内存屏障),典型案例是在单例模式中使用。
volatile变量的开销:
volatile不会导致线程上下文切换,但是其读取变量的成本较高,因为其每次都需要从高速缓存或者主内存中读取,无法直接从寄存器中读取变量。
volatile在什么情况下可以替代锁?
volatile是一个轻量级的锁,适合多个线程共享一个状态变量,锁适合多个线程共享一组状态变量。可以将多个线程共享的一组状态变量合并成一个对象,用一个volatile变量来引用该对象,从而替代锁。
JVM
JVM中的内存是怎么划分的?
方法区:方法区是一个线程之间共享的区域。常量,静态变量以及JIT编译后的代码都在方法区。主要用于存储已被虚拟机加载的类信息,也可以称为“永久代”,垃圾回收效果一般,通过-XX:MaxPermSize控制上限。
堆内存:堆内存是垃圾回收的主要场所,也是线程之间共享的区域,主要用来存储创建的对象实例,通过-Xmx 和-Xms 可以控制大小。
虚拟机栈(栈内存):栈内存中主要保存局部变量、基本数据类型变量以及堆内存中某个对象的引用变量。每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表,操作数栈,动态链接,方法出口等信息。栈中的栈帧随着方法的进入和退出有条不紊的执行着出栈和入栈的操作。
程序计数器: 程序计数器是当前线程执行的字节码的位置指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,是内存区域中唯一一个在虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
本地方法栈: 主要是为JVM提供使用native 方法的服务
对象创建过程中的内存分配
一般情况下我们通过new指令来创建对象,当虚拟机遇到一条new指令的时候,会去检查这个指令的参数是否能在常量池中定位到某个类的符号引用,并且检查这个符号引用代表的类是否已经被加载,解析和初始化。如果没有,那么会执行类加载过程。
通过执行类的加载,验证,准备,解析,初始化步骤,完成了类的加载,这个时候会为该对象进行内存分配,也就是把一块确定大小的内存从Java堆中划分出来,在分配的内存上完成对象的创建工作。
对象的内存分配有两种方式,即指针碰撞和空闲列表方式。
-
指针碰撞方式:
假设Java堆中的内存是绝对规整的,用过的内存在一边,未使用的内存在另一边,中间有一个指示指针,那么所有的内存分配就是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
-
空闲列表方式:
如果Java堆内存中不是规整的,已使用和未使用的内存相互交错,那么虚拟机就必须维护一个列表用来记录哪块内存是可用的,在分配的时候找到一块足够大的空间分配对象实例,并且需要更新列表上的记录。
那么内存的分配如何保证线程安全呢?
- 对分配内存空间的动作进行同步处理,通过“CAS + 失败重试”的方式保证更新指针操作的原子性
- 把分配内存的动作按照线程划分在不同的空间之中,即给每一个线程都预先分配一小段的内存,称为本地线程分配缓存(TLAB),只有TLAB用完并分配新的TLAB时,才需要进行同步锁定。 虚拟机是否使用TLAB,可以通过**-XX: +/-UserTLAB**参数来设定
对象被访问的时候是怎么被找到的?
-
句柄访问方式:
在JVM的堆内存中划分出一块内存来作为句柄池,引用变量中存储的就是对象的句柄地址,句柄中包含了对象实例数据与类型数据各自的具体地址信息。在内存垃圾收集之后,对象会移动,但是引用reference中存储的是稳定的句柄地址,但是句柄地址方式不直接,访问速度较慢。
-
直接指针访问方式:
引用变量中存储的就是对象的直接地址,通过指针直接访问对象。直接指针的访问方式节省了一次指针定位的时间开销,速度较快。Sun HotSpot使用了直接指针方式进行对象的访问
内存分配与垃圾回收
-
Minor GC(年轻代GC):
对象优先在Eden中分配,当Eden中没有足够空间时,虚拟机将发生一次Minor GC,因为Java大多数对象都是朝生夕灭,所以Minor GC非常频繁,而且速度也很快。 -
Full GC(老年代GC):
Full GC是指发生在老年代的GC,当老年代没有足够的空间时即发生Full GC,发生Full GC一般都会有一次Minor GC
JVM堆内存的分配:
JVM初始分配的堆内存由**-Xms**指定,默认是物理内存的1/64。
JVM最大分配的堆内存由**-Xmx**指定,默认是物理内存的1/4。
默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制。空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制。因此我们一般设置-Xms和-Xmx相等以避免在每次GC 后调整堆的大小。
通过参数**-Xmn2G** 可以设置年轻代大小为2G。通过**-XX:SurvivorRatio**可以设置年轻代中Eden区与Survivor区的比值,设置为8,则表示年轻代中Eden区与一块Survivor的比例为8:1。注意年轻代中有两块Survivor区域。
JVM非堆内存的分配:
JVM使用**-XX:PermSize** 设置非堆内存初始值,默认是物理内存的1/64。由**-XX:MaxPermSize**设置最大非堆内存的大小,默认是物理内存的1/4。
堆内存上对象的分配与回收:
我们创建的对象会优先在Eden分配,如果是大对象(很长的字符串数组)则可以直接进入老年代。虚拟机提供一个
-XX:PretenureSizeThreshold参数,令大于这个参数值的对象直接在老年代中分配,避免在Eden区和两个Survivor区发生大量的内存拷贝。
另外,长期存活的对象将进入老年代,每一次MinorGC(年轻代GC),对象年龄就大一岁,默认15岁晋升到老年代,通过
-XX:MaxTenuringThreshold设置晋升年龄。
动态对象年龄判定:
如果Survivor空间中相同年龄所有对象的大小总和大于Survivor空间的一半,那么年龄大于等于该对象年龄的对象即可晋升到老年代,不必要等到-XX:MaxTenuringThreshold。
空间分配担保:
发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小。如果大于,则进行一次Full GC(老年代GC),如果小于,则查看HandlePromotionFailure设置是否允许担保失败,如果允许,那只会进行一次Minor GC,如果不允许,则改为进行一次Full GC。
JVM如何判定一个对象是否应该被回收?
-
引用计数法
- 优点:简单高效。
- 缺点:无法解决对象之间相互循环引用的问题。所以目前的虚拟机都不会使用这个方法。例如,t1的成员变量指向t2,t2的成员变量指向t1,即使t1,t2都判空,GC也无法回收他们。
-
可达性分析算法
- 引出一个“GC Roots”(背:线程栈的本地变量、静态变量、本地方法栈的变量等等),将**“GC Roots”** 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象。
- root对象有这些:
- 栈内存中引用的对象
- 方法区中静态引用和常量引用指向的对象
- 被启动类(bootstrap加载器)加载的类和创建的对象
- Native方法中JNI引用的对象。
对象引用
- **强引用:**普通存在, P p = new P(),只要强引用存在,垃圾收集器永远不会回收掉被引用的对象。
- **软引用:**通过SoftReference类来实现软引用,在内存不足的时候会将这些软引用回收掉。
- **弱引用:**通过WeakReference类来实现弱引用,每次垃圾回收的时候肯定会回收掉弱引用。
- **虚引用:**也称为幽灵引用或者幻影引用,通过PhantomReference类实现。设置虚引用只是为了对象被回收时候收到一个系统通知。
垃圾收集算法
垃圾收集器
面试回答年轻代和老年代分别使用那些垃圾收集器:
Serial收集器:
Serial收集器是一个单线程的垃圾收集器,并且在执行垃圾回收的时候需要 Stop The World。虚拟机运行在Client模式下的默认新生代收集器。
- 优点是简单高效,对于限定在单个CPU环境来说,Serial收集器没有多线程交互的开销。
Serial Old收集器:
Serial Old是Serial收集器的老年代版本,也是一个单线程收集器。主要也是给在Client模式下的虚拟机使用。在Server模式下存在主要是做为CMS垃圾收集器的后备预案,当CMS并发收集发生Concurrent Mode Failure时使用。
ParNew收集器:
ParNew是Serial收集器的多线程版本,新生代是并行的(多线程的),老年代是串行的(单线程的),新生代采用复制算法,老年代采用标记整理算法。可以使用参数:-XX:UseParNewGC使用该收集器,使用 -XX:ParallelGCThreads可以限制线程数量。
Parallel Scavenge垃圾收集器:
Parallel Scavenge是一种新生代收集器,使用复制算法的收集器,而且是**并行的多线程收集器。Paralle收集器特点是更加关注吞吐量(吞吐量就是cpu用于运行用户代码的时间与cpu总消耗时间的比值)。可以通过-XX:MaxGCPauseMillis参数控制最大垃圾收集停顿时间;通过-XX:GCTimeRatio参数直接设置吞吐量大小;通过-XX:+UseAdaptiveSizePolicy参数可以打开GC自适应调节策略,**该参数打开之后虚拟机会根据系统的运行情况收集性能监控信息,动态调整虚拟机参数以提供最合适的停顿时间或者最大的吞吐量。自适应调节策略是Parallel Scavenge收集器和ParNew的主要区别之一。
Parallel Old收集器:
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。
CMS(Concurrent Mark Sweep)收集器:
CMS收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于标记-清除算法实现的,是一种老年代收集器,通常与ParNew一起使用。
CMS的垃圾收集过程分为4步:
- 初始标记:需要“Stop the World”,初始标记仅仅只是标记一下GC Root能直接关联到的对象,速度很快。
- 并发标记:是主要标记过程,这个标记过程是和用户线程并发执行的。
- 重新标记:需要“Stop the World”,为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录(停顿时间比初始标记长,但比并发标记短得多)。
- 并发清除:和用户线程并发执行的,基于标记结果来清理对象。
优点:
-
CMS以降低垃圾回收的停顿时间为目的,很显然其具有并发收集,停顿时间低的优点。
-
如果在重新标记之前刚好发生了一次MinorGC,会不会导致重新标记阶段Stop the World时间太长?
答:不会的,在并发标记阶段其实还包括了一次并发的预清理阶段,虚拟机会主动等待年轻代发生垃圾回收,这样可以将重新标记对象引用关系的步骤放在并发标记阶段,有效降低重新标记阶段Stop The World的时间。
缺点:
- 对CPU资源非常敏感,因为并发标记和并发清理阶段和用户线程一起运行,当CPU数变小时,性能容易出现问题。
- 收集过程中会产生浮动垃圾,所以不可以在老年代内存不够用了才进行垃圾回收,必须提前进行垃圾收集。通过参数**-XX:CMSInitiatingOccupancyFraction的值来控制内存使用百分比。如果该值设置的太高,那么在CMS运行期间预留的内存可能无法满足程序所需,会出现Concurrent Mode Failure失败,之后会临时使用Serial Old收集器做为老年代收集器**,会产生更长时间的停顿。
- 标记-清除方式会产生内存碎片,可以使用参数**-XX:UseCMSCompactAtFullCollection来控制是否开启内存整理(无法并发,默认是开启的)。参数-XX:CMSFullGCsBeforeCompaction**用于设置执行多少次不压缩的Full GC后进行一次带压缩的内存碎片整理(默认值是0)。
浮动垃圾:
由于在应用运行的同时进行垃圾回收,所以有些垃圾可能在垃圾回收进行完成时产生,这样就造成了**“Floating Garbage”**,这些垃圾需要在下次垃圾回收周期时才能回收掉。所以,并发收集器一般需要20%的预留空间用于这些浮动垃圾。
阅读下面的JVM调优参数配置:自行翻译
JAVA_OPTS="-Xms4096m –Xmx4096m -XX:NewRatio=2 -XX:SurvivorRatio=8 -Xloggc:/home/work/log/serviceName/gc.log -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCApplicationStoppedTime -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=10 "
剩下收集器找“JVM.md”
Spring
Spring是一个轻量级的IOC和AOP容器框架。是为Java应用程序提供基础服务的一套框架,目的是用于简化企业应用程序的开发,它使得开发者只需要关心业务需求。
Spring的核心模块如下所示:
- **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实现。
Sring的优点:
- Spring的依赖注入将对象之间的依赖关系交给了框架来处理,减小了各个组件之间的耦合性;
- AOP面向切面编程,可以将通用的任务抽取出来,复用性更高;
- Spring对于其余主流框架都提供了很好的支持,代码的侵入性很低
IOC控制反转
IOC也叫控制反转,将对象间的依赖关系交给Spring容器,使用配置文件来创建所依赖的对象,由主动创建对象改为了被动方式,实现解耦合。可以通过注解**@Autowired和@Resource**来注入对象,被注入的对象必须被下边的四个注解之一标注:
@Controller
@Service
@Repository
@Component
在Spring配置文件中配置 context:annotation-config/元素开启注解。还有一个概念是DI(依赖注入),和控制反转是同一个概念的不同角度的描述,即应用程序在运行时依赖IOC容器来动态注入对象需要的外部资源(对象等)。
AOP
答: AOP,面向切面编程是指当需要在某一个方法之前或者之后做一些额外的操作,比如说日志记录,权限判断,异常统计等,可以利用AOP将功能代码从业务逻辑代码中分离出来。
AOP中有如下的操作术语:
- Joinpoint(连接点): 类里面可以被增强的方法,这些方法称为连接点
- **Pointcut(切入点):**所谓切入点是指我们要对哪些Joinpoint进行拦截的定义
- **Advice(通知/增强):**所谓通知是指拦截到Joinpoint之后所要做的事情就是通知。
- **Aspect(切面):**是切入点和通知(引介)的结合
- **Introduction(引介):**引介是一种特殊的通知在不修改类代码的前提下,Introduction可以在运行期为类动态地添加一些方法或属性
- **Target(目标对象):**代(dai)理的目标对象(要增强的类)
- **Weaving(织入):**是把增强应用到目标的过程,把advice 应用到 target的过程
- **Proxy(代(dai)理):**一个类被AOP织入增强后,就产生一个结果代(dai)理类
Spring中的AOP主要有两种实现方式:
JDK动态代理
-
真实角色:创建业务接口,业务类实现接口
public interface PeopleInterface { public void fun(); } public class People implements PeopleInterface{ @Override public void fun() { System.out.println("这是People的fun方法"); } }
-
代理角色:继承
InvocationHandler
,重写invoke(),构造器获得真实角色,封装获取代理对象public class MyHandle implements InvocationHandler { // 被代理对象实例 private Object object; // 构造器 public MyHandle(Object object) { this.object = object; } // 封装获取代理对象 public Object getProxy() { return Proxy.newProxyInstance( this.getClass().getClassLoader(), object.getClass().getInterfaces(), this); } // 实现抽象接口的实体方法 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { before(); Object result = method.invoke(this.object, args); after(); return result; } // 增强的方法 private void before() { System.out.println("增强的before方法"); } private void after() { System.out.println("增强的after方法"); } }
-
通过代理类调用方法
public class DynamicProxy { public static void main(String[] args) { // 1 被代理对象:真实角色 PeopleInterface peopleI = new People(); // 2 自定义处理器:代理角色 // 代理角色实现真实角色的抽象接口 MyHandle myHandle = new MyHandle(peopleI); // 3 代理角色获得代理类 PeopleInterface people = (PeopleInterface) myHandle.getProxy(); // 4 由proxy动态代理调用被代理的接口方法 people.fun(); } }
cglib实现动态代理
-
创建被代理类,无需实现其他接口
public class Person { public void eat() { System.out.println("Person:我要开始吃饭了"); } public void play() { System.out.println("Person:我要出去玩了"); } }
-
两个方法实现
MethodInterceptor
public class MyApiInterceptor implements MethodInterceptor { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println("吃饭前,必须得洗手"); Object result = proxy.invokeSuper(obj, args); System.out.println("吃饭后,我要看会儿电视"); return result; } } public class MyApiInterceptorForPlay implements MethodInterceptor { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println("出去玩,我要带玩具"); Object result = proxy.invokeSuper(obj,args); System.out.println("玩完后,我就要回家"); return result; } }
-
实现失败过滤器,实现
CallbackFilter
public class CallbackFilterImpl implements CallbackFilter { @Override public int accept(Method method) { if (method.getName().equals("play")) { return 1; } else { return 0; } } }
-
实现cglib动态代理
public class CglibTest { public static void main(String[] args) { // 定义回调函数的接口 Callback[] callbacks = new Callback[]{ new MyApiInterceptor(), new MyApiInterceptorForPlay() }; // cglib使用enhancer实现动态代理 Enhancer enhancer = new Enhancer(); // 设置被动态代理的父类 enhancer.setSuperclass(Person.class); // 回调的拦截器数组 enhancer.setCallbacks(callbacks); // 回调选择器 enhancer.setCallbackFilter(new CallbackFilterImpl()); // 创建代理对象 Person person = (Person) enhancer.create(); person.eat(); System.out.println("------"); person.play(); } }
IOC初始化过程
IOC容器的初始化主要包括Resource定位,载入,注册三个步骤
-
Resource资源定位:
Resouce定位是指BeanDefinition的资源定位,也就是IOC容器找数据的过程。Spring中使用外部资源来描述一个Bean对象,IOC容器第一步就是需要定位Resource外部资源。由ResourceLoader通过统一的Resource接口来完成定位。
-
BeanDefinition的载入:
载入过程就是把定义好的Bean表示成IOC容器内部的数据结构,即BeanDefinition。在配置文件中每一个Bean都对应着一个BeanDefinition对象。
通过BeanDefinitionReader读取,解析Resource定位的资源,将用户定义好的Bean表示成IOC容器的内部数据结构BeanDefinition。
在IOC容器内部维护着一个BeanDefinitionMap的数据结构,通过BeanDefinitionMap,IOC容器可以对Bean进行更好的管理。
-
BeanDefinition的注册:
注册就是将前面的BeanDefition保存到Map中的过程,通过BeanDefinitionRegistry接口来实现注册。
BeanFactory和FactoryBean区别?
- BeanFactory:Bean工厂,作用是管理Bean,是一个工厂(Factory), 是Spring IOC容器的最顶层接口,即实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。
- FactoryBean:工厂Bean,作用是产生其他Bean实例,它本身是一个Bean,,需要提供一个工厂方法,该方法用来返回其他Bean实例。
BeanFactory和ApplicationContext区别?
-
BeanFactory是Spring里面最顶层的接口,包含了各种Bean的定义,读取Bean配置文档,管理Bean的加载、实例化,控制Bean的生命周期,维护Bean之间的依赖关系。
-
ApplicationContext接口是BeanFactory的派生,除了提供BeanFactory所具有的功能外,还提供了更完整的框架功能:
- 继承了MessageSource,支持国际化。
- 提供了统一的资源文件访问方式。
- 提供在Listener中注册Bean的事件。
- 提供同时加载多个配置文件的功能。
- 载入多个(有继承关系)上下文 ,使得每一个上下文都专注于一个特定的层次,比如应用的web层。
ApplicationContext 三种常见的实现方式:
- FileSystemXmlApplicationContext:此容器从一个XML文件中加载Bean的定义,XML Bean 配置文件的全路径名必须提供给它的构造函数。
- ClassPathXmlApplicationContext:此容器也从一个XML文件中加载Bean的定义,需要正确设置classpath因为这个容器将在classpath里找Bean配置。
- WebXmlApplicationContext:此容器加载一个XML文件,定义了一个WEB应用的所有Bean。
在创建Bean和内存占用方面的区别:
- BeanFactory采用的是延迟加载形式来注入Bean的,即只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化。这样,就不能发现一些存在于Spring配置中的问题。如果Bean的某一个属性没有注入,BeanFactory加载后,直至第一次使用调用getBean方法才会抛出异常。
- ApplicationContext,它是在容器启动时,一次性创建了所有的Bean。这样,在容器启动时,我们就可以发现Spring中存在的配置错误,这样有利于检查所依赖属性是否注入。 ApplicationContext启动后预载入所有的单实例Bean,通过预载入单实例Bean ,确保当需要的时候,可以直接获取。
- 相对于基本的BeanFactory,ApplicationContext不足之处是占用内存空间。当应用程序配置Bean较多时,程序启动较慢,因为其一次性创建了所有的Bean。
BeanFactory和ApplicationContext的优缺点分析:
-
BeanFactory的优缺点:
- 优点:应用启动的时候占用资源很少,对资源要求较高的应用,比较有优势;
- 缺点:运行速度会相对来说慢一些。而且有可能会出现空指针异常的错误,而且通过Bean工厂创建的Bean生命周期会简单一些。
-
ApplicationContext的优缺点:
- 优点:所有的Bean在启动的时候都进行了加载,系统运行的速度快;在系统启动的时候,可以发现系统中的配置问题。
- 缺点:把费时的操作放到系统启动中完成,所有的对象都可以预加载,缺点就是内存占用较大
Spring中Bean的作用域
- singleton : 只有一个实例,Spring默认作用域。
- prototype:原型bean,可以有多个实例。
- request:每次http请求都会创建一个Bean,该作用域仅在基于web的Spring ApplicationContext情形下有效。
- session:在一个HTTP Session中,一个Bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。
- global-session:在一个全局的HTTP Session中,一个Bean对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。
Spring中Bean的作用域常使用的是singleton,也就是单例作用域。那么单例Bean是线程安全的吗?
**单例Bean和线程安全与否没有必然的关系**。多个线程在多个**工作内存和主内存交互**的时候会出现不一致的地方,那么就不是线程安全的。大部分的Spring Bean并**没有可变的状态**(比如Service类和DAO类),所以一定程度上可以说Spring的单例Bean是线程安全的。如果你的Bean有多种状态的话(比如 View Model 对象),就需要自行保证线程安全。在一般情况下,**只有无状态的Bean才可以在多线程环境下共享。**
Spring事务
- 编程式事务管理:使用TransactionTemplate实现。
-
声明式事务管理:建立在AOP之上的。其本质是通过AOP功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。事务选择:
- 优点:非侵入式的开发方式,使业务代码不受污染,只要加上注解就可以获得完全的事务支持
- 缺点:最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。
当多个Spring事务存在的时候,Spring定义了下边的7个传播行为来处理这些事务行为:
事务名称 | 解释 |
---|---|
propagation_required | 有事务,就加入当前事务;没有事务,就创建新的事务(默认) |
propagation_required_new | 无论有没有事务,都创建新的事务 |
propagation_supports | 有事务,就加入当前事务;没有事务,就以非实物执行 |
propagation_not_supported | 有事务,将当期事务挂起,以非事务执行;没有事务,以非实物执行 |
propagation_mandatory | 有事务,就加入当前事务;没有事务,就抛出异常 |
propagation_never | 有事务,抛出异常,以非事务执行;没有事务,以非事务执行 |
propagation_nested | 有事务,则嵌套事务内执行;没有事务,就新建一个事务 |
Spring事务的隔离级别和MySQL事务的隔离级别类似,依然存在读未提交,读已提交,可重复读以及串行四种隔离级别。
SpringMVC的消息处理流程
- 通过前端控制器DispatcherServlet来接收并且分发请求
- 然后通过HandlerMapping和HandlerAdapter找到具体可以处理该请求的Handler,返回一个ModelAndView,
- 经过ViewResolver处理,最后生成了一个View视图返回给了客户端
Mysql
事务隔离级别
事务个隔离级别越高,并发问题越少,但是付出的代价也越大
show variables like 'tx_isolation';# 查看隔离级别
set tx_isolation = 'REPEATABLE-READ';# 默认隔离级别就是可重复读,Spring框架如果没有指定也是可重复读
隔离级别 | 脏读 | 不可重复度 | 幻读 |
---|---|---|---|
读未提交(Read Uncommitted) | 可能 | 可能 | 可能 |
读已提交(Read Commited) | 不可能 | 可能 | 可能 |
可重复读(默认级别,Repeatable Read) | 不可能 | 不可能 | 可能 |
可串行化(Serializable) | 不可能 | 不可能 | 不可能 |
7大传播行为
事务名称 | 解释 |
---|---|
propagation_required | 有事务,就加入当前事务;没有事务,就创建新的事务(默认) |
propagation_required_new | 无论有没有事务,都创建新的事务 |
propagation_supports | 有事务,就加入当前事务;没有事务,就以非实物执行 |
propagation_not_supported | 有事务,将当期事务挂起,以非事务执行;没有事务,以非实物执行 |
propagation_mandatory | 有事务,就加入当前事务;没有事务,就抛出异常 |
propagation_never | 有事务,抛出异常,以非事务执行;没有事务,以非事务执行 |
propagation_nested | 有事务,则嵌套事务内执行;没有事务,就新建一个事务 |
Redis
答:redis(Remote Dictionary Server远程字典服务),是一款高性能的(key/value)分布式内存数据库,基于内存运行并支持持久化的NoSQL数据库。因为数据都在内存中,所以运行速度快。redis支持丰富的数据类型并且支持事务,事务中的所有命令会被序列化、按顺序执行,在执行的过程中不会被其他客户端发送来的命令打断
redis都支持哪些数据类型?应用场景有哪些?
redis支持五种数据类型作为其Value,redis的Key都是字符串类型的。
- **string:**redis 中字符串 value 最大可为512M。可以用来做一些计数功能的缓存(也是实际工作中最常见的)。
- **list:**简单的字符串列表,按照插入顺序排序,可以添加一个元素到列表的头部(左边)或者尾部(右边),其底层实现是一个链表。可以实现一个简单消息队列功能,做基于redis的分页功能等。
- **set:**是一个字符串类型的无序集合。可以用来进行全局去重等。
- sorted set:是一个字符串类型的有序集合,给每一个元素一个固定的分数score来保持顺序。可以用来做排行榜应用或者进行范围查找等。
- **hash:**键值对集合,是一个字符串类型的 Key和 Value 的映射表,也就是说其存储的Value是一个键值对(Key- Value)。可以用来存放一些具有特定结构的信息。
一般情况下,可以认为redis的支持的数据类型有上述五种,其底层数据结构包括:简单动态字符串,链表,字典,跳表,整数集合以及压缩列表。
“redis的配置文件有了解吗?”
下载并且解压redis之后,在解压目录下有个配置文件 redis.windows.conf(大家可以自行查阅),在配置文件中对redis进行了分模块配置,常用的模块如下:
- **NETWORK:**该模块可以配置一些redis服务器地址,端口以及超时时间等
- **GENERAL:**该模块可以对日志文件的路径和日志级别等进行配置
- **SNAPSHOTTING:**redis持久化配置信息等
- **REPLICATION:**redis集群配置等信息
- **MEMORY MANAGEMENT:**内存管理,包括数据过期删除策略信息的设置
- **APPEND ONLY MODE:**日志持久化方式信息设置
上一篇: Java异常面试题