# For the interview **Repository Path**: aaahxz/For-the-interview ## Basic Information - **Project Name**: For the interview - **Description**: Java面试自用,如有错误请及时提出! - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 0 - **Created**: 2020-03-17 - **Last Updated**: 2020-12-20 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## Java面试自用 [TOC] ### 1. 重写equals为什么一定要重写hashcode? ```java Student s1=new Student("小明",18); Student s2=new Student("小明",18) ``` 这里就是Object类里面有两个方法,一个是equals,另一个就是hashcode,这两个在Object里面的作用是一样的,都是比较的对象的引用地址。 假如只重写equals而不重写hashcode,那么Student类的hashcode方法就是Object默认的hashcode方法,由于默认的hashcode方法是根据对象的内存地址经哈希算法得来的,显然此时s1!=s2,故两者的hashcode不一定相等。 而如果我们设计的equals的方法此时返回true呢,是不是就矛盾了,hashcode说这两个对象不相等,而equals说相等。 > 两个对象相等,hashcode一定相等 > > 两个对象不等,hashcode不一定不等 > > hashcode相等,两个对象不一定相等 > > hashcode不等,两个对象一定不等 #### 拓展: 如上所示,是String类型的一种强引用 #### 1. 创建的规则就是: 1. 先到字符串常量池里去找相应地对象 2. 若找不到,则分别在堆里和字符串常量池里面创建一个对象 #### 2. 针对字符串常量池的位置: 1. 1.7以前在非堆也就是方法区里 2. 1.7将字符串常量池放到了堆里 3. 1.8将方法区永久代取缔,改为永久代 #### 3. 方法区和永久代的区别以及联系: 前者是 JVM 的规范,而后者则是 JVM 规范的一种实现,并且只有 HotSpot 才有 “PermGen space”,而对于其他类型的虚拟机,如 JRockit(Oracle)、J9(IBM) 并没有“PermGen space”。 有强引用就有弱引用, #### 4. 四种引用: 1. 强引用:最基本的就比如 ```java Date date = new Date(); ``` 这是一很明显的强引用,其他的例如 ```java String a = “123”; ``` 这是一种非显示引用,先去字符串池中去找,然后如若没有,则在堆里创建一个。这种引用有一个特点就是:即使JVM会扔出一个OOME也不去回收这个强引用,只能用我们自己声明这个对象为NULL比如 ```java Date date = null ``` 开发注意:避免一直使用强引用,不用对象可以在finally声明为**null** 2. 软引用:在使用软引用时,如果内存的空间足够,软引用就能继续被使用,而不会被垃圾回收器回收;**只有在内存空间不足时,软引用才会被垃圾回收器回收。** ```java SoftReference softName = new SoftReference<>("张三"); ``` 开发注意:适合做缓存。 3. 弱引用:只要GC发生就回收 ```java WeakReference weakName = new WeakReference("hello"); ``` 开发注意:适合做消息队列等。 4. 虚引用:顾名思义,就是形同虚设,如果一个对象仅持有虚引用,那么它相当于没有引用,在任何时候都可能被垃圾回收器回收。 ```java PhantomReference pr = new PhantomReference(new String("hello"), queue); ``` ### 2. 八种基本类型 **整型:** byte,short,int, long **浮点型:** float,double **字符型**:char **boolean型**:boolean #### 2.1 整数型变量 - 整型类变量用来存储整数数值,即没有小数部分的值 - 整数类型分四中不同的类型:字节型(byte)、短整型(short)、整型(int)、长整型(long) ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190409205439833.png) - 在为一个long类型的变量赋值时需要注意一点,所赋值的后面要加上一个字母L(或小写l),说明赋值为long类型。如果赋的值未超出int型的取值范围,可以省略字母L(或小写l) - (题外话)之前在一个java交流群里我居然不知道有这个L的存在,之前一直自动生成的序列号没仔细看原来还有个L代表是Long型变量。例如: ```java long num =2200000000L; // 所赋的值超出了int型的取值范围,后面必须加上字母L long num =198L; // 所赋的值未超出int型的取值范围,后面可以加上字母L long num =198; // 所赋的值未超出int型的取值范围,后面可以省略字母L ``` - 这里又引申出了一点,当我们get到一个**int**类型的数据为0时,就不可以认为这个数据没有被赋值,因为**int类型初始值就是0** #### 2.2 浮点型变量 - 浮点类型变量用来存储小数数值,浮点数不能用来表示精准的值,如:货币。 - 浮点类型分两种:单精度浮点(float)、双精度浮点(double)。 - double类型所表示的浮点数比float类型更加准确。 - 浮点数的默认类型为double类型。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190409205632955.png) - 若用float类型的变量,必须在后加上f或者F,用double类型可以不用,因为是默认的 ```java float f = 123.4f; //为一个float类型的变量赋值,后面必须加上字母f; double d1=100.1; //为一个double类型的变量赋值,后面可以省略字母d; double d2=199.3d; //为一个double类型的变量赋值,后面可以加上字母d; ``` - 也可以为一个浮点类型变量赋予一个整型数值 #### 2.3 字符类型变量 - 字符类型变量用于存储单一字符,Java中用char表示 - Java中,每个char类型的字符变量都会占用两个字节,16位。 - char类型可以用于存储中文汉字:char是用来存储Unicode的字符的,Unicode的字符集里面包含了汉字,所以是可以用来存储汉字的, **说明:unicode编码占用两个字节,所以,char类型的变量也是占用两个字节**。 - char类型的变量赋值时,需要英文的单引号’’把字符括起来,如’a’。 - char类型的变量赋值范围是0~65535内的整数。 - 最小值是\u0000(即为0);最大值是\uffff(即为65535)。 - char数据类型可以存储任何字符 ```java //字符类型 ,占2个字节,16位 包装类 Character char c = ‘a’; //为一个char类型的变量赋值字符a; char ch = 97; //为一个char类型的变量赋值整数97,相当于赋值字符a; char h='\r'; //特殊的转义字符 char i='\u9990'; //Unicode字符集 \u0000‐\uFFFF char j=65535; //字符0到 65535 ``` ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190409205849523.png) #### 2.4 布尔型变量 - 布尔类型变量用来存储布尔值,在Java中用boolean表示,boolean类型的变量只有两个值,即true和false,默认值为false。 - boolean数据类型表示一位的信息 ```java boolean flag = false; //声明一个boolean类型的变量,初始值为false; flag = true; //改变flag变量的值未true; ``` ------ ### 3. 抽象类和接口的区别 - 抽象类要被子类继承,接口要被类实现 - 接口只能做方法声明,抽象可以做方法声明,也可以做方法实现 - 接口里的变量只能是公共的静态的常量,抽象类中的变量是普通变量 - 接口是设计的结果,抽象类是重构的结果 - 抽象类和接口都是用来抽象具体对象的,但是接口的抽象级别更高 - 抽象类可以有具体的方法和属性,接口只有抽象方法和不可变常量 - 接口主要用来抽象类别,接口主要用来抽象功能 ### 4. 普通类和抽象类的区别 - 抽象类不能被实例化。 - 抽象类可以有抽象方法,抽象方法可以只需申明,无须实现。 - 含有抽象方法的类必须申明为抽象类。 - 抽象类的子类必须实现抽象类的所有抽象方法,否则这个子类也是抽象类。 - 抽象方法不能申明为静态,不能用private、final修饰。 #### 5. Synchronize和lock的区别 - synchronize自动释放锁,而Lock必须手动释放,并且代码中出现异常会导致unlock代码不执行,所以Lock一般在Finally中释放,而synchronize释放锁是由JVM自动执行的。即一个表现为API层面的互斥锁,一个表现为原生语法层面的互斥锁。 - Lock有共享锁的概念,所以可以设置读写锁提高效率,synchronize不能。(两者都可重入) - Lock可以让线程在**获取锁的过程中响应中断**,而synchronize不会,**线程会一直等待下去**。lock.lockInterruptibly()方法会优先响应中断,而不是像lock一样优先去获取锁。 - Lock锁的是代码块,synchronize还能锁方法和类。 - Lock可以知道线程有没有拿到锁,而synchronize不能。 - 在竞争很激烈的情况下,lock性能明显优于synchronize关键字 Synchronize是通过monitorenter和monitorexit来实现,monitor可实现监视器的功能,调用monitorenter就是尝试获取这个对象,获取成功则+1,离开则-1,如果是线程重入,则继续+1,即synchronize是可重入的。 ##### 补充:谈谈对AQS的理解: ![image-20200210095542227](C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200210095542227.png) 1. AQS全称AbstractQueuedSynchronizer,是java并发包中的核心类,诸如ReentrantLock,CountDownLatch等工具内部都使用了AQS去维护锁的获取与释放 2. 内部维护了一个state,state为0时代表内有线程持有锁,大于0时(可重入,每次获取锁都会加一)表示线程持有锁。通过acquire(int arg)获取独占锁。 3. head和tail表示当前持有锁的线程和未持有锁的线程,一个线程尝试获取锁,如果失败则加入等待队列末尾 4. AQS内部通过**一个CLH阻塞队列(一个FIFO线程等待队列)**去维持线程的状态,并且使用LockSupport工具去实现线程的阻塞和和唤醒,同时里面大量运用了无锁的CAS算法去实现锁的获取和释放。 5. AQS 定义了两种资源共享的方式 Exclusive(独占,一时间只有一个线程能访问该资源)、Share (共享,一时间可以有多个线程访问资源). 6. AQS源码中帮我们做好了线程排队、等待、唤醒等操作我们只需要重写决定如何获取和释放的锁,这是典型的模板方法。 引申出来的问题: ##### 锁类型 - 可重入锁**(synchronized和ReentrantLock)**:在执行对象中所有同步方法不用再次获得锁 - 可中断锁(**Synchronize为不可中断锁,而lock是**):在等待获取锁过程中可中断 - 公平锁:**(ReentrantLock和ReentrantReadWriteLock)**:按照获取锁等待的时间进行获取,等待时间长的可以优先获取 - 读写锁:**(ReadWritelockheReentrantReadWriteLock)**:对资源读取和写入的时候拆分为2部分处理,读的时候可以多线程一起读,写的时候必须同步地写 - 乐观锁/悲观锁:悲观锁认为对同一个数据的并发操作,其他线程一定会修改数据。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。而乐观锁对同一数据进行并发操作,总是认为其他线程不会对数据进行修改。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。 - 偏向锁/轻量级锁/重量级锁 | biased_lock | lock | 状态 | | :---------: | :--: | :------: | | 0 | 01 | 无锁 | | 1 | 01 | 偏向锁 | | 0 | 00 | 轻量级锁 | | 0 | 10 | 重量级锁 | | 0 | 11 | GC标记 | ##### 乐观锁代表CAS操作: CAS是**Compare and Swap** 的简写,CAS的思想很简单:三个参数,一个当前内存值V、一个内存旧值A还有一个是新值B,当且仅当A==V时,即认为其他线程没有对数据进行修改则将B(新值)设置为内存值,并返回true,**否则什么都不做。**JVM中的CAS操作正是利用了处理器提供的CMPXCHG指令实现的;CAS操作会引起ABA带来的问题,可以通过版本号来解决。 ##### 1. ABA问题: 抽象一个场景,小明有一百元,需要转给妈妈五十,来到银行ATM机,输入50,点击转账,由于不可避免因素,小明又重试了一次,而此时,小明的妈妈也给小明转了50,那么此时就有三个线程: ```java 线程一:小明原有100,小明转账五十,新余额应有50 线程二:小明原有100,小明转账五十,新余额应有50//由于不可避免因素导致线程二阻塞,最后进行 线程三:预期小明应有50,妈妈转账五十,小明应有余额100 ``` 那么我们来分析一下, ​ 首先线程一知道了银行卡原有一百,转账五十后发现没问题,确认 ​ 线程三预期和实际值一致,转账成功,此时小明应有100 ​ 线程三预期值、旧值一致,直接将余额赋值为50,此时小明痛哭 加了版本号呢,就是:1A,2B,3A,这时ABA问题得到解决。 ##### 2. 循环时间长开销大 自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升。pause指令有两个作用:第一,它可以延迟流水线执行指令,使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零;第二,它可以避免在退出循环的时候因内存顺序冲突而引起CPU流水线被清空,从而提高CPU的执行效率。 ##### 3. 只能保证一个共享变量的原子操作。 当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。还有一个巧取的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij=2a,然后用CAS来操作ij。从JDK1.5开始提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。 ##### CAS是如何保证原子操作的 JVM的CAS操作是利用了处理器提供的CMPXCHG指令实现的。CAS通过调用JNI(Java native Interface本地C方法)的代码实现的。程序会根据当前处理器的类型来决定是否为COMPXCHG指令添加lock前缀:多CPU的情况下加lock前缀 > cmpxchg是汇编指令 > 作用:比较并交换操作数. > 如:CMPXCHG r/m,r 将累加器AL/AX/EAX/RAX中的值与首操作数(目的操作数)比较,如果相等,第2操作数(源操作数)的值装载到首操作数,zf置1。如果不等, 首操作数的值装载到AL/AX/EAX/RAX并将zf清0 > 该指令只能用于486及其后继机型。第2操作数(源操作数)只能用8位、16位或32位寄存器。第1操作数(目地操作数)则可用寄存器或任一种存储器寻址方式。 ##### 引申出来的问题 AtomticInteger实现的原理: 核心代码:即CAS操作,AtomticXXX所有的类都是基于CAS(比较并交换实现的),即使用了Unsafe.compareAndSwapInt进行更新,其内部主要是维护了一个用volatile修饰的·int类型的value ```java private volatile int value; //比较并交换 public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); } ``` 我的理解:CAS能使其保证原子性,所以得到的AtmoticInteger也是一个原子数。 #### 6. volatile关键字 变量修饰符,可以使用内存屏障保证变量的内存可见性以及禁止指令重排序,但是volatile不能保证原子性。 ##### 6.1 内存可见性的原因 随着科技发展,为了提高处理器的执行速度,在处理器和内存之间增加了多级缓存来提升。但是由于引入了多级缓存,就存在缓存数据不一致问题。但是,对于volatile变量,当对volatile变量进行写操作的时候,JVM会向处理器发送一条lock前缀的指令,将这个缓存中的变量回写到系统主存中。 ##### 6.2 禁止指令重排序的原因 ```java public class Singleton { private volatile static Singleton singleton; private Singleton() {} public static Singleton getInstance() { if (singleton == null) { // 1 sychronized(Singleton.class) { if (singleton == null) { singleton = new Singleton(); // 2 } } } return singleton; } } ``` 上述代码是一个典型的单例模式, singleton = new Singleton(); 可分三步: 1. 给 singleton 分配内存 2. 调用 Singleton 的构造函数来初始化成员变量 3. 将 singleton 对象指向分配的内存空间(执行完这步 singleton 就为非 null 了) 假设虚拟机存在指令重排序优化,2、3调换位置。如果A线程率先进入同步代码块并先执行了3而没有执行2,此时因为singleton已经非null。这时候线程B到了1处,判断singleton非null并将其返回使用,因为此时Singleton实际上还未初始化,(并未有实际内容,我的理解),自然就会出错。sychronized可以解决内存可见性,但是不能解决重排序问题。 ##### 6.3 volatile关键字不能保证原子操作的原因 volatile的Integer自增i++,分成三步: 1. 读取volatile变量到local 2. 增加变量的值 3. 把local的值写回 这三部的JVM指令为: ```c mov 0xc(%r10),%r8d ; Load inc %r8d ; Increment mov %r8d,0xc(%r10) ; Store lock addl $0x0,(%rsp) ; StoreLoad Barrier //内存屏障以确保一些特定的操作顺序和影响一些数据的可见性 ``` 4. 从Load到store到内存屏障,一共4步,其中最后一步jvm让这个最新的变量的值在所有线程可见,也就是最后一步让所有的CPU内核都获得了最新的值,但**中间的几步(从Load到Store)**是不安全的,**中间如果其他的CPU修改了值将会丢失。** #### 7. Java多线程 ##### 7.1 线程与进程的区别 ![进程](https://img2018.cnblogs.com/blog/1629488/201906/1629488-20190622115300479-2129397443.png) 进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程——**资源分配的最小单位**。 线程:系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。线程——**程序执行的最小单位**。 也就是,进程可以包含多个线程,而线程是程序执行的最小单位。 #### 7.1.2 补充:死锁的概念 **死锁**: 指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,**这些永远在互相等待的进程称为死锁进程。** 死锁产生的四个必要条件(缺一不可): 1. 互斥条件:顾名思义,线程对资源的访问是排他性,当该线程释放资源后下一线程才可进行占用 2. 请求和保持:简单来说就是自己拿的不放手又等待新的资源到手。线程T1至少已经保持了一个资源R1占用,但又提出对另一个资源R2请求,而此时,资源R2被其他线程T2占用,于是该线程T1也必须等待,但又对自己保持的资源R1不释放。 3. 不可剥夺:在没有使用完资源时,其他线性不能进行剥夺 4. 循环等待:一直等待对方线程释放资源 我们可以根据死锁的四个必要条件破坏死锁的形成。 ##### 7.2 创建线程的方法 - 继承Thread类 - 实现Runable接口 - 线程池方式创建 - 通过Callable和Future创建线程 ​ 实现Runnable接口这种方式更受欢迎,因为这不需要继承Thread类。在应用设计中已经继承了别的对象的情况下,这需要多继承(而Java不支持多继承),只能实现接口。同时,线程池也是非常高效的,很容易实现和使用。 ##### 7.2.3 线程池创建线程 线程池,顾名思义,线程存放的地方。和数据库连接池一样,存在的目的就是为了较少系统开销,主要由以下几个特点: - 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗(主要)。 - 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。 - 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性。 Java提供四种线程池创建方式: 1. newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。 2. newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。 3. newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。 4. newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。 实际项目中,用的最多的就是ThreadPoolExecutor这个类,而《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 new ThreadPoolExecutor 实例的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 ![image-20200214201701085](C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200214201701085.png) 通过源码我们得知ThreadPoolExecutor继承自AbstractExecutorService,而AbstractExecutorService实现了ExecutorService ```java public class ThreadPoolExecutor extends AbstractExecutorService public abstract class AbstractExecutorService implements ExecutorService ``` ##### ThreadPoolExecutor 我们从 ThreadPoolExecutor入手多线程创建方式,先看一下线程池创建的最全参数 ```java public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; } ``` 参数说明如下: - **corePoolSize:**线程池的核心线程数,说白了就是,即便是线程池里没有任何任务,也会有corePoolSize个线程在候着等任务。 - **maximumPoolSize:**最大线程数,不管你提交多少任务,线程池里最多工作线程数就是maximumPoolSize。 - **keepAliveTime:**线程的存活时间。当线程池里的线程数大于corePoolSize时,如果等了keepAliveTime时长还没有任务可执行,则线程退出。 - **unit:**这个用来指定keepAliveTime的单位,比如秒:TimeUnit.SECONDS。 - **BlockingQueue:**一个阻塞队列,提交的任务将会被放到这个队列里。 - **threadFactory:**线程工厂,用来创建线程,主要是为了给线程起名字,默认工厂的线程名字:pool-1-thread-3。 - **handler:**拒绝策略,当线程池里线程被耗尽,且队列也满了的时候会调用。 对于BlockingQueue个人感觉还需要单独拿出来说一下 BlockingQueue:阻塞队列,有先进先出(注重公平性)和先进后出(注重时效性)两种,常见的有两种阻塞队列:**ArrayBlockingQueue**和**LinkedBlockingQueue** 队列的数据结构大致如图: ![img](https://pic002.cnblogs.com/images/2010/161940/2010112414472791.jpg) 队列一端进入,一端输出。而当队列满时,阻塞。BlockingQueue核心方法:1. 放入数据 put2. 获取数据take。常见的Queue: ##### 7.2.3.1 ArrayBlockingQueue 基于数组实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。 一段代码来验证一下: ```java package map; import java.util.concurrent.*; public class MyTestMap { // 定义阻塞队列大小 private static final int maxSize = 5; public static void main(String[] args){ ArrayBlockingQueue queue = new ArrayBlockingQueue(maxSize); new Thread(new Productor(queue)).start(); new Thread(new Customer(queue)).start(); } } class Customer implements Runnable { private BlockingQueue queue; Customer(BlockingQueue queue) { this.queue = queue; } @Override public void run() { this.cusume(); } private void cusume() { while (true) { try { int count = (int) queue.take(); System.out.println("customer正在消费第" + count + "个商品==="); // 只是为了方便观察输出结果 Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } } } class Productor implements Runnable { private BlockingQueue queue; private int count = 1; Productor(BlockingQueue queue) { this.queue = queue; } @Override public void run() { this.product(); } private void product() { while (true) { try { queue.put(count); System.out.println("生产者正在生产第" + count + "个商品"); count++; } catch (InterruptedException e) { e.printStackTrace(); } } } } //输出如下 /** 生产者正在生产第1个商品 生产者正在生产第2个商品 生产者正在生产第3个商品 生产者正在生产第4个商品 生产者正在生产第5个商品 customer正在消费第1个商品=== */ ``` ##### 7.2.3.2 LinkedBlockingQueue 基于链表的阻塞队列,内部也维护了一个数据缓冲队列。需要我们注意的是如果构造一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。 ##### 7.2.3.3 **LinkedBlockingQueue**和**ArrayBlockingQueue**的主要区别 - ArrayBlockingQueue的初始化必须传入队列大小,LinkedBlockingQueue则可以不传入 - ArrayBlockingQueue用一把锁控制并发,LinkedBlockingQueue俩把锁控制并发,锁的细粒度更细。即前者生产者消费者进出都是一把锁,后者生产者生产进入是一把锁,消费者消费是另一把锁。 - ArrayBlockingQueue采用数组的方式存取,LinkedBlockingQueue用Node链表方式存取 #### 7.3.3 handler拒绝策略 java提供了4种丢弃处理的方法,当然你也可以自己实现,主要是要实现接口:RejectedExecutionHandler中的方法 - AbortPolicy:不处理,直接抛出异常。 - CallerRunsPolicy:只用调用者所在线程来运行任务,即提交任务的线程。 - DiscardOldestPolicy:LRU策略,丢弃队列里最近最久不使用的一个任务,并执行当前任务。 - DiscardPolicy:不处理,丢弃掉,不抛出异常。 ##### 7.2.4 线程池五种状态 ```java private static final int RUNNING = -1 << COUNT_BITS; private static final int SHUTDOWN = 0 << COUNT_BITS; private static final int STOP = 1 << COUNT_BITS; private static final int TIDYING = 2 << COUNT_BITS; private static final int TERMINATED = 3 << COUNT_BITS; ``` - RUNNING:在这个状态的线程池能判断接受新提交的任务,并且也能处理阻塞队列中的任务 - SHUTDOWN:处于关闭的状态,该线程池不能接受新提交的任务,但是可以处理阻塞队列中已经保存的任务,在线程处于RUNNING状态,调用shutdown()方法能切换为该状态。 - STOP:线程池处于该状态时既不能接受新的任务也不能处理阻塞队列中的任务,并且能中断现在线程中的任务。当线程处于RUNNING和SHUTDOWN状态,调用shutdownNow()方法就可以使线程变为该状态 - TIDYING:在SHUTDOWN状态下阻塞队列为空,且线程中的工作线程数量为0就会进入该状态,当在STOP状态下时,只要线程中的工作线程数量为0就会进入该状态。 - TERMINATED:在TIDYING状态下调用terminated()方法就会进入该状态。可以认为该状态是最终的终止状态。 回到线程池创建ThreadPoolExecutor,我们了解了这些参数,再来看看ThreadPoolExecutor的内部工作原理: ![image-20200215110131968](C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200215110131968.png) 1. 判断核心线程是否已满,是进入队列,否:创建线程 2. 判断等待队列是否已满,是:查看线程池是否已满,否:进入等待队列 3. 查看线程池是否已满,是:拒绝,否创建线程 ##### 7.2.3.4深入理解ThreadPoolExecutor 进入execute方法可以看到: ```java public void execute(Runnable command) { if (command == null) throw new NullPointerException(); int c = ctl.get(); //判断当前活跃线程数是否小于corePoolSize,如果小于,则调用addWorker创建线程执行任务 if (workerCountOf(c) < corePoolSize) { if (addWorker(command, true)) return; c = ctl.get(); } //如果不小于corePoolSize,则将任务添加到workQueue队列。 if (isRunning(c) && workQueue.offer(command)) { int recheck = ctl.get(); if (! isRunning(recheck) && remove(command)) reject(command); else if (workerCountOf(recheck) == 0) addWorker(null, false); } //如果放入workQueue失败,则创建线程执行任务,如果这时创建线程失败(当前线程数不小于maximumPoolSize时),就会调用reject(内部调用handler)拒绝接受任务。 else if (!addWorker(command, false)) reject(command); } ``` addWorker方法: - 创建Worker对象,同时也会实例化一个Thread对象。在创建Worker时会调用threadFactory来创建一个线程。 - 启动启动这个线程 ##### 7.2.3.5 线程池中ctl属性的作用是什么? ctl属性包含两个概念: ```java private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); private static int ctlOf(int rs, int wc) { return rs | wc; } ``` 1. workerCount:表明当前有效的线程数 2. runState:表明当前线程池的状态,是否处于Running,Shutdown,Stop,Tidying, 简单来说就是**ctl中是高3位作为状态值,低28位作为线程总数值来进行存储。** 我们点击workerCount即工作状态记录值,以RUNNING为例,RUNNING = -1 << COUNT_BITS;,即-1无符号左移COUNT_BITS位,进一步我们得知COUNT_BITS位29,因为Integer位数为31位(*2的五次方减一*) ```java private static final int COUNT_BITS = Integer.SIZE - 3; ``` 既然是29位那么就是Running的值为 ``` 1110 0000 0000 0000 0000 0000 0000 0000 ||| 31~29位 ``` 那低28位呢,就是记录当前线程的总线数啦 ```java // Packing and unpacking ctl private static int runStateOf(int c) { return c & ~CAPACITY; } private static int workerCountOf(int c) { return c & CAPACITY; } private static int ctlOf(int rs, int wc) { return rs | wc; } ``` 从上述代码可以看到`workerCountOf`这个函数传入ctl之后,是通过ctl&CAPACITY操作来获取当前运行线程总数的。也就是RunningState|WorkCount&CAPACITY,算出来的就是低28位的值。因为CAPACITY得到的就是高3位(29-31位)位0,低28位(0-28位)都是1,所以得到的就是ctl中低28位的值。 而`runStateOf`这个方法的话,算的就是RunningState|WorkCount&CAPACITY,高3位的值,因为CAPACITY是CAPACITY的取反,所以得到的就是高3位(29-31位)为1,低28位(0-28位)为0,所以通过&运算后,所得到的值就是高3为的值。 #### 8. 线程间通信的几种方式 提及多线程又不得不提及多线程通信的机制。首先,要短信线程间通信的模型有两种:共享内存和消息传递,以下方式都是基本这两种模型来实现的。我们来基本一道面试常见的题目来分析: > **题目:有两个线程A、B,A线程向一个集合里面依次添加元素"abc"字符串,一共添加十次,当添加到第五次的时候,希望B线程能够收到A线程的通知,然后B线程执行相关的业务操作**。 1. **使用 volatile 关键字定义一个全局变量** ```java package thread; /** * * @author hxz * @description 多线程测试类 * @version 1.0 * @data 2020年2月15日 上午9:10:09 */ public class MyThreadTest { public static void main(String[] args) throws Exception { notifyThreadWithVolatile(); } /** * 定义一个测试 */ private static volatile boolean flag = false; /** * 计算I++,当I==5时,通知线程B * @throws Exception */ private static void notifyThreadWithVolatile() throws Exception { Thread thc = new Thread("线程C"){ @Override public void run() { for (int i = 0; i < 10; i++) { if (i == 5) { flag = true; try { Thread.sleep(500L); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } break; } System.out.println(Thread.currentThread().getName() + "====" + i); } } }; Thread thd = new Thread("线程D") { @Override public void run() { while (true) { // 防止伪唤醒 所以使用了while while (flag) { System.out.println(Thread.currentThread().getName() + "收到通知"); System.out.println("do something"); try { Thread.sleep(500L); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } return ; } } } }; thd.start(); Thread.sleep(1000L); thc.start(); } } ``` 2. **锁机制** 3. **信号机制** ```java public class test { public static void main(String[] args){ Integer a=1,b=1,c=500,d=500; System.out.println(a==b); System.out.println(c==d); }} ``` Integer类似于String,当Integer的值在范围[-128,127]时,Integer不会生成新的对象,会直接从缓存中获取对象的引用。当超出这个范围后,会生成新的Integer对象。 #### 9. 字符串存放的位置 ```java class A { private String a = “aa”; public boolean methodB() { String b = “bb”; final String c = “cc”; } } ``` **a是类中的成员变量,存放在堆区** **b、c都是方法中的局部变量,存放在栈区** 堆区:只存放类对象,线程共享; 方法区:又叫静态存储区,存放class文件和静态数据,线程共享; 栈区:存放方法局部变量,基本类型变量区、执行环境上下文、操作指令区,线程不共享; #### 10. 重载和重写的区别 重载(overload)和重写(override)的区别: 重载就是同一个类中,有多个方法名相同,但参数列表不同(包括参数个数和参数类型),与返回值无关,与权限修饰符也无关。调用重载的方法时通过传递给它们不同的参数个数和参数类型来决定具体使用哪个方法,这叫多态。 重写就是子类重写基类的方法,方法名,参数列表和返回值都必须相同,否则就不是重写而是重载。权限修饰符不能小于被重写方法的修饰符。重写方法不能抛出新的异常或者是比被重写方法声明更加宽泛的检查型异常。 #### 11. 为什么Java是解释性语言 java程序在运行时字节码才会被jvm翻译成机器码,所以说java是解释性语言 类总是有一个构造函数(可能由java编译器自动提供) #### 12. Mybatis执行流程 1. 获取sqlSessionFactory对象: 根据配置文件(全局,sql映射)初始化出Configuration对象 解析文件的每一个信息保存在Configuration中,返回包含Configuration的DefaultSqlSession; 注意:MappedStatement:代表一个增删改查的详细信息 2. 获取sqlSession对象 返回一个DefaultSQlSession对象,包含Executor和Configuration; 这一步会创建Executor对象; Executor(根据全局配置文件中的defaultExecutorType创建出对应的Executor) 3. 获取接口的代理对象(MapperProxy) DefaultSqlSession.getMapper():拿到Mapper接口对应的MapperProxy; 使用MapperProxyFactory创建一个MapperProxy的代理对象 代理对象里面包含了,DefaultSqlSession(Executor) 而MyBatis 中 Mapper 和 SQL 语句的绑定正是通过动态代理来完成的,此时我们就已经拿到了具体的SQL语句是怎么写的了。 4. 执行增删改查方法 1)调用DefaultSqlSession的增删改查(Executor); 2)会创建一个StatementHandler对象。(同时也会创建出ParameterHandler和ResultSetHandler) 3)调用StatementHandler预编译参数以及设置参数值; 使用ParameterHandler来给sql设置参数 4)调用StatementHandler的增删改查方法; 5)ResultSetHandler封装结果返回 #### 13. Mybatis缓存 mybatis 也提供了对缓存的支持, 分为一级缓存和二级缓存。 但是在默认的情况下, 只开启一级缓存(一级缓存是对同一个 SqlSession 而言的)。 **一级缓存:** - 在同一个 SqlSession 中, Mybatis 会把执行的方法和参数通过算法生成缓存的键值, 将键值和结果存放在一个 基于 PerpetualCache 的 HashMap (key为hashcode+statementId+sql语句。Value为查询出来的结果集映射成的java对象。)本地缓存中, 如果后续的键值一样, 则直接从 HashMap 中获取数据;默认打开一级缓存。 - 不同的 SqlSession 之间的缓存是相互隔离的;作用域为SqlSession - 用一个 SqlSession, 可以通过配置使得在查询前清空缓存; - 任何的 UPDATE, INSERT, DELETE 语句都会清空缓存。 **二级缓存:** 二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在它的映射文件中配置 ; #### 14. Mybatis用到的设计模式 Mybatis至少遇到了以下的设计模式的使用: 1. **Builder模式** : 例如 `SqlSessionFactoryBuilder`、`XMLConfigBuilder`、`XMLMapperBuilder`、`XMLStatementBuilder`、`CacheBuilder`; 2. **工厂模式** : 例如`SqlSessionFactory`、`ObjectFactory`、`MapperProxyFactory`; 3. **单例模式** :例如`ErrorContext`和`LogFactory`; 4. **代理模式** :Mybatis实现的核心,比如`MapperProxy`、`ConnectionLogger`,用的jdk的动态代理;还有`executor.loader`包使用了cglib或者javassist达到延迟加载的效果; 5. **组合模式** :例如`SqlNode`和各个子类`ChooseSqlNode`等; 6. **模板方法模式** : 例如`BaseExecutor`和`SimpleExecutor`,还有`BaseTypeHandler`和所有的子类例如`IntegerTypeHandler`; 7. **适配器模式** : 例如Log的Mybatis接口和它对jdbc、log4j等各种日志框架的适配实现; 8. **装饰者模式** : 例如`cache`包中的`cache.decorators`子包中等各个装饰者的实现; 9. **迭代器模式** : 例如迭代器模式`PropertyTokenizer`; #### 15. ConcurrentHashMap 1.7和1.8的区别 ##### 1、整体结构 1.7:Segment + HashEntry + Unsafe 1.8: 移除Segment,使锁的粒度更小,Synchronized + CAS + Node + Unsafe ##### 2、put() 1.7:先定位Segment,再定位桶,put全程加锁,没有获取锁的线程提前找桶的位置,并最多自旋64次获取锁,超过则挂起。 1.8:由于移除了Segment,类似HashMap,可以直接定位到桶,拿到first节点后进行判断,1、为空则CAS插入;2、为-1则说明在扩容,则跟着一起扩容;3、else则加锁put(类似1.7) ##### 3、get() 基本类似,由于value声明为volatile,保证了修改的可见性,因此不需要加锁。 ##### 4、resize() 1.7:跟HashMap步骤一样,只不过是搬到单线程中执行,避免了HashMap在1.7中扩容时死循环的问题,保证线程安全。 1.8:支持并发扩容,HashMap扩容在1.8中由头插改为尾插(为了避免死循环问题),ConcurrentHashmap也是,迁移也是从尾部开始,扩容前在桶的头部放置一个hash值为-1的节点,这样别的线程访问时就能判断是否该桶已经被其他线程处理过了。 ##### 5、size() 1.7:很经典的思路:计算两次,如果不变则返回计算结果,若不一致,则锁住所有的Segment求和。 1.8:用baseCount来存储当前的节点个数,通过累加baseCount和CounterCell数组中的数量,即可得到元素的总个数; #### 16. 地址栏输入URL发生了什么![image-20200218133836156](C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200218133836156.png) #### 17. 组合和聚合的区别: 组合和聚合是有很大区别的,这个区别不是在形式上,而是在本质上: 比如A类中包含B类的一个引用b,当**A类的一个对象消亡时,b这个引用所指向的对象也同时消亡**(没有任何一个引用指向它,成了垃圾对象),这种情况叫做组合, 反之b所指向的对象还会有另外的引用指向它,这种情况叫聚合。 #### 18. OSI七层模型 1. 应用层 网络服务与最终用户的一个接口。 协议有:**HTTP FTP TFTP SMTP SNMP DNS TELNET HTTPS POP3 DHCP** 2. 表示层 数据的表示、安全、压缩。(在五层模型里面已经合并到了应用层) 格式有,**JPEG、ASCll、DECOIC、加密格式等** 3. 会话层 建立、管理、终止会话。(在五层模型里面已经合并到了应用层) **对应主机进程,指本地主机与远程主机正在进行的会话** 4. 传输层 定义传输数据的协议端口号,以及流控和差错校验。 协议有:**TCP UDP,数据包一旦离开网卡即进入网络传输层** 5. 网络层 进行逻辑地址寻址,实现不同网络之间的路径选择。 协议有:**ICMP IGMP IP(IPV4 IPV6)** 6. 数据链路层 建立逻辑连接、进行硬件地址寻址、差错校验等功能。(由底层网络定义协议) 将比特组合成字节进而组合成帧,用MAC地址访问介质,错误发现但不能纠正。 7. 物理层 建立、维护、断开物理连接。(由底层网络定义协议) ![img](https://mmbiz.qpic.cn/mmbiz_gif/bGribGtYC3mJebROaNfpaTGxauaAjuwU4HUXPOHvlVnyEk7pyrWicosheXtjic2mjqvG8Y3VgpASuj7XQrOvKkHfg/640?wx_fmt=gif&tp=webp&wxfrom=5&wx_lazy=1) ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190105164025264.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQxOTIzNjIy,size_16,color_FFFFFF,t_70) #### 19. 讲一下CMS垃圾回收器 CMS是concurrent Mark sweep的简写,并发标记清除,相较于其他垃圾回收器有以下特点: 1. 回收速度快,系统停顿时间少(Stop The World) 2. 采用标记清除算法,且只能在老年代进行回收 3. 过程分为四步:1) 初始标记 2) 并发标记 3)重新标记 4)并发清除 4. 大部分web应用使用在老年代使用CMS进行垃圾回收、新生代使用Parllernew #### 20. Spring事务管理原理? 通过这样一个动态代理对所有需要事务管理的Bean进行加载,并根据配置在invoke方法中对当前调用的 方法名进行判定,并在method.invoke方法前后为其加上合适的事务管理代码,这样就实现了Spring式的事务管理。Spring中的AOP实 现更为复杂和灵活,不过基本原理是一致的。 #### 21. JDK动态代理和GClib动态代理 ##### 21.1、JDK动态代理具体实现原理: - 通过实现InvocationHandlet接口创建自己的调用处理器; - 通过为Proxy类指定ClassLoader对象和一组interface来创建动态代理; - 通过反射机制获取动态代理类的构造函数,其唯一参数类型就是调用处理器接口类型; - 通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数参入; JDK动态代理是面向**接口**的代理模式,如果被代理目标没有接口那么Spring也无能为力,Spring通过Java的反射机制生产被代理接口的新的匿名实现类,重写了其中AOP的增强方法。 ##### 21.2、CGLib动态代理: CGLib是一个强大、高性能的Code生产类库,可以实现运行期动态扩展java类,Spring在运行期间通过 CGlib继承要被动态代理的类,重写父类的方法,实现AOP面向切面编程呢。 ##### 21.3、两者对比: JDK动态代理是面向接口的。 CGLib动态代理是通**过字节码底层继承要代理类**来实现。 ##### 21.4、使用注意: 如果要被代理的对象是个实现类,那么Spring会使用JDK动态代理来完成操作(Spirng默认采用JDK动态代理实现机制); 如果要被代理的对象不是个实现类那么,Spring会强制使用CGLib来实现动态代理。 #### 22. bean的生命周期 1. bean实例化 2. bean属性注入 3. 调用setBeanName方法 4. 调用setBeanFactory方法 5. 调用setApplicationContext方法 6. 调用初始化方法 7. 使用bean 8. 销毁bean,调用destory方法 #### 23. 类加载机制 ![img](https://pics7.baidu.com/feed/eaf81a4c510fd9f9a14099a4c13f2f2e2934a444.jpeg?token=e2880e42daad3c92998175106108d106&s=3E2C702394A8C1011CF511CE0100E0B1) #### 24. I/O多路复用 #### 25. 讲一下请求servlet的过程 1. client 发起一个请求request,首先client发起DNS解析先拿到服务器的ip:通过解析本地host文件,如没有,则将域名发到上级DNS,一直到能解析出ip。 2. 发起网络通信,建立连接:三次握手 3. 发送报文,到服务器然后服务器解析 4. web服务器将请求转到servlet容器,web服务器将请求转到servlet容器 5. servlet容器查找sertvlet实例,若没有,则实例化、servlet容器查找sertvlet实例,若没有,则实例化 6. 响应请求,servlet实例调用service()方法处理请求响应请求,servlet实例调用service()方法处理请求 如果是get 请求则走doGet()方法,如果是post请求走doPost()方法。 (以Spring为例来说,会进入FrameworkServlet---->DispatcherServlet分发---->各种拦截器—>业务处理----> 返回客户端) 7. 终止,调用destroy()方法销毁 servlet,一般servlet容器被关闭后,servlet才会被销毁,也可以主动发动发起servlet销毁。 #### 26. Java I/O ##### 26.1 BIO 即阻塞I/O,一个线程执行一个请求,如果请求数据量较大线程就会一直占用着,又或者请求什么也不做,也是会占用一个线程的,这样当客户端请求数量变多时,服务端线程数也跟着变多,最终就会导致服务端CPU崩溃 ##### 26.2 NIO NIO主要有三大核心部分:**Channel(通道),Buffer(缓冲区), Selector**(选择器)。传统IO基于字节流和字符流进行操作,而NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。 NIO和传统IO(一下简称IO)之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 1. **Channel** 国内大多翻译成“通道”。Channel和IO中的Stream(流)是差不多一个等级的。只不过Stream是单向的,譬如:InputStream, OutputStream.而Channel是双向的,既可以用来进行读操作,又可以用来进行写操作。 NIO中的Channel的主要实现有: - FileChannel 文件I/O - DatagramChannel UDP传输 - SocketChannel TCPClient传输 - ServerSocketChannel TCPServer传输 2. **Buffer** NIO中的关键Buffer实现有:ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer,八个基本类型除了Boolbean都是,用的最多的是ByteBuffer 3. **Selector** 运行单线程处理多个Channel,如果你的应用打开了多个通道,但每个连接的流量都很低,使用Selector就会很方便。例如在一个聊天服务器中。要使用Selector, 得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新的连接进来、数据接收等。 ##### 26.3 AIO AIO是java中IO模型的一种,作为NIO的改进和增强随JDK1.7版本更新被集成在JDK的nio包中,因此AIO也被称作是**NIO2.0**。区别于传统的BIO(Blocking IO,同步阻塞式模型,JDK1.4之前就存在于JDK中,NIO于JDK1.4版本发布更新)的阻塞式读写,AIO提供了从建立连接到读、写的全异步操作。AIO可用于异步的文件读写和网络通信。 AIO 是彻底的异步通信。 NIO 是同步非阻塞通信。 #### 同步异步与阻塞非阻塞区别 1. **同步:执行一个操作之后,等待结果,然后才继续执行后续的操作。** 2. **异步:执行一个操作后,可以去执行其他的操作,然后等待通知再回来执行刚才没执行完的操作** 3. **阻塞:进程给CPU传达一个任务之后,一直等待CPU处理完成,然后才执行后面的操作** 4. **非阻塞:进程给CPU传达任务后,继续处理后续的操作,隔断时间再来询问之前的操作是否完成。这样的过程其实也叫轮询。** ​ 同步IO和异步IO的区别就在于:数据拷贝的时候进程是否阻塞! ​ 阻塞IO和非阻塞IO的区别就在于:应用程序的调用是否立即返回! ​ 阻塞、非阻塞、多路IO复用,都是同步IO,异步必定是非阻塞的 #### 27. Liunx下的I/O模型 1. 阻塞I/O 2. 非阻塞I/O 3. I/O多路复用 4. 信号驱动I/O 5. 异步I/O ##### 27.1 阻塞I/O 同Java I/O一样,进程会一直阻塞,直到数据拷贝完成 ##### 27.2 非阻塞I/O 非阻塞IO通过进程反复调用IO函数(多次系统调用,并马上返回);在数据拷贝的过程中,进程是阻塞的; ##### 27.3 I/O多路复用(重点) 简介:主要是select和epoll;对一个IO端口,两次调用,两次返回,比阻塞IO并没有什么优越性;关键是能实现同时对多个IO端口进行监听; IO多路复用模型是建立在内核提供的多路分离函数select基础之上的,使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。 用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在**同一个线程内同时处理多个IO请求的目的**。 结合Redis的I/O多路复用:Redis可以并发接受网络请求。 ##### 27.4 信号驱动I/O 两次调用,两次返回; 首先我们允许套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。 ##### 27.5 异步I/O 简介:数据拷贝的时候进程无需阻塞。 当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者的输入输出操作 同步IO引起进程阻塞,直至IO操作完成。 异步IO不会引起进程阻塞。 IO复用是先通过select调用阻塞。 #### 28. 双亲委派模型 如果不想打破双亲委派模型,就重写ClassLoader类中的findClass()方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。 而如果想打破双亲委派模型则需要重写loadClass()方法(当然其中的坑也不会少)。典型的打破双亲委派模型的框架和中间件有tomcat与osgi,如果相对java的类加载过程有更深入的了解学习这两个框架的源码会是不错的选择。 #### 29. 说一说动态代理 动态代理即为其他对象提供一个代理以控制对某个对象的访问。代理类主要负责为委托了(真实对象)预处理消息、过滤消息、传递消息给委托类,代理类不现实具体服务,而是利用委托类来完成服务,并将执行结果封装处理。 ##### 29.1 AOP 代理的两种实现: - jdk是代理接口,私有方法必然不会存在在接口里,所以就不会被拦截到; - cglib是子类,private的方法照样不会出现在子类里,也不能被拦截。 ##### 29.2 JDK动态代理 1. 通过实现 InvocationHandler 接口创建自己的调用处理器; 2. 通过为 Proxy 类指定 ClassLoader 对象和一组 interface 来创建动态代理类; 3. 通过反射机制获得动态代理类的构造函数,其唯一参数类型是调用处理器接口类型; 4. 通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数被传入。 ##### 29.3 GCLIB代理 cglib(Code Generation Library)是一个强大的,高性能,高质量的Code生成类库。它可以在运行期扩展Java类与实现Java接口。 - cglib封装了asm,可以在运行期动态生成新的class(子类)。 - cglib用于AOP,jdk中的proxy必须基于接口,cglib却没有这个限制。 ##### 29.4 二者区别 java动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。而cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。 1. 如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP 2. 如果目标对象实现了接口,可以强制使用CGLIB实现AOP 3. 如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换 #### 30. 一个Java进程至少会启动几个线程? ```java import java.lang.management.ManagementFactory; import java.lang.management.ThreadInfo; import java.lang.management.ThreadMXBean; /** * @author haohh.zhang * @date 2019-12-20 */ public class ThreadNum { public static void main(String[] args) { //构建 ThreadMXBean ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); //获取所有存活的线程信息 ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false); for (ThreadInfo threadInfo : threadInfos) { //打印出线程id 以及 线程name System.out.println(threadInfo.getThreadId() + " - " + threadInfo.getThreadName()); } } } ``` 输出结果: ```java /Library/Java/JavaVirtualMachines/jdk1.8.0_111.jdk/... 4 - Signal Dispatcher 3 - Finalizer 2 - Reference Handler 1 - main Process finished with exit code 0 ``` - main : 主线程 - Reference Handler : 处理引用对象本身(软、弱、虚引用)的线程 - Finalizer : 调用对象的finalize方法的线程,也就是说垃圾回收的守护线程 - Signal Dispatcher : 接受处理各种信号的线程 #### 31. 数据库范式 第一范式 属性的原子性 第二范式 属性完全依赖于主键 第三范式(3NF)要求一个数据库表中不包含已在其它表中已包含的非主关键字信息 BCNF 范式: 所有的非主属性对每一个码都是完全函数依赖 (暗含 主关键字里面可能有多个码可以将实体区分) 所有的主属性对每一个不包含它的码也是完全函数依赖(即所选码与未选择的码之间也是完全函数依赖的) 没有任何属性完全函数依赖于非码的任何一组属性(即非主属性之间不能函数依赖) #### 32. SpringBoot启动流程 1. 设置WebApplicationType(Web应用类型) 2. 准备上下文环境 3. 读取属性文件 4. 创建并配置上下文对象 5. 刷新上下文对象 6. 启动内置服务器 7. 启动完成 ![image-20200222151047461](C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200222151047461.png) | 结构类型 | 结构存储方式 | 结构的读写方式 | 常见使用场景 | | :------: | :----------------------------------------------------------: | :----------------------------------------------------------: | :--------------------------: | | String | 可以是字符串、整数或者浮点数等基本类型 | 对整个字符串或者字符串的其中一部分执行操对 | 存储 json字符串,生成自增 id | | List | 链表上的每个节点都包含了一个字符串 | 从链表的两端推入或者弹出元素:根据偏移量对链表进行修剪。读取单个或者多个元素;根据值来查找或者移除元素 | 消息队列、最新内容 | | Hash | 包含键值对的无序散列表 | 添加、获取、移除单个键值对:获取所有键值对 | 对象类数据 | | Set | 据值来查找或者移除元素并且被包含的每个字符事都是独一无的、各不相同 | 添加、获取、移除单个元素;检查-一个元素是否存在于某个集合中;计算交集、并集、差集;从集合里卖弄随机获取元素 | 共同好友列表、附近的人 | | Zset | 字符串成员(member)与浮点数分值(score)之间的有序映射,元素的排列顺序由分值的大小决定 | 添加、获取、删除单个元素;根据分值范围(range)或者成员来获取元素 | 排行榜 | #### 32. Spring生命周期 以XML配置方式,整个生命周期大致可分为以下几步 1. 初始化容器 2. Bean属性注入、更改以及初始化 3. 容器、Bean初始化成功,开始调用 4. 关闭容器、销毁Bean ##### 32.1 初始化容器 该部分即为Bean提供一个容器环境,先有容器再有Bean,否则Bean将无从存放 1. 准备更新容器 2. ClassPathXmlApplicationContext启动,为解析XML做准备 3. 加载XML配置文件 4. 调用BeanFactoryPostProcessor实现类构造器 5. BeanFactoryPostProcessor调用postProcessBeanFactory方法 6. 调用BeanPostProcessor实现类构造器 7. 调用InstantiationAwareBeanPostProcessorAdapter实现类构造器 8. DefaultListableBeanFactory 9. InstantiationAwareBeanPostProcessor调用postProcessBeforeInstantiation方法 10. 容器初始化成功! ##### 32.2 Bean属性注入、更改以及初始化 1. 调用Bean的构造器进行实例化 2. InstantiationAwareBeanPostProcessor调用postProcessPropertyValues方法 3. 属性注入 4. 调用BeanNameAware.setBeanName 5. 调用BeanFactoryAware.setBeanFactory() 6. BeanPostProcessor接口方法postProcessBeforeInitialization对属性进行更改 7. 调用InitializingBean.afterPropertiesSet() 8. 调用bean的init-method属性指定的初始化方法 9. BeanPostProcessor接口方法postProcessAfterInitialization对属性进行更改 10. InstantiationAwareBeanPostProcessor调用postProcessAfterInitialization方法 11. 整个容器加载Bean完成,Bean可以使用了! ##### 32.3 Bean的使用 这部分主要依据使用者而言,暂不描述 ##### 32.4 关闭容器、销毁Bean 1. 调用DiposibleBean.destory() 2. 调用bean的destroy-method属性指定的初始化方法 整个Bean从生产到灭亡走完了全部流程 整个Spring生命周期可分为四个部分 1. Bean自身方法调用 2. Bean级生命周期接口方法调用 3. 容器即生命方法调用 4. 工厂后处理接口方法调用 ##### **32.5 Bean的生命周期:** 1. Spring 容器 从 XML 文件中读取 bean 的定义,并实例 化 bean。 2. Spring 根据 bean 的定义填充所有的属性。 3. 如果 bean 实现了 BeanNameAware 接口,Spring 传 递 bean 的 ID 到 setBeanName 方法。 4. 如果 Bean 实现了 BeanFactoryAware 接口, Spring 传递 beanfactory 给 setBeanFactory 方法。 5. 如果有任何与 bean 相关联的 BeanPostProcessors, Spring 会在 postProcesserBeforeInitialization()方法 内调用它们。 6. 如果 bean 实现 IntializingBean 了,调用它的 afterPropertySet 方法,如果 bean 声明了初始化方 法,调用此初始化方法。 7. 如果有 BeanPostProcessors 和 bean 关联,这些 bean 的 postProcessAfterInitialization() 方法将被调用。 8. 如果 bean 实现了 DisposableBean,它将调用 destroy()方法 #### 33. Spring如何解决bean的循环依赖 所谓的循环依赖是指在运行状态下,a调用b,b调用a的情况,即两个或者多个bean互相调用的情况。spring将循环依赖分成三部分:setter循环依赖和容器循环依赖以及构造器循环依赖。 ##### 33.1 容器循环依赖: 表示通过构造器注人构成的循环依赖,此依赖是无法解决的,只能抛出BeanCurentlyInCreationException异常表示循环依赖,对于创建完毕的bean将从当前创建bean池中删除、被清理掉。 ##### 33.2 setter循环依赖 表示通过setter方法注入构成的循环依赖。而对于setter注入造成的循环依赖是通过提前暴露钢完成构造器注入但未完成其他步骤(如setter注入)的bean来完成的,而且只能解决单力作用域bean的循环依赖。即提前暴露一个单例工厂方法,从而使其他bean能够引用到该bean ##### 33.3 构造器循环依赖 对于“prototype" 作用域bean, Spring 容器无法完成依赖注人,因为Spring容器不进行缓存"prototype”作用域的bean,因此无法提前暴露-个创建中的bean。对于“singleton" 作用域bean,可以通过“setAllowCircularReferences(false ); "来禁用循环 #### 34. Bean的加载过程 - 转换对应的beanName - 尝试从缓存中加载单例,也就是说单例在Spring的同一个容器只会被创建一次,后续在获取bean的话,就直接从单例缓存中获取 - bean的实例化 - 原型模式的以来坚持 - 检测parentBeanFactory - 将存储XML配置文件的GernericBeanDefinition转换为RootBeanDefinition,因为从XML配置文件中读取到的Bean信息是存储在前者中的,但是后续Bean处理都是针对于后者的,所以要进行一个转换 - 寻找依赖 - 针对不同的scope进行bean的创建 - 类型转换 #### 35. 一致性哈希? 随着互联网业务慢慢增大,单机的redis缓存已经只撑不住了 ,因此考虑redis集群,然而高并发集群的数据一致性性问题,是一个难以解决的问题,由于缓存数据量很大,Redis快正是快在其基于内存的快速存取。 redis存在的问题,所有的缓存数据是分散存放在各个Redis节点上的,通过客户端实现路由算法,来将某个key路由到某个具体的节点。下面简单的了解下 hash算法 ![img](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8xNTU3OTI1MC1mNDlmZGRhZTI2MWVjNzAyLnBuZz9pbWFnZU1vZ3IyL2F1dG8tb3JpZW50L3N0cmlwJTdDaW1hZ2VWaWV3Mi8yL3cvNjQ3L2Zvcm1hdC93ZWJw?x-oss-process=image/format,png) 一致性hash是一个**0-2^32**的闭合圆,(拥有2^23个桶空间,每个桶里面可以存储很多数据,可以理解为s3的存储桶)所有节点存储的数据都是不一样的。计算一致性哈希是采用的是如下步骤: 1. 对节点进行hash,通常使用其节点的ip或者是具有唯一标示的数据进行hash(ip),将其值分布在这个闭合圆上。 2. 将存储的key进行hash(key),然后将其值要分布在这个闭合圆上。 3. 从hash(key)在圆上映射的位置开始顺时针方向找到的一个节点即为存储key的节点。如果到圆上的0处都未找到节点,那么0位置后的顺时针方向的第一个节点就是key的存储节点。 **一致性hash的特性** - 单调性(Monotonicity),单调性是指如果已经有一些请求通过哈希分派到了相应的服务器进行处理,又有新的服务器加入到系统中时候,应保证原有的请求可以被映射到原有的或者新的服务器中去,而不会被映射到原来的其它服务器上去。 这个通过上面新增服务器ip5可以证明,新增ip5后,原来被ip1处理的user6现在还是被ip1处理,原来被ip1处理的user5现在被新增的ip5处理。 - 分散性(Spread):分布式环境中,客户端请求时候可能不知道所有服务器的存在,可能只知道其中一部分服务器,在客户端看来他看到的部分服务器会形成一个完整的hash环。如果多个客户端都把部分服务器作为一个完整hash环,那么可能会导致,同一个用户的请求被路由到不同的服务器进行处理。这种情况显然是应该避免的,因为它不能保证同一个用户的请求落到同一个服务器。所谓分散性是指上述情况发生的严重程度。好的哈希算法应尽量避免尽量降低分散性。 一致性hash具有很低的分散性 - 平衡性(Balance):平衡性也就是说负载均衡,是指客户端hash后的请求应该能够分散到不同的服务器上去。一致性hash可以做到每个服务器都进行处理请求,但是不能保证每个服务器处理的请求的数量大致相同, #### **36. RedisCluster哈希槽为什么是2^14个(16384)个?** **1.如果槽位为65536,发送心跳信息的消息头达8k,发送的心跳包过于庞大。** 在消息头中,最占空间的是 myslots[CLUSTER_SLOTS/8]。当槽位为65536时,这块的大小是: 65536÷8=8kb因为每秒钟,redis节点需要发送一定数量的ping消息作为心跳包,如果槽位为65536,这个ping消息的消息头太大了,浪费带宽。 **2.redis的集群主节点数量基本不可能超过1000个。** 如上所述,集群节点越多,心跳包的消息体内携带的数据越多。如果节点过1000个,也会导致网络拥堵。因此redis作者,不建议redis cluster节点数量超过1000个。那么,对于节点数在1000以内的redis cluster集群,16384个槽位够用了。没有必要拓展到65536个。 **3.槽位越小,节点少的情况下,压缩率高。** Redis主节点的配置信息中,它所负责的哈希槽是通过一张bitmap的形式来保存的,在传输过程中,会对bitmap进行压缩,但是如果bitmap的填充率slots / N很高的话(N表示节点数),bitmap的压缩率就很低。如果节点数很少,而哈希槽数量很多的话,bitmap的压缩率就很低。而16384÷8=2kb **一致性哈希VS哈希槽** ​ 使用hash槽的好处是可以做到数据分配更均匀,如果有N个节点,每个节点是准确的承担1/N的容量。而一致性hash做不到这点,因为它使用的是hash函数返回的值是随机的。而hash槽类似于我们准确配置每个节点的位置。 无论是hash槽还是一致性hash,本质上都是通过增加一层来解决依赖性问题。未使用时,key的分配依赖于节点个数,当节点个数变化时,key映射的节点也就改变了。增加了一个稳定层(hash槽),hash槽的个数是固定的,这样key分配到的hash槽也就是固定的。从而实现key与节点个数的解耦。hash槽与节点映射,当增加一个节点时,我们可以自己控制迁移哪些槽到新节点。 #### 36. Collection和Collections的区别: 1.**Collection:** 是集合类的上层接口。本身是一个Interface,里面包含了一些集合的基本操作。 Collection接口是Set接口和List接口的父接口 **2.Collections** Collections是一个集合框架的帮助类,里面包含一些对集合的排序,搜索以及序列化的操作。 最根本的是Collections是一个类, Collections 是一个包装类,Collection 表示一组对象,这些对象也称为 collection 的元素。一些 collection 允许有重复的元素, 而另一些则不允许,一些 collection 是有序的,而另一些则是无序的。 #### 37. Set和List的区别: ##### 37.1 **重复对象** list方法可以允许重复对象,而set方法不允许重复对象 ##### 37.2 null元素 list可以有多个null元素,set只有一个 ##### 37.3 容器是否有序 list是一个有序的容器,保持了每个元素的插入顺序。即输出顺序就是输入顺序,而set方法是无序容器,无法保证每个元素的存储顺序,**TreeSet通过 Comparator 或者 Comparable 维护了一个排序顺序** ##### 37.4 常用的实现类 - list方法常用的实现类有ArrayList、LinkedList 和 Vector。其中ArrayList 最为流行,它提供了使用索引的随意访问,而LinkedList 则对于经常需要从 List 中添加或删除元素的场合更为合适,Vector 表示底层数组,线程安全 - Set方法中最流行的几个实现类是 HashSet、LinkedHashSet 以及 TreeSet。最流行的是基于 HashMap实现的 HashSet;TreeSet 还实现了 SortedSet 接口,因此 TreeSet 是一个根据其 compare() 和compareTo() 的定义进行排序的有序容器 #### 38. 微服务&分布式 ##### 38.1 SpringCloud五大组件 - 服务发现——Netflix Eureka - 客服端负载均衡——Netflix Ribbon - 断路器——Netflix Hystrix - 服务网关——Netflix Zuul|Getway - 分布式配置——Spring Cloud Config #### 39. select,poll,epoll 这三者都是I/O多路复用的机制,不同点就在于忙轮询和无差别轮询。 ##### 39.1select 无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。o(n)时间复杂度 ##### 39.2 poll poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, **但是它没有最大连接数的限制**,原因是它是基于链表来存储的。 ##### 39.3 epoll **epoll可以理解为event poll**,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是**事件驱动(每个事件关联上fd)**的,此时我们对这些流的操作都是有意义的。**(复杂度降低到了O(1))** select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。**但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的**,而异步I/O则无需自己负责进行读写,异步I/O的实现会**负责把数据从内核拷贝到用户空间**。 **1、表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。** **2、select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善** #### 40. Spring事务传播行为 事务传播行为用来描述由某**一个事务传播行为修饰的方法**被**嵌套进另一个方法**的时事务如何传播。 ```java public void method1(){ method2(); //doSomething } @Transaction(Propagation=XXX) public void method2(){ //doSomething } ``` | 事务传播行为类型 | 说明 | | :-----------------------: | :----------------------------------------------------------- | | PROPAGATION_REQUIRED | 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。 | | PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行。 | | PROPAGATION_MANDATORY | 使用当前的事务,如果当前没有事务,就抛出异常。 | | PROPAGATION_REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起。 | | PROPAGATION_NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 | | PROPAGATION_NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常。 | | PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。 | ####