Java中有哪几种方式来创建线程执行任务

继承Thread类

1
2
3
4
5
6
7
8
9
10
11
public class myThread extends Thread{
public static void main(String[] args) {
myThread myThread = new myThread();
myThread.start();
}

@Override
public void run(){
System.out.println("Hello DrownedFish!");
}
}

重写了run方法,而不是start方法。但是占用了继承名额,Java中类是单继承的。

实现Runnable接口

1
2
3
4
5
6
7
8
9
10
public class myThread implements Runnable{
public static void main(String[] args) {
Thread thread = new Thread(new myThread());
thread.start();
}

public void run(){
System.out.println("Hello DrownedFIsh!");
}
}

实现runnable接口,实现run()方法,依然用到Thread,这种方式更常用。

$\color{red}注意:$ Thread实际上也是实现了Runnable接口。

匿名内部类方法:

1
2
3
4
5
6
7
8
9
public static void main(String[]args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello DrownedFIsh!");
}
});
thread.start();
}

看下图,因为Runnable是一个函数式接口,所以可以使用lambda表达式。

1
2
3
4
public static void main(String[] args) {
Thread thread = new Thread(() -> System.out.println("Hello DrownedFIsh!"));
thread.start();
}

实现Callable接口

1
2
3
4
5
6
7
8
9
10
11
12
13
public class myThread implements Callable<String> {
public static void main(String[] args) throws ExecutionException, InternalError, InterruptedException {
FutureTask<String> futureTask = new FutureTask<>(new myThread());
Thread thread = new Thread(futureTask);
thread.start();
String result = futureTask.get();
System.out.println(result);
}

public String call(){
return "Hello DrownedFish!";
}
}

使用Thread+FutureTask配合,这种方式支持拿到异步执行的结果。

1
public class FutureTask<T> implements RunnableFuture<T>
1
>public interface >RunnableFuture<V> extends >Runnable, Future<V>

FutureTask实现了RunnableFuture接口,而RunnableFuture接口继承了Runnable接口和Future接口。所以FutureTask实际上也是一个Runnable。(注意,java类和类是单继承,而接口和接口可以多继承)

用线程池来创建线程

1
2
3
4
5
6
7
8
9
public class myThread implements Runnable {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
executorService.execute(new myThread());
}
public void run(){
System.out.println("Hello DrownedFish!");
}
}

实现callable或Runnable接口都可以,由ExecutorService来创建线程。

注意:工作中不建议使用Executors来创建线程。

总结:以上几种方式,都是基于Runnable。

为什么工作中不建议使用Executors来创建线程。

newFixedThreadPool

当我们使用Executor创建FixedThreadPool时,对应的构造方法为:

1
2
3
4
5
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}

发现创建的队列为LinkedBlockingQueue,是一个无界阻塞队列,如果使用该线程池执行任务,任务过多就会不断的添加到队列中,任务越多占用的内存就越多,最终可能耗尽内存,导致OOM(内存用完了)。

newSingleThreadExecutor

使用Exetutors创建newSingleThreadExecutor时,对应的构造方法为:

1
2
3
4
5
6
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(11
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}

也是LinkedBlockingQueue。
除了可能造成OOM外,使用Exetutors来创建线程池也不能自定义名字,不利于排查问题。所以建议直接使用ThreadPoolExetutor来定义线程池,这样可以灵活控制。

线程池有哪几种状态?每种状态分别表示什么?

RUNNING

表示线程池正常运行,既能接受新任务,也会正常出来队列中的任务。

SHUTDOWN

当调用线程池的shutdown()方法时,线程池就进入SHUTDOWN状态,表示线程池处于关闭状态,此状态下线程池不会接受新任务,但是会继续把队列中的任务处理完

STOP

当调用线程池的shutdownnow()方法时,线程池就进入STOP状态,表示线程池处于正在停滞状态,此状态下线程池既不会接受新任务,也不会处理队列中的任务,并且正在运行的线程也会被中断。

TIDYING

线程池中没有线程在运行后,线程池的状态就会自动变成TIDYING,并且会调用terminated(),该方法是空方法,留给程序员进行扩展。

TERMINATED

terminated()方法执行完之后,线程池状态就会变为TERMINATED

Synchronized和ReentrantLock有哪些不同点?

Synchronized ReentrantLock
Java中的一个关键字 JDK提供的一个类
自动加锁与释放锁 需要手动加锁与释放锁
jvm层面的锁 API层面的锁
非公平锁 公平锁或非公平锁
锁的是对象,锁信息保存在对象中 int类型的state标识来标识锁的状态
底层有锁升级过程 没有锁升级过程

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对象。

    1
    2
    3
    4
    5
    6
    7
    8
    public class myThread {
    private ThreadLocal<String> local = new ThreadLocal<>();

    public void doTask(){
    local.set("DrownedFish");
    local.remove();
    }
    }
  4. ThreadLocal经典的应用场景就是连接管理。(一个线程持有一个连接,该连接对象可以在不同的方法之间进行传递,线程之间不共享同一连接)

ReentrantLock分为公平锁和非公平锁,那底层是如何实现的?

它们的底层实现都会使用AQS(一个用来构建锁和同步器的框架)来排队,它们的区别在于使用lock()方法加锁时:

  1. 如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果用线程在排队,则当前线程也进行排队。

  2. 如果是非公平锁,则不会去检查是否有线程在排队,而是直接竞争锁。

    TIP:AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。 CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。 AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。

    参考:通俗易懂的AQS

    另外,无论是公平锁还是非公平锁,一旦没竞争到锁,都会进行排队,锁释放时,都是唤醒排在最前面的线程,所以非公平锁只体现在了线程加锁阶段,而没有体现在线程被唤醒阶段,ReentrantLock是可重入锁,不管是公平锁还是非公平锁都是可重入的

Synchronized的锁升级过程是怎样的?

  1. 偏向锁:在锁对象的对象头中记录一下当前获取到该锁的锁的线程ID,该线程下次如果又来获取该锁就可以直接获取到,也就是支持锁重入
  2. 轻量级锁:由偏向锁升级而来。当一个线程获取到锁后,此时这把锁是偏向锁,如果有第二个线程竞争锁,偏向锁就会升级为轻量级锁。轻量级锁底层通过自旋来实现,并不会阻塞线程。
  3. 如果自旋次数过多仍没有回去到锁,则升级为重量级锁,重量级锁会导致线程阻塞。
  4. 自旋锁:自旋锁就是在线程获取锁的过程中,不会去阻塞线程,也就是所谓的唤醒线程,阻塞和唤醒这两部都是需要操作系统去进行的,比较耗时间。自旋锁是线程通过CAS获取预期的一个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程一直在运行,相对而言没有使用太多操作系统资源,比较轻量。

Tomcat中为什么要使用自定义类加载器?

一个Tomcat中可以部署多个应用,而每个应用中都存在很多类,并且各个应用中的类是独立的,全类名是可以相同的。一个Tomcat,不管内部部署了多少个应用,Tomcat启动后就是一个Java进程,也就是一个JVM,所以如果Tomcat中只存在一个类加载器,比如默认的AppClassLoader,那么就只能加载一个类,这是有问题的。而在Tomcat中,会为部署的每个应用都生产一个类加载器实例,名字叫WebAppClassLoader,这样Tomcat中每个应用就可以使用自己的类加载器去价值自己的类,从而达到应用之间的类隔离,不出现冲突。另外Tomcat还利用自定义类加载器实现了热加载功能。

JDK、JRE、JVM之间的区别

  • JDK(Java SE Development Kit):Java标准开发包,提供编译、运行Java程序所需的各种工具和资源,包括Java编译器、Java运行时环境,以及常用的Java类库等。
  • JRE(Java Runtime Environment):Java运行环境,用于运行Java字节码文件。JRE中包括了JVM以及JVM工作需要的类库,普通用户只需要安装JRE来运行JAVA程序。
  • JVM(Java Virtual Mechinal):Java虚拟机,是JRE的一部分,它是整个Java实现跨平台的核心。负责运行字节码文件。

hashCode()与equals()之间的关系

在Java中,每个对象都可以调用自己的hashCode()方法来得到自己的哈希值。

  • 如果两个对象的hashCode不同,那么这两个对象肯定不同。
  • 如果两个对象的hashCode相同,不代表这两个对象一定是同一个对象。
  • 如果两个对象相等,那么它们的hashCode就一定相同。

在Java的一些集合类的实现中,比较两个对象是否相同时,会根据上面的原则,先调用对象的hashCode()方法,如果hashCode不同,就直接认为对象不同。反之,进一步调用equals()方法,而equals()方法的实现逻辑比较多。

所以重写equals方法,就要注意hashCode方法。

String、StringBuffer、StringBuilder的区别

  1. String是不可变的,如果尝试修改,会生成一个新的字符串对象。StringBuffer、StringBuilder是可变的。
  2. StringBuffer是线程安全的,StringBuilder是线程不安全的,所以在单线程环境下StringBuilder效率会更高。

泛型中extends和super的区别

  1. < ? extends T>表示包括T在内的任何T的子类
  2. < ? super T>表示包括T在内的任何T的父类

== 和equals方法的区别

  • ==:对于基本数据类型,比较的是值。对于引用类型,比较的是地址。
  • equals:具体看各个类重写equals方法之后的比较逻辑。

重写和重载的区别

  • 重写(override)是子类对父类方法的重新定义,改变了方法的实现,但方法签名(名称和参数列表)保持不变。
  • 重载(overload)是同一类中多个方法名相同但参数列表不同的方法,允许使用不同的参数来调用不同的实现。重写关注的是继承关系中的方法替换,而重载关注的是方法多态性在同一类中的实现。

List和Set的区别

  • List:有序,按对象插入的顺序确保对象,可重复,允许多个Null元素对象,可以使用Iterator取出所有元素,再逐一遍历,还可以使用get(int index)获取指定下标的元素。
  • Set:无序,不可重复,最多允许有一个Null元素对象,取元素时只能用Iterator接口获取所有元素,再逐一便利各个元素。

ArrayList和LinkedList的区别

  1. 首先,他们的底层数据结构不同,ArrayList底层是基于数组实现的,LinkedList底层是基于链表实现的。
  2. ArrayList适合随机查找,LinkedList适合删除和添加,、查询、添加、删除的时间复杂度不同。
  3. 另外ArrayList和LinkedList都实现了List、
    接口,但LinkedList还额外实现了Deque接口,所以LinkedList还可以当作队列来使用。

ConcurrentHashMap的扩容机制

1.7版本

  1. 基于Segment分段实现。
  2. 每个Segment相对于一个小型的HashMap。每个Segment内部会进行扩容,和HashMap扩容逻辑类似。
  3. 先生成新的数组,然后转移元素到新数组中。
  4. 扩容的判断也是每个Segment内部单独判断,判断是否超过阈值。

1.8版本

  1. 不再基于Segment实现。
  2. 当某个线程put时,如果发现ConcurrentHashMap正在进行扩容,那么该线程一起进行扩容,
  3. 如果某个线程put时,发现没有正在进行扩容,则将key-value添加到ConcurrentHashMap中,然后判断是否超过阈值,超过了进行扩容。
  4. ConcurrentHashMap支持多线程同时扩容。扩容前先生成一个新数组,转移元素时将原数组分组,将每组分给不同的线程来进行元素的转移,每个线程负责一组或多组的元素转移工作。

JDK1.7到1.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方法

  1. 根据key通过哈希算法与运算得出数组下标。
  2. 如果数组下标元素空,则将key和value封装为对象(1.7是Entry对象,1.8是Node对象)并放入该位置。
  3. 如果数组下标位置元素非空,则
    a. 如果是1.7,判断是否需要扩容,如果不扩容则生成Entry对象,并使用头插法添加到当前位置的链表中。
    b. 如果是1.8,则会判断当前位置上的Node类型,查看是红黑树Node还是链表Node。
    i. 如果是红黑树Node,则将key和value封装为一个红黑树节点并添加到红黑树中,这个过程会判断红黑树中是否存在当前key,如果存在则更新value
    ii. 如果此位置上是Node对象的链表节点,则封装成链表Node并用尾插法插入到链表的最后位置。因为是尾插法,所以需要遍历链表,在遍历链表的过程中会判断是否存在当前key,存在则更新value,当遍历完链表后,将新链表Node插入到链表中,插入到链表后,会看当前链表的结点个数,如果大于等于8,则将该链表转换成红黑树。
    ii. 将key和value封装为Node插入到链表或红黑树中后,再判断是否需要进行扩容,如果需要,就不用结束PUT方法。

深拷贝和浅拷贝

深拷贝和浅拷贝是指对象的拷贝,一个对象中存在两种类型的属性,一种是基本数据类型,一种是实例对象的引用。

  1. 浅拷贝是指,只拷贝基本数据类型的值,以及实例对象的引用地址,并不会复制一份引用地址所指向的对象。
  2. 深拷贝是指,即会拷贝基本数据类型的值,也会针对实例对象的引用地址所指向的对象进行复制,深拷贝出来的对象,内部的属性指的不是同一个对象。

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会比较占内存,同时可能读到数据不是实时数据,所以不适合实时性要求很高的场景

什么是字节码?采用字节码的好处是什么?

编译器(javac)将源文件(*.java)文件编译为字节码文件(*.class),可以做到一次编译到处运行,windows上编译好的class文件,可以直接在linux上运行,通过这种方式做到跨平台,不过java的跨平台有一个前提条件,就是不同的操作系统上安装的JDK或JRE是不一样的,虽然字节码是通用的,但需要把字节码解释成各个操作系统的机器码是需要不同的解释器的。

采用字节码的好处,一方面实现了跨平台,另一方面提高了代码执行的性能,编译器在编译源代码时可以做一些编译期的优化,比如锁消除,标量替换,方法内联等。

Java中的异常体系是怎样的?

  • 所有异常都来自顶级父类Throwable
  • Throwable下有两个子类Exception和Error
  • Error表示严重错误,通常这些错误仅仅靠程序自己是解决不了的,可能是虚拟机,磁盘,操作系统层面出现的问题,所以通常不建议在代码中去捕获这些Error。
  • Exception的子类分为:RuntimeException和非RuntimeException。
  • RuntimeException:运行时异常,表示在代码运行中抛出,可以捕获处理,也可以不处理。一般是由程序逻辑错误引起,比如NullPointerException等。
  • 非RuntimeException:非运行时异常,必须处理的异常。不处理程序就不能检查异常通过。如IOException,SQLException等以及用户自定义的Exception。

Java的异常处理机制中,什么时候抛出异常,什么时候捕获异常?

异常相当于一种提示,如果我们抛出异常,就相当于告示上层,我抛出了一个异常,我处理不了,交给你来处理,而对于上层方法来说,也需要决定自己能不能处理这个异常,是否也需要交给它的上层。
所以我们在写一个方法时,我们需要考虑的就是,本方法能否合理的处理该异常,如果处理不了就继续向上抛出异常,包括本方法中在调用另外一个方法时,发现出现了异常,如果这个异常应该由自己处理,那就捕获该异常并进行处理。

Java中有哪些类加载器?

JDK自带的三个:

  1. BootStrapClassLoader:ExtClassLoder的父类加载器,默认负责加载%JAVA_HOME%/lib下的jar包和class文件。
  2. ExtClassLoader:AppClassLoader的父类加载器,负责加载%JAVA_HOME%/lib/ext文件夹下单jar包和class类。
  3. AppClassLoader:自定义加载器的父类,负责加载classPath下的类文件。

双亲委派模型:JVM在加载类时,会调用AppClassLoader的loadClass方法加载,该方法先使用ExtClassLoader的loadClass,同样,ExtClassLoader先使用BootStrapClassLoader。

JVM中哪些是线程共享区?

堆区和方法区是所有线程共享的,栈,本地方法栈,程序计数器是每个线程独有的。

项目如何排查JVM问题?

对于正常运行的系统:

  1. 用jmap来查看JVM中各个区域的使用情况。
  2. 用jstack来查看线程的运行情况,比如哪些线程阻塞,是否出现死锁。
  3. 用jstat来查看垃圾回收的情况,特别是fullgc,如果比较频繁。那么就得进行调优。
  4. 通过各个命令的结果,或者jvisualvm等工具来分析。
  5. 首先,初步猜测频繁发送fullgc的原因,如果频繁发生fullgc但一直没出现内存溢出,那么fullgc实际上是回收了很多对象,所以这些对象最好能在younggc过程中就直接回收掉,避免这些对象进入老年代,对于这种情,得考虑活时间不长的对象是不是比较大,导致年轻代放不下,直接进入老年代,尝试加大年轻代的大小,如果改完后,fullgc减少,则修改有效。
  6. 可以找到占用CPU最多的线程,定位到具体方法,优化它的执行,看能否避免某些对象的创建,从而节省内存。

对于已经OOM的系统:

  1. 一般生产系统中都会设置当系统OOM时,生成当时的dump文件。
  2. 可以利用jsisualvm等工具分析dump文件。
  3. 根据dump文件找到异常的实例对象,和异常的线程(CPU占用高),定位到具体的代码。
  4. 然后再进行详细的分析和调试。

一个对象从加载到JVM,再到被GC清除,都经历了什么?

  1. 首先把字节码文件内容加载到方法区。
  2. 根据类信息在堆区创建对象。
  3. 对象首先会分配在堆区中的年轻代的Eden区,经过一次Minor GC后,对象如果存活,就会进入Suvivor区。在后续每次Minor GC中,如果对象一直存活,就会在Suvivor区来回拷贝,每移动一次,年龄加1.
  4. 当年龄超过15年后,对象依然存活,对象就会进入老年代。
  5. 如果经过Full GC,被标记为垃圾对象,那么就会被GC线程清理掉。

怎么确定一个对象到底是不是垃圾?

  1. 引用计算法:这种方式是给堆内存当中的每个对象记录一个引用个数。引用个数为0的就认为是垃圾。这是早期JDK中使用的方式。引用计数无法解决循环引用的问题。
  2. 可达性算法:在内存中,从根对象向下一直找引用,找到的对象就不是垃圾,没找到的是垃圾。

JVM中有哪些垃圾回收算法?

  1. 标记清除算法:

    a. 标记阶段:把垃圾内存标记出来。

    b. 清除阶段:直接将垃圾内存回收。

    c. 这种算法是比较简单的,但是有个很严重的问题,就是会产生大量的内存碎片。
  2. 复制算法:为了解决标记清除算法的内存碎片问题,就产生了复制算法。复制算法将内存分为大小相等的两半,每次只使用其中一半。垃圾回收时,将当前这一块的存活对象全部拷贝到另一半,然后当前这一半内存就可以直接清除。这种算法没有内存碎片,但是他的问题就在于浪费空间。而且,他的效率跟存活对象的个数有关。
  3. 标记压缩算法:为了解决复制算法的缺陷,就提出了标记压缩算法。这种算法在标记阶段跟标记清除算法是一样的,但是在完成标记之后,不是直接清理垃圾内存,而是将存活对象往一端移动,然后将边界以外的所有内存直接清除。

什么是STW?

Stop-The-World,是在垃圾回收算法执行过程当中,需要将JVM内存冻结的一种状态。在STW状态下,JAVA的所有线程都是停止执行的-GC线程除外,native方法可以执行,但是,不能与JM交互。GC各种算法优化的重点,就是减少STW,同时这也是JVM调优的重点。

JVM参数有哪些?

JVM参数大致可以分为三类:

  1. 标注指令: -开头,这些是所有的HotSpot都支持的参数。可以用java-help打印出来。
  2. 非标准指令: -X开头,这些指令通常是跟特定的HotSpot版本对应的。可以用java -X打印出来。
  3. 不稳定参数: -XX开头,这一类参数是跟特定HotSpot版本对应的,并且变化非常大。

说说对线程安全的理解

线程安全指的是,我们写的某段代码,在多个线程同时执行这段代码时,不会产生混乱,依然能够得到正常的结果,比如i++,i初始化值为0,那么两个线程来同时执行这
行代码,如果代码是线程安全的,那么最终的结果应该就是一个线程的结果为1,一个线程的结果为2,如果出现了两个线程的结果都为1,则表示这段代码是线程不安全

所以线程安全,主要指的是一段代码在多个线程同时执行的情况下,能否得到正确的结果。

对守护线程的理解

线程分为用户线程和守护线程,用户线程就是普通线程,守护线程就是JVM的后台线程,比如垃圾回收线程就是一个守护线程,守护线程会在其他普通线程都停止运行之后自动关闭。我们可以通过设置thread.setDaemon(true)来把一个线程设置为守护线程。

ThreadLocal的底层原理

  1. ThreadLocal是Java中提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部。该线程可以在任意时刻。任意方法中获取缓存的数据。
  2. ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象中都存在一个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的值。
  3. 如果在线程池中使用ThreadLocal会造成内存泄漏,因为当THreadLocal对象使用完之后,应该要把设置的key,value,也就是Entry对象进行回收,但线程池中的线程不会回收,而线程对象是通过强引用指向ThreadLocal对象之后,手动调用ThreadLocal的remove方法,手动清除Entry对象。
  4. ThreadLocal经典的应用场景就是连接管理(一个线程持有一个连接,该连接对象可以在不同的方法之间进行传递,线程之间不共享同一连接)。

并发、并行、串行之间的区别

  1. 串行:一个任务执行完,才能执行下一个任务
  2. 并行(Parallelism):两个任务同时执行
  3. 并发(Concurrency):两个任务整体看上去是同时执行,在底层,两个任务被拆成了很多份,然后一个一个执行,站在更高的角度看来两个任务是同时在执行的

Java死锁如何避免?

造成死锁的几个原因:

  1. 一个资源每次只能被一个线程使用
  2. 一个线程在阻塞等待某个资源时,不释放已经占有的资源
  3. 一个线程已经获得资源,在未使用完之前,不能被强行剥夺
  4. 若干线程形成头尾相连的循环等待资源关系

以上是四个必要条件,如果要避免死锁,只需要不满足其中一个条件即可。而其中前三个条件是作为锁要符合的条件,所以要避免死锁就需要打破第4个条件,不出现循环等待的关系。

在开发过程中:

  1. 要注意加锁顺序,保证每个线程按同样的顺序进行加锁
  2. 要注意加锁时限,可以针对所设置一个超时时间
  3. 要注意死锁检查,这是一种预防机制,确保在第一时间发现死锁并进行解决

线程池的底层原理

线程池内部是通过队列+线程实现的,当我们利用线程池执行任务时:

  1. 如何此时线程池中的线程数量小于corePoolSoze,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
  2. 如果此时线程池中的线程数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。
  3. 如果此时线程池中的线程数量大于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小maximumPoolSize,建新的线程来处理被添加的任务。
  4. 如果此时线程池中的线程数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过handler所指定的策略来处理此任务。
  5. 当线程池中的线程数量大于corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数

线程池为什么是先添加队列而不是创建最大线程?

当线程池中的核心线程都在忙时,如果继续往线程池中添加任务,那么任务会先放入队列,队列满了之后,才会新开线程。这就相当于,一个公司本来有10个程序员,本来这10个程序员能正常的处理各种需求,但是随着公司的发展,需求在慢慢的增加,但是一开始这些需求只会增加在待开发列表中,然后这10个程序员加班加点的从待开发列表中获取需求并进行处理,但是某一天待开发列表满了,公司发现现有的10个程序员是真的处理不过来了,所以就开始新招员工了。

AQS如何实现可重入锁?

  1. AQS是一个JAVA线程同步的框架是JDK中很多锁工具的核心实现框架。
  2. 在AQS中,维护了一个信号量state和一个线程组成的双向链表队列。其中,这个线程队列,就是用来给线程排队的,而state就像是一个红绿灯,用来控制线程排队或者放行的。在不同的场景下,有不用的意义。
  3. 在可重入锁这个场景下,state就用来表示加锁的次数。0标识无锁,每加一次锁,state就加1。释放锁state就减1。

谈谈对IOC的理解

Spring的两大特性IOC和AOP。那么要如何理解IOC呢?

IOC表示控制反转。
那么问题来啦:

  1. 什么是控制?控制了什么?
    在使用Spring时,我们需要建立一些类,用一些注释。但在程序运行时,用的时具体的对象,那么这些对象是什么时候创建的?谁创建的?包括对象里的属性是什么时候赋值的?谁赋的?作为程序员,我们只是写了类,而Spring帮我们做了这些。
    控制
  • 控制对象的创建
  • 控制对象内属性的值
  1. 什么是反转
    如果我们不用Spring,那我们得自己来做,反过来,我们用Spring,我们仅仅定义类,定义哪些属性需要Spring赋值(比如加上@Autowried),这其实就是反转,表示一种对象控制权的转移。

那反转有什么用,为什么要反转?
现在有三个类:

  1. A类,A类里有一个属性Cc;
  2. B类,B类里也有一个属性Cc;
  3. C 类

现在程序要运行,这三个类的对象都需要创建出来,并且相应的属性都需要有值,那么除开定义这三个类之外,我们还得写:

  1. A a = new A();
  2. B b = new B();
  3. Cc = new C();
  4. a.c = C;
  5. b.c = c;

这五行代码是不用Spring的情况下多出来的代码,而且,如果类在多一些,类中的属性在多一些,那相应的代码会更多,而且代码会更复杂。所以我们可以发现,我们自己来控制比交给Spring来控制,我们的代码量以及代码复杂度是要高很多的,反言之,将对象交给Spring来控制,减轻了程序员的负担。

总结一下,IOC表示控制反转,表示如果用Spring,那么Spring会负责来创建对象,以及给对象内的属性赋值,也就是如果用Spring,那么对象的控制权会转交给Spring。

单例Bean和单例模式

单例模式表示JVM中某个类的对象只会存在唯一一个。
而单例Bean并不表示JVM中只能存在唯一的某个类的Bean对象。

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的操作一样(开启一个事务)。

Spring事务什么时候会失效?

Spring事务的原理是AOP,进行了切面增强,那么失效的根本原因是这个AOP不起作用了。通常有以下几种:

  1. 发生自调用,类里面使用this调用本类的方法,此时这个this对象不是代理类,而是UserService对象本身!解决方法很简单,让那个this变成UserService的代理类即可!
  2. 方法不是public的:@Transactional只能用于public的方法上,否则事务不会失效,如果要用在非public方法上,可以开启aspectj代理模式。
  3. 数据库不支持事务
  4. 没有被spring管理
  5. 异常被吃掉,事务不会回滚(或者抛出的异常没有被定义,默认为RuntimeException)

Spring中的Bean创建的生命周期有哪些步骤?

Spring中一个Bean的创建大概分为以下几个步骤:

  1. 推断构造方法
  2. 实例化
  3. 填充属性,也就是依赖注入
  4. 处理Aware回调
  5. 初始化前,处理@PostConstruct注解
  6. 初始化,处理InitalizingBean接口
  7. 初始化后,进行AOP

Spring中的Bean是线程安全的吗?

Spring本身没有针对Bean做线程安全处理,所以:

  1. 如果Bean是无状态的,那么Bean则是线程安全的。
  2. 如果Bean是有状态的,那么Bean则不是线程安全的。

ApplicationContext和BeanFactory有什么区别?

BeanFactory是Spring中非常核心的组件,表示Bean工厂,可以生成Bean,维护Bean,而ApplicationContext继承了BeanFactory,所以ApplicationContext拥有BeanFactory
所有的特点,也是一个Bean工厂,但是ApplicationContext除开继承了BeanFactory之外,还继承了诸如EnvironmentCapable、MessageSource、ApplicationEventPublisher等接口,从而ApplicationContext还有获取系统环境变量、国际化、事件发布等功能,这是BeanFactory所不具备的。

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

Spring中什么时候@Transactional会失效?

因为Spring事务是基于代理来实现的,所以某个加了@Transactional的方法只有是被代理对象调用时,那么这个注解才会生效,所以如果是被代理对象来调用这个方法,那么@Transactional是不会生效的。

同时如果某个方法是private的,那么@Transactional也会失效,因为底层cglib是基于父子类来实现的,子类是不能重载父类的private方法的,所以无法很好的利用代理,也会导致@Transactianal失效

Spring容器启动流程是怎样的?

在创建Spring容器,也就是启动Spring时:

  1. 首先会进行扫描,扫描得到所有的BeanDefinition对象,并存在一个Map中
  2. 然后筛选出非懒加载的单例BeanDefinition进行创建Bean,对于多例Bean不需要在启动过程中去进行创建,对于多例Bean会在每次获取Bean时利用BeanDefinition去创建
  3. 利用BeanDefinition创建Bean就是Bean的创建生命周期,这期间包括了合并BeanDefinition、推断构造方法、实例化、属性填充、初始化前、初始化、初始化后等步骤,其中AOP就是发生在初始化后这一步骤中
  4. 单例Bean创建完了之后,Spring会发布一个容器启动事件
  5. Spring启动结束
  6. 在源码中会更复杂,比如源码中会提供一些模板方法,让子类来实现,比如源码中还涉及到一些BeanFactoryPostProcessor和BeanPostProcessor的注册,Spring的扫描
    就是通过BenaFactoryPostProcessor来实现的,依赖注入就是通过BeanPostProcessor来实现的
  7. 在Spring启动过程中还会去处理@lmport等注解

Spring中用了哪些设计模式?

  1. 工厂模式:BeanFactory、FactoryBean、ProxyFactory。
  2. 原型模式:原型Bean、PrototypeTargetSource、PrototypeAspectInstanceFactory。
  3. 单例模式:单例Bean、SingletonTargetSource。
  4. 构造器模式:BeanDefinitionBuilder、StringBuilder
  5. 适配器模式:ApplicationListenerMethodAdapter(将@EventListener注解的方法适配成ApplicationListener)
  6. 访问者模式:PropertyAccessor(属性访问器,用来访问和设置某个对象的某个属性)
  7. 装饰器模式:BeanWrapper(比单纯的Bean对象功能更强大)
  8. 代理模式:AOP
  9. 观察者模式:ApplicationListener
  10. 策略模式:InstantiationStrategy(Spring需要根据BeanDefinition来实例化Bean,但是具体可以选择不同的策略来实例化)
  11. 模板方法模式
  12. 责任链模式

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都可以说

Spring Boot是如何启动Tomcat的

  1. 首先,SpringBoot在启动时会先创建一个Spring容器
  2. 在创建Spring容器过程中,会利用@ConditionalOnClass技术来判断当前classpath中是否存在Tomcat依赖,如果存在则会生成一个启动Tomcat的Bean
  3. Spring容器创建完之后,就会获取启动Tomcat的Bean,并创建Tomcat对象,并绑定端口等,然后启动Tomcat

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语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。

#{}和${}的区别是什么?

#{}是预编译处理、是占位符,${}是字符串替换、是拼接符。

Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement来赋值;

Mybatis在处理${}时,会将sql中的${}替换成变量的值,调用Statement来赋值;

使用#()可以有效的防止SQL注入,提高系统安全性。

索引的基本原理

索引用来快速地寻找那些具有特定值的记录。如果没有索引,一般来说执行查询时遍历整张表。

索引的原理:就是把无序的数据变成有序的查询

  1. 把创建了索引的列的内容进行排序
  2. 对排序结果生成倒排表
  3. 在倒排表内容上拼上数据地址链
  4. 在查询的时候,先拿到倒排表内容,再取出数据地址链,从而拿到具体数据

索引设计的原则

查询更快、占用空间更小

  1. 适合索引的列是出现在where子句中的列,或者连接子句中指定的列

  2. 基数较小的类,索引效果较差,没有必要在此列建立索引

  3. 使用短索引,如果对长字符串列进行索引,应该指定一个前缀长度,这样能够节省大量索引空间,如果搜索词超过索引前缀长度,则使用索引排除不匹配的行,然后检查其余行是否可能匹配。

  4. 不要过度索引。索引需要额外的磁盘空间,并降低写操作的性能。在修改表内容的时候,索引会进行更新甚至重构,索引列越多,这个时间就会越长。所以只保持需要的索引有利于查询即可。

  5. 定义有外键的数据列一定要建立索引。

  6. 更新频繁字段不适合创建索引。

  7. 若是不能有效区分数据的列不适合做索引列。(如性别,男女未知,最多也就三种,区分度实在太低)

  8. 尽量的扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。

  9. 对于那些查询中很少涉及的列,重复值比较多的列不要建立索引。

  10. 对于定义为text、image和bit的数据类型的列不要建立索引。

事务的基本特性和隔离级别

事务基本特性ACID分别是:

  1. 原子性:一个事务中的操作要么全部成功,要么全部失败。

  2. 一致性:数据库总是从一个一致性的状态转换到另外一个一致性的状态。比如A转账给B100块钱,假设A只有90块,支付之前我们数据库里的数据都是符合约束的,但是
    如果事务执行成功了,我们的数据库数据就破坏约束了,因此事务不能成功,这里我们说事务提供了一致性的保证

  3. 隔离性:一个事务的修改在最终提交前,对其他事务是不可见的。

  4. 持久性:一旦事务提交,所做的修数就会永久保存到数据库中。

隔离性有4个隔离级别,分别是:

  • read uncommit:读未提交,可能会读到其他事务未提交的数据,也叫做脏读。
    用户本来应该读取到id=1的用户age应该是10,结果读取到了其他事务还没有提交的事务,结果读取结果age=20,这就是脏读。
  • read commit:读已提交,两次读取结果不一致,叫做不可重复读。
    不可重复读解决了脏读的问题,他只会读取已经提交的事务。用户开启事务读取id=1用户,查询到age=10,再次读取发现结果=20,在同一个事务里同一个查询读取到不同的结果叫做不可重复读。
  • repeatable read:可重复复读,这是mysql的默认级别,就是每次读取结果都一样,但是有可能产生幻读。
  • serializable:串行,一般是不会使用的,他会给每一行读取的数据加锁,会导致大量超时和锁竞争的问题。

什么是MVCC

MVCC(Multi-Version Concurrency Control,多版本并发控制)指的就是在使用READ COMMITTD、REPEATABLE READ这两种隔离级别的事务在执行普通的SEELCT操作时访问记录的版本链的过程。可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。READ COMMITTD、REPEATABLE READ这两个隔离级别的一个很大不同就是:生成ReadView的时机不同,READ COMMITTD在每一次进行普通SELECT操作前都会生成一个ReadView,而REPEATABLE READ只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView就好了。

简述MylSAM和InnoDB的区别

MyISAM:

  • 不支持事务,但是每次查询都是原子的;
  • 支持表级锁,即每次操作是对整个表加锁;
  • 存储表的总行数;
  • 一个MYISAM表有三个文件:索引文件、表结构文件、数据文件;
  • 采用非聚集索引,索引文件的数据域存储指向数据文件的指针。辅索引与主索引基本一致,但是辅索引不用保证唯一性。

InnoDb:

  • 支持ACID的事务,支持事务的四种隔离级别;
  • 支持行级锁及外键约束:因此可以支持写并发;
  • 不存储总行数;
  • 一个InnoDb引擎存储在一个文件空间(共享表空间,表大小不受操作系统控制,一个表可能分布在多个文件里),也有可能为多个(设置为独立表空,表大小受操作系统文件大小限制,一般为2G),受操作系统文件大小的限制;
  • 主键索引采用聚集索引(索引的数据域存储数据文件本身),辅索引的数据域存储主键的值;因此从辅索引查找数据,需要先通过辅索引找到主键值,再访问辅索引;最好使用自增主键,防止插入数据时,为维持B+树结构,文件的大调整。

索引覆盖是什么

索引覆盖就是一个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语句后,会先根据条件找到数据所在的页,并将该页缓存在Buffer Pool中
  2. 执行update语句,修改Buffer Pool中的数据,也就是内存中的数据
    3.针对update语句生成一个RedoLog对象,并存入LogBuffer中
  3. 针对update语句生成undolog日志,用于事务回滚
  4. 如果事务提交,那么则把RedoLog对象进行持久化,后续还有其他机制将Buffer Pool中所修改的数据页持久化到磁盘中
  5. 如果事务回滚,则利用undolog日志进行回滚

B树和B+树的区别,为什么Mysql使用B+树

B树的特点:

  1. 节点排序
  2. 一个节点了可以存多个元素,多个元素也排序了

B+树的特点:

  1. 拥有B树的特点
  2. 叶子节点之间有指针
  3. 非叶子节点上的元素在叶子节点上都冗余了,也就是叶子节点中存储了所有的元素,并且排好顺序

Mysql索引使用的是B+树,因为索引是用来加快查询的,而B+树通过对数据进行排序所以是可以提高查询速度的,然后通过一个节点中可以存储多个元素,从而可以使得B+树的高度不会太高,在Mysql中一个Innodb页就是一个B+树节点,一个Innodb页默认16kb,所以一般情况下一颗两层的B+树可以存2000万行左右的数据,然后通过利用B+树叶子节点存储了所有数据并且进行了排序,并且叶子节点之间有指针,可以很好的支持全表扫描,范围查找等SQL语句。

Mysql锁有哪些,如何理解

按锁粒度分类:

  1. 行锁:锁某行数据,锁粒度最小,并发度高
  2. 表锁:锁整张表,锁粒度最大,并发度低
  3. 间隙锁:锁的是一个区间

还可以分为:

  1. 共享锁:也就是读锁,一个事务给某行数据加了读锁,其他事务也可以读,但是不能写
  2. 排它锁:也就是写锁,一个事务给某行数据加了写锁,其他事务不能读,也不能写

还可以分为:

  1. 乐观锁:并不会真正的去锁某行记录,而是通过一个版本号来实现的
  2. 悲观锁:上面所的行锁、表锁等都是悲观锁

在事务的隔离级别实现中,就需要利用锁来解决幻读