Java开发面试专题

JAVA并发篇

1.JAVA如何开启线程?怎样保证线程安全?

线程和进程的区别:进程是操作系统进行资源分配的最小单元;线程是操作系统进行任务分配的最小单元。线程隶属于进程。

如何开启线程?1、继承Thread类,重写run方法;2、实现Runnable接口,实现run方法;3、实现Callable接口,实现call方法。通过FutureTask创建一个线程,获取到线程执行的返回值;4、通过线程池来开启线程。

怎样保证线程安全?加锁:1、JVM提供的锁,也就是Synchronized关键字。2、JDK提供的各种锁Lock。

2.Volatile和Synchronized有什么区别?Volatile能不能保证线程安全?DCL(Double Check Lock)单例为什么要加Volatile?

Synchronized关键字,用来加锁。Volatile只是保持变量的线程可见性,通常适用于一个线程写,多个线程读的场景。

不能。Volatile关键字只能保证线程可见性,不能保证原子性。

Volatile防止指令重排。在DCL中,防止高并发情况下,指令重排造成的线程安全问题。

3.JAVA线程锁机制是怎样的?偏向锁、轻量级锁、重量级锁有什么区别?锁机制是如何升级的?

JAVA的锁就是在对象的Markword中记录一个锁状态。无锁、偏向锁、轻量级锁、重量级锁对应不同的锁状态。

JAVA的锁机制就是根据资源竞争的激烈程度不断进行锁升级的过程。

4.谈谈你对AQS的理解。AQS如何实现可重入锁?

AQS是一个JAVA线程同步的框架。是JDK中很多锁工具的核心实现框架。

在AQS中,维护了一个信号量state和一个线程组成的双向链表队列。其中,这个线程队列,就是用来给线程排队,而state就像是一个红绿灯,用来控制线程排队或者放行的。在不同的场景下,有不同的意义。

在可重入锁这个场景下,state就用来表示加锁的次数。0标识无锁,每加一次锁,state就加1.释放锁state就就减1。

5.有A,B,C三个线程,如何保证三个线程同时执行?如何在并发情况下保证三个线程依次执行?如何保证三个线程有序交错执行?

CountDownLatch,CylicBarrier,Semaphore。

//三个线程同时执行
public class ThreadSafeDemo{
    public int count = 0;
    public void add() {count++;}
    
    public static void main(String[] args) throws InterruptedException{
        int size = 3;
        
        ThreadSafeDemo threadSafeDemo = new ThreadSafeDemo();
        
        CountDownLatch countDownLatch = new CountDownLatch(1);
        for(int i=0;i{
                try{
                    countDownLatch.await();
                    System.out.printlin(System.currentTimeMillis());
                    Thread.sleep(100)
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
           }).start();
        }
        Thread.sleep(5000);
        countDownLatch.countDown();
    } 
}
//三个线程依次执行

public class OrderThread2 {

    static volatile int ticket=1;  //信号量
    
    public static void main(String[] args){
        Thread t1 = new Thread(()->{
            while(true){
                if(ticket==1){
                    try{
                        Thread.sleep(100);
                        for(int i=0;i<10;i++){
                            System.out.println("a" + i);
                        }
                    } catch(InterruptedException e){
                         e.printStackTrace();
                    }
                }
                ticket = 2;
                return;
            }
        }); 
        Thread t2 = new Thread(()->{
            while(true){
                if(ticket==2){
                    try{
                        Thread.sleep(100);
                        for(int i=0;i<10;i++){
                            System.out.println("b" + i);
                        }
                    } catch(InterruptedException e){
                         e.printStackTrace();
                    }
                }
                ticket = 3;
                return;
            }
        }); 
        Thread t3 = new Thread(()->{
            while(true){
                if(ticket==3){
                    try{
                        Thread.sleep(100);
                        for(int i=0;i<10;i++){
                            System.out.println("c" + i);
                        }
                    } catch(InterruptedException e){
                         e.printStackTrace();
                    }
                }
                ticket = 1;
                return;
            }
        }); 



    } 
}
public class OrderThread{

    private static Semaphore s1 = new Semaphore(1);
    private static Semaphore s2 = new Semaphore(1);
    private static Semaphore s3 = new Semaphore(1);

    public static void main(String[] args){
        try{
            s1.acquire();
            s2.acquire();
            //s3.acquire(); 释放一个
        }
        new Thread(()->{
            while(true){
                s1.acquire();
                System.out.println('A');
                s2.release();
            }
        });
        new Thread(()->{
            while(true){
                s2.acquire();
                System.out.println('B');
                s3.release();
            }
        });
        new Thread(()->{
            while(true){
                s3.acquire();
                System.out.println('C');
                s1.release();
            }
        });
    }
}

如何对一个字符串快速进行排序?

Fork/Join 框架。

JAVA网络通信篇

1.TCP和UDP有什么区别?TCP为什么是三次握手,而不是两次?

TCP(Transfer Control Protocol)是一种面向连接的、可靠的、传输层通信协议。
特点:面向连接的,点对点的通信,高可靠性,效率比较低,占用的系统资源比较多。
UDP(User Datagram protocol)是一种无连接,不可靠的、传输层通信协议。
特点:好比是广播:不需要连接,发送方不管接收方有没有准备好,直接发消息;可以进行广播发送;传输不可靠,有可能丢失消息;效率比较高;协议比较简单,占用资源比较少。

TCP建立连接三次握手,断开连接四次挥手。
如果是两次握手,可能会造成连接资源浪费。

2.JAVA有哪几种IO模型?有什么区别?

BIO 同步阻塞IO。可靠性差、吞吐量低、适用于连接比较少且比较固定的场景。

Java开发面试专题_第1张图片

 NIO 同步非阻塞IO。可靠性比较好,吞吐量比较高,适用于连接比较多并且连接比较短(轻操作)。

Java开发面试专题_第2张图片

AIO 异步非阻塞IO。可靠性是最好的、吞吐量也非常高。适用于连接比较多、并且连接比较长(重操作)。

Java开发面试专题_第3张图片

 3.JAVA NIO的几个核心组件是什么?分别有什么作用?

Channel Buffer Selector

Java开发面试专题_第4张图片

 channel 类似于一个流,每个channel对应一个buffer缓冲区,channel会注册到selector。

select会根据channel上发生的读写事件,将请求交由某个空闲的线程处理。selector对应一个或多个线程。

Buffer和Channel都是可读可写的。

4.select、poll和epoll有什么区别。

他们是NIO中实现多路复用的三种实现机制,有Linux操作系统提供的。

用户空间和内核空间:操作系统为了保护系统安全,将内核划分为两部分,一个用户空间,一个是内核空间。用户空间不能直接访问底层的硬件设备,必须通过内核空间。

文件描述符 File Descriptor(FD):是一个抽象的概念,形式上是一个整数,实际上是一个索引值,指向内核中为每个进程维护进程所打开的文件的记录表。当程序打开一个文件或者创建一个文件时,内核就会向进程返回一个FD。通常Unix,Linux。

select机制:会维护一个FD集合fd_set。将fd_set从用户空间复制到内核空间,激活socket。x64 2048

poll机制:和select机制差不多,把fd_set结构进行优化,FD集合的大小就突破了操作系统的限制。pollfd结构来代替fd_set,通过链表实现。

epoll机制:event poll。不在扫描所有的FD,只将用户关心的FD的事件放到内核的一个事件表中,这样,可以减少用户空间和内核空间之间需要拷贝的数据。

5.描述下HTTP和HTTPS的区别。

http:是互联网上应用最为广泛的一种网络通信协议,基于TCP,可以使浏览器工作更为高效,减少网络传输。

https:是http的加强版,可以认为是HTTP+SSL(Secure Socket Layer)。在http的基础上增加了一系列的安全机制。一方面保证数据传输安全,另一方面对访问者增加了验证机制。是目前现行架构下,最为安全的解决方案。

主要区别:

1、http的连接是简单无状态的,https的数据传输时经过证书加密的,安全性更高。

2、http是免费的,而https需要申请证书,而且通常是需要收费的,

3、他们的传输协议是不相同的,http默认80端口,https443端口。

https缺点:

1、https的握手协议比较费时,所以会影响服务的响应速度以及吞吐量。

2、https也并不是完全安全的,他的证书体系其实也不是完全的。

3、证书需要花钱。

JAVA调优篇

1.JVM内存模型。

2.JAVA类加载的全过程是怎样的?什么是双亲委派机制?有什么作用? 

类加载过程:加载-》连接-》初始化

加载:把java的字节码数据加载到JVM内存当中,并映射成jvm认可的数据结构。

连接:1、验证,检测加载到的字节信息是否符合JVM规范;2、准备,创建类或接口的静态变量,并赋初始值,半初始化状态;3、解析:把符号引用转为直接引用。

初始化:代码块

双亲委派机制:向上委托查找,向下委托加载。

Java开发面试专题_第5张图片

 作用: 保护JAVA的底层类不会被应用程序覆盖。

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

1、用户创建一个对象,JVM首先需要到方法去找对象的类型信息,然后再创建对象。

2、JVM实例化一个对象,首先要在堆中先创建一个对象。--》半初始化状态

3、对象首先会分配在堆内存中新生代的Eden。然后经过一次Minor GC,对象如果存活 ,就会进入S区。在后续的每次GC中,如果对象一直存活,就会在S区来回拷贝,每移动一次,年龄加1。多大年龄才会移入老年代,最大15。超过一定年龄后,对象转入老年代。

4、当方法执行结束后,栈中的指针会先移除。

5、堆中的对象,经过Full GC,就会标记为垃圾,然后被GC线程清除掉。

3.怎么确定一个对象到底是不是垃圾?GC ROOT?

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

那些是GC Root? Stack->JVM Stack,Native Stack,class 类,runtime constant pool 常量池,static refernece 静态变量。

4.JVM有哪些垃圾回收算法?

MarkSweep 标记清除算法Java开发面试专题_第6张图片

这个算法分为两个阶段,标记阶段:把垃圾内存标记出来,清除阶段:直接将垃圾内存回收。这种算法实现比较简单,有可能产生大量的内存碎片。 

Copying 拷贝算法Java开发面试专题_第7张图片

 为了解决标记清除算法的内存碎片问题,就产生了拷贝算法。拷贝算法将内存分为大小相等的两半,每次只使用其中一半。垃圾回收时,将当前这一块的存活对象全部拷贝到另一半,然后当前这一半内存就可以直接清除。这种算法没有内存碎片,但是他的问题在于浪费空间。而且,他的效率跟存活的对象个数有关。

MarkCompack 标记压缩算法Java开发面试专题_第8张图片

为了解决拷贝算法的缺陷,这种算法在标记阶段跟标记清除算法是一样的,但是在完成标记之后,不是直接清除垃圾内存,而是存活对象往一端移动,然后将端界以外的所有内存直接清除。

5.JVM有哪些垃圾回收器?他们都是怎样工作的?什么是STW?他都发生在哪些阶段?什么是三色标记?如何解决错标记和漏标记的问题?为什么要设计这么多的垃圾回收器?

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

JVM垃圾回收器:Java开发面试专题_第9张图片

Serial算法 串行,是早期垃圾回收器,只执行一个线程执行GC,只适用几十兆内存。Java开发面试专题_第10张图片

 Parallel 并行,在串行基础上,增加多线程GC。在多CPU的架构下,性能比Serial高。Java开发面试专题_第11张图片

 CMS concurrent mark sweep。核心:将STW分散,让一部分GC线程与用户线程并发执行。整个GC过程分为四个阶段。Java开发面试专题_第12张图片

1、初始标记阶段:STW只标记出根对象直接引用的对象。

2、并发标记:继续标记其他对象,以应用程序是并发执行。

3、重新标记: STW对并发执行阶段的对象进行重新标记。

4、并发清除:并行。将产生的垃圾清除,清除过程中,应用程序又会不断产生新的垃圾,叫做浮动垃圾。这些垃圾就要留到下一次GC过程中清除。

G1 Garbage First 垃圾优先他的内存模型是实际不分代,但是逻辑上是分代的。在内存模型中,对于堆内存就不再分老年代和新生代,而是划分成一个一个的小内存块,叫做Region。每个Region可以隶属于不同的年代。

GC分为四个阶段:
第一:初始标记 标记出GCRoot直接引用的对象。STW
第二:标记Region,通过RSet标记出上一个阶段标记的Region引用到的Old区Region。
第三:并发标记阶段:跟CMS的步骤是差不多的。只是遍历的范围不再是整个Old区,而只需要遍历第二步标记出来的Region。
第四:重新标记: 跟CMS中的重新标记过程是差不多的。
第五:垃圾清理:与CMS不同的是,G1可以采用拷贝算法,直接将整个Region中的对象拷贝到另一个Region。而这个阶段,G1只选择垃圾较多的Region来清理,并不是完全清理。

CMS的核心算法就是三色标记:
三色标记:是一种逻辑上的抽象。
将每个内存对象分成三种颜色:
黑色:表示自己和成员变量都已经标记完毕。
灰色:自己标记完了,但是成员变量还没有完全标记完。
白色:自己未标记完。
CMS通过增量标记 increment update 的方式来解决漏标的问题。

六、如何进行JVM调优?JVM参数有哪些?怎么查看一个JAVA进程的JVM参数?谈谈你了解的JVM参数。如果一个java程序每次运行一段时间后,就变得非常卡顿,你准备如何对他进行优化?
JVM调优主要就是通过定制JVM运行参数来提高JAVA应用程度的运行数据。
JVM参数大致可以分为三类:
1)标准指令: -开头,这些是所有的HotSpot都支持的参数。可以用java -help 打印出来。
2)非标准指令: -X开头,这些指令通常是跟特定的HotSpot版本对应的。可以用java -X 打印出来。
3)不稳定参数: -XX 开头,这一类参数是跟特定HotSpot版本对应的,并且变化非常大。详细的文档资料非常少。
在JDK1.8版本下,有几个常用的不稳定指令:
java -XX:+PrintCommandLineFlags : 查看当前命令的不稳定指令。
java -XX:+PrintFlagsInitial : 查看所有不稳定指令的默认值。
java -XX:+PrintFlagsFinal: 查看所有不稳定指令最终生效的实际值。

消息队列篇

1.MQ有什么用?有哪具体的使用场景?

缓存篇

为什么使用缓存?

高性能;高可用;

什么是缓存穿透?缓存击穿?缓存雪崩? 怎么解决?

缓存穿透:缓存中查不到,数据库中也查不到。

解决方案:
1)对参数进行合法性校验。
2)将数据库中没有查到结果的数据也写入到缓存。这时要注意为了防止Redis被无用的Key占满,这一类缓存的有效期要设置得短一点。
3) 引入布隆过滤器,在访问Redis之前判断数据是否存在。 要注意布隆过滤器存在一定的误判率,并且,布隆过滤器只能加数据不能减数据。

缓存击穿:缓存中没有,数据库中有。一般是出现在存数数据初始化以及key过期了的情况。他的问题在于,重新写入缓存需要一定的时间,如果是在高并发场景下,过多的请求就会瞬间写到DB上,给DB造成很大的压力。
解决方案:
1)设置这个热点缓存永不过期。这时要注意在value当中包含一个逻辑上的过期时间,然后另起一个线程,定期重建这些缓存。
2)加载DB的时候,要防止并发。
缓存雪崩: 缓存大面积过期,导致请求都被转发到DB。
解决方案:
1)把缓存的时效时间分散开。例如,在原有的统一失效时间基础上,增加一个随机值。
2)对热点数据设置永不过期。

如何保证Redis与数据库的数据一致?

当我们对数据进行修改的时候,到底是先删缓存,还是先写数据库?
1、如果先删缓存,再写数据库: 在高并发场景下,当第一个线程删除了缓存,还没有来得及写数据库,第二个线程来读取数据,会发现缓存中的数据为空,那就会去读数据库中的数据(旧值,脏数据),读完之后,把读到的结果写入缓存(此时,第一个线程已经将新的值写到缓存里面了),这样缓存中的值就会被覆盖为修改前的脏数据。
总结:在这种方式下,通常要求写操作不会太频繁。
解决方案:
1)先操作缓存,但是不删除缓存。将缓存修改为一个特殊值(-999)。客户端读缓存时,发现是默认值,就休眠一小会,再去查一次Redis。 特殊值对业务有侵入。 休眠时间,可能会多次重复,对性能有影响。
2)延时双删。 先删除缓存,然后再写数据库,休眠一小会,再次删除缓存。如果数据写操作很频繁,同样还是会有脏数据的问题。
2、先写数据库,再删缓存: 如果数据库写完了之后,缓存删除失败,数据就会不一致。
总结: 始终只能保证一定时间内的最终一致性。
解决方案:
1)给缓存设置一个过期时间 问题:过期时间内,缓存数据不会更新。
2)引入MQ,保证原子操作。
解决方案:
将热点数据缓存设置为永不过期,但是在value当中写入一个逻辑上的过期时间,另外起一个后台线程,扫描这些key,对于已逻辑上过期的缓存,进行删除。

如何设计一个分布式锁?如何对锁性能进行优化?
分布式锁的本质:就是在所有进程都能访问到的一个地方,设置一个锁资源,让这些进程都来竞争锁资源。通常对于分布式锁,会要求响应快、性能高、与业务无关。
Redis实现分布式锁:SETNX key value:当key不存在时,就将key设置为value,并返回1。如果key存在,就返回0。EXPIRE key locktime: 设置key的有效时长。 DEL key: 删除。 GETSET key value: 先GET,再SET,先返回key对应的值,如果没有就返回空。然后再将key设置value。
1)最简单的分布式锁: SETNX 加锁, DEL解锁。
问题: 如果获取到锁的进程执行失败,他就永远不会主动解锁,那这个锁就被锁死了。
2)给锁设置过期时长:
问题: SETNX 和EXPIRE并不是原子性的,所以获取到锁的进程有可能还没有执行EXPIRE指令,就挂了,这时锁还是会被锁死。
3)将锁的内容设置为过期时间(客户端时间+过期时长)
SETNX获取锁失败时,拿这个时间跟当前时间比对,如果是过期的锁,就先删除锁,再重新上锁。
问题: 在高并发场景下,会产生多个进程同时拿到锁的情况。
4)setNX失败后,获取锁上的时间戳,然后用getset,将自己的过期时间更新上去,并获取旧值。如果这个旧值,跟之前获得的时间戳是不一致的,就表示这个锁已经被其他进程占用了,自己就要放弃竞争锁。如下代码:
 

public boolean tryLock(RedisnConnection conn) {
    long nowTime = System.currnetTimeMillis();
    12
    long expireTIme = nowTime + 1000;
    if (conn.SETNX("mykey", expireTIme) == 1) {
        conn.EXPIRE("mykey", 1000);
        return true;
    } else {
        long oldVal = conn.get("mykey");
        if (oldVal != null && oldVal < nowTime) {
            long currentVal = conn.GETSET("mykey", expireTime);
            if (oldVal == curentVal) {
                conn.EXPIRE("mykey", 1000);
                return true;
            }
            return false;
        }
        return false;
    }
}

上面就形成了一个比较高效的分布式锁。分析一下,上面各种优化的根本问题在于SETNX和EXPIRE两个指令无法保证原子性。Redis2.6提供了直接执行lua脚本的方式,通过Lua脚本来保证原子性。redission。

Redis如何配置Key的过期时间?他的实现原理是什么?
redis设置key的过期时间: 1、 EXPIRE 。 2 SETEX
实现原理:
1)定期删除: 每隔一段时间,执行一次删除过期key的操作。
2)懒汉式删除: 当使用get、getset等指令去获取数据时,判断key是否过期。过期后,就先把key删除,再执行后面的操作。 Redis是将两种方式结合来使用。
懒汉式删除定期删除:平衡执行频率和执行时长。
定期删除时会遍历每个database(默认16个),检查当前库中指定个数的key(默认是20个)。随机抽查这些key,如果有过期的,就删除。 程序中有一个全局变量记录到秒到了哪个数据库。

海量数据下,如何快速查找一条记录?
1、使用布隆过滤器:快速过滤不存在的记录。 使用Redis的bitmap结构来实现布隆过滤器。
2、在Redis中建立数据缓存:将我们对Redis使用场景的理解尽量表达出来。
以普通字符串的形式来存储,(userId -> user.json)。 以一个hash来存储一条记录 (userId key-> username field-> , userAge->)。 以一个整的hash来存储所有的数据,UserInfo-> field就用userId , value就用user.json。一个hash最多能支持 2^32-1(40多个亿)个键值对。
缓存击穿:对不存在的数据也建立key。这些key都是经过布隆过滤器过滤的,所以一般不会太多。
缓存过期:将热点数据设置成永不过期,定期重建缓存。 使用分布式锁重建缓存。
查询优化:按槽位分配数据,自己实现槽位计算,找到记录应该分配在哪台机器上,然后直接去目标机器上找。

微服务篇

谈谈你对微服务的理解,微服务有哪些优缺点?
微服务是由Martin Fowler大师提出的。微服务是一种架构风格,通过将大型的单体应用划分为比较小的服务单元,从而降低整个系统的复杂度。
优点:
1)服务部署更灵活:每个应用都可以是一个独立的项目,可以独立部署,不依赖于其他服务,耦合性降低。
2)技术更新灵活:在大型单体应用中,技术要进行更新,往往是非常困难的。而微服务可以根据业务特点,灵活选择技术栈。
3)应用的性能得到提高: 大型单体应用中,往往启动就会成为一个很大的难关。而采用微服务之后,整个系统的性能是能够得到提高的。
4)更容易组合专门的团队: 在单体应用时,团队成员往往需要对系统的各个部分都要有深入的了解,门槛是很高的。而采用微服务之后,可以给每个微服务组建专
门的团队。
5)代码复用: 很多底层服务可以以REST API的方式对外提供统一的服务,所有基础服务可以在整个微服务系统中通用。
缺点:
1)服务调用的复杂性提高了:网络问题、容错问题、负载问题、高并发问题......
2)分布式事务:尽量不要使用微服务事务。
3)测试的难度提升了:
4)运维难度提升:单体架构只要维护一个环境,而到了微服务是很多个环境,并且运维方式还都不一样。所以对部署、监控、告警等要求就会变得非常困难。

Spring底层篇

什么是Spring?谈谈你对IOC和AOP的理解?
Spring: 是一个企业级java应用框架,他的作用主要是简化软件的开发以及配置过程,简化项目部署环境。
Spring的点有:
1、Spring低侵入设计,对业务代码的污染非常低。
2、Spring的DI机制将对象之间的关系交由框架处理,减少组件的耦合。
3、Spring提供了AOP技术,支持将一些通用的功能进行集中式管理,从而提供更好的复用。
4、Spring对于主流框架提供了非常好的支持。
IOC就是控制反转,指创建对象的控制权转移给Spring来进行管理。简单来说,就是应用不用去new对象了,而全部交由Spring自动生产。
IOC有三种注入方式:1、 构造器注入 2、setter方法注入 3、注解注入。
AOP 面向切面:
用于将那些与业务无关,但却对多个对象产生影响的公共行为。抽取并封装成一个可重用的模块。AOP的核心就是动态代理。JDK的动态代理和CGLIB动态代理。

Spring容器的启动流程是怎么样的?
使用AnnotationConfigApplicationContext 来跟踪一下启动流程:
(1)this():初始化reader和scanner
(2)scan(basePackages):使用scanner组件扫描basePackage下的所有对象,将配置类的BeanDefinition注册到容器中。
(3)refresh():刷新容器。
(4)prepareRefresh:刷新前的预处理
(5)obtainFreshBeanFactory: 获取在容器初始化时创建的BeanFactory
(6)prepareBeanFactory: BeanFactory的预处理工作,会向容器中添加一些组件。
(7)postProcessBeanFactory: 子类重写该方法,可以实现在BeanFactory创建并预处理完成后做进一步的设置。
(8)invokeBeanFactoryPostProcessors: 在BeanFactory初始化之后执行BeanFactory的后处理器。
(9)registerBeanPostProcessors: 向容器中注册Bean的后处理器,他的主要作用就是干预Spring初始化Bean的流程,完成代理、自动注入、循环依赖等这些功能。
(10)initMessageSource: 初始化messagesource组件,主要用于国际化。
(11)initApplicationEventMulticaster: 初始化事件分发器
(12)onRefresh: 留给子容器,子类重写的方法,在容器刷新的时候可以自定义一些逻辑。
(13)registerListeners: 注册监听器。
(14)finishBeanFactoryInitialization: 完成BeanFactory的初始化,主要作用是初始化所有剩下的单例Bean。
(15)finishRefresh: 完成整个容器的初始化,发布BeanFactory容器刷新完成的事件。

Spring框架中Bean的创建过程是怎样?
简单来说,Spring框架中的Bean经过四个阶段: 实例化 -》 属性赋值 -》 初始化 -》 销毁
 具体来说,Spring中Bean 经过了以下几个步骤:
1、实例化: new xxx(); 两个时机: a.当客户端向容器申请一个Bean时。b.当容器在初始化一个Bean时发现还需要依赖另一个Bean。 BeanDefinition 对象保存。-到底是new一个对象还是创建一个动态代理?
2、设置对象属性(依赖注入):Spring通过BeanDefinition找到对象依赖的其他对象,并将这些对象赋予当前对象。
3、处理Aware接口:Spring会检测对象是否实现了xxxAware接口,如果实现了,就会调用对应的方法。 BeanNameAware/BeanClassLoaderAware/BeanFactoryAware/ApplicationContextAware
4、BeanPostProcessor前置处理: 调用BeanPostProcessor的postProcessBeforeInitialization方法
5、InitializingBean: Spring检测对象如果实现了这个接口,就会执行他的afterPropertiesSet()方法,定制初始化逻辑。
6、init-method: 如果Spring发现Bean配置了这个属性,就会调用他的配置方法,执行初始化逻辑。@PostConstruct
7、BeanPostProcessor后置处理: 调用BeanPostProcessor的postProcessAfterInitialization方法
到这里,这个Bean的创建过程就完成了, Bean就可以正常使用了。
8、DisposableBean: 当Bean实现了这个接口,在对象销毁前就会调用destory()方法。
9、destroy-method: @PreDestroy

Spring框架中的Bean是线程安全的吗? 如果线程不安全,要如何处理?

Spring容器本身没有提供Bean的线程安全策略,因此,也可以说Spring容器中的 Bean不是线程安全的。
要如何处理线程安全问题,就要分情况来分析:
Spring中的作用域:
1、 sington
2、prototype: 为每个Bean请求创建给实例。
3、request:为每个request请求创建一个实例,请求完成后失效。
4、 session: 与request是类似的。
5、global-session:全局作用域。
对于线程安全问题:
1> 对于prototype作用域,每次都是生成一个新的对象,所以不存在线程安全问题。
2>sington作用域: 默认就是线程不完全的。但是对于开发中大部分的Bean,其实是无状态的,不需要保证线程安全。所以在平常的MVC开发中,是不会有线程安全问题的。
无状态表示这个实例没有属性对象, 不能保存数据,是不变的类。比
如: controller、service、dao
有状态表示示例是有属性对象,可以保存数据,是线程不安全的, 比如pojo.
但是如果要保证线程安全,可以将Bean的作用域改为prototype 比如像 ModelView。 另外还可以采用ThreadLocal来解决线程安全问题。ThreadLocal为每个线程保存一个副本变量, 每个线程只操作自己的副本变量。

Spring如何处理循环依赖问题?

循环依赖: 多个对象之间存在循环的引用关系,在初始化过程当中,就会出现"先有蛋还是先有鸡"的问题。
一种是使用@Lazy注解: 解决构造方法造成的循环依赖问题
另一种是使用三级缓存:
一级缓存: 缓存最终的单例池对象: private final Map singletonObjects = new ConcurrentHashMap<>(256);
二级缓存: 缓存初始化的对象: private final Map earlySingletonObjects = new ConcurrentHashMap<>(16);
三级缓存: 缓存对象的ObjectFactory: private final Map> singletonFactories = new HashMap<>(16);
对于对象之间的普通引用,二级缓存会保存new出来的不完整对象,这样当单例池中找到不依赖的属性时,就可以先从二级缓存中获取到不完整对象,完成对象创建,在后续的依赖注入过程中,将单例池中对象的引用关系调整完成。
三级缓存:如果引用的对象配置了AOP,那在单例池中最终就会需要注入动态代理对象,而不是原对象。而生成动态代理是要在对象初始化完成之后才开始的。于是Spring增加三级缓存,保存所有对象的动态代理配置信息。在发现有循环依赖时,将这个对象的动态代理信息获取出来,提前进行AOP,生成动态代理。 核心代码就在DefaultSingletonBeanRegistry的getSingleton方法当中。

    @Nullable
    protected Object getSingleton(String beanName, boolean allowEarlyReference) {
        Object singletonObject = this.singletonObjects.get(beanName);
        if (singletonObject == null && this.isSingletonCurrentlyInCreation(beanName)) {
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null && allowEarlyReference) {
                synchronized(this.singletonObjects) {
                    singletonObject = this.singletonObjects.get(beanName);
                    if (singletonObject == null) {
                        singletonObject = this.earlySingletonObjects.get(beanName);
                        if (singletonObject == null) {
                            ObjectFactory singletonFactory = (ObjectFactory)this.singletonFactories.get(beanName);
                            if (singletonFactory != null) {
                                singletonObject = singletonFactory.getObject();
                                this.earlySingletonObjects.put(beanName, singletonObject);
                                this.singletonFactories.remove(beanName);
                            }
                        }
                    }
                }
            }
        }
 
        return singletonObject;
    }

Spring如何处理事务?
Spring当中支持编程式事务管理和声明式事务管理两种方式:
1、编程式事务可以使用TransactionTemplate。
2、声明式事务: 是Spring在AOP基础上提供的事务实现机制。他的最大优点就是不需要在业务代码中添加事务管理的代码,只需要在配置文件中做相关的事务规则 声明就可以了。但是声明式事务只能针对方法级别,无法控制代码级别的事务管理。
Spring中对事务定义了不同的传播级别: Propagation
1、 PROPAGATION_REQUIRED:默认传播行为。 如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入到事务中。
2、PROPAGATION_SUPPORTS: 如果当前存在事务,就加入到该事务。如果当前不存在事务,就以非事务方式运行。
3、PROPAGATION_MANDATORY: 如果当前存在事务,就加入该事务。如果当前不存在事务,就抛出异常。
4、PROPAGATION_REQUIRES_NEW: 无论当前存不存在事务,都创建新事务进行执行。
5、PROPAGATION_NOT_SUPPORTED: 以非事务方式运行。如果当前存在事务,就将当前事务挂起。
6、PROPAGATION_NEVER : 以非事务方式运行。如果当前存在事务,就抛出异常。
7、PROPAGATION_NESTED: 如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则按REQUEIRED属性执行。
Spring中事务的隔离级别:
1、ISOLATION_DEFAULT: 使用数据库默认的事务隔离级别。
2、ISOLATION_READ_UNCOMMITTED: 读未提交。允许事务在执行过程中,读取其他事务未提交的数据。
3、ISOLATION_READ_COMMITTED: 读已提交。允许事务在执行过程中,读取其他事务已经提交的数据。
4、ISOLATION_REPEATABLE_READ: 可重复读。 在同一个事务内,任意时刻的查询结果是一致的。
5、ISOLATION_SERIALIZABLE: 所有事务依次执行。

SpringMVC中的控制器是不是单例模式?如果是,如何保证线程安全?

控制器是单例模式。
单例模式下就会有线程安全问题。
Spring中保证线程安全的方法
1、将scop设置成非singleton。 prototype, request。
2、最好的方式是将控制器设计成无状态模式。在控制器中,不要携带数据。但是可以引用无状态的service和dao。

MySQL数据库篇

MySQL有哪几种数据存储引擎?有什么区别?
MySQL中通过show ENGINES指令可以看到所有支持的数据库存储引擎。 最为常用的就是MyISAM 和InnoDB 两种。
MyISAM和InnDB的区别:
1、存储文件。 MyISAM每个表有两个文件。 MYD和MYISAM文件。 MYD是数据文件。 MYI是索引文件。 而InnDB每个表只有一个文件,idb。
2、InnoDB支持事务,支持行级锁,支持外键。
3、InnoDB支持XA事务
4、InnoDB支持savePoints

什么是脏读、幻读、不可重复读?要怎么处理?
这些问题都是MySQL进行事务并发控制时经常遇到的问题。
脏读: 在事务进行过程中,读到了其他事务未提交的数据。
不可重复读: 在一个事务过程中,多次查询的结果不一致。
幻读: 在一个事务过程中,用同样的操作查询数据,得到的记录数不相同。
处理的方式有很多种:加锁、事务隔离、MVCC加锁:
1、脏读:在修改时加排他锁,直到事务提交才释放。读取时加共享锁,读完释放锁。
2、不可重复读: 读数据时加共享锁,写数据时加排他锁。
3、幻读: 加范围锁。

事务的基本特性和隔离级别有哪些?
事务: 表示多个数据操作组成一个完整的事务单元,这个事务内的所有数据操作要么同时成功,要么同时失败。
事务的特性 :ACID
1、原子性:事务是不可分割的,要么完全成功,要么完全失败。
2、一致性:事务无论是完成还是失败,都必须保持事务内操作的一致性。当失败时,都要对前面的操作进行回滚,不管中途是否成功。
3、隔离性:当多个事务操作一个数据的时候,为防止数据损坏,需要将每个事务进行隔离,互相不干扰。
4、持久性: 事务开始就不会终止。他的结果不受其他外在因素的影响。
事务的隔离级别 :SHOW VARIABLES like 'transaction%'
设置隔离级别: set transaction level xxx 设置下次事务的隔离级别。
set session transaction level xxx 设置当前会话的事务隔离级别
set global transaction level xxx 设置全局事务隔离级别
MySQL当中有五种隔离级别
NONE : 不使用事务。
READ UNCOMMITED: 允许脏读
READ COMMITED: 防止脏读,最常用的隔离级别
REPEATABLE READ:防止脏读和不可重复读。MYSQL默认
SERIALIZABLE: 事务串行,可以防止脏读、幻读,不可重复度。
五种隔离级别,级别越高,事务的安全性是更高的,但是,事务的并性能也就会越低。

MySQL的锁有哪些?什么是间隙锁?
从锁的粒度来区分
1、行锁:加锁粒度小,但是加锁资源开销比较大。 InnDB支持。
共享锁: 读锁。多个事务可以对同一个数据共享同一把锁。持有锁的事务都可以访问数据,但是只能读不能修改。select xxx LOCK IN SHARE MODE。
排他锁: 写锁。只有一个事务能够获得排他锁,其他事务都不能获取该行的锁。 InnoDB会对update\delete\insert语句自动添加排他锁。SELECT xxx FOR UPDATE。
自增锁: 通常是针对MySQL当中的自增字段。如果有事务回滚这种情况,数据会回滚,但是自增序列不会回滚。
2、表锁:加锁粒度大,加锁资源开销比较小。MyISAM和InnoDB都支持。
表共享读锁
表排他写锁
意向锁: 是InnoDB自动添加的一种锁,不需要用户干预。
3、全局锁: Flush tables with read lock 。 加锁之后整个数据库实例都处于只读状态。所有的数据变更操作都会被挂起。一般用于全库备份的时候。
常见的锁算法: user: userid ( 1,4,9) update user set xxx where userid=5; REPEATABLE READ 间隙锁锁住(5,9)
记录锁: 锁一条具体的数据。
间隙锁: RR隔离级别下,会加间隙锁。锁一定的范围,而不锁具体的记录。是为了防止产生幻读。(-xx,1)(1,4)(4,9)(9,xxx)
Next-key : 间隙锁+右记录锁。(-xx,1](1,4](4,9](9,xxx)

MySQL的索引结构是什么样的?聚簇索引和非聚簇索引又是什么?
二叉树 -》 AVL树 -》 红黑树 -》 B-树 -》 B+树
二叉树: 每个节点最多只有两个子节点, 左边的子节点都比当前节点小,右边的子节点都比当前节点大。
AVL树: 树中任意节点的两个子树的高度差最大为1
红黑树:
1)每个节点都是红色或者黑色。
2)根节点是黑色。
3)每个叶子节点都是黑色的空节点。
4)红色节点的父子节点都必须是褐色。
5)从任一节点到其每个叶子节点的所有路径都包含相同的黑色节点。
B-树:
1)B-树的每个非叶子节点的子节点个数都不会超过D(这个D就是B-树的阶)
2)所有的叶子节点都在同一层。3.所有节点关键字都是按照递增顺序排列。
B+树:
1)非叶子节点不存储数据,只进行数据索引。
2)所有数据都存储在叶子节点当中。
3)每个叶子节点都存有相邻叶子节点的指针。
4)叶子节点按照本身关键字从小到大排序。
聚簇索引就是数据和索引是在一起的。
MyISAM使用的是非聚簇索引,树的子节点上的data不是数据本身,而是数据存放的地址。
InnoDB采用的是聚簇索引,树的叶子节点上的data就是数据本身。
聚簇索引的数据物理存放顺序和索引顺序是一致的,所以一个表当中只能有一个聚簇索引,而非聚簇索引可以有多个。
InnoDB中,如果表定义了PK,那PK就是聚簇索引。 如果没有PK,就会找第一个非空的unique列作为聚簇索引。否则,InnoDB会创建一个隐藏的row-id作为聚簇索引。
MySQL的覆盖索引和回表:
如果只需要在一颗索引树上就可以获取SQL所需要的所有列,就不需要再回表查询,这样查询速度就可以更快。
实现索引覆盖最简单的方式就是将要查询的字段,全部建立到联合索引当中。
user(PK id , name ,sex)
select count(name) from user ; -> 在name 字段上建立一个索引。
select id , name ,sex from user; -> 将name上的索引升级成为(name,sex)的联合索引。

MySQL的集群是如何搭建的?读写分离是怎么做的?
MySQL通过将主节点的Binlog同步给从节点完成主从之间的数据同步。 MySQL的主从集群只会将binlog从主节点同步到从节点,而不会反过来同步。由此也就引申出了读写分离的问题。 因为要保证主从之间的数据一致, 写数据的操作只能在主节点完成, 而读数据的操作,可以在主节点或者从节点上完成。

谈谈如何对MySQL进行分库分表?多大数据量需要进行分库分表?分库分表的方式和分片策略由哪些?分库分表后,SQL语句的执行流程是怎样的?
什么是分库分表?
就是当表中的数据量过大时,整个查询效率就会降低得非常明显。这时为了提升查询效率,就要将一个表中的数据分散到多个数据库的多个表当中。
分库分表最常用的组件: Mycat\ ShardingSphere
数据分片的方式有垂直分片和水平分片。
垂直分片就是从业务角度将不同的表拆分到不同的库中,能够解决数据库数据文件过大的问题,但是不能从根本上解决查询问题。
水平分片就是从数据角度将一个表中的数据拆分到不同的库或表中,这样可以从根本上解决数据量过大造成的查询效率低的问题。
有非常多的分片策略:比如 取模、按时间、按枚举值...
阿里提供的开发手册当中,建议:一个表的数据量超过500W或者数据文件超过2G,就要考虑分库分表了。

Java开发面试专题_第13张图片

一个user表,按照userid进行了分片,然后我需要按照sex字段去查,这要怎么查?
强制指定只查一个数据库,要怎么做?查询结果按照userid来排序,要怎么排?
分库分表的问题: 垮库查询、跨库排序、分布式事务、公共表、主键重复...

搜索引擎篇

什么是倒排索引?有什么好处?

ES了解多少?说说你们公司的ES集群架构?

如何进行中文分词?用过哪些分词器?
IK Analyzer 
ICU Analyzer

ES写入数据的工作原理是什么?
1、客户端发写数据的请求时,可以发往任意节点。这个节点就会成为coordinating node协调节点。
2、计算的点文档要写入的分片:计算时就采用hash取模的方式来计算。
3、协调节点就会进行路由,将请求转发给对应的primary sharding所在的datanode。
4、datanode节点上的primary sharding处理请求,写入数据到索引库,并且将数据同步到对应的replica sharding
5、等primary sharding 和 replica sharding都保存好文档了之后,返回客户端响应。

ES查询数据的工作原理是什么?
1、客户端发请求可发给任意节点,这个节点就成为协调节点。
2、协调节点将查询请求广播到每一个数据节点,这些数据节点的分片就会处理改查询请求。
3、每个分片进行数据查询,将符合条件的数据放在一个队列当中,并将这些数据的文档ID、节点信息、分片信息都返回给协调节点。
4、由协调节点将所有的结果进行汇总,并排序。
5、协调节点向包含这些文档ID的分片发送get请求,对应的分片将文档数据返回给协调节点,最后协调节点将数据整合返回给客户端

ES部署时,要如何进行优化?
1)集群部署优化
调整ES的一些重要参数。path.data目录尽量使用SSD。定时JVM堆内存大小。 关于ES的参数,大部分情况下是不需要调优的,如果有性能问题,最好的办法是安排更合理的sharding布局并且增加节点数量。
2)更合理的sharding布局
让sharding和对应的replica sharding尽量在同一个机房。
3)Linux服务器上的一些优化策略
不要用root用户;修改虚拟内存大小;修改普通用户可以创建的最大线程数。
ES生态: ELK日志收集解决方案- filebeat(读log日志)-> logstash -> ElasticSearch -> kibana、Grafana、自研的报表平台。

安全验证篇​​​​​​​

什么是认证和授权?如何设计一个权限认证框架?

认证: 就是对系统访问者的身份进行确认。 用户名密码登录、 二维码登录、手机短信登录、指纹、刷脸......

授权:就是对系统访问者的行为进行控制。授权通常是在认证之后,对系统内的用户隐私数据进行保护。后台接口访问权限、前台控件的访问权限。

​​​​​​​RBAC模型: 主体 -》 角色 -》 资源 -》访问系统的行为。

Cookie和Session有什么区别?
当服务器tomcat第一次接收到客户端的请求时,会开辟一块独立的session空间, 建立一个session对象,同时会生成一个session id,通过响应头的方式保存到客户端浏览器的cookie当中。以后客户端的每次请求,都会在请求头部带上这个session id,这样就可以对应上服务端的一些会话的相关信息,比如用户的登录状态。
如果没有客户端的Cookie,Session是无法进行身份验证的。
当服务端从单体应用升级为分布式之后,cookie+session这种机制要怎么扩展?
1、session黏贴: 在负载均衡中,通过一个机制保证同一个客户端的所有请求都会转发到同一个tomcat实例当中。问题: 当这个tomcat实例出现问题之后,请求就会被转发到其他实例,这时候用户的session信息就丢了。
2、session复制: 当一个tomcat实例上保存了session信息后,主动将session 复制到集群中的其他实例。问题: 复制是需要时间的,在复制过程中,容易产生session信息丢失。
3、session共享: 就是将服务端的session信息保存到一个第三方中,比如Redis。

什么是CSRF攻击?如何防止?
CSRF: Cross Site Requst Forgery 跨站请求伪造
一个正常的请求会将合法用户的session id保存到浏览器的cookie。这时候,如果用户在浏览器中打来另一个tab页, 那这个tab页也是可以获得浏览器的cookie。黑客就可以利用这个cookie信息进行攻击。
攻击过程:
1、某银行网站A可以以GET请求的方式发起转账操作。 www.xxx.com/transfor.do?accountNum=100&money=1000 accountNum表示目标账户。这个请求肯定是需要登录才可以正常访问的。
2、攻击者在某个论坛或者网站上,上传一个图片,链接地址是 www.xxx.com/transfer.do?accountNum=888&money=10000 其中这个accountNum就是攻击者自己的银行账户。
3、如果有一个用户,登录了银行网站,然后又打开浏览器的另一个tab页,点击了这个图片。这时,银行就会受理到一个带了正确cookie的请求,就会完成转账。用户的钱就被盗了。
CSRF方式方式:
1、尽量使用POST请求,限制GET请求。POST请求可以带请求体,攻击者就不容易伪造出请求。
2、将cookie设置为HttpOnly : respose.setHeader("SetCookie","cookiename=cookievalue;HttpOnly")。
3、增加token; 在请求中放入一个攻击者无法伪造的信息,并且该信息不存在于cookie当中。这也是Spring Security框架中采用的防范方式。

什么是OAuth2.0协议?有哪几种认证方式?什么是JWT令牌?和普通令牌有什么区别?
OAuth2.0是一个开放标准,允许用户授权第三方应用程序访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方应用或分享他们数据的所有内容。
OAuth2.0协议的认证流程,简单理解,就是允许我们将之前的授权和认证过程交给一个独立的第三方进行担保。
OAuth2.0协议有四种认证方式:
1、授权码模式
2、简化模式
3、密码模式
4、客户端模式

在梳理OAuth2.0协议流程的过程中,其实有一个主线,就是三方参与者之家的信任程度。
普通令牌: b9f2eaa1-8715-4f03-86c7-06bf757a5f7c
普通令牌只是一个随机的字符串,没有特殊的意义。这就意味着,当客户带上令牌去访问应用的接口时,应用本身无法判断这个令牌是否正确,他就需要到授权服务器上去判断令牌是否有效。在高并发场景下,检查令牌的网络请求就有可能成为一个性能瓶颈。
改良的方式就是JWT令牌:将令牌对应的相关信息全部冗余到令牌本身,这样资源服务器就不再需要发送请求给授权服务器去检查令牌了,他自己就可以读取到令牌的授权信息。JWT令牌的本质就是一个加密的字符串!!

什么是SSO?与OAuth2.0有什么关系?

OAuth2.0的使用场景通常称为联合登录。 
一处注册,多处使用
SSO Single Sign On 单点登录。 一处登录,多处同时登录
​​​​​​​SSO的实现关键是将Session信息集中存储。Spring Security。

如何设计一个开放授权平台?
开放授权平台也可以按照认证和授权两个方向来梳理。
1、认证: 就可以按照OAuth2.0协议来规划认证的过程。
2、授权: 首先需要待接入的第三方应用在开放授权平台进行注册,注册需要提供几个必要的信息 clintID, 消息推送地址,密钥(一对公私钥,私钥由授权平台自己保存,公钥分发给第三方应用)。
然后,第三方应用引导客户发起请求时,采用公钥进行参数加密,授权开放平台使用对应的私钥解密。
接下来:授权开放平台同步响应第三方应用的只是消息是否处理成功的结果。而真正的业务数据由授权开放平台异步推动给第三方应用预留的推送地址。
 

你可能感兴趣的:(java,面试,jvm)