面试
1.jdk、jre、jvm的区别?
jdk是开发工具,jdk包含jre,jdk有一个名为下jre的目录里面包含bin和lib,bin是jvm,lib是jvm所需要的类库, jre是Java运行时环境;jvm是Java虚拟机。
2.==和equals⽅法的区别?
对于基本数据类型==比较的是值,引用数据类型比较的是地址值;equal不能用于基本数据类型的比较,如果没有重写equal方法,equal相当于==,如果重写了equal方法,比较的是对象的内容。
(1)修饰
基本数据类型,则该引用为常量,值无法修改;
引用数据类型,比如数组,对象,则该数组,对象本身可以修改,但该数组指向的地址的引用不可以修改;
如果是类的成员变量的就必须当场赋值,否则编译会报错
如果final修饰的是类变量,只能在静态初始化块中指定初始值或者声明该类变量时指定初始值。
如果final修饰的是成员变量,可以在⾮静态初始化块、声明该变量或者构造器中执⾏初始值。
(2)修饰方法
表示该方法为最终方法,不可以被子类覆盖,但是可以重载。
(3)修饰类
表示该类为最终类,无法继承,比如string类就是最终类。
4.String、StringBuffer、StringBuilder的区别?
(1)string是final修饰的是不可变的,如果尝试修改的话,会产生一个新的字符串对象,stringbuffer和stringbuilder是可变的。
(2)stringbufffer是线程安全的,stringbuilder是线程不安全的,所有在单线程环境下stringbufffer的效率会更高。
5.重载和重写的区别?
- 重载:发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。
- 重写:发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出异常范围小于等于父类,访问修饰符大于等于父类;如果父类方法的访问修饰符为private则子类不可以重写该方法。
6.接⼝和抽象类的区别?
(1)接口
接口使用interface修饰,接口不可以实例化,类可以实现多个接口,接口中的方法都是抽象方法省略了public abstract;
(2)抽象类
抽象类使用abstract修饰,抽象类不能被实例化,抽象类只能单继承,抽象类中可以包含抽象方法和非抽象方法,非抽象方法需要有方法体。
如果一个类继承了抽象类,如果子类实现了所有的抽象方法,子类可以不是抽象类;如果子类没有实现所有的抽象方法,子类仍然是抽象类。
接⼝的设计⽬的,是对类的⾏为进⾏约束(更准确的说是⼀种“有”约束,因为接⼝不能规定类不可以有 什么⾏为),也就是提供⼀种机制,可以强制要求不同的类具有相同的⾏为。它只约束了⾏为的有⽆, 但不对如何实现⾏为进⾏限制
⽽抽象类的设计⽬的,是代码复⽤。当不同的类具有某些相同的⾏为(记为⾏为集合A),且其中⼀部分⾏ 为的实现⽅式⼀致时(A的⾮真⼦集,记为B),可以让这些类都派⽣于⼀个抽象类。在这个抽象类中实 现了B,避免让所有的⼦类来实现B,这就达到了代码复⽤的⽬的。⽽A减B的部分,留给各个⼦类⾃⼰ 实现。正是因为A-B在这⾥没有实现,所以抽象类不允许实例化出来(否则当调⽤到A-B时,⽆法执 ⾏)。
使⽤场景:当你关注⼀个事物的本质的时候,⽤抽象类;当你关注⼀个操作的时候,⽤接⼝。
7.集合的体系?
Java集合分为两种体系:Collection和Map
Collection:
List:
ArrayList:数组,10,1.5倍扩容,扩容因子1
LinkedList:双向链表
Vector:数组,默认长度为10,1,两倍扩容
Set:
HashSet:数组+链表结构,
--LinkedHashSet:
TreeSet:
Map:
HashMap:16,2倍,阈值0.75
--LinkedHashMap:
TreeMap
Hashtable:数组+链表,默认长度为11,扩容为原容量的2倍+1,阈值四分之三
8.List和Set的区别?
- (1)list
- list是有序的,按对象进入的顺序保存对象,可重复,允许多个null元素对象,可以使用iterator取出所有的元素,在逐一遍历时,还可以使用get(int index)获取指定下标的元素
- (2)set
- 无序,不可重复,最大允许一个null元素对象,取元素时只能用iterator接口取出所有元素,在逐一遍历各个元素。
9.ArrayList和LinkedList区别?
- ArrayList
- ①基于数组,需要连续内存, 线程不安全,查询速度快
- ②随机访问快(根据下标进行访问)实现了RandomAccess
- ③尾部插入、删除性能可以,其他部分插入、删除都会移动数据,因此性能会低
- ④可以利用cpu缓存,局部性原理(把相邻的数据也读取到cpu缓存中,提高读写效率)
- LinkedList
- ①基于双向链表
- ②随机访问慢(需要沿着链表遍历)
- ③头尾删除插入性能高
- ④占用内存多
ArrayList和LinkedList都实现了List接⼝,但是LinkedList还额外实现了Deque接⼝,所以 LinkedList还可以当做队列来使⽤
10.ArrayList的扩容机制?
add()方法:ArrayList的无参构造初始容量是0,添加数据时会触发第一次扩容,容量为10;当容量满了的时候会再次扩容,扩容为原来的1.5倍,容量为奇数时,采用的是移位计算(15>>1=7 ,15+7=22)。
addAll()方法:第一次扩容为10,当下一次扩容后容量仍不够存放数据,则会在下一次扩容容量和数据个数之间选择最大值为写一次容量。
ArrayList 和Vector,底层都是Object数组,默认加载因子都是1(元素满了才扩展容量).默认容量都是10;但是ArrayList 在jdk1.8时默认为空,当添加元素时,才初始化为10个容量。
ArrayList:新容量为原容量的1.5倍,
Vector:新容量为原容量的2倍.
- ArrayList()**会使用长度为零的数组
- ArrayList(int initalCapacity)**会使用指定容量的数组
- public ArrayList(Collection<?extends E> c)**会使用c的大小作为数组容量
- add(Object o)首次扩容为10,再次扩容为上一次的1.5倍>
- addAll(Collection c)扩容为Math.Max(10,实际元素个数),有元素时扩容为Math.Max(原容量1.5倍,实际元素个数)
11.HashMap底层数据结构,1.7和1.8有何不同?为何要用红黑树,为何一上来不树化,树化阈值为何是8,何时会树化,何时会退化为链表?
1.7数组+链表 1.8 数组+链表+红黑树
- ①红黑树用来避免DoS攻击,防止链表超长时性能下降;树化应当是偶然情况,hash表的查找,更新的时间复杂度是0(1),而红黑树的查找,更新的时间复杂度是0(log2 n),TreeNode占用空间也比普通Node的大,如非必要,尽量还是使用链表。
- ②hash值如果足够随机,则在 hash表内按泊松分布,在负载因子0.75的情况下,长度超过8的链表出现概率是0.00000006,选择8就是为了让树化几率足够小,树化两个条件:链表长度超过树化阈值8;数组容量>=64
- ③退化情况1:在扩容时如果拆分树时,树元素个数<=6则会退化链表,退化情况2: remove树节点时,若root、root.left、root.right、root.left.left有一个为null,也会退化为链表
通过hashcode()方法得到原始的hash值,再使用map里面的hash()方法计算二次hash值,把二次hash值跟数组容量求模得到的余数为桶下标,也可以使用按位与计算97&(64-1),要求容量为2的次方,可以实现快速查找(时间复杂的度O(n))。超过容量的四分之三会发生扩容。树化需要满足两个条件,当链表数量大于树化阈值8,数组容量大于64时才会树化,容量不够时会先扩容减少链表长度(0(log2N))。扩容为原来的两倍。
12.HashMap和HashTable有什么区别?其底层实现是什么?
区别 :
- HashMap⽅法没有synchronized修饰,线程⾮安全,HashTable线程安全;
- HashMap允许key和value为null,⽽HashTable不允许 底层实现:数组+链表实现,jdk8开始链表⾼度到8、数组⻓度超过64,链表转变为红⿊树,元素以内部 类Node节点存在
- 计算key的hash值,⼆次hash然后对数组⻓度取模,对应到数组下标,
- 如果没有产⽣hash冲突(下标位置没有元素),则直接创建Node存⼊数组,
- 如果产⽣hash冲突,先进⾏equal⽐较,相同则取代该元素,不同,则判断链表⾼度插⼊链表,链 表⾼度达到8,并且数组⻓度到64则转变为红⿊树,⻓度低于6则将红⿊树转回链表
- key为null,存在下标0的位置。
13.
- ①HashMap是懒惰创建数组的,首次使用才创建数组
- ②计算索引(桶下标)
- ③如果桶下标还没人占用,创建Node占位返回
- ④如果桶下标已经有人占用
①已经是TreeNode走红黑树的添加或更新逻辑
②是普通Node,走链表的添加或更新逻辑,如果链表长度超过树化阈值,走树化逻辑
⑤返回前检查容量是否超过阈值,一旦超过进行扩容
⑥不同
①链表插入节点时,1.7是头插法,1.8是尾插法
②1.7是大于等于阈值且没有空位时才扩容,而1.8是大于阈值就扩容
③31.8在扩容计算Node索引时,会优化加载因子为何默认是0.75f①在空间占用与查询时间之间取得较好的权衡②大于这个值,空间节省了,但链表就会比较长影响性能③小于这个值,冲突减少了,但扩容就会更频繁,空间占用多
先说HashMap的Put⽅法的⼤体流程:
1. 根据Key通过哈希算法与与运算得出数组下标
2. 如果数组下标位置元素为空,则将key和value封装为Entry对象(JDK1.7中是Entry对象,JDK1.8中 是Node对象)并放⼊该位置
3. 如果数组下标位置元素不为空,则要分情况讨论
a. 如果是JDK1.7,则先判断是否需要扩容,如果要扩容就进⾏扩容,如果不⽤扩容就⽣成Entry 对象,并使⽤头插法添加到当前位置的链表中
b. 如果是JDK1.8,则会先判断当前位置上的Node的类型,看是红⿊树Node,还是链表Node ⅰ. 如果是红⿊树Node,则将key和value封装为⼀个红⿊树节点并添加到红⿊树中去,在这个 过程中会判断红⿊树中是否存在当前key,如果存在则更新value
ⅱ. 如果此位置上的Node对象是链表节点,则将key和value封装为⼀个链表Node并通过尾插 法插⼊到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历链表的过程中会 判断是否存在当前key,如果存在则更新value,当遍历完链表后,将新链表Node插⼊到链 表中,插⼊到链表后,会看当前链表的节点个数,如果⼤于等于8,那么则会将该链表转成 红⿊树
ⅲ. 将key和value封装为Node插⼊到链表或红⿊树中后,再判断是否需要进⾏扩容,如果需要 就扩容,如果不需要就结束PUT⽅法
15.
1. Hashtable 与ConcurrentHashMap都是线程安全的Map集合
2. Hashtable并发度低,整个Hashtable对应一把锁,同一时刻,只能有一个线程操作它
3. 1.8之前ConcurrentHashMap使用了Segment+数组+链表的结构,每个Segment对应一把锁,如果多个线程访问不同的Segment,则不会冲突
4. 1.8开始ConcurrentHashMap将数组的每个头节点作为锁,如果多个线程访问的头节点不同,则不会冲突
Hashtable,数组+链表,初始容量11,扩容为原容量的2倍+1,阈值四分之三ConcurrentHashMap:容量16,扩容为原来的两倍,扩容因子0.75,1.7超过扩容,1.8满0.75扩容
16.谈谈ConcurrentHashMap的扩容机制
1.7版本
1. 1.7版本的ConcurrentHashMap是基于Segment分段实现的
2. 每个Segment相对于⼀个⼩型的HashMap
3. 每个Segment内部会进⾏扩容,和HashMap的扩容逻辑类似
4. 先⽣成新的数组,然后转移元素到新数组中
5. 扩容的判断也是每个Segment内部单独判断的,判断是否超过阈值
1.8版本
1. 1.8版本的ConcurrentHashMap不再基于Segment实现
2. 当某个线程进⾏put时,如果发现ConcurrentHashMap正在进⾏扩容那么该线程⼀起进⾏扩容
3. 如果某个线程put时,发现没有正在进⾏扩容,则将key-value添加到ConcurrentHashMap中,然 后判断是否超过阈值,超过了则进⾏扩容
4. ConcurrentHashMap是⽀持多个线程同时扩容的
5. 扩容之前也先⽣成⼀个新的数组
6. 在转移元素时,先将原数组分组,将每组分给不同的线程来进⾏元素的转移,每个线程负责⼀组或 多组的元素转移⼯作
17.HashMap的扩容机制原理
1.7版本 1. 先⽣成新数组
2. 遍历⽼数组中的每个位置上的链表上的每个元素
3. 取每个元素的key,并基于新数组⻓度,计算出每个元素在新数组中的下标
4. 将元素添加到新数组中去
5. 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性
1.8版本
1. 先⽣成新数组
2. 遍历⽼数组中的每个位置上的链表或红⿊树
3. 如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去
4. 如果是红⿊树,则先遍历红⿊树,先计算出红⿊树中每个元素对应在新数组中的下标位置 a. 统计每个下标位置的元素个数
b. 如果该位置下的元素个数超过了8,则⽣成⼀个新的红⿊树,并将根节点的添加到新数组的对应 位置
c. 如果该位置下的元素个数没有超过8,那么则⽣成⼀个链表,并将链表的头节点添加到新数组的 对应位置
5. 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性
17.泛型中extends和super的区别?
- <? extends T>表示包括T在内的任何T的⼦类
- <? super T>表示包括T在内的任何T的⽗类
18.深拷⻉和浅拷⻉?
深拷⻉和浅拷⻉就是指对象的拷⻉,⼀个对象中存在两种类型的属性,⼀种是基本数据类型,⼀种是实 例对象的引⽤。
- 浅拷⻉是指,只会拷⻉基本数据类型的值,以及实例对象的引⽤地址,并不会复制⼀份引⽤地址所 指向的对象,也就是浅拷⻉出来的对象,内部的类属性指向的是同⼀个对象
- 深拷⻉是指,既会拷⻉基本数据类型的值,也会针对实例对象的引⽤地址所指向的对象进⾏复制, 深拷⻉出来的对象,内部的属性指向的不是同⼀个对象
19.
①扩容死链(1.7)
②数据错乱(1.7/1.8)
21.Java中的异常体系是怎样的?
Java中的所有异常都来⾃顶级⽗类Throwable。
Throwable下有两个⼦类Exception和Error。
Error是程序⽆法处理的错误,⼀旦出现这个错误,则程序将被迫停⽌运⾏。
Exception不会导致程序停⽌,⼜分为两个部分RunTimeException运⾏时异常和 CheckedException检查异常。
RunTimeException常常发⽣在程序运⾏过程中,会导致程序当前线程执⾏失败。 CheckedException常常发⽣在程序编译过程中,会导致程序编译不通过。
22.Java中有哪些类加载器?
JDK⾃带有三个类加载器:bootstrap ClassLoader、ExtClassLoader、AppClassLoader。
BootStrapClassLoader是ExtClassLoader的⽗类加载器,默认负责加载%JAVA_HOME%lib下的 jar包和class⽂件。
ExtClassLoader是AppClassLoader的⽗类加载器,负责加载%JAVA_HOME%/lib/ext⽂件夹下的 jar包和class类。
AppClassLoader是⾃定义类加载器的⽗类,负责加载classpath下的类⽂件。
23.说说类加载器双亲委派模型?
JVM中存在三个默认的类加载器: 1. BootstrapClassLoader 2. ExtClassLoader 3. AppClassLoader AppClassLoader的⽗加载器是ExtClassLoader,ExtClassLoader的⽗加载器是 BootstrapClassLoader。
JVM在加载⼀个类时,会调⽤AppClassLoader的loadClass⽅法来加载这个类,不过在这个⽅法中,会 先使⽤ExtClassLoader的loadClass⽅法来加载类,同样ExtClassLoader的loadClass⽅法中会先使⽤ BootstrapClassLoader来加载类,如果BootstrapClassLoader加载到了就直接成功,如果 BootstrapClassLoader没有加载到,那么ExtClassLoader就会⾃⼰尝试加载该类,如果没有加载到, 那么则会由AppClassLoader来加载这个类。 所以,双亲委派指得是,JVM在加载类时,会委派给Ext和Bootstrap进⾏加载,如果没加载到才由⾃⼰ 进⾏加载。
24.什么是字节码?采⽤字节码的好处是什么?
Java中的编译器和解释器:Java中引⼊了虚拟机的概念,即在机器和编译程序之间加⼊了⼀层抽象的虚 拟的机器。这台虚拟的机器在任何平台上都提供给编译程序⼀个的共同的接⼝。编译程序只需要⾯向虚 拟机,⽣成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执⾏。在 Java中,这种供虚拟机理解的代码叫做 字节码(即扩展名为 .class的⽂件),它不⾯向任何特定的处理 器,只⾯向虚拟机。 每⼀种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节 码,字节码由虚拟机解释执⾏,虚拟机将每⼀条要执⾏的字节码送给解释器,解释器将其翻译成特定机 器上的机器码,然后在特定的机器上运⾏。这也就是解释了Java的编译与解释并存的特点。
Java源代码---->编译器---->jvm可执⾏的Java字节码(即虚拟指令)---->jvm---->jvm中解释器-- --->机器可执⾏的⼆进制机器码---->程序运⾏。
采⽤字节码的好处:Java语⾔通过字节码的⽅式,在⼀定程度上解决了传统解释型语⾔执⾏效率低的问 题,同时⼜保留了解释型语⾔可移植的特点。所以Java程序运⾏时⽐较⾼效,⽽且,由于字节码并不专 对⼀种特定的机器,因此,Java程序⽆须重新编译便可在多种不同的计算机上运⾏。
25.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⽅法。
执⾏finalize⽅法完毕后,GC会再次判断该对象是否可达,若不可达,则进⾏回收, 否则,对象“复活” 每个对象只能触发⼀次finalize()⽅法 由于finalize()⽅法运⾏代价⾼昂,不确定性⼤,⽆法保证各个对象的调⽤顺序,不推荐⼤家使⽤,建议 遗忘它。
26.JVM中哪些是线程共享区?
堆区和⽅法区是所有线程共享的,栈、本地⽅法栈、程序计数器是每个线程独有的
27.⼀个对象从加载到JVM,再到被GC清除,都经历了什么过程?
1. ⽤户创建⼀个对象,JVM⾸先需要到⽅法区去找对象的类型信息。然后再创建对象。
2. JVM要实例化⼀个对象,⾸先要在堆当中先创建⼀个对象。-> 半初始化状态
3. 对象⾸先会分配在堆内存中新⽣代的Eden。然后经过⼀次Minor GC,对象如果存活,就会进⼊S 区。在后续的每次GC中,如果对象⼀直存活,就会在S区来回拷⻉,每移动⼀次,年龄加1。-> 多 ⼤年龄才会移⼊⽼年代? 年龄最⼤15, 超过⼀定年龄后,对象转⼊⽼年代。
4. 当⽅法执⾏结束后,栈中的指针会先移除掉。
5. 堆中的对象,经过Full GC,就会被标记为垃圾,然后被GC线程清理掉。
28.怎么确定⼀个对象到底是不是垃圾?
1. 引⽤计数: 这种⽅式是给堆内存当中的每个对象记录⼀个引⽤个数。引⽤个数为0的就认为是垃 圾。这是早期JDK中使⽤的⽅式。引⽤计数⽆法解决循环引⽤的问题。
2. 根可达算法: 这种⽅式是在内存中,从引⽤根对象向下⼀直找引⽤,找不到的对象就是垃圾。
29.JVM有哪些垃圾回收算法?
1. MarkSweep 标记清除算法:这个算法分为两个阶段,标记阶段:把垃圾内存标记出来,清除阶 段:直接将垃圾内存回收。这种算法是⽐较简单的,但是有个很严重的问题,就是会产⽣⼤量的内 存碎⽚。 2. Copying 拷⻉算法:为了解决标记清除算法的内存碎⽚问题,就产⽣了拷⻉算法。拷⻉算法将内存 分为⼤⼩相等的两半,每次只使⽤其中⼀半。垃圾回收时,将当前这⼀块的存活对象全部拷⻉到另 ⼀半,然后当前这⼀半内存就可以直接清除。这种算法没有内存碎⽚,但是他的问题就在于浪费空 间。⽽且,他的效率跟存货对象的个数有关。
3. MarkCompack 标记整理算法:为了解决拷⻉算法的缺陷,就提出了标记压缩算法。这种算法在标 记阶段跟标记清除算法是⼀样的,但是在完成标记之后,不是直接清理垃圾内存,⽽是将存活对象 往⼀端移动,然后将端边界以外的所有内存直接清除。
30.什么是STW?
STW: Stop-The-World,是在垃圾回收算法执⾏过程当中,需要将JVM内存冻结的⼀种状态。在STW 状态下,JAVA的所有线程都是停⽌执⾏的-GC线程除外,native⽅法可以执⾏,但是,不能与JVM交 互。GC各种算法优化的重点,就是减少STW,同时这也是JVM调优的重点。
31.JVM有哪些垃圾回收器?
新⽣代收集器: Serial 、ParNew 、Parallel Scavenge
⽼年代收集器: CMS、 Serial Old、 Parallel Old
整堆收集器: G1
32.垃圾回收分为哪些阶段?
- 第⼀:初始标记 标记出GCRoot直接引⽤的对象。STW
- 第⼆:标记Region,通过RSet标记出上⼀个阶段标记的Region引⽤到的Old区Region。
- 第三:并发标记阶段:跟CMS的步骤是差不多的。只是遍历的范围不再是整个Old区,⽽只需要遍 历第⼆步标记出来的Region。 第
- 四:重新标记: 跟CMS中的重新标记过程是差不多的。
- 第五:垃圾清理:与CMS不同的是,G1可以采⽤拷⻉算法,直接将整个Region中的对象拷⻉到另 ⼀个Region。⽽这个阶段,G1只选择垃圾较多的Region来清理,并不是完全清理。
33.什么是三⾊标记?
三⾊标记:是⼀种逻辑上的抽象。将每个内存对象分成三种颜⾊: 1. ⿊⾊:表示⾃⼰和成员变量都已经标记完毕。 2. 灰⾊:⾃⼰标记完了,但是成员变量还没有完全标记完。 3. ⽩⾊:⾃⼰未标记完。
34.JVM参数有哪些?
JVM参数⼤致可以分为三类: 1. 标注指令: -开头,这些是所有的HotSpot都⽀持的参数。可以⽤java -help 打印出来。
2. ⾮标准指令: -X开头,这些指令通常是跟特定的HotSpot版本对应的。可以⽤java -X 打印出来。
3. 不稳定参数: -XX 开头,这⼀类参数是跟特定HotSpot版本对应的,并且变化⾮常⼤。详细的⽂档 资料⾮常少。
在JDK1.8版本下,有⼏个常⽤的不稳定指令: java -XX:+PrintCommandLineFlags : 查看当前命令的不稳定指令。 java -XX:+PrintFlagsInitial : 查看所有不稳定指令的默认值。 java -XX:+PrintFlagsFinal: 查看所有不稳定指令最终⽣效的实际值。
Java并发(20)
35.线程的⽣命周期?线程有⼏种状态?
线程通常有五种状态,创建,就绪,运⾏、阻塞和死亡状态:
1. 新建状态(New):新创建了⼀个线程对象。
2. 就绪状态(Runnable):线程对象创建后,其他线程调⽤了该对象的start⽅法。该状态的线程位于 可运⾏线程池中,变得可运⾏,等待获取CPU的使⽤权。
3. 运⾏状态(Running):就绪状态的线程获取了CPU,执⾏程序代码。
4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使⽤权,暂时停⽌运⾏。直到线程 进⼊就绪状态,才有机会转到运⾏状态。
5. 死亡状态(Dead):线程执⾏完了或者因异常退出了run⽅法,该线程结束⽣命周期。
阻塞的情况⼜分为三种:
1. 等待阻塞:运⾏的线程执⾏wait⽅法,该线程会释放占⽤的所有资源,JVM会把该线程放⼊“等待 池”中。进⼊这个状态后,是不能⾃动唤醒的,必须依靠其他线程调⽤notify或notifyAll⽅法才能被 唤醒,wait是object类的⽅法
2. 同步阻塞:运⾏的线程在获取对象的同步锁时,若该同步锁被别的线程占⽤,则JVM会把该线程放 ⼊“锁池”中。
3. 其他阻塞:运⾏的线程执⾏sleep或join⽅法,或者发出了I/O请求时,JVM会把该线程置为阻塞状 态。当sleep状态超时、join等待线程终⽌或者超时、或者I/O处理完毕时,线程重新转⼊就绪状 态。sleep是Thread类的⽅法
36.sleep()、wait()、join()、yield()之间的的区别?
锁池:所有需要竞争同步锁的线程都会放在锁池当中,⽐如当前对象的锁已经被其中⼀个线程得到,则 其他线程需要在这个锁池进⾏等待,当前⾯的线程释放同步锁后锁池中的线程去竞争同步锁,当某个线 程得到后会进⼊就绪队列进⾏等待cpu资源分配。
等待池:当我们调⽤wait()⽅法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁。只 有调⽤了notify()或notifyAll()后等待池的线程才会开始去竞争锁,notify()是随机从等待池选出⼀ 个线程放到锁池,⽽notifyAll()是将等待池的所有线程放到锁池当中
1. sleep 是 Thread 类的静态本地⽅法,wait 则是 Object 类的本地⽅法。
2. sleep⽅法不会释放lock,但是wait会释放,⽽且会加⼊到等待队列中
3. sleep⽅法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。
4. sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别⼈中断)。
5. sleep ⼀般⽤于当前线程休眠,或者轮循暂停操作,wait 则多⽤于多线程之间的通信。
6. sleep 会让出 CPU 执⾏时间且强制上下⽂切换,⽽ wait 则不⼀定,wait 后可能还是有机会重新竞 争到锁继续执⾏的。 sleep()、wait()、join()、yield()之间的的区别 Plain Text 复制代码 sleep就是把cpu的执⾏资格和执⾏权释放出去,不再运⾏此线程,当定时时间结束再取回cpu资源, 参与cpu的调度,获取到cpu资源后就可以继续运⾏了。⽽如果sleep时该线程有锁,那么sleep不会 释放这个锁,⽽是把锁带着进⼊了冻结状态,也就是说其他需要这个锁的线程根本不可能获取到这个 锁。也就是说⽆法执⾏程序。如果在睡眠期间其他线程调⽤了这个线程的interrupt⽅法,那么这个 线程也会抛出interruptexception异常返回,这点和wait是⼀样的。
7. yield()执⾏后线程直接进⼊就绪状态,⻢上释放了cpu的执⾏权,但是依然保留了cpu的执⾏资 格,所以有可能cpu下次进⾏线程调度还会让这个线程获取到执⾏权继续执⾏
8. join()执⾏后线程进⼊阻塞状态,例如在线程B中调⽤线程A的join(),那线程B会进⼊到阻塞队 列,直到线程A结束或中断线程
37.对线程安全的理解?
不是线程安全、应该是内存安全,堆是共享内存,可以被所有线程访问,当多个线程访问⼀个对象时, 如果不⽤进⾏额外的同步控制或其他的协调操作,调⽤这个对象的⾏为都可以获得正确的结果,我们就 说这个对象是线程安全的。
堆是进程和线程共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是⽤户分 配的空间。堆在操作系统对进程初始化的时候分配,运⾏过程中也可以向系统要额外的堆,但是⽤完了 要还给操作系统,要不然就是内存泄漏。在Java中,堆是Java虚拟机所管理的内存中最⼤的⼀块,是所 有线程共享的⼀块内存区域,在虚拟机启动时创建。堆所存在的内存区域的唯⼀⽬的就是存放对象实 例,⼏乎所有的对象实例以及数组都在这⾥分配内存。
栈是每个线程独有的,保存其运⾏状态和局部⾃动变量的。栈在线程开始的时候初始化,每个线程的栈 互相独⽴,因此,栈是线程安全的。操作系统在切换线程的时候会⾃动切换栈。栈空间不需要在⾼级语 ⾔⾥⾯显式的分配和释放。
⽬前主流操作系统都是多任务的,即多个进程同时运⾏。为了保证安全,每个进程只能访问分配给⾃⼰ 的内存空间,⽽不能访问别的进程的,这是由操作系统保障的。
在每个进程的内存空间中都会有⼀块特殊的公共区域,通常称为堆(内存)。进程内的所有线程都可以 访问到该区域,这就是造成问题的潜在原因
38.Thread和Runable的区别?
Thread和Runnable的实质是继承关系,没有可⽐性。⽆论使⽤Runnable还是Thread,都会new Thread,然后执⾏run⽅法。⽤法上,如果有复杂的线程操作需求,那就选择继承Thread,如果只是简 单的执⾏⼀个任务,那就实现runnable。
39.对守护线程的理解?
守护线程:为所有⾮守护线程提供服务的线程;任何⼀个守护线程都是整个JVM中所有⾮守护线程的保 姆;
40.ThreadLocal的底层原理?
1. ThreadLocal是Java中所提供的线程本地存储机制,可以利⽤该机制将数据缓存在某个线程内部, 该线程可以在任意时刻、任意⽅法中获取缓存的数据
2. ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对 象)中都存在⼀个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的 值
3. 如果在线程池中使⽤ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使⽤完之后,应该要把 设置的key,value,也就是Entry对象进⾏回收,但线程池中的线程不会回收,⽽线程对象是通过强 引⽤指向ThreadLocalMap,ThreadLocalMap也是通过强引⽤指向Entry对象,线程不被回收, Entry对象也就不会被回收,从⽽出现内存泄漏,解决办法是,在使⽤了ThreadLocal对象之后,⼿ 动调⽤ThreadLocal的remove⽅法,⼿动清楚Entry对象
4. ThreadLocal经典的应⽤场景就是连接管理(⼀个线程持有⼀个连接,该连接对象可以在不同的⽅ 法之间进⾏传递,线程之间不共享同⼀个连接)
41.并发、并⾏、串⾏之间的区别?
1. 串⾏在时间上不可能发⽣重叠,前⼀个任务没搞定,下⼀个任务就只能等着
2. 并⾏在时间上是重叠的,两个任务在同⼀时刻互不⼲扰的同时执⾏。
3. 并发允许两个任务彼此⼲扰。统⼀时间点、只有⼀个任务运⾏,交替执⾏
42.并发的三⼤特性?
原子性:原⼦性是指在⼀个操作中cpu不可以在中途暂停然后再调度,即不被中断操作,要不全部执⾏完成,要 不都不执⾏。就好⽐转账,从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元, 往账户B加上1000元。2个操作必须全部完成。
有序性:虚拟机在进⾏代码编译时,对于那些改变顺序之后不会对最终结果造成影响的代码,虚拟机不⼀定会按 照我们写的代码的顺序来执⾏,有可能将他们重排序。实际上,对于有些代码进⾏重排序之后,虽然对 变量的值没有造成影响,但有可能会出现线程安全问题。
可见性:当多个线程访问同⼀个变量时,⼀个线程修改了这个变量的值,其他线程能够⽴即看得到修改的值。
43.Java死锁如何避免?
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。
造成死锁的⼏个原因:
1.互斥条件: ⼀个资源每次只能被⼀个线程使⽤
2.请求和保持条件: ⼀个线程在阻塞等待某个资源时,不释放已占有资源
3.不剥夺条件: ⼀个线程已经获得的资源,在未使⽤完之前,不能被强⾏剥夺
4. 循环等待条件:若⼲线程形成头尾相接的循环等待资源关系
这是造成死锁必须要达到的4个条件,如果要避免死锁,只需要不满⾜其中某⼀个条件即可。⽽其中前3 个条件是作为锁要符合的条件,所以要避免死锁就需要打破第4个条件,不出现循环等待锁的关系。
在开发过程中: 1. 要注意加锁顺序,保证每个线程按同样的顺序进⾏加锁 2. 要注意加锁时限,可以针对所设置⼀个超时时间 3. 要注意死锁检查,这是⼀种预防机制,确保在第⼀时间发现死锁并进⾏解决
44.如何理解volatile关键字?
保证被volatile修饰的共享变量对所有线程总是可⻅的,也就是当⼀个线程修改了⼀个被volatile修饰共 享变量的值,新值总是可以被其他线程⽴即得知。inc++; 其实是两个步骤,先加加,然后再赋值。不是原⼦性操作,所以volatile不能保证线程安全。
45.为什么⽤线程池?解释下线程池参数?
1、降低资源消耗;提⾼线程利⽤率,降低创建和销毁线程的消耗。
2、提⾼响应速度;任务来了,直接有线程可⽤可执⾏,⽽不是先创建线程,再执⾏。
3、提⾼线程的可管理性;线程是稀缺资源,使⽤线程池可以统⼀分配调优监控。
corePoolSize 代表核⼼线程数,也就是正常情况下创建⼯作的线程数,这些线程创建后并不 会消除,⽽是⼀种常驻线程
maxinumPoolSize 代表的是最⼤线程数,它与核⼼线程数相对应,表示最⼤允许被创建的线程 数,⽐如当前任务较多,将核⼼线程数都⽤完了,还⽆法满⾜需求时,此时就会创建新的线程,但 是线程池内线程总数不会超过最⼤线程数
keepAliveTime 、 unit 表示超出核⼼线程数之外的线程的空闲存活时间,也就是核⼼线程 不会消除,但是超出核⼼线程数的部分线程如果空闲⼀定的时间则会被消除,我们可以通过
setKeepAliveTime 来设置空闲时间
workQueue ⽤来存放待执⾏的任务,假设我们现在核⼼线程都已被使⽤,还有任务进来则全部 放⼊队列,直到整个队列被放满但任务还再持续进⼊则会开始创建新的线程
ThreadFactory 实际上是⼀个线程⼯⼚,⽤来⽣产线程执⾏任务。我们可以选择使⽤默认的创 建⼯⼚,产⽣的线程都在同⼀个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以选 择⾃定义线程⼯⼚,⼀般我们会根据业务来制定不同的线程⼯⼚
Handler 任务拒绝策略,有两种情况,第⼀种是当我们调⽤ shutdown 等⽅法关闭线程池 后,这时候即使线程池内部还有没执⾏完的任务正在执⾏,但是由于线程池已经关闭,我们再继续 想线程池提交任务就会遭到拒绝。另⼀种情况就是当达到最⼤线程数,线程池已经没有能⼒继续处 理新提交的任务时,这是也就拒绝
46.线程池的底层⼯作原理
线程池内部是通过队列+线程实现的,当我们利⽤线程池执⾏任务时:
1. 如果此时线程池中的线程数量⼩于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建 新的线程来处理被添加的任务。
2. 如果此时线程池中的线程数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放⼊ 缓冲队列。
3. 如果此时线程池中的线程数量⼤于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数 量⼩于maximumPoolSize,建新的线程来处理被添加的任务。
4. 如果此时线程池中的线程数量⼤于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等 于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
5. 当线程池中的线程数量⼤于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被 终⽌。这样,线程池可以动态的调整池中的线程数
47.线程池中阻塞队列的作⽤?为什么是先添加列队⽽不是先创建最 ⼤线程?
1、⼀般的队列只能保证作为⼀个有限⻓度的缓冲区,如果超出了缓冲⻓度,就⽆法保留当前的任务 了,阻塞队列通过阻塞可以保留住当前想要继续⼊队的任务。 阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进⼊wait状态,释放cpu资 源。
阻塞队列⾃带阻塞和唤醒的功能,不需要额外处理,⽆任务执⾏时,线程池利⽤阻塞队列的take⽅法挂 起,从⽽维持核⼼线程的存活、不⾄于⼀直占⽤cpu资源
2、在创建新线程的时候,是要获取全局锁的,这个时候其它的就得阻塞,影响了整体效率。
就好⽐⼀个企业⾥⾯有10个(core)正式⼯的名额,最多招10个正式⼯,要是任务超过正式⼯⼈数 (task > core)的情况下,⼯⼚领导(线程池)不是⾸先扩招⼯⼈,还是这10⼈,但是任务可以稍微积 压⼀下,即先放到队列去(代价低)。10个正式⼯慢慢⼲,迟早会⼲完的,要是任务还在继续增加,超 过正式⼯的加班忍耐极限了(队列满了),就的招外包帮忙了(注意是临时⼯)要是正式⼯加上外包还 是不能完成任务,那新来的任务就会被领导拒绝了(线程池的拒绝策略)。
48.线程池中线程复⽤原理
线程池将线程和任务进⾏解耦,线程是线程,任务是任务,摆脱了之前通过 Thread 创建线程时的⼀个 线程必须对应⼀个任务的限制。 在线程池中,同⼀个线程可以从阻塞队列中不断获取新任务来执⾏,其核⼼原理在于线程池对 Thread 进⾏了封装,并不是每次执⾏任务都会调⽤ Thread.start() 来创建新线程,⽽是让每个线程去执⾏⼀ 个“循环任务”,在这个“循环任务”中不停检查是否有任务需要被执⾏,如果有则直接执⾏,也就是调⽤ 任务中的 run ⽅法,将 run ⽅法当成⼀个普通的⽅法执⾏,通过这种⽅式只使⽤固定的线程就将所有任 务的 run ⽅法串联起来。
49.ReentrantLock中的公平锁和⾮公平锁的底层实现?
⾸先不管是公平锁和⾮公平锁,它们的底层实现都会使⽤AQS来进⾏排队,它们的区别在于:线程在使 ⽤lock()⽅法加锁时,如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队, 则当前线程也进⾏排队,如果是⾮公平锁,则不会去检查是否有线程在排队,⽽是直接竞争锁。不管是公平锁还是⾮公平锁,⼀旦没竞争到锁,都会进⾏排队,当锁释放时,都是唤醒排在最前⾯的线 程,所以⾮公平锁只是体现在了线程加锁阶段,⽽没有体现在线程被唤醒阶段。 另外,ReentrantLock是可重⼊锁,不管是公平锁还是⾮公平锁都是可重⼊的。
50.ReentrantLock中tryLock()和lock()⽅法的区别
1. tryLock()表示尝试加锁,可能加到,也可能加不到,该⽅法不会阻塞线程,如果加到锁则返回 true,没有加到则返回false 2. lock()表示阻塞加锁,线程会阻塞直到加到锁,⽅法也没有返回值
2. lock()表示阻塞加锁,线程会阻塞直到加到锁,⽅法也没有返回值
51.CountDownLatch和Semaphore的区别和底层原理
CountDownLatch表示计数器,可以给CountDownLatch设置⼀个数字,⼀个线程调⽤
CountDownLatch的await()将会阻塞,其他线程可以调⽤CountDownLatch的countDown()⽅法来对
CountDownLatch中的数字减⼀,当数字被减成0后,所有await的线程都将被唤醒。
对应的底层原理就是,调⽤await()⽅法的线程会利⽤AQS排队,⼀旦数字被减为0,则会将AQS中 排队的线程依次唤醒。 Semaphore表示信号量,可以设置许可的个数,表示同时允许最多多少个线程使⽤该信号量,通 过acquire()来获取许可,如果没有许可可⽤则线程阻塞,并通过AQS来排队,可以通过release() ⽅法来释放许可,当某个线程释放了某个许可后,会从AQS中正在排队的第⼀个线程开始依次唤 醒,直到没有空闲许可。
52.Sychronized的偏向锁、轻量级锁、重量级锁
1. 偏向锁:在锁对象的对象头中记录⼀下当前获取到该锁的线程ID,该线程下次如果⼜来获取该锁就 可以直接获取到了
2. 轻量级锁:由偏向锁升级⽽来,当⼀个线程获取到锁后,此时这把锁是偏向锁,此时如果有第⼆个 线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻 量级锁底层是通过⾃旋来实现的,并不会阻塞线程
3. 如果⾃旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞
4. ⾃旋锁:⾃旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就⽆所谓唤醒线程,阻塞和唤醒 这两个步骤都是需要操作系统去进⾏的,⽐较消耗时间,⾃旋锁是线程通过CAS获取预期的⼀个标 记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程⼀直在运 ⾏中,相对⽽⾔没有使⽤太多的操作系统资源,⽐较轻量。
53.Sychronized和ReentrantLock的区别
1. sychronized是⼀个关键字,ReentrantLock是⼀个类
2. sychronized会⾃动的加锁与释放锁,ReentrantLock需要程序员⼿动加锁与释放锁
3. sychronized的底层是JVM层⾯的锁,ReentrantLock是API层⾯的锁
4. sychronized是⾮公平锁,ReentrantLock可以选择公平锁或⾮公平锁
5. sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识 来标识锁的状态
6. sychronized底层有⼀个锁升级的过程
54.谈谈你对AQS的理解,AQS如何实现可重⼊锁?
1. AQS是⼀个JAVA线程同步的框架。是JDK中很多锁⼯具的核⼼实现框架。
2. 在AQS中,维护了⼀个信号量state和⼀个线程组成的双向链表队列。其中,这个线程队列,就是⽤ 来给线程排队的,⽽state就像是⼀个红绿灯,⽤来控制线程排队或者放⾏的。 在不同的场景下,有
不⽤的意义。
3. 在可重⼊锁这个场景下,state就⽤来表示加锁的次数。0标识⽆锁,每加⼀次锁,state就加1。释 放锁state就减1。
开发框架(28)
55.Spring是什么?
轻量级的开源的J2EE框架。它是⼀个容器框架,⽤来装javabean(java对象),中间层框架(万能胶) 可以起⼀个连接作⽤,⽐如说把Struts和hibernate粘合在⼀起运⽤,可以让我们的企业开发更快、更简 洁,Spring是⼀个轻量级的控制反转(IoC)和⾯向切⾯(AOP)的容器框架:
- 从⼤⼩与开销两⽅⾯⽽⾔Spring都是轻量级的。
- 通过控制反转(IoC)的技术达到松耦合的⽬的 提供了⾯向切⾯编程的丰富⽀持,允许通过分离应⽤的业务逻辑与系统级服务进⾏内聚性的开发
- 包含并管理应⽤对象(Bean)的配置和⽣命周期,这个意义上是⼀个容器。
- 将简单的组件配置、组合成为复杂的应⽤,这个意义上是⼀个框架。
56.谈谈你对AOP的理解
系统是由许多不同的组件所组成的,每⼀个组件各负责⼀块特定功能。除了实现⾃身核⼼功能之外,这 些组件还经常承担着额外的职责。例如⽇志、事务管理和安全这样的核⼼服务经常融⼊到⾃身具有核⼼ 业务逻辑的组件中去。这些系统服务经常被称为横切关注点,因为它们会跨越系统的多个组件。
当我们需要为分散的对象引⼊公共⾏为的时候,OOP则显得⽆能为⼒。也就是说,OOP允许你定义从上 到下的关系,但并不适合定义从左到右的关系。例如⽇志功能。
⽇志代码往往⽔平地散布在所有对象层次中,⽽与它所散布到的对象的核⼼功能毫⽆关系。 在OOP设计中,它导致了⼤量代码的重复,⽽不利于各个模块的重⽤。
AOP:将程序中的交叉业务逻辑(⽐如安全,⽇志,事务等),封装成⼀个切⾯,然后注⼊到⽬标对象 (具体业务逻辑)中去。AOP可以对某个对象或某些对象的功能进⾏增强,⽐如对象中的⽅法进⾏增 强,可以在执⾏某个⽅法之前额外的做⼀些事情,在某个⽅法执⾏之后额外的做⼀些事情
57.谈谈你对IOC的理解
容器概念、控制反转、依赖注⼊
ioc容器:实际上就是个map(key,value),⾥⾯存的是各种对象(在xml⾥配置的bean节点、 @repository、@service、@controller、@component),在项⽬启动的时候会读取配置⽂件⾥⾯的 bean节点,根据全限定类名使⽤反射创建对象放到map⾥、扫描到打上上述注解的类还是通过反射创建 对象放到map⾥。
这个时候map⾥就有各种对象了,接下来我们在代码⾥需要⽤到⾥⾯的对象时,再通过DI注⼊ (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容器在运⾏期间,动态地将某种依赖关系注⼊到对象 之中。
58.解释下Spring⽀持的⼏种bean的作⽤域。
- singleton:默认,每个容器中只有⼀个bean的实例,单例的模式由BeanFactory⾃身来维护。该对 象的⽣命周期是与Spring IOC容器⼀致的(但在第⼀次被注⼊时才会创建)。
- prototype:为每⼀个bean请求提供⼀个实例。在每次注⼊时都会创建⼀个新的对象 request:bean被定义为在每个HTTP请求中创建⼀个单例对象,也就是说在单个请求中都会复⽤这 ⼀个单例对象。
- session:与request范围类似,确保每个session中有⼀个bean的实例,在session过期后,bean 会随之失效。
- application:bean被定义为在ServletContext的⽣命周期中复⽤⼀个单例对象。 websocket:bean被定义为在websocket的⽣命周期中复⽤⼀个单例对象。
- global-session:全局作⽤域,global-session和Portlet应⽤相关。当你的应⽤部署在Portlet容器 中⼯作时,它包含很多portlet。如果你想要声明让所有的portlet共⽤全局的存储变量的话,那么这 全局变量需要存储在global-session中。全局作⽤域与Servlet中的session作⽤域效果相同。
59.Spring事务的实现⽅式和原理以及隔离级别?
在使⽤Spring框架时,可以有两种使⽤事务的⽅式,⼀种是编程式的,⼀种是申明式的, @Transactional注解就是申明式的。
⾸先,事务这个概念是数据库层⾯的,Spring只是基于数据库中的事务进⾏了扩展,以及提供了⼀些能 让程序员更加⽅便操作事务的⽅式。
⽐如我们可以通过在某个⽅法上增加@Transactional注解,就可以开启事务,这个⽅法中所有的sql都 会在⼀个事务中执⾏,统⼀成功或失败。
在⼀个⽅法上加了@Transactional注解后,Spring会基于这个类⽣成⼀个代理对象,会将这个代理对象 作为bean,当在使⽤这个代理对象的⽅法时,如果这个⽅法上存在@Transactional注解,那么代理逻辑 会先把事务的⾃动提交设置为false,然后再去执⾏原本的业务逻辑⽅法,如果执⾏业务逻辑⽅法没有出 现异常,那么代理逻辑中就会将事务进⾏提交,如果执⾏业务逻辑⽅法出现了异常,那么则会将事务进 ⾏回滚。
当然,针对哪些异常回滚事务是可以配置的,可以利⽤@Transactional注解中的rollbackFor属性进⾏配 置,默认情况下会对RuntimeException和Error进⾏回滚。
spring事务隔离级别就是数据库的隔离级别:外加⼀个默认级别
- read uncommitted(未提交读)
- read committed(提交读、不可重复读)
- repeatable read(可重复读)
- serializable(可串⾏化)
60.Spring事务传播机制
多个事务⽅法相互调⽤时,事务如何在这些⽅法间传播,⽅法A是⼀个事务的⽅法,⽅法A执⾏过程中调 ⽤了⽅法B,那么⽅法B有⽆事务以及⽅法B对事务的要求不同都会对⽅法A的事务具体执⾏造成影响, 同时⽅法A的事务对⽅法B的事务执⾏也有影响,这种影响具体是什么就由两个⽅法所定义的事务传播类 型所决定。
1. REQUIRED(Spring默认的事务传播类型):如果当前没有事务,则⾃⼰新建⼀个事务,如果当前存 在事务,则加⼊这个事务
2. SUPPORTS:当前存在事务,则加⼊当前事务,如果当前没有事务,就以⾮事务⽅法执⾏ 3. MANDATORY:当前存在事务,则加⼊当前事务,如果当前事务不存在,则抛出异常。
4. REQUIRES_NEW:创建⼀个新事务,如果存在当前事务,则挂起该事务。
5. NOT_SUPPORTED:以⾮事务⽅式执⾏,如果当前存在事务,则挂起当前事务
6. NEVER:不使⽤事务,如果当前事务存在,则抛出异常
7. NESTED:如果当前事务存在,则在嵌套事务中执⾏,否则REQUIRED的操作⼀样(开启⼀个事 务)
61.Spring事务什么时候会失效?
spring事务的原理是AOP,进⾏了切⾯增强,那么失效的根本原因是这个AOP不起作⽤了!常⻅情况有 如下⼏种
1、发⽣⾃调⽤,类⾥⾯使⽤this调⽤本类的⽅法(this通常省略),此时这个this对象不是代理类,⽽ 是UserService对象本身! 解决⽅法很简单,让那个this变成UserService的代理类即可!
2、⽅法不是public的:@Transactional 只能⽤于 public 的⽅法上,否则事务不会失效,如果要⽤在⾮ public ⽅法上,可以开启 AspectJ 代理模式。
3、数据库不⽀持事务
4、没有被spring管理
5、异常被吃掉,事务不会回滚(或者抛出的异常没有被定义,默认为RuntimeException)
62.什么是bean的⾃动装配,有哪些⽅式?
开启⾃动装配,只需要在xml配置⽂件中定义“autowire”属性。
autowire属性有五种装配的⽅式:
- no – 缺省情况下,⾃动配置是通过“ref”属性⼿动设定 。
- ⼿动装配:以value或ref的⽅式明确指定属性值都是⼿动装配。
- 需要通过‘ref’属性来连接bean。
- byName-根据bean的属性名称进⾏⾃动装配。
- Cutomer的属性名称是person,Spring会将bean id为person的bean通过setter⽅法进⾏⾃动 装配。
- <bean id="cutomer" class="com.xxx.xxx.Cutomer" autowire="byName"/>
- <bean id="person" class="com.xxx.xxx.Person" />
- byType-根据bean的类型进⾏⾃动装配。
- Cutomer的属性person的类型为Person,Spirng会将Person类型通过setter⽅法进⾏⾃动装 配。
- <bean id="cutomer" class="com.xxx.xxx.Cutomer" autowire="byType"/>
- <bean id="person" class="com.xxx.xxx.Person" />
- constructor-类似byType,不过是应⽤于构造器的参数。如果⼀个bean与构造器参数的类型形 同,则进⾏⾃动装配,否则导致异常。
- Cutomer构造函数的参数person的类型为Person,Spirng会将Person类型通过构造⽅法进⾏⾃动 装配。
- <bean id="cutomer" class="com.xxx.xxx.Cutomer" autowire="construtor"/>
- <bean id="person" class="com.xxx.xxx.Person" />
- autodetect-如果有默认的构造器,则通过constructor⽅式进⾏⾃动装配,否则使⽤byType⽅式进 ⾏⾃动装配。
@Autowired⾃动装配bean,可以在字段、setter⽅法、构造函数上使⽤
63.Spring中的Bean创建的⽣命周期有哪些步骤
Spring中⼀个Bean的创建⼤概分为以下⼏个步骤:
1. 推断构造⽅法
2. 实例化
3. 填充属性,也就是依赖注⼊
4. 处理Aware回调
5. 初始化前,处理@PostConstruct注解
6. 初始化,处理InitializingBean接⼝
7. 初始化后,进⾏AOP
64.Spring中Bean是线程安全的吗
Spring本身并没有针对Bean做线程安全的处理,所以: 1. 如果Bean是⽆状态的,那么Bean则是线程安全的 2. 如果Bean是有状态的,那么Bean则不是线程安全的 另外,Bean是不是线程安全,跟Bean的作⽤域没有关系,Bean的作⽤域只是表示Bean的⽣命周期范 围,对于任何⽣命周期的Bean都是⼀个对象,这个对象是不是线程安全的,还是得看这个Bean对象本 身。
65.ApplicationContext和BeanFactory有什么区别?
BeanFactory是Spring中⾮常核⼼的组件,表示Bean⼯⼚,可以⽣成Bean,维护Bean,⽽ ApplicationContext继承了BeanFactory,所以ApplicationContext拥有BeanFactory所有的特点,也 是⼀个Bean⼯⼚,但是ApplicationContext除开继承了BeanFactory之外,还继承了诸如 EnvironmentCapable、MessageSource、ApplicationEventPublisher等接⼝,从⽽ ApplicationContext还有获取系统环境变量、国际化、事件发布等功能,这是BeanFactory所不具备的
66.Spring中的事务是如何实现的
1. Spring事务底层是基于数据库事务和AOP机制的
2. ⾸先对于使⽤了@Transactional注解的Bean,Spring会创建⼀个代理对象作为Bean 3. 当调⽤代理对象的⽅法时,会先判断该⽅法上是否加了@Transactional注解
4. 如果加了,那么则利⽤事务管理器创建⼀个数据库连接
5. 并且修改数据库连接的autocommit属性为false,禁⽌此连接的⾃动提交,这是实现Spring事务⾮ 常重要的⼀步
6. 然后执⾏当前⽅法,⽅法中会执⾏sql
7. 执⾏完当前⽅法后,如果没有出现异常就直接提交事务
8. 如果出现了异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务
9. Spring事务的隔离级别对应的就是数据库的隔离级别
10. Spring事务的传播机制是Spring事务⾃⼰实现的,也是Spring事务中最复杂的
11. Spring事务的传播机制是基于数据库连接来做的,⼀个数据库连接⼀个事务,如果传播机制配置为 需要新开⼀个事务,那么实际上就是先建⽴⼀个数据库连接,在此新数据库连接上执⾏sql
67.Spring中什么时候@Transactional会失效
因为Spring事务是基于代理来实现的,所以某个加了@Transactional的⽅法只有是被代理对象调⽤时, 那么这个注解才会⽣效,所以如果是被代理对象来调⽤这个⽅法,那么@Transactional是不会失效的。 同时如果某个⽅法是private的,那么@Transactional也会失效,因为底层cglib是基于⽗⼦类来实现 的,⼦类是不能重载⽗类的private⽅法的,所以⽆法很好的利⽤代理,也会导致@Transactianal失效
68.Spring容器启动流程是怎样的
1. 在创建Spring容器,也就是启动Spring时:
2. ⾸先会进⾏扫描,扫描得到所有的BeanDefinition对象,并存在⼀个Map中
3. 然后筛选出⾮懒加载的单例BeanDefinition进⾏创建Bean,对于多例Bean不需要在启动过程中去进 ⾏创建,对于多例Bean会在每次获取Bean时利⽤BeanDefinition去创建
4. 利⽤BeanDefinition创建Bean就是Bean的创建⽣命周期,这期间包括了合并BeanDefinition、推断 构造⽅法、实例化、属性填充、初始化前、初始化、初始化后等步骤,其中AOP就是发⽣在初始化 后这⼀步骤中
5. 单例Bean创建完了之后,Spring会发布⼀个容器启动事件
6. Spring启动结束
7. 在源码中会更复杂,⽐如源码中会提供⼀些模板⽅法,让⼦类来实现,⽐如源码中还涉及到⼀些 BeanFactoryPostProcessor和BeanPostProcessor的注册,Spring的扫描就是通过 BenaFactoryPostProcessor来实现的,依赖注⼊就是通过BeanPostProcessor来实现的 8. 在8.在Spring启动过程中还会去处理@Import等注解
69.Spring⽤到了哪些设计模式
70.Spring Boot、Spring MVC 和 Spring 有什么区别
spring是⼀个IOC容器,⽤来管理Bean,使⽤依赖注⼊实现控制反转,可以很⽅便的整合各种框架,提 供AOP机制弥补OOP的代码重复问题、更⽅便将不同类不同⽅法中的共同处理抽取成切⾯、⾃动注⼊给 ⽅法执⾏,⽐如⽇志、异常等
springmvc是spring对web框架的⼀个解决⽅案,提供了⼀个总的前端控制器Servlet,⽤来接收请求, 然后定义了⼀套路由策略(url到handle的映射)及适配执⾏handle,将handle结果使⽤视图解析技术⽣ 成视图展现给前端
springboot是spring提供的⼀个快速开发⼯具包,让程序员能更⽅便、更快速的开发spring+springmvc 应⽤,简化了配置(约定了默认配置),整合了⼀系列的解决⽅案(starter机制)、redis、 mongodb、es,可以开箱即⽤
71.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 响应⽤户。
72.Spring MVC的主要组件?
Handler:也就是处理器。它直接应对着MVC中的C也就是Controller层,它的具体表现形式有很多,可 以是类,也可以是⽅法。在Controller层中@RequestMapping标注的所有⽅法都可以看成是⼀个 Handler,只要可以实际处理请求就可以是Handler
1、HandlerMapping initHandlerMappings(context),处理器映射器,根据⽤户请求的资源uri来查找Handler的。在 SpringMVC中会有很多请求,每个请求都需要⼀个Handler处理,具体接收到⼀个请求之后使⽤哪个 Handler进⾏,这就是HandlerMapping需要做的事。
2、HandlerAdapter initHandlerAdapters(context),适配器。因为SpringMVC中的Handler可以是任意的形式,只要能处理 请求就ok,但是Servlet需要的处理⽅法的结构却是固定的,都是以request和response为参数的⽅法。 如何让固定的Servlet处理⽅法调⽤灵活的Handler来进⾏处理呢?这就是HandlerAdapter要做的事情。 Handler是⽤来⼲活的⼯具;HandlerMapping⽤于根据需要⼲的活找到相应的⼯具;HandlerAdapter 是使⽤⼯具⼲活的⼈。
3、HandlerExceptionResolver initHandlerExceptionResolvers(context), 其它组件都是⽤来⼲活的。在⼲活的过程中难免会出现问 题,出问题后怎么办呢?这就需要有⼀个专⻔的⻆⾊对异常情况进⾏处理,在SpringMVC中就是 HandlerExceptionResolver。具体来说,此组件的作⽤是根据异常设置ModelAndView,之后再交给 render⽅法进⾏渲染。
4、ViewResolver initViewResolvers(context),ViewResolver⽤来将String类型的视图名和Locale解析为View类型的视 图。View是⽤来渲染⻚⾯的,也就是将程序返回的参数填⼊模板⾥,⽣成html(也可能是其它类型)⽂ 件。这⾥就有两个关键问题:使⽤哪个模板?⽤什么技术(规则)填⼊参数?这其实是ViewResolver主 要要做的⼯作,ViewResolver需要找到渲染所⽤的模板和所⽤的技术(也就是视图的类型)进⾏渲染, 具体的渲染过程则交由不同的视图⾃⼰完成。
5、RequestToViewNameTranslator initRequestToViewNameTranslator(context),ViewResolver是根据ViewName查找View,但有的 Handler处理完后并没有设置View也没有设置ViewName,这时就需要从request获取ViewName了,如 何从request中获取ViewName就是RequestToViewNameTranslator要做的事情了。 RequestToViewNameTranslator在Spring MVC容器⾥只可以配置⼀个,所以所有request到 ViewName的转换规则都要在⼀个Translator⾥⾯全部实现。
6、LocaleResolver initLocaleResolver(context), 解析视图需要两个参数:⼀是视图名,另⼀个是Locale。视图名是处理 器返回的,Locale是从哪⾥来的?这就是LocaleResolver要做的事情。LocaleResolver⽤于从request 解析出Locale,Locale就是zh-cn之类,表示⼀个区域,有了这个就可以对不同区域的⽤户显示不同的 结果。SpringMVC主要有两个地⽅⽤到了Locale:⼀是ViewResolver视图解析的时候;⼆是⽤到国际 化资源或者主题的时候
7、ThemeResolver initThemeResolver(context),⽤于解析主题。SpringMVC中⼀个主题对应⼀个properties⽂件,⾥⾯ 存放着跟当前主题相关的所有资源、如图⽚、css样式等。SpringMVC的主题也⽀持国际化,同⼀个主 题不同区域也可以显示不同的⻛格。SpringMVC中跟主题相关的类有 ThemeResolver、ThemeSource 和Theme。主题是通过⼀系列资源来具体体现的,要得到⼀个主题的资源,⾸先要得到资源的名称,这 是ThemeResolver的⼯作。然后通过主题名称找到对应的主题(可以理解为⼀个配置)⽂件,这是 ThemeSource的⼯作。最后从主题中获取资源就可以了。
8、MultipartResolver initMultipartResolver(context),⽤于处理上传请求。处理⽅法是将普通的request包装成 MultipartHttpServletRequest,后者可以直接调⽤getFile⽅法获取File,如果上传多个⽂件,还可以调 ⽤getFileMap得到FileName->File结构的Map。此组件中⼀共有三个⽅法,作⽤分别是判断是不是上传 请求,将request包装成MultipartHttpServletRequest、处理完后清理上传过程中产⽣的临时资源。
9、FlashMapManager initFlashMapManager(context),⽤来管理FlashMap的,FlashMap主要⽤在redirect中传递参数。
73.Spring Boot ⾃动配置原理?
@Import + @Configuration + Spring spi
⾃动配置类由各个starter提供,使⽤@Configuration + @Bean定义配置类,放到METAINF/spring.factories下
使⽤Spring spi扫描META-INF/spring.factories下的配置类
使⽤@Import导⼊⾃动配置类
74.如何理解 Spring Boot 中的 Starter
使⽤spring + springmvc使⽤,如果需要引⼊mybatis等框架,需要到xml中定义mybatis需要的bean
starter就是定义⼀个starter的jar包,写⼀个@Configuration配置类、将这些bean定义在⾥⾯,然后在 starter包的META-INF/spring.factories中写⼊该配置类,springboot会按照约定来加载该配置类
开发⼈员只需要将相应的starter包依赖进应⽤,进⾏相应的属性配置(使⽤默认配置时,不需要配 置),就可以直接进⾏代码开发,使⽤对应的功能了,⽐如mybatis-spring-boot--starter,springboot-starter-redis
75.什么是嵌⼊式服务器?为什么要使⽤嵌⼊式服务器?
节省了下载安装tomcat,应⽤也不需要再打war包,然后放到webapp⽬录下再运⾏
只需要⼀个安装了 Java 的虚拟机,就可以直接在上⾯部署应⽤程序了
springboot已经内置了tomcat.jar,运⾏main⽅法时会去启动tomcat,并利⽤tomcat的spi机制加载 springmvc
76.Spring Boot中常⽤注解及其底层实现
1.@SpringBootApplication注解:这个注解标识了⼀个SpringBoot⼯程,它实际上是另外三个注解的 组合,这三个注解是:
- a. @SpringBootConfiguration:这个注解实际就是⼀个@Configuration,表示启动类也是⼀个 配置类
- b. @EnableAutoConfiguration:向Spring容器中导⼊了⼀个Selector,⽤来加载ClassPath下 SpringFactories中所定义的⾃动配置类,将这些⾃动加载为配置Bean
- c. @ComponentScan:标识扫描路径,因为默认是没有配置实际扫描路径,所以SpringBoot扫 描的路径是启动类所在的当前⽬录
2.@Bean注解:⽤来定义Bean,类似于XML中的标签,Spring在启动时,会对加了@Bean注 解的⽅法进⾏解析,将⽅法的名字做为beanName,并通过执⾏⽅法得到bean对象
3.@Controller、@Service、@ResponseBody、@Autowired都可以说
77.Spring Boot是如何启动Tomcat的
1. ⾸先,SpringBoot在启动时会先创建⼀个Spring容器
2. 在创建Spring容器过程中,会利⽤@ConditionalOnClass技术来判断当前classpath中是否存在 Tomcat依赖,如果存在则会⽣成⼀个启动Tomcat的Bean
3. Spring容器创建完之后,就会获取启动Tomcat的Bean,并创建Tomcat对象,并绑定端⼝等,然后 启动Tomcat
78.Spring Boot中配置⽂件的加载顺序是怎样的?
优先级从⾼到低,⾼优先级的配置覆盖低优先级的配置,所有配置会形成互补配置。
1. 命令⾏参数。所有的配置都可以在命令⾏上进⾏指定;
2. Java系统属性(System.getProperties());
3. 操作系统环境变量 ;
4. jar包外部的application-{profile}.properties或application.yml(带spring.profile)配置⽂件
5. jar包内部的application-{profile}.properties或application.yml(带spring.profile)配置⽂件 再来加 载不带profile
6. jar包外部的application.properties或application.yml(不带spring.profile)配置⽂件
7. jar包内部的application.properties或application.yml(不带spring.profile)配置⽂件
8. @Configuration注解类上的@PropertySource
79.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 语句依赖于数据库, 导致数据库移植性差, 不能随意更换数据库。
80.#{}和${}的区别是什么?
#{}是预编译处理、是占位符, ${}是字符串替换、是拼接符。
Mybatis 在处理#{}时,会将 sql 中的#{}替换为?号,调⽤ PreparedStatement 来赋值;
Mybatis 在处理 {}替换成变量的值,调⽤ Statement 来赋值;
#{} 的变量替换是在DBMS 中、变量替换后,#{} 对应的变量⾃动加上单引号,{} 的变量替换是在 DBMS 外、变量替换后,{} 对应的变量不会加上单引号
使⽤#{}可以有效的防⽌ SQL 注⼊, 提⾼系统安全性。1.
81.简述 Mybatis 的插件运⾏原理,如何编写⼀个插件。
Mybatis 只⽀持针对 ParameterHandler、ResultSetHandler、StatementHandler、Executor 这 4 种 接⼝的插件, Mybatis 使⽤ JDK 的动态代理, 为需要拦截的接⼝⽣成代理对象以实现接⼝⽅法拦截功 能, 每当执⾏这 4 种接⼝对象的⽅法时,就会进⼊拦截⽅法,具体就是 InvocationHandler 的 invoke() ⽅法, 拦截那些你指定需要拦截的⽅法。
编写插件: 实现 Mybatis 的 Interceptor 接⼝并复写 intercept()⽅法, 然后在给插件编写注解, 指定 要拦截哪⼀个接⼝的哪些⽅法即可, 在配置⽂件中配置编写的插件。
Mysql(20)
82.索引的基本原理
索引⽤来快速地寻找那些具有特定值的记录。如果没有索引,⼀般来说执⾏查询时遍历整张表。
索引的原理:就是把⽆序的数据变成有序的查询
1. 把创建了索引的列的内容进⾏排序
2. 对排序结果⽣成倒排表
3. 在倒排表内容上拼上数据地址链
4. 在查询的时候,先拿到倒排表内容,再取出数据地址链,从⽽拿到具体数据
浙公网安备 33010602011771号