java高频面试题整理
内容将会持续更新,希望对有需要的小伙伴有所帮助。
java基础
jdk,jre,jvm的区别?
- jdk是开发工具
- jre是运行时环境
- jvm是虚拟机
什么是字节码?
字节码:java源代码经过虚拟机编译器编译后产生的文件(.class文件),只面向于虚拟机。
java数据类型
基本数据类型:
- 整数数据类型(byte,short,int long)
- 浮点类型(float,double)
- 字符型(char)
- 布尔型(boolean)
引用数据类型:
- 类(class)
- 接口(interface)
- 数组([])
装箱和拆箱
自动装箱是java编译器在基本数据类型和包装类型直接的一个转换,比如把int转为integer,double转为Double.
反之就是拆箱。
原始类型:boolean,char,byte,short,int,float,double
封装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double
java基本类型和引用类型的区别?
基本数据类型,存储的是原始值,引用数据类型保存的是引用值(对象在堆中的引用地址)
int和Integer的区别:
- integer是int的包装类,int是基本数据类型
- integer需要实例化后才可以使用,而int不需要
- integer实际是对象的引用,当new一个integer对象时,实际是生成一个指针指向该对象
- integer的默认值是null,int的默认值是0.
延伸:
int和integer的比较:
1、由于Integer变量实际上是对一个Integer对象的引用,所以两个通过new生成的Integer变量永远是不相等的(因为new生成的是两个对象,其内存地址不同)
2、Integer变量和int变量比较时,只要两个变量的值是相等的,则结果为true (因为包装类Integer和基本数据类型int比较时,java会自动拆包装为int,然后进行比较,实际上就变为两个int变量的比较)
Integer i = new Integer(100);
int j = 100;
System.out.print(i == j); //true
3、非new生成的Integer变量和new Integer()生成的变量比较时,结果为false。
(因为 当变量值在-128~127之间时,非new生成的Integer变量指向的是java常量池中的对象,而new Integer()生成的变量指向堆中新建的对象,两者在内存中的地址不同;
当变量值不在-128~127之间时,非new生成Integer变量时,java API中最终会按照new Integer(i)进行处理,最终两个Interger的地址同样是不相同的)
Integer i = new Integer(100);
Integer j = 100;
System.out.print(i == j); //false
4、对于两个非new生成的Integer对象,进行比较时,如果两个变量的值在区间-128到127之间,则比较结果为true,如果两个变量的值不在此区间,则比较结果为false
Integer i = 100;
Integer j = 100;
System.out.print(i == j); //true
Integer i = 128;
Integer j = 128;
System.out.print(i == j); //false
;对于第4条的原因:java在编译Integer i = 100 ;
;时会翻译成为Integer i = Integer.valueOf(100);
;而java API中对Integer类型的valueOf的定义如下:
public static Integer valueOf(int i){
assert IntegerCache.high >= 127;
if (i >= IntegerCache.low && i <= IntegerCache.high){
return IntegerCache.cache[i + (-IntegerCache.low)];
}
return new Integer(i);
}
注意:
java对于-128到127之间的数,会进行缓存,Integer i = 127时,会将127进行缓存,下次再写Integer j = 127时,就会直接从缓存中取,就不会new了
面向对象的三大特征
封装、继承、多态。
java创建对象的方式
1、使用new关键字:比如:Student student = new Student();
2、使用Class类的newInstance方法:
可以使用Class类的newInstance方法创建对象,这个newInstance方法调用无参的构造器创建对象,如:Student student2 = (Student)Class.forName(“根路径.Student”).newInstance(); 或者:Student stu = Student.class.newInstance();
3、使用Constructor类的newInstance方法:本方法和Class类的newInstance方法很像,java.lang.relect.Constructor类里也有一个newInstance方法可以创建对象。我们可以通过这个newInstance方法调用有参数的和私有的构造函数。
如: Constructor constructor = Student.class.getInstance(); Student stu = constructor.newInstance(); 这两种newInstance的方法就是大家所说的反射,事实上Class的newInstance方法内部调用Constructor的newInstance方法。这也是众多框架Spring、Hibernate、Struts等使用后者的原因。
4、使用Clone的方法:无论何时我们调用一个对象的clone方法,JVM就会创建一个新的对象,将前面的对象的内容全部拷贝进去,用clone方法创建对象并不会调用任何构造函数。要使用clone方法,我们必须先实现Cloneable接口并实现其定义的clone方法。如:Student stu2 = stu.clone();这也是原型模式的应用。
5、使用反序列化:序列化和反序列化一个对象,JVM会给我们创建一个单独的对象,在反序列化时,JVM创建对象并不会调用任何构造函数。为了反序列化一个对象,我们需要让我们的类实现Serializable接口。如:ObjectInputStream in = new ObjectInputStream (new FileInputStream(“data.obj”)); Student stu3 = (Student)in.readObject();
什么是反射
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
静态编译和动态编译
- 静态编译:在编译时确定类型,绑定对象
- 动态编译:运行时确定类型,绑定对象
优点: 运行期类型的判断,动态加载类,提高代码灵活度。
缺点: 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的java代码要慢很多。
反射是框架设计的灵魂。
在我们平时的项目开发过程中,基本上很少会直接使用到反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架也大量使用到了反射机制。
举例: 我们在使用JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序;
Spring框架也用到很多反射机制,最经典的就是xml的配置模式。
Spring 通过 XML 配置模式装载 Bean 的过程:
1)将程序内所有 XML 或 Properties 配置文件加载入内存中;
2)Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息;
3)使用反射机制,根据这个字符串获得某个类的Class实例;
4)动态配置实例的属性
java获取反射的三种方法
public class Student {
private int id;
String name;
protected boolean sex;
public float score;
}
public class Get {
//获取反射机制三种方式
public static void main(String[] args) throws ClassNotFoundException {
//方式一(通过建立对象)
Student stu = new Student();
Class classobj1 = stu.getClass();
System.out.println(classobj1.getName());
//方式二(所在通过路径-相对路径)
Class classobj2 = Class.forName("fanshe.Student");
System.out.println(classobj2.getName());
//方式三(通过类名) Class classobj3 = Student.class;
System.out.println(classobj3.getName());
}
}
深拷贝和浅拷贝的区别
数据分为基本数据类型和引用数据类型。
基本数据类型:数据直接存储在栈中;
引用数据类型:存储在栈中的是对象的引用地址,真实的对象数据存放在堆内存里。
浅拷贝:对于基础数据类型:直接复制数据值;对于引用数据类型:只是复制了对象的引用地址,新旧对象指向同一个内存地址,修改其中一个对象的值,另一个对象的值随之改变。
深拷贝:对于基础数据类型:直接复制数据值;对于引用数据类型:开辟新的内存空间,在新的内存空间里复制一个一模一样的对象,新老对象不共享内存,修改其中一个对象的值,不会影响另一个对象。
深拷贝相比于浅拷贝速度较慢并且花销较大。
==和eques的区别
==是运算符,对比的是栈中的值,如果是基本数据类型,则比较的是变量的值,如果是引用类型,则比较的是堆中内存对象的引用地址;
equals是Object的一个方法,默认采用的也是==比较,一般情况下,引用数据类型都会重写equals方法,String类中被重写的equals()方法,其实比较的是两个字符串的内容。
String、StringBuffer、StringBuilder的区别?
String是final修饰的,不可变的,每次操作都会产生一个新的String对象。
StringBuffer和StringBuilder都是在原对象上操作。
StringBuffer是线程安全的,StringBuilder是线程不安全的。
StringBuffer底层是synchronized修饰的。
性能:StringBuilder→StringBuffer→String
优先使用StringBuilder,在多线程场景,需要用到共享变量的情况下,可以考虑使用StringBuffer。
重载和重写的区别
重载:发生在同一个类中,方法名必须相同,参数类型不同,参数个数不同,顺序不同,和返回值没有关系。
重写:发生在父子类中,方法名,参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类,如果父类方法访问修饰符为private,则子类就不能重写该方法。
接口和抽象类的区别
抽象类可以有普通方法,而接口只能定义public abstract方法
抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的。
抽象类只能继承一个,接口可以实现多个
接口的设计目的,是对类的行为进行约束。(更准确的说是一种“有”约束,因为接口不能规定类不可以有什么行为),也就是提供一种机制,可以强制要求不同的类具有相同的行为,它只约束了行为的有无,但不对如何实现行为进行限制。
而抽象的设计目的,是代码复用。当不同的类具有某些相同的行为(记为行为集合A),且其中一部分行为的实现方式一致时(A的非真子集,记为B),可以让这些类派生于一个抽象类,这个抽象类中实现了B,避免让所有的子类来实现B,这就达到了代码复用的目的。而A减B的部分,留给各个子类自己实现,正是因为A-B在这里没有实现,所以抽象类不允许实例化出来(否则当调到A-B时,无法执行)。
抽象类是对类本质的抽象,表达的是is a 的关系,比如:user is Person.抽象类包含并实现子类的通用特效,将子类存在差异化的特征进行抽象,交由子类去实现。
而接口是对行为的抽象,表达的是like a的关系。接口的核心是定义行为,至于实现类具体怎么实现,接口并不关心。
使用场景:当需要关注一个事物本质的时候,用抽象类,当关注一个操作的时候,用接口。
抽象类的功能远超过接口,但是,定义抽象的代价高,因为每个类只能继承一个类,在这个类中,必须继承或者写出字类的所有共性,虽然接口在功能上弱化许多,但是它只针对一个动作的描述。
List和Set的区别
List:有序,按对象进入的顺序保存对象,可重复,允许多个null元素对象,可以使用iterator取出所有的元素,再逐一遍历,还可以使用get(index)获取指定下标的元素。
Set:无序,不可重复,最多允许有一个null元素对象,取元素时只能用iterator接口取得所有元素,再逐一遍历各个元素。
HashCode和equals?
hashCode()的作用是获取哈希码,也称散列码;
它实际是返回一个int的整数,这个哈希码的作用是确定该对象在哈希表中的索引位置。
hashCode()定义在jdk的Object.java中,java中的任何类都包含有hashCode()方法。
散列表存储的是键值对(key-value),它的特点是:根据“key”快速检索出对应的值,这其中就利用了散列码。
为什么要有hashCode
以“hashSet如何检查重复”为例子来说明为什么要有hashcode。
对象加入hashSet时,hashSet会先计算对象的hashCode值来判断对象加入的位置,看该位置是否有值,如果没有,hashCode会假设对象没有重复出现,但是如果发现有值,这时会调用equals()方法来检查两个对象是否真的相同,如果两者相同,hashCode就不会让其加入操作成功,如果不同的话,就会重新散列到其他位置,这样就减少了equals()的次数,相应就大大提高了执行速度。
如果两个对象相等,则hashCode一定也相同的。
两个对象相等,两个对象分别调用equals()方法都返回true。
两个对象有相同的hashCode值,他们也不一定是相等的
因此,equals()方法被覆盖过,则hashCode方法也必须被覆盖。
hashCode()的默认行为是对堆上的对象产生独特值,如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
ArrayList和LinkedList的区别?
首先,他们底层数据结构不同,ArrayList底层是基于数组实现的,linkedList底层是基于链表实现的。
ArrayList:基于动态数组,是连续内存存储,适合下标访问(随机访问),默认容量为10,当元素数量到达容量时,会生成一个新的数组,大小为前一次的1.5倍,然后将原来的数组copy过来。
扩容机制:因为数组长度固定,超出长度存数据时需要新建数组,然后将老数组的数据拷贝到新数组,如果不是尾部插入数据,还会涉及到元素的移动(往后复制一份,插入新元素),使用尾插法并指定初始容量可以极大提升性能,甚至超过linkedList(需要创建大量的node对象)
LinkedList:基于链表,可以存储在分散的内存中,适合做数据插入及删除操作,不适合查询频繁的场景,需要逐一遍历
遍历LinkedList必须使用iterator不能使用for循环,因为每次for循环体内通过get(i)取的某一元素时都需要对list重新进行遍历,性能消耗极大。
另外不要试图使用indexOf等返回元素索引,并利用其进行遍历,使用indexOf对list进行了遍历,当结果为空时会遍历整个列表。
因为数组有索引,所以ArrayList查找数据更快,但是添加数据效率更低
LinkedList的底层使用链表,在内存中是离散的,没有扩容机制;
LinkedList在查找数据时需要从头遍历,所以查找慢,但是添加数据效率更高
List线程安全的三种方式,优缺点?
获取线程安全的List我们可以通过Vector、Collections.synchronizedList() 方法和CopyOnWriteArrayList三种方式
读多写少的情况下,推荐使用CopyOnWriteArrayList方式
读少写多的情况下,推荐使用Collections.synchronizedList()的方式
CopyOnWriteArrayList的底层原理?
首先CopyOnWriteArrayList内部也是用过数组来实现的,在向CopyOnWriteArrayList添加元素时,会复制一个新的数组,写操作在新数组上进行,读操作在原数组上进行
并且,写操作会加锁,防止出现并发写入丢失数据的问题。
写操作结束之后会把原数组指向新数组。
CopyOnWriteArrayList允许在写操作时读取数据,大大提高了读的性能,因此适合读多写少的应用场景,但是CopyOnWriteArrayList会比较占内存,同时可能读到的数据不是实时最新数据,所以不适合实时性要求高的场景。
HashMap和HashTable的区别?底层原理是什么?
hashMap中的方法没有synchronized修饰,非线程安全,
hashTable有synchronized修饰,是线程安全的。
hashMap允许key和value为null,而hashTable不可以
底层实现:数组+链表实现
1)从jdk8开始,链表高度>8,数组长度超过64,链表转变为红黑树;
2)元素以内部类node节点存在计算key的hash值,二次hash,然后对数组长度取模,对应到数组下标;
3)如果没有产生hash冲突(下标位置没有元素),则直接创建node存入数组;
4)如果产生hash冲突,先进行equals比较,相同则取代该元素,不同则判断链表高度插入链表,链表高度达到8,并且数组长度达到64则转为红黑树,长度低于6则将红黑树转回链表;
5)key为null,存在下标0的位置
HashMap的put方法?
- 判断table长度为null或者为0,如果长度为null或者为0就会调用resize方法 ,初始化容量,默认为16
- 如果长度不为null或者0,
- 根据键值key计算hash值,通过哈希算法及异或运算得出数组下标i
- 如果数组下标位置元素为空,则将key和value封装为Entry对象(jdk7中是entry对象,jdk8中是node对象)并放入该位置
- 如果数组下标不为空,并且是jdk7,则先判断是否需要扩容,如果需要扩容,就扩容,如果不用扩容就生成entry对象,并使用头插法添加到当前位置的链表中;
- 如果table[i]不为空,并且是jdk8,则会先判断当前位置上的node的类型,看是红黑树node,还是链表node
- 如果是红黑树node,则将key和value封装为一个红黑树节点(TreeNode)并添加到红黑树中去,在这个过程中会判断红黑树中是否存在当前key,如果存在则更新value
- 如果此位置上的node对象是链表节点,则将key和value封装为一个链表node并通过尾插法插入到链表的最后位置上去,因为是尾插法,所以需要遍历链表,在遍历链表的过程中会判断是否存在当前key,如果存在则更新value,当遍历完链表后,将新链表node插入到链表中,插入到链表后,会看当前链表的节点个数,如果大于等于8,那么则会将该链表转成红黑树
- 将key和value封装为node插入到链表或红黑树中后,再判断是否需要进行扩容,如果需要就扩容,如果不需要就结束put方法。
HashMap的扩容机制原理?
HashMap的默认容量为16,默认的负载因子为0.75,当HashMap中元素个数超过 容量乘以负载因子的个数 时(≈n> (16*0.75)),就创建一个大小为前一次两倍的新数组,再将原来数组中的数据复制到新数组中。当数组长度到达64且链表长度大于8时,链表转为红黑树
线程安全的hashMap?
使用ConcurrentHashMap
使用HashTable
Collections.synchronizedHashMap()方法
ConcurrentHashMap原理,jdk7和jdk8的区别?
jdk7:
JDK1.7:使用分段锁,将一个Map分为了16个段,每个段都是一个小的hashmap,每次操作只对其中一个段加锁
数据结构:ReentrantLock+Segment+HashEntry,一个Segment中包含一个hashEntry数组,每个hashEntry又是一个链表结构
元素查询:二次hash,第一次hash定位到segment,第二次hash定位到元素所在的链表的头部锁(Segment分段锁);
Segment继承了ReentrankLock,锁定操作的Segment,其他的Segment不受影响,并发度为Segment个数,可以通过构造函数指定,数组扩容不会影响其他的segment,get无需加锁,volatile保证;
jdk8:
数据结构: Synchronized+cas+node+红黑树,node的val和next都用volatile修饰,保证可见性查找,替换,赋值操作都使用了cas
锁:锁链表的head节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容时,阻塞所有的读写操作,并发扩容;
读操作无锁:
- node的val和next使用voatile修饰,读写线程对该变量互相可见;
- 数组用volatile修饰,保证扩容时被度线程感知;
JDK1.8:采用CAS+Synchronized保证线程安全,每次插入数据时判断在当前数组下标是否是第一次插入,是就通过CAS方式插入,然后判断f.hash是否=-1,是的话就说明其他线程正在进行扩容,当前线程也会参与扩容;删除方法用了synchronized修饰,保证并发下移除元素安全
java的异常体系?
java中的所有异常都来自顶级父类Throwable
Throwable下有两个子类Exception和Error。
Error是程序无法处理的错误,一旦出现这个错误,则程序将被迫停止运行。
Exception不会导致程序停止,又分为两个部分RunTimeException运行时异常和CheckedException检查异常。
RunTimeException常常发生在程序运行过程中,会导致程序当前线程执行失败,CheckedException常常发生在程序编译过程中,会导致程序编译不通过。
java类加载器?
jdk自带有三个类加载器,bootstrapclassLoader,ExtClassLoader,AppClassLoader;
BootStrapClassLoader是ExtClassLoader的父类加载器,默认负责加载%java_home%lib下的jar包和class文件。
ExtClassLoader是AppClassLoader的父类加载器,负责加载%java_home%/lib/ext/文件夹下的jar包和class类。
AppClassLoader是自定义加载器的父类,负责加载classpath下的类文件,系统类加载器,线程上下文加载器。
继承classLoader实现自定义类加载器。
双亲委派模型
向上委派:实际上就是查找缓存,是否加载了该类,有则直接返回,没有则继续向上委派,委派到顶层之后,缓存中还是没有,则到加载路径中查找,有则加载返回,没有则向下查找。
向下查找:查找加载路径,有则加载返回,没有则继续向下查找。
双亲委派模型的好处:
主要是为了安全性,避免用户自己编写的类动态替换java的一些核心类,比如String。
同时也避免了类的重复加载,因为jvm中区分不同类,不仅仅是根据类名,相同的class文件被不同的calssLoader加载就是不同的两个类。
ThreadLocal的底层原理
- ThreadLocal是java中所提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻,任意方法中获取缓存的数据。
- TheadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对象)中都存在一个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的值。
- 如果在线程池中使用ThreadLocal会造成内存泄露,因为当ThreadLocal对象使用完之后,应该要把设置的key,value,也就是Entry对象进行回收,但线程池中的线程不会回收,而线程对象是通过强引用指向ThreadLocalMap,ThreadLocalMap也是通过强引用指向Entry对象,线程不被回收,Entry对象也就不会被回收,从而出现内存泄露,解决的办法是,在使用了ThreadLocalMap对象之后,手动调用ThreadLocal的remove方法,手动清除Entry对象。
- ThreadLocal经典的应用场景就是连接管理(一个线程持有一个连接,该连接对象可以在不同的方法之间进行传递,线程之间不共享同一个连接)
ThreadLocal的原理和使用场景
每一个Thread对象均含有一个ThreadLocalMap类型的成员变量threadLocals,它存储本线程中所有ThreadLocal对象及其对应的值。
threadLocalMap由一个个Entry对象构成。
Entry继承自weakReference<ThreadLocal<?>>,一个Entry由ThreadLocal对象和object构成,由此可见,Entry的key是ThreadLocal对象,并且是一个弱引用,当没有指向key的强引用后,该key就会被垃圾收集器回收。
当执行set方法时,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象,再以当前ThreadLocal对象为key,将值存储进ThreadLocalMap对象中。
get方法执行过程类似。ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象,再以当前ThreadLocal对象为key,获取对应的value。
由于每一条线程均含有各自私有的ThreadLocalMap容器,这些容器相互独立互不影响,因此不会存在线程安全性问题,从而也无需使用同步机制来保证多条线程访问容器的互斥。
使用场景:
1)、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
2)、线程间数据隔离
3)、进行事物操作,用户存储线程事物信息。
4)、数据库连接,session会话管理。
注释:spring框架在事物开始时会给当前线程绑定一个jdbc Connection,在整个事物过程都是使用该线程绑定的Connection来执行数据库操作,实现了事物隔离性。spring框架里面就是用ThreadLocal来实现这种隔离。
如何理解volatile关键字
在并发领域中,存在三大特性:原子性,有序性,可见性。volatile关键字用来修饰对象的属性,在并发环境下可以保证这个属性的可见性,对于加了volatile关键字的属性,在对这个属性进行修改时,会直接将CPU高级缓存中的数据写回到主内存,对这个变量的读取也会直接从内存中读取,从而保证了可见性,底层是通过操作系统的内存屏障来实现的,由于使用了内存屏障,所以会禁止指令重排,所以同时也就保证了有序性,在很多并发场景下,如果用好volatile关键字可以很好的提高执行效率。
JUC常用辅助类
CountDownLatch:设定一个数,当调用CountDown()时数量减一,当调用await() 时判断计数器是否为0,不为0就阻塞,直到计数器为0
CyclicBarrier:设定一个数,当调用await() 时判断计数器是否达到目标值,未达到就阻塞,直到计数器达到目标值
Semaphore:设定一个信号量,当调用acquire()时判断是否还有信号,有就信号量减一线程继续执行,没有就阻塞等待其他线程释放信号量,当调用release()时释放信号量,唤醒阻塞线程
什么是cas锁?
CAS锁可以保证原子性,思想是更新内存时会判断内存值是否被别人修改过,如果没有就直接更新。如果被修改,就重新获取值,直到更新完成为止。这样的缺点是
(1)只能支持一个变量的原子操作,不能保证整个代码块的原子操作
(2)CAS频繁失败导致CPU开销大
(3)ABS问题:线程1和线程2同时去修改一个变量,将值从A改为B,但线程1突然阻塞,此时线程2将A改为B,然后线程3又将B改成A,此时线程1将A又改为B,这个过程线程2是不知道的,这就是ABA问题,可以通过版本号或时间戳解决
ReentrantLock中的公平锁和非公平锁的底层实现
首先不管是公平锁和非公平锁,他们的底层实现都会使用AQS来进行排队,他们的区别在于:线程在使用lock方法加锁时,如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队,则当前线程也进行排队,如果是非公平锁,则不会去检查是否有线程在排队,而是直接竞争锁。
不管是公平锁还是非公平锁,一旦没竞争到锁,都会进行排队,当锁释放时,都是唤醒排在最前面的线程,所以非公平锁只是体现在了线程加锁阶段,而没有体现线程被唤醒阶段。
另外,ReentrantLock是可重入锁,不管是公平锁还是非公平锁都是可重入的。
ReentrantLock中的tryLock()和lock()方法的区别
- tryLock表示尝试加锁,可能加到,也可能加不到,该方法不会阻塞线程,如果加到锁则返回true,没有加到则返回false。
- lock表示阻塞加锁,线程会阻塞直到加到锁,方法也没有返回值。
CountDownLatch和Semaphore的区别和底层原理
CountDownLatch表示计数器,可以给CountDownLatch设置一个数字,一个线程调用CountDownLatch的await()将会阻塞,其他线程可以调用CountDownLatch的CountDown()方法来对CountDownLatch中的数字减1,当数字被减到0时,所有await的线程都将被唤醒。
对应的底层原理是,调用await()方法的线程会利用AQS排队,一旦数字被减0,则会将AQS中排队的线程依次唤醒。
Semaphore表示信号量,可以设置许可的个数,表示同时允许最多多少个线程使用该信号量,通过acquire()来获取许可,如果没有许可可用线程阻塞,并通过AQS来排队,可以通过release()方法来释放许可,当某个线程释放了某个许可后,会从AQS中正在排队的第一个线程开始依次唤醒,直到没有空闲许可。
Sychronized的偏向锁,轻量级锁,重量级锁
- 偏向锁:在锁对象的对象头中记录一下当前获取到该锁的线程id,该线程下次如果又来获取该锁就可以直接获取到了。
- 轻量级锁:由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时如果有第二个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻量级锁底层是通过自旋来实现的,并不会阻塞线程
- 如果自旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞
- 自旋锁:自旋锁就是线程在获取锁的过程中,不会阻塞线程,也就无所谓唤醒线程,阻塞和唤醒这两个步骤都是需要操作系统去进行的,比较消耗时间,自旋锁是线程通过CAS获取预期的一个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程一直在运行中,相对而言没有使用太多的操作系统资源,比较轻量。
Synchrpnized和lock的区别
(1)synchronized是关键字,lock是一个类
(2) synchronized在发生异常时会自动释放锁,lock需要手动释放锁
(3)synchronized是可重入锁、非公平锁、不可中断锁,lock是可重入锁,可中断锁,可以是公平锁
Sychronized和ReentrankLock的区别
- sychronized是一个关键字,ReentrankLock是一个类
- sychronized会自动的加锁和释放锁,ReentrankLock需要手动加锁和释放锁
- sychronized底层是jvm层面的锁,ReentrankLock是api层面的锁。
- sychronized是非公平锁,ReentrankLock可以选择公平锁和非公平锁
- sychronized锁的是对象,锁信息保存在对象头中,ReentrankLock通过代码中int类型的state标识来标识锁的状态
- sychronized底层有一个锁升级的过程
sleep和wait的区别?
(1)wait()是Object的方法,sleep()是Thread类的方法
(2)wait()会释放锁,sleep()不会释放锁
(3)wait()要在同步方法或者同步代码块中执行,sleep()没有限制
(4)wait()要调用notify()或notifyall()唤醒,sleep()自动唤醒
yield和join的区别?
yield()调用后线程进入就绪状态
A线程中调用B线程的join() ,则B执行完前A进入阻塞状态
sleep(),wait(),join(),yield()
1)锁池:
所有需要竞争同步锁的线程都会放在锁池当中,比如当前对象的锁已经被其中一个线程得到,则其他线程需要在这个锁池进行等待,当前面的线程释放同步锁后锁池中的线程去竞争同步锁,当某个线程得到后会进入就绪队列进行等待cpu资源分配。
2)等待池:
当我们调用wait()方法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁。只有调用了notify()或者notifyAll()方法后等待池的线程才会开始去竞争锁,notify()是随机从等待池选出一个线程放到锁池,而notifyAll()是将等待池的所有线程放到锁池当中。
- sleep是Thread类的静态本地方法,wait是object类的本地方法。
- sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中
sleep就是把cpu的执行资格和执行权释放出去,不再运行此线程,当定时时间结束再取回cpu资源,参与cpu的调度,获取到cpu资源后就可以继续运行了。而如果sleep时该线程有锁,那么sleep不会释放这个锁,而是把锁带着进入了冻结状态,也就是说其他需要这个锁的线程根本不可能获取到这个锁,也就是说无法执行程序。如果在睡眠期间其他线程调用了这个线程的interrupt方法,那么这个线程也会抛出interruptException异常返回,这点和wait是一样的。
- sleep方法不能依赖于同步器synchronized,但是wait需要依赖synchronized关键字。
- sleep不需要被唤醒(休眠之后退出阻塞),但是wait需要(不知道时间需要被别人中断)。
- sleep一般用于当前线程休眠,或者轮循暂停操作,wait则用于多线程之间的通行。
- sleep会让出cpu执行时间且强制上下文切换,而wait则不一定,wait后可能还是有机会重新竞争到锁继续执行的。
yield()执行后线程直接进入就绪状态,马上释放cpu的执行权,但是依然保留了cpu的执行资格,所以有可能cpu下次进行线程调度还会让这个线程获取到执行权继续执行。
join()执行后线程进入阻塞状态,例如在线程B中调用线程A的join(),那线程B会进入到阻塞队列,直到线程A结束或中断线程。
线程的使用?
(1)继承 Tread 类
(2)实现 Runnable 接口
(3)实现 Callable 接口:带有返回值
线程池的底层工作原理
线程池内部是通过队列+线程实现的,当我们利用线程池执行任务时:
1)如果此时线程池中的线程数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
2)如果此时线程池中的线程数量等于codePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。
3)如果此时线程池中的线程数量大于等于codePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
4)如果此时线程池中的线程数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过handler所指定的策略来处理此任务。
5)当线程池中的线程数量大于corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,这样,线程池可以动态的调整池中的线程数。
如何根据 CPU 核心数设计线程池线程数量
IO 密集型:核心数*2
计算密集型: 核心数+1
为什么加 1?即使当计算密集型的线程偶尔由于缺失故障或者其他原因而暂停时,这个额外的线程也能确保 CPU 的时钟周期不会被浪费。
线程的生命周期?线程有几种状态
1)线程通常有五种状态,创建,就绪,运行,阻塞,死亡。
2)阻塞的情况又分为三种:
- 等待阻塞:运行的线程执行wait方法,该线程会释放占用的所有资源,jvm会把该线程放入“等待池”中,进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify或notifyAll方法才能被唤醒,wait是object类的方法。
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则jvm会把该线程放入“锁池”中。
- 其他阻塞:运行的线程执行sleep或者join方法,或者发出了I/O请求时,jvm会把该线程置为阻塞状态,当sleep状态超时,join等待线程终止或者超时,或者I/O处理完毕时,线程重新转入就绪状态。sleep是Thread类的方法
3)状态
- 新建状态(new):新创建了一个线程对象。
- 就绪状态(runnable):线程对象创建后,其他线程调用了该对象的start方法,该状态的线程位于可运行线程池中,变的可运行等待获取Cpu的使用权。
- 运行状态(running):就绪状态的线程获取了CPU,执行程序代码。
- 阻塞状态(blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行,直到线程进入就绪状态,才有机会转到运行状态。
- 死亡状态(dead):线程执行完了或者因异常退出了run方法,该线程结束生命周期。
对线程安全的理解
不是线程安全,应该是内存安全,堆是共享内存,可以被所有线程访问。
当多个线程访问一个对象时,如果不进行额外的同步控制或者其他协调操作,调用这个对象的行为都可以获得正确的结果,我们就说这个对象是线程安全的。
堆是进程和线程共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完了就要还给操作系统,要不然就是内存泄露。
在java中,堆是java虚拟机所管理的内存中最大的一块,是所有线程共享的一块内存区域,在虚拟机启动时创建。堆所存在的内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
栈是每个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈相互独立,因此,栈是线程安全的。操作系统在切换线程的时候会自动切换栈,栈空间不需要在高级语言里显示的分配和释放。
目前主流操作系统都是多任务的,即多个进程同时运行,为了保证安全,每个进程只能访问分配给自己的内存空间,而不能方法别的进件的,这是由操作系统保障的。
在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存)。进程内的所有线程都可以访问到该区域,这就是造成问题的潜在原因。
保证并发安全的三大特征?
原子性:一次或多次操作在执行期间不被其他线程影响
可见性:当一个线程在工作内存修改了变量,其他线程能立刻知道
有序性:JVM对指令的优化会让指令执行顺序改变,有序性是禁止指令重排
Thread,Runable的区别
Thread和Runable的实质是继承关系,没有可比性。无论使用Runnable还是Thread,都会new Thread,然后执行run方法。用法上,如果有复杂的线程操作需求,那就选择继承Thread,如果只是简单的执行一个任务,那就实现Runable。
对守护线程的理解
守护线程:为所有非守护线程提供服务的线程;任何一个守护线程都是整个JVM中所有非守护线程的保姆。
守护线程类似于整个进程的一个默默无闻的小喽喽;它的生死无关重要,它却依赖整个进程而运行;哪天其他线程结束了,没有要执行的了,程序就结束了,理都没理守护线程,它就中断了。
注意:由于守护线程的终止是自身无法控制的,因此千万不要把IO,file等重要操作逻辑分配给它,因为它不靠谱。
守护线程的作用:
比如,GC垃圾回收线程,就是一个典型的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是jvm上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
应用场景:
1)来为其他线程提供服务支持的情况;
2)或者在任何情况下,程序结束时,这个线程必须正常且立刻关闭,就可以作为守护线程来使用;反之,如果一个正在执行某个操作的线程必须要正确的关闭掉,否则就会出现不好的后果的话,那么这个线程就不能是守护线程,而是用户线程,通常都是些关键的事物,比方说,数据库录入或者更新,这些操作都是不能中断的。
thread.setDeamon(true)必须在thread.start()之前设置,否则会抛出一个lllegalThreadStateException异常。不能把政治运行的常规线程设置为守护线程。
在Daemon线程中产生的新线程也是Daemon的。
守护线程不能用于访问固有资源,比如读写操作或者记录逻辑,因为它会在任何时候甚至在一个操作中间发生中断。
java自带的多线程框架,比如ExecutorService,会将守护线程转换为用户线程,所以如果要使用后台线程就不能用java的线程池。
ThreadLocal内存泄露原因,如何避免
内存泄露为程序在申请内存后,无法释放已申请的内存空间。
不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄漏。
强引用:使用普遍的引用,一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,java虚拟机宁愿抛出OutOfMenmoryError错误,使程序异常终止,也不回收这种对象。
如果想取消强引用和某个对象之间的关联,可以显示地将引用赋值为null,这样可以使jvm在合适的时间会回收该对象。
弱引用:jvm进行垃圾回收时,无论内存是否充足,都会回收弱引用关联的对象,在java中,用java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用。
ThreadLocal的实现原理,每个Thread维护一个ThreadLoalMap,key为使用弱引用的ThreadLoal实例,value为线程变量的副本。
headLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时,key(threadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中keynull,而value还存在着强引用,只有thread线程退出以后,value的强引用链条才会断掉,但如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链(红色链条)
key使用强引用
当ThreadLocalMap的key为强引用回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal强引用,如果没有手动删除,ThreadLocal不会被回收,导致entry内存泄漏。
key使用弱引用
当ThreadLocalMap的key为弱引用回收ThreadLocal时,由于ThreadLocalMap持有的ThreadLocal为弱引用,即使没有手动删除,ThreadLocal也会被回收。当key为null,在下一次ThreadLocalMap调用set(),get(),remove()方法的时候会被清除value值。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thead一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
ThreadLocal正确的使用方法
- 每次使用完ThreadLocal都要调用remove()方法清除数据。
- 将ThreadLocal变量定义成private static,这样就一直存在threadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉。
并发,并行,串行的区别
串行在时间上不可能发生重叠,前一个任务没执行完成,下一任务就只能等着
并行在时间上是重叠的,两个任务在同一时刻互不干扰的同时执行。
并发允许两个任务彼此干扰,统一时间点,只有一个任务运行,交替执行。
并发的三大特性
原子性:
原子性是指在一个操作中cpu不可以在中途暂停然后再调度,即不被中断操作,要不全部执行完成,要不都不执行。
程序中原子性指的是最小的操作单元,比如自增操作,它本身其实并不是原子性操作,分了3步的,包括读取变量的原始值,进行加1操作,写入工作内存。所以在多线程中,有可能一个线程还没自增完,可能才执行到第二部,另一个线程就已经读取了值,导致结果错误,那如果我们能保证自增操作是一个原子性的操作,那么就能保证其他线程读取到的一定是自增后的数据。
关键字:synchronized
可见性
当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
若两个线程在不同的cpu,那么线程1改变了i的值还没有刷新到主存,线程2又使用了i,那么这个i值肯定还是之前的,线程i对变量的修改线程没看到这就是可见性问题。
volatle
1.保证被volatle修饰的共享变量对所有线程总是可见的,也就是当一个线程修改了一个被volatle修饰共享变量的值,新值总是可以被其他线程立即得知。
2.禁止指令重排序优化。
为什么用线程池?解释下线程池参数
1、降低资源消耗;提高线程利用率,降低创建和销毁线程下消耗。
2、提高响应速度,任务来了,有可用线程可之间使用而不是来一个创建一个。
3、提高线程的可管理性,线程是稀缺资源,使用线程池可以统一分配调优监控。
corePoolSize代表核心线程数,也就是正常情况下创建工作的线程数,这些线程创建后并不会消除,而是一种常驻线程。
maxinumPoolSize代表的是最大线程数,它与核心线程数相对应,表示最大允许被创建的线程数,比如当前任务较多,将核心线程数都用完了,还无法满足需求时,此时就会创建新的线程,但是线程池内线程总数不会超过最大线程数。
keepAliveTime,unit表示超出核心线程数之外的线程的内存存活时间,也就是核心线程不会消除 ,但是超出核心线程数的部分线程如果空闲一定的时间则会被消除,我们可以通过setkeepAliveTime来设置空闲时间。
workQueue用来存放待执行的任务,建设我们现在核心线程都被使用,还有任务进来则全部放入队列,直到整个队列被放满但任务还在持续进入则会开始创建新的线程。
threadFactory实际上一个线程工厂,用来生产线程执行任务。我们可以选择使用默认的创建工厂,产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以选择自定义线程工厂,一般我们会根据业务来指定不同的线程工厂。
handler任务拒绝策略,有两种情况,第一种是当我们调用shutdown等方法关闭线程后,这时候即使线程池内部还有没有执行完的任务正在执行,但是由于线程池已经关闭,我们再继续向线程池提交任务就会遭到拒绝。另一种情况就是当达到最大线程数,线程池已经没有能力继续处理新提交的任务时,这时也会拒绝。
简述线程池处理流程
线程池执行任务→核心线程是否已满(未满,创建核心线程执行),已满→任务队列是否已满(未满,将任务放入到队列中),已满→最大线程数是否已达到(未达到,创建临时线程执行)→已达到→根据拒绝策略处理任务
线程池中阻塞队列的作用?为什么是先添加队列而不是先创建最大线程?
1、一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,阻塞队列通过阻塞可以保留住当前想要继续入队列的任务。
阻塞队列可以保证队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源
阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活,不至于一直占用cpu资源。
2、在创建新线程的时候,是要获取全局锁的,这个时候其他的就得阻塞,影响了整体效率。
线程池中线程复用原理
线程池将线程和任务进行解耦,线程就是线程,任务就是任务,摆脱了之前通过thread创建线程时的一个线程必须对应一个任务的限制。
在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对thread进行了封装,并不是每次执行任务都会调用thread.start()来创建新线程,而是让每个线程去执行-个“循环任务”,在这个“循环任务”中不停检查是否有任务需要执行,如果有则直接执行,也就是调用任务中的run方法,将run方法当成一个普通的方法执行,通过这种方式只使用固定的线程数就将所有任务的run方法串联起来。
如何查看线程死锁
1)可以通过jstack命令来进行查看,jstack命令中显示发生了死锁的线程
2)或者两个线程去操作数据库时,数据库发生了死锁,这是可以查询数据库的死锁情况
1、查询是否锁表
show open tables where in_use >0;
2、查询进程
show processlist;
3、查看正在锁的事物
select * from information_schema.innodb_locks;
4、查看等待锁的事物
select * from information_schema.innodb_lock_waits;
线程之间是如何通讯的
1)线程之间可以通过共享内存或者基于网络来进行通信
2)如果是通过共享内存来进行通信,则需要考虑并发问题,什么时候阻塞,什么时候被唤醒
3)像java中的wait(),notify()就是阻塞和唤醒
4)通过网络就简单了,通过网络连接将通信数据发送给对方,当然也要考虑并发问题,处理方式就是加锁的方式。
tomcat中为什么要使用自定义类加载器
一个tomcat中可以部署多个应用,而每个应用中都存在多个类,而且各个应用中的类是独立的,全类名可以相同的,比如一个订单系统中可能存在com.zz.c.User类,一个库存系统也可能存在user类,一个tomcat中,不管内部部署了多少个应用,tomcat启动之后就是一个java进程,也就是一个jvm,所以如果tomcat中只存在一个类加载器,比如默认的AppClassLoader,那么就只能加载一个com.user类,这是有问题的,而在tomcat中,会为部署的每个应用都生成一个类加载器实例,名字叫WebAppClassLoader,这样tomcat中每个应用就可以使用自己的类加载器去加载自己的类,从而达到应用之间的类隔离,不出现冲突,另外tomcat还利用自定义加载器实现了热加载功能。
tomcat如何进行优化
对于tomcat调优,可以从两个方面:内存和线程。
首先启动tomcat,实际上就是启动了一个jvm,所以可以按jvm调优的方式来进行调整,从而达到tomcat优化的目的。
另外tomcat中涉及了一些缓存区,比如appReadBufSize,bufferPoolSize等缓存区来提高吞吐量。
还可以调整tomcat的线程,比如调整minSpareThreads参数来改变tomcat空闲时的线程数,调整maxThrads参数来设置tomcat处理连接的最大线程数。
并且还可以调整io模型,比如使用nio,apr这种相比于bio更加高效的io模型。
浏览器发出一个请求到响应经历了哪些步骤
1)浏览器解析用户输入的url,生成一个http格式的请求
2)先根据url域名从本地hosts文件查找是否有映射ip,如果没有就将域名发送给电脑所配置的dns进行域名解析,得到ip地址
3)浏览器通过操作系统将通过四层网络协议发送出去
4)途中可能会经过各种路由器,交换机,最终到达服务器
5)服务器收到请求后,根据请求所指定的端口,将请求传递给绑定了该端口的应用程序,比如8080被tomcat占用了。
6)tocmat接收到请求数据后,按照http协议的格式进行解析,解析得到所要访问的servlet
7)然后servlet来处理这个请求,如果是springmvc中的dispatcherServlet,那么则会找到对应的controller中的方法,并执行该方法得到的结果
8)tomcat得到响应结果后封装成http响应的格式,并再次通过网络发送给浏览器所在的服务器
9)浏览器所在的服务器拿到结果后再传递给浏览器,浏览器则负责解析并渲染。
跨域请求是什么,有什么问题,如何解决?
跨域是指浏览器在发起网络请求时,会检查该请求所对应的协议,域名,端口和当前网页是否一致,如果不一致则浏览器会进行限制,比如在www.baidu.com的某个网页中,如果使用ajax去访问www.jd.com是不行的,但是如果是img,iframe,script等标签的src属性去访问则是可以的,之所以浏览器会做这层限制,是为了用户信息安全,但是如果开发者想绕过这层限制也是可以的,
1)response添加header,比如resp.setHeader("Access-Control-Allow-Origin","*");表示可以访问所有网站,不受是否同源的限制
2)jsonp的方式,该技术底层就是基于script标签来实现的,因为script标签是可以跨域的
3)后台自己控制,先访问同域名下的接口,然后在接口中再去使用HTTPClient等工具去调用目标接口
4)网关,和第三种方式类似,都是交给后台服务来进行跨域处理
JVM
JVM概念
jvm是可运行java代码的虚拟计算机,包括一套字节码指令集,一组寄存器,一个栈,一个垃圾回收,堆和一个存储方法域。jvm是运行在操作系统上的,它与硬件没有直接的交互。
JVM运行过程
我们都知道java源文件,通过编译器,能够生产相应的.class文件,也就是字节码文件,而字节码文件又通过java虚拟机中的解释器,编译成特定机器上的机器码。
也就是:
1)java源文件→编译器→字节码文件
2)字节码文件→jvm→机器码
每一种平台的解释器是不同的,但是实现的虚拟机是相同的,这也就是java为什么能够跨平台的原因,当一个程序从开始运行,这时虚拟机就开始实例化了,多个程序启动就会存在多个虚拟机实例。程序退出或者关闭,则虚拟机实例消亡,多个虚拟机实例直接数据不共享。
JVM线程
这里所说的线程指程序执行过程中的一个线程实体,jvm运行一个应用并发执行多个线程。
hotpost jvm中的java线程与原生操作系统线程有直接的映射关系。当线程本地存储,缓冲区分配,同步对象,栈,程序计数器等准备好以后,就会创建一个操作系统原生线程。
java线程结束,原生线程随之被回收,操作系统负责调度所有线程,并把他们分配到任何可用的cpu上。当原生线程初始化完毕,就会调用java线程的run方法。当线程结束时,会释放原生线程和java线程的所有资源。
hotspot jvm后台运行的系统线程有:
虚拟机线程(vm Thread):这个线程等待jvm到达安全点操作出现,这些操作必须要在独立的线程里执行,因为当堆修改无法进行时,线程都需要jvm位于安全点。这些操作的类型有stop-the-world垃圾回收,线程栈dump,线程暂停,线程偏向锁接触。
周期性任务线程:负责定时器事件,用来调度周期性操作的执行。
gc线程:这些线程支持jvm中不同的垃圾回收活动。
编译器线程:这些线程在运行时将字节码动态编译成本地平台相关的机器码
信号分发线程:这个线程接收发送到jvm的信号并调用适当的jvm方法处理。
JVM内存区域
jvm内存区域主要分为线程私有区域(程序计数器,虚拟机栈,本地方法区),线程共享区域(java堆,方法区),直接内存。
程序计数器
一块较小的内存空间,是当前线程执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”内存。
正在执行java方法的话,计数器记录的是虚拟机字节码指令的地址
这个内存区域是唯一一个在虚拟机中没有规定任何OutOfMemoryError情况的区域。
虚拟机栈
是具体描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表,操作数栈,动态连接,方法出口等信息,每一个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
栈帧是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态连接,方法返回值和异常分派,栈帧随着方法调用而创建,随着方法结束而销毁。
本地方法区
本地方法区和java stack作用类似,区别是虚拟机栈为执行java方法服务,而本地方法栈则为native方法服务,如果一个vm实现使用c-linkage模型来支持native调用,那么该栈将会是一个c栈,但hotspot vm直接就把本地方法栈和虚拟机栈合二为一。
堆(Heap-线程共享)-运行时数据区
是被线程共享的一块内存区域,创建的对象和数组都保存在java堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域,由于现代vm采用分代收集算法,因此java堆从gc的角度还可以细分:新生代(eden区,from survivor区)和老年代区。
方法区/永久代(线程共享)
即我们通常说的永久代,用于存储被jvm加载的类信息,常量,静态变量,即时编译器编译后的代码等数据,hotspot vm把gc分代收集扩展至方法区,即使用java堆堆永久代来实现方法区,这样hotspot的垃圾收集器可以像管理java堆一样管理这部分内存,而不必为方法区开发专门的内存管理器(永久代的内存回收的主要目的是针对常量池的回收和类型的卸载,因此受益一般很小)。
运行时常量池是方法区的一部分。class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中,java虚拟机对class文件的每一部分的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可,装载和执行。
JVM运行时内存
java堆从gc的角度可以细分为:新生代和老年代。
新生代
用来存放新生代的对象,一般占据堆的1/3空间,由于频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收,新生代又分为eden区,servivorFrom,servivorTo三个区。
eden区:
java新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当eden区内存不够的时候就会触发MinorGC,对新生代区进行一次垃圾回收。
servivorFrom
上一次GC的幸存者,作为这一次GC的被扫描者。
servivorTo
保留了一次MinorGC过程中的幸存者。
MinorGC的过程:复制→清空→互换
MinorGC采用复制算法
1)eden,servicorFrom复制到servicorTo,年龄+1
首先,把eden和servivorFrom区域中存活的对象复制到servicorTo区域(如果有对象的年龄已经达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1,如果servicorTo不够位置了就放到老年区
2)清空eden,servicorFrom
然后,清空eden和servicoFrom中的对象
3)servicorTo和servicorFrom互换
最后,servicorTo和servicorFrom互换,原servicor成为下一次GC时的servicorFrom区。
老年代
主要存放应用程序中生命周期长的内存对象。
老年代的对象比较稳定,所以majorGC不会频繁执行,在进行MajorGC前一般都先进行了一次MinorGC,使得有新生代的对象晋升为老年代,导致内存空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次majorGC进行垃圾回收让出空间。
MajorGC采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC的耗时比较长,因为要扫描再回收,majorGC会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配,当老年代也满了,装不下了的时候,会抛出OOM异常。
永久代
指内存的永久保存区域,主要存放class和meta(元数据)的信息,class在加载的时候被放入永久代区域,它和存放实例的区域不同,GC不会在主程序运行期间对永久区域进行清理,所以这也导致了永久代的区域会随着加载的class的增多而装满,最终抛出OOM异常。
java8与元数据
在java8中,永久代已经被移除,被一个称为“元数据区(元空间)”的区域所取代,元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不再虚拟机中,而是使用本地内存,因此,默认情况下,元空间的大小仅受本地内存限制,类的元数据放入native memory,字符串池和类的静态变量放入java堆中,这样可以加载多少类的元数据就不再由maxPermSize控制,而由系统的实际可用空间来控制。
垃圾回收与算法
如何确定垃圾
1)引用计数法
在java中,引用和对象是有关联的。如果要操作对象必须用引用进行,因此,一个简单的办法就是通过引用计数来判断一个对象是否可以回收,(即一个对象如果没哟耦任何与之关联的引用,即他们的引用计数都不为0,则说明对象不太可能再被用到,那么这个对象就是可回收对象)
2)可达性分析:
为了解决引用计数法循环引用的问题,java使用可达性分析的方法,通过一系列的GC roots对象作为起点搜索,如果在GC roots和一个对象之间没有可达路径,则称该对象是不可达的。需要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程,两次标记后仍然是可回收对象,则将面临回收。
标记清除算法
最基础的垃圾回收算法,分为两个阶段,标注和清除,标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。
缺点是:内存碎片化严重,后续可能发生达对象不能找到可利用空间的问题。
复制算法
为了解决mark-sweep算法内存碎片化的缺陷而被提出的算法,按内存容量将内存划分为等大小的两块,每次只使用其中的一块,当这一块内存滞后将存活的对象复制到另一块上去,把已使用的内存清理掉。
这种算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题可用内存被压缩到了原本的一半,且存活对象增多的话,copying算法的效率会大大降低。
标记整理算法
标记阶段和mark-sweep算法相同,标记后不是清理对象,而是将存活的对象移向内存的一端,然后清理端边界外的对象。
分代收集算法
分代收集算法是目前大部分jvm所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将GC堆划分为老年代和新生代,老年代的特点是每次垃圾回收时只有少量对象需要被回收,新生代代特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。
1)新生代与复制法:
目前大部分jvm的gc对于新生代都采用copying算法,因为新生代中每次垃圾回收都要回收大部分对象,即要复制的操作比较少,但通常并不是按照1:1来划分新生代。一般将新生代划分为一块较大的eden空间和两个较小的survivor空间,每次使用eden空间和其中一块的survivor空间,当进行回收时,将该两块空间中还存活的对象复制到另一块survivor空间中。
2)老年代与标记复制算法
而老年代因为每次只回收少量对象,因而采用mark-compact算法
1)java虚拟机提到过的处于方法区的永生代,它用来存储class类,常量,方法描述等。对永生代的回收主要包括废弃常量和无用的类。
2)对象的内存分配主要在新生代的eden 区和survivor区,少数情况会直接分配到老年代。
3)当新生代的eden区和from区空间不足时会发生一次gc,进件gc后,eden区和from 区的存活对象会被挪到to space,然后将eden space 和from space 进行清理。
4)如果to space 无法足够存储某个对象,则将这个对象存储到老年代。
5)在进行gc后,使用的便是eden space和to space了,如此反复循环。
6)当对象在survivor区躲过一次gc后,其年龄就会+1,默认情况下年龄到达15的对象会被移到老年代。
JAVA中四大引用类型
1)强引用:
在java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用,当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到jvm也不会回收,因此强引用是造成java内存泄漏的主要原因之一。
2)软引用:
软引用需要用softReference类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收,软引用通常用在内存敏感的程序中。
3)弱引用:
弱引用需要用weakReference类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管jvm的内存空间是否足够,总会回收该对象占用的内存。
4)虚引用:
虚引用需要phantomReference类来实现,它不能单独使用,必须和引用队列联合使用,虚引用的主要作用是跟踪对象被垃圾回收的状态。
GC分代收集算法VS分区收集算法
1)分代收集算法
这种算法会根据对象存活周期的不同将内存划分为几块,如jvm中的新生代,老年代,永久代,这样就可以根据各年代特点分别采用最适合的gc算法。
在新生代-复制算法:
每次垃圾收集都能发现大批对象已死,只有少量存活,因此选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
在老年代-标记整理算法:
因为对象存活率高,没有额外空间对它进行分配担保,就必须采用“标记-清理”或者“标记-整理”算法来进行回收,不必进行内存复制,且直接腾出空闲内存。
分区收集算法:
分区算法则将整个堆空间划分为连续的不同小区间,每个小区间独立使用,独立回收,这样做的好处是可以控制一次回收多少个小区间,根据目标停顿时间,每次合理地回收若干个小区间(而不是整个堆),从而减少一次gc所产生的停顿。
GC垃圾收集器
java堆内存被划分为新生代和老年代两部分,新生代主要使用复制和标记-清除垃圾回收算法;
老年代主要使用标记-整理垃圾回收算法,因此java虚拟中针对新生代和老年代分别提供了多种不同的垃圾收集器,jdk6中sun- hotspost虚拟机的垃圾收集器如下:
JAVA IO/NIO
阻塞IO模型
最传统的一种IO模型,即在读写数据过程中会发生阻塞现象,当前用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出cpu,当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除block状态,典型的阻塞IO模型的例子为:data=socket.read(),如果数据没有就绪,就会一直阻塞在read方法。
非阻塞IO模型
当用户线程发起一个read操作后,并不需要等待,而是马上就得到一个结果,如果结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作,一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。所以事实上,在非阻塞IO模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用cpu,典型的非阻塞IO模型一般如下:
while(true){
data=socket.read();
if(data!=error){
//处理数据
break;
}
}
但是对于非阻塞io就有一个非常严重的问题,在while循环中需要不断的去询问内核是否需要就绪,这样会导致cpu占用率非常高,因此一般情况下,很少用while循环这种方式读取数据。
多路复用IO模型
多路复用IO模型是目前使用得比较多的模型,java nio实际上是多路复用IO,在多路复用IO模型中,会有一个线程不断去轮询多个socket状态,只有当socket真正有读写事件时,才真正调用实际的io读写操作,因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket读写事件进行时,才会使用IO资源,所以它大大减少了资源占用,在java NIO中,是通过selector.select()去查询每个通道是否有到达事件,如果没有事件,则一直阻塞在那里,因此这种方式会导致用户线程的阻塞。
多路复用IO模型,通过一个线程就可以管理多个socket,只有当scoket真正有读写事件发生才会占用资源来进行实际的读写操作,因此,多路复用io比较适合连接数比较多的情况。
另外多路复用IO为何比非阻塞IO模型的效率高是因为在非阻塞IO中,不断的询问socket状态时通过用户线程去进行的,而在多路复用IO中,轮询每个socket状态是内核在进行的,这个效率要比用户线程要高的多。
不过要注意的是,多路复用IO模型是通过轮询的方式来检测是否有事件到达,而且对到达的事件一一进行响应,因此对于多路复用IO模型来说,一旦事件响应体很大,那么就会导致后续的事件迟迟得不到处理,并且会影响新的事件轮询。
JVM类加载机制
jvm类加载机制分为五个部分:加载,验证,准备,解析,初始化。
1)加载(loading)
加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各个数据的入口,注意这里不一定非得要从一个class文件读取,这里即可以从zip包中读取(比如从jar包和war包中读取),也可以在运行时计算生成(动态代理),也可以由其他文件生成(比如将js文件转换成对应的class类)。
2)验证(verification)
这一阶段的主要目的是为了确保class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机的安全。
3)准备(Preparation)
准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间,注意这里所说的初始值概念,比如一个类变量定义为:
public static int v=8080;
实际上变量v在准备阶段过后的初始值为0而不是8080,将v赋值为8080的put static指令是程序被编译后,存放于类构造器方法中。
但是注意:如果声明为:
public static final int v=8080;
在编译阶段会为v生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将v赋值为8080.
4)解析(Resolution)
解析阶段是指虚拟机将常量池中的符号因为替换为直接引用的过程,符合引用就是class文件中的。
- CONSTANT_Class_info
- CONSTANT_Field_info
- CONSTANT_Method_info
等类型常量。
5)卸载(Unloading)
6)使用(Using)
7)初始化(Initialization)
初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义加载器以外,其他操作都由jvm主导,到了初始阶段,才开始真正执行类中定义的java程序代码。
启动类加载类
负责加载java_home/lib目录中,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。
扩展类加载器
负责加载java_home/lib/ext目录中,或通过java.ext.dirs系统变量指定路径中的类库。
应用程序类加载器
负责加载用户路径上的类库。
jvm通过双亲委派模型进行类的加载,当然我们也可以通过继承java.lang.classloader实现自定义类加载器。
项目中如何排查jvm问题
1)可以使用jmap来查看jvm中各个区域的使用情况
查看内存情况
jmap pid
查看堆信息,如年轻代,老年代
jmap -head pid
查看堆对象统计信息,如数量,大小
jmap -histo pid
查看堆中元空间信息
jmap -clstats pid
2)可以通过jstack来查看线程的运行情况,比如哪些线程阻塞,是否出现了死锁
3)可以通过jstat命令来查看垃圾回收的情况,特别是fullgc,比如发现fullgc比较频繁,那么就得进行调优了
查看垃圾回收情况
jstat -gc pid
他的意思就是每隔1秒钟更新出来最新的一行jstat统计信息,一共执行10次jstat统计
jstat -gc pid 1000 10
4)通过各个命令的结果,或者jvisualvm等工具来进行分析
5)首先,初步猜测频繁发送fullgc的原因,如果频繁发送fullgc但是又一直没有出现内存溢出,那么表示funllgc实际上是回收了很多对象了,所以这些对象最好能在younggc过程中就直接回收掉,避免这些对象进入到老年代,对于这种情况,就要考虑这些存活时间不长的对象是不是比较大,导致年轻代放不下,直接进入到了老年代,尝试加大年轻代的大小,如果改完之后,fullgc减少,则证明修改有效。
6)同时,还可以找到cpu最多的线程,定位到具体的方法,优化这个方法的执行,看是否能避免某些对象的创建,从而节省内存。
对于已经发生了OOM的系统:
1)一般生产系统中都会设置当系统发生OOM时,生成当时的dump文件
(-XX:+HeapDumpOnOutOfMemoryError -XX :HeapDumpPath=/usr/local/base)
2)我们可以利用jsisualvm等工具来分析dump文件
3)根据dump文件找到异常的实例对象,和异常的线程(占用cpu高),定位具体的代码
4)然后再进行详细的分析和调试
GC如何判断对象可以被回收
- 引用计数法:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。
- 可达性分析法:从GC Roots开始向下搜索,搜索所走过的路径为引用链。当一个对象GC Roots没有任何引用链相连时,则证明次对象是不可用的,那么虚拟机就判断是可回收对象。
引用计数法,可能会出现A引用B,B又引用了A,这时候就算他们都不再使用了,但因为相互引用 ,计数器=1,永远无法被回收。
GC Roots的对象有:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
可达性算法中的不可达对象并不是立即死亡的,对象拥有一次自我拯救的机会,对象被系统宣告死亡至少要经历两次标记过程;第一次是经过可达性分析发现没有与GC Roots相连接的引用链,第二次是在由虚拟机自动建立的Finalizer队列中判断是否需要执行finalize()方法。
当对象变成GC Roots不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法,执行finallize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”
每个对象只能触发一次finalize方法
由于finalize方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,不推荐大家使用,建议遗忘它。
总结:
(1)引用计数法:已淘汰,为每个对象添加引用计数器,引用为0时判定可以回收,会有两个对象相互引用无法回收的问题
(2)可达性分析法:从GCRoot开始往下搜索,搜索过的路径称为引用链,若一个对象GCRoot没有任何的引用链,则判定可以回收
GCRoot有:虚拟机栈中引用的对象,方法区中静态变量引用的对象,本地方法栈中引用的对象
JVM运行时数据区(内存结构)
线程私有区:
(1)虚拟机栈:每次调用方法都会在虚拟机栈中产生一个栈帧,每个栈帧中都有方法的参数、局部变量、方法出口等信息,方法执行完毕后释放栈帧
(2)本地方法栈:为native修饰的本地方法提供的空间,在HotSpot中与虚拟机合二为一
(3)程序计数器:保存指令执行的地址,方便线程切回后能继续执行代码
线程共享区:
(4)堆内存:Jvm进行垃圾回收的主要区域,存放对象信息,分为新生代和老年代
(5)方法区:存放类信息、静态变量、常量、运行时常量池等信息。JDK1.8之前用持久代实现,JDK1.8后用元空间实现,元空间使用的是本地内存,而非在JVM内存结构中
什么情况下会内存溢出?
堆内存溢出:
(1)当对象一直创建而不被回收时
(2)加载的类越来越多时
(3)虚拟机栈的线程越来越多时
栈溢出:方法调用次数过多,一般是递归不当造成
JVM有哪些垃圾回收算法?
(1)标记清除算法: 标记不需要回收的对象,然后清除没有标记的对象,会造成许多内存碎片。
(2)复制算法: 将内存分为两块,只使用一块,进行垃圾回收时,先将存活的对象复制到另一块区域,然后清空之前的区域。用在新生代
(3)标记整理算法: 与标记清除算法类似,但是在标记之后,将存活对象向一端移动,然后清除边界外的垃圾对象。用在老年代
典型垃圾回收器
G1 :JDK1.9以后的默认垃圾回收器,支持并发,采用标记整理+复制算法,注重响应速度
类加载过程
(1)加载 :加载字节码文件,将字节码中的静态变量和常量转换到方法区中,在堆中生成class对象作为方法区入口
(2)连接:
验证:验证字节码文件的正确性。
准备:正式为类变量在方法区中分配内存,并设置初始值。
解析:将符号引用(如类的全限定名)解析为直接引用(类在实际内存中的地址)。()
(3)初始化 :执行类构造器(不是常规的构造方法),为静态变量赋初值并初始化静态代码块。
JVM中有哪些引用?
强引用:new的对象。哪怕内存溢出也不会回收
软引用:只有内存不足时才会回收
弱引用:每次垃圾回收都会回收
虚引用:必须配合引用队列使用,一般用于追踪垃圾回收动作
对象头中有哪些信息
对象头中有两部分,一部分是MarkWork,存储对象运行时的数据,如GC分代年龄、GC标记、锁的状态、线程ID等;另外一部分是指向对象类型的指针,如果是数组,还有一个部分存放数组长度
JVM内存参数
-Xmx[]:堆空间最大内存
-Xms[]:堆空间最小内存,一般设置成跟堆空间最大内存一样的
-Xmn[]:新生代的最大内存
-xx[use 垃圾回收器名称]:指定垃圾回收器
-xss:设置单个线程栈大小
一般设堆空间为最大可用物理地址的百分之80
JVM类初始化顺序
父类静态代码块和静态成员变量->子类静态代码块和静态成员变量->父类代码块和普通成员变量->父类构造方法->子类代码块和普成员变量->子类构造方法
说一下jvm中,哪些是共享区,哪些可以作为gc root
1、堆区和方法区是所有线程共享的,栈,本地方法栈,程序计数器是每个线程独有的
2、什么是gc root,jvm在进行垃圾回收时,需要找到“垃圾”对象,也就是没有被引用的对象,但是直接找“垃圾”对象是比较耗时的,所以反过来,先找“非垃圾”对象,也就是正常对象,那么就需要从某些根开始找,根据这些根的引用路径找到正常对象,而这些根有一个特征,就是它只会引用其他对象,而不会被其他对象引用,如:栈中的本地变量,方法区中的静态变量,本地方法栈中的变量,正在运行的线程等可以作为gc root。
spring
Spring是什么?
轻量级的开源j2ee框架,它是一个容器框架,用来装javabean(java对象),中间层框架(万能胶)可以起到一个连接作用,比如说把struts和hibernate粘合在一起运行,可以让我们的企业开发更快,更简洁
spring是一个轻量级的控制反转(ioc)和面向切面(aop)的容器框架
1)从大小与开销两方面spring都是轻量级的
2)通过控制反转(ioc)的技术达到松耦合的目的
3)提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务进行内聚性的开发
4)包含并管理应用对象(bean)的配置和生命周期,这个意义上是一个容器
5)将简单的组件配置,组合成为复杂的应用,这个意义上是一个框架
谈谈你对ioc的理解
容器概念,控制反转,依赖注入
ioc容器:
实际上就是map(key,value),里面存的是各种对象(在xml里配置的bean节点,@Repository,@service,@Controller,@Component),在项目启动的时候会读取配置文件里面的bean节点,根据全限类名使用反射创建对象放到map里,扫描到搭上标注注解的类还是通过反射创建对象放到map里。
这时候map里就有各种对象了,接下来我们在代码里需要用到里面的对象时,再通过id注入(autowired,resource)等注解,xml里bean节点内的ref属性,项目启动的时候会读取xml节点ref属性根据id注入,也会扫描这些注解,根据类型或id注入;id就是对象名);
控制反转:
没有引入ioc容器之前,对象a依赖对象b,那么对象a在初始化或者运行到某一点的时候,自己必须主动去创建对象b或者使用已经创建的对象b,无论是创建还是使用对象b,控制权都在自己手上。
引入ioc容器后,对象a与对象b之间失去了直接联系,当对象a运行到需要对象b的时候,ioc容器会主动创建一个对象b,注入到对象a需要的地方。
通过前后的对比,不难看出,对象a获得依赖对象b的过程,由主动行为变为被动行为,控制权颠倒过来了,这就是控制反转。
全部对象的控制权给ioc容器,所以,ioc容器成了整个系统的关键核心,它起到了一种类似“粘合剂”的作用,把系统中的所有对象粘合在一起发挥作用,如果没有这个粘合剂,对象与对象之间会彼此失去联系。
依赖注入:
获得依赖对象的过程被反转了,控制反转后,获得依赖对象的过程由ioc容器主动注入,依赖注入是实现ioc的方法,就是由ioc容器在运行期间,动态的将某种依赖关系注入到对象之中。
如何实现一个ioc容器
1)配置文件配置包扫描路径
2)递归包扫描获取.class文件
3)反射,确定需要交给ioc管理的类
4)对需要注入的类进行依赖注入
- 配置文件中指定需要扫描的包路径
- 定义一些注解,分别表示控制层,业务服务层,数据持久层,依赖注入注解,获取配置文件注解
- 从配置文件中获取需要扫描的包路径,获取当前路径下文件信息及文件夹信息,我们将当前路径下所有以.class结尾的文件添加到一个set集合中进行存储
- 遍历着set集合,获取在类上有指定注解的类,并将其交给ioc容器,定义一个安全的map用来存储这些对象
- 遍历这个ioc容器,获取到每个类的实例,判断里面是否有依赖其他类的实例,然后进行递归注入
谈谈你对aop的理解
系统是有许多不同的组件组成的,每一个组件各自负责一块特定的功能,除了实现自身的核心功能外,这些组件还经常承担着额外的职责,比如:日志,事务管理,安全这样的核心服务经常融入到自身具有核心业务逻辑的组件中去,这些系统服务经常被称为横切关注点,因为他们会跨越系统的多个组件。
当我们需要为分散的对象引入公共行为的时候,oo则显的无能为力,也就是说,oop允许你定义从上到下的关系,但不适合定义从左到右的关系,比如日志功能。
日志代码往往水平的散步在所有对象层次中,而与它所散步的对象的核心功能毫无关系。
在oop设计中,它导致类大量代码的重复,而不利于各个模块的重用。
aop:将程序中交叉的业务逻辑(比如,日志,事务等),封装成一个切面,然后注入到目标对象(具体业务中),aop可以对某个对象或者某些对象的功能进行增强,比如对象中的方法进行增强,可以在执行某个方法之前额外的做一些事情,在某个方法执行之后做一些事情。
介绍下spring,spring的启动流程?
1)spring是一个快速开发框架。
2)spring的源码实现是非常优秀的,设计模式的应用,并发安全的实现,面向接口的设计等
3)在创建spring容器,也就是启动spring时:
- 首先会进行扫描,扫描得到所有BeanDefinition对象,并存在一个map中
- 然后筛选出非懒加载的单例BeanDefinition进行创建bean,对于多例bean不需要在启动过程中去创建,对于多例bean会在每次获取bean时利用beanDefinition去创建
- 利用BeanDefinition创建Bean就是Bean的创建生命周期,这期间包括了合并BeanDefiniton,推断构造方法,实例化,属性填充,初始化前,初始化,初始化后等步骤,其中aop就是发生在初始化后这一步骤中。
4)单例bean创建完之后,Spring会发布一个容器启动事件
5)Spring启动结束
6)在源码中会更复杂,比如源码中会提供一些模版方法,让子类实现,还设计到BeanFactoryPostProcessor和BeanPostProcessor的注册,Spring的扫描就是通过BeanFactoryPostPrecessor来实现的,依赖注入就是通过BeanPostProcessor来实现的
7)在Spring启动过程还会去处理@Import等注解。
spring中的bean创建的生命周期有哪些步骤
sprng中的bean的创建大概分为以下几个步骤:
- 推断构造方法
- 实例化
- 填充属性,也就是依赖注入
- 处理aware回调
- 初始化前处理@postConstruct注解
- 初始化,处理initializingBean接口
- 初始化后,进行aop
BeanFactory和ApplicationContext有什么区别?
ApplicationContext是BeanFactory的子接口
ApplicationContext提供了更完整的功能
1)继承MessageSource,因此支持国际化
2)统一的资源文件访问方式
3)提供在监听器中注册bean的事件
4)同时加载多个配置文件
5)载入多个上下文,使得每一个上下文都专注于一个特定的层次,比如应用web层,
- beanFactroy采用的是延迟加载形式来注入bean的,即只有在使用到某个bean时(调用getBean()),才对该bean进行加载实例化,这样,我们就不能发现一些存在的spring的配置问题,如果bean的某一个属性没有注入,beanFactory加载后,直至第一次使用调用getBean方法才会抛出异常。
- ApplicationContext,它是在容器启动时,一次性创建了所有的bean,这样,在容器启动时,我们就可以发现spring中存在的配置错误,这样有利于检查所依赖属性是否注入。ApplicaitonContext启动后预载入所有的单实例bean,通过预载入单实例bean,确保当你需要的时候,你就不用等待,因为他们已经创建好了。
- 相对于基本的beanFactory,ApplicationContext,唯一不足的是占用内存空间,当应用程序配置bean较多时,程序启动较慢。
- BeanFactory和ApplicationContext都支持BeanPostProcessor,BeanFactoryPostProcessor的使用,使两者之间的区别是:BeanFactory需要手动注册,而ApplicationContext则是自动注册。
描述一下Spring Bean的生命周期?
- 解析类得到BeanDefinition
- 如果有多个构造方法,则要推断构造方法
- 确定好构造方法,进行实例化得到一个对象
- 对对象中加了@Autowired注解的属性进行属性填充
- 回调Aware方法,比如BeanNameAware,BeanFactoryAware
- 调用BeanPostProcessor的初始化前的方法
- 调用初始化方法
- 调用BeanPostProcessor的初始化后的方法,在这里会进行aop
- 如果当前创建的bean是单例则会把bean放入单例池
- 使用bean
- spring容器关闭时调用DisposableBean中的destory方法。
解释下spring支持的几种bean的作用域?
- singleton:默认,每个容器中只有一个bean的实例,单例的模式由BeanFactory自身来维护。该对象的生命周期是spring ioc容器一直的。
- prototype:为每一个bean请求提供一个实例,在每次注入时都会创建一个新的对象
- request:bean被定义为在每个http请求中创建一个单例对象,也就是说在单个请求中都会复用这一个单例对象。
- session:与request范围类似,确保每个sesssion中有一个bean的实例,在session过期后,bean会随之失效。
- application:bean被定义为在servletContext的生命周期中复用一个单例对象。
- websocket:bean被定义为websocket的生命周期中复用一个单例对象。
- global-session:全局作用域,global-session和portlet应用相关,当你的应用部署在portlet容器中工作时,它包含很多portlet,如果你想要生命让所有的portlet共用全局的存储变量的话,那么这全局变量需要存储在global-session中,全局作用域与servlet中的session作用域效果相同。
Spring框架中的单例bean是线程安全的吗?
spring中的bean默认时单例模式的,框架并没有对bean进行多线程的封装处理。
如果bean是有状态的,那就需要自己来来进行线程安全的保证,最简单的办法就是改变bean的作用域把"singleton"改为“prototype”这样每次请求bean就相当于是new bean()这样就可以保证线程的安全了。
- 有状态就是有数据存储功能
- 无状态就是不会保存数据,controller,service和dao层本身并不是线程安全的,如果只是调用里面的方法,而且多线程调用一个实例方法,会在内存中复制变量,这是自己的线程的工作内存,是安全的 。
dao 会操作数据库Connection,Connection是来有状态的,比如说数据库事务,spring的事务管理器使用TreadLocal为不同的线程维护了一套独立的connection副本,保证线程之间不会相互影响。
不要在bean中声明任何有状态的实例变量或类变量,如果必须如此,那么就使用threadlocal把变量变为线程私有的,如果bean的实例变量和类变量需要在多个线程之间共享,那么就只能使用synchronized,lock,cas,等这些实例线程同步的方法了。
Spring框架中都用到了哪些设计模式?
简单工厂:由一个工厂类根据传入的参数,动态决定应该创建哪一个产品类。
spring中的BeanFactory就是简单工厂模式的体现,根据传入的一个唯一的标识来获取bean对象,但是否是在传入参数后创建还是传入前创建要根据具体情况来定
工厂方法:
实现了FactoryBean接口的bean是一个类叫做factory的bean,特点是,spring会在使用getBean()调用获取该bean时,会自动调用该bean的getObject()方法,所以返回的不是factory这个bean,而是这个bean.getObject()方法的返回值。
单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点
spring中的单例模式完成了后半句话,即提供了全局的访问点BeanFactory,但没有从构造器级别去控制单例,这是因为spring管理的是任意的java对象。
适配器模式:
spring定义了一个适配器接口,使得每一种Controller有一种对应的适配器实现类,让适配器代替controller执行相应的方法,这样在扩展controller时,只需要增加一个适配器类就完成了springmvc的扩展了。
装饰器模式:动态的给一个对象添加一些额外的职责,就增加功能来说,Decorator模式相比生成子类更为灵活。
spring中用到的包装器模式在类上有两种表现:一种是类名中含有Wrapper,另一种是类名中含有Decorator.
动态代理:
一般情况下,在使用切面时,aop容器会为目标对象动态的创建一个代理对象,springaop就是以这种方式切入的。
观察者模式:
spring的事件驱动模型使用的是观察者模式,spring中Observer模式常用的地方是listener的实现。
策略模式:
spring框架的资源访问resource接口,该接口提供了更强的资方访问能力,spring框架本身使用了resouce接口来访问底层资源。
模版方法:父类定义了骨架(调用哪些方法及顺序),某些特定方法由子类实现
最大的好处:代码复用,减少重复代码,除了子类要实现的特定方法,其他方法及方法调用顺序都在父类中预先写好了。
refresh方法。
Spring事务的实现方式和原理以及隔离级别?
在使用spring框架时,有两种使用事务的方式,一种是编程式的,一种是声明式的。
@Transaction注解就是声明式的。
首先,事务这个概念是数据库层面的,spring只是基于数据库中的事务进行了扩展,以及提供了一些能让程序员更加方便操作事务的方式。
比如我们可以通过在某个方法上增加@Transcation注解,就可以开启事务,这个方法中所有的sql都会在一个事务中执行,统一成功/失败。
在一个方法上加了@Transcation注解后,spring会基于这个类生成一个代理对象,会将这个代理对象作为bean,当在使用这个代理对象时,如果这个方法会存在@Transaction注解,那么代理逻辑会先把事务的自动提交设置为false,然后再去执行原本的业务逻辑方法,如果执行业务逻辑方法没有出现异常,那么代理逻辑中就会将事务进行提交,如果执行业务逻辑方法出现了异常,那么则会将事务进行回滚。
当然,针对哪些异常回滚事务是可以配置的,可以利用@Transcation注解中的rollbackFor属性进行配置,默认情况下会对RuntimeException和Error进行回滚。
spring事务隔离级别就是数据库的隔离级别:外加一个默认级别
- read uncommitted(未提交读)
- read committed(提交读,不可重复读)
- repeatable read(可重复的)
- serializable(可串行化)
数据库的配置隔离级别是 read commited,而spring配置的隔离级别是repeatable read ,请问这时隔离级别是以哪一个为准?
以spring配置为准,如果spring设置的隔离级别数据库不支持,效果取决于数据库。
Spring事务传播机制
多个事务方法相互调用时,事务如何在这些方法间传播
方法a是一个事务的方法,方法a执行过程中调用了方法b,那么方法b有无事务以及方法b对事务的要求不同都会对方法a的事务具体执行造成影响,同时方法a的事务对方法b的事务执行也有影响,这种影响具体是什么就由两个方法定义的事务传播类型所决定。
REQUIRED(spring默认的事务传播类型):如果当前没有事务,则自己新建一个事务,如果当前存在事务,则加入这个事务。
SUPPORTS:当前存在事务,则加入当前事务,如果当前没有事务,就以非事务方法执行
MANDATORY:当前存在事务,则加入当前事务,如果当前事务不存在,则抛出异常。
REQUIRES_NEW:创建一个新事物,如果存在当前事务,则挂起该事务。
NOT_SUPPORTED:以非事务方法执行,如果当前存在事务,则挂起当前事务
NEVER:不使用事务,如果当前事务存在,则抛出异常
NESTED:如果当前事务存在,则在嵌套事务中执行,否则REQUIRED的操作一样(开启一个事务)
和REQUIRES_NEW的区别
REQUIRES_NEW是新建一个事务并且新开启的这个事务与原事务无关,而NESTED则是当前存在事务时,会开启一个嵌套事务,在NESTED情况下父事务回滚时,子事务也会回滚,而在REQUIRES_NEW情况下,原有事务回滚,不会影响新开启的事务。
和REQUIRED的区别
REQUIRED情况下,调用方法存在事务时,则被调用方和调用方使用同一事务,那么被调用方出现异常时,由于共用一个事务,所以无论调用方法是否catch其异常,事务都会回滚,而在NESTED情况下,被调用方发生异常时,调用方可以catch其异常,这样只有子事务回滚,父事务不受影响。
总结:
(1)支持当前事务,如果不存在,则新启一个事务
(2)支持当前事务,如果不存在,则抛出异常
(3)支持当前事务,如果不存在,则以非事务方式执行
(4)不支持当前事务,创建一个新事物
(5)不支持当前事务,如果已存在事务就抛异常
(6)不支持当前事务,始终以非事务方式执行
Spring事务什么时候会失效?
spring事务的原理是aop,进行了切面增强,那么失效的根本原因就是这个aop不起作用了,常见的情况有:
1)发生自调用,类里面使用this调用本类的方法(this通常省略),此时这个this对象不是代理类,而是UserService对象本身。
解决方法很简单,让那个this变成UserService的代理类即可。
2)方法不是public的
@Transaction只能用于public方法上,否则事务不会生效,如果非要在非public方法上,可以开启Aspectj代理模式
3)数据库不支持事务
4)没有被spring管理
5)异常被吃掉,事务不会回滚(或者抛出的异常没有被定义,默认为RunTimeException)
什么是bean的自动装配,有哪些方式?
开启自动装配,只需要在xml配置文件中定义“autowire”属性。
<bean id="cutomer" class="com.xx.xx.cutomer" autowire="" />
autowire属性有五种装配方式:
1)no-缺省情况下,自动配置是通过ref属性手动设定
手动装配:以value或ref的方式声明明确指定属性值都是手动装配。需要通过ref属性来连接bean。
2)byName-根据bean的属性名称进行自动装配
cutomer的属性名称是person,spring会将bean id为person的bean通过setter方法进行自动装配
<bean id="cutomer" class="com.xx.xx.Cutomer" autowire="byName"/>
<bean id="person" class="com.xx.Person">
3)byType-根据bean的类型进行自动装配
4)constructor:类似byType,不过是应用于构造器的参数,如果一个bean与构造器参数的类型相同,则进行自动装配,否则导致异常。
5)autodetect:如果有默认值的构造器,则通过constructor方式进行自动装配,否则使用byType方式进行自动装配。
@Autowired自动装配bean,可以在字段,setter方法,构造器上使用。
说一下Spring的事务机制
1)spring事务底层基于数据库事务和aop机制的。
2)首先对于使用了@Transaction注解的bean,spring会创建一个代理对象作为对象bena。
3)当调用代理对象的方法时,会先判断该方法上是否加了@Transaction注解
4)如果加了,那么则利用事务管理器创建一个数据库连接
5)并且修改数据库连接的autocommit属性为false,禁止此连接的自动提交,这是实现spring事务非常重要的一步。
6)然后执行当前方法,方法中会执行sql。
7)执行完当前方法后,如果没有出现异常就直接提交事务
8)如果出现异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务
9)spring事务的隔离级别对应的就是数据库的隔离级别
10)spring事务的传播机制是spring事务自己实现的,也是spring事务中最复杂的
11)spring事务的传播机制是基于数据库连接来的,一个数据库连接一个事务,如果传播机制配置为需要新开一个事务,那么实际上就是先建立一个数据库连接,在此新数据库连接上执行sql。
什么时候@Transaction失效
因为spring事务是基于代理实现的,所以某个加了@Transcation的方法只有是被代理对象调用时,那么这个注解才会生效。
同时如果某个方法是private的,那么@Transcation也会失效,因为底层cglib是基于父子类来实现的 ,自来是不能重载父类private方法的,所以无法很好的利用代理,也会导致@Transcation失效。
Bean 的作用域
(1)Singleton:一个IOC容器只有一个
(2)Prototype:每次调用getBean()都会生成一个新的对象
(3)request:每个http请求都会创建一个自己的bean
(4)session:同一个session共享一个实例
(5)application:整个serverContext只有一个bean
(6)webSocket:一个websocket只有一个bean
Bean 生命周期
实例化 Instantiation->属性赋值 Populate->初始化 Initialization->销毁 Destruction
在这四步的基础上面,Spring 提供了一些拓展点:
*Bean 自身的方法: 包括了 Bean 本身调用的方法和通过配置文件中的 init-method 和 destroy-method 指定的方法
*Bean 级生命周期接口方法:包括了 BeanNameAware、BeanFactoryAware、InitializingBean 和 DiposableBean 这些接口的方法
*容器级生命周期接口方法:包括了 InstantiationAwareBeanPostProcessor 和 BeanPostProcessor 这两个接口实现,一般称它们的实现类为“后处理器”。
*工厂后处理器接口方法: 包括了 AspectJWeavingEnabler, ConfigurationClassPostProcessor, CustomAutowireConfigurer 等等非常有用的工厂后处理器接口的方法。工厂后处理器也是容器级的。在应用上下文装配配置文件之后立即调用。
Spring 事务原理?
spring事务有编程式和声明式,我们一般使用声明式,在某个方法上增加@Transactional注解,这个方法中的sql会统一成功或失败。
原理是:
当一个方法加上@Transactional注解,spring会基于这个类生成一个代理对象并将这个代理对象作为bean,当使用这个bean中的方法时,如果存在@Transactional注解,就会将事务自动提交设为false,然后执行方法,执行过程没有异常则提交,有异常则回滚、
spring事务失效场景
(1)事务方法所在的类没有加载到容器中
(2)事务方法不是public类型
(3)同一类中,一个没有添加事务的方法调用另外以一个添加事务的方法,事务不生效
(4)spring事务默认只回滚运行时异常,可以用rollbackfor属性设置
(5)业务自己捕获了异常,事务会认为程序正常秩序
spring用了哪些设计模式
BeanFactory用了工厂模式,AOP用了动态代理模式,RestTemplate用来模板方法模式,SpringMVC中handlerAdaper用来适配器模式,Spring里的监听器用了观察者模式
SpringMV工作原理
SpringMVC工作过程围绕着前端控制器DispatchServerlet,几个重要组件有HandleMapping(处理器映射器)、HandleAdapter(处理器适配器)、ViewReslover(试图解析器)
工作流程:
(1)DispatchServerlet接收用户请求将请求发送给HandleMapping
(2)HandleMapping根据请求url找到具体的handle和拦截器,返回给DispatchServerlet
(3)DispatchServerlet调用HandleAdapter,HandleAdapter执行具体的controller,并将controller返回的ModelAndView返回给DispatchServler
(4)DispatchServerlet将ModelAndView传给ViewReslover,ViewReslover解析后返回具体view
(5)DispatchServerlet根据view进行视图渲染,返回给用户
springboot自动配置原理
启动类@SpringbootApplication注解下,有三个关键注解
(1)@springbootConfiguration:表示启动类是一个自动配置类
(2)@CompontScan:扫描启动类所在包外的组件到容器中
(3)@EnableConfigutarion:最关键的一个注解,他拥有两个子注解,其中@AutoConfigurationpackageu会将启动类所在包下的所有组件到容器中,@Import会导入一个自动配置文件选择器,他会去加载META_INF目录下的spring.factories文件,这个文件中存放很大自动配置类的全类名,这些类会根据元注解的装配条件生效,生效的类就会被实例化,加载到ioc容器中
springboot常用注解
@RestController :修饰类,该控制器会返回Json数据
@RequestMapping("/path") :修饰类,该控制器的请求路径
@Autowired : 修饰属性,按照类型进行依赖注入
@PathVariable : 修饰参数,将路径值映射到参数上
@ResponseBody :修饰方法,该方法会返回Json数据
@RequestBody(需要使用Post提交方式) :修饰参数,将Json数据封装到对应参数中
@Controller@Service@Compont: 将类注册到ioc容器
springmvc
spring boot,spring mvc ,spring 的区别是什么?
1)Spring是一个ioc容器,用来管理bean,使用依赖注入实现控制反转,可以很方便的整合各种框架,提供aop机制弥补oop代码重复问题,更方便将不同类不同方法中的共同处理抽取成切面,自动注入给方法执行,比如日志,异常等。
2)spring mvc是spring对web框架的一个解决方案,提供了一个总的前端控制器servlet,用来接收请求,然后定义了一套路由策略(url到handle的映射)及适配执行handle,将handle结果使用视图解析技术生成视图展现给前端。
3)springboot是spring提供了一个快速开发工具包,让程序员能更方便,更快速的开发spring+springmvc应用,简化了配置(约定了默认配置),整合了一系列的解决方案(starter机制),redis,mongodb,es,可以开箱即用。
spring mvc工作流程
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响应用户。
spring mvc 的主要组件?
Handler:也就是处理器,它直接应对着mvc中的Controller层,它的具体表现形式有很多,可以是类,也可以是方法,在Controller层中@RequestMapping标注的所有方法都可以看成是一个Handler,只要可以实际处理请求就可以是Handler。
1)HandlerMapping
initHanderMapping(context),处理器映射器,根据用户请求的资源uri来查找Handler的,在springMvc中会有很多请求,每个请求都需要一个Handler处理,具体接受到一个请求之后使用哪个Handler进行,这就是HandlerMapping需要做的事。
2)HandlerAdapter
initHandlerAdapters(context),适配器,因为springMVC中的Handler可以是任意的形式,只要能处理请求就ok,但是servlet需要的处理方法的结构却是固定的,都是以request和response为参数的方法,如何让固定的servlet处理方法调用灵活的Handler来进行处理呢?这就是HandlerAdapter要做的事情。
Handler是用来干活的工具,HandlerMapper用于根据需要干的活找到相应的工具,HandlerAdapter是使用用工具干活的人。
3)HandlerExceptionResolver
initHandlerExceptionResolver(context),作用是根据异常设置ModelAndView,之后再交给render方法进行渲染。
4)ViewResolver
initViewResolvers(context),ViewResolver用来将string类型的视图名和locale解析为view类型的视图,view是用来渲染页面的,也就是将程序返回的参数填入模版中,生成html(也可能是其他类型文件),这里就有两个关键问题,使用哪个模版?用什么规则填入参数?这其实是ViewResolver主要做的工作,ViewResolver需要找到渲染所用的模版和所用的技术(也就是视图的类型)进行渲染,具体的渲染过程规则交由不同的视图自己完成。
5)RequestToViewNameTranslator
initRequestToViewNameTranslator(context),ViewResolver是根据ViewName查找view,但有的Handle处理完后并没有设置view也没有设置ViewName,这时就需要从Request获取ViewName了,如何从Request中获取ViewName就是RequestToViewNameTranslator要做的事了。
RequestToViewNameTranslator在spring mvc容器里只可以配置一个,所以所有request到viewName的转换规则都要在一个Translator里全部实现。
6)LocaleResolver
initLocaleResolver(context),解析视图需要两个参数,一是视图名,二是locale,视图名是处理器返回到,locale是从哪里带的?localeResolver用于从Request解析出locale,locale就是zh-cn之类,表示一个区域,有了这个就可以对不同区域的用户显示不同的结果,Spring mvc主要有两个地方用到了locale,一是viewReslover视图解析到时候,而是国际化资源或者主题的时候。
7)ThemeResolver
initThemeResolver(context),用于解析主题,spring mvc中一个主题对应一个properties文件,里面存放着根当前主题相关的所有资源,如图片,css等,spring mvc的主题也是支持国际化的,同一个主题不同的区域也可以显示不同的风格,spring mvc中跟主题相关的类有ThremeResolver,ThremeSource,Threme,主要是通过一系列资源来具体体现的,要得到一个主题的资源,首先要得到资源的名称,这是ThemeResolver的工作,然后通过主题名称找到对应的主题(可以理解为一个配置文件),这是ThemeSource的工作,最后从主题中获取资源就可以了。
8)MultipartResolver
initMultipartResolver(context),用于处理上传请求,处理方法是将普通的request包装成MultipartHttpServletRequest,后者可以直接调用getFile方法获取file,如果上传多个文件,还可以调用getFileMap得到fileName→file结构的map,此组件一共有三个方法,作用分别是判断是不是上传请求,将request包装成MultipartHttpServletRequest,处理完后清理上传过程中产生的临时资源。
9)FlashMapManager
initFlashMapManager(context),用来管理flashMap的,flashMap主要用在redirect中传递参数。
**由于文章超过最大字数限制,其他后续内容,可添加微信,单独发你 **