谈谈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. 在转移元素时,先将原数组分组,将每组分给不同的线程来进⾏元素的转移,每个线程负责⼀组或多组的元素转移⼯作
Jdk1.7到Jdk1.8 HashMap 发⽣了什么变化(底层)?
1. 1.7中底层是数组+链表,1.8中底层是数组+链表+红⿊树,加红⿊树的⽬的是提⾼HashMap插⼊和查询整体效率
2. 1.7中链表插⼊使⽤的是头插法,1.8中链表插⼊使⽤的是尾插法,因为1.8中插⼊key和value时需要判断链表元素个数,所以需要遍历链表统计链表元素个数,所以正好就直接使⽤尾插法
3. 1.7中哈希算法⽐较复杂,存在各种右移与异或运算,1.8中进⾏了简化,因为复杂的哈希算法的⽬的就是提⾼散列性,来提供HashMap的整体效率,⽽1.8中新增了红⿊树,所以可以适当的简化哈希算法,节省CPU资源
说⼀下HashMap的Put⽅法
先说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⽅法
泛型中extends和super的区别
1. <? extends T>表示包括T在内的任何T的⼦类
2. <? super T>表示包括T在内的任何T的⽗类
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属性
CopyOnWriteArrayList的底层原理是怎样的
1、首先CopyOnWriteArrayList内部也是⽤过数组来实现的,在向CopyOnWriteArrayList添加元素时,会复制一个新的数组,写操作在新数组上进行,读操作在原数组上进行
2. 并且,写操作会加锁,防止出现并发写入丢失数据的问题
3. 写操作结束之后会把原数组指向新数组
4. CopyOnWriteArrayList允许在写操作时来读取数据,大大提高了读的性能,因此适合读多写少的应用场景,但是CopyOnWriteArrayList会比较占内存,同时可能读到的数据不是实时最新的数据,所以不适合实时性要求很高的场景
什么是字节码?采⽤字节码的好处是什么?
Java中的编译器和解释器:Java中引⼊了虚拟机的概念,即在机器和编译程序之间加⼊了⼀层抽象的虚拟的机器。这台虚拟的机器在任何平台上都提供给编译程序⼀个的共同的接⼝。编译程序只需要⾯向虚拟机,⽣成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执⾏。在Java中,这种供虚拟机理解的代码叫做 字节码(即扩展名为 .class的⽂件),它不⾯向任何特定的处理器,只⾯向虚拟机。
每⼀种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执⾏,虚拟机将每⼀条要执⾏的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运⾏。这也就是解释了Java的编译与解释并存的特点。
Java源代码---->编译器---->jvm可执⾏的Java字节码(即虚拟指令)---->jvm---->jvm中解释器----->机器可执⾏的⼆进制机器码---->程序运⾏。
采⽤字节码的好处:
Java语⾔通过字节码的⽅式,在⼀定程度上解决了传统解释型语⾔执⾏效率低的问题,同时⼜保留了解释型语⾔可移植的特点。所以Java程序运⾏时⽐较⾼效,⽽且,由于字节码并不专对⼀种特定的机器,因此,Java程序⽆须重新编译便可在多种不同的计算机上运⾏。
Java中的异常体系是怎样的
Java中的所有异常都来⾃顶级⽗类Throwable。
Throwable下有两个⼦类Exception和Error。
Error是程序⽆法处理的错误,⼀旦出现这个错误,则程序将被迫停⽌运⾏。
Exception不会导致程序停⽌,⼜分为两个部分RunTimeException运⾏时异常和
CheckedException检查异常。
RunTimeException常常发⽣在程序运⾏过程中,会导致程序当前线程执⾏失败。
CheckedException常常发⽣在程序编译过程中,会导致程序编译不通过。
Java中有哪些类加载器
JDK⾃带有三个类加载器:bootstrap ClassLoader、ExtClassLoader、AppClassLoader。
BootStrapClassLoader是ExtClassLoader的⽗类加载器,默认负责加载%JAVA_HOME%lib下的jar包和class⽂件。
ExtClassLoader是AppClassLoader的⽗类加载器,负责加载%JAVA_HOME%/lib/ext⽂件夹下的jar包和class类。
AppClassLoader是⾃定义类加载器的⽗类,负责加载classpath下的类⽂件。
GC如何判断对象可以被回收
引⽤计数法:每个对象有⼀个引⽤计数属性,新增⼀个引⽤时计数加1,引⽤释放时计数减1,计数为0时可以回收,
可达性分析法:从 GC Roots 开始向下搜索,搜索所⾛过的路径称为引⽤链。当⼀个对象到 GCRoots 没有任何引⽤链相连时,则证明此对象是不可⽤的,那么虚拟机就判断是可回收对象。
引⽤计数法,可能会出现A 引⽤了 B,B ⼜引⽤了 A,这时候就算他们都不再使⽤了,但因为相互引⽤ 计数器=1 永远⽆法被回收。21
GC Roots的对象有:
虚拟机栈(栈帧中的本地变量表)中引⽤的对象
⽅法区中类静态属性引⽤的对象
⽅法区中常量引⽤的对象
本地⽅法栈中JNI(即⼀般说的Native⽅法)引⽤的对象
可达性算法中的不可达对象并不是⽴即死亡的,对象拥有⼀次⾃我拯救的机会。对象被系统宣告死亡⾄少要经历两次标记过程:第⼀次是经过可达性分析发现没有与GC Roots相连接的引⽤链,第⼆次是在由虚拟机⾃动建⽴的Finalizer队列中判断是否需要执⾏finalize()⽅法。
当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize⽅法,若未覆盖,则直接将其回收。否则,若对象未执⾏过finalize⽅法,将其放⼊F-Queue队列,由⼀低优先级线程执⾏该队列中对象的finalize⽅法。执⾏finalize⽅法完毕后,GC会再次判断该对象是否可达,若不可达,则进⾏回收,否则,对象“复活”
每个对象只能触发⼀次finalize()⽅法
由于finalize()⽅法运⾏代价⾼昂,不确定性⼤,⽆法保证各个对象的调⽤顺序,不推荐⼤家使⽤,建议遗忘它。
你们项⽬如何排查JVM问题
对于还在正常运⾏的系统:
1. 可以使⽤jmap来查看JVM中各个区域的使⽤情况
2. 可以通过jstack来查看线程的运⾏情况,⽐如哪些线程阻塞、是否出现了死锁
3. 可以通过jstat命令来查看垃圾回收的情况,特别是fullgc,如果发现fullgc⽐较频繁,那么就得进⾏调优了
4. 通过各个命令的结果,或者jvisualvm等⼯具来进⾏分析
5. ⾸先,初步猜测频繁发送fullgc的原因,如果频繁发⽣fullgc但是⼜⼀直没有出现内存溢出,那么表示fullgc实际上是回收了很多对象了,所以这些对象最好能在younggc过程中就直接回收掉,避免这些对象进⼊到⽼年代,对于这种情况,就要考虑这些存活时间不⻓的对象是不是⽐较⼤,导致年轻代放不下,直接进⼊到了⽼年代,尝试加⼤年轻代的⼤⼩,如果改完之后,fullgc减少,则证明修改有效
6. 同时,还可以找到占⽤CPU最多的线程,定位到具体的⽅法,优化这个⽅法的执⾏,看是否能避免某些对象的创建,从⽽节省内存
对于已经发⽣了OOM的系统:
1. ⼀般⽣产系统中都会设置当系统发⽣了OOM时,⽣成当时的dump⽂件(-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/base)
2. 我们可以利⽤jsisualvm等⼯具来分析dump⽂件
3. 根据dump⽂件找到异常的实例对象,和异常的线程(占⽤CPU⾼),定位到具体的代码
4. 然后再进⾏详细的分析和调试
总之,调优不是⼀蹴⽽就的,需要分析、推理、实践、总结、再分析,最终定位到具体的问题
⼀个对象从加载到JVM,再到被GC清除,都经历了什么过程?
1. ⽤户创建⼀个对象,JVM⾸先需要到⽅法区去找对象的类型信息。然后再创建对象。
2. JVM要实例化⼀个对象,⾸先要在堆当中先创建⼀个对象。-> 半初始化状态
3. 对象⾸先会分配在堆内存中新⽣代的Eden。然后经过⼀次Minor GC,对象如果存活,就会进⼊S区。在后续的每次GC中,如果对象⼀直存活,就会在S区来回拷⻉,每移动⼀次,年龄加1。-> 多⼤年龄才会移⼊⽼年代? 年龄最⼤15, 超过⼀定年龄后,对象转⼊⽼年代。
4. 当⽅法执⾏结束后,栈中的指针会先移除掉。
5. 堆中的对象,经过Full GC,就会被标记为垃圾,然后被GC线程清理掉
怎么确定⼀个对象到底是不是垃圾?
1. 引⽤计数: 这种⽅式是给堆内存当中的每个对象记录⼀个引⽤个数。引⽤个数为0的就认为是垃圾。这是早期JDK中使⽤的⽅式。引⽤计数⽆法解决循环引⽤的问题。
2. 根可达算法: 这种⽅式是在内存中,从引⽤根对象向下⼀直找引⽤,找不到的对象就是垃圾。
线程的⽣命周期?线程有⼏种状态
线程通常有五种状态,创建,就绪,运⾏、阻塞和死亡状态:
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类的⽅法
sleep()、wait()、join()、yield()之间的的区别
锁池:所有需要竞争同步锁的线程都会放在锁池当中,⽐如当前对象的锁已经被其中⼀个线程得到,则其他线程需要在这个锁池进⾏等待,当前⾯的线程释放同步锁后锁池中的线程去竞争同步锁,当某个程得到后会进⼊就绪队列进⾏等待cpu资源分配。
等待池:当我们调⽤wait()⽅法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁。只有调⽤了notify()或notifyAll()后等待池的线程才会开始去竞争锁,notify()是随机从等待池选出⼀个线程放到锁池,⽽notifyAll()是将等待池的所有线程放到锁池当中
1. sleep 是 Thread 类的静态本地⽅法,wait 则是 Object 类的本地⽅法。
2. sleep⽅法不会释放lock,但是wait会释放,⽽且会加⼊到等待队列中。
sleep就是把cpu的执⾏资格和执⾏权释放出去,不再运⾏此线程,当定时时间结束再取回cpu资源,参与cpu的调度,获取到cpu资源后就可以继续运⾏了。⽽如果sleep时该线程有锁,那么sleep不会释放这个锁,⽽是把锁带着进⼊了冻结状态,也就是说其他需要这个锁的线程根本不可能获取到这个锁。也就是说⽆法执⾏程序。如果在睡眠期间其他线程调⽤了这个线程的interrupt⽅法,那么这个线程也会抛出interruptexception异常返回,这点和wait是⼀样的
3. sleep⽅法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。
4. sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别⼈中断)。
5. sleep ⼀般⽤于当前线程休眠,或者轮循暂停操作,wait 则多⽤于多线程之间的通信。
6. sleep 会让出 CPU 执⾏时间且强制上下⽂切换,⽽ wait 则不⼀定,wait 后可能还是有机会重新竞争到锁继续执⾏的。
7. yield()执⾏后线程直接进⼊就绪状态,⻢上释放了cpu的执⾏权,但是依然保留了cpu的执⾏资格,所以有可能cpu下次进⾏线程调度还会让这个线程获取到执⾏权继续执⾏
8. join()执⾏后线程进⼊阻塞状态,例如在线程B中调⽤线程A的join(),那线程B会进⼊到阻塞队列,直到线程A结束或中断线程
Thread和Runable的区别
Thread和Runnable的实质是继承关系,没有可⽐性。⽆论使⽤Runnable还是Thread,都会newThread,然后执⾏run⽅法。⽤法上,如果有复杂的线程操作需求,那就选择继承Thread,如果只是简单的执⾏⼀个任务,那就实现runnable。
会卖出多⼀倍的票
public static void main(String[] args) {
new MyThread().start();
new MyThread().start();
}
static class MyThread extends Thread{
private int ticket = 5;
public void run(){
while(true){
System.out.println("Thread ticket = " + ticket--);
if(ticket < 0){
break;
}
}
}
}
原因是:MyThread创建了两个实例,⾃然会卖出两倍,属于⽤法错误
正常的售卖
public static void main(String[] args) {
MyThread2 mt=new MyThread2();
new Thread(mt).start();
new Thread(mt).start();
}
static class MyThread2 implements Runnable{
private int ticket = 5;
public void run(){
while(true){
System.out.println("Runnable ticket = " + ticket--);
if(ticket < 0){
break;
}
}
}
}
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经典的应⽤场景就是连接管理(⼀个线程持有⼀个连接,该连接对象可以在不同的⽅法之间进⾏传递,线程之间不共享同⼀个连接)
并发、并⾏、串⾏之间的区别
1. 串⾏在时间上不可能发⽣重叠,前⼀个任务没搞定,下⼀个任务就只能等着
2. 并⾏在时间上是重叠的,两个任务在同⼀时刻互不⼲扰的同时执⾏。
3. 并发允许两个任务彼此⼲扰。统⼀时间点、只有⼀个任务运⾏,交替执⾏
并发的三⼤特性
原⼦性 关键字:synchronized
可⻅性 关键字:volatile、synchronized、final
有序性 关键字:volatile、synchronized
但是volatile关键字不满⾜原⼦性
synchronized关键字同时满⾜以上三种特性,但是volatile关键字不满⾜原⼦性。
在某些情况下,volatile的同步机制的性能确实要优于锁(使⽤synchronized关键字或java.util.concurrent包⾥⾯的锁),因为volatile的总开销要⽐锁低。
我们判断使⽤volatile还是加锁的唯⼀依据就是volatile的语义能否满⾜使⽤的场景(原⼦性)
Java死锁如何避免?
造成死锁的⼏个原因:
1. ⼀个资源每次只能被⼀个线程使⽤
2. ⼀个线程在阻塞等待某个资源时,不释放已占有资源
3. ⼀个线程已经获得的资源,在未使⽤完之前,不能被强⾏剥夺
4. 若⼲线程形成头尾相接的循环等待资源关系
这是造成死锁必须要达到的4个条件,如果要避免死锁,只需要不满⾜其中某⼀个条件即可。⽽其中前3个条件是作为锁要符合的条件,所以要避免死锁就需要打破第4个条件,不出现循环等待锁的关系。
在开发过程中:
1. 要注意加锁顺序,保证每个线程按同样的顺序进⾏加锁
2. 要注意加锁时限,可以针对所设置⼀个超时时间
3. 要注意死锁检查,这是⼀种预防机制,确保在第⼀时间发现死锁并进⾏解决
为什么⽤线程池?解释下线程池参数?
1、降低资源消耗;提⾼线程利⽤率,降低创建和销毁线程的消耗。
2、提⾼响应速度;任务来了,直接有线程可⽤可执⾏,⽽不是先创建线程,再执⾏。
3、提⾼线程的可管理性;线程是稀缺资源,使⽤线程池可以统⼀分配调优监控。
线程池的底层⼯作原理
线程池内部是通过队列+线程实现的,当我们利⽤线程池执⾏任务时:
1. 如果此时线程池中的线程数量⼩于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
2. 如果此时线程池中的线程数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放⼊缓冲队列。
3. 如果此时线程池中的线程数量⼤于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量⼩于maximumPoolSize,建新的线程来处理被添加的任务。
4. 如果此时线程池中的线程数量⼤于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
5. 当线程池中的线程数量⼤于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终⽌。这样,线程池可以动态的调整池中的线程数
ReentrantLock中的公平锁和⾮公平锁的底层实现
⾸先不管是公平锁和⾮公平锁,它们的底层实现都会使⽤AQS来进⾏排队,它们的区别在于:线程在使⽤lock()⽅法加锁时,如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队,则当前线程也进⾏排队,如果是⾮公平锁,则不会去检查是否有线程在排队,⽽是直接竞争锁。
不管是公平锁还是⾮公平锁,⼀旦没竞争到锁,都会进⾏排队,当锁释放时,都是唤醒排在最前⾯的线程,所以⾮公平锁只是体现在了线程加锁阶段,⽽没有体现在线程被唤醒阶段。
ReentrantLock中tryLock()和lock()⽅法的区别
1. tryLock()表示尝试加锁,可能加到,也可能加不到,该⽅法不会阻塞线程,如果加到锁则返回true,没有加到则返回false
2. lock()表示阻塞加锁,线程会阻塞直到加到锁,⽅法也没有返回值
Sychronized的偏向锁、轻量级锁、重量级锁
1. 偏向锁:在锁对象的对象头中记录⼀下当前获取到该锁的线程ID,该线程下次如果⼜来获取该锁就可以直接获取到了
2. 轻量级锁:由偏向锁升级⽽来,当⼀个线程获取到锁后,此时这把锁是偏向锁,此时如果有第⼆个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻量级锁底层是通过⾃旋来实现的,并不会阻塞线程
3. 如果⾃旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞
4. ⾃旋锁:⾃旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就⽆所谓唤醒线程,阻塞和唤醒这两个步骤都是需要操作系统去进⾏的,⽐较消耗时间,⾃旋锁是线程通过CAS获取预期的⼀个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程⼀直在运⾏中,相对⽽⾔没有使⽤太多的操作系统资源,⽐较轻量。
Sychronized和ReentrantLock的区别
1. sychronized是⼀个关键字,ReentrantLock是⼀个类
2. sychronized会⾃动的加锁与释放锁,ReentrantLock需要程序员⼿动加锁与释放锁
3. sychronized的底层是JVM层⾯的锁,ReentrantLock是API层⾯的锁
4. sychronized是⾮公平锁,ReentrantLock可以选择公平锁或⾮公平锁
5. sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识来标识锁的状态
6. sychronized底层有⼀个锁升级的过程
Spring是什么?
轻量级的开源的J2EE框架。它是⼀个容器框架,⽤来装javabean(java对象),中间层框架(万能胶)可以起⼀个连接作⽤,⽐如说把Struts和hibernate粘合在⼀起运⽤,可以让我们的企业开发更快、更简洁,Spring是⼀个轻量级的控制反转(IoC)和⾯向切⾯(AOP)的容器框架:
从⼤⼩与开销两⽅⾯⽽⾔Spring都是轻量级的。
通过控制反转(IoC)的技术达到松耦合的⽬的
提供了⾯向切⾯编程的丰富⽀持,允许通过分离应⽤的业务逻辑与系统级服务进⾏内聚性的开发
包含并管理应⽤对象(Bean)的配置和⽣命周期,这个意义上是⼀个容器。
将简单的组件配置、组合成为复杂的应⽤,这个意义上是⼀个框架。
谈谈你对AOP的理解
系统是由许多不同的组件所组成的,每⼀个组件各负责⼀块特定功能。除了实现⾃身核⼼功能之外,这些组件还经常承担着额外的职责。例如⽇志、事务管理和安全这样的核⼼服务经常融⼊到⾃身具有核⼼业务逻辑的组件中去。这些系统服务经常被称为横切关注点,因为它们会跨越系统的多个组件。
当我们需要为分散的对象引⼊公共⾏为的时候,OOP则显得⽆能为⼒。也就是说,OOP允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如⽇志功能。⽇志代码往往⽔平地散布在所有对象层次中,⽽与它所散布到的对象的核⼼功能毫⽆关系。
在OOP设计中,它导致了⼤量代码的重复,⽽不利于各个模块的重⽤。
AOP:将程序中的交叉业务逻辑(⽐如安全,⽇志,事务等),封装成⼀个切⾯,然后注⼊到⽬标对象(具体业务逻辑)中去。AOP可以对某个对象或某些对象的功能进⾏增强,⽐如对象中的⽅法进⾏增强,可以在执⾏某个⽅法之前额外的做⼀些事情,在某个⽅法执⾏之后额外的做⼀些事情
谈谈你对IOC的理解容器概念、控制反转、依赖注⼊
容器概念
1、 ioc容器:实际上就是个map(key,value),⾥⾯存的是各种对象(在xml⾥配置的bean节点、@repository、@service、@controller、@component),在项⽬启动的时候会读取配置⽂件⾥⾯的bean节点,根据全限定类名使⽤反射创建对象放到map⾥、扫描到打上上述注解的类还是通过反射创建对象放到map⾥。
2、 这个时候map⾥就有各种对象了,接下来我们在代码⾥需要⽤到⾥⾯的对象时,再通过DI注⼊(autowired、resource等注解,xml⾥bean节点内的ref属性,项⽬启动的时候会读取xml节点ref属性根据id注⼊,也会扫描这些注解,根据类型或id注⼊;id就是对象名)。
控制反转
1、没有引⼊IOC容器之前,对象A依赖于对象B,那么对象A在初始化或者运⾏到某⼀点的时候,⾃⼰必须主动去创建对象B或者使⽤已经创建的对象B。⽆论是创建还是使⽤对象B,控制权都在⾃⼰⼿上。引⼊IOC容器之后,对象A与对象B之间失去了直接联系,当对象A运⾏到需要对象B的时候,IOC容器会主动创建⼀个对象B注⼊到对象A需要的地⽅。
2、通过前后的对⽐,不难看出来:对象A获得依赖对象B的过程,由主动⾏为变为了被动⾏为,控制权颠倒过来了,这就是“控制反转”这个名称的由来。
3、全部对象的控制权全部上缴给“第三⽅”IOC容器,所以,IOC容器成了整个系统的关键核⼼,它起到了⼀种类似“粘合剂”的作⽤,把系统中的所有对象粘合在⼀起发挥作⽤,如果没有这个“粘合剂”,对象与对象之间会彼此失去联系,这就是有⼈把IOC容器⽐喻成“粘合剂”的由来。
依赖注⼊:
“获得依赖对象的过程被反转了”。控制被反转之后,获得依赖对象的过程由⾃身管理变为了由IOC容器主动注⼊。依赖注⼊是实现IOC的⽅法,就是由IOC容器在运⾏期间,动态地将某种依赖关系注⼊到对象之中。
Spring事务的实现⽅式和原理以及隔离级别?
1、在使⽤Spring框架时,可以有两种使⽤事务的⽅式,⼀种是编程式的,⼀种是申明式的,
2、@Transactional注解就是申明式的。
3、⾸先,事务这个概念是数据库层⾯的,Spring只是基于数据库中的事务进⾏了扩展,以及提供了⼀些能让程序员更加⽅便操作事务的⽅式。
4、⽐如我们可以通过在某个⽅法上增加@Transactional注解,就可以开启事务,这个⽅法中所有的sql都会在⼀个事务中执⾏,统⼀成功或失败。
5、在⼀个⽅法上加了@Transactional注解后,Spring会基于这个类⽣成⼀个代理对象,会将这个代理对象作为bean,当在使⽤这个代理对象的⽅法时,如果这个⽅法上存在@Transactional注解,那么代理逻辑会先把事务的⾃动提交设置为false,然后再去执⾏原本的业务逻辑⽅法,如果执⾏业务逻辑⽅法没有出现异常,那么代理逻辑中就会将事务进⾏提交,如果执⾏业务逻辑⽅法出现了异常,那么则会将事务进⾏回滚。
6、当然,针对哪些异常回滚事务是可以配置的,可以利⽤@Transactional注解中的rollbackFor属性进⾏配置,默认情况下会对RuntimeException和Error进⾏回滚。
spring事务隔离级别就是数据库的隔离级别:外加⼀个默认级别
read uncommitted(未提交读)
Spring事务的实现⽅式和原理以及隔离级别?
read committed(提交读、不可重复读)
repeatable read(可重复读)
serializable(可串⾏化)