面试题汇总

JAVA基础

JDK基础:

1:强引用、弱引用、虚引用、软引用

强引用:就是普通的对象引用

StringBuffer str = new StringBuffer(“hello world”);

局部变量str会被放到栈里,而StringBuffer实例对象会被放在堆内,局部变量str指向堆内的StringBuffer对象,通过str可以操作该对象,那么str就是StringBuffer的强引用。

强引用具备如下特点:

1、通过强引用可以直接访问目标对象

2、强引用所指向的对象在任何时候都不会被系统回收,虚拟机宁愿抛出OOM(内存溢出)异常,也不会回收强引用所指向的对象

3、强引用可能导致内存泄漏(站着空间不释放,积累的多了内存泄漏会导致内存溢出)

软引用:描述一些还有用但非必需的对象,用java.lang.ref.SoftReference类表示,对于软引用关联的对象GC未必会一定会收,只有当内存资源紧张时,软引用对象才会被回收,所以软引用对象不会引起内存溢出(OOM)。

弱引用:是比软引用还弱的引用,在系统进行GC 时,只要发现弱引用,不管系统的堆空间是用了一点还是用了一大半,都会回收弱引用的对象。但是通常GC线程的优先级较低,因此不能立即发现持有弱引用的对象,在这种情况下弱引用对象可以存在较长的时间,一旦弱引用对象被回收,弱引用对象会加到一个注册的引用队列中去。

虚引用是所有引用中最弱的一个持有一个虚引用的对象,和没有引用一样,随时都有可能会被垃圾回收器回收,当用虚引用的get方法去尝试获得强引用对象时总是会失败,并且他必须和引用队列一起使用,用于跟踪垃圾回收过程,当垃圾回收器回收一个持有虚引用的对象时,在回收对象后,将这个虚引用对象加入到引用队列中,用来通知应用程序垃圾的回收情况。

2:final关键字的作用 (方法、变量、类)

final关键字可以用来修饰引用、方法和类。

   1、用来修饰一个引用

 如果引用为基本数据类型,则该引用为常量,该值无法修改;

 如果引用为引用数据类型,比如对象、数组,则该对象、数组本身可以修改,但指向该对象或数组的地址的引用不能修改。

 如果引用时类的成员变量,则必须当场赋值,否则编译会报错。

 2.用来修饰一个方法

    当使用final修饰方法时,这个方法将成为最终方法,无法被子类重写。但是,该方法仍然可以被继承。

3.用来修饰类

 当用final修改类时,该类成为最终类,无法被继承。简称为“断子绝孙类”。

常用的String类就是final类。

3:泛型、泛型继承、泛型擦除

泛型擦除:在继承(实现)或使用泛型父类时,没有指定具体的类型。一旦擦除后按object处理。

4:jdk ServiceLoader

ServiceLoader是jdk提供动态加载类的一种方式。可以使得用户能够在运行时动态解析目标文件夹下接口配置文件来动态加载相关类使得直接可以在运行时直接保证相关类的加载;

一、使用场景

一般使用接口的实现类都是静态new一个实现类赋值给接口引用,如下:

HelloService service = new HelloImpl();

如果需要动态的获取一个接口的实现类呢?全局扫描全部的Class,然后判断是否实现了某个接口?代价太大,一般不会这么做。一种合适的方式就是使用配置文件,把实现类名配置在某个地方,然后读取这个配置文件,获取实现类名。JDK给我们提供的TestServiceLoader 就是这种方式。

ServiceLoader是SPI的是一种实现,所谓SPI,即Service Provider Interface,用于一些服务提供给第三方实现或者扩展,可以增强框架的扩展或者替换一些组件。

其实关于ServiceLoader,我们平时虽然很少用到,但是却在背后为我们做了很多事情,最常见的就是JDBC的操作了,相信大家对DriverManager类并不陌生,我们可以通过该类加载驱动并获得数据库连接,在这个类中其实就用到了ServiceLoader。

5:LinkedList、LinkedHashMap、LRU

LinkedList也是List接口的实现类,与ArrayList不同之处是采用的存储结构不同,ArrayList的数据结构为线性表,而LinkedList数据结构是链表。链表数据结构的特点是每个元素分配的空间不必连续、插入和删除元素时速度非常快、但访问元素的速度较慢。

LinkedList是一个双向链表, 当数据量很大或者操作很频繁的情况下,添加和删除元素时具有比ArrayList更好的性能。但在元素的查询和修改方面要弱于ArrayList。LinkedList类每个结点用内部类Node表示,LinkedList通过first和last引用分别指向链表的第一个和最后一个元素,当链表为空时,first和last都为NULL值

 

HashMap是无序的,当我们希望有顺序地去存储key-value时,就需要使用LinkedHashMap了

LinkedHashMap是HashMap的子类,但是内部还有一个双向链表维护键值对的顺序,每个键值对既位于哈希表中,也位于双向链表中。LinkedHashMap支持两种顺序插入顺序 、 访问顺序

插入顺序:先添加的在前面,后添加的在后面。修改操作不影响顺序

访问顺序:所谓访问指的是get/put操作,对一个键执行get/put操作后,其对应的键值对会移动到链表末尾,所以最末尾的是最近访问的,最开始的是最久没有被访问的,这就是访问顺序。

LRU缓存算法:缓存这个东西就是为了提高运行速度的,由于缓存是在寸土寸金的内存里面,不是在硬盘里面,所以容量是很有限的。LRU这个算法就是把最近一次使用时间离现在时间最远的数据删除掉。先说说List:每次访问一个元素后把这个元素放在 List一端,这样一来最远使用的元素自然就被放到List的另一端。缓存满了t的时候就把那最远使用的元素remove掉。但更实用的是HashMap。因为List太慢,要删掉的数据总是位于List底层数组的第一个位置,删掉之后,后面的数据要向前补位。。所以复杂度是O(n),那就用链表结构的LinkedHashMap呗~,LinkedHashMap默认的元素顺序是put的顺序,但是如果使用带参数的构造函数,那么LinkedHashMap会根据访问顺序来调整内部 顺序。 LinkedHashMap的get()方法除了返回元素之外还可以把被访问的元素放到链表的底端,这样一来每次顶端的元素就是remove的元素。

6:装饰者模式、代理模式、责任链模式、工厂模式、适配器模式、建造者模式、单例模式、模板模式、观察者模式..

一、简单工厂模式(Factory Method)

一些容易变化的地方,考虑用一个单独的类来做这个实例化的过程,这就是工厂。

先来看看它的组成:

1) 工厂类角色:这是本模式的核心,含有一定的商业逻辑和判断逻辑。在java中它往往由 一个具体类实现。

2) 抽象产品角色:它一般是具体产品继承的父类或者实现的接口。在java中由接口或者抽 象类来实现。

3) 具体产品角色:工厂类所创建的对象就是此角色的实例。在java中由一个具体类实现

凡是出现了大量的产品需要创建,并且具有共同的接口时,可以通过工厂方法模式进行创建。

二、抽象工厂模式(Abstract Factory)

抽象工厂模式的用意为:

给客户端提供一个接口,可以创建多个产品族中的产品对象,而且使用抽象工厂模式还要满足以下条件:

1) 系统中有多个产品族,而系统一次只可能消费其中一族产品。

2) 同属于同一个产品族的产品一起使用

当然,抽象工厂的问题也是显而易见的,比如我们要加个显示器,就需要修改所有的工厂,给所有的工厂都加上制造显示器的方法。这有点违反了对修改关闭,对扩展开放这个设计原则。

 

三、单例模式(Singleton)

单例模式的实现:

1、私有的构造方法

2、私有的静态的当前类对象作为属性

3、公有的静态的方法返回当前类对象

1、饿汉式

/**

 * 单例模式------ 1、饿汉式  ==   线程安全

 * 单例:不是无例  ---在本类中的某个成员位置上创建唯一的一个对象

 * 在类被加载的时候实例化,这样多次加载会照成多次实例

 */

public class EHanShiSingleton {

//在自己内部定义自己一个实例

//注意这是 private 只供内部调用

private static EHanShiSingleton instance = new EHanShiSingleton();//直接new,立即加载

//如上面所述,将构造函数设置为私有

private EHanShiSingleton() {

}

//静态工厂方法,提供了一个供外部访问得到对象的静态方法

public static EHanShiSingleton geInstance() {

return instance;

}

}

2、懒汉式

/**

 * 单例模式-----2、懒汉式

 * 防止多线程环 境中产生多个实例

 *  使用了 同步处理,在反应速度上要比第一种慢一些。  

 */

public class LazySingleton {

    // 和饿汉模式相比,这边不需要先实例化出来,注意这里的 volatile,它是必须的

private static volatile LazySingleton instance = null;

//设置为私有的构造函数

private LazySingleton() {

}

/**

 * 静态工厂方法-----(提供一个获取单个对象的方法给用户)

 * 返回值  将对象返回出去

 */

public static LazySingleton getInstance() {//将类对自己的实例化延迟到第一次被引用的时候。

if(instance == null) {

// 加锁

synchronized (LazySingleton.class) {

//// 这一次判断也是必须的,不然会有并发问题

if(instance == null) {

instance = new LazySingleton();

}

}

}

return instance;//引用类型

}

}

 

3、嵌套类最经典,以后大家就用它吧

 public class Singleton3 {

    private Singleton3() {}

    // 主要是使用了 嵌套类可以访问外部类的静态属性和静态方法 的特性

    private static class Holder {

        private static Singleton3 instance = new Singleton3();

    }

    public static Singleton3 getInstance() {

        return Holder.instance;

    }

}

7: 关于精度损失问题:int、long 超过最大值

Double:将字段值转为指定精度的decimal数值

int max=2147483647

int min=-2147483648

long max=9223372036854775807

long min=-9223372036854775808

思路一般为是将数字转换为其他类型,然后进行计算,那么这道题的做法就可以将数字转换为字符串进行下一步的求解。

超过用BigDecimal

8: 关于注解:元注解的种类、继承java.lang.Annotation、注解的基础类型、注解的常用方法

注解主要分为三大类:普通注解、元注解、自定义注解

普通注解,常见的主要有三个 :@Override、@Deprecated、@SuppressWarnings

@Override注解我们可能见到的比较多,主要用于子类对父类方法的重写

@Deprecated主要用于注解过期、废弃的方法或者类等

@SuppressWarnings该批注的作用是给编译器一条指令,告诉它对被批注的代码元素内部的某些警告保持静默,可以忽略编译器的警告信息

元注解说白了就是用来注解其他注解的注解。

Java中总共有5种元注解:@Retention,@Documented,@Target,@Inherited,@Repeatable

@Retention

用来说明注解的存活时间,有三种取值:

RetentionPolicy.SOURCE:注解只在源码阶段保留,编译器开始编译时它将被丢弃忽视

RetentionPolicy.CLASS:注解会保留到编译期,但运行时不会把它加载到JVM中

RetentionPolicy.RUNTIME:注解可以保留到程序运行时,它会被加载到JVM中,所以程序运行过程中可以获取到它们

(2) @Documented

这个注解跟文档相关,它的作用是能够将注解中的元素包含到Javadoc中去,应该被JavaDoc所记录。

(3)@Target

用来说明注解的目标

(4)@Inherited

一个父类被该类注解修饰,那么它的子类如果没有任何注解修饰,就会继承父类的这个注解。

(5)@Repeatable

它是java1.8引入的,标记的注解可以多次应用于相同的声明或类型

Annotation类型定义了Annotation的名字、类型、成员默认值。一个Annotation类型可以说是一个特殊的java接口,它的成员变量是受限制的,而声明Annotation类型时需要使用新语法。当我们通过java反射api访问Annotation时,返回值将是一个实现了该 annotation类型接口的对象,通过访问这个对象我们能方便的访问到其Annotation成员。

9: 关于ClassLoader,类加载器,双亲委派模型

ClassLoader类的作用就是根据一个指定的类的全限定名,找到对应的Class字节码文件,然后加载它转化成一个java.lang.Class类的一个实例

类加载器的划分:

启动类加载器(Bootstrap ClassLoader): 这个类加载器负责将\lib目录下的类库加载到虚拟机内存中,用来加载java的核心库,此类加载器并不继承于java.lang.ClassLoader,不能被java程序直接调用,代码是使用C++编写的.是虚拟机自身的一部分.

扩展类加载器(Extendsion ClassLoader): 这个类加载器负责加载\lib\ext目录下的类库,用来加载java的扩展库,开发者可以直接使用这个类加载器.

应用程序类加载器(Application ClassLoader): 这个类加载器负责加载用户类路径(CLASSPATH)下的类库,一般我们编写的java类都是由这个类加载器加载,这个类加载器是CLassLoader中的getSystemClassLoader()方法的返回值,所以也称为系统类加载器.一般情况下这就是系统默认的类加载器.

  除此之外,我们还可以加入自己定义的类加载器,以满足特殊的需求,需要继承java.lang.ClassLoader类.

类加载器的双亲委派模型:

双亲委派模型是一种组织类加载器之间关系的一种规范,他的工作原理是:如果一个类加载器收到了类加载的请求,它不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,这样层层递进,最终所有的加载请求都被传到最顶层的启动类加载器中,只有当父类加载器无法完成这个加载请求(它的搜索范围内没有找到所需的类)时,才会交给子类加载器去尝试加载.

  这样的好处是:java类随着它的类加载器一起具备了带有优先级的层次关系.这是十分必要的,比如java.langObject,它存放在\jre\lib\rt.jar中,它是所有java类的父类,因此无论哪个类加载都要加载这个类,最终所有的加载请求都汇总到顶层的启动类加载器中,因此Object类会由启动类加载器来加载,所以加载的都是同一个类,如果不使用双亲委派模型,由各个类加载器自行去加载的话,系统中就会出现不止一个Object类,应用程序就会全乱了.

Class.forname()与ClassLoader.loadClass():

Class.forname():是一个静态方法,最常用的是Class.forname(String className);根据传入的类的全限定名返回一个Class对象.该方法在将Class文件加载到内存的同时,会执行类的初始化.

如: Class.forName("com.wang.HelloWorld");

ClassLoader.loadClass():这是一个实例方法,需要一个ClassLoader对象来调用该方法,该方法将Class文件加载到内存时,并不会执行类的初始化,直到这个类第一次使用时才进行初始化.该方法因为需要得到一个ClassLoader对象,所以可以根据需要指定使用哪个类加载器.

如:ClassLoader cl=.......;cl.loadClass("com.wang.HelloWorld");

J.U.C

10: 线程池参数说明,线程池的线程回收、shutdown

线程池的构造函数有7个参数,分别是corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler。下面会对这7个参数一一解释。

一、corePoolSize 线程池核心线程大小

线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会 被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。

二、maximumPoolSize 线程池最大线程数量

一个任务被提交到线程池后,首先会缓存到工作队列(后面会介绍)中,如果工作队列满了,则会创建一个新线程,然后从工作队列中的取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize来指定。

三、keepAliveTime 空闲线程存活时间

一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定

四、unit 空间线程存活时间单位

keepAliveTime的计量单位

五、workQueue 工作队列

新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列:

①ArrayBlockingQueue

基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。

②LinkedBlockingQuene

基于链表的*阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似*性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。

③SynchronousQuene

一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。

④PriorityBlockingQueue

具有优先级的*阻塞队列,优先级通过参数Comparator实现。

七、handler 拒绝策略

当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的

 

1、线程是稀缺资源,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以重复使用。

2、可以根据系统的承受能力,调整线程池中工作线程的数量,防止因为消耗过多内存导致服务器崩溃。

shutdown只是将线程池的状态设置为SHUTWDOWN状态,正在执行的任务会继续执行下去,没有被执行的则中断。

而shutdownNow则是将线程池的状态设置为STOP,正在执行的任务则被停止,没被执行任务的则返回

11: 线程池的生命周期?

面试题汇总

线程池有运行、关闭、停止、结束四种状态,结束后就会释放所有资源

2.shutdown 与 shutdowNow 分别对应

平缓关闭:已经启动的任务全部执行完毕,同时不再接受新的任务 

立即关闭:取消所有正在执行和未执行的任务

3.检测线程池是否正处于关闭中,使用isShutdown()

       描述的是非RUNNING状态,也就是SHUTDOWN/STOP/TERMINATED三种状态

4.检测线程池是否已经关闭使用isTerminated()

       描述的是关闭状态,也就是TERMINATED三种状态

5.定时或者永久等待线程池关闭结束使用awaitTermination()操作

        shutdown 与 shutdowNow不是阻塞操作,只是发起关闭任务,awaitTermination则是等待到线程isTerminated()

12: 线程池的拒绝策略有哪4种?

线程池中,有三个重要的参数,决定影响了拒绝策略:corePoolSize - 核心线程数,也即最小的线程数。workQueue - 阻塞队列 。 maximumPoolSize - 最大线程数

  当提交任务数大于 corePoolSize 的时候,会优先将任务放到 workQueue 阻塞队列中。当阻塞队列饱和后,会扩充线程池中线程数,直到达到 maximumPoolSize 最大线程数配置。此时,再多余的任务,则会触发线程池的拒绝策略了。

总结起来,也就是一句话,当提交的任务数大于(workQueue.size() + maximumPoolSize ),就会触发线程池的拒绝策略。

RejectedExecutionHandler 

jdk默认提供了四种拒绝策略:

CallerRunsPolicy - 当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大

AbortPolicy - 丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。

 DiscardPolicy - 直接丢弃,其他啥都没有

 DiscardOldestPolicy -  当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入

13: 线程池的提交,execute与submit有什么区别?在实际开发中需要注意哪些问题?

线程池中的execute方法大家都不陌生,即开启线程执行池中的任务。还有一个方法submit也可以做到,它的功能是提交指定的任务去执行并且返回Future对象,即执行的结果。下面简要介绍一下两者的三个区别:

1、接收的参数不一样

2、submit有返回值,而execute没有

3、submit方便Exception处理,如果在你的task里会抛出checked或者unchecked exception,
而你又希望外面的调用者能够感知这些exception并做出及时的处理,那么就需要用到submit,通过捕获Future.get抛出的异常。

14:并发集合类了解哪些?

我们平时写程序需要经常用到集合类,比如ArrayList、HashMap等,但是这些集合不能够实现并发运行机制,这样在服务器上运行时就会非常的消耗资源和浪费时间,并且对这些集合进行迭代的过程中不能进行操作,否则会出现错误

ConcurrentHashMap; ConcurrentSkipListMap;  ConCurrentSkipListSet; CopyOnWriteArrayList; CopyOnWriteArraySet;         ConcurrentLinkedQueue;

ConcurrentHashMap是Java中的一个线程安全且高效的HashMap实现。平时涉及高并发如果要用map结构,那第一时间想到的就是它。

ConcurrentHashMap最常用的方法也就是put方法和get方法

利用 ==CAS + synchronized== 来保证并发更新的安全 
底层使用数组+链表+红黑树来实现

15: 线程池的核心模型Worker对象的运作流程是怎样的?

面试题汇总

面试题汇总

16: threadlocal原理,数据结构

ThreadLocal 是线程的局部变量, 是每一个线程所单独持有的,其他线程不能对其进行访问。

当使用ThreadLocal维护变量的时候 为每一个使用该变量的线程提供一个独立的变量副本,即每个线程内部都会有一个该变量,这样同时多个线程访问该变量并不会彼此相互影响,因此他们使用的都是自己从内存中拷贝过来的变量的副本, 这样就不存在线程安全问题,也不会影响程序的执行性能。

但是要注意,虽然ThreadLocal能够解决上面说的问题,但是由于在每个线程中都创建了副本,所以要考虑它对资源的消耗,比如内存的占用会比不使用ThreadLocal要大。

在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。

  而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。

对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

public T get() { } // 用来获取ThreadLocal在当前线程中保存的变量副本

public void set(T value) { } //set()用来设置当前线程中变量的副本

public void remove() { } //remove()用来移除当前线程中变量的副本

protected T initialValue() { } //initialValue()是一个protected方法,一般是用来在使用时进行重写的

在这个方法内部我们看到,首先通过getMap(Thread t)方法获取一个和当前线程相关的ThreadLocalMap,然后将变量的值设置到这个ThreadLocalMap对象中,当然如果获取到的ThreadLocalMap对象为空,就通过createMap方法创建。这里的this是指向threadlocal实例对象的。 

17:CopyOnWrite集合、原理、锁机制

和单词描述的一样,他的实现就是写时复制, 在往集合中添加数据的时候,先拷贝存储的数组,然后添加元素到拷贝好的数组中,然后用现在的数组去替换成员变量的数组(就是get等读取操作读取的数组)。这个机制和读写锁是一样的,但是比读写锁有改进的地方,那就是读取的时候可以写入的 ,这样省去了读写之间的竞争,看了这个过程,你也发现了问题,同时写入的时候怎么办呢,当然果断还是加锁.

copyonwrite的机制虽然是线程安全的,但是在add操作的时候不停的拷贝是一件很费时的操作,所以使用到这个集合的时候尽量不要出现频繁的添加操作,而且在迭代的时候数据也是不及时的,数据量少还好说,数据太多的时候,实时性可能就差距很大了。在多读取,少添加的时候,他的效果还是不错的(数据量大无所谓,只要你不添加,他都是好用的)。

18:ConcurrentLinkedQueue、LinkedTransferQueue、ArrayBlockingQueue、PriorityBlockingQueue、SynchronousQueue、DelayQueue

ConcurrentLinkedQueue是一个基于链接节点的*线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部,当我们获取一个元素时,它会返回队列头部的元素

LinkedTransferQueue是一个由链表结构组成的*阻塞TransferQueue队列。相对于其他阻塞队列,LinkedTransferQueue多了tryTransfer和transfer方法。

LinkedTransferQueue采用一种预占模式。意思就是消费者线程取元素时,如果队列不为空,则直接取走数据,若队列为空,那就生成一个节点(节点元素为null)入队,然后消费者线程被等待在这个节点上,后面生产者线程入队时发现有一个元素为null的节点,生产者线程就不入队了,直接就将元素填充到该节点,并唤醒该节点等待的线程,被唤醒的消费者线程取走元素,从调用的方法返回。我们称这种节点操作为“匹配”方式。

LinkedTransferQueue是ConcurrentLinkedQueue、SynchronousQueue(公平模式下转交元素)、LinkedBlockingQueue(阻塞Queue的基本方法)的超集。而且LinkedTransferQueue更好用,因为它不仅仅综合了这几个类的功能,同时也提供了更高效的实现。

AQS 原理:

19:独占 & 共享

抽象队列同步器,是并发包当中非常重要的一个类.大部分使用到锁的地方都有继承它.它有个重要的内部类Node-队列节点结构.AQS分独占和共享模式

独占模式&ReentrantLock实现

我们先了解独占模式的代码. 获取独占锁的入口函数是acquire方法源码如下,它做如下行为:

尝试获取锁 tryAcquire

如果获取成功则结束, 失败则将当前线程封装成节点Node放到等待AQS的队列尾部 addWaiter

让Node自旋, 不断检查自己的前驱节点是否是头节点, 如果是就尝试获取锁, 不是就阻塞等待.只有头节点能获取到锁 acquireQueued

释放锁的过程,上层的方法就是release方法,做了如下动作

尝试释放锁 tryRelease

释放成功后,将后继节点唤起.

尝试获取锁.

获取不到则加入到队列中去, 并且让当前加入的线程自旋(循环不断检查自己的前驱节点是否是头节点, 是的话尝试获取锁, 否则就挂起)

当当前线程释放锁之后, 会唤起后继节点.(因为节点一旦被唤醒, for循环就会继续执行,后继节点检查自己的前驱节点就是刚刚释放锁的头节点的话, 它就会尝试去获取锁)

ReenterantLock

可重入锁分为公平锁和非公平锁.

公平锁的意思是永远是等待最久的线程(即最先入等待队列的线程)先获取到锁, 打算获取锁的新线程必须加入到等待队列.

非公平锁就是一旦有线程来获取锁,就让它尝试获取,获取不到才加入到等待队列.

20:state & CHL队列

AQS 中有两个重要的东西,一个以Node为节点实现的链表的队列(CHL队列),还有一个STATE标志,并且通过CAS来改变它的值。

CLH队列:

链表结构,在头尾结点中,需要特别指出的是头结点是一个空对象结点,无任何意义,即傀儡结点;

每一个Node结点都维护了一个指向前驱的指针和指向后驱的指针,结点与结点之间相互关联构成链表;

入队在尾,出队在头,出队后需要**该出队结点的后继结点,若后继结点为空或后继结点waitStatus>0,则从队尾向前遍历取waitStatus<0的触发阻塞唤醒;

21:锁:Synchronized、ReentrantLock、RWLock、Condition、LockSupport、StampedLock、

(1)synchronized是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活。ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。

(2)synchronized可重入,因为加锁和解锁自动进行,不必担心最后是否释放锁;ReentrantLock也可重入,但加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁。

(3)synchronized不可响应中断,一个线程获取不到锁就一直等着;ReentrantLock可以相应中断。

ReentrantLock好像比synchronized关键字没好太多,我们再去看看synchronized所没有的,一个最主要的就是ReentrantLock还可以实现公平锁机制。什么叫公平锁呢?也就是在锁上等待时间最长的线程将获得锁的使用权。通俗的理解就是谁排队时间最长谁先执行获取锁。

它可以替代传统的Object中的wait()、notify()和notifyAll()方法来实现线程间的通信,使线程间协作更加安全和高效。

Condition必须被绑定到一个独占锁上使用,在ReentrantLock中,有一个newCondition方法,该方法调用了Sync中的newCondition方法,看下Sync中newCondition的实现:

22:概念:CAS 自旋、重入、偏向

CAS:Compare and Swap,即比较再交换。

CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

CAS比较与交换的伪代码可以表示为:

do{

备份旧数据;

基于旧数据构造新数据;

}while(!CAS( 内存地址,备份的旧数据,新数据 ))

1、自旋锁:

采用让当前线程不停的在循环体内执行实现,当循环的条件被其它线程改变时才能进入临界区

优缺点分析:

由于自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。如果线程竞争不激烈,并且保持锁的时间段。适合使用自旋锁。

3、重入锁:

Java中的synchronized同步块是可重入的。这意味着如果一个java线程进入了代码中的synchronized同步块,并因此获得了该同步块使用的同步对象对应的管程上的锁,那么这个线程可以进入由同一个管程对象所同步的另

一个java代码块。

偏向锁(Biased Locking)是Java6引入的一项多线程优化。 
偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。 
如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。

23: volatile:多线程共享 & 阻止指令重排序

当一个变量定义为 volatile 之后,将具备两种特性:

1.保证此变量对所有的线程的可见性,这里的“可见性”是当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存来完成。

2.禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。

24:jvm的逃逸分析 & Tlab & 消除伪共享 & UNsafe &

逃逸分析,是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。 

TLAB的全称是Thread Local Allocation Buffer,即线程本地分配缓存区,这是一个线程专用的内存分配区域。

事务总不是完美的,TLAB也又自己的缺点。因为TLAB通常很小,所以放不下大对象。
1,TLAB空间大小是固定的,但是这时候一个大对象,我TLAB剩余的空间已经容不下它了。(比如100kb的TLAB,来了个110KB的对象)
2,TLAB空间还剩一点点没有用到,有点舍不得。(比如100kb的TLAB,装了80KB,又来了个30KB的对象)
所以JVM开发人员做了以下处理,设置了最大浪费空间。
当剩余的空间小于最大浪费空间,那该TLAB属于的线程在重新向Eden区申请一个TLAB空间。进行对象创建,还是空间不够,那你这个对象太大了,去Eden区直接创建吧!
当剩余的空间大于最大浪费空间,那这个大对象请你直接去Eden区创建,我TLAB放不下没有使用完的空间。

当然,又回造成新的病垢。
3,Eden空间够的时候,你再次申请TLAB没问题,我不够了,Heap的Eden区要开始GC,
4,TLAB允许浪费空间,导致Eden区空间不连续,积少成多。以后还要人帮忙打理。

填充缓存行消除伪共享

缓存行最常见的是64字节。
需要独占的属性的左填充7个字节,右填充7个字节。
由于JAVA7中会优化掉无用字段。
所以要采用继承的方式绕过优化。

但是在JAVA8中有个@Contended的注解,可以自动填充缓存行。
执行时,必须加上虚拟机参数-XX:-RestrictContended,@Contended注释才会生效。

java和C++语言的一个重要区别就是Java中我们无法直接操作一块内存区域,不能像C++中那样可以自己申请内存和释放内存。Java中的Unsafe类为我们提供了类似C++手动管理内存的能力。
Unsafe类,全限定名是sun.misc.Unsafe,从名字中我们可以看出来这个类对普通程序员来说是“危险”的,一般应用开发者不会用到这个类。

Unsafe类是"final"的,不允许继承。且构造函数是private的:

Unsafe无法实例化,那么怎么获取Unsafe呢?答案就是通过反射来获取Unsafe:

面试题汇总

面试题汇总

25:atomic:

atomic作用:多线程下将属性设置为atomic可以保证读取数据的一致性。因为他将保证数据只能被一个线程占用,也就是说一个线程对属性进行写操作时,会使用自旋锁锁住该属性。不允许其他的线程对其进行读取操作了。
但是它有一个很大的缺点:因为它要使用自旋锁锁住该属性,因此它会消耗更多的资源,性能会很低。要比nonatomic慢20倍。


26:CAS的缺点,自旋、ABA问题

ABA:

问题描述:线程t1将它的值从A变为B,再从B变为A。同时有线程t2要将值从A变为C。但CAS检查的时候会发现没有改变,但是实质上它已经发生了改变 。可能会造成数据的缺失。

解决方法:CAS还是类似于乐观锁,同数据乐观锁的方式给它加一个版本号或者时间戳,如AtomicStampedReference

自旋消耗资源:

问题描述:多个线程争夺同一个资源时,如果自旋一直不成功,将会一直占用CPU。

解决方法:破坏掉for死循环,当超过一定时间或者一定次数时,return退出。JDK8新增的LongAddr,和ConcurrentHashMap类似的方法。当多个线程竞争时,将粒度变小,将一个变量拆分为多个变量,达到多个线程访问多个资源的效果,最后再调用sum把它合起来。

多变量共享一致性问题:

解决方法: CAS操作是针对一个变量的,如果对多个变量操作,1. 可以加锁来解决。2 .封装成对象类解决

27:atomic 原子性、Reference、referenceArray、longadder

28:并发控制:

在数据库中,并发控制是指在多个用户/进程/线程同时对数据库进行操作时,保证事务的一致性和隔离性,同时最大程度地并发。并发控制的目的是保证一个用户的工作不会对另一个用户的工作产生不合理的影响。在某些情况下,这些措施保证了当用户和其他用户一起操作时,所得的结果和她单独操作时的结果是一样的。

解决方案:

事务

原子操作

锁-共享锁和排他锁,公平锁和非公平锁,乐观锁和悲观锁

29:barrier、countdownlatch、exchanger、future、semaphore、CyclicBarrier

CountDownLatch:CountDownLatch latch = new CountDownLatch(4);比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行;A调用await(),等其他四个任务都执行完countDown()方法后,才能继续执行A被阻塞的逻辑;

CyclicBarrier:CyclicBarrier barrier  = new CyclicBarrier(4);有4干个线程都要进行写数据操作,并且只有所有4个线程都完成写数据操作之后,这些线程才能继续做后面的事情,此时就可以利用CyclicBarrier了;只有await方法,4个线程都执行完await方法后,4个线程才能继续自己后面的任务;

Semaphore:信号量:Semaphore semaphore = new Semaphore(5),默认非公平模式;可以控同时访问某个资源的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。

总结:

1)CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:

CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;

    而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;

    另外,CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。

2)Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限。

jvm虚拟机:

30:虚拟机内存模型

面试题汇总

面试题汇总

31:新生代(Eden S0 S1)、老年代 、MetaSpace (比例)

  1)年轻代(Young Gen):年轻代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。年轻代分成1个Eden Space和2个Suvivor Space(命名为A和B)

 2)年老代(Tenured Gen):年老代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁(譬如可能几个小时一次)。年老代主要采用压缩的方式来避免内存碎片(将存活对象移动到内存片的一边,也就是内存整理)。当然,有些垃圾回收器(譬如CMS垃圾回收器)出于效率的原因,可能会不进行压缩。

      3)持久代(Perm Gen):持久代主要存放类定义、字节码和常量等很少会变更的信息。

4.有关年轻代的JVM参数

1)-XX:NewSize和-XX:MaxNewSize

      用于设置年轻代的大小,建议设为整个堆大小的1/3或者1/4,两个值设为一样大。

2)-XX:SurvivorRatio

      用于设置Eden和其中一个Survivor的比值,这个值也比较重要。

3)-XX:+PrintTenuringDistribution

      这个参数用于显示每次Minor GC时Survivor区中各个年龄段的对象的大小。

4).-XX:InitialTenuringThreshol和-XX:MaxTenuringThreshold

      用于设置晋升到老年代的对象年龄的最小值和最大值,每个对象在坚持过一次Minor GC之后,年龄就加1

32:垃圾回收算法(引用计数、标记压缩、清除、复制算法、分区)、垃圾收集器

如果一个对象没有任何引用与之关联,则说明该对象基本不太可能在其他地方被使用到,那么这个对象就成为可被回收的对象了。这种方式成为引用计数法

1.Mark-Sweep(标记-清除)算法

这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间

2.Copying(复制)算法

  为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题

3.Mark-Compact(标记-整理)算法(压缩法)

为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存

4.Generational Collection(分代收集)算法

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

目前大部分垃圾收集器对于新生代都采取复制算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。

而由于老年代的特点是每次回收都只回收少量对象,一般使用的是标记-整理算法(压缩法)

垃圾收集算法是 内存回收的理论基础,而垃圾收集器就是内存回收的具体实现。下面介绍一下HotSpot(JDK 7)虚拟机提供的几种垃圾收集器,用户可以根据自己的需求组合出各个年代使用的收集器。

1.Serial/Serial Old收集器 是最基本最古老的收集器,它是一个单线程收集器,并且在它进行垃圾收集时,必须暂停所有用户线程。Serial收集器是针对新生代的收集器,采用的是Copying算法,Serial Old收集器是针对老年代的收集器,采用的是Mark-Compact算法。它的优点是实现简单高效,但是缺点是会给用户带来停顿。

2.ParNew收集器 是Serial收集器的多线程版本,使用多个线程进行垃圾收集。

3.Parallel Scavenge收集器 是一个新生代的多线程收集器(并行收集器),它在回收期间不需要暂停其他用户线程,其采用的是Copying算法,该收集器与前两个收集器有所不同,它主要是为了达到一个可控的吞吐量。

4.Parallel Old收集器 是Parallel Scavenge收集器的老年代版本(并行收集器),使用多线程和Mark-Compact算法。

5.CMS(Current Mark Sweep)收集器 是一种以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,采用的是Mark-Sweep算法。

6.G1收集器 是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多CPU、多核环境。因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型。

33:GC停顿、吞吐量,进入老年代阈值、大对象回收问题等

GC任务是识别和回收垃圾对象,进行内存清理 ,为了让GC可以高效的执行,在进行GC时,系统会进入一个停顿的状态,停顿目的 ,终止所有应用线程 ,只有这样系统才不会有新的垃圾产生,停顿保证了系统状态在某一瞬间的一致性,有益于更好的标记垃圾对象 ,因此,在GC时,都会产生应用程序的停顿,减少GC ,可以减少程序的停顿,提高系统的性能

对象诞生即新生代->eden,在进行minor gc过程中,如果依旧存活,移动到from,变成Survivor,进行标记。当一个对象存活默认超过15次都没有被回收掉,就会进入老年代。

34:jvm性能调优、参数配置

对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数。

1.Full GC会对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比较慢,因此应该尽可能减少Full GC的次数。

2.导致Full GC的原因

1)年老代(Tenured)被写满,调优时尽量让对象在新生代GC时被回收、让对象在新生代多存活一段时间和不要创建过大的对象及数组避免直接在旧生代创建对象 。

2)持久代Pemanet Generation空间不足

增大Perm Gen空间,避免太多静态对象 , 控制好新生代和旧生代的比例

3)System.gc()被显示调用

垃圾回收不要手动触发,尽量依靠JVM自身的机制

在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节

  1. 监控GC的状态
  2. 生成堆的dump文件

通过JMX的MBean生成当前的Heap信息,大小为一个3G(整个堆的大小)的hprof文件,如果没有启动JMX可以通过Java的jmap命令来生成该文件。

  1. 分析dump文件

文件太大,建议使用Eclipse专门的静态内存分析工具Mat打开分析。

  1. 分析结果,判断是否需要优化
  2. 调整GC类型和内存分配

如果内存分配过大或过小,或者采用的GC收集器比较慢,则应该优先调整这些参数,并且先找1台或几台机器进行beta,然后比较优化过的机器和没有优化的机器的性能对比,并有针对性的做出最后选择。

  1. 不断的分析和调整

通过不断的试验和试错,分析并找到最合适的参数,如果找到了最合适的参数,则将这些参数应用到所有服务器。

JVM调优参数参考

1.针对JVM堆的设置,一般可以通过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,通常把最大、最小设置为相同的值;

2.年轻代和年老代将根据默认的比例(1:2)分配堆内存, 可以通过调整二者之间的比率NewRadio来调整二者之间的大小,也可以针对回收代。

比如年轻代,通过 -XX:newSize -XX:MaxNewSize来设置其绝对大小。同样,为了防止年轻代的堆收缩,我们通常会把-XX:newSize -XX:MaxNewSize设置为同样大小。

3.年轻代和年老代设置多大才算合理

1)更大的年轻代必然导致更小的年老代,大的年轻代会延长普通GC的周期,但会增加每次GC的时间;小的年老代会导致更频繁的Full GC

2)更小的年轻代必然导致更大年老代,小的年轻代会导致普通GC很频繁,但每次的GC时间会更短;大的年老代会减少Full GC的频率

如何选择应该依赖应用程序对象生命周期的分布情况: 如果应用存在大量的临时对象,应该选择更大的年轻代;如果存在相对较多的持久对象,年老代应该适当增大。但很多应用都没有这样明显的特性。

在抉择时应该根 据以下两点:

(1)本着Full GC尽量少的原则,让年老代尽量缓存常用对象,JVM的默认比例1:2也是这个道理 。

(2)通过观察应用一段时间,看其他在峰值时年老代会占多少内存,在不影响Full GC的前提下,根据实际情况加大年轻代,比如可以把比例控制在1:1。但应该给年老代至少预留1/3的增长空间。

4.在配置较好的机器上(比如多核、大内存),可以为年老代选择并行收集算法: -XX:+UseParallelOldGC 。

5.线程堆栈的设置:每个线程默认会开启1M的堆栈,用于存放栈帧、调用参数、局部变量等,对大多数应用而言这个默认值太了,一般256K就足用。

理论上,在内存不变的情况下,减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统。

JVM调优总结 -Xms -Xmx -Xmn -Xss

1、常见参数配置

-XX:+PrintGC 每次触发GC的时候打印相关日志

-XX:+UseSerialGC 串行回收

-XX:+PrintGCDetails 更详细的GC日志

-Xms 堆初始值

-Xmx 堆最大可用值

-Xmn 新生代堆最大可用值

-XX:SurvivorRatio 用来设置新生代中eden空间和from/to空间的比例.

-XX:NewRatio 配置新生代与老年代占比 1:2

含以-XX:SurvivorRatio=eden/from=den/to

总结:在实际工作中,我们可以直接将初始的堆大小与最大堆大小相等,
这样的好处是可以减少程序运行时垃圾回收次数,从而提高效率。

-XX:SurvivorRatio 用来设置新生代中eden空间和from/to空间的比例.

1、Java堆溢出

错误原因: java.lang.OutOfMemoryError: Java heap space 堆内存溢出

解决办法:设置堆内存大小 // -Xms1m -Xmx10m -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError

2、虚拟机栈溢出

错误原因: java.lang.*Error 栈内存溢出

栈溢出 产生于递归调用,循环遍历是不会的,但是循环方法里面产生递归调用, 也会发生栈溢出。

解决办法:设置线程最大调用深度

-Xss5m 设置最大调用深度

3、内存溢出与内存泄漏区别

Java内存泄漏就是没有及时清理内存垃圾,导致系统无法再给你提供内存资源(内存资源耗尽);

而Java内存溢出就是你要求分配的内存超出了系统能给你的,系统不能满足需求,于是产生溢出。

内存溢出,这个好理解,说明存储空间不够大。就像倒水倒多了,从杯子上面溢出了来了一样。

内存泄漏,原理是,使用过的内存空间没有被及时释放,长时间占用内存,最终导致内存空间不足,而出现内存溢出。

35:常用命令:jstat、jmap、jstack等

jstat -gcutil pid 5s

特别的,一个极强的监视内存的工具,可以用来监视VM内存内的各种堆和非堆的大小及其内存使用量,以及加载类的数量。

每隔5s监控一次内存回收情况

E 代表 Eden 区使用率;
O(Old)代表老年代使用率    ;
P(Permanent)代表永久代使用率;

jstack -F pid 检查是否有死锁

    jstack主要用来查看某个Java进程内的线程堆栈信息

 jstack可以定位到线程堆栈,根据堆栈信息我们可以定位到具体代码,所以它在JVM性能调优中使用得非常多。

 jmap用来查看堆内存使用状况,一般结合jhat使用。

打印进程的类加载器和类加载器加载的持久代对象信息,输出:类加载器名称、对象是否存活(不可靠)、对象地址、父类加载器、已加载的类大小等信息

监视进程运行中的jvm物理内存的占用情况,该进程内存内,所有对象的情况,例如产生了哪些对象,对象数量;

系统崩溃了?jmap 可以从core文件或进程中获得内存的具体匹配情况,包括Heap size, Perm size等等

jinfo

观察进程运行环境参数,包括Java System属性和JVM命令行参数

系统崩溃了?jinfo可以从core文件里面知道崩溃的Java应用程序的配置信息。

jps

查看所有的jvm进程,包括进程ID,进程启动的路径等等。

我自己也用PS,即:ps -ef | grep java

36:内存溢出分析:堆内、堆外 (含义、如何设置)

1、Java堆溢出

Java堆用于存储对象的实例,只要不断地创建对象,并保证GC Roots到对象之间有可达路径

来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常

java.lang.OutOfMemoryError: Java heap space 的信息,说明在堆内存空间产生内存溢出的异常。

2、虚拟机栈/本地方法栈溢出

(1)*Error:当线程请求的栈的深度大于虚拟机所允许的最大深度,则抛出*Error,简单理解就是虚拟机栈中的栈帧数量过多(一个线程嵌套调用的方法数量过多)时,就会抛出*Error异常。

-XX:+HeapDumpOnOutofMemoryError dump的时候转储堆快照

-Xms 堆最小容量(heap min size)

-Xmx 堆最大容量(heap max size)

-Xss 栈容量(stack size)

-XX:PermSize=size 永生代最小容量

-XX:MaxPermSize=size 永生代最大容量

37:CPU飙升:死锁、线程阻塞

首先,需要知道哪个进程占用CPU比较高,

其次,需要知道占用CPU高的那个进程中的哪些线程占用CPU比较高,

然后,需要知道这些线程的stack trace。

找出了CPU占用高的线程号和其stack trace并再结合应用日志基本上就可以找到问题根源。接下来,将介绍相应的工具来找到这些问题的答案。

首先,通过top和pgrep来查看系统中Java进程的CPU占用情况。命令如下:

top -p `pgrep -d , java`

其次,通过top来查看进程中CPU占用最高的那些线程,命令为:

top -Hp 12345

然后,通过jstack导出Java应用中线程的stack trace,命令如下:

jstack 12345

需要把第2步中的线程号和jstack输出结果中的线程号关联起来。因为top中显示的线程号是10进制,jstack的输出结果中的线程号是16进制,所以只需要把top中看到线程号转换成16进制,然后到jstack的输出结果中即可找到对应线程的stacktrace了。

38:关于GC: minor major full

堆内存划分为 Eden、Survivor 和 Tenured/Old 空间

从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC

1、当 JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。所以分配率越高,越频繁执行 Minor GC。

2、内存池被填满的时候,其中的内容全部会被复制,指针会从0开始跟踪空闲内存。

3、执行 Minor GC 操作时,不会影响到永久代。

每次 Minor GC 会清理年轻代的内存。

Major GC 是清理老年代。

Full GC 是清理整个堆空间—包括年轻代和老年代

39:stw,安全点等

Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。

GC时的Stop the World(STW)是大家最大的敌人。但可能很多人还不清楚,除了GC,JVM下还会发生停顿现象。

JVM里有一条特殊的线程--VM Threads,专门用来执行一些特殊的VM Operation,比如分派GC,thread dump等,这些任务,都需要整个Heap,以及所有线程的状态是静止的,一致的才能进行。所以JVM引入了安全点(Safe Point)的概念,想办法在需要进行VM Operation时,通知所有的线程进入一个静止的安全点。

除了GC,其他触发安全点的VM Operation包括:

1. JIT相关,比如Code deoptimization, Flushing code cache ;

2. Class redefinition (e.g. javaagent,AOP代码植入的产生的instrumentation) ;

3. Biased lock revocation 取消偏向锁 ;

4. Various debug operation (e.g. thread dump or deadlock check);

 

- 数据结构&算法

40:- 数组、链表、树、队列..

41:- 关于时间复杂度,时间换空间转换案例

  • 时间复杂度:就是说执行算法需要消耗的时间长短,越快越好。
  • 空间复杂度:就是说执行当前算法需要消耗的存储空间大小,也是越少越好

42:- 关于排序、冒泡、快排、递归、二分搜索、位运算

快排:

public class jj {

public static void main(String[] args) {

int []a={1,3,43,5,33,53,3,7,-9,23,90};

int low=0;

int high=a.length-1;

quicksort(a,low,high);

for(int x=0;x<=high;x++)

System.out.print(a[x]+",");

}

public static void quicksort(int a[],int low,int high)

{

int l=low;

int r=high;

int temp=a[l];

if(l<r)

{

while(l<r)

{

while(l<r&&a[r]>temp)

r--;

if(l<r)

a[l]=a[r];

while(l<r&&a[l]<=temp)

l++;

if(l<r)

a[r]=a[l];

}

a[l]=temp;

quicksort(a,low,l-1);

quicksort(a,l+1,high);

}

}

}

Spring

43:Spring生命周期,流程梳理

面试题汇总

Spring启动,查找并加载需要被Spring管理的bean,进行Bean的实例化

  1. Bean实例化后对将Bean的引入和值注入到Bean的属性中
  2. 如果Bean实现了BeanNameAware接口的话,Spring将Bean的Id传递给setBeanName()方法
  3. 如果Bean实现了BeanFactoryAware接口的话,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入
  4. 如果Bean实现了ApplicationContextAware接口的话,Spring将调用Bean的setApplicationContext()方法,将bean所在应用上下文引用传入进来。
  5. 如果Bean实现了BeanPostProcessor接口,Spring就将调用他们的postProcessBeforeInitialization()方法。
  6. 如果Bean 实现了InitializingBean接口,Spring将调用他们的afterPropertiesSet()方法。类似的,如果bean使用init-method声明了初始化方法,该方法也会被调用
  7. 如果Bean 实现了BeanPostProcessor接口,Spring就将调用他们的postProcessAfterInitialization()方法。
  8. 此时,Bean已经准备就绪,可以被应用程序使用了。他们将一直驻留在应用上下文中,直到应用上下文被销毁。
  9. 如果bean实现了DisposableBean接口,Spring将调用它的destory()接口方法,同样,如果bean使用了destory-method 声明销毁方法,该方法也会被调用。

44:Spring扩展点作用

1   beanPostProcessor   该接口作用是:如果我们需要在Spring容器完成Bean的实例化,配置和其他的初始化后添加一些自己的逻辑处理,我们就可以定义一个或者多个BeanPostProcessor接口的实现。

2  HandlerInterceptorAdapter相当于一个Filter拦截器,但是这个颗粒度更细,能使用Spring的@Autowired注入。重写public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)方法,可在执行请求之前做判断,比如判断登录、权限信息等

3  authorizationInterceptor类似于配置Mapping的路径,可以控制上述拦截的路径:比如说排除登录请求验证~,其他路径都需要验证

45:Spring IOC AOP 基本原理

IoC(Inversion of Control)是指容器控制程序对象之间的关系,而不是传统实现中,由程序代码直接操控。控制权由应用代码中转到了外部容器,控制权的转移是所谓反转。 对于Spring而言,就是由Spring来控制对象的生命周期和对象之间的关系;IoC还有另外一个名字——“依赖注入(Dependency Injection)”。从名字上理解,所谓依赖注入,即组件之间的依赖关系由容器在运行期决定,即由容器动态地将某种依赖关系注入到组件之中。  

(2). 在Spring的工作方式中,所有的类都会在spring容器中登记,告诉spring这是个什么东西,你需要什么东西,然后spring会在系统运行到适当的时候,把你要的东西主动给你,同时也把你交给其他需要你的东西。所有的类的创建、销毁都由 spring来控制,也就是说控制对象生存周期的不再是引用它的对象,而是spring。对于某个具体的对象而言,以前是它控制其他对象,现在是所有对象都被spring控制,所以这叫控制反转。

(3). 在系统运行中,动态的向某个对象提供它所需要的其他对象。  

(4). 依赖注入的思想是通过反射机制实现的,在实例化一个类时,它通过反射调用类中set方法将事先保存在HashMap中的类属性注入到类中。 总而言之,在传统的对象创建方式中,通常由调用者来创建被调用者的实例,而在Spring中创建被调用者的工作由Spring来完成,然后注入调用者,即所谓的依赖注入or控制反转。 注入方式有两种:依赖注入和设置注入; IoC的优点:降低了组件之间的耦合,降低了业务对象之间替换的复杂性,使之能够灵活的管理对象。

(1). AOP面向方面编程基于IoC,是对OOP的有益补充;

(2). AOP利用一种称为“横切”的技术,剖解开封装的对象内部,并将那些影响了 多个类的公共行为封装到一个可重用模块,并将其名为“Aspect”,即方面。所谓“方面”,简单地说,就是将那些与业务无关,却为业务模块所共同调用的 逻辑或责任封装起来,比如日志记录,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。

(3). AOP代表的是一个横向的关 系,将“对象”比作一个空心的圆柱体,其中封装的是对象的属性和行为;则面向方面编程的方法,就是将这个圆柱体以切面形式剖开,选择性的提供业务逻辑。而 剖开的切面,也就是所谓的“方面”了。然后它又以巧夺天功的妙手将这些剖开的切面复原,不留痕迹,但完成了效果。

(4). 实现AOP的技术,主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码。

(5). Spring实现AOP:JDK动态代理和CGLIB代理 JDK动态代理:其代理对象必须是某个接口的实现,它是通过在运行期间创建一个接口的实现类来完成对目标对象的代理;其核心的两个类是InvocationHandler和Proxy。 CGLIB代理:实现原理类似于JDK动态代理,只是它在运行期间生成的代理对象是针对目标类扩展的子类。CGLIB是高效的代码生成包,底层是依靠ASM(开源的java字节码编辑类库)操作字节码实现的,性能比JDK强;需要引入包asm.jar和cglib.jar。     使用AspectJ注入式切面和@AspectJ注解驱动的切面实际上底层也是通过动态代理实现的。

(6). AOP使用场景:                     

Authentication 权限检查        

Caching 缓存        

Context passing 内容传递        

Error handling 错误处理        

Lazy loading 延迟加载        

Debugging  调试      

logging, tracing, profiling and monitoring 日志记录,跟踪,优化,校准        

Performance optimization 性能优化,效率检查        

Persistence  持久化        

Resource pooling 资源池        

Synchronization 同步        

Transactions 事务管理    

动态代理

46:BeanPostProcessor 作用?

BeanPostProcessor也称为Bean后置处理器,它是Spring中定义的接口,在Spring容器的创建过程中(具体为Bean初始化前后)会回调BeanPostProcessor中定义的两个方法

该接口我们也叫后置处理器,作用是在Bean对象在实例化和依赖注入完毕后,在显示调用初始化方法的前后添加我们自己的逻辑。注意是Bean实例化完毕后及依赖注入完成后触发的。

面试题汇总

47:ApplicationContextAware 的作用和使用?

ApplicationContextAware 通过它Spring容器会自动把上下文环境对象调用ApplicationContextAware接口中的setApplicationContext方法。

我们在ApplicationContextAware的实现类中,就可以通过这个上下文环境对象得到Spring容器中的Bean。

  看到—Aware就知道是干什么的了,就是属性注入的,但是这个ApplicationContextAware的不同地方在于,实现了这个接口的bean,当spring容器初始化的时候,会自动的将ApplicationContext注入进来

当一个类实现了这个接口(ApplicationContextAware)之后,这个类就可以方便获得ApplicationContext中的所有bean。换句话说,就是这个类可以直接获取spring配置文件中,所有有引用到的bean对象。

48:BeanNameAware与BeanFactoryAware的先后顺序?

实现BeanNameAware接口需要实现setBeanName()方法,这个方法只是简单的返回我们当前的beanName

实现 BeanFactoryAware 接口的 bean 可以直接访问 Spring 容器,被容器创建以后,它会拥有一个指向 Spring 容器的引用。

BeanFactoryAware 接口只有一个方法void setBeanFactory(BeanFactorybeanFactory)。配置和一般的bean一样。

如果某个 bean 需要访问配置文件中本身的 id 属性,则可以使用 BeanNameAware 接口,该接口提供了回调本身的能力。实现

该接口的 bean,能访问到本身的 id 属性。该接口提供一个方法:void setBeanName(String name)。

49:InitializingBean 和 BeanPostProcessor 的after方法先后顺序?

50:ApplicationListener监控的Application事件有哪些?

51:Spring模块装配的概念,比如@EnableScheduling @EnableRetry @EnableAsync,@Import注解的作用?

52:ImportBeanDefinitionRegistrar 扩展点用于做什么事情?

53:ClassPathBeanDefinitionScanner 的作用?

54:NamespaceHandlerSupport 命名空间扩展点的作用?

55:如何实现动态注入一个Bean?

思路: 基于ApplicationContextAware来获取ApplicationContext的引用,然后基于ApplicationContext进行对象的动态获取。

@Component

public class SpringContextUtil implements ApplicationContextAware {

// Spring应用上下文环境

private static ApplicationContext applicationContext;

public static Object getBean(String name) throws BeansException {

return applicationContext.getBean(name);

}

}

之后就可以直接在代码中动态获取所需要的Bean实例了:

1

BeanType bean = SpringContextUtil.getBean("beanName")

56:如何把自定义注解所在的Class 初始化注入到Spring容器?

57:BeanDefinition指的是什么,与BeanDefinitionHolder的区别,Spring如何存储BeanDefinition实例?

58:ASM 与 CGlib

59:Spring的条件装配,自动装配

60:spring cloud和spring boot的区别

spring cloud 是一系列框架的有序集合。它利用 spring boot 的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用 spring boot 的开发风格做到一键启动和部署。

Spring Cloud是一种应用程序开发风格...

Spring Cloud风格所体现的大部分特性都已经被Spring Boot涵盖了,而且Spring Cloud正是构建于Spring Boot之上的。

Spring boot可以离开Spring Cloud独立使用开发项目,但是Spring Cloud离不开Spring boot,属于依赖的关系。

spring -> spring boot > spring cloud 这样的关系。

看名字就知道是Spring的引导,就是用于启动Spring的,使得Spring的学习和使用变得快速无痛。不仅适合替换原有的工程结构,更适合微服务开发。

Spring Cloud基于Spring Boot,为微服务体系开发中的架构问题,提供了一整套的解决方案——维度(springcloud)服务开发:springboot spring springmvc,服务配置与管理:Netfix公司的Archaiusm ,阿里的Diamond,服务注册与发现:Eureka,Zookeeper,服务调用:Rest RPC gRpc,服务熔断器:Hystrix,服务负载均衡:Ribbon Nginx,服务接口调用:Fegin,消息队列:Kafka Rabbitmq activemq,服务配置中心管理:SpringCloudConfig,服务路由(API网关)Zuul,事件消息总线:SpringCloud Bus等。

学过Spring的都知道,Spring开发有非常头疼的三点: 以启动一个带Hibernate的Spring MVC为例。

1.依赖太多了,而且要注意版本兼容。这个应用,要添加10-20个依赖,Spring相关的包10多个,然后是Hibernate包,Spring与Hibernate整合包,日志包,json包一堆,而且要注意版本兼容性。

2. 配置太多了,要配置注解驱动,要配置数据库连接池,要配置Hibernate,要配置事务管理器,要配置Spring MVC的资源映射,要在web.xml中配置启动Spring和Spring MVC。。等

3.部署和运行麻烦。要部署到tomcat里面。不能直接用java命令运行。

太多重复和大家都一样的配置了。

Spring Boot的哲学就是约定大于配置。既然很多东西都是一样的,为什么还要去配置。

1. 通过starter和依赖管理解决依赖问题。

2. 通过自动配置,解决配置复杂问题。

3. 通过内嵌web容器,由应用启动tomcat,而不是tomcat启动应用,来解决部署运行问题。

Spring Cloud体系就比较复杂了。基本可以理解为通过Spring Boot的三大魔法,将各种组件整合在一起,非常简单易用。

61:spring cloud 和dubbo区别?

  1. 服务调用方式, dubbo是RPC, springcloud Rest Api。

1).RPC主要的缺陷是服务提供方和调用方式之间的依赖太强,需要对每一个微服务进行接口的定义,并通过持续继承发布,严格版本控制才不会出现冲突。
2).REST是轻量级的接口,服务的提供和调用不存在代码之间的耦合,只需要一个约定进行规范。

  1. 注册中心,dubbo 是zookeeper ,springcloud是eureka,也可以是zookeeper
  2. 服务网关,dubbo本身没有实现,只能通过其他第三方技术整合,springcloud有Zuul路由网关,作为路由服务器,进行消费者的请求分发,springcloud支持断路器,与git完美集成配置文件支持版本控制,事物总线实现配置文件的更新与服务自动装配等等一系列的微服务架构要素。

1).Eureka取CAP的AP,注重可用性,Zookeeper取CAP的CP注重一致性。

2).Zookeeper在选举期间注册服务瘫痪,虽然服务最终会恢复,但选举期间不可用。

3).eureka的自我保护机制,会导致一个结果就是不会再从注册列表移除因长时间没收到心跳而过期的服务。依然能接受新服务的注册和查询请求,但不会被同步到其他节点。不会服务瘫痪。

4).Zookeeper有Leader和Follower角色,Eureka各个节点平等。

5).Zookeeper采用过半数存活原则,Eureka采用自我保护机制解决分区问题。

6).eureka本质是一个工程,Zookeeper只是一个进程。

62: Mybatis内部实现流程?

SqlSessionFactory:每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为中心的。SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或通过Java的方式构建出 SqlSessionFactory 的实例。SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,建议使用单例模式或者静态单例模式。一个SqlSessionFactory对应配置文件中的一个环境(environment),如果你要使用多个数据库就配置多个环境分别对应一个SqlSessionFactory。

    SqlSession:SqlSession是一个接口,它有2个实现类,分别是DefaultSqlSession(默认使用)以及SqlSessionManager。SqlSession通过内部存放的执行器(Executor)来对数据进行CRUD。此外SqlSession不是线程安全的,因为每一次操作完数据库后都要调用close对其进行关闭,官方建议通过try-finally来保证总是关闭SqlSession。

Executor:Executor(执行器)接口有两个实现类,其中BaseExecutor有三个继承类分别是BatchExecutor(重用语句并执行批量更新),ReuseExecutor(重用预处理语句prepared statement,跟Simple的唯一区别就是内部缓存statement),SimpleExecutor(默认,每次都会创建新的statement)。以上三个就是主要的Executor。通过下图可以看到Mybatis在Executor的设计上面使用了装饰器模式,我们可以用CachingExecutor来装饰前面的三个执行器目的就是用来实现缓存。

 MappedStatement:MappedStatement就是用来存放我们SQL映射文件中的信息包括sql语句,输入参数,输出参数等等。一个SQL节点对应一个MappedStatement对象。

面试题汇总

1.第一步通过SqlSessionFactoryBuilder创建SqlSessionFactory: 首先在SqlSessionFactoryBuilder的build()...

2.第二步通过SqlSessionFactory创建SqlSession:

3.第三步通过SqlSession拿到Mapper对象的代理:

4.第四步通过MapperProxy调用Maper中相应的方法

RPC通信框架

  1. Dubbo

Dubbo是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。

    作为一个轻量级RPC框架,Dubbo的设计架构简洁清晰,主要组件包括Provider(服务提供者),Consumer(服务消费者),Registry(注册中心)三部分组成

  1. Dubbo的Spi机制?

spi机制的思想提供一种更加灵活的,可插拔式的机制

SPI,Service Provider Interface,主要是被框架的开发人员使用,比如java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,mysql和postgresql都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。

当服务的提供者提供了一种接口的实现之后,需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。当其他的程序需要这个服务的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的META-INF/services/中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。

  1. Dubbo的核心模型 invoker、invocation、filter

面试题汇总

  1. Invoker是Dubbo核心模型

Invoker是Dubbo中的实体域,也就是真实存在的。其他模型都向它靠拢或转换成它,它也就代表一个可执行体,可向它发起invoke调用。在服务提供方,Invoker用于调用服务提供类。在服务消费方,Invoker用于执行远程调用。

在服务提供方中的Invoker是由ProxyFactory创建而来的,Dubbo默认的ProxyFactory实现类为JavassistProxyFactory。

在服务消费方,Invoker用于执行远程调用。Invoker是由 Protocol实现类构建而来。Protocol实现类有很多但是最常用的两个,分别是RegistryProtocol和DubboProtocol。

3:Invocation
是会话域,它持有调用过程中的变量,比如方法名,参数等。
4:Protocol
是服务域,它是 Invoker 暴露和引用的主功能入口,它负责 Invoker 的生命周期管理。

filter在dubbo中的应用非常广泛,它可以对服务端、消费端的调用过程进行拦截,从而对dubbo进行功能上的扩展,我们所熟知的RpcContext就用到了filter。

  1. Dubbo的隐式传递?

RpcContext.getContext().setAttachment("index", "1"); // 隐式传参,后面的远程调用都会隐式将这些参数发送到服务器端,类似cookie,用于框架集成,不建议常规业务使用 xxxService.xxx(); // 远程调用

public class XxxServiceImpl implements XxxService { public void xxx() { // 获取客户端隐式传入的参数,用于框架集成,不建议常规业务使用 String index = RpcContext.getContext().getAttachment("index"); } }

  1. Dubbo的泛化调用?
  2. Dubbo的export与importer时机?
  3. Dubbo的服务调用过程?面试题汇总
  4. 0. 服务容器负责启动,加载,运行服务提供者。

1. 服务提供者在启动时,向注册中心注册自己提供的服务。

2. 服务消费者在启动时,向注册中心订阅自己所需的服务。

3. 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。

4. 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。

5. 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

  1. Dubbo的负载均衡策略?

Dubbo 框架 的负载均衡策略 有以下几种:

1、Random 随机策略:该策略比较均匀,可以动态的调节 权重;

2、RoundRobin 轮询策略:可以按照权重 设置轮询的的比率;

3、LeastActive 最小活跃数 策略:该策略是按照服务提供者的并发数目,该数目越小那么落在该 服务提供者的身上越大的概率;

4、ConsistentHash 一致性策略:hash一致性算法 ,请求分发到同一台服务上去,当该服务宕机 ,通过虚拟节点 把该机器的请求 均匀的分发到其他服务上去;

  1. Dubbo的集群容错?

Failover Cluster(默认)

    失败自动切换,当出现失败,重试其它服务器 [1]。通常用于读操作,但重试会带来更长延迟。可通过 retries="2" 来设置重试次数(不含第一次)。

Failfast Cluster

    快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。

Failsafe Cluster

    失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。

Failback Cluster

    失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。

Forking Cluster

    并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks="2" 来设置最大并行数。

 

 

网络通信

  1. IO / NIO

NIO主要用到的是块,所以NIO的效率要比IO高很多。在Java API中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO。

  1. IO NIO区别?
  2. 面试题汇总
  3. 1、面向流与面向缓冲

     Java IO和NIO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

2、阻塞与非阻塞IO

     Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

3、选择器(Selectors)

     Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。

  1. 多路复用的概念,Selector

Java NIO的selectors允许一条线程去监控多个channels的输入,你可以向一个selector上注册多个channel,然后调用selector的select()方法判断是否有新的连接进来或者已经在selector上注册时channel是否有数据进入。selector的机制让一个线程管理多个channel变得简单。

  1. Channel的概念、Bytebuf的概念,flip、position...
  2.  
  3. 面试题汇总
  4.  
  5. NIO允许你用一个单独的线程或几个线程管理很多个channels(网络的或者文件的),代价是程序的处理和处理IO相比更加复杂

如果你需要同时管理成千上万的连接,但是每个连接只发送少量数据,例如一个聊天服务器,用NIO实现会更好一些,相似的,如果你需要保持很多个到其他电脑的连接,例如P2P网络,用一个单独的线程来管理所有出口连接是比较合适的

  1. FileChannel 如何使用?
  2. RAF使用,seek、skip方法

 

- Netty

  1. - 关于Netty的Reactor实现?

Reactor是一种广泛应用在服务器端开发的设计模式,是一种基于事件驱动的设计模式,所谓的事件驱动通俗点说就是回调的方式。我们知道,对于应用服务器,一个主要规律就是,CPU的处理速度是要远远快于IO速度的,如果CPU为了IO操作(例如从Socket读取一段数据)而阻塞显然是不划算的。好一点的方法是分为多进程或者线程去进行处理,但是这样会带来一些进程切换的开销,试想一个进程一个数据读了500ms,期间进程切换到它3次,但是CPU却什么都不能干,就这么切换走了,是不是也不划算,这时先驱们找到了事件驱动,或者叫回调的方式,来完成这件事情。这种方式就是,应用业务向一个中间人注册一个回调(event handler),当IO就绪后,就这个中间人产生一个事件,并通知此handler进行处理。

  1. - Netty的ByteBuf有哪些?
  2. - 内存与非内存Bytebuffer的区别与使用场景?
  3. - 池化与非池化buffer的区别与使用场景?
  4. - 关于Netty的请求Buffer和响应Buffer?
  5. - Netty的ChannelPipeline设计模式?
  6. 面试题汇总
  7. 一个 Channel 包含了一个 ChannelPipeline, 而 ChannelPipeline 中又维护了一个由 ChannelHandlerContext 组成的双向链表. 这个链表的头是 HeadContext, 链表的尾是 TailContext, 并且每个 ChannelHandlerContext 中又关联着一个 ChannelHandler。

数据从head节点流入,先拆包,然后解码成业务对象,最后经过业务Handler处理,调用write,将结果对象写出去。而写的过程先通过tail节点,然后通过encoder节点将对象编码成ByteBuf,最后将该ByteBuf对象传递到head节点,调用底层的Unsafe写到jdk底层管道。

  1. - Netty的核心option参数配置?

1、通用参数
(1)CONNECT_TIMEOUT_MILLIS : Netty参数,连接超时毫秒数,默认值30000毫秒即30秒。

(2)MAX_MESSAGES_PER_READ Netty参数,一次Loop读取的最大消息数,对于ServerChannel或者NioByteChannel,默认值为16,其他Channel默认值为1。默认值这样设置,是因为:ServerChannel需要接受足够多的连接,保证大吞吐量,NioByteChannel可以减少不必要的系统调用select。

(3)WRITE_SPIN_COUNT Netty参数,一个Loop写操作执行的最大次数,默认值为16。也就是说,对于大数据量的写操作至多进行16次,如果16次仍没有全部写完数据,此时会提交一个新的写任务给EventLoop,任务将在下次调度继续执行。这样,其他的写请求才能被响应不会因为单个大数据量写请求而耽误。

(4)ALLOCATOR Netty参数,ByteBuf的分配器,默认值为ByteBufAllocator.DEFAULT,4.0版本为UnpooledByteBufAllocator,4.1版本为PooledByteBufAllocator。该值也可以使用系统参数io.netty.allocator.type配置,使用字符串值:“unpooled”,“pooled”。

  1. - Netty的ChannelInboundHandlerAdapter和SimpleChannelInboundHandler关系?
  2. - Netty的EventLoop核心实现?
  3. - Netty的连接管理事件接口有哪些常用方法(ChannelDuplexHandler)?
  4. - Netty的编解码与序列化手段
  5. - Netty的FastThreadLocal实现?
  6. - Netty中应用的装饰者 和 观察者模式在哪里体现?

MQ-消息队列

面试题汇总

  1. API使用,常用生产消费模型,集群架构搭建
  2. 常见问题,消息可靠性投递、幂等性保障

一个集群,为什么需要多个Broker(kafka实例),多实例保证一个broker挂了,整个集群还能继续使用,高可用

一个topic,为什么要有多个part:为了方便多个生产者忘多个part并发写入消息,提供吞吐量;

一个part,为什么要有多个副本:为了防止某个broker宕机,导致消息的丢失,提高可用性;

几个名词:

ISR(In-Sync Replicas):副本 同步队列,

OSR(Outof-Sync Replicas):副本 非同步队列

AR(Assigned Replicas)= ISR+OSR

Kafka消息保证生产的信息不丢失和重复消费问题:

使用同步模式的时候,有3种状态保证消息被安全生产,在配置为1(只保证写入leader成功)的话,如果刚好leader partition挂了,数据就会丢失。

还有一种情况可能会丢失消息,就是使用异步模式的时候,当缓冲区满了,如果配置为0(还没有收到确认的情况下,缓冲池一满,就清空缓冲池里的消息),数据就会被立即丢弃掉。

在数据生产时避免数据丢失的方法:

只要能避免上述两种情况,那么就可以保证消息不会被丢失。

在同步模式的时候,确认机制设置为-1,也就是让消息写入leader和所有的副本。

在异步模式下,如果消息发出去了,但还没有收到确认的时候,缓冲池满了,在配置文件中设置成不限制阻塞超时的时间,也就说让生产端一直阻塞,这样也能保证数据不会丢失。在数据消费时,避免数据丢失的方法:如果使用了storm,要开启storm的ackfail机制;如果没有使用storm,确认数据被完成处理之后,再更新offset值。低级API中需要手动控制offset值。

数据重复消费的情况,如何处理:

去重:将消息的唯一标识保存到外部介质中,每次消费处理时判断是否处理过(幂等)

不管:大数据场景中,报表系统或者日志信息丢失几条都无所谓,不会影响最终的统计分析结

Consumer端丢失消息的情形比较简单:如果在消息处理完成前就提交了offset,那么就有可能造成数据的丢失。由于Kafka consumer默认是自动提交位移的,所以在后台提交位移前一定要保证消息被正常处理了,因此不建议采用很重的处理逻辑,如果处理耗时很长,则建议把逻辑放到另一个线程中去做。为了避免数据丢失,现给出两点建议:enable.auto.commit=false  关闭自动提交位移在消息被完整处理之后再手动提交位移。

  1. 概念、原理、存储、消息投递、通信机制、性能相关优化
  2. MQ常见的作用于目的、服务解耦、削峰填谷等

异步处理,应用解耦,流量削锋和消息通讯四个场景

  1. RocketMQ

·  灵活可扩展性
RocketMQ 天然支持集群,其核心四组件(Name Server、Broker、Producer、Consumer)每一个都可以在没有单点故障的情况下进行水平扩展。

·  ·  海量消息堆积能力
RocketMQ 采用零拷贝原理实现超大的消息的堆积能力,据说单机已可以支持亿级消息堆积,而且在堆积了这么多消息后依然保持写入低延迟。

·  ·  支持顺序消息
可以保证消息消费者按照消息发送的顺序对消息进行消费。顺序消息分为全局有序和局部有序,一般推荐使用局部有序,即生产者通过将某一类消息按顺序发送至同一个队列来实现。

·  ·  多种消息过滤方式
消息过滤分为在服务器端过滤和在消费端过滤。服务器端过滤时可以按照消息消费者的要求做过滤,优点是减少不必要消息传输,缺点是增加了消息服务器的负担,实现相对复杂。消费端过滤则完全由具体应用自定义实现,这种方式更加灵活,缺点是很多无用的消息会传输给消息消费者。

·  ·  支持事务消息
RocketMQ 除了支持普通消息,顺序消息之外还支持事务消息,这个特性对于分布式事务来说提供了又一种解决思路。

·  ·  回溯消费
回溯消费是指消费者已经消费成功的消息,由于业务上需求需要重新消费,RocketMQ 支持按照时间回溯消费,时间维度精确到毫秒,可以向前回溯,也可以向后回溯。

  1. Kafka

kafka为什么那么快

1:内存缓存池

客户端发消息到服务端,不是一条一条发送的,而是有一个内存缓存池(18.2中有讲到这个概念)的设计思路在里面,当消息累计到一定数量后再发给服务端;内存缓存池避免每次新建立内存导致的GC过于频繁。

2:Reactor多路复用模型

Kafka采用的架构策略是Reactor多路复用模型,简单来说,就是搞一个acceptor线程,基于底层操作系统的支持,实现连接请求监听。如果有某个设备发送了建立连接的请求过来,那么那个线程就把这个建立好的连接交给processor线程。每个processor线程会被分配N多个连接,一个线程就可以负责维持N多个连接,他同样会基于底层操作系统的支持监听N多连接的请求。如果某个连接发送了请求过来,那么这个processor线程就会把请求放到一个请求队列里去。接着后台有一个线程池,这个线程池里有工作线程,会从请求队列里获取请求,处理请求,接着将请求对应的响应放到每个processor线程对应的一个响应队列里去。最后,processor线程会把自己的响应队列里的响应发送回给客户端。

3: 写入数据的超高性能:页缓存技术 + 磁盘顺序写

1)kafka是以磁盘顺序写的方式来写的。也就是说,仅仅将数据追加到文件的末尾,不是在文件的随机位置来修改数据。普通的机械磁盘如果你要是随机写的话,确实性能极差,也就是随便找到文件的某个位置来写数据。但是如果你是追加文件末尾按照顺序的方式来写数据的话,那么这种磁盘顺序写的性能基本上可以跟写内存的性能本身也是差不多的。

2)操作系统本身有一层缓存,叫做page cache,是在内存里的缓存,我们也可以称之为os cache,意思就是操作系统自己管理的缓存。你在写入磁盘文件的时候,可以直接写入这个os cache里,也就是仅仅写入内存中,接下来由操作系统自己决定什么时候把os cache里的数据真的刷入磁盘文件中。仅仅这一个步骤,就可以将磁盘文件写性能提升很多了,因为其实这里相当于是在写内存,不是在写磁盘。

4:从Kafka里我们经常要消费数据,那么消费的时候实际上就是要从kafka的磁盘文件里读取某条数据然后发送给下游的消费者,如图一所示。假设要是kafka什么优化都不做,就是很简单的从磁盘读数据发送给下游的消费者,那么大概过程图二所示:先看看要读的数据在不在os cache里,如果不在的话就从磁盘文件里读取数据后放入os cache。接着从操作系统的os cache里拷贝数据到应用程序进程的缓存里,再从应用程序进程的缓存里拷贝数据到操作系统层面的Socket缓存里,最后从Socket缓存里提取数据后发送到网卡,最后发送出去给下游消费。Kafka为了解决这个问题,在读数据的时候是引入零拷贝技术。也就是说,直接让操作系统的cache中的数据发送到网卡后传输给下游的消费者,中间跳过了两次拷贝数据的步骤,Socket缓存中仅仅会拷贝一个描述符过去,不会拷贝数据到Socket缓存,如图三。通过零拷贝技术,就不需要把os cache里的数据拷贝到应用缓存,再从应用缓存拷贝到Socket缓存了,两次拷贝都省略了,所以叫做零拷贝。对Socket缓存仅仅就是拷贝数据的描述符过去,然后数据就直接从os cache中发送到网卡上去了,这个过程大大的提升了数据消费时读取文件数据的性能。而且大家会注意到,在从磁盘读数据的时候,会先看看os cache内存中是否有,如果有的话,其实读数据都是直接读内存的。如果kafka集群经过良好的调优,大家会发现大量的数据都是直接写入os cache中,然后读数据的时候也是从os cache中读。相当于是Kafka完全基于内存提供数据的写和读了,所以这个整体性能会极其的高。

  1. RabbitMQ
  2. ActiveMQ

缓存

  1. 内存缓存
  2. 堆外内存缓存 回收释放

1. 堆内存完全由JVM负责分配和释放;

2. 使用堆外内存,就是为了能直接分配和释放内存,提高效率。

3. JDK5.0之后,代码中能直接操作本地内存的方式有2种:使用未公开的Unsafe和NIO包下ByteBuffer。

4. NIO直接内存的回收,需要依赖于System.gc()。如果我们的应用中使用了java nio中的direct memory,那么使用-XX:+DisableExplicitGC一定要小心(这个参数作用是禁止代码中显示调用GC),存在潜在的内存泄露风险。

5. 我们知道java代码无法强制JVM何时进行垃圾回收,也就是说垃圾回收这个动作的触发,完全由JVM自己控制,它会挑选合适的时机回收堆内存中的无用java对象。代码中显示调用System.gc(),只是建议JVM进行垃圾回收,但是到底会不会执行垃圾回收是不确定的,可能会进行垃圾回收,也可能不会。什么时候才是合适的时机呢?一般来说是,系统比较空闲的时候(比如JVM中活动的线程很少的时候),还有就是内存不足,不得不进行垃圾回收。我们例子中的根本矛盾在于:堆内存由JVM自己管理,堆外内存必须要由我们自己释放;堆内存的消耗速度远远小于堆外内存的消耗,但要命的是必须先释放堆内存中的对象,才能释放堆外内存,但是我们又不能强制JVM释放堆内存。

6. Direct Memory的回收机制:Direct Memory是受GC控制的,例如ByteBuffer bb = ByteBuffer.allocateDirect(1024),这段代码的执行会在堆外占用1k的内存,Java堆内只会占用一个对象的指针引用的大小,堆外的这1k的空间只有当bb对象被回收时,才会被回收,这里会发现一个明显的不对称现象,就是堆外可能占用了很多,而堆内没占用多少,导致还没触发GC,那就很容易出现Direct Memory造成物理内存耗光。Direct ByteBuffer分配出去的内存其实也是由GC负责回收的,而不像Unsafe是完全自行管理的,Hotspot在GC时会扫描Direct ByteBuffer对象是否有引用,如没有则同时也会回收其占用的堆外内存。

7. 使用堆外内存与对象池都能减少GC的暂停时间,这是它们唯一的共同点。生命周期短的可变对象,创建开销大,或者生命周期虽长但存在冗余的可变对象都比较适合使用对象池。生命周期适中,或者复杂的对象则比较适合由GC来进行处理。然而,中长生命周期的可变对象就比较棘手了,堆外内存则正是它们的菜。堆外内存的好处是:

(1)可以扩展至更大的内存空间。比如超过1TB甚至比主存还大的空间;

(2)理论上能减少GC暂停时间;

(3)可以在进程间共享,减少JVM间的对象复制,使得JVM的分割部署更容易实现;

(4)它的持久化存储可以支持快速重启,同时还能够在测试环境中重现生产数据

  1. Redis

Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API的非关系型数据库。

  1. 缓存穿透、雪崩、热点Key、大Key、无底洞问题,缓存更新与淘汰、缓存与数据库的一致性

缓存穿透

一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,就应该去后端系统查找(比如DB)。一些恶意的请求会故意查询不存在的key,请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。

如何避免?

1:对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该key对应的数据insert了之后清理缓存。

2:对一定不存在的key进行过滤。可以把所有的可能存在的key放到一个大的Bitmap中,查询时通过该bitmap过滤。

缓存雪崩

当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,会给后端系统带来很大压力。导致系统崩溃。

如何避免?

1:在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。

2:做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期

3:不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。

内存淘汰策略:redis 内存数据集大小上升到一定大小的时候,就会进行数据淘汰策略。

通过配置redis.conf中的maxmemory(config set maxmemory 100000:设置最大内存)这个值来开启内存淘汰功能(maxmemory为0的时候表示我们对Redis的内存使用没有限制)。

通过配置redis.conf中的maxmemory-policy设置淘汰策略设置:策略类型:

 1、最近最少使用(设置、不设置了过期时间的key数据集)

 2、将要过期的数据(设置、不设置设置了过期时间的key数据集)

 3、任意选择数据(设置、不设置了过期时间的key数据集)

 4、不可写入任何数据集(也不删除)

  1. Redis的幂等性
  2. Redis的分布式锁实现

分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁

我们加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time),这个set()方法一共有五个形参:

第一个为key,我们使用key来当锁,因为key是唯一的。

第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。

第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;

第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。

第五个为time,与第四个参数相呼应,代表key的过期时间。

  1. Redis的原子性,Redis的特点

Redis快的主要原因是:
   1、完全基于内存;
   2、数据结构简单,对数据操作也简单;
   3、使用多路 I/O 复用模型;(nio的Selector也是基于select/poll模型实现,是基于IO复用技术的非阻塞IO)

  1. Redis集群相关问题、一致性hash、slot概念等
  2. 面试题汇总
  3. Redis sentinel 是一个分布式系统中监控 redis 主从服务器,并在主服务器下线时自动进行故障转移。其中三个特性:

监控(Monitoring):    Sentinel  会不断地检查你的主服务器和从服务器是否运作正常。

提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。

自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作。

特点:

1、保证高可用

2、监控各个节点

3、自动故障迁移

缺点:主从模式,切换需要时间丢数据

没有解决 master 写的压力

流控组件

  1. Hystrix
  2. Sentinel
  3. 高可用服务中间件
  4. Zookeeper / Curator

Zookeeper 是一个分布式协调服务的开源框架。主要用来解决分布式集群中应用系统的一致性问题,例如怎样避免同时操作同一数据造成脏读的问题。

ZooKeeper 本质上是一个分布式的小文件存储系统。提供基于类似于文件系统的目录树方式的数据存储,并且可以对树中的节点进行有效管理。从而用来维 护和监控你存储的数据的状态变化。通过监控这些数据状态的变化,从而可以达 到基于数据的集群管理。诸如:统一命名服务、分布式配置管理、分布式消息队列、分布式锁、分布式协调等功能。

1.2. ZooKeeper 特性

全局数据一致、可靠性、顺序性、数据更新原子性、实时性

面试题汇总

Zookeeper有四种类型的znode: 

PERSISTENT-持久化目录节点 :客户端与zookeeper断开连接后,该节点依旧存在 

PERSISTENT_SEQUENTIAL-持久化顺序编号目录节点 :客户端与zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号 

EPHEMERAL-临时目录节点 :客户端与zookeeper断开连接后,该节点被删除 

EPHEMERAL_SEQUENTIAL-临时顺序编号目录节点 :客户端与zookeeper断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号 

Zookeeper 的核心是原子广播,这个机制保证了各个Server之间的同步。实现这个机制的协议叫做Zab协议。Zab协议有两种模式,它们分别是恢复模式(选主)和广播模式(同步)。当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数Server完成了和 leader的状态同步以后,恢复模式就结束了。状态同步保证了leader和Server具有相同的系统状态。 

直接使用Zookeeper原生API的人并不多,因为:

1)连接的创建是异步的,需要开发人员自行编码实现等待 

2)连接没有超时自动的重连机制 

3)Zookeeper本身没提供序列化机制,需要开发人员自行指定,从而实现数据的序列化和反序列化 

4)Watcher注册一次只会生效一次,需要不断的重复注册 

5)Watcher的使用方式不符合java本身的术语,如果采用监听器方式,更容易理解 

6)不支持递归创建树形节点

消息广播:

在消息广播的过程中,leader服务器会为每一个follower服务器都各自分配一个单独的队列,然后将需要广播的事务proposal依次放入这些队列中去,并且根据FIFO策略进行消息发送。每一个follower服务器在接受到这个事务proposal后,都会首先将其以事务日志的形式写入到本地磁盘中去,并且在成功写入后反馈给leader服务器一个ack响应。当leader服务器收到超过半数follower的ack响应后,就会广播一个commit消息给所有的follower服务器以通知其进行事务提交,同时leader自身也会完成对事务的提交,而每一个follower服务器在收到commit消息后,也会完成对事务的提交

崩溃恢复:

目前有5台服务器,每台服务器均没有数据,它们的编号分别是1,2,3,4,5,按编号依次启动,它们的选择举过程如下:

服务器1启动,给自己投票,然后发投票信息,由于其它机器还没有启动所以它收不到反馈信息,服务器1的状态一直属于Looking。

服务器2启动,给自己投票,同时与之前启动的服务器1交换结果,由于服务器2的编号大所以服务器2胜出,但此时投票数没有大于半数,所以两个服务器的状态依然是LOOKING。

服务器3启动,给自己投票,同时与之前启动的服务器1,2交换信息,由于服务器3的编号最大所以服务器3胜出,此时投票数正好大于半数,所以服务器3成为领导者,服务器1,2成为小弟。

服务器4启动,给自己投票,同时与之前启动的服务器1,2,3交换信息,尽管服务器4的编号大,但之前服务器3已经胜出,所以服务器4只能成为小弟。

服务器5启动,后面的逻辑同服务器4成为小弟

  1. Nginx

1、什么是Nginx

Nginx是一个高性能的HTTP和反向代理服务器,及电子邮件代理服务器,同时也是一个非常高效的反向代理、负载平衡。

2、为什么要用Nginx

跨平台、配置简单,非阻塞、高并发连接:处理2-3万并发连接数,官方监测能支持5万并发,

内存消耗小:开启10个nginx才占150M内存 ,nginx处理静态文件好,耗费内存少,

内置的健康检查功能:如果有一个服务器宕机,会做一个健康检查,再发送的请求就不会发送到宕机的服务器了。重新将请求提交到其他的节点上。

节省宽带:支持GZIP压缩,可以添加浏览器本地缓存

稳定性高:宕机的概率非常小

接收用户请求是异步的:浏览器将请求发送到nginx服务器,它先将用户请求全部接收下来,再一次性发送给后端web服务器,极大减轻了web服务器的压力,一边接收web服务器的返回数据,一边发送给浏览器客户端, 网络依赖性比较低,只要ping通就可以负载均衡,可以有多台nginx服务器 使用dns做负载均衡,事件驱动:通信机制采用epoll模型(nio2 异步非阻塞)

3、为什么Nginx性能这么高

得益于它的事件处理机制:异步非阻塞事件处理机制:运用了epoll模型,提供了一个队列,排队解决

4、Nginx是如何处理一个请求的

首先,nginx在启动时,会解析配置文件,得到需要监听的端口与ip地址,然后在nginx的master进程里面先初始化好这个监控的socket,再进行listen,然后再fork出多个子进程出来,  子进程会竞争accept新的连接。此时,客户端就可以向nginx发起连接了。当客户端与nginx进行三次握手,与nginx建立好一个连接后,此时,某一个子进程会accept成功,然后创建nginx对连接的封装,即ngx_connection_t结构体,接着,根据事件调用相应的事件处理模块,如http模块与客户端进行数据的交换。最后,nginx或客户端来主动关掉连接,到此,一个连接就寿终正寝了

5、正向代理

 一个位于客户端和原始服务器之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。客户端才能使用正向代理

正向代理总结就一句话:代理端代理的是客户端

6、反向代理

反向代理是指以代理服务器来接受internet上的连接请求,然后将请求,发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器

反向代理总结就一句话:代理端代理的是服务端

7、动态资源、静态资源分离

动态资源、静态资源分离是让动态网站里的动态网页根据一定规则把不变的资源和经常变的资源区分开来,动静资源做好了拆分以后,我们就可以根据静态资源的特点将其做缓存操作,这就是网站静态化处理的核心思路,动态资源、静态资源分离简单的概括是:动态文件与静态文件的分离

8、为什么要做动、静分离

在我们的软件开发中,有些请求是需要后台处理的(如:.jsp,.do等等),有些请求是不需要经过后台处理的(如:css、html、jpg、js等等文件),这些不需要经过后台处理的文件称为静态文件,因此我们后台处理忽略静态文件。这会有人又说那我后台忽略静态文件不就完了吗,当然这是可以的,但是这样后台的请求次数就明显增多了。在我们对资源的响应速度有要求的时候,我们应该使用这种动静分离的策略去解决,动、静分离将网站静态资源(HTML,JavaScript,CSS,img等文件)与后台应用分开部署,提高用户访问静态代码的速度,降低对后台应用访问,这里我们将静态资源放到nginx中,动态资源转发到tomcat服务器中

9、负载均衡

负载均衡即是代理服务器将接收的请求均衡的分发到各服务器中,负载均衡主要解决网络拥塞问题,提高服务器响应速度,服务就近提供,达到更好的访问质量,减少后台服务器大并发压力

  1. Haproxy
  2. LVS
  3. Haproxy

数据库存储 & 调度

  1. Sharding-JDBC
  2. ElasticJob

数据分片的目的在于把一个任务分散到不同的机器上运行,既可以解决单机计算能力上限的问题,也能降低部分任务失败对整体系统的影响。elastic-job并不直接提供数据处理的功能,框架只会将分片项分配至各个运行中的作业服务器(其实是Job实例,部署在一台机器上的多个Job实例也能分片),开发者需要自行处理分片项与真实数据的对应关系。框架也预置了一些分片策略:平均分配算法策略,作业名哈希值奇偶数算法策略,轮转分片策略。同时也提供了自定义分片策略的接口。

分片原理

elastic-job的分片是通过zookeeper来实现的。分片的分片由主节点分配,如下三种情况都会触发主节点上的分片算法执行:

1新的Job实例加入集群

2现有的Job实例下线(如果下线的是leader节点,那么先选举然后触发分片算法的执行)

3主节点选举

  1. 调度平台相关:DAG、airflow等

DAG:表示一个有向无环图,一个任务链, 其id全局唯一. DAG是airflow的核心概念, 任务装载到dag中, 封装成任务依赖链条. DAG决定这些任务的执行规则,比如执行时间.这里设置为从9月1号开始,每天8点执行.

  1. 搜索相关
  2. ELK ,数据库加速、主搜(算法)

日志分析系统:ELK是Elasticsearch、Logstash、Kibana的简称,这三者是核心套件,但并非全部。

Elasticsearch是实时全文搜索和分析引擎,提供搜集、分析、存储数据三大功能;是一套开放REST和JAVA API等结构提供高效搜索功能,可扩展的分布式系统。它构建于Apache Lucene搜索引擎库之上。

Logstash是一个用来搜集、分析、过滤日志的工具。它支持几乎任何类型的日志,包括系统日志、错误日志和自定义应用程序日志。它可以从许多来源接收日志,这些来源包括 syslog、消息传递(例如 RabbitMQ)和JMX,它能够以多种方式输出数据,包括电子邮件、websockets和Elasticsearch。

Kibana是一个基于Web的图形界面,用于搜索、分析和可视化存储在 Elasticsearch指标中的日志数据。它利用Elasticsearch的REST接口来检索数据,不仅允许用户创建他们自己的数据的定制仪表板视图,还允许他们以特殊的方式查询和过滤数据

  1. Logback、Slf4j2

Logback是由log4j创始人设计的另一个开源日志组件,官方网站: http://logback.qos.ch。它当前分为下面下个模块:

logback-core:其它两个模块的基础模块

logback-classic:它是log4j的一个改良版本,同时它完整实现了slf4j API使你可以很方便地更换成其它日志系统如log4j或JDK14 Logging

logback-access:访问模块与Servlet容器集成提供通过Http来访问日志的功能

logback取代log4j的理由:

1:更快的实现:Logback的内核重写了,在一些关键执行路径上性能提升10倍以上。而且logback不仅性能提升了,初始化内存加载也更小了。

2:非常充分的测试:Logback经过了几年,数不清小时的测试。Logback的测试完全不同级别的。

3:Logback-classic非常自然实现了SLF4j:Logback-classic实现了SLF4j。在使用SLF4j中,你都感觉不到logback-classic。而且因为logback-classic非常自然地实现了slf4j , 所 以切换到log4j或者其他,非常容易,只需要提供成另一个jar包就OK,根本不需要去动那些通过SLF4JAPI实现的代码。

4:非常充分的文档 官方网站有两百多页的文档。

5:自动重新加载配置文件,当配置文件修改了,Logback-classic能自动重新加载配置文件。扫描过程快且安全,它并不需要另外创建一个扫描线程。这个技术充分保证了应用程序能跑得很欢在JEE环境里面。

6:Lilith是log事件的观察者,和log4j的chainsaw类似。而lilith还能处理大数量的log数据 。

7:谨慎的模式和非常友好的恢复,在谨慎模式下,多个FileAppender实例跑在多个JVM下,能 够安全地写道同一个日志文件。RollingFileAppender会有些限制。Logback的FileAppender和它的子类包括 RollingFileAppender能够非常友好地从I/O异常中恢复。

8:配置文件可以处理不同的情况,开发人员经常需要判断不同的Logback配置文件在不同的环境下(开发,测试,生产)。而这些配置文件仅仅只有一些很小的不同,可以通过,和来实现,这样一个配置文件就可以适应多个环境。

9:Filters(过滤器)有些时候,需要诊断一个问题,需要打出日志。在log4j,只有降低日志级别,不过这样会打出大量的日志,会影响应用性能。在Logback,你可以继续 保持那个日志级别而除掉某种特殊情况,如alice这个用户登录,她的日志将打在DEBUG级别而其他用户可以继续打在WARN级别。要实现这个功能只需加4行XML配置。可以参考MDCFIlter 。

10:SiftingAppender(一个非常多功能的Appender):它可以用来分割日志文件根据任何一个给定的运行参数。如,SiftingAppender能够区别日志事件跟进用户的Session,然后每个用户会有一个日志文件。

11:自动压缩已经打出来的log:RollingFileAppender在产生新文件的时候,会自动压缩已经打出来的日志文件。压缩是个异步过程,所以甚至对于大的日志文件,在压缩过程中应用不会受任何影响。

12:堆栈树带有包版本:Logback在打出堆栈树日志时,会带上包的数据。

13:自动去除旧的日志文件:通过设置TimeBasedRollingPolicy或者SizeAndTimeBasedFNATP的maxHistory属性,你可以控制已经产生日志文件的最大数量。如果设置maxHistory 12,那那些log文件超过12个月的都会被自动移除。

  1. Solr & Lucene

1、Solr是一个独立的企业级搜索应用服务器,它对外提供类似于Web-service的API接口。

用户可以通过HTTP的POST请求,向Solr服务器提交一定格式的XML或者JSON文件,Solr服务器解析文 件之后,根据具体需求对索引库执行增删改操作;

用户可以通过HTTP的GET请求,向Solr服务器发送搜索请求,并得到XML/JSON格式的返回结果。

Solr 是Apache下的一个*开源项目,采用Java开发,基于Lucene。 Solr可以独立运行在Jetty、Tomcat等这些Servlet容器中。
Solr提供了比Lucene更为丰富的查询语言,同时实现了可配置、可扩展,并对索引、搜索性能进行了优化。 

2、Solr和Lucene的区别

Lucene是一个开放源代码的全文检索引擎工具包,它不是一个完整的全文检索应用。

Lucene仅提供了完整的查询引擎和索引引擎,目的是为软件开发人员提供一个简单易用的工具包,以方便的在目 标系统中实现全文检索的功能,或者以Lucene为基础构建全文检索应用。 

Solr的目标是打造一款企业级的搜索引擎系统,它是基于Lucene一个搜索引擎服务器,可以独立运行,通过Solr可 以非常快速的构建企业的搜索引擎,通过Solr也可以高效的完成站内搜索功能。

Solr默认提供Jetty(java写的Servlet容器)启动solr服务器。

BPM相关

  1. JBPM
  2. Activiti
  3. 大数据相关:
  4. monogdb
  5. 集群模式:replication复制集、shard分片
  6. 复制集原理、分片原理
  7. 分片键选择、最佳实践
  8. Hadoop

Hadoop就是存储海量数据和分析海量数据的工具。

Hadoop的框架最核心的设计就是:HDFS和MapReduce。HDFS为海量的数据提供了存储,则MapReduce为海量的数据提供了计算。

把HDFS理解为一个分布式的,有冗余备份的,可以动态扩展的用来存储大规模数据的大硬盘。

把MapReduce理解成为一个计算引擎,按照MapReduce的规则编写Map计算/Reduce计算的程序,可以完成计算任务。

2、Hadoop能干什么

大数据存储:分布式存储

日志处理:擅长日志分析

ETL:数据抽取到oracle、mysql、DB2、mongdb及主流数据库

机器学习: 比如Apache Mahout项目

搜索引擎:Hadoop + lucene实现

数据挖掘:目前比较流行的广告推荐,个性化广告推荐

Hadoop是专为离线和大规模数据分析而设计的,并不适合那种对几个记录随机读写的在线事务处理模式。

  1. hfds原理、mr概念模型
  2. 应用场景,使用;业务实践
  3. hbase、hive
  4. hbase特点、模型、应用场景(列簇模型)
  5. hive、kylin实践场景(olap)
  6. etl工具、etl实践、思路
  7. 批处理/流处理:spark/flink
  8. streaming/sql
  9. flink 应用场景实践
  10. 数据仓库、数据建模
  11. 数据采集
  12. 标签系统实践、数据仓库实践
  13. 用户画像、行为分析
  14. 精准营销、推送;
  15. 指标度量
  16. OpenStack、kvm
  17. Docker、k8s
  18. devops/aiops相关
  19. 全链路压测
  20. 灰度、埋点、热变更
  21. 数据同步
  22. mysql binlog、文件同步

工作原理:

面试题汇总

1、主节点必须启用二进制日志,记录任何修改了数据库数据的事件。
2、从节点开启一个线程(I/O Thread)把自己扮演成 mysql 的客户端,通过 mysql 协议,请求主节点的二进制日志文件中的事件
3、主节点启动一个线程(dump Thread),检查自己二进制日志中的事件,跟对方请求的位置对比,如果不带请求位置参数,则主节点就会从第一个日志文件中的第一个事件一个一个发送给从节点。
4、从节点接收到主节点发送过来的数据把它放置到中继日志(Relay log)文件中。并记录该次请求到主节点的具体哪一个二进制日志文件内部的哪一个位置(主节点中的二进制文件会有多个,在后面详细讲解)。
5、从节点启动另外一个线程(sql Thread ),把 Relay log 中的事件读取出来,并在本地再执行一次

基础面试题:

  1. ThreadPoolExecutor核心参数概念、含义;IO、CPU密集型概念;ThreadPoolExecutor内部数据结构,核心运行流程;
  2. 时间复杂度的概念,数组、双重for循环、二分法、属性节点的时间复杂度;
  3. 双亲委派模式,三种类加载器机制;
  4. Unsafe类都有哪些操作方法,具体大体分哪些种类,分别在哪些源码里有体现;
  5. 元注解的概念,自定义注解;
  6. java字节码技术,agent探针概念,哪些框架使用了该技术,如何使用的;
  7. AQS底层数据结构,如何实现公平与非公平;
  8. CAS的ABA问题,如何解决;
  9. 静态代理和动态代理;

按照代理的创建时期,代理类可以分为两种: 

静态:由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的.class文件就已经存在了。

动态:在程序运行时运用反射机制动态创建而成。

静态代理类优缺点

优点:代理使客户端不需要知道实现类是什么,怎么做的,而客户端只需知道代理即可(解耦合),对于如上的客户端代码,newUserManagerImpl()可以应用工厂将它隐藏,如上只是举个例子而已。

缺点:

1)代理类和委托类实现了相同的接口,代理类通过委托类实现了相同的方法。这样就出现了大量的代码重复。如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。

2)代理对象只服务于一种类型的对象,如果要服务多类型的对象。势必要为每一种对象都进行代理,静态代理在程序规模稍大时就无法胜任了。如上的代码是只为UserManager类的访问提供了代理,但是如果还要为其他类如Department类提供代理的话,就需要我们再次添加代理Department的代理类。

区别:静态代理只能针对对应的类进行代理,如果类很多就需要很多的代理。 动态代理就是弥补了静态代理的这个缺陷。通过使用动态代理,我们可以通过在运行时,动态生成一个持有RealObject、并实现代理接口的Proxy,同时注入我们相同的扩展逻辑。哪怕你要代理的RealObject是不同的对象,甚至代理不同的方法,都可以动过动态代理,来扩展功能。

  1. ReentrantLock的lock方法和lockinterrupt方法区别;

ReentrantLock的加锁方法Lock()提供了无条件地轮询获取锁的方式,lockInterruptibly()提供了可中断的锁获取方式。这两个方法的区别在哪里呢?通过分析源码可以知道lock方法默认处理了中断请求,一旦监测到中断状态,则中断当前线程;而lockInterruptibly()则直接抛出中断异常,由上层调用者区去处理中断。

1  lock操作:lock获取锁过程中,忽略了中断,在成功获取锁之后,再根据中断标识处理中断,即selfInterrupt中断自己。

2 lockInterruptibly操作:可中断加锁,即在锁获取过程中不处理中断状态,而是直接抛出中断异常,由上层调用者处理中断。源码细微差别在于锁获取这部分代码,这个方法与acquireQueue差别在于方法的返回途径有两种,一种是for循环结束,正常获取到锁;另一种是线程被唤醒后检测到中断请求,则立即抛出中断异常,该操作导致方法结束。

 

ReentrantLock的中断和非中断加锁模式的区别在于:线程尝试获取锁操作失败后,在等待过程中,如果该线程被其他线程中断了,它是如何响应中断请求的。lock方法会忽略中断请求,继续获取锁直到成功;而lockInterruptibly则直接抛出中断异常来立即响应中断,由上层调用者处理中断。

     那么,为什么要分为这两种模式呢?这两种加锁方式分别适用于什么场合呢?根据它们的实现语义来理解,我认为lock()适用于锁获取操作不受中断影响的情况,此时可以忽略中断请求正常执行加锁操作,因为该操作仅仅记录了中断状态(通过Thread.currentThread().interrupt()操作,只是恢复了中断状态为true,并没有对中断进行响应)。如果要求被中断线程不能参与锁的竞争操作,则此时应该使用lockInterruptibly方法,一旦检测到中断请求,立即返回不再参与锁的竞争并且取消锁获取操作(即finally中的cancelAcquire操作)

  1. ThreadLocal内部数据结构,在哪些框架使用过ThreadLocal;
  2. java中的引用类型:强、弱、软、虚引用;
  3. 面试题汇总
  4. cache策略,LRU和LFU的概念;
  5. 雪花算法的概念,和数据结构;

雪花算法:分布式唯一ID生成算法

SnowFlake的结构如下(每部分用-分开):0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000

 * 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0

 * 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。

 * 41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69

 * 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId

 * 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号加起来刚好64位,为一个Long型。

 * SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,

 * 经测试,SnowFlake每秒能够产生26万ID左右。

public class SnowflakeIdWorker {

    /** 开始时间截 */

    private final long twepoch = 1420041600000L;

    /** 机器id所占的位数 */

    private final long workerIdBits = 5L;

    /** 数据标识id所占的位数 */

    private final long datacenterIdBits = 5L;

    /** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */

    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);

    /** 支持的最大数据标识id,结果是31 */

    private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);

    /** 序列在id中占的位数 */

    private final long sequenceBits = 12L;

    /** 机器ID向左移12位 */

    private final long workerIdShift = sequenceBits;

    /** 数据标识id向左移17位(12+5) */

    private final long datacenterIdShift = sequenceBits + workerIdBits;

    /** 时间截向左移22位(5+5+12) */

    private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    /** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */

    private final long sequenceMask = -1L ^ (-1L << sequenceBits);

    /** 工作机器ID(0~31) */

    private long workerId;

    /** 数据中心ID(0~31) */

    private long datacenterId;

    /** 毫秒内序列(0~4095) */

    private long sequence = 0L;

    /** 上次生成ID的时间截 */

    private long lastTimestamp = -1L;

     * 构造函数

     * @param workerId 工作ID (0~31)

     * @param datacenterId 数据中心ID (0~31)

    public SnowflakeIdWorker(long workerId, long datacenterId) {

        if (workerId > maxWorkerId || workerId < 0) {

            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));

        }

        if (datacenterId > maxDatacenterId || datacenterId < 0) {

            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));

        }

        this.workerId = workerId; this.datacenterId = datacenterId;

    }

     * 获得下一个ID (该方法是线程安全的)

    public synchronized long nextId() {

        long timestamp = timeGen();

        //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常

        if (timestamp < lastTimestamp) {

            throw new RuntimeException( String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); }

        //如果是同一时间生成的,则进行毫秒内序列

        if (lastTimestamp == timestamp) {

            sequence = (sequence + 1) & sequenceMask;

            //毫秒内序列溢出

            if (sequence == 0) {

                //阻塞到下一个毫秒,获得新的时间戳

                timestamp = tilNextMillis(lastTimestamp);

            }

        }

        //时间戳改变,毫秒内序列重置

        else { sequence = 0L;  }

        //上次生成ID的时间截

        lastTimestamp = timestamp;

        //移位并通过或运算拼到一起组成64位的ID

        return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | sequence;

    }

     * 阻塞到下一个毫秒,直到获得新的时间戳

     * @param lastTimestamp 上次生成ID的时间截

     * @return 当前时间戳

    protected long tilNextMillis(long lastTimestamp) {

        long timestamp = timeGen();

        while (timestamp <= lastTimestamp) { timestamp = timeGen(); } return timestamp; }

     * 返回以毫秒为单位的当前时间

     * @return 当前时间(毫秒)

    protected long timeGen() { return System.currentTimeMillis(); }

    public static void main(String[] args) {

        SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);

        for (int i = 0; i < 1000; i++) {

            long id = idWorker.nextId(); System.out.println(Long.toBinaryString(id)); System.out.println(id); } } }

  1. tcp滑动窗口的概念;

TCP协议作为一个可靠的面向流的传输协议,其可靠性和流量控制由滑动窗口协议保证,而拥塞控制则由控制窗口结合一系列的控制算法实现。

“窗口”对应的是一段可以被发送者发送的字节序列,其连续的范围称之为“窗口”;

“滑动”则是指这段“允许发送的范围”是可以随着发送的过程而变化的,方式就是按顺序“滑动”

  1. 原生的NIO如何处理TCP的半包读写,比如发送端有8个字节,如何处理11个字节或者7个字节的数据;
  2. 关于linux的内核态和用户态的概念说明,为什么需要用户进程(位于用户态中)要通过系统调用(Java中即使JNI:Java Native Interface)来调用内核态中的资源,或者说调用操作系统的服务了?
  3. Intel cpu 提供四种级别的运行模式概念;

Intel CPU的四种运行模式:高性能模式、单核模式,省电模式,用户隔离模式

1、performance高性能模式:在这个模式系统会按设定最大主频率满负荷运转,主频会一直保持在设定范围内的最大值。它和省电模式相反,始终按设定最高频率运行,此模式亦无任何日常使用价值;

2、hotplug单核模式:在这个模式系统会在检测到CPU低负载关闭一个核心变成单核;

3、powersave省电模式:此模式下系统将保持在设定最小频率低负荷运行。按设定最低频率运行,日常没有使用价值,除非配合setcpu情景模式,关屏睡眠时使用此调节模式;

4、userspace用户隔离模式-:当cpu设置模块处于非工作状态时控制cpu速度的一种方法。严格来说它并不是一个模式,是允许非内核进程控制cpu频率的设置,现在已经不需要它了,setcpu官方的建议是,“不要使用此选项”。

  1. 那为什么操作系统不直接访问Java堆内的内存区域了?
  2. java的MMap的概念和使用?

mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享

mmap优点共有一下几点:

  1. 对文件的读取操作跨过了页缓存,减少了数据的拷贝次数,用内存读写取代I/O读写,提高了文件读取效率。
  2. 实现了用户空间和内核空间的高效交互方式。两空间的各自修改操作可以直接反映在映射的区域内,从而被对方空间及时捕捉。
  3. 提供进程间共享内存及相互通信的方式。不管是父子进程还是无亲缘关系的进程,都可以将自身用户空间映射到同一个文件或匿名映射到同一片区域。从而通过各自对映射区域的改动,达到进程间通信和进程间共享的目的。

J.U.C面试QA

AQS相关问题

J.U.C是基于AQS实现的,AQS是一个同步器,设计模式是模板模式。
核心数据结构:双向链表 + state(锁状态)
底层操作:CAS

169. AQS的父类是什么?

AbstractOwnableSynchronizer:一种同步器,可能只属于一个线程。该类为创建可能涉及所有权概念的锁和相关同步器提供了基础。AbstractOwnableSynchronizer类本身不管理或使用此信息。但是子类和工具可以使用适当维护的值来帮助控制和监视访问并提供诊断。

面试题汇总

 170. AQS内部的整体数据结构是什么样子的?

CHL + state, 即同步等待队列+共享域

171. AQS的CHL是一个什么结构?

CHL是一个FIFO的同步等待队列, 类似双向链表的结构, 简单来说: 没有成功获取控制权的线程会在这个队列中等待;

172. AQS数据模型Node是什么样的?

内部的同步等待队列是由一系列节点组成的一个链表, Node内部有指向其前驱和后继节点的引用(类似双向链表)

如果要将一个线程入队(竞争失败,进入队列等待): 只需将这个线程及相关信息组成一个节点, 拼接到队列链表尾部(尾节点)即可;

如果要将一个线程出队(竞争成功): 只需重新设置新的队列首部(头节点)即可;

173. AQS实现的同步机制可以按自己的需要来灵活使用这个state域, 举例那些子类使用了state域并做了什么工作?

ReentrantLock用它记录锁重入次数;

CountDownLatch用它表示内部的count值;

FutureTask用它表示任务运行状态(Running, Ran和Cancelled);

Semaphore用它表示许可数量;

174. AQS提供了独占和共享两种模式, 说一说两种的区别, AQS子类中那些类是独占的, 那些是共享的, 那些是独占且共享的实现?

在独占模式下,当一个线程获取了AQS的控制权,其他线程获取控制权的操作就会失败;

在共享模式下,其他线程的获取控制权操作就可能成功;

ReentrantLock就是典型的独占模式, Semaphore是共享模式, ReentrantReadWriteLock两种模式并存;

175. AQS内部提供了一个ConditionObject类来支持独占模式下的(锁)条件, 与Object的wait/notify/notifyAll区别是什么?

Condition能让线程在各自条件下的等待队列等待; 而不是像Object一样在同一个等待队列里面等待;

AQS衍生子类

176. ReentrantLock底层实现, ReentrantReadWriteLock底层实现?

ReentrantLock是一种独占方式的锁, 允许重入, 所以也叫重入锁;

实现了公平和非公平锁, 不管是那种锁, 如果首次获取许可失败, 最终都是会进入到CHL队列中去排队等待;

公平锁是把当前请求线程加入到链表尾部, 而非公平锁则是先使用CAS做比较交换操作, 如失败则加入到CHL队列中;

ReentrantReadWriteLock是一种读采用共享、写采用独占方式的一种锁;

177. Semaphore底层实现?

Semaphore是一种AOS共享方式的实现, 利用state域表示许可数量, Semaphore也支持公平和非公平策略, 默认为非公平策略; 公平策略首先需要检查同步队列里有没有比当前线程更早的线程在等待, 如果有则返回-1, 没有则做CAS操作state域赋值;

而非公平策略则直接会进行CAS操作state域赋值;

首先根据许可初始值给state域赋值, 然后调用Acquire和Release方法对state进行 CAS++和CAS--操作;

也可以支持reducePermits方法实现批量减去一定的许可;

178. CountDownLatch底层实现?

CountDownLatch内部同步器, 利用AQS的state来表示count, CountDownLatch是一种闭锁的实现;

所谓闭锁就是只有state域值为0时, 线程才获取锁, 从而进入等待状态; 根据初始化时的count值进行线程的等待,

当其他线程(可能多个)去调用countDown方法时, 就会递减最开始的count(state)值, 直到count=0 才返回true; 否则一直返回false;

  1. CyclicBarrier底层实现?

CyclicBarrier是一种可重复使用的栅栏机制, 可以让一组线程在某个点上相互等待, 这个点就可以类比为栅栏;

CyclicBarrier还支持在所有线程到达栅栏之后, 在所有线程从等待状态转到可运行状态之前, 执行一个命令;

当然在某些情况下, 栅栏可以被打破, 比如某个线程无法在规定的时间内到达栅栏:

当建立一个使用方数量为n的栅栏时, 栅栏内部有一个为n的计数; 当使用方调用await方法时, 如果其他n-1个使用方没有全部到达await方法(内部计数减1后,不等于0); 那么使用方(线程)阻塞等待。

当第n个使用方调用await时, 栅栏开放(内部计数减1后等于0), 会唤醒所有在await方法上等待着的使用方(线程); 大家一起通过栅栏, 然后重置栅栏(内部计数又变成n), 栅栏变成新建后的状态, 可以再次使用;

CyclicBarrier的核心方法只有一个, 那就是dowait, 核心实现巧妙的使用了ReentrantLock与Condition的组合;

CyclicBarrier初始化可以设置栅栏的数量count, 对于每次其他线程调用等待方法时, 都会首先尝试获取重入锁,

然后对count--操作, 然后调用Condition条件等待方法使当前进入的线程处于等待状态(由于Condition条件的等待状态释放重入锁,

所以后面的线程也可以继续获取重入锁), 当count=0时, 则一起唤醒, 调用Condition条件的signalAll唤醒所有等待线程;

应用思考:

  1. Spring应用: 实现一个自定义注解,把所有类中带有该注解的Class注入到Spring容器,类似@Service @component的 实现机制?
  2. 池化应用:如何实现一个自己的连接池,能够实现资源的管理,需要用到哪些设计模式,给出设计思路,所参考的开源框架有哪些?(可以参考apache.commons.pool2实现原理)
  3. RPC应用:如何实现一个最简单的RPC通信服务(使用动态代理+ 反射 + NIO 实现一个最简单的RPC服务通信)
  4. 如何实现一个简单的分布式定时任务?给出设计思路,要求实现扩展性、高可用(失败转移)、分片策略,和动态化配置功能。
  5. 常用的模板模式开发,如何实现动态模板方式,结合DDD实现动态规则注入功能。