面试题由浅入深

Java多线程相关

 

  • 线程池的原理,为什么要创建线程池?创建线程池的方式;

答:线程池原理:通过本地缓存来管理事先创建好的线程,达到线程复用的目的,减少重复创建线程和释放线程带来的资源消耗。
      为什么创建线程池:线程的运行周期:从重复创建到销毁不仅很会消耗系统资源,还会降低系统的稳定性,创建线程池可以降低系统消耗,提高响应速度,提高线程的可管理型。
      创建线程池的方式:newFixedThreadPool(int nThreads, ThreadFactory threadFactory) ://一个固定线程数量的线程池:corePoolSize跟maximumPoolSize值一样,同时传入一个无界阻塞队列,该线程池的线程会维持在指定线程数,不会进行回收.
                                    newCachedThreadPool();//这个线程池corePoolSize为0,maximumPoolSize为Integer.MAX_VALUE ,意思也就是说来一个任务就创建一个woker,回收时间是60s
                                     newSingleThreadExecutor();//创建一个只能执行一个任务的线程,其他任务放到阻塞队列;
                                     newScheduledThreadPool(int corePoolSize);//支持定时以指定周期循环执行任务:
最常用的只有一个: ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,RejectedExecutionHandler handler)
  • 线程的生命周期,什么时候会出现僵死进程;

线程的生命周期: 如图线程生命周期有6个状态:新建,运行状态(就绪和运行中),阻塞,等待,超市等待,死亡。
僵死进程:A,B两个线程相互死锁。
  • 说说线程安全问题,什么实现线程安全,如何实现线程安全;

线程安全问题:多个线程之间共享一个数据,对同一个数据进行修改,并且线程之间并不知道对方修改了数据,造成数据安全问题。官方语言就是不能保证操作的原子性,可见性,顺序性。
实现线程安全:保证原子性——对共享数据进行加锁保证同一时刻只有一个线程进行修改。可见性——Java提供了volatile关键字来保证可见性。顺序性——Java中可通过volatile在一定程序上保证顺序性,另外还可以通过synchronized和锁来保证顺序性。
  • 创建线程池有哪几个核心参数?如何合理配置线程池的大小?

核心参数:corePoolSize:创建线程池时线程池中保留的线程数量。
                 maximumPoolSize:线程池中容纳的最大的线程数量
                 keepAliveTime:当线程池中的数量>corePollSize时并且线程空闲一段时间keepAliveTime后,多出corePollSize的线程会被回收。
                 unit:时间keepAliveTime参数的时间单位,有毫秒,秒,分钟等等单位
                 workQueue:当任务task数量>corePoolSize时,会将task存放在workQueue中。当workQueue存放不下时,会创建不大于maximumPoolSize数量的线程执行task。
                 threadFactory:创建线程的工厂。
                 handler:当任务task大于maximumPoolSize会执行拒绝策略。
  • volatile、ThreadLocal的使用场景和原理;

volatile:一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰后,就具备了两层语义:保证了不同线程对这个变量进行操作时的可见性和禁止了指令重排序。
       在加入volatile关键字时,会多出一个lock前缀指令。lock前缀指令相当于一个内存屏障,其提供三个功能。
  1、它会强制将对缓存的修改操作立即写入主内存。
  2、如果是写操作,它会导致其他CPU中对应的缓存行无效
  3、它确保指定重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面。即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。
ThreadLocal:用于线程之间的数据隔离,每个线程thread中都有一个ThreadLocalMap,ThreadLocalMap中有个Entry[]数组,当threadLocad.set(AAA),实际是将threadLocal为key进行hash得到一个数组下标索引k,然后将AAA存进ThreadLocalMapEntry[]数组Entry[k]中。threadLocal.get()时根据
threadLocalMap.getEntry(threadLocal) 将threadLocal进行hash得到一个数组下标索引k,然后ThreadLocalMapEntry[]数组Entry[k]得到数据。
  • ThreadLocal什么时候会出现OOM的情况?为什么?

每个线程thread中都有一个ThreadLocalMap,ThreadLocalMap中有个Entry[]数组,当threadLocad.set(AAA),实际是将threadLocal作为key进行hash得到一个数组下标索引k,然后将AAA存进ThreadLocalMapEntry[]数组Entry[k]中。而threadLocal是一个弱引用,如果没有强引用对threadLocal进行引用的话,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏OOM。
  • synchronized、volatile区别、synchronized锁粒度、模拟死锁场景、原子性与可见性;

synchronized、volatile区别:
1.synchronized是锁定当前变量、方法、和类级别的,使当前元素同一时间只能被一个线程访问;而volatile修饰变量级别,目的是告诉jvm当前变量在寄存器中不确定需要到内存中取值,而且告诉jvm不能对该变量进行指令重排序。
2.volatile不能保证原子性,只能保证修改可见性;而synchronized能保证原子性和修改可见性。
3.volatile不会造成线程阻塞;synchronized会造成线程堵塞。

synchronized锁粒度:synchronized对对象进行上锁——多线程调用同一对象的上锁代码,只能有一个线程运行,其他的线程等待。多线程调用不同对象的上锁代码,能够同时进行;synchronized对类进行上锁——多线程调用同一个对象或者类的上锁代码,只能有一个线程运行,其他线程等待。多线程调用不同对象或者类的上锁代码,也只能有一个线程运行,其他线程等待。
模拟死锁场景:Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock1){
                    try{
                        System.out.println(Thread.currentThread().getName()+"获得了lock1锁");
                        Thread.sleep(500);//sleep的原因是等待thread2获取锁
                    }catch (InterruptedException ex){
                        ex.printStackTrace();
                    }
                    synchronized (lock2){
                        System.out.println(Thread.currentThread().getName()+"获得了lock2锁");
                    }
                }
            }
        },"Thread1");
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock2){
                    try{
                        System.out.println(Thread.currentThread().getName()+"获得了lock2锁");
                        Thread.sleep(500);
                    }catch (InterruptedException ex){
                        ex.printStackTrace();
                    }
                    synchronized (lock1){
                        System.out.println(Thread.currentThread().getName()+"获得了lock1锁");
                    }
                }
            }
        },"Thread2");
        thread1.start();
        thread2.start();

原子性:(jdk1.6版本以前)每一个对象都有一个Monitor对象,线程通过执行monitorenter指令尝试获取Monitor对象的拥有权,如果拥有当前Monitor对象的线程数为0,则将_count++,当前线程称为Monitor对象的拥有者。如果当前线程已经拥有了此Monitor对象,则将_count++即可。如果其他线程已经拥有了此Monitor对象,则当前线程阻塞直到Monitor的计数_count==0,然后重新竞争获取锁。当线程执行到monitorenter指令,会进入ObjectMonitor对象的_EntryList队列,通过CAS会将_owner指针指向当前线程,同时_count++,当前线程执行monitorexit指令,会释放持有的Monitor对象,并将_owner置为null同时_count--如果调用wait(),同上,但是会进入_WaitSet队列,等待被唤醒。(看到没:wait状态的线程在唤醒之后,还得需要获取锁④,然后执行完毕)。更新版本具体可以参考:https://blog.csdn.net/javazejian/article/details/72828483#synchronized%E5%BA%95%E5%B1%82%E8%AF%AD%E4%B9%89%E5%8E%9F%E7%90%86
可见性:释放锁会将缓存刷新到主存。

JVM相关

 

  • JVM内存模型,GC机制和原理;

GC机制和原理:我们新创建的对象一般先存放在eden区,当Eden区满了对象无法再加入时,就会进行一次Minor GC,把eden区存活的对象转移到from区(s0区域);
             eden被清空继续存放新创建的对象,随着对象增多eden区又不足了触发Minor GC清理eden和s0把存活的对象放入s1(to区); 
             eden继续存放新创建的对象,随着对象增多eden区又不足了触发Minor GC清理eden和s1把存活的对象放入s0区;
             反反复复当对象被MInor GC15次后还存活该对象就会进入老年区;
特殊情况:创建的对象太大eden区存不下,直接放入老年区中;
         大于这个参数PretenureSizeThreshold的对象直接放入老年区中;
         如果在From空间中,相同年龄所有对象的大小总和大于From和To空间总和的一半,那么年龄大于等于该年龄的对象就会被移动到老年代,而不用等到15岁;

 

  • GC分哪两种,Minor GC 和Full GC有什么区别?什么时候会触发Full GC?分别采用什么算法?

GC分为Minor GC和Full GC;
Minor GC:新生代进行一次GC操作称为Minor GC;
Full GC: 是清理整个堆空间的一次GC—包括年轻代和老年代。
Minor GC采用的时coping算法,老年代GC采用的是标记-清除算法
Full GC采用的是单线程收集
  • JVM里的有几种classloader,为什么会有多种?

JVM中有bootstrapClassLoad 、ExtensionClassLoad、AppClassload三种类加载器,每个加载器分别加载不同路径下面的class。
bootstrap:加载jre\lib\
extension:加载jre\lib\ext\
appclassLoad:加载classpath下面的类
如果不用三中只有一种bootstrapClassLoad的话,用户调用自己的类加载器去加载JDK中的类,可以对jdk中其他类默认访问修饰符属性和方法的能力。会对原生的jdk造成破坏。
  • 什么是双亲委派机制?介绍一些运作过程,双亲委派模型的好处;

双亲委派机制:jdk1.2开始采用的是父亲委托机制,除了虚拟机自带的根类加载器之外,其余的类加载器有且只有一个父加载器。当java程序请求加载器loader1在在Sample类时,loader1首先会委托自己的父加载器去加载Sample类,若父加载器能加载,就让父加载器完成任务,否则才让加载器本身加载Sample类(一般我们自己不会定义类加载器,所有的类基本上是App ClassLoader帮我们加载);
 好处:1.可以确保java核心库的类型安全;
     2.可以确保java核心类库所提供的类不会被自定义的类所取代。   
     3.不同的类加载器可以为相同的名称(binary name)的类创建额外的命名空间,相同名称的类可以并存在java虚拟机中,只需要用不同的类加载器来加载他们即可。不同类加载器所加载的类之间是不兼容的,这就是相当于在java虚拟机内部创建了一个又一个相互隔离的java类空间,这类技术在很多框架中都得到了很多实际应用。
  • 什么情况下我们需要破坏双亲委派模型;

SPI机制坏破坏双亲委托机制—— 在java的jdk中有大量的SPI实现(JDK定义了接口,并没有功能实现,实现需要用户端自己实现,比如:数据库驱动),由于这些实现是父classloader可以使用当前线程Thread.currentThread().getContextClassLoader()所制定的classloader加载的类,这就改变了父classloader不能使用子classloader或者其他没有直接父子关系的classloader加载的类的情况,即改变了双亲委托模型。(有些接口是java核心库提供的,而java核心库是由启动类加载器来加载的,而这些接口的实现却来自不同的jar包(厂商提供),java的启动类加载器是不会加载其他来源的jar包,这样传统的双亲委托模型就无法满足SPI要求。而通过给当前线程设置上下文类加载器,就可以由设置的上下文类加载器来实现对于接口实现类的加载 。)
  • 常见的JVM调优方法有哪些?可以具体到调整哪个参数,调成什么值?

堆配置
-Xms:初始堆大小
-Xmx:最大堆内存大小,一般Xms和Xmx设置一样大,不然会有扩容缩容消耗
-XX:NewSize=n;设置年轻代大小
-XX:NewRatio=n;设置年轻代和年老代的比值。如:为3表示年轻代和年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
-XX:SurvivorRatio=n;年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如3表示Eden: 3 Survivor:2,一个Survivor区占整个年轻代的1/5
-XX:MaxPermSize=n;设置持久代大小
具体可以参考:https://www.liangzl.com/get-article-detail-17354.html
  • JVM虚拟机内存划分、类加载器、垃圾收集算法、垃圾收集器;

具体的参考:https://blog.csdn.net/a78270528/article/details/81867862;

Redis

  • Redis为什么这么快?redis采用多线程会有哪些问题?

redis快:1.redis读写数据都是在内存中;
               2.redis是单线程的避免多线程阻塞和线程切换;
               3.底层是c语言编写;
多线程问题:数据安全问题;
           阻塞问题;
           线程之间的管理问题
  • Redis支持哪几种数据结构;

redis支持String  hash  list   set   zset五种数据结构。
 后来新增了BitMaps位图   HyperLogLog超小内存唯一值计数   GEO地理信息定位
  • Redis跳跃表的问题;

redis的跳跃表由zskiplist和zskiplistNode组成
typedef struct zskiplist {
    // 头节点,尾节点
    struct zskiplistNode *header, *tail;
    // 节点数量
    unsigned long length;
    // 目前表内节点的最大层数
    int level;
} zskiplist;
typedef struct zskiplistNode {
    // member 对象
    robj *obj;
    // 分值
    double score;
    // 后退指针
    struct zskiplistNode *backward;
    // 层
    struct zskiplistLevel {
        // 前进指针
        struct zskiplistNode *forward;
        // 这个层跨越的节点数量
        unsigned int span;
    } level[];
} zskiplistNode;

详情可以参考https://blog.csdn.net/lz710117239/article/details/78408919
  • Redis单进程单线程的Redis如何能够高并发?

如图:redis采用多路复用模式,可以并发处理client请求,并将他们塞进队列中进行排队处理。内部实现采用epoll,采用了epoll+自己实现的简单的事件框架。epoll中的读、写、关闭、连接都转化成了事件,然后利用epoll的多路复用特性,绝不在io上浪费一点时间
  • Redis如何使用Redis实现分布式锁?

jedis实现原理:互斥性——同一时间只能有一个线程拿到锁
              安全性——只有加锁的服务才能解锁
              避免死锁—— 要有加锁也能解锁    
java实现加锁:String var1 = jedis.set(key,value,"NX","EX",timeOut); //给一个key如果在redis中不存在就设置value值,“NX”表示不存在就添加,“EX”表示设置过期时间java实现解锁: Object var2 = jedis.eval(luaScript,Collections.singletonList(key), Collections.singletonList(value));//jedis并没有提供删除指定value值得key,但是lua脚本提供了该实现。保证谁加的锁谁才能解锁。
redission实现原理:Redisson使用非阻塞的I/O和基于Netty框架的事件驱动的通信层,其方法调用是异步的。Redisson的API是线程安全的,所以可以操作单个Redisson连接来完成各种操作。并且redission还有可重入锁,公平锁,连锁,红锁,读写锁等多功能锁的APi实现
//可重入锁RLock lock = redisson.getLock("anyLock");
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
//公平锁
RLock fairLock = redisson.getFairLock("anyLock");//优先分配给先发出请求的线程。所有请求线程会在一个队列中排队,当某个线程出现宕机时,Redisson会等待5秒后继续下一个线程,也就是说如果前面有5个线程都处于等待状态,那么后面的线程会等待至少25秒
//联锁
RLock lock1 = redissonInstance1.getLock("lock1"); 
RLock lock2 = redissonInstance2.getLock("lock2"); 
RLock lock3 = redissonInstance3.getLock("lock3");   
RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3); // 同时加锁:lock1 lock2 lock3  所有的锁都上锁成功才算成功。
//红锁
RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");  
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3); // 同时加锁:lock1 lock2 lock3 大部分的锁都上锁成功才算成功。
//读写锁
rwlock.readLock().lock(10, TimeUnit.SECONDS);// 10秒钟以后自动解锁  无需调用unlock方法手动解锁 
rwlock.writeLock().lock(10, TimeUnit.SECONDS);
boolean res = rwlock.readLock().tryLock(100, 10, TimeUnit.SECONDS); //尝试加锁,最多等待100秒,上锁以后10秒自动解锁 
boolean res = rwlock.writeLock().tryLock(100, 10, TimeUnit.SECONDS);
加锁

解锁
  • Redis分布式锁操作的原子性,Redis内部是如何实现的?

redis的单个命令都是原子性的,
事务保证原子性:MULTI——>BEGIN;EXEC——>COMMIT;DISCARD——>ROLLBACK;WATCH——>mysql锁的功能。
redis事务的实现原理是把事务中的命令先放入队列中,当client提交了exec命令后,redis会把队列中的每一条命令按序执行一遍。如果在执行exec之前事务中断了,那么所有的命令都不会执行;如果执行了exec命令之后,那么所有的命令都会按序执行。但如果在事务执行期间redis被强制关闭,那么则需要使用redis-check-aof 工具对redis进行修复,删除那些部分执行的命令。当redis在执行命令时,如果出现了错误,那么redis不会终止其它命令的执行。即只要是正确的命令,无论在错误命令之前还是之后,都会顺利执行。
lua脚本保证原子性:redis确保正一条script脚本执行期间,其它任何脚本或者命令都无法执行。正是由于这种原子性,script才可以替代MULTI/EXEC作为事务使用。

Java高级部分

  • 红黑树的实现原理和应用场景;

红黑树代码实现:
public class RBTNode<T extends Comparable<T>> {
        public boolean isBlack;
        public T key;
        public RBTNode<T> parent;
        public RBTNode<T> left;
        public RBTNode<T> right;
}
红黑树的特性:1.每个节点不是红色就是黑色;
             2.根节点和叶子节点都是黑色;
             3.不能有两个连续节点都是红色;
             4.根节点到叶子节点经过的黑色节点要相等;
应用场景:jdk中的TreeSet和HashMap的实现。
  • NIO是什么?适用于何种场景?

NIO(noblocking io非阻塞)是相对于BIO进行的io优化,BIO是clientsocket去请求serverSocket,建立的serverSocket会一直阻塞监听cilentSocket的连接请求,当有socket请求过来时,serverSocket会建立一个新的socket与cilentSocket进行传输数据,当新建的socket获取数据输出流时,这是一个阻塞的过程(有其他的clientSocket无法建立连接请求),单线程用BIO就有阻塞问题。另外clientSocket在获取输入流读取获取数据的时候也有可能一直阻塞。服务端两个阻塞点erverSocket.accept()和inputStream.read(bytes);           
    NIO:改进BIO中以读写流的方式传输数据,而是以通道和缓冲区的形式传输数据的。所有的数据将会存进buffer缓冲区中,然后让buffer在channel通道中进行传输,client与server之间数据数据传输client——buffer——channel——channel——buffer——server这样形式 。
服务端代码:
public class ServerSocketChannels implements Runnable {
    //服务端通道
    private ServerSocketChannel serverSocketChannel;
    //轮训选择器
    private Selector selector;
    //是否停止
    private volatile boolean stop;

    public ServerSocketChannels(int port) {
        try {
            //创建多路复用器selector,工厂方法
            selector = Selector.open();
            //创建ServerSocketChannel,工厂方法
            serverSocketChannel = ServerSocketChannel.open();
            //绑定ip和端口号,默认的IP=127.0.0.1,对连接的请求最大队列长度设置为backlog=1024,如果队列满时收到连接请求,则拒绝连接
            serverSocketChannel.socket().bind(new InetSocketAddress(port), 1024);
            //设置非阻塞方式
            serverSocketChannel.configureBlocking(false);
            //注册serverSocketChannel到selector多路服用器上面,监听accrpt请求
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("the time is start port = " + port);
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }
    }

    public void stop() {
        this.stop = true;
    }

    /**
     * selector.select()会一直阻塞到有一个通道在你注册的事件上就绪了.
     * 所以起一个线程防止主线程堵塞
     */
    @Override
    public void run() {
        //如果server没有停止
        while (!stop) {
            try {
                //selector.select()会一直阻塞到有一个通道在你注册的事件上就绪了
                //selector.select(1000)会阻塞到1s后然后接着执行,相当于1s轮询检查
                selector.select(1000);
                //找到所有准备接续的key
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> it = selectionKeys.iterator();
                SelectionKey key = null;
                while (it.hasNext()) {
                    key = it.next();
                    it.remove();
                    try {
                        //处理准备就绪的key
                        handle(key);
                    } catch (Exception e) {
                        if (key != null) {
                            //请求取消此键的通道到其选择器的注册
                            key.cancel();
                            //关闭这个通道
                            if (key.channel() != null) {
                                key.channel().close();
                            }
                        }
                    }
                }
            } catch (Throwable e) {
                e.printStackTrace();
            }
        }
        if (selector != null) {
            try {
                selector.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public void handle(SelectionKey key) throws IOException {
        //如果key是有效的
        if (key.isValid()) {
            //监听到有新客户端的接入请求
            //完成TCP的三次握手,建立物理链路层
            if (key.isAcceptable()) {
                ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                SocketChannel sc = (SocketChannel) ssc.accept();
                //设置客户端链路为非阻塞模式
                sc.configureBlocking(false);
                //将新接入的客户端注册到多路复用器Selector上
                sc.register(selector, SelectionKey.OP_READ);
            }
            //监听到客户端的读请求
            if (key.isReadable()) {
                //获得通道对象
                SocketChannel sc = (SocketChannel) key.channel();
                ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                //从channel读数据到缓冲区
                int readBytes = sc.read(readBuffer);
                if (readBytes > 0) {
                    //Flips this buffer.    The limit is set to the current position and then
                    // the position is set to zero,就是表示要从起始位置开始读取数据
                    readBuffer.flip();
                    //eturns the number of elements between the current position and the    limit.
                    // 要读取的字节长度
                    byte[] bytes = new byte[readBuffer.remaining()];
                    //将缓冲区的数据读到bytes数组
                    readBuffer.get(bytes);
                    String body = new String(bytes, "UTF-8");
                    System.out.println("the time server receive order: " + body);
                    String currenttime = "往收到信息的通道塞点东西,只要client监听到读请求就能读出数据";
                    doWrite(sc, currenttime);
                } else if (readBytes < 0) {
                    key.channel();
                    sc.close();
                }
            }
        }
    }

    public static void doWrite(SocketChannel channel, String response) throws IOException {
        if (response!=null && response.length()>0 ) {
            byte[] bytes = response.getBytes();
            //分配一个bytes的length长度的ByteBuffer
            ByteBuffer write = ByteBuffer.allocate(bytes.length);
            //将返回数据写入缓冲区
            write.put(bytes);
            write.flip();
            //将缓冲数据写入渠道,返回给客户端
            channel.write(write);
        }
    }

    public static void main(String[] args) {
        new Thread(new ServerSocketChannels(8080)).start();
    }
}

客户端代码:
public class ClientSocketChannels extends Thread {
    //服务器端的ip
    private String host;
    //服务器端的端口号
    private int port;
    //多路服用选择器
    private Selector selector;
    //客户端通道
    private SocketChannel socketChannel;
    //关闭通道
    private volatile boolean stop=false;
    public ClientSocketChannels(String host, int port) {
        this.host = host == null ? "127.0.0.1" : host;
        this.port = port;
        try {
            //初始化一个Selector,工厂方法
            selector = Selector.open();
            //初始化一个SocketChannel,工厂方法
            socketChannel = SocketChannel.open();
            //设置非阻塞模式
            socketChannel.configureBlocking(false);
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }
    }

    /**
     * 首先尝试连接服务端
     * @throws IOException
     */
    public void doConnect() throws IOException {
        //如果连接成功,像多路复用器selector监听读请求
        if (socketChannel.connect(new InetSocketAddress(this.host, this.port))) {
            socketChannel.register(selector, SelectionKey.OP_READ);
            //执行写操作,像服务器端发送数据
            doWrite(socketChannel);
        } else {
            //监听连接请求
            socketChannel.register(selector, SelectionKey.OP_CONNECT);
        }
    }

    public static void doWrite(SocketChannel sc) throws IOException {
        //构造请求消息体
        byte[] bytes = "query time order".getBytes();
        //构造ByteBuffer
        ByteBuffer write = ByteBuffer.allocate(bytes.length);
        //将消息体写入发送缓冲区
        write.put(bytes);
        write.flip();
        //调用channel的发送方法异步发送
        sc.write(write);
        //通过hasRemaining方法对发送结果进行判断,如果消息全部发送成功,则返回true
        if (!write.hasRemaining()) {
            System.out.println("我要往服务端发送东西。。。。");
        }
    }

    @Override
    public void run() {
        try {
            doConnect();
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }
        while (!stop) {
            try {
                selector.select(1000);
                Set<SelectionKey> keys = selector.selectedKeys();
                Iterator<SelectionKey> its = keys.iterator();
                SelectionKey key = null;
                while (its.hasNext()) {
                    key = its.next();
                    its.remove();
                    try {
                        handle(key);
                    } catch (Exception e) {
                        if (key != null) {
                            key.cancel();
                            if (key.channel() != null) {
                                key.channel().close();
                            }
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
                System.exit(1);
            }
        }
    }

    public void handle(SelectionKey key) throws IOException {
        if (key.isValid()) {
            SocketChannel sc = (SocketChannel) key.channel();
            if (key.isConnectable()) {
                //如果连接成功,监听读请求
                if (sc.finishConnect()) {
                    sc.register(this.selector, SelectionKey.OP_READ);
                    //像服务端发送数据
                    doWrite(sc);
                } else {
                    System.exit(1);
                }
            }
            //监听到读请求,从服务器端接受数据
            if (key.isReadable()) {
                ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                int readBytes = sc.read(byteBuffer);
                if (readBytes > 0) {
                    byteBuffer.flip();
                    byte[] bytes = new byte[byteBuffer.remaining()];
                    byteBuffer.get(bytes);
                    String body = new String(bytes, "UTF-8");
                    System.out.println("我收到的东西" + body);
                    stop = true;
                } else if (readBytes < 0) {
                    key.cancel();
                    sc.close();
                }
            }
        }
        //释放所有与该多路复用器selector关联的资源
        if(selector != null&&stop==true){
            try {
                selector.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        int port = 8080;
        ClientSocketChannels client = new ClientSocketChannels("", port);
        new Thread(client, "client-001").start();
    }
}
  • Java9比Java8改进了什么;

1.modularity System 模块系统—在引入了模块系统之后,JDK 被重新组织成 94 个模块。Java 应用可以通过新增的 jlink 工具,创建出只包含所依赖的 JDK 模块的自定义运行时镜像。这样可以极大的减少 Java 运行时环境的大小。使得JDK可以在更小的设备中使用。采用模块化系统的应用程序只需要这些应用程序所需的那部分JDK模块,而非是整个JDK框架了;
2.HTTP/2——http2.0版本的支持;
3.jshell——让Java也可以像脚本语言一样来运行,可以从控制台启动 jshell;
4.Java 9增加了List.of()、Set.of()、Map.of()和Map.ofEntries()等工厂方法来创建不可变集合;
5.私有接口方法——java8中提供了接口的默认方法和静态方法,java9中这些方法可以共享接口中的私有方法;
6.HTML5风格的Java帮助文档——生成的文档是html5风格的;
7.多版本兼容 JAR;
8.统一 JVM 日志;
9.java9的垃圾收集机制——G1代替GC;
10.I/O 流新特性;
  • HashMap内部的数据结构是什么?底层是怎么实现的?(还可能会延伸考察ConcurrentHashMap与HashMap、HashTable等,考察对技术细节的深入了解程度);

1.7版本hashMap底层是数组+链表实现
1.支持key和value均可以为null,如果key为null,首先会遍历整个数组,判是否有key为null的值,如果有就覆盖掉,如果没有就放在数组的第一个位置,然后让这个对象的next指向原本在这个位置的对象,也就是从链表的队头开始添加。
2.初始化数组的大小默认是16,负载因子是0.75,扩容是当hashmap中的所有元素达到(大于等于)数组size*负载因子时,就会把当前的数组扩容一倍,所有的元素重新进行hash存储。如果指定初始化大小为size,那么初始化数组大小是大于size的最小的一个2的n次方。利用key的hash算法与数组的长度-1进行与运算得到i就是存值的数组下标,当前数组下标是个链表模式,循环链表取出链表中的key进行一一equals比较,如果equals返回ture,则用当前的value替换老的key的value,否则在链表头插入该key-value。
3.线程安全问题:e.next = newTable[i] 由于在扩容的时候是在链表头进行添加。本来链表是a→b→c,扩容之后可能变成c→b→a,当多线程进行操作时,线程1正在执行a→b,线程2已经执行到b→a ,结果这个链表就变成环形链表导致死循环。
1.8版本hashMap底层是数组+红黑树实现
1.hashmap由entry[]改成了note[]数组,是由数组加链表加红黑树组成。当new hashMap()时候并不会初始化hashMap,而是当我们put数据的时候才会初始化一个16数组大小,负载因子时0.75的map。当不指定初始值大小默认数组大小是16,负载因子时0.75。如果指定初始化大小为size,那么初始化数组大小是大于size的最小的一个2的n次方。添加元素时利用数组长度-1与hash(key)计算得到数组中位值,如果该位置不为null且和插入key相同,则替换,否则就把这个元素添加这个链表的尾部。链表大于等于8并且数组的长度大于等于64时则要转成红黑树。
1.7的ConcurrentHashMap
1.在concurrentHashMap以前,用的是hashtable实现并发安全,它的实现是在每个方法中加上synchronized。synchronized是重级锁,导致并发速度缓慢,已经被废弃。
实现方式是segment[]+hashEntry[]+链表实现的。segment[]和hashEntry[]默认数组大小为16,负载因子为0.75,也就是说完美情况下能支持16个线程对concurrentmap的并行非阻塞操作。segment可以看一把锁,当一个线程put元素时,根据hash算法得到segment下标,对该segment上锁然后操作。key和value均不能为null。 
2.new ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel) 初始化方式,参数分别是初始容量,加载因子,并发级别。初始容量会保证每个segment上分多少个hashEntry数组,concurrencyLevel做一个向上扩展成最接近2的n次方。当put元素时,现将元素的key做一个(h>>>segemntshift)&segmentMask算法得到segment数组的下标i(segemntshift=32-n;n就是从1到concurrencyLevel向上扩展成最接近2的n次方的次数。segmentMask=2的n次方-1),tryLock获取锁,获取到了就锁住segment[i]避免其他线程操作里面的hashEntry[],没有获取到锁就一直自旋等待。然后将key的hash&(tab.length-1)做一次hash得到segment中hashEntry[]的下标a(tab.length就initialCapacity/segment数组的长度 不能整除向上取整,整除还要+1 所得到的值向上扩展成最接近2的n次方),遍历hashEntry[a]链表,equlas比较如果存在则替换,否则就插入链表的表头。
3.扩容机制:每个segment中hashEntry[]表示capacity容量值,当put元素之后当前segment下的hashEntry[]里面元素大于capacity*loadFactor时,需要扩容成原来hashEntry的两倍。所以扩展前在同一个桶中的元素,现在要么还是在原来的序号的桶里,或者就是原来的序号再加上一个原来的总槽位,就这两种选择。
所以原桶里的元素只有一部分需要移动,其余的都不要移动。该函数为了提高效率,就是找到最后一个不在原桶序号的元素,那么连接到该元素后面的子链表中的元素的序号都是与找到的这个不在原序号的元素的序号是一样的 那么就只需要把最后一个不在原序号的元素移到新桶里,那么后面跟的一串子元素自然也就连接上了,而且序号还是相同的。在找到的最后一个不在原桶序号的元素之前的元素就需要逐个的去遍历,加到和原桶序号相同的新桶上 或者加到偏移2的幂次方的序号的新桶上。所有元素都是加到桶的头部,也就是链表的头部。
1.8的ConcurrentHashMap
1.key和value均不能为null,否则报错。note[]+红黑树实现和1.8的HashMap差不多,只是使用了CAS与synchronized将数据的锁粒度减小了可以并发安全。new ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel) 的时候不会初始化,put的时候才会初始化。初始化容量为16,加载因子为0.75。初始化的时候CAS一个int sizeCtl为-1表明表正在初始化,其他线程无法再初始化只能等待。
2.当进行put的时候,对传入的参数进行合法性判断,note[]还没有初始化,那就初始化。然后根据int i=hash(key)&(n-1)找到数组索引下标,如果此时note[i]是null,那么以CAS无锁式向该位置添加一个节点。如果note[i]上面已经有值,(如果此时表正在扩容,则帮助扩容)则synchronized锁住该槽位上note,如果该槽位上是链表,则在链表的尾部加note,如果是红黑树,就用红黑树的规则加上treeNote。(当链表长度大于等于8时会做两件事情1.如果此时的note[]长度小于默认阈值MIN_TREEIFY_CAPACITY的64时则会调用tryPresize方法将数组长度调为原来的两倍,并且触发transfer方法重新分配。2.将链表转成红黑树。3.调用addCount方法记录元素个数如果大于等于加载因子控制的值时,将数组扩为两倍并且transfer方法重新分配。)
3.当put时候发现有其他线程正在扩容,则要帮助扩容。如果线程2进行扩容发现线程1已经开始扩容,此时线程2访问到了ForwardingNode节点,如果线程2执行的put或remove等写操作,那么就会先帮其扩容。如果线程2执行的是get等读方法,则会调用ForwardingNode的find方法,去nextTable里面查找相关元素。当线程2发现transferIndex=0或者扩容线程已经达到最大数则放弃扩容直接返回。
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
    Node<K,V>[] nextTab; int sc;
    // 如果 table 不是空 且 node 节点是转移类型,数据检验
    // 且 node 节点的 nextTable(新 table) 不是空,同样也是数据校验
    // 尝试帮助扩容
    if (tab != null && (f instanceof ForwardingNode) &&
            (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
        // 根据 length 得到一个标识符号
        int rs = resizeStamp(tab.length);
        // 如果 nextTab 没有被并发修改 且 tab 也没有被并发修改
        // 且 sizeCtl  < 0 (说明还在扩容)
        while (nextTab == nextTable && table == tab &&
                (sc = sizeCtl) < 0) {
            // 如果 sizeCtl 无符号右移  16 不等于 rs ( sc前 16 位如果不等于标识符,则标识符变化了)
            // 或者 sizeCtl == rs + 1  (扩容结束了,不再有线程进行扩容)(默认第一个线程设置 sc ==rs 左移 16 位 + 2,当第一个线程结束扩容了,就会将 sc 减一。这个时候,sc 就等于 rs + 1)
            // 或者 sizeCtl == rs + 65535  (如果达到最大帮助线程的数量,即 65535)
            // 或者转移下标正在调整 (所有的桶全分配出去了)
            // 结束循环,返回 table
            if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || transferIndex <= 0)
                break;
            // 如果以上都不是, 将 sizeCtl + 1, (表示增加了一个线程帮助其扩容)
            if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                // 进行转移
                transfer(tab, nextTab);
                // 结束循环
                break;
            }
        }
        return nextTab;
    }
    return table;
}
4.扩容机制
1.计算需要迁移多少个hash桶(MIN_TRANSFER_STRIDE该值作为下限,以避免扩容线程过多。)
2.逆序迁移已经获取到的hash桶集合,如果迁移完毕,则更新transferIndex,获取下一批待迁移的hash桶,如果transferIndex=0,表示所以hash桶均被分配,将i置为-1,准备退出transfer方法         
3.如果是链表迁移扩容,则把链表每个node的hash(key)&旧的数组长度,0分为一组,不是0分为一组,比如a0—>b1—>c0—>d1—>e0—>f1—>g1 会分成e0->c0->a0和d1—>b1—>f1—>g1,前者加入到新链表的原槽位中,后者加入原槽位+原来数组长度的槽位中。如果是红黑树迁移,把原来树拆成两棵树(node的hash(key)&旧的数组长度方式,0放低位树,否则放高位树),当树的节点数小于等于 6,那么转成链表。
  • 说说反射的用途及实现,反射是不是很慢,我们在项目中是否要避免使用反射;

反射的定义:在jvm运行时才动态的加载类或调用方法或属性,他不需要事先知道(写代码的时候或者编译器)运行对象是谁。
反射的功能:在运行时判断任意一个对象所属的类;
           在运行时构造任意一个类的对象;
           在运行时判断任意一个类所具有的成员变量和方法;
      用途:当我们在写代码时候调用一个对象user的方法时候,一按user. 就会显示这个对象的方法,这就用到了反射。
实现:
Class clz = Class.forName("com.zhenai.api.Apple");//获取类的 Class 对象实例
Constructor appleConstructor = clz.getConstructor();//根据 Class 对象实例获取 Constructor 对象
Object appleObj = appleConstructor.newInstance();//使用 Constructor 对象的 newInstance 方法获取反射类对象
Method setPriceMethod = clz.getMethod("setPrice", int.class);//而如果要调用某一个方法,则需要经过下面的步骤:获取方法的 Method 对象
setPriceMethod.invoke(appleObj, 14);//利用 invoke 方法调用方法
从上面的反射实现可以看出,当要调用某个类的方法时,要做很多的操作,比new User().getName()方法复杂太多,性能消耗很大。所以我们在项目中一般不用反射。
  • 说说自定义注解的场景及实现;
java元注解:java.lang.annotation包下的
1.Documented:用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。它是一个标记注解,没有成员。@Documented
2.Inherited:用于表示某个被标注的类型是被继承的。如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。
3.Native:表示定义常量值的字段可以被native代码引用,当native代码和java代码都需要维护相同的常量时,如果java代码使用了@Native标志常量字段,可以通过工具将它生成native代码的头文件。例如:@Native public static final int TYPE_NEAREST_NEIGHBOR = 1;
4.Repeatable:jdk1.8新增的注解,被标注的类型是可以重复使用的,如果注解aaa中使用了@Repeatable(aaas.class),那么这个@aaa注解就可以在一个成员中多次使用。(其中@aaas注解中包含了@aaa的数组)
5.Retention:注解的生命周期
      SOURCE:在源文件中有效(即源文件保留)@Retention(RetentionPolicy.SOURCE)    
      CLASS:在class文件中有效(即class保留)
     RUNTIME:在运行时有效(即运行时保留)
6.Target:该注解所修饰的对象范围。      
     CONSTRUCTOR:用于描述构造器,例如@Target(ElementType.CONSTRUCTOR)
     FIELD:用于描述域即类成员变量  
     LOCAL_VARIABLE:用于描述局部变量  
     METHOD:用于描述方法  
     PACKAGE:用于描述包  
     PARAMETER:用于描述参数  
     TYPE:用于描述类、接口(包括注解类型) 或enum声明  
     TYPE_PARAMETER:1.8版本开始,描述类、接口或enum参数的声明  
     TYPE_USE:1.8版本开始,描述一种类、接口或enum的使用声明
  • List 和 Map 区别,Arraylist 与 LinkedList 区别,ArrayList 与 Vector 区别;

list和map区别:list是Collection的接口下面的,是一个value的集合;map是一个存key-value的集合。
ArrayList和LinkedList区别:
     1.ArrayList是由一个object数组实现的,初始化一个空的数组;而LinkedList是由一个Node双向链表实现的,new的时候什么也不做。
     2.ArrayList在add的时候是把元素填在数组最后一个元素的后面一个槽位;LinkedList在add时候是把元素添加到链表的最后端。两者均可以添加null。两者均可以添加重复元素。
     3.ArrayList在add时候初始化一个默认new Object[10],当数组容量不够的时候,如果此时数组size为1,则新建一个size=2的数组,copy存值。如果此时数组size≥2,则按int newCapacity = oldCapacity + (oldCapacity >> 1);算法新建数组。LinedList链表实行则没有扩容机制。
     4.ArrayList查找易增删慢,LinkedList查找慢增删快;
ArrayList和Vector区别:
      1.ArrayList默认初始化是一个空的数组,Vector初始化一个大小10的数组;
      2.Vector是jdk1.2之前的类,基本废弃;扩容是2倍扩容;
      3.vector类方法都用了synchronized修饰,线程安全效率不高;

Spring相关

  • Spring AOP的实现原理和场景?

实现原理:动态代理。大白话说就是在某个方法的前后添加一些行为。
场景一:数据库事务管理。
场景二:使用AspectJ框架对操作日志进行记录。
  • Spring bean的作用域和生命周期;

作用域:spring中定义了bean的五个作用域
         1.singletion:单例,所有的IOC容器中只有一个实例对象,这是默认的。
         2.prototype:多例,每次使用对象都会new一个新的对象。
         3.request:一个http请求都会产生一个bean对象,只基于web的springApplication中使用。
         4.session:限定一个bean的生命周期是HTTPsession,不同的session使用不同的bean。只基于web的springApplication中使用。
         5.global session:限定一个bean的生命周期是全聚德HTTPsession,只基于web的SpringApplication中使用。
生命周期:单例的时候,默认情况下,Spring在读取xml文件的时候,就会创建对象。在创建对象的时候先调用构造器,然后调用init-method属性值中所指定的方法。对象在被销毁的时候,会调用destroy-method属性值中所指定的方法(例如调用Container.destroy()方法的时候)。
         非单例的时候,容器也会延迟初始化bean,Spring读取xml文件的时候,并不会立刻创建对象,而是在第一次请求该bean时才初始化(如调用getBean方法时)。在第一次请求每一个prototype的bean时,Spring容器都会调用其构造器创建这个对象,然后调用init-method属性值中所指定的方法。对象销毁的时候,Spring容器不会帮我们调用任何方法,因为是非单例,这个类型的对象有很多个,Spring容器一旦把这个对象交给你之后,就不再管理这个对象了。
  • Spring Boot比Spring做了哪些改进?Spring5比Spring4做了哪些改进;

Spring Boot对比Spring做了最大一点改变:约定大于配置,约定一些推荐的默认配置,开发人员只需要规定不符约定的部分。
1.内嵌了如Tomcat,Jetty和Undertow这样的容器,也就是说可以直接跑起来,用不着再做部署工作了;
2.无xml配置自动配置:核心注解@EnabeAutoConfiguration,它能根据类路径下的jar包和配置动态加载配置和注入bean。(例如在lib下放一个druid连接线程池jar包,application.yml文件配置druid相关参数,boot就能自动配置数据库连接池,把jar拿掉boot就不会配置。类似的还有好多:redis,rabbitmq等等)
Spring5比Spring4做了哪些改进:
       1.基准升级:你至少需要 J2EE7 和 JDK8,其他框架基准也提升(Hibernate 5、JUnit 5);
       2.兼容 JDK9 运行时;不用mave用grade依赖。
       3.响应式编程支持;
                响应式编程是 SpringFramework5.0 最重要的特性之一。响应式编程提供了另一种编程风格,专注于构建对事件做出响应的应用程序。 SpringFramework5 包含响应                            流(定义响应性API的语言中立尝试)和 Reactor(由Spring Pivotal团队提供的 Reactive Stream 的Java实现), 以用于其自身的用途以及其许多核心API。Spring Web Reactive 在 spring-webmvc 模块中现有的(而且很流行)Spring Web MVC旁边的新的 spring-web-reactive 模块中。 请注意,在 Spring5 中,传统的 SpringMVC 支持 Servlet3.1 上运行,或者支持 JavaEE7 的服务器
       4. 函数式web框架;
                 除了响应式功能之外,Spring5 还提供了一个函数式Web框架。它提供了使用函数式编程风格来定义端点的特性。 该框架引入了两个基本组件:HandlerFunction 和 RouterFunction。
       5.Kotlin支持;
       6.移除的特性;(移除Velocity、XMLBeans、Guava);
  • 如何自定义一个Spring Boot Starter?

前景:一般的我们只需要在pom文件中引入相应的jar包,然后在properties或者yml文件中配置相关的参数,系统就能自动给我们加载初始化相应的类,我们并不需要xml文件配置很多bean配置(配置数据库连接、配置spring事务)。
spring-boo-starter原理:在导入的第三方jar包的时候,jar里面的类上就已经写上了相应的@Configuration,@Bean表明该类是一个配置类和把bean交割springIOC处理。(所以在MYbatisAutoVOnfiguration类中已经生成SqlSessionFactory这些mybatis实例并交springIOC管理)那么这些类又是怎么结合properties文件数据来创建类的呢?在DataSourceAutoConfiguration类里面,我们注意到使用了EnableConfigurationProperties这个注解。
@Configuration
@ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class})
@EnableConfigurationProperties({DataSourceProperties.class})
@Import({DataSourcePoolMetadataProvidersConfiguration.class, DataSourceInitializationConfiguration.class})
public class DataSourceAutoConfiguration {
...
}
DataSourceProperties中封装了数据源的各种属性以及指定了属性在配置文件中的前缀(例如:spring.datasource.username=xiaoming)。
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
 private ClassLoader classLoader;
 private String name;
 private boolean generateUniqueName;
 private Class<? extends DataSource> type;
 private String driverClassName;
 private String url;
 private String username;
 private String password;
 private String jndiName;
 ...
}
@ConfigurationProperties注解的作用是把yml或者properties配置文件转化为bean。
@EnableConfigurationProperties注解的作用是使@ConfigurationProperties注解生效。如果只配置@ConfigurationProperties注解,在spring容器中是获取不到yml或者properties配置文件转化的bean的。
但是springboot默认扫描启动类所在的包下的主类与子类的所有组件,但并没有包括依赖包的中的类,那么依赖包中的bean是如何被发现和加载的?
那就是靠@EnableAutoConfiguration这个注解的功能很重要,借助@Import的支持,收集和注册依赖包中相关的bean定义。
       在@EnableAutoConfiguration中有个@Import({Registrar.class}),在Registrar类中有个
             new AutoConfigurationPackages.PackageImport(metadata)).getPackageName();
             new AutoConfigurationPackages.PackageImport(metadata);
    就是把加载启动类所在的包下的所有主类和子类所有的组件注册到spring容器中。通过反射机制将spring.factories中@Configuration类实例化为对应的java实列。
    @EnableAutoConfiguration注解借助@Import注解将这组bean注入到spring容器中,springboot正式通过这种机制来完成bean的注入的。
自己实现:
1.编写一个目标类,用ConfigurationProperties把application配置文件中属性设置的该类属性上。
@ConfigurationProperties(prefix = "com.itpsc")
public class UserProperties {
    private String username;
    private String password;
     ...
}
2.写一个service对目标类进行初处理
@service
public class UserService {  
       @Autowired 
       UserProperties userProperties;     
      public void setUserProperties(UserProperties userProperties) {         
             this.userProperties = userProperties;     
      }     
      public String getNameAndPassWord() {         
               return userProperties.getName() + "-"  + userProperties.getPassword();    
      }
}
3.将service注册到springIOC
@Configuration
@EnableConfigurationProperties(UserProperties.class)
public class UserAutoConfiguration {
    @Bean
    public UserService getBean(UserProperties userProperties) {
        //创建组件实例
        UserService userService = new UserService();
        userService.setUsername(userProperties.getUsername());
        userService.setPassword(userProperties.getPassword());
        return userService;
    }
}
4.\META-INF\spring.factories该文件用来定义需要自动配置的类,springboot启动时会进行对象的实例化,会通过加载类SpringFactoriesLoader加载该配置文件,将文件中的配置类加载到spring容器。
在src/main/resources新建META-INF文件夹,在META-INF文件夹下新建spring.factories文件。配置内容如下:org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.itpsc.spring.boot.starter.UserAutoConfiguration
  • Spring IOC是什么?优点是什么?

Spring IOC是控制反转,将项目中所有的bean统一交给spring管理。
优点:不用频繁的经历对象的创建到死亡的生命周期以及垃圾回收,节省性能。
  • SpringMVC、动态代理、反射、AOP原理、事务隔离级别;

springMvc流程:
     1.创建一个dispatcherServlet接收用户的http请求。
     2.dispatcherServlet收到请求调用handlerMapping映射器处理器,handlerMapping将根据配置生成处理器对象以及处理器拦截器一并返  回给dispatcherServlet.
     3.dispatcherServlet调用handlerAdapter适配器处理器,handlerAdapter经过适配调用具体的controller,controller执行完返回modelAndView   
     4.dispatcherServlet将modelAndView交割ViewReslover视图解析器,viewReslover处理完返回view。
     5.dispatcherServlet对view进行视图渲染并返回给用户。
动态代理:
      在gdk中动态代理有两种:1.有实现接口的用GKD动态代理;2.有继承关系的用cglib动态代理;
反射:
       Class clz = Class.forName("com.zhenai.api.Apple");//获取类的 Class 对象实例 
       Constructor appleConstructor = clz.getConstructor();//根据 Class 对象实例获取 Constructor 对象 
       Object appleObj = appleConstructor.newInstance();//使用 Constructor 对象的 newInstance 方法获取反射类对象 
       Method setPriceMethod = clz.getMethod("setPrice", int.class);//而如果要调用某一个方法,则需要经过下面的步骤:获取方法的 Method 对象                                      setPriceMethod.invoke(appleObj, 14);//利用 invoke 方法调用方法
AOP:利用动态代理在某个方法的方法前,方法后,方法报错....增加一些行为对方法进行加强。
事务隔离:uncommitted(会有脏读); committed(会可重复读); repeatable read(会有幻读); serializable; 
 

中间件篇

  • Dubbo完整的一次调用链路介绍;

service接口层:provider和consumer层,留给你来实现的。
config层:任何一个框架,都需要提供配置文件,让你进行配置。
proxy层:代理层,无论是consumer还是provider,dubbo都会给你生成代理,代理之间进行通信。
registry层:provider注册自己为一个服务,consumer就可以注册中心去寻找自己要调用的服务。
cluster层:provider可以部署在多台机器上,多个provider就组成一个集群。
monitor层:consumer调用provider,统计信息监控。
protocol层:负责具体的provider和consumer之间调用接口的时候的网络通信。
exchange层:信息交换层。
serialize层:序列化层。

1.provider提供服务接口,并且实现。将自己的服务注册到注册中心(可以是zookeeper),暴露服务(启动tomcat)。
2.consumer从注册中心获取服务地址并且缓存,根据负载均衡选择一台服务器进行服务调用。
3.consumer动态代理处代理类让代理类调用dubbo协议远程访问provider提供的接口。(用Hessian序列化传输内容)。
  • Dubbo支持几种负载均衡策略,容错机制,动态代理?

负载均衡策略:
       random loadbalance:随机分配,可以根据对provider设置不同的权重,按比例进行分配流量。
       roundrobin loadbanlance:轮训平均分配,也可以按照权重进行比例分配。
       leastactive loadbalance:dubbo会判别系统的性能,性能高低和分配流量成正比。
       consistanthash loadbalance:一致性hash算法,相同参数的请求一定分发到一个provider上去。
集群容错机制:failover cluster模式:失败自动切换,自动重试其他机器。默认的。
                        failfast cluster模式:一次调用失败就立即失败,常见于写操作。
                        failsafe cluster模式:出现异常时忽略掉,常用于不重要的接口调用,比如日志记录。
                        failbackc cluster模式:失败了后台自己自动记录请求,然后定时重发。
                        forking cluster:并行调用多个provider,只要一个成功就立即返回。
                        broadcacst cluster:逐个调用所有provider。
动态代理:默认使用javaassist动态字节码生成,创建代理类。
  • Dubbo Provider服务提供者要控制执行并发请求上限,具体怎么做?

在Provider配置的Consumer端属性:actives,消费者端,最大并发调用限制,即当Consumer对一个服务的并发调用到上限后,新调用会Wait直到超时。
Provider上配置的Provider端属性: executes,一个服务提供者并行执行请求上限,即当Provider对一个服务的并发调用到上限后,新调用会Wait(Consumer可能到超时)。在方法上配置(dubbo:method )则并发限制针对方法,在接口上配置(dubbo:service),则并发限制针对服务。
  • Dubbo启动的时候支持几种配置方式?

1.XML配置
2.动态配置中心
3.annotation 注解配置方式
4.API 配置方式
5.配置加载流程
6.自动加载环境变量
  • 了解几种消息中间件产品?各产品的优缺点介绍;

activemq:单机吞吐量万级,延迟毫秒级,主从架构,较低丢数据,鼻祖功能完善,太老了使用者越来越少
rabbitmq:单机吞吐量万级,延迟微秒级,主从架构,较低丢数据,erlang语言开发性能好,社区活跃延迟低小公司用的多
rocketmq:单机吞吐量十万级,延迟毫秒级,分布式,可以做到0丢失,扩展好java开发可自己掌控,阿里出品性能ok,
kafka:单机吞吐量十万级,延迟毫秒级,分布式,可以做到0丢失,大数据使用,超高吞吐量,可能消费重复消息,适合大数据实时计算以及日志采集。

  • 消息中间件如何保证消息的一致性和如何进行消息的重试机制?

rabbitmq:provider→mq有confirm确认机制,确保provider到mq消息不丢失,mq本身可以设置持久化,mq→consumer可以手动ack机制确保这个过程消息不会丢失。
重试机制:@RabbitListener底层使用AOP进行拦截,如果程序没有抛出异常,自动提交事务。 如果Aop使用异常通知拦截获取异常信息的话 , 自动实现补偿机制,该消息会一直缓存在Rabbitmq服务器端进行重放,一直重试到不抛出异常为准。
一般来说默认5s重试一次,可以修改重试策略,消费者配置:
  • Spring Cloud熔断机制介绍;

熔断机制原理:当服务A因为某些原因失败,变得不可用,所有对服务A的调用都会超时。当对A的调用失败达到一个特定的阀值(5秒之内发生20次失败是Hystrix定义的缺省值), 链路就会被处于open状态, 之后所有所有对服务A的调用都不会被执行, 取而代之的是由断路器提供的一个表示链路open的Fallback消息.  Hystrix提供了相应机制,可以让开发者定义这个Fallbak消息。

  • Spring Cloud对比下Dubbo,什么场景下该使用Spring Cloud?

在性能上来说,由于Dubbo底层是使用Netty这样的NIO框架,是基于TCP协议传输的,配合以Hession序列化完成RPC。而SpringCloud是基于Http协议+rest接口调用远程过程的,相对来说,Http请求会有更大的报文,占的带宽也会更多。
从功能上来说spring cloud很完善。

数据库篇

  • 数据库索引,锁机制介绍:行锁、表锁、排他锁、共享锁;

索引:建立索引要先排序(好处:1.查到即可返回,后面的数据由于排序不用再往后查;2.实现b+树查询)。
主键索引:
一个页里面存数据,上图是一页存两条数据,红色是主键索引,根据主键索引找到具体的页,然后在页中每个数据都有一个指向下一个数据的链表,确定页之后再轮询页中找数据。
联合索引:
三个字段abc组成的联合索引,根据abc三个字段的按顺序组成b+树。此索引不会存全亮的数据,只存三个索引数据和主键数据,如果涉及查询其他字段请求,会先走着三个联合索引查出主键然后去主键数索引查询主键查出数据。
行锁:mysql的InnoDB引擎支持行锁,行锁是通过索引加载的,即是行锁是加在索引相应的行上的。只有通过索引条件检索数据,InnoDB才会使用行级锁,否则,InnoDB将使用表锁!
①.select ... from语句:InnoDB引擎采用多版本并发控制(MVCC)的方式实现了非阻塞读,所以对于普通的select读语句,InnoDB并不会加锁【注1】。
②.select ... from lock in share mode语句:这条语句和普通select语句的区别就是后面加了lock in share mode,通过字面意思我们可以猜到这是一条加锁的读语句,并且锁类型为共享锁(读锁)。InnoDB会对搜索的所有索引记录加next-key锁,但是如果扫描的唯一索引的唯一行,next-key降级为索引记录锁。
③.select ... from for update语句:和上面的语句一样,这条语句加的是排他锁(写锁)。InnoDB会对搜索的所有索引记录加next-key锁,但是如果扫描唯一索引的唯一行,next-key降级为索引记录锁。
④.update ... where ...语句:InnoDB会对搜索的所有索引记录加next-key锁,但是如果扫描唯一索引的唯一行,next-key降级为索引记录锁。【注2】
⑤.delete ... where ...语句:InnoDB会对搜索的所有索引记录加next-key锁,但是如果扫描唯一索引的唯一行,next-key降级为索引记录锁。
⑥.insert语句:InnoDB只会在将要插入的那一行上设置一个排他的索引记录锁。
(记录锁(Record Locks):记录锁锁定索引中一条记录。记录锁的锁定范围是单独的索引记录,就是3、5、8、9这四行数据
  间隙锁(Gap Locks):或锁住索引记录中间的值,或锁住第一个索引记录前面的值或者最后一个索引记录后面的值。用集合表示为(-∞,3)、(3,5)、(5,8)、(8,9)、(9,+∞)。
  Next-Key Locks:Next-Key锁是索引记录上的记录锁和在索引记录之前的间隙锁的组合。用集合的方式表示为(-∞,3]、(3,5]、(5,8]、(8,9]、(9,+∞)。)
表锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。表共享锁(Table Read Lock)和表独占写锁(Table Write Lock)。
页锁:页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
排他锁共享锁并发情况:(0:非阻塞;1:阻塞)
  • 乐观锁的业务场景及实现方式; 

在数据库添加一列version,记录着修改的版本。每次修改之前查一下版本号,修改的时候加上条件where version="查询的版本号" 才进行修改。
  • 事务介绍,分布式事物的理解,常见的解决方案有哪些,什么事两阶段提交、三阶段提交;

事务:原子性:一段sql执行要么全部成功要么全部失败;
          一致性:数据完整性。
           隔离性:多个事务执行之间相互不影响;
          持久性:事务一旦提交,就会被物理保存;
分布式事务:一个操作导致不同系统需要同时对数据进行修改,确保多个系统操作要么同时成功要么同时失败。
两阶段提交: 投票阶段 和 事务提交阶段;(TCC模式,对rabbitmq进行预发消息,本地消息一致性)
                    1.协调者向所有的参与者发送事务执行请求,并等待参与者反馈事务执行结果。
                    2.事务参与者收到请求之后,执行事务但不提交,并记录事务日志。
                    3.参与者将自己事务执行情况反馈给协调者,同时阻塞等待协调者的后续指令。
三阶段体检:提交投票阶段→预提交或者终止阶段→提交或终止阶段
  • MySQL记录binlog的方式主要包括三种模式?每种模式的优缺点是什么?

1、statement level模式:每一条会修改数据的sql都会记录到master的bin-log中。slave在复制的时候sql进程会解析成和原来master端执行过的相同的sql来再次执行。优点:节约io,快。缺点:需要记录语句顺序。
2、rowlevel模式:日志中会记录成每一行数据被修改的形式,然后在slave端再对相同的数据进行修改。优:精确,和新功能不冲突;缺:记录信息量大。
3、mixed模式:上面两种混合模式
  • MySQL锁,悲观锁、乐观锁、排它锁、共享锁、表级锁、行级锁;

悲观锁:在数据开始读取的时候就把数据锁定住。
乐观锁:在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让用户返回错误的信息,让用户决定如何去做。
排它锁:可认为是读写锁中的写锁,只要获取锁,其他线程均无法再对数据进行读写。
共享锁:可认为是读写锁中的读锁,可以同时读数据,不能对数据进行修改。
表级锁:对整个表进行锁操作。
行级锁:对一行就行锁操作。
  • 数据库事务隔离级别,MySQL默认的隔离级别、Spring如何实现事务、JDBC如何实现事务、嵌套事务实现;

事务隔离级别脏读不可重复读幻读
读未提交(read-uncommitted)
不可重复读(read-committed)
可重复读(repeatable-read)
串行化(serializable)
mysql默认的隔离级别是:repeatable-read;
spring事物:建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。
JDBC如何实现事务:
       1.autoCommited(false);关闭自动提交
       2.执行sql,手动commited;
       3.出错就rollback;
       4.关闭连接资源;
spring事物传播行为:
        TransactionDefinition.PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值。                              TransactionDefinition.PROPAGATION_REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。
        TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。(注意这里,不要采坑)           TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。 
        TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。 
        TransactionDefinition.PROPAGATION_MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。           TransactionDefinition.PROPAGATION_NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。
嵌套事务实现:在MySQL的官方文档中有明确的说明不支持嵌套事务:(可以用java代码实现)
  • SQL的整个解析、执行过程原理、SQL行转列;

785056-20190114235428795-1807161750.png

客户端发送一条查询给服务器;
服务器先检查查询缓存,如果命中了缓存,则立刻返回存储在缓存中的结果。否则进入下一阶段。
服务器段进行SQL解析、预处理,在优化器生成对应的执行计划;
mysql根据优化器生成的执行计划,调用存储引擎的API来执行查询。
将结果返回给客户端。
参考:https://www.cnblogs.com/fanguangdexiaoyuer/p/10268570.html
行转列:
select user_name, sum(case subject when '语文' then source else 0 end )as '语文',
sum(case subject when '数学' then source else 0 end )as '数学',
sum(case subject when '英语' then source else 0 end )as '英语' from  table_这个表格 group by user_name;
列转行:
SELECT user_name,'语文' AS subject,yuwen AS score FROM test_table2
UNION ALL
SELECT user_name,'数学' AS subject,shuxue AS score FROM test_table2
UNION ALL
SELECT user_name,'英语' AS subject,yingyu AS score FROM test_table2
ORDER BY user_name

 

 

转载于:https://my.oschina.net/yaojianpeng/blog/3075099

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值