我的秋招之路-面经篇

Java基础

  1. 类加载的时机和类初始化的时机(引出tomcat类加载器)
    JVM和绝大多数用户自定义的类在JVM启动的时候被加载,少量用户的类在运行的时候被动态的加载。比如说tomcat中的jsp就是通过运行的时候动态的加载到JVM中,这样可以避免用户每次修改完jsp之后频繁的重启tomcat。其实就是一个热部署的过程。
    Tomcat5.0类加载器
    BootStrap
    Extension
    Application
    Common
    Catalina Shared
    WebApp
    JSP
    为什么tomcat会要用到这么多的类加载器呢?
    因为对于一个web容器来讲,两个web应用所使用的类库应当被隔离,所以每个web应用都有一个webapp classloader
    两个web应用使用的类库应当共享,所以每个web应用都有一个shared classloader
    web容器要尽量的保证自身的安全不受到web应用的影响,所以有了catalina classloader
    web容器也可以web应用共享某些类库,所以有了common classloader

  2. 类加载的5个阶段

    1. 加载
      通过类加载器加载class文件,然后在方法区生成class对象
    2. 验证
      验证加载进来的class字节流的正确性
    3. 准备
      对类中的各个变量赋零值
    4. 解析
      解析阶段在某些情况下可能会在初始化之后执行,这是为了实现Java语言中的动态绑定。解析阶段的工作是变符号引用为直接引用
    5. 初始化
      执行类中的clinit()方法
  3. 什么是动态绑定?
    把一个方法调用和方法的主体关联起来叫做绑定。那么动态绑定就是在程序运行的时候根据对象的类型进行绑定。Java中除了static,final,private(编译时为final),其他的方法都为动态绑定。

  4. 哪些对象可作为GC Roots?

    1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
    2. 静态变量引用的对象
    3. 方法区常量引用的对象
  5. 双亲委派模型
    每一个类加载器收到一个类的加载请求时,不会尝试自己去加载这个类,而是交由自己的父类加载器去完成这个类加载的请求,只有当父类加载器完成不了这个请求时,才会交由子类加载器去加载。
    这样做可以保证Java体系最基本的行为,比如java.lang.object类不会被篡改。如果你自己写了一个java.lang.object类,那么会发现可以正常编译但是永远无法被执行。

  6. Java热部署

    1. 自定义ClassLoader类加载器,实时监听class文件的变化
    2. 改变对象的创建方式,改为通过ClassLoader动态创建,虚拟机在碰到一个new指令时,会先去方法区检查是否有该类的符号引用,并且检查该类是否已经被加载,解析和初始化,如果没有,就会执行这个类的加载过程
    3. 在JVM启动时使用JavaAgent拦截默认类加载器行为
  7. HashMap可不可以存null,HashTable呢?

  8. HashMap1.7与1.8的区别

    1. 处理哈希码的方式
      1.7处理hashCode采用了4次位运算加5次异或运算
      1.8处理hashCode采用了仅使用了一次位运算和一次异或运算,key如果为null,计算结果为0
    2. 扩容时链表的插入方式
      1.7采用头插法,扩容的时候会造成链表逆序,容易出现环形链表
      并发插入时会出现数据丢失,因为并发时拿到的链头可能不是最新的链头,会出现后面的覆盖掉前面数据的情况
      1.8采用尾插法,不会出现链表逆序,不容易出现环形链表
    3. 数据结构
      1.7采用数组+链表
      1.8采用数组+链表+红黑树
  9. JVM参数
    -Xmn20M 新生代20M
    -Xmx2G 堆内存最大为2G
    -Xms2G 堆内存初始大小为2G
    -XX:+PrintGCDetails 打印GC日志
    -XX:SurvivorRatio=8 新生代Eden区和Survivor区的比值为 8:1:1 6:2:2

  10. ConcurrentHashMap和HashTable
    ConcurrentHashMap与HashTable都是线程安全的容器,在面对多线程竞争激烈的情况下,HashTable
    的性能相比于ConcurrentHashMap要逊色的多,原因在于HashTable采用的是synchronize对整个容器
    进行加锁,而ConcurrentHashMap采用的是锁分段技术,大大提高了多线程下的读写性能。

    1. ConcurrentHashMap由多个Segment组成,而每个Segment又由多个HashEntry组成
    2. 每个Segment都配有一把锁,对某个Segment进行加锁的同时不影响其他Segment的读写
    3. get()操作不用加锁,原因在于HashEntry中共享变量count,value都是volatile类型的,即使在读的时候有一个线程在写,也能保证读取到的value是最新的,因为volatile保证了Java内存模型中的happen-before原则,即写操作总是优先于读操作的
    4. get()时会先进行一次再散列运算定位到某个Segment,然后再进行一次再散列定位至某个HashEntry,之后进行读操作即可
    5. put()操作需要加锁,这是为了防止同时有两个线程同时对某个HashEtry进行写操作
    6. put()操作同样需要先进行一次再散列定位至某个Segment,然后判断其是否需要扩容,如果需要扩容,待其进行扩容操作后再进行插入操作
    7. size()操作先采用两次不加锁的方式统计每个Segment的count,如果统计的过程中发现count发生了,变化,再采用加锁的方式统计每个Segment的count
  11. ConcurrentHashMap1.7与1.8的区别

    1. 数据结构不同
      1.7采用Segment+HashEntry进行存储
      1.8采用数组+链表+红黑树
    2. 保证并发的方式不同
      1.7采用的锁分段技术保证并发
      1.8大量采用CAS保证并发
    3. 初始化map的时机不同
      1.7在构造的时候进行初始化
      1.8在插入的时候进行初始化
  12. 使用线程池的好处

    1. 降低资源消耗
    2. 提高响应速度
    3. 提高线程的可管理性
  13. 线程池的应用场景
    单机高性能的本质问题就是解决I/O,进程,线程的问题。多线程为了提高资源的利用率和进行资源管控都会用到线程池,要说典型的应用场景,比如说某个容器吧,一般几百的并发量直接进进程就可以,但是量大了就会使用到线程池。

  14. 线程池的拒绝策略

    1. 直接抛异常 AbortPolicy
    2. 直接抛弃 DiscardPolicy
    3. 运用调度线程执行该任务 CallerRunsPolicy
    4. 丢掉队列中最近的一个任务,直接执行当前任务 DiscardOldestPolicy
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
        coreSize,即使其他线程空闲也会创建一个新线程来执行新来任务
        maxSize,当线程池大小达到coreSize,并且有界队列满的情况下会继续创建线程直到达到maxSize
        keepAliveTime,线程空闲时存活时间,如果任务多而短的话,可以把这个值调大提高线程的利用率
        timeUnit,
        blockingQueue,new ArrayBlockingQueue<>(),
        rejectHandler,new ThreadPoolExecutor.DiscardPolicy()
    );
    
  15. Lock与synchronized的区别

    1. Lock可以中断式的获取锁
    2. Lock可以尝试非阻塞的获取锁
    3. Lock可以超时的获取锁
  16. Java中都有什么锁?
    Java中的锁都是基于队列同步器AQS实现的

    1. 独占锁
      独占锁同一时间只允许一个线程获取到锁
    2. 共享锁
      共享锁同一时间可允许多个线程获取到锁
    3. 可重入锁
      可重入锁允许一个线程获取到锁之后再次获取锁,即保证获取到锁的线程不会被自己阻塞
      同时可重入锁支持公平锁和非公平锁
    4. 读写锁
      读锁是共享锁,写锁是排他锁,通过对一个32位整形类型的数值表达了读和写的两种状态。
  17. 队列同步器AbstractQueuedSynchronized
    提供可重写的方法

      1. tryAcquire()
      2. tryRelease()
      3. tryAcquireShared()
      4. tryReleaseShared()
      5. isHeldExclusively()
    

    提供的模板方法

  1. acquire() {
     if(!tryAcquire()&&acquireQueued(addWaiter(Node.Exclusive,arg))) {
         selfInterrupt();
       }
     }
 2. acquierInterruptibly()
 3. tryAcquirNanos()
 4. acquireShared() {
     if(tryAcquireShared() < 0) {
         doAcquireShared();
       }
     }
 5. acquireSharedInterruptibly()
 6. tryAcquireSharedNanos()
 7. release() {
     if(tryRelease()) {
         Node h = head;
         if(h!=null && h.waitStatus != 0) {
             unparkSuccessor(h);
         }
         return true;
     }
   }
 8. releaseShared() {
       if(tryReleaseShared()) {
         doReleaseShared();
       }
     }
 9. getQueuedThreads()
  1. 独占锁

     public class MyLock {
         private static class Sync {
             public boolean tryAcquire() {
                 if(compareAndSetState(0,1)) {
                    setExclusiveOwnerThread(Thread.currentThread());
                    return true;
                 }
                 return false;
             }
    
             public boolean tryRelease() {
                 if(getState() == 0) {
                     throw new Exception();
                 }
                 setExclusiveOwnerThread(null);
                 setState(0);
                 return true;
             }
         }
     }
    
  2. 共享锁

    public class TwinsLock {
        private Sync sync = new Sync(2);
        private static final class Sync extends AbstactQueuedSynchronizer {
            Sync(int count) {
                if(count < 0)
                    throw new Excetion();
                setState(count);
            }
    
            public int tryAcquireShared(int reduceCount) {
                for(;;) {
                    int current = getState();
                    int newCount = current - reduceCount;
                    if(newCount < 0 || compareAndSetState(current,newCount)) {
                        return newCount;
                    }
                }
            }
    
            public boolean tryReleaseShared(int returnCount) {
                for(;;) {
                    int current = getState();
                    int newCount = current + returnCount;
                    if(compareAndSetState(current,newCount)) {
                        return true;
                    }
                }
            }
        }
    
        public void lock() {
            acquireShared(1);
        }
    
        public void unLock() {
            releaseShared(1);
        }
    }
    
  1. 可重入锁

    public class ReentryLock{
        private static class Sync {
            public boolean tryAcquire(int acquires) {
                final Thread current = Thread.currentThread();
                int c = getState();
                if(c == 0) {
                    if(compareAndSetState(0,acquires)) {
                        setExclusiveOwnerThread(current);   
                        return true;
                    }
                } else {
                    if(current = getExclusiveOwnerThread()) {
                        int nextc = c + acquires;
                        setstate(nextc);
                        return true;
                    }
    
                    return false;
                }
            }
    
            public boolean tryRelease(int releases) {
                int c = getState() - release;
                if(Thread.currentThread() != getOwerExclusiveThread()) {
                    throw new Exception();
                }
                boolean free = false;
                if(c == 0) {
                    free = true;
                    setExclusiveOwnerThread(null);
                }
                setState(c);
                return free;
            }
        }
    }
    
  2. 读写锁

    tryAcquire(int acquires) {
        Thread current = Thread.currentThread();
        int c = getState();
        int w = exclusiveCount(c);
        if(c != 0) {
            if(w == 0 || current != getOwnerExclusiveThread()) {
                return false;
            } else {
                setState(c+acquires);
                return true;
            }
        }
        if(compareAndSetState(c,c+acquires)) {
            setOwnerExclusiveThread(current);
        }
        return true;
        
    }
    
    tryAcquireShared(int acquires) {
        int c = getState();
        int nextc = c + (1<<16);
        if(nextc < c) {
            throw new Exception();
        }
        if(exclusiveCount(c) != 0 && getOwnerExclusiveThread() != Thread.currentThread()) {
            return -1;
        }
        if(compareAndSetState(c,nextc)) {
            return 1;
        }
    }
    
  3. Condition和Object监视器方法的区别(调用该类方法必须加锁,不加锁会抛出IllegalMonitorStateException异常)

    1. condition可以拥有多个等待队列(确切的说是一个lock可以有多个等待队列,一个condition只有一个等待队列),Object只有一个
    2. condition可以在当前线程释放锁进入等待状态,并且在等待状态不响应中断
    3. condition支持当前线程释放所并等待到将来的某个时间
  4. condition.await()

    public final void await() {
        if(Thread.interrupted()) {
            throw new Exception();
        }
        //构造等待节点,并加入到等待队列中
        Node node = addConditionWaiter();
        //saveState释放节点前的状态,用于acquireQueued(node,saveState)
        int saveState = fullyRelease(node);
        int interruptMode = 0;
        //如果在等待队列一直循环
        while(!isOnSyncQueue()) {
            LockSupport.park(this);
            if((interruptMode = checkInterruptWhileWaiting(node) != 0)) {
                break;
            }
        }
        //某个线程调用signal方法,将该等待节点从等待队列中移除,移除之后加入同步队列中去获取锁
        if(acquireQueued(node,saveState) && interruptMode != THROW_IE) {
            interruptMode = REINTERRUPT;
        }
        if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
        if (interruptMode != 0)
            reportInterruptAfterWait(interruptMode);
    }
    
    public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);    
    }
    
  5. CountDownLatch和CyclicBarrier的区别

    1. CountDonwLatch只能使用一次,而CyclicBarrier可以reset()方法使用多次
    2. CyclicBarrier可以使用getNumberWaiting()查看被阻塞线程的数量
    3. CyclicBarrier可以使用isBroken()方法检查被阻塞线程是否被中断
  6. Raft算法描述
    分布式系统中存在三个理论,即CAP理论

        一致性     Consistency
        可用性     Availability
        分区容错性  Partition Tolerance
    

    Raft算法是一种强一致性算法

    领导选举(lead election)

    1. 每个节点都有三种状态-跟随者,候选者,领导者
    2. 每个节点的初始状态都为跟随者,如果一段时间内没有收到来自领导者的消息,就会变为候选者
    3. 成为候选者之后就会给其他节点发送投票消息,其他节点接受后只能发送同意的响应,当有超过半数的节点同意之后,这个后选者节点就成为了领导者,成为领导者之后就会向所有的跟随者定时发送心跳包

    日志同步(log replication)

    1. 所有数据的变化都要通过这个领导者来完成
    2. client发出一个数据变更信息后,领导者收到这个信息然后把变更信息写入日志,并通过心跳包发送给
      跟随者,跟随者收到心跳包中后,不做数据变更,直接返回响应
    3. 领导者如果收到过半跟随者的响应后,对自身数据做变更并给client端发送响应,之后通知其他跟随者进行数据变更,此时,数据一致性就得到了保证;如果没有收到,数据变更将仍然存储到日志中

    其他(other)

    1. 每个跟随者在没有收到领导者的心跳包时,都会随机产生一个倒计时(150ms-300ms),倒计时结束之后
      就会变成一个候选者,如果在倒计时结束之前收到来自领导者的心跳包,则重置倒计时。
    2. 如果某个跟随者在倒计时结束之前没有收到来自领导者的心跳包,那么他就会变成候选者,并把任期数加1,然后向其他跟随者发送投票信息
    3. 其中可能出现的情况是,有两个跟随者同时成为了同一时期的候选人,并且得到的投票数还相同,此时,这两个
      候选人再次进入一轮选举,得票多的成为领导者
    4. 如果集群网络出现了故障,导致集群被分成了A和B两个分区,原来的leader在A区,那么B区就会重新进行一轮选举,显然B区leader的任期要高于A区,经过一段时间之后,两个分区都进行了数据变更,如果此时网络恢复,分区消失,那么leaderB因为任期高于leaderA,所以leaderA收到leaderB的心跳包之后会对之前的数据进行回滚,然后按照leaderB发送过来的日志进行复制操作
  7. CAS为什么能保证操作的原子性
    因为CAS底层采用的是总线锁,即一个CPU在对一个共享变量进行输出时,其他CPU的请求将会被阻塞,这样一个CPU可以独占整个共享内存

  8. Full GC的触发时机

      1. System.gc()
      2. 老年代空间不足
      3. 方法区空间不足
      4. 发生miror GC时老年代最大连续空间小于新生代存活对象的平均大小,可以设置HandlePromotionFailure为true,会冒险进行一次Minor GC,避免频繁Full GC
    
  9. Java中有哪些引用

       1. 强引用,永远不会被垃圾收集器回收
       2. 软引用,在内存不足时被垃圾收集器回收,用来做缓存
       3. 弱引用,垃圾收集器每次扫描都要回收,用来作缓存
       4. 虚引用,作用仅仅在回收的时候得到一个通知
    
  10. Spring IOC,AOP,依赖注入

    1. IOC即控制反转,用来解决复杂系统对象耦合度太高的问题,常见的解决方式是DI(依赖注入),即用一个容器统一维护bean,方便在其他地方使用,而不是用的时候直接new
    2. AOP即面向切面编程,我们知道代码一定要力求简洁,比如我们的所有的动物类都有run(),eat()方法,这时候我们就可以抽象出来一个动物类来声明这个方法,这样子类就没有必要再对这些方法进行声明。那么,对于一些业务来说,我们可能要实现一些监控和日志打印,那么此时用父类抽象就无能为力了,对应的解决办法就是使用AOP把这些非业务代码给抽象出来,使代码保持简洁
    3. 所谓依赖注入,就是把底层类作为参数传入上层类,实现上层类对下层类的“控制”。这样上层类就不需要关心下层类是如何实现的,依赖注入是IOC的一种实现方式
  11. 重载和重写的区别
    重载即为某个类存在多个相同的函数名,但是每个函数的入参却各不相同
    重写即为子类继承父类的时候对父类方法的重定义

  12. 进程间的通信方式

    1. 管道通信
    2. 消息传递通信
    3. 共享内存通信
    4. 信号量通信
    P,V操作
    p(Samephore s) {
        s.value = s.value - 1;
        if(s.value < 0) {
            block();
        }
    }
    v(Samephore s) {
        s.value = s.value + 1;
        if(s.value <= 0) {
            wake(Q);
        }
    }
    
  13. Dubbo底层通信原理(阻塞调用-必须等待结果返回后才能继续向下执行,但是没有返回结果的可以异步)
    本质上是两个进程基于单个socket进行远程通信的问题,主要解决两个问题

    1. 短时间内如果有多个线程对服务端进行调用,那么socket连接上就会存在大量的消息传递,client端的某个线程如何知道server端返回的消息是给自己发送的呢?
    2. 客户端的某个线程发送完消息之后如何进行等待?(Dubbo底层采用的是apache mina框架进行通信)
1. client中的一个线程调用远程接口,生成一个唯一的ID
2. 将调用方法的信息(接口,方法,入参)以及处理结果的回调对象callback打包成一个对象object
3. 向专门存放调用信息的concurrentHashmap中进行put(ID,object)
4. 将ID和调用方法的信息打包成一个connRequest对象,之后使用Apache Mina框架中的IOSession.write(connRequest)方法异步发送出去
5. 当前线程使用get()方法获取远程调用的结果,在get()方法内部先使用synchronized获取对象锁,之后如果没有获取到结果就调用callback.wait()方法,释放当前线程获取的锁,并进入等待状态
6. 服务端收到消息之后进行处理,并把处理结果进行回传(包含UUID和处理结果),客户端的监听线程收到消息之后,根据UUID找到对应的callback,并把结果设置到callback对象中
7. 监听线程使用synchronized获取回调对象的锁,并使用notify()方法唤醒等待线程,等待线程唤醒之后执行get()方法即可取到服务端的结果,之后代码即可往后执行
总的来说,dubbo底层的通信原理是在apache mina框架上进行了一个封装,把之前的异步调用改成了适用于RPC框架的阻塞调用,并且通过UUID解决了收到多个调用结果如何找到callback对象的问题。
  1. Dubbo中zookeeper作为注册中心,如果注册中心挂掉,那么发布者和订阅者还能通信吗?
    可以,订阅者缓存有发布者的服务地址,可以通过缓存地址找到对应的服务发布者并进行调用,但是新增的服务发布者不能被调用到,因为注册中心已经挂掉,不能通过长连接及时的把发布者的变更推送给订阅者。

  2. Dubbo在安全机制方面是如何解决的
    Dubbo通过设置安全令牌防止用户绕过注册中心直连,然后在注册中心管理授权;Dubbo还提供黑白名单来控制服务所允许的调用方

  3. dubbo连接注册中心和直连的区别?
    一般在开发和测试环境下,采用的是点对点的直连的方式,直连将忽略注册中心提供的服务者列表。
    采用连接注册中心的方式,调用者将基于注册中心提供的服务提供者列表,基于软负载均衡算法,选择一台服务提供者进行调用,如果调用失败,再选择另外一台进行调用。注册中心不转发请求,消费者和提供者只在启动的时候与注册中心进行交互。注册中心通过长连接感知服务提供者的存在,如果发生变更,立即推送变更给消费者。

  4. dubbo通讯协议的适用场景
    传输数据包小,消费者比提供者个数多
    如果使用dubbo协议传输大文件或者大字符串,那么将会影响服务提供者的TPS
    单个提供者一般至少需要20个消费者才能压满提供者的网卡

  5. dubbo的核心配置

    <dubbo:application name="ele"/>
    <dubbo:service interface="com.ele.UserService" ref="com.ele.UserServiceImpl"/>
    <dubbo:registry address="zookeeper://127.0.0.1:2181"/>
    <dubbo:protocol name="dubbo" port="20881" />
    <dubbo:reference id="UserService" interface="com.ele.UserService" 
        loadBalance="random"/>
    

    random: 随机调用
    roundRobin: 轮询,一般设置性能最好的机子权重最大,这样每次请求过来优先轮询这个机子
    leastActive: 最少活跃调用数,越慢的机子调用的次数越少
    consistentHash: 一致性hash,含相同参数的总是调用同一台机子

  6. 什么是死锁
    进程集合中的每个进程都在等待另一个进程释放资源而造成的无限等待下去的僵持局面
    解决方案:1. 通过一个监控系统监控死锁的存在,如果存在死锁直接抛异常或报错
    2. 超时的获取锁,如果一段时间内获取不到锁就放弃获取锁的这个请求

  7. 乐观锁和悲观锁的策略
    悲观锁:假设会发生并发冲突,屏蔽一切可能违反数据完整性的操作
    适用场景:写大于读
    乐观锁:假定不会发生冲突,只是在提交操作时检查是否违背数据完整性
    适用场景:读大于写
    存在ABA问题

  8. 事务中的四大特性
    ACID 原子性,一致性,隔离性,持久性

  9. TCP与UDP的区别
    UDP是无连接的,即发送数据之前不需要事先建立连接
    UDP尽最大努力交付,不保证可靠交付
    UDP是面向报文的,应用层交给它多长的报文,它就发送多长的报文
    UDP支持一对一,一对多,多对多的通信
    UDP首部较小8个字节

  10. 源端口,目的端口

  11. 长度,检验和(检测数据传输中是否有差错)

TCP是面向连接的,发送数据之前需要经过三次握手使得双方建立可靠的连接
TCP提供可靠的数据交付
TCP提供全双工通信
TCP面向字节流
TCP首部较大,20个字节

  1. 源端口,目的端口

  2. 序号,确认号

  3. 数据偏移,保留字段

  4. URG(插队),ACK,SYN,FIN,RST(TCP连接出现严重差错,必须先释放然后重新建立连接),PSH(双方都不用等到缓存区满就进行报文交付)

  5. 窗口,发送该报文段一方的接收窗口,即接收方告诉发送方允许对方发送的数据量
    默认单次最大传输包为536字节
    MSS(Max Segment Size)=536字节+20(首部) = 556字节

  6. TCP流量控制
    流量控制是为了解决发送速率过快,而接收方来不及接受的情况,这是一种端对端的控制。

    1. 连接建立时,接收方B就给A发送接受窗口(receive window)的大小
    2. 发送方的窗口大小不能超过接收方的窗口大小
    3. 当接受方来不及接受时,便在发送ACK确认报文段时缩小接受窗口的值
    4. 发送方收到确认报文以后及时调整发送窗口的大小
    5. 考虑一种极端情况,接受窗口的值为0,并把这个ACK数据报成功发送给了发送方A,过了一段时间之后,接收方B又有了缓存空间,想发送方A发送rwnd=400的报文段,但是这个报文段在传输过程中丢失了,此时,发送A在等待接收方B发送新的接受窗口,接受方B在等待A发送新的数据,这样就形成了死锁的局面。
    6. 打破死锁的解决方案就是任意一方收到0窗口值时,都立即设定一个计时器,计时器结束的时候就向对方发送一个零窗口探测报文段,对方收到后给出现在的窗口值,如果还为零重置计数器继续上述流程,直到窗口值不为0为止。
  7. TCP拥塞控制
    解决的是整个网络对资源的需求>实际可用资源的问题。解决的思路就是减少对网络资源的需求。

    1. 慢开始
      拥塞窗口从1(指的是最大报文段MSS的大小)开始,每经过一个传输轮次之后就把拥塞窗口的值加倍
    2. 拥塞避免
      当拥塞窗口增加到大于阈值时,改为使用拥塞避免的算法增加拥塞窗口的值,即经过一个传输轮次之后拥塞窗口值加1

    无论是在慢开始还是拥塞避免阶段,只要出现网络拥塞(即没有按时收到报文段的确认),那么拥塞窗口的大小就直接变为1,并且阈值变为当前拥塞窗口的一半

    1. 快重传
      接受方每收到一个失序的报文段都立即发送重复确认,而不用等待自己发送报文段的时候才进行捎带确认
      发送方在一连收到接受方3个重复确认之后,就会立即重传失序的报文段,而不用等待计时器结束之后再重传
    2. 快恢复
      与快重传算法进行配合,在收到3次重复确认时,不必执行慢开始算法(因为此时网络状况还行,要不然不会收到3次重复确认),而是把拥塞窗口减半,并把阈值也减半之后执行拥塞避免算法。
  8. 三次握手和四次挥手
    握手:


    IMG_5896.JPG

    挥手:


    IMG_5894.JPG

    重点关注client端发送的最后一个ACK确认报文段之后的状态为Time_wait,client端需要等待2MSL(报文段最大生存时间)时间之后才会彻底关闭,这么做的目的主要如下:
    1. 最后发送的ACK报文段可能会在网络中丢失,造成server端一直等待client端的ACK报文段而无法关闭的情况,因为server端在发送FIN关闭请求之后会进行一个倒计时,倒计时结束还没有收到ACK确认就会重新发送FIN关闭请求,此时client端就可以利用这2MSL时间对这个重发的FIN关闭请求进行确认
    2. 防止“已失效的连接请求”,经过2MSL之后,该次连接所产生的请求都将失效(保证都传输到server端,不会阻塞在某个网络节点上),保证下一次连接中不会收到旧的连接请求
  9. 数据库的并发问题

    1. 脏读
      事务A读取了事务B尚未提交的数据
    2. 不可重复读
      同一个事务在不同的时间点读取到的数据不同(因为某个事务对数据进行了删除或者修改)
      处理策略:对数据施加行级锁
    3. 幻读
      同一事务在不同的时间点读取到的数据不同(因为某个事务对数据进行了增加)
      处理策略:施加表级锁
  10. 事务的隔离级别
    Read Uncommited
    Read Commited
    Repeatable Read--MySQL默认隔离级别
    Serialiable
    设置会话的隔离级别:
    SET TRANSCATION ISOLATION LEVEL READ COMMITED

  11. MySQL的存储引擎
    InnoDB:
    支持事务,通过MVCC支持高并发,实现了4个隔离级别,默认的隔离级别是Repeatable Read,并通过间隙锁来防止幻影行的出现
    InnoDB表是基于聚簇索引建立的
    MyISAM:
    不支持事务和行级锁,MyISAM对整张表施加锁,读的时候施加共享锁,写的时候施加排他锁
    MyISAM表是基于非聚簇索引建立的

  12. MySQL的索引

    1. B+Tree
      支持的存储引擎:InnoDB,MyISAM
      所有的值都是按照顺序存储的(具体排列顺序和定义索引的顺序有关),查找时不需要进行全盘扫描,而是从根节点开始往下搜索节点页,根据节点页值的不同(节点页值即为子层的上下限)来选择指针进入下层子节点,直到找到该值或者该值不存在


      b+tree

      优点:可以按范围查找,适用于左前缀,列前缀,精确匹配某一列并范围匹配另一列
      缺点:如果不是从索引最左列开始查找,索引失效;
      不能跳过索引中的列
      如果查询中某个列是范围查询,其右边的都不能用索引优化查询

    2. Hash索引
      支持的存储引擎:Memory
      对所有索引列都计算一个hash码,然后维护一个hash表,表中存放着每个数据行的hashCode和对应的数据行指针,hash表中每个slot(槽)的编号的顺序的,但是对应的数据行却不是

      hash索引

      SELCT lname FROM user WHERE fname = 'Peter';
      如上查询会先计算f('Peter')的hash值,然后根据hash值找到对应的行指针,找到之后取出数据与'Peter'进行比对,以便确保就是要查找的行

      优点:适用于精确查找,查找速度非常快
      缺点:无法排序,不支持按部分索引列查找,不支持范围查询,hash冲突的时候维护代价高昂

      创建自定义hash索引,典型应用场景(表中存放的url需要建立索引),此时即可再增加一列用来存放hashCode,在进行查询时MySQL优化器会基于这个hashCode列的索引来完成查找,因为整数匹配远比字符匹配来的快,这样实现的缺陷是需要手动维护hash值,维护的实现方式是手动创建触发器

      SELECT id FROM url WEHRE url = 'http://www.mysql.com' AND url_crc = CRC32('http://www.mysql.com');

  13. MySQL索引实现
    MySQL中有两种存储引擎,MyISAM和InnoDB,两种存储引擎实现索引的方式都是采用B+Tree
    MyISAM(非聚簇索引):

    1. MyISAM中索引文件和数据文件是相分离的
    2. MyISAM索引中的叶节点的data域存放的是指向数据记录的地址
    3. MyISAM主索引和二级索引没有区别,只是主索引不能重复

    InnoDB(聚簇索引):

    1. InnoDB数据文件本身就是索引文件
    2. InnoDB索引中的叶节点包含了完整的数据记录
    3. InnoDB索引按照主键进行聚集,如果没有主键就选一个非空的索引作为主键,如果还没有,InnoDB会隐式定义一个主键
    4. 二级索引需要查找两次,第一次找到主键,之后通过主键找到数据
  14. 索引是不是越多越好?
    索引虽然可以加快查询速度,但是索引本身也是需要占一定的存储空间的,同时插入和删除记录时也会额外的耗费一些时间和资源,并且在MySQL运行的时候也需要消耗资源去维护索引
    以下两种情况不建议使用索引:

    1. 表中的数据较少时(少于2000)
    2. 索引的选择性较低时(选择性等于索引列的不重复数/表中的行数)
  15. 聚簇索引和非聚簇索引分别在什么情况下使用?
    聚簇索引:

    1. 范围查找
    2. 返回结果集较多
      非聚簇索引:
    3. 频繁更新的列
    4. 返回结果是少量的数据集
  16. 多表查询怎么优化?

  17. SQL优化

    1. 避免查询不必要的行,使用limit,当一个表的分页多达几万页的时候,此时使用limit的效率将变得非常低,因为limit每次都会从初始位置开始向后进行遍历,然后找到该页数据并返回。改善的方法有两种:1. 子查询使用索引先查找出偏移量,然后父查询通过limit限定取出的结果数 2. 如果某列有序,添加where语句使得直接从分页位置开始查找
      select * from user where user_id >= (select user_id from user limit 100000,1) limit 30;
      
      limit优化.png
    2. 避免每次查询取出所有的行,这样做会使覆盖索引失效,但是却简化了开发,提高了代码的通用性
    3. 查询相同的数据尽量使用缓存
    4. 避免在SQL语句中使用函数或者表达式,会使索引失效
  1. 对RocketMQ了解多少?
    RocketMQ中总共有四种角色,producer,consumer,nameServer,broker

    1. producer定期从nameServer中获取topic路由信息,并缓存到本地,包括可用的broker。当producer需要发送一个消息时,会先根据路由表查找某个topic对应的broker,然后根据某种策略将消息发送到该broker上,如果nameServer挂掉,那么producer可以继续使用本地缓存的路由表进行查找
      发送策略:
      同步:一直等到broker响应(返回一个sendResult包含发送状态)
      异步:调用callback函数处理Broker的响应(返回一个sendResult包含发送状态)
      单向:只发送不接受响应,适合于对消息的可靠性要求不高的场景,比如说发送日志

    2. consumer通过nameServer拿到路由表并缓存到本地,然后并发的从消息队列中获取消息并进行消费,随之而来的就是消费顺序的问题(先要创建订单,才能确认订单,最后进行付款),消息队列是支持消息顺序的
      pull consumer: 主动从broker拉取消息
      push consumer: 当消息到达时调用callback函数处理

    3. broker负责存储队列信息,维护消费进度,定时向NameServer上报topic路由信息,如果一台broker挂掉,那么producer不会立即感知,而是nameServer发现该broker心跳包超时的时候,会将该broker从集群中清除并告知相应的producer,如果producer已经把消息发送给挂掉的broker,那么肯定会发送失败,producer会重新选择另外一台broker发送消息,这样就避免了消息的丢失。

    4. nameServer,可集群,一台nameServer挂掉之后,另一台顶上,broker会对nameServer集群中的每一个节点都发送心跳包,这样保证了集群中的每个节点的路由信息都是同步的。

  2. 如果保证消息能被顺序消费?
    假设一个场景,订单必须先创建,然后才能让用户确认订单,最后才能付款
    方案一:

    1. 那么就可以根据每个订单的订单号进行hash运算,把相同订单号的消息放在同一个消息队列中
    2. 然后消费端的线程池降为单线程,每次只能从消息队列中取一条消息进行消费。

    方案二:

    1. 相同订单号的消息可以放在不同的消息队列中,但是后面的消息在被消费前先检查前面的消息有没有被消费完,如果被消费完,后面的消息才可以被消费
    2. 否则,后面的消息所在消息队列将被阻塞,或者借鉴Java并发里面的解决方案,设置一个等待队列,将被阻塞的消息临时存放在等待队列中,等待前置消息被消费完毕之后唤醒等待中的消息,然后重新把消息放在消息队列中。
  3. 平时关注了哪些行业前沿?
    比特币,比特币在2009年由中本聪发明,为什么会在2009年这个时间发明呢?因为2008年的金融危机导致了大量的银行倒闭甚至国家处于破产的边缘,是的民众不在相信自己存在银行里的钱是安全可靠的,所以促使了这种去中心化的货币出现。
    比特币具有匿名性,不可抹灭性,公开透明性。
    匿名性是指每个人都可以去申请一个比特币钱包,而这个钱包的地址是一串随机的字符,这样就没有人知道谁是谁了。
    不可抹灭性是指一旦一笔交易被写入到区块链中,那么他就永远无法被修改,除非你所控制的算力能够达到51%
    公开透明性指每个人都可以看到其他人的钱包里都多少币

    比特币发行总量为2100万枚,现在已经发行了80%多,比特币所有的交易都会在区块链中进行存储,每笔交易都需要支付一定量的手续费,这样才会有节点帮助你进行存储这笔交易,一旦某个节点存储了某笔交易那么就会通知其他所有节点都去存储这笔交易。
    区块链中的一个块大小为1M,每10分钟产生一个块,每个块中都有一定数量的比特币,当一个块存满的时候,需要暂停全部的交易,然后所有的节点都会去算这个块的nonce值,第一个算出来的将获得这个节点中存储的比特币,之后交易继续进行,现在大概已经有了50多个块。

  4. 手写快排,单例

  5. Maven

  6. 如果线上某个机器的CPU占用高达100%,如何快速进行问题定位?

    1. 利用top命令查看哪个进程占用了CPU(top -c P)
    2. 找出进程中最耗CPU的线程(top -Hp 10675)
    3. 将进程ID转化为16进制(printf "%x\n" 10765)
    4. 然后使用jstack工具看看线程在干什么(jstack 10675 | grep '0x2a34'),同时可以配合使用jmap -histo:live pid 查看该进程中存活对象(如果发生OOM的话)
  7. 如果线上出现了OOM异常,如何进行排查?

    1. 开启-XX:+HeapDumpOnOutOfMemoryError参数使得JVM在发生OOM时生成一个dump(堆转储)文件
    2. 可以使用VisualVM工具导入dump文件,然后进行查看
    3. 首先确定是内存泄漏还是内存溢出
    4. 如果是内存溢出那么就要增大堆的内存空间,如果是内存泄漏就要查看内存泄漏报告,分析是哪个线程的哪个对象出了问题
    5. 如果发现一个对象非常大,那么就可以查看该对象的GC链,看看他到底引用了哪个GCRoots节点,然后在根据引用链去定位代码
    查看异常对象.png
    查看对象的GC链.png
    查看GCRoots.png
  8. Java应用的异常行为都有哪些?

    1. Java应用被认为杀死(kill,kill -9)
    2. Java应用发生OOM
    3. 系统发生OOM
  9. Java内存泄漏的场景

    1. 静态集合类
    2. 各种连接
    3. 单例模式
  10. 如何进行JVM调优?
    性能调优的原则;

    1. MinorGC回收原则:每次发生MinorGC时应该尽可能的回收垃圾对象,以减少应用发生FullGC的频率
    2. GC内存最大化的原则:处理吞吐量和时延问题时,可供垃圾收集器使用的内存越大,垃圾的收集效果越好,应用程序也越来越流畅
    3. GC调优3选2原则:在性能属性里面,内存,延迟,吞吐量三者我们只能选其中两者进行调优,不可三者兼得。
    4. 调优需要按照先后顺序来,即内存>延迟>吞吐量

    内存

    1. 确定应用在压力测试下进入稳定运行时的内存占用,然后计算此时的对象活跃大小,如何确定应用已经进入了稳定阶段呢?那就是查看GC日志,多收集几次,然后取平均值即可获得老年代对象的平均活跃大小。没有GC日志的可以使用jmap -heap 查看堆的使用情况
    2. 然后对各个区域的内存大小进行调整
      堆大小 老年代4
      新生代 老年代
      1.5
      老年代 堆大小-新生代大小
      永久代 永久代*1.5
    3. 一般情况下,为了避免老年代扩容的时候触发fullGC,通常需要制定-Xms和-Xmx(-XX:PermSize和-XX:MaxPermSize)的大小一样,这样可以让JVM在启动的时候就把老年代和永久代的空间定下来,避免运行时自动扩展。这样可以避免在启动的时候发生多次fullGC

    延迟
    导致延迟的主要原因还是发生GC,导致系统出现某段STW(Stop the world)
    主要关注以下四点:

    1. minor gc的时间
    2. minor gc的频率
    3. full gc的时间
    4. full gc的频率

    主要是通过gc日志查看,得到的数据越平均越好,调小空间可以降低gc时间,但是会增加gc频率,根据具体需要动态调整

    增加吞吐量
    可以使用CMS垃圾收集器增大吞吐量

  11. CMS垃圾收集器有哪些缺点?

    1. CMS垃圾收集器对CPU资源非常敏感
      虽然并发标记和垃圾回收这两个阶段不会导致用户线程停顿,但是由于垃圾收集器会占用相当一部分的CPU资源会导致用户的应用程序变慢。
    2. CMS垃圾收集器无法处理浮动垃圾
      可能出现"Concurrent Mode Failure"失败而导致另一次FullGC的产生。因为在垃圾收集阶段用户线程还在执行,这将导致在该阶段用户线程产生的垃圾不能进行回收,只能等到下次回收,因此CMS垃圾收集器每次都需要预留一部分空间用户线程使用,而不能等到老年代几乎满了再进行收集,如果预留的内存不够用,那么就会出现一次"Concurrent Mode Failure"失败,此时JVM会采用预备方案--临时使用Serial Old垃圾收集器进行老年代的垃圾回收,这样停顿时间就很长了
    3. CMS垃圾收集器会产生内存碎片
      CMS垃圾收集器采用的是标记-清除算法进行垃圾回收,这样不可避免的会产生内存碎片的问题,可以使用-XX:+UseCMSCompactAtFullCollection参数开启内存碎片的合并整理过程,这个过程也是需要STW的。还可以通过-XX:CMSFullGCsBeforeCompaction设置执行多少次不压缩的FullGC之后跟着来一次带压缩的FullGC
  12. 各种排序的思想

    1. 快速排序
    2. 归并排序 采用分治法,先将子序列有序,然后再使子序列合并成为一个有序的子序列段,最后使整个序列有序
    3. 希尔排序 把记录按照下标的一定增量进行分组,对每组使用直接插入排序,随着增量的逐渐减少,每组包含的关键词越来越多,当增量减小到1时,整个记录恰好被分成一组,算法终止
  13. ThreadLocal
    用来保存线程的局部变量,不会发生内存泄漏,因为Entry类继承自WeakReference,会在GC发生之前回收这部分数据。

  14. Spring中存在什么样的设计模式
    适配器模式(HandlerAdpter,根据HandlerMapper找到对应的Adpter)
    单例模式(容器中存在的实例基本都是单例,SpringMVC中的Controller)
    工厂模式(BeanFactory)
    装饰器模式(用来设置JavaBean属性的BeanWrapper)
    模板模式(JDBCTemplate,1.获取数据库连接2.创建执行语句3.执行SQL语句4.处理结果集5.释放资源)

  15. 设计模式分为哪几类

    1. 创建型模式
      单例模式 工厂模式 抽象工厂模式
    2. 结构型模式
      外观模式 隐藏系统的复杂性,并向客户端提供一个可以调用的接口
      装饰器模式 允许向一个对象增加新的功能,但是却不改变对象的结构(如Java中的IO)
      适配器模式 适配器模式。解决两个系统不兼容的问题(SpringMVC中的HandlerAdapter)
    3. 行为型模式
      模板模式 定义一个操作中算法的骨架,而将一些步骤延迟到子类中,使得子类不改变算法的结构即可重新定义算法的某个步骤
    4. J2EE模式
      MVC模式
  16. Java中创建对象的4种方式

    1. new
    2. 反射(class.newInstance,class.getDeclaredConstructor().newInstance())
    3. clone(),implements Cloneable接口,并且重写clone()方法,默认是浅拷贝
    4. 反序列化
  17. 使用Statement和PreparedStatement有什么区别?

    1. 使用Statement每次查询都会进行编译,一次查询只会产生一次网络请求,而且安全性很差
    2. 使用PreparedStatement,对于相似的SQL只会编译一次,编译之后只要缓存命中,那么就可以跳过编译阶段直接运行,并且SQL语句使用占位符,提高了代码的可读性和安全性
    3. 可以使用preparedStatement.addBatch()对SQL语句进行批处理,也就是一次给DB发送多条SQL,这样可以减少网络请求
  18. 负载均衡算法
    负载均衡服务器需要实现两个功能:1. 根据负载均衡算法和应用服务器列表计算出处理该请求的web服务器地址 2. 将该请求发送到该web服务器上

    1. 轮询(Round Robin) 硬件都一致
    2. 加权轮询(Weight Round Robin) 硬件不一致
    3. 随机(Random) 集群数量越高越均匀
    4. 最少连接(Lest Connections) 最合理
    5. 源地址散列(Source Hashing) 相同IP的请求总是转发到固定的节点上,保证会话的生命周期
  19. 分布式缓存集群
    分布式缓存集群和服务集群完全不同,分布式缓存集群的节点上每个节点存储的数据各不相同,必须先找到缓存有该数据的服务器,然后才能访问,并且从集群的伸缩性考虑,必须使新加入节点对整个集群的影响最小(使整个缓存集群中已经缓存的数据还能被访问到)
    可以采用一致性hash算法来解决该问题:

    1. 构造一个长度为2的32次方的整数环
    2. 根据节点名称的hash值将该服务器放置在这个hash环上
    3. 根据请求的key的hash值在hash环上顺时针查找离它最近的服务器节点
    hash环.JPG

    采用上述算法会存在新增节点之后各个服务器负载压力不均衡的问题,此时可以通过增加虚拟节点来解决,即在hash环上存在的都是一些虚拟节点,多个虚拟节点映射到同一个物理服务器上,当增加一个物理服务器时,同时在虚拟环上增加多个虚拟节点,这样可保证增加节点后各个物理节点的负载是均衡的,在实战中为了权衡负载和性能的影响,通常虚拟节点的数量选择为150

    虚拟节点hash环.JPG
  20. 负载均衡服务器实现方式

    1. HTTP重定向负载均衡
      该负载均衡服务器是一个普通的web应用服务器,收到一个请求之后,根据某种策略计算出真实的web应用服务器的地址,并将web应用服务器的地址写入HTTP的重定向响应中,缺点是每次请求都需要进行重定向,并且不利于搜索引擎的SEO

    2. DNS域名解析负载均衡
      在DNS进行域名解析的阶段,解析为某个应用服务器的IP地址。缺点是某个IP地址的应用服务器挂掉之后,DNS服务器最快也要十几分钟才可以感知,如果此时用户访问页面将会得到一个错误的页面

    3. 反向代理服务器(通过应用进行请求的转发)
      应用服务器不需要对外暴露IP地址,只需要对反向代理服务器暴露内网的IP即可,反向代理服务器需要配备双网卡,实现内外两套IP地址。一个请求到达之后,根据某种算法计算出真实的web应用服务器地址,然后将请求转发给它,并且收到响应之后再将该响应交给客户。缺点是反向代理服务器是整个集群请求和响应的中转站,它可能成为整个集群性能的瓶颈。

    4. IP负载均衡
      通过进程进行IP的转发,较与反向代理服务器的通过应用进行转发拥有更好的处理性能

    5. 数据链路层负载均衡
      通过在通讯协议的数据链路层修改MAC地址完成转发,较与IP负载均衡性能更好

  21. Diamond介绍
    Diamond是一个管理持久配置的系统,从系统架构来看,总共分为三个角色

    1. client:client是配置信息的使用者,client在启动并第一次获取数据的时候会将数据的MD5保存在内存中,还会在启动的时候创建一个定时任务(15s),定时检查配置是否发生了更新
    2. http server:存放着server的地址,client通过http server找到某一台server并获取到配置信息,充当着一个路由的角色
    3. diamond server:可集群,server在启动时,向http server注册自己的地址,然后把数据的MD5(从MySQL获取)保存在内存中,数据更新时,会更新内存中的MD5。

    架构和过程:

    1. 当收到client发出来的数据校验时会对MD5进行比对,如果没有变化,返回一个标识不变的字符串给clinet,如果有变化,返回变化的group和dataId,之后client会再次请求新的数据
    2. diamond server可集群,集群中的每台机器都连接同一个mysql,集群中的数据同步方式有两种:1. 每台机器定时去MySQL dump数据到本地文件 2. 当一个server发生了数据变更以后,通知其他server去dump Mysql最新数据到本地
    3. 发布数据时,数据先写到mysql,再写到本地文件,订阅数据时,直接查本地文件而不用去查数据库,最大程度减小mysql 的查询压力

    容灾机制:

    1. 数据库不可用
    2. 所有的server不可用
    3. clinet主动删除snapshot
    4. client没有备份配置数据,导致其不能配置“容灾目录”
  22. MySQL处理SQL语句的流程


    MySQL逻辑架构.jpg
    1. 线程池处理该请求
    2. 解析器解析SQL语句(如果缓存命中可以跳过该步骤)
    3. 是否命中缓存,如果命中缓存,跳过解析、优化和执行的过程,直接返回缓存中的结果集
    4. 没有命中缓存进行语句解析,优化并执行
    5. 通过存储引擎查询结果,并返回结果集
  23. MySQL语句执行顺序

    1. FROM 表之间做笛卡尔乘积产生虚表v1
    2. ON 根据ON指定的条件进行筛选,产生虚表v2
    3. JOIN 如果有外连接,会进行补偿(添加null值),产生虚表v3
    4. WHERE 根据where条件筛选v3表,产生v4虚表
    5. GROUP BY 根据某个字段对表进行分组,产生v5虚表
    6. HAVING 对组进行过滤,产生v6虚表
    7. SELECT 选择指定的列插入到v7虚表
    8. DISTINCT 对v7虚表进行去重操作,然后生成v8虚表
    9. ORDER BY 按照某个字段进行排序,生成v9虚表
    10. LIMIT 限制查询的个数,生成v10虚表,并将结果返回
  24. Thread.join()
    如果A线程执行了thread.join()语句,那么线程A会等待thread线程终止之后才能继续向下运行

  25. MySQL explain参数说明

    1. id 查询的ID
    2. select_type
      • simple 简单查询
      • primary 包含复杂的子查询,最外层的select_type为该值
      • subquery 在select或where里包含子查询
    3. table 本次查询是哪张表
    4. type 表的连接类型,性能由高到低排列如下
      • system 表中只有一行记录,相当于系统表
      • const 通过索引一次就能找到,只匹配一行数据
      • eq_ref 唯一性索引扫描,每个索引键只匹配表中一行数据
      • ref 非唯一性索引扫描,返回匹配某个索引键的所有行
      • range 范围索引,使用一个索引来选择行
      • index 只遍历索引树
      • ALL 全盘扫描
    5. possible_keys 指出MySQL使用哪个索引在该表找到行记录,如果该值为NULL,说明没有用到索引,需要使用索引来提高性能
    6. key MySQL实际使用的索引
    7. key_len 使用索引的最大长度
    8. ref 显示该表的索引字段关联了哪张表的哪个字段
    9. rows 本次查找遍历的行数
    10. extra 额外信息
      • using index 使用了覆盖索引
      • using where 使用where语句限定某一行
  26. QLExpress规则引擎内部原理
    QLExpress是一个脚本引擎,在编写机审项目中的售卖规则时就使用到了该规则引擎,使用此规则引擎可以灵活的定义各种售卖规则,并且使这些配置即时生效。

    1. 用户定义好自己的规则脚本之后,规则引擎对用户输入的脚本进行语法和词法的解析
    2. 然后生成对应的指令集合
    3. 输入上下文中变量的具体值,这些具体的变量值就是售卖规则中的某一个条件,
    4. 当一个方案订单过来之后,解析脚本,并调用runner执行脚本,订单需要一一匹配脚本中变量的条件,当所有条件都符合时,方案才能提交成功。
  27. Java中能够创建泛型数组吗?
    不可以,因为泛型会在编译时期被擦除掉,这会使得数组中可以存放任意的数据类型,而Java中的数组是强类型的,在向数组中添加元素时会进行类型检查。

  28. 四种线程池

    1. Executors.newCachedThreadPool()
      核心线程池大小为0,意味着空闲时可以回收所有线程,适用于小而短的任务,这样可以重复使用线程,降低资源消耗
    2. Executors.newFixedThreadPool()
      创建固定大小的线程池,并使用无界队列,可控制线程的最大并发数
    3. Execuotrs.newScheduledThreadPool()
      可以执行一些定时和周期任务
    4. Executors.newSingleThreadExecutor()
      单线程,保证任务的FIFO顺序
  29. Java中的锁升级

    1. 偏向锁
      引入偏向锁的目的是减少无竞争状态下的同步操作。当线程A第一次获取到锁对象时,会把锁对象中MarkWorld中的标志位置为01(01就标志着偏向锁),并且通过CAS操作将线程ID写入到锁对象的MarkWord中。此时如果另外一个线程B再次去获取锁,那么等待A线程释放偏向锁之后,偏向锁就升级为轻量级锁。

    2. 轻量级锁
      引入轻量级锁目的是减少使用操作系统的信号量,实现机制是,线程在竞争轻量级锁之前,在线程的栈帧中分配一段空间作为锁记录空间(也就是轻量级锁对应的对象的对象头的拷贝),然后尝试CAS将锁对象的MarkWord更新为指向Local Record的指针,如果修改成功,则获取锁成功。如果此时有另外一个线程竞争锁,那么轻量锁就升级为重量锁。同样,释放锁需要将锁对象的MarkWord替换回来。

      偏向锁和轻量锁比较:
      偏向锁是为了消除同步操作,轻量锁是为了避免使用系统信号量,两种锁都是在无竞争状态下表现最优
      轻量锁相对于偏向锁多了CAS解锁和加锁操作,所以开销比偏向锁大

    3. 自旋锁
      自旋状态是为了避免线程过早的进入阻塞状态,进入阻塞状态之后就需要进程的挂起和恢复,这中间的开销是比较大的。根据大量的数据分析,占有锁的时间一般是非常短的,所以获取轻量级锁失败后线程仍然活跃的去尝试获取锁(占用CPU资源),如果尝试的次数到达一定数量,锁升级为重量级锁

    4. 重量级锁
      重量级锁就是Java中的对象监视器,所有线程竞争的获取锁,获取失败就进入阻塞状态,等待被唤醒,唤醒之后再次尝试获取重量级锁,而线程的挂起和唤醒都需要操作系统来完成,因此线程需要频繁的在用户态和核心态之间不断的切换,开销很大。JDK1.5之前synchronized采用的都是重量级锁,JDK1.6优化之后会根据条件适当的选择对应的锁。

  30. Java中的锁降级
    写锁可以降级为读锁,目的是为了保证刚写入的值能够被立刻读取
    方法就是先获取写锁,写操作完毕之后再获取读锁,之后读取值,再依次释放写锁和读锁

  31. 高可用

    1. 使用反向代理和负载均衡实现分流
    2. 通过限流保护应用免受雪崩之灾
    3. 通过降级实现服务部分可用,有损服务
    4. 通过隔离实现故障隔离
    5. 通过设置合理的超时调用与重试机制避免请求堆积造成雪崩
    6. 通过回滚机制快速修复错误版本
  32. MySQL的MVCC(Muti-Version Concurrency Control)
    早期的数据库只支持读读并发操作,但是读写,写读,写写都会被阻塞,引入MVCC之后,只有写写被阻塞,其他三种操作是可以并行的。这样大幅提高了InnoDB存储引擎的并发量。

    1. MVCC只在Read Commited,Repeatable Read隔离级别下工作
    2. 通过在每行中添加三个隐藏的列来实现MVCC(事务ID,回滚指针,DB_ROW_ID)
      1. 事务ID:用来标识最后一个对该行进行修改的事务ID
      2. 回滚指针:当提交时发现数据完整性被破坏,利用回滚指针指向的undo log进行回滚数据操作
      3. DB_ROW_ID:当用户没有指定聚簇索引的主键或唯一的索引时,InnoDB会自动生成聚簇索引,并使用DB_ROW_ID作为聚簇索引的主键
    3. 修改数据时,拷贝出当前数据的版本,然后任意修改,各个事务之间互不影响
    4. 提交数据时,检查数据的版本号,如果成功,覆盖原纪录,失败通过指针进行数据回滚,
  33. 四层负载均衡和七层负载均衡

    1. 二层负载均衡
      修改MAC地址
    2. 三层负载均衡
      修改IP地址
    3. 四层负载均衡(IP地址+端口)
      通过对报文中的目标IP进行修改,使得客户端可以和web应用服务器三次握手建立连接
    4. 七层负载均衡(URL进行转发)
      负载均衡服务器首先和客户端三次握手建立连接,根据客户端请求的报文,负载均衡服务器再与真实的web应用服务器三次握手建立连接
      优点:
      可以根据请求的内容进行转发,如图片请求转发到图片服务器
      可以防止DDOS攻击到真实的web应用服务器
      可以过滤特定的报文,防止SQL注入攻击

推荐阅读更多精彩内容