1.java有哪几种方式来创建线程执行任务?
以下几种方式,底层都是基于Runnable
答 1.继承Thread类,重写run方法(缺点:没办法继承其他类)
public class WangkaiheThread extends Thread {
public static void main(Sring[]args){
WangkaiheThread thread = new WangkaiheThread();
thread.start();
}
@Override
public void run(){
System.out.println("hello wkh");
}
}
总结:重写的是run()方法,而不是satart()方法,但是占用了继承的名额,java中的类是单继承的。java中的接口是可以多继承的。
2.实现Runnable接口,实现run方法(依然要用到thread,这种方法更常用:推荐)
public class WangkaiheThread implements Runnable{
public static void main(String[]args){
Thread thread = new Thread(new WangkaiheThread());
thread.start();
}
@Override
public void run(){
System.out.println("hello wkh")
}
}
2.1也可以通过匿名内部类生成rannable对象
public class myClass{
public static void main(String[] args) {
//通过匿名内部类
Thread thread = new Thread(new Runnable(){
@Override
public void run() {
System.out.println("这是线程的run方法");
}
});
thread.start();
}
}
2.2rannable函数式接口,通过lambda表达式
public class myClass{
public static void main(String[] args) {
//通过lambda表达式,Java8新特性函数式接口->一个接口里面只有一个方法,所以可以直接执行该接口
Thread thread = new Thread(() ->System.out.println("这是thread的run方法"));
thread.start();
}
}
总结:实现Runable接口,实现run()方法,使用依然要用到Thread,这种方法更常用
3.实现Callable接口,实现call方法
public class WangkaiheThread implements Callable<String>{
public static void main(String[]args) throws ExecutionException,InterruptedException{
FutureTask<String> stringFutureTask = new FutureTask<>(new myClass());
Thread thread = new Thread(stringFutureTask);
thread.start();
String result = stringFutureTask.get();
System.out.println(result);
}
0
@Override
public void run(){
System.out.println("hello wkh")
}
}
总结:实现callable接口,实现call()方法,得使用Thread+FutureTask配合,这种方式可以拿到执行任务的结果
4.利用线程池来创建线程(Executors.newFixedThreadPool)
public class myClass implements Runnable {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(10);
executorService.execute(new myClass());
}
@Override
public void run() {
System.out.println("这是run方法");
}
}
(Call方法可以抛出异常,run方法不可以。)
附加:

java8的新特性,一个接口里只有一个方法,函数式接口@FunctionalInterface

2.为什么不建议使用Executors来创建线程池?
答:1.FixedThreadPool
当我们使用Executors创建FixedThreadPool时,对应的构造方法为:
public static ExecutorService newFixedThreadPool(int nThreads){
return new ThreadPoolExecutor(nTHread,nThreads,0L,TimeUnit,MILLISECONDS,new LinkedBlockingQueue<Punnable>());
}
我们发现创建的队列为LinkedBlockingQueue,是一个无界阻塞队列,如果使用该线程池执行任务,如果任务过多就会不断的添加到队列中,任务越多占用的内存就越多。最终可能耗尽内存,导致OOM。
2.SingleThreadExecutor
当我们使用Executor创建SingleThreadExecutor时,对应的构造方法为:
public static ExecutorService newSingleThreadExecutor(){
return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1,1,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
}
也是LinkedBlockingQueue,所以同样可能会耗尽内存。
总结:除开有可能造成OOM之外,我们使用Executors来创建线程池也不能自定义线程的名字,不利于排查问题,所以建议直接使用ThreadPoolExecutor来定义线程池,这样可以灵活控制。
3.线程池有哪几种状态?每种状态分别表示什么?
答:1.RUNNING:表示线程池正常运行,既能接受新任务,也会正常处理队列中的任务
2.SHUTDOWN:当调用线程池的shutdown()方法时,线程池就进入SHUTDOWN状态,表示线程池处于正在关闭状态,此状态下线程池不会接受新任务,但是会继续把从队列中的任务处理完
3.STOP:当调用线程池的shutdownnow()方法时,线程池就进入STOP状态,表示线程池处于正在停止状态,此状态下线程池既不会接受新任务了,也不会处理队列中的任务,并且正在运行的线程也会被中断
4.TIDYING:线程池中没有线程在运行后,线程池的状态就会自动变为TIDYING,并且会调用terminated(),该方法是空方法,留给程序员进行扩展
5.TERMINATED:terminated()方法执行完成后,线程池状态就会变为TERMINATED
4.SYchronized和ReentrantLock有哪些不同点?
答:
5.ThreadLocal有哪些应用场景?它底层是如何实现的?
答:1.ThreadLocal是java中提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据
2.ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象中都存在一个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的值
3.如果在线程池中使用ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使用完之后,应该要把设置的key,value,也就是Entry对象进行回收,但线程池中的线程不会回收,而线程对象是通过强引用指向ThreadLocalMap,ThreadLocalMap也是通过强引用指向Entry对象,线程不被回收,Entry对象也就不会被回收,从而出现内存泄漏,解决办法是,在使用了ThreadLocal对象后,手动调用ThreadLocal的remove方法,手动清除Entry对象吧
4.ThreadLocal经典的应用场景就是连接管理(一个线程有一个连接,连接对象可以在不同的方法之间进行传递,线程之间不共享一个连接)
6.ReentrantLock分为公平锁和非公平锁,那底层分别是如何实现的?
答:首先不管是公平锁还是非公平锁,它们的底层实现都是使用AQS来进行排队,他们的区别在于线程在使用lock()方法加锁时:
1.如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队,则当前线程也进行排队。
2.如果是非公平锁,则不会去检查是否有线程在排队,而是直接竞争锁。
另外,不管是公平锁还是非公平锁,一旦没竞争到锁,都会进行排队,当锁释放时,都是唤醒排在最前面的线程,所以非公平锁只是体现在了线程加锁阶段,而没有体现在线程被唤醒阶段,ReentrantLock是可重入锁,不管是公平锁还是非公平锁都是可以重入的。(可重入:同一个线程里可以连续加同一把锁)
7.JDK、JRE、JVM之间的区别
JDK:java标准开发包,它提供了编译、运行 java程序所需的各种工具和资源,包括 Java编译器、java运行时环境,以及常用的java类库等
JRE:java运行环境,用于运行java的字节码文件。JRE种包括了JVM以及JVM工作所需要的类库,普通用户只需要安装JRE来运行Java程序,而程序的开发者必须安装JDK来编译、调试程序
JVM:java虚拟机,是JRE的一部分,它是整个java实现跨平台的最核心的部分,负责运行字节码文件
1.我们写java代码,用txt就可以写,但是写出来的java代码,想要运行,需要先编译成字节码,那就需要编译器javac,编译之后的字节码,想要运行,就需要一个可以执行字节码的程序,这个程序就是JVM,专门用来执行java字节码的。
2.如果我们要开发java程序,那就需要JDK,因为要编译java源文件
3.如果我们想运行已经编译好的java字节码,也就是*.class文件,那么就只需要JRE
4.JDK中包含了JRE,JRE中包含了JVM
5.另外,JVM在执行java字节码时,需要把字节码解释为机器指令,而不同操作系统的机器指令是有可能不一样的,所以就导致不同操作系统上的JVM是不一样的,所以我们在安装JDK是需要选择操作系统
6.另外,JVM是用来执行java字节码的,所以凡是某个代码编译之后是java字节码,那就能在JVM上运行,比如Apache,Groovy,Scala and Kotlin等
8.hashcode()与equals()之间的关系
在java中,每个对象都可以调用自己的hashcode()方法得到自己的哈希值(hashcode),相当于对象的指纹信息,通常来说世界上没有完全相同的两个指纹,但是在java中做不到这么绝对,但是我们仍然可以通过hashCode来做一些判断,比如:
1.如果两个对象的hashcode不相同,那么两个对象肯定是不同的
2.如果两个对象的hashcode相同,不代表这两个对象一定是同一个对象,也可能是两个对象
3.如果两个对象相等,那么他们的hashcode就一定相同
在java的一些集合类的实现中,比较两个对象是否相等时,会根据上面的原则,先调用对象的hashcode()方法,得到hashcode进行比较,如果hashcode不相同,就直接认为这两个对象不同,如果hashcode相同,那么就会进一步调用equals()方法进行比较。而equals()方法,就是用来最终确定两个对象是不是相等的,通常equals()的实现会比较重,逻辑比较多,而hashcode()主要就是得到一个哈希值,实际上就是一个数字,相对而言比较轻,所以在比较两个对象时,通常都会先根据hashcode相比较一下。
所以我们需要注意,如果我们重写了equals()方法,那么就要注意hashcode方法
9.String、StringBuffer、StringBuilder的区别
1.String是不可变的,如果尝试去修改,会新生成一个字符串对象,StringBuffer和StringBuild是可变的
2.StringBuffer是线程安全的,StringBuilder是线程不安全的,所以在单线程环境下StringBuilder效率更高
10.泛型中extends和super的区别
1.<? extends T>表示包括T在内的任何T的子类
2.<? super T>表示包括T在内的任何T的父类
11.==和equals方法的区别
1.==:如果是基本数据类型,比较值。如果是引用数据类型,比较的是引用地址
2.equals:具体看各个类重写equals方法之后的比较逻辑,比如string类,虽然是引用类型,但是string类中重写了equals方法,方法内部比较的是字符串中的各个字符是否全部相等
12.重载和重写的区别
重载:发生在同一个类中,方法名必须相同,参数类型不同,个数不同,顺序不同都属于重载,方法返回值和访问修饰符可以不同,发生在编译时。
重写:发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为private则子类就不能重写该方法。(ps:不属于重载)

13.List和Set的区别
List:有序,按对象进入的顺序保存对象,可重复,允许多个Null元素对象,可以使用Iterator(迭代器)取出所有元素,再逐一遍历,还可以使用get(int index)获取指定下标的元素
Set:无序,不可重复,最多允许有一个Null元素对象,取元素时只能用Iterator接口取得所有元素,再逐一遍历各个元素
14.ArrayList和LinkdeList区别
1.首先,他们的底层数据结构不同,ArrayList底层是基于数组实现的,LinkedList底层是基于链表实现的
2.由于底层数据结构不同,他们所使用的场景也不同,ArrayList更适合随即查找。LinkedList更适合删除和添加,查询、添加、删除的时间复杂度不同
3.ArrayList和LinkedList都实现了List接口,但是LinkedList还额外实现了Deque接口(对头尾进行操作),所以LinkedList还可以当作队列来使用

15.JDK1.7到JDK1.8HashMap发生了什么变化(底层)?
1.1.7中底层是数组+链表,1.8中底层是数组+链表+红黑树,加红黑树的目的是提高HashMap插入和查询整体效率
2.1.7中链表插入使用的是头插法,1.8中链表插入使用的是尾插法,因为1.8中插入key和value时需要判断链表元素个数,所以需要遍历链表统计链表元素个数,所以直接使用尾插法
3.1.7中哈希算法比较复杂,存在各种右移与异或运算。1.8中进行了简化,因为复杂的哈希算法目的就是提高散列性,来提供HashMap的整体效率,而1.8中新增了红黑树,所以可以适当的简化哈希算法,节省CPU资源
16.深拷贝和浅拷贝
深拷贝和浅拷贝就是指对象的拷贝,一个对象的数据类型有基本数据类型和引用数据类型。
1.浅拷贝是指,只会拷贝基本数据类型的值,以及引用数据类型的引用地址,并不会复制一份引用地址所指向的对象,也就是浅拷贝出来的对象,内部的类属性指向的是同一个对象
2.深拷贝是指,即会拷贝基本数据类型的值,也会针对实例对象的引用地址所指向的对象进行复制,深拷贝出来的对象,内部的属性指向的不是同一个对象
17.CopyOnWriteArrayList的底层原理是怎么样的
1.首先CopyOnWriteArrayList的内部也是用数组来实现的,再向CopyOnWriteArrayList添加元素时,会复制一个新的数组,写操作在新数组上进行
2.并且,写操作时会加锁,防止出现并发写入丢失的数据
3.写操作结束后会把原数组指向新数组
4.CopyOnWriteArrayList允许在写操作时来读取数据,大大提高了读的性能,因此适合读多写少的应用场景,但是CopyOnWriteArrayList会比较占内存,同时可能读到的数据不是实时最新的数据,所以不适合实时性要求很高的场景
18.什么是字节码?采用字节码的好处是什么?
编译器(javac)将源文件(.java)文件编译成字节码文件(✳.class),可以做到一次编译到处运行,windows上编译好的class文件,可以直接在linux上运行,通过这种方式做到跨平台。不过java的跨平台有一个前提条件,就是不同的操作系统上安装的JDK或JRE是不一样的,虽然字节码是通用的,但是需要把字节码解释成各个操作系统的机器码是需要不同的解释器的,所以针对各个操作系统需要有各自的JDK、JRE
采用字节码的好处,一方面实现了跨平台,另一方面也提高了代码执行的性能,编译器在编译源代码时可以做一些编译期的优化,比如锁消除、标量替换、方法内联等
19.Java中的异常体系是怎么样的
1.java中的所有异常都来自顶级父类Throwble
2.Throwble下面有两个子类Exception和Error
3.Error表示非常严重的错误,比如java.lang.StackOverFlowError(栈溢出错误)和java.lang.OutOfMemoryError(内存不足错误),通常这些错误出现时,仅仅想靠程序自己是解决不了的,可能是虚拟机、磁盘、操作系统层面出现的问题,所以通常不建议在代码中去获取这些Error,因为捕获的意义不大,程序可能已经不能运行了
4.Exception的子类通常又可以分为RuntimeException和 非RuntimeException 两类
5.RunTimeException表示运行期异常,这个异常是在代码运行过程中抛出的,这些异常是非检查异常,程序中可以选择捕获处理,也可以不处理,运⾏时异常的特点是Java编译器不会检查它,即使没有⽤try-catch语句捕获它,也没有⽤throws⼦句声明抛出它,也会编译通过。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生,例如NullPointerException(空指针)、IllengalAccessException(访问权限异常)、IndexOutOfBoundsException(越界异常)等
6.非RunTimeException表示非运行期异常,也就是我们常说的检查异常,是必须进行处理的异常,如果不处理,程序就不能检查异常通过。如IOException、SQLException等以及用户自定义的Exception
20.在java的异常处理机制中,什么时候应该抛出异常,什么时候捕获异常?
我们在写一个方法时,我们需要考虑的就是,本方法能否合理的处理该异常,如果处理不了,就继续向上抛出异常。包括本方法中在调用另外一个方法时,发现出现了异常,如果这个异常应该由自己处理,那就捕获该异常并进行处理
21.Java中有哪些类加载器
JDK自带有三个类加载器:BootstrapClassLoder、ExtClassLoader、AppClassLoader
1.BootStrapClassLoader是ExtClassLoader的父类加载器,默认负责加载%JAVA_HOME%lib下的jar包和class文件
2.ExtClassLoader是AppClassLoader的父类加载器,负责加载%JAVA_HOME%/libext文件夹下的jar包和class类
3.AppClassLoader是自定义类加载器的父类,负责加载classpath下的类文件
22.你们项目如何排查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.然后再进行详细的分析和调试
调优不是一蹴而就的,需要分析、推理、实践、总结、再分析,最终定位到具体的问题
23.一个对象从加载到JVM,再到被GC清除,都经历了什么?
1.首先把字节码文件内容加载到方法区
2.然后再根据类信息在堆区创建对象
3.对象首先会分配在堆区中年轻代的Eden区,经过一次Minor GC后,对象如果存活,就会进入Suvivor区。在后续的每次Minor GC中,如果对象一直存活,就会在Suvivor区来回拷贝,每移动一次,年龄+1
4.当年龄超过15后,对象依然存活,对象就会进入老年代
5.如果经过FullGC,被标记为垃圾对象,那么就会被GC线程清理掉
24.怎么确定一个对象到底是不是垃圾?
1.引用计数算法:这种方法是给堆内存当中的每个对象记录一个引用个数。引用个数对象为0的就认为是垃圾,这是早期JDK中使用的方式,引用基数无法解决循环引用的问题
2.可达性算法:这种方法是在内存中,从根对象向下一直找引用,找到的对象就不是垃圾,没找到的对象就是垃圾
25.JVM有哪些垃圾回收算法?
1.标记清除算法:
a.标记阶段:把垃圾内存标记出来
b.清除阶段:直接将垃圾内存回收
c.这种算法是比较简单的,但是有个很严重的问题,就是会产生大量的内存碎片
2.复制算法:为了解决标记清除算法的内存碎片问题,就产生了复制算法。复制算法将内存分为大小相等的两半,每次只使用其中一半。垃圾回收时,将当前这一块的存活对象全部拷贝到另一半,然后当前这一半内存就可以直接清除。这种算法没有内存碎片,但是他的问题就在于浪费空间。而且,他的效率跟存活对象的个数有关。
3.标记压缩算法:为了解决复制算法的缺陷,就提出了标记压缩算法。这种算法在标记阶段跟标记清除算法是一样的,但是在完成标记后,不是直接清理垃圾内存,而是将存活对象往一端移动,然后将边界以外的所有内存直接清除。
26.什么是STW?
STW:Stop-The-World,是在垃圾回收算法执行过程当中,需要将JVM内存冻结的一种状态。在STW状态下,JAVA的所有线程都是停止执行-GC(垃圾回收)线程除外,native方法可以执行,但是不能于JVM交互。GC各种算法优化的重点,就是减少STW,同时这也是JVM调优的重点。
27.常用JVM参数有哪些?
#JVM启动参数不换行
#设置堆内存
-Xmx4g -Xms4g
(堆空间的最大值是4g)
(堆空间的初始值/最小值4g)
#指定GC算法
-XX:+UseG1GC -XX:MaxGCPauseMillis=50
(指定G1的垃圾回收器,+开启的意思)
(最大的垃圾回收暂停时间 ms)
#指定GC并行线程数
-XX:ParallelGCThreads=4
#打印GC日志
1.-XX:+PrintGCDetails -XX:+PrintGCDateStamps
(打印日志详情/打印日志时间戳)
2.-XXPrintGC
(打印简约日志)
#指定GC日志文件
-Xloggc:gc.log
#指定Meta区的最大值
-XX:MaMetaspaceSize=2g
##设置单个线程栈的大小
-Xss1m
##指定堆内存溢出时进行自动的Dump
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/user/local/
28.说说对线程安全的理解
29.对守护线程的理解
线程分为用户线程和守护线程,用户线程就是普通线程,守护线程就是JVM的后台线程。比如垃圾回收线程就是一个守护线程,守护线程会在其他普通线程都停止运行之后自动关闭。我们通过设置thread.setDaemon(true)来把一个线程设置为守护线
30.并发、并行、串行之间的区别
1.串行:一个任务执行完,才能执行下一个任务
2.并行(Parallelism):两个任务同时执行
3.并发(Concurrency):两个任务看上去是同时执行,在底层,两个任务被拆成很多份,然后一个一个执行,站在更高的角度来看两个任务是同时在执行的
31.Java死锁如何避免?
造成死锁的几个原因:
1.一个资源每次只能被一个线程使用
2.一个线程在阻塞等待某个资源时,不释放已占有资源
3.一个线程已经获得的资源,在未使用完之前,不能被强行剥夺
4.若干线程形成头尾相接的循环等待资源关系
这是造成死锁必须要达到的4个条件,如果要避免死锁,只需要不满足其中的一个条件即可。而其中前3个条件是作为锁要符合的条件,所以要避免死锁就需要打破第4个条件,不出现循环等待锁的关系
在开发过程中:
1.要注意加锁顺序,保证每个线程按照同样的顺序加锁
2.要注意加锁时限,可以针对锁设置一个超时时间
3.要注意死锁检查,这是一种预防机制,确保在第一时间发现死锁并解决(例如:jstack)
32.线程池的底层工作原理
线程池内部是通过队列+线程实现的,当我们利用线程池执行任务时:
1.如果poolSize(线程池中当前线程的数量)< corePoolSize(线程池的基本大小),即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
2.如果poolSize=corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。
3.如果阻塞队列的容量达到上限,且这时poolSize<maximumPoolSize,新增线程来处理任务。
4.如果阻塞队列满了,且poolSize=maximumPoolSize,那么线程池已经达到极限,会根据饱和策略RejectedExecutionHandler拒绝新的任务。
33.线程池为什么是先添加队列而不是先创建最大线程?
当线程池中的核心线程都在忙时,如果继续往线程池中添加任务,那么任务会先放入队列,队列满了之后,才会新开线程。这就相当于,一个公司本来有10个程序员,本来这10个程序员能正常的处理各种需求,但是随着公司的发展,需求在慢慢的增加,但是一开始这些需求只会增加在开发列表中,然后这10个程序员从待开发表中获取需求并进行处理,但是某一天待开发列表满了,公司发现现有的10个程序员处理不过来,开始招聘新员工。
34.ReentrantLock中tryLock()和lock()方法的区别
1.tryLock():(非阻塞加锁)表示尝试加锁,可能加到,也可能加不到,该方法不会阻塞线程,如果加到锁则返回true,没有加到则返回false
2.lock():表示阻塞加锁,线程会阻塞直到加到锁,方法没有返回值
35.CountDownLatch和Semaphore的区别和底层原理
CountDownLatch表示计数器,可以给CountDownLatch设置一个数字,一个线程调用CountDownLatch的await()将会阻塞,其他线程可以调用CountDownLatch的countDown()方法来对CountDownLatch中的数字减一,当数字被减成0后,所以await的线程都将被唤醒。对应的底层原理就是,调用await()方法的线程会利用AQS排队,一旦数字被减为0,则会将AQS中排队的线程依次唤醒。
Semaphore表示信号量,可以设置许可的个数,表示同时允许最多多少个线程使用该信号量,通过acquire()来获取许可,如果获取不到,则线程堵塞,并通过AQS来排队,可以通过release()方法来释放许可,当某个线程释放某个许可后,会从AQS正在排队的第一个线程开始依次唤醒,直到没有空闲许可
36.Sychronized的偏向锁、轻量级锁、重量级锁
1.偏向锁:在锁对象的对象头中记录一下当前获取到该锁的线程ID,该线程下次如果又来获取锁就可以直接获取到
2.轻量级锁:由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时如果有第二个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分。轻量级锁底层是通过自旋来实现的,并不会阻塞线程。
3.重量级锁:如果自旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞
4.自旋锁:自旋锁就是线程在获取锁的过程中,不会去阻塞线程,自旋锁是线程通过CAS获取预期的一个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程一直在运行中,相对而言没有使用太多操作系统资源,比较轻量
37.谈谈你对AQS的理解,AQS如何实现可重入锁?
1.AQS是一个java线程同步的框架。是JDK中很多锁工具的核心实现框架
2.在AQS中,维护了一个信号量state和一个线程组成的双向链表队列。其中,这个线程队列就是用来给线程排队的,而state就像一个红绿灯,用来控制线程排队或者放行的。在不同的场景下,有不同的意义。
3.在可重入锁这个场景下,state就用来表示加锁的次数。0标识无锁,每加一次锁,state就加1.释放锁state就减1
38.谈谈你对IOC的理解
IOC表示控制反转(待记录)
39.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的操作一样,如果当前没有事务,则自己新建一个事务(开启一个事务)
40.Spring事务什么时候会失效?
Spring事务的原理是AOP,进行了切面增强,那么失效的根本原因就是这个AOP不起作用,常见情况有以下几种
1.发生自调用,类里面使用this调用本类的方法(this通常省略),此时这个this对象不是代理类,而是UserService对象本身
2.方法不是public的:@Transation只能用于public的方法上,否则事务不会生效,如果要用在非public方法上,可以开启AspectJ代理模式。
3.数据库不支持事务
4.没有被Spring管理
5.异常被吃掉,事务不会回滚(或者抛出的异常没有被定义,默认为RuntimeException)
41.Spring中的Bean是安全的吗?
Spring本身并没有针对Bean做线程安全的处理,所以:
1.如果Bean是无状态的,那么Bean则是线程安全的
2.如果Bean是有状态的,那么Bean则不是线程安全的
另外,Bean是不是线程安全,跟Bean的作用域没有关系,Bean的作用域只是表示Bean的生命周期范围,对于任何生命周期的Bean都是一个对象,这个对象是不是线程安全的,还得看这个Bean对象本身
42.Spring中的Bean创建生命周期有哪些步骤
43.ApplicationContext和BeanFactory有什么区别
BeanFactory是Spring中非常核心的组件,表示Bean工厂,可以生成Bean,维护Bean,而ApplicationContext继承了BeanFactory,所以ApplicationContext拥有BeanFactory所有的特点,也是一个Bean工厂,但是ApplicationContext除了继承BeanFactory之外,还继承了EnvironmentCapable、MessageSource等接口,从而ApplicationContext还有获取系统环境变量、国际化、事件发布等功能,这是BeanFactory所不具备的。
44.Spring中的事务是如何实现的
1.Spring事务底层是基于数据库事务和AOP机制的
2.首先对于使用了@Transaction注解的Bean,Spring会创建一个代理对象作为Bean
3.当调用代理对象的方法时,会先判断该方法上是否加了@Transaction注解
4.如果加了,那么则利用事务管理器创建一个数据库连接
5.并且修改数据库连接的autocommit属性为false,禁止此连接的自动提交,这是实现Spring事务非常重要的一步
6.然后执行当前方法,方法中会执行sql
7.执行完当前方法后,如果没有出现异常就直接提交事务
8.如果出现了异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务
9.Spring事务的隔离级别对应的就是数据库的隔离级别
10.Spring事务的传播机制是Spring事务自己实现的,也是Spring事务中最复杂的
11.Spring事务的传播机制就是基于数据库连接来做的,一个数据库一个事务,如果传播机制配置为需要新开一个事务,那么实际上就是先建立一个数据库连接,在此新数据库连接上执行sql
45.Spring中什么时候@Transactional注解会失效
因为Spring事务是基于代理来实现的,所以某个加了@Transaction的方法只有是被代理对象调用时,这个注解才会生效,所以被代理对象调用这个方法,@Transaction是不会失效的。
如果某个方法是private的,那么@Transaction也会失效,因为底层cglib是基于父子类来实现的,子类是不能重载父类的private方法的,所以无法很好的利用代理,也会导致@Transaction失效。
46.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的扫描就是通过BeanFactoryPostProcessor来实现的,依赖注入就是通过BeanPostProcessor来实现的
8.在Spring启动过程中还会去处理@Import等注解
47.Spring中用到了哪些设计模式
48.SpringBoot中常用注解及其底层实现
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
49.SpringBoot是如何启动Tomcat的
1.首先,SpringBoot在启动时会先创建一个Spring容器
2.在创建Spring容器过程中,会利用@ConditionalOnClass技术来判断当前classpath中是否存在Tomcat依赖,如果存在则会生成一个启动Tomcat的Bean
3.Spring容器创建完成后,就会获取启动Tomcat的Bean,并创建Tomcat对象,并绑定端口,然后启动Tomcat
50.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语句依赖于数据库,导致数据库移植性差,不能随意更换数据库
51.#{}和${}的区别是什么?
52.索引的基本原理
索引用来快速的寻找那些具有特定值的记录。如果没有索引,一般来说执行查询时遍历整张表。
索引的原理:就是把无需的数据变成有序的查询
1.把创建了索引的列的内容进行排序
2.对排序结果生成倒排表
3.在倒排表内容上拼上数据地址链
4.在查询的时候,先拿到倒排表内容,再取出数据地址链,从而拿到具体数据
53.索引的设计原则
首先查询更快,占用空间更小
1.适合索引的列是出现在where子句中的列,或者连接子句中指定的列
2.基数较小的表,索引效果较差,没必要在此列建立索引
3.使用短索引,如果对长字符串列进行索引,应该指定一个前缀长度,这样能够节省大量索引空间,如果搜索词超过索引前缀长度,则使用索引排除不匹配的行,然后检查其余行是否可能匹配
4.不要过度索引。索引需要额外的磁盘空间,并降低写操作的性能。在修改表内容的时候,索引会进行更新甚至重构,索引列越多,这个时间就会越长。所以只保持需要的索引有利于查询即可。
5.定义有外键的数据列一定要建立索引
6.更新频繁字段不适合创建索引
7.若是不能有效区分数据的列不适合做索引列(例如男女未知,最多也就三种,区分度太低)
8.尽量的拓展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可
9.对于查询中很少涉及的列,重复值比较多的列不要建立索引
10.对于定义为text、image喝bit的数据类型的列不要建立索引
54.事务的基本特性和隔离级别
事务的基本特性(ACID)分别是:
原子性:一个事务中的操作要么全部成功,要么全部失败
一致性:数据库从一个一致性的状态转换到另外一个一致性的状态。比如a转账给b100,假设a只有90,支付之前我们数据库里的数据都是符合约束的,但是如果执行成功了,我们数据库的数据就破坏约束了,因此事务不能成功,这里我们说事务提供了一致性的保证
隔离性:一个事务的修改在最终提交前,对其他事务是不可见的
持久性:一旦事务提交,所做的修改就会永久保存到数据库中
隔离性有四个级别,分别是:
read uncommit :读未提交,可能会读到其他事务未提交的数据,也叫做脏读。用户本来应该读取到id=1的用户age应该是10,结果读取到了其他事务还没有提交的事务age=20,这就是脏读。
read commited:读已提交,两次读取结果不一致,叫做不可重复读。不可重复读解决了脏读的问题,他只会读取已经提交的事务。用户开启事务读取id=1用户,查询到age=10,再次读取发现结果age=20,在同一个事务里同一个查询读取到不同的结果叫做不可重复读。
repeatable read:可重复复读,这是mysql的默认级别,就是每次读取结果都一样,但是可能产生幻读。
serializable:串行,一般是不会使用的,他会给每一行的数据加锁,会导致大量超时和锁竞争的问题
55.什么是MVCC
MVCC(多版本并发控制):MVCC最大的优势:读不加锁,读写不冲突,极大的增加了系统的并发性能。
一般是在使用读已提交(READ COMMITTED)和可重复读(REPEATABLE READ)隔离级别的事务中实现。准确的说,MVCC多版本并发控制指的是 “维持一个数据的多个版本,使得读写操作没有冲突” 这么一个概念。
56.简述MyISAM和InnoDB两个存储引擎的区别
区别:
- InnoDB支持事务,MyISAM不支持,对于InnoDB每一条SQL语言都默认封装成事务,自动提交,这样会影响速度,所以最好把多条SQL语言放在begin和commit之间,组成一个事务;
- InnoDB支持外键,而MyISAM不支持。对一个包含外键的InnoDB表转为MYISAM会失败;
- InnoDB是聚集索引,使用B+Tree作为索引结构,数据文件是和(主键)索引绑在一起的(表数据文件本身就是按B+Tree组织的一个索引结构),必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。MyISAM是非聚集索引,也是使用B+Tree作为索引结构,索引和数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的
- InnoDB不存储总行数,MyISAM存储表的总行数
- 如果表中绝大多数都只是读查询,可以考虑MyISAM,如果既有读也有写,使用InnoDB
Explain语句结果中各个字段分别表示什么

索引覆盖是什么
索引覆盖就是一个SQL在执行时,可以利用索引来快速查找,并且此SQL所要查询的字段在当前索引对应的字段中都包含了,那么就表示此SQL走完索引后不用回表了,所需要的字段都在当前索引的叶子节点上存在,可直接作为结果返回了
最左前缀原则是什么
即最左优先,当一个SQL想要利用索引时,就一定要提供该索引所对应的字段中最左边的字段,也就是排在最前面的字段,比如针对a,b,c三个字段建立了一个联合索引,那么在写一个sql时就一定要提供a字段的条件,这样才能用到联合索引,这是由于在建立a,b,c三个字段的联合索引时,底层的B+树是按照a,b,c三个字段从左到右取比较大小进行排序的,所以如果想要利用B+树进行快速查找也得符合这个规则
Innodb是如何实现事务的
Innodb通过Buffer Pool,LogBuffer,Redo Log,Undo Log来实现事务,以一个update语句为例:
1.Innodb在收到一个update语句后
860

被折叠的 条评论
为什么被折叠?



