java 后端开发师面试
java 基础
-
什么是面向对象,谈谈你对面向对象的理解。
三大特性
封装:明确标识出允许外部使用的所有成员函数和数据项
内部细节对外部调用透明,外部调用无需修改或者关心内部实现
-
javabean 的属性私有,提供get/set对外访问。
-
orm框架
操作数据库,我们不需要关心链接是如何建立,sql是如何执行,只需要引入mybatis,调用方法即可
继承:继承基类的方法,并做出自己的改变或扩展,求同存异。
多态:基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同
条件:继承、方法重写、父类引用指向子类对象
父类类型 变量名 = new 字类对象 变量名.方法名();
无法调用子类特有的功能(只能是重写的)
-
-
JDK、JRE、JVM三者区别和联系
JDK:
Java Develpment Kit java 开发工具
JRE:
Java Runtime Environment java运行时环境
JVM:
java virtual Machine java 虚拟机
JDK directory 包含 JRE目录以及java工具目录(统称),JRE目录包含bin(JVM)和lib(类库)目录
-
== 和 equals
==对比的是栈中的值,基本数据类型是变量值,引用类型是堆中内存对象的地址(引用类型在栈中存的是堆中对应的地址,而真实的值存在于堆中)
equals:object中默认也是采用 == 比较,通常会重写
String 中进行了重写,对于引用类型,会将地址里面的值进行一一比较:
String类中,被复写的equals()方法其实是比较两个字符串的内容
-
简述final作用
最终的
- 修饰类:表示类不可被继承
- 修饰方法:表示方法不可被子类覆盖,但可以重载
- 修饰变量:表示变量一旦被赋值就不可更改它的值
-
修饰成员变量
- 如果final修饰的是类变量,只能在静态初始化块中指定初始值或者声明该类变量时指定初始值。
- 如果final修饰的是成员变量,可以在非静态初始化块、声明该变量或者构造器中执行初始值。
-
修饰局部变量
系统不会为局部变量进行初始化,局部变量必须由程序员显示初始化。因此使用final修饰局部变量时,即可以在定义时指定默认值(后面的代码不能对变量再次赋值),也可以不指定默认值,而在后面的代码中对final变量赋值(仅一次)
-
修饰基本类型数据和引用类型数据
- 如果基本数据类型的变量,则其数值一旦在初始化之后便不能更改
- 如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。但是引用的值是可变的。
-
为什么局部内部类和匿名内部类只能访问局部final变量?
编译之后生成两个class文件,Test.class Test1.class
-
匿名内部类
-
局部内部类
attention: 内部类和外部类是出于同一个级别的,内部类不会因为定义在这个方法中就会随着方法的执行完比就被销毁。
problem: 当外部类的方法结束时,局部变量就会被销毁了,但是内部类对象可能还在(只有没有人再引用它时,才会死亡)。如此就出现了一个矛盾:内部类对象访问了一个不存在的变量(比如,1中,Thread中访问了a b,但是a和b已经被销毁了)。为了解决这个问题,就将局部变量复制了一份作为内部类的成员变量,这样当局部变量死亡后,内部类依旧可以访问它,实际访问的是局部变量的“copy”。这样就好像延长了局部变量的生命周期。
将局部变量复制为内部类的成员变量时,必须保证这两个变量是一样的,也就是如果我们在内部类中修改了成员变量,方法中的局部变量也得跟着改变,怎么解决问题呢?
就将局部变量设置为final,对它初始化后,我就不让你再去修改这个变量,就保证了内部类的成员变量和方法的局部变量的一致性。这实际上也是一种妥协,使得局部变量和内部类建立的拷贝保持一致。
-
-
String、StringBuffer、StringBuilder区别及使用场景。
String是final修饰的,不可变,每次操作都会产生新的String对象
StringBuffer是线程安全的,StringBuilder是线程不安全的
StringBuffer方法都是synchronized修饰的
performance:StringBuilder>StringBuffer>String
(Synchronized:多线程,共享变量,结果不受影响 == 线程安全)
场景:经常需要改变字符串内容时使用后面两个
优先使用StringBuilder,多线程使用共享变量时使用StringBuffer
-
重载和重写的区别
-
重载:发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。
-
重写:发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符大于等于父类;如果父类方法修饰为private则子类就不能重写该方法。
-
-
接口和抽象类的区别
- 抽象类可以存在普通函数,接口只能存在public abstract 方法
- 抽象类中的成员变量可以各种类型的,而接口中的成员变量只能是public static final类型的(默认)。
- 抽象类只能继承一个,接口可以实现多个。
接口的设计目的,是对类的行为的一种“有”约束,因为接口不能规定类不可以有什么行为,也就是提供一种强制要求不同的类具有相同行为的机制。它只约束了行为的有无,但不对如何实现进行限制。
抽象类的设计目的,是代码复用。当不同的类某些相同的行为(A),且其中一部分行为的实现方式一致时(B),可以让这些类都派生于一个抽象类。这个抽象类实现了B,避免让所有的子类自己来实现B,这样就达到了代码复用的目的。而A减B的部分,抽象类只是定义为抽象方法,留给各个子类自己实现。正是因为A-B在这里没有实现,因此抽象类不允许实例化对象。
抽象类是对类本质的抽象,表达的是is a的关系,比如:BMW is a car。抽象类包含并实现子类的通用特性,将子类存在差异化的特性进行抽象,交由子类去实现。
而接口本质是对行为的抽象,表达的是like a的关系。比如:Bird like a Aircraft(飞行器)。接口的核心是定义行为,即实现类可以做什么,至于实现类主体是谁、是如何实现,接口并不关心。
使用场景:当你关注一个事物本质的时候,用抽象类;当你关注一个操作的时候,用接口。
抽象类的功能要远超过接口,但是,定义抽象类的代价高。因为高级语言来说每个类只能继承一个类。在这个类中,你必须继承或者编写出其所有子类的所有共性。虽然接口在功能上会弱化很多,但是它只是针对一个动作的描述。而且你可以在一个类中同时实现多个接口。在设计阶段会降低难度。(接口,可以预先定义;而抽象类,只能在子类定义完成后,再进行抽取)
-
List和Set的区别
- List: 有序,按照对象进入的顺序保存对象,可重复,允许多个Null元素对象,可以使用Iterator取出所有元素,再逐一遍历,还可以使用get(int index)获取指定下标的元素
- Set: 无序,不可重复,最多允许有一个Null元素对象,取元素时只能使用Iterator接口取得所有元素,再逐一遍历各个元素。
-
hashCode与equals
hashCode()定义在JDK的Object.java中,Java中任何类都包含hashCode()函数,散列码(int整数)、确定该对象在哈希表中的索引位置。散列表中存储的是键值对。
为什么要有hashCode
以“HashSet如何检查重复”为例子进行说明:
对象加入HashSet时,HashSet会先计算该对象的hashCode值来判断对象加入的位置,看该位置是否有值,如果没有,HashSet会认为该对象没有重复出现。如果发现有值,这时会调用equals()方法来检查两个对象是否真的相同。如果两者相同,HashSet就不会让其加入操作成功。如果不同的话,就会重写散列到其它位置。这样就大大减少了equals的次数,相应就大大提高了执行速度。
- 如果两个对象相等,则hashCode一定也是相同
- 如果两个对象相等,则两个对象分别调用equals方法都返回true
- 两个对象有相同的hashCode值,它们也不一定是相等的
- 因此,equals方法被覆盖过,则hashCode方法也必须被覆盖过
- hashCode()的默认行为是对堆上的对象产生特殊值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
-
ArrayList和LinkedList区别
如果indexOf要查找索引的元素,不存在于LinkedList中,也同样会遍历整个列表。所以,大部分情况下,使用ArrayList。
虽然,LinkedLis在插入和删除操作时,涉及的时间复杂度低,但是,如果插入新节点的次数很多,则会创建大量的内部类node对象,严重影响性能。此时,使用ArrayList初始化足够容量(不需要进行copy操作) 加 尾插法的性能更好。
-
HashMap和HashTable的区别?底层实现是什么?
区别:
- HashMap方法没有synchronize修饰,线程非安全,HashTable线程安全
- HashTable允许key和value为空,而HashTable不允许
底层实现:数组+链表实现
jdk8开始链表高度到8,数组长度超过64,链表转变为红黑树,元素以内部类Node节点存在
- 计算key的hash值,然后用hash值对数组长度取模,对应到数组下标
- 如果没有产生hash冲突(下标位置没有元素),则直接创建Node存入数组
- 如果产生hash冲突,先对key值进行equal比较,相同则取代该元素。不同,则判断链表高度插入链表,链表高度达到8,并且数组长度到64则转变为红黑树,红黑树元素个数低于6则转回链表
- key为null,存在下标0的位置
扩容机制,HashMap中默认的hash数组大小是16个,如果HashMap里存储的元素大于阈值(长度乘以扩容因子0.75)时,就会重新new 一个数组,进行迁移,并且长度扩大一倍。
-
ConcurrentHashMap原理,jdk7和jdk8版本的区别
JDK7:
- 数据结构:ReentranLock+segment+HashEntry,一个Segment包含一个HashEntry数组,每个HashEntry元素又是一个链表结构。(HashEntry和HashMap一致)
- 元素查询:二次hash,第一次Hash定位Segment,第二次Hash定位到元素所在的链表的头部(HashEntry数组中的位置)
- 分段锁:segment分段锁 segment继承了ReentranLock,锁定操作的Segment,其它的Segment不受影响,并发度为segment个数,可以通过构造函数指定,数组扩容机制不会影响其它的segment
- get方法无需加锁,volatile保证,且是对数组和Node节点加volatile(用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值)
JDK8:
- 数据结构:synchronize+CAS(乐观锁)+Node+红黑树,Node的val和next都用volatile修饰,保证可见性
- 查找、替换、赋值操作都使用CAS,其它操作使用synchronize查漏补缺,譬如扩容
- 锁:锁链表的head节点,不影响其它元素的读写,锁粒度更细,效率更高,扩容时,阻塞所有的读写操作,并发扩容
- 读操作无锁:
- Node的val和next使用volatile修饰,读写线程对该变量相互可见
- 数组用volatile修饰,保证扩容时被读线程感知
-
如何实现一个IOC容器
-
配置文件 配置 包扫描路径
-
递归包 扫描获取.class文件
-
反射、确定需要交给IOC管理的类
-
对需要注入的类进行依赖注入
- 配置文件中指定需要扫描的包路径
- 定义一些注解,分别表示访问控制层、业务控制层、数据持久层、依赖注入注解、获取配置文件注解
- 从配置文件中获取需要扫描的包路径,获取当前路径下的文件信息以及文件夹信息,我们将当前路径下的所有以.class结尾的文件添加到一个Set集合中进行存储
- 遍历这个Set集合,获取在类上有指定注解的类,并将其交给IOC容器,定义一个安全的Map用来存储这些对象
- 遍历这个IOC容器,获取到每一个类的实例,判断里面是否依赖其它的类的实例,然后进行递归注入
-
-
什么是字节码?采用字节码的好处是什么?
-
Java类加载器
JDK自带有三个类加载器:
- BootStrapClassLoader是ExtClassLoader的父类加载器(不是继承,而是通过变量 parent 引用),默认负责加载 %JAVA_HOME%/lib 下的 jar 包 和 class 文件。
- ExtClassLoader 是 AppClassLoader 的父类加载器(同上), 负责加载 %JAVA_HOME%/lib/ext文件夹下的 jar 包 和 class 类。
- AppClassLoader 是 自定义加载类的父类,负责加载 classpath 下的类文件。系统加载类(SystemLoader ,辅助AppClassLoader 加载系统文件和classpath文件,即自己写的代码和一下引用的jar包),线程上下文加载器(贯穿三个系统加载器,都能于之直接联系)
- 继承ClassLoader实现自定义加载器。
-
双亲委派模型
向上委派查找缓存是否已经加载了该类,如果到了顶层类加载器的缓存也没有找到,就从顶层类加载器向下根据其加载路径查找,直到发起加载的类加载器为止(不一定是AppClassLoader,也可以是自定义的类加载器发起)
双亲委派模型的好处:
- 主要是为了安全性,避免用户自己编写的类动态替换了Java的一些核心类,比如String。
- 同时也避免了类的重复加载,因为JVM区分不同类,不仅仅是根据类名,相同的class文件被不同的ClassLoader加载就是不同的两个类。
-
Java 中的异常体系
-
Java中的所有异常都来自顶级父类Throwable
-
Throwable下有两个子类Exception和Error
-
Error是程序无法处理的错误,一旦出现这个错误,则程序将被迫停止运行(OOM OutOfMemoryError 异常)
-
Exception不会导致程序停止,它又分为两个部分 RuntimeException 运行时异常 和 CheckedException 检查异常。
-
RuntimeException 常常发生在程序运行过程中,会导致程序当前线程执行失败(代码的逻辑错误,除零、数组越界、空指针异常)。CheckedException 常常发生在程序编译过程中,会导致程序编译不通过(语法问题,idea能够自动提醒)。
-
-
GC如何判断对象可以被回收 !!!
-
引用计数法(python采用):每个对象有一个引用计数属性,新增一个引用计数加1,引用释放时计数减1,计数为0时可以回收
-
可达性分析法(Java采用):从GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots 没有任何引用链相连时,则证明该对象是不可用的,那么虚拟机就判断是可回收对象。
引用计数法,可能会出现A 引用了 B, B又引用了A,这时候就算他们都不在使用了,但因为相互引用计数器 = 1 永远无法被回收。
GC Roots的对象有:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
可达性算法中的不可达对象并不是立即死亡的,对象拥有一次自我拯救的机会。对象被系统宣告死亡至少要经历两次标记过程:第一次经过可达性分析发现没有与GC Roots相连接的引用链,第二次是由虚拟机自动建立的Finalizer队列中判断是否需要执行finalize()方法。
当对象变成不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize()方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”
每个对象只能出发一次finalize()方法
由于finalize()方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,不推荐大家使用。
-
-
线程的生命周期,线程有哪些状态
-
线程通常有5种状态,创建、就绪、运行、阻塞和死亡
-
阻塞的情况又分为三种:
- 等待阻塞:运行的线程执行wait方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其它线程调用notify或者notifyAll方法才能被唤醒,wait是object类的方法。
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。
- 其它阻塞:运行的线程执行sleep和join方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep状态超时,join等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。sleep是Thread类的方法
-
新建状态(New):创建了一个线程对象
-
就绪状态(Runnable):线程对象创建后,其它线程调用了该对象的start方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权
-
运行状态(Running):就绪状态的线程获取了CPU,执行程序代码
-
阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
-
死亡状态(Dead):线程执行完了或者因异常退出了run方法,该线程结束生命周期。
-
-
sleep()、wait()、join()、yield()的区别
-
锁池
所有需要竞争同步锁的线程都会放在锁池中,比如当前对象的锁已经被其中一个线程得到,则其它线程需要在这个锁池进行等待,当前面的线程释放同步锁后,锁池中的线程再去竞争同步锁,当某个线程得到后会进入就绪队列进行等待CPU资源分配。
-
等待池
当我们调用wait()方法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁。只有调用了notify()或者notifyAll()后,等待池的线程才会开始去竞争锁,notify()是随机从等待池选出一个线程放到锁池中,而notifyAll()是将等待池的所有线程放到锁池当中。
-
sleep是Tread类的静态本地方法,wait则是Object类的本地方法。
-
sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。
-
sleep方法不依赖同步器synchronized,但是wait需要依赖synchronized关键字
-
sleep不需要被唤醒(休眠之后退出阻塞),但是wait需要(不指定时间需要被别人中断)
-
sleep一般是用于当前线程休眠,或者轮询暂停操作,wait则用于多线程之间的通信(A线程wait,需要其它线程调用notify唤醒)
-
sleep会让出CPU执行时间且强制上下文切换,而wait则不一定,wait后可能还是有机会重新竞争到锁,继续执行。
-
yield()执行后线程直接进入就绪状态,马上释放cpu的执行权,但是依然保留了cpu的执行资格,所以有可能cpu下次进行线程调度还是会让这个线程获取到执行权继续执行。
-
join()执行后线程进入阻塞状态,例如在线程B中调用线程A的join(),那么线程B会进入到阻塞队列,直到线程A结束或中断线程。
在线程main方法中,调用线程t1的join方法,main方法进入阻塞,所以首先是t1线程执行,打印22,然后是打印11
-
-
对线程安全的理解
-
Thread、Runnable的区别
Thread和Runnable的实质是继承关系,没有可比性(Runnable是接口,而Thread是实现Runnable的类)。无论使用Runnable和Thread,都会new Thread,然后执行run方法。用法上,如果有复杂的线程操作需求,那就选择继承Thread(因为Thread实现了Runnable接口,且自身也有方法扩展),如果只是简单的执行一个任务,那就实现runnable。
Question:
Reason:MyThread创建了两个实例,而ticket属于局部变量,因此每次都会从5开始,自然卖出两倍,属于用法错误
Solution:将ticket定义为static类变量,且用synchronized修饰。
-
说说对守护线程的理解
守护线程:为所有非守护线程(用户线程)提供服务的线程;任何一个守护线程都是整个JVM中所有非守护线程的保姆;
守护线程类似于整个进程的一个默默无闻的小喽喽;它的生死无关重要,它却依赖整个进程而运行;如果其它线程都结束了,没有要执行的了,程序就结束了,不会去理会守护线程,直接就把它中断。
**attention: ** 由于守护线程的终止是自身无法控制的,因此千万不要把IO、File等重要操作逻辑分配给它;因此它不靠谱;
守护线程的作用?
举例,GC垃圾回收线程:就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
应用场景:
- 来为其它线程提供服务支持的情况
- 在任何情况下,程序结束时,这个线程必须正常且立刻关闭,就可以作为守护线程来使用;反之,如果一个正在执行某个操作的线程必须要正确地关闭掉否则就会出去不好地后果的话,那么这个线程就不能作为守护线程,而是用户线程。通过都是些关键的事务,比如说,数据库录入或者更新,这些操作都是不能中断的。
thread.setDeamon(True)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常,你不能把正在运行的常规线程设置为守护线程
在Deamon线程中产生的新线程也是Deamon的。
守护线程不能访问固有资源,比如读写操作或者计算逻辑。因为它会在任何时候甚至在一个操作的中间发生中断。
Java自带的多线程框架,比如ExecuteService,会将守护线程转换为用户线程,所以如果使用后台线程就不能使用Java的线程池。
-
ThreadLocal的原理和使用场景
解读:
场景1,当在controller、service、dao层,传递数据时,需要每个方法都定义相应的形参,可以将要传递的数据存到ThreadLocal中,再通过get方法取出来。
其它场景,是由于ThreadLocal的独立性,每个线程都有各自的私有的ThreadLocalMap容器,无需同步机制就能保证多个线程访问容器的互斥性
-
ThreadLocal内存泄漏原因,如何避免
内存泄漏为程序在申请内存后,无法释放已经申请的内存空间,一次内存泄漏危机可以忽略,但内存泄漏堆积后果很严重,无论多少内存,迟早会被占光
不再会被使用的对象或者变量占用的内存不能回收,就是内存泄漏。
强引用:使用最普遍的引用(new),一个对象具有强引用,不会被垃圾回收器回收。当内存不足,java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。
如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象
弱引用:JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用。
ThreadLocal的实现原理,每一个Thread维护一个ThreadLocalMap,key为使用弱引用的ThreadLocal实例,value为线程变量的副本
实线=强引用、虚线=弱引用
headLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时,key(ThreadLocal)势必会被GC回收,这样导致ThreadLocalMap中key为null,而value还存在着强引用,只有thread线程退出以后,value的强引用链条才会断掉,如果当前线程迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链(红色链条)
key使用强引用
当threadLocalMap的key为强引用,回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏
key使用弱引用
当ThreadLocalMap使用key为弱引用,回收ThreadLocal时,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。当key为null时,在下一次ThreadLocalMap调用set(),get(),remove()方法时会被清除value值。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期和Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用
ThreadLocal正确的使用方法
-
每次使用完ThreadLocal都调用它的remove()方法清除数据
-
将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉。
-
并发、并行、串行的区别
串行在时间上不能重叠,前一个任务没搞定,下一个任务只能等着。
并行在时间上是重叠的,两个任务在同一时刻互不干扰的同时执行(多核CPU)。
并发允许两个任务彼此干扰。同一时间点,只有一个任务执行,交替执行(单核CPU)。
并发的三大特性
原子性是指在一个操作中CPU不可以中途暂停然后再调度,即不被中断操作,要么全部执行完成,要么全部不执行。比如转账,从账户A向账户B转1000元,那么必然包括两个操作:从账户A减去1000元,往账户B加上1000元。2个操作必须全部完成。
private long count = 0;
public void calc() {
count++;
}
-
将count从主存读到工作内存中的副本中(每个线程都有自己的工作内存,把count copy 到该线程的工作内存中)
-
在工作内存中进行 +1 的运算
-
将结果写入工作内存,即赋值给工作内存的count
-
将工作内存的值刷回主存(什么时候刷入由操作系统决定、不确定的)
程序中的原子性指的是最小的操作单元,自增操作,它本身其实并不是原子性操作,分了3步的,包括读取变量的原始值、进行加1操作、结果写入工作内存。所以在多线程的情况下,有可能一个线程还没自增完成,才执行到第二步,另一个线程就已经读取到了主存的值,导致结果错误(正常情况,线程二读取到的应该是1,错误情况,线程二读取到的还是0)。如果我们能够保证自增操作是一个原子性的操作,那么就能保证其它线程读取到的一定是自增后的数据。
关键字:synchronized 保证原子性
attention:在count++中,原子性是指前三步的原子性
可见性是当多个线程访问同一个变量时,一个线程修改了这个变量的值,其它线程能够立即看得到修改的值。
若两个线程在不同的CPU,线程1正在循环执行,那么线程2改变了stop的值还没刷新到主存,线程1中的stop依旧为false在循环执行。因此,线程2对变量的修改,线程1没看到这就是可见性问题。
//线程1
boolean stop = false;
while (!stop) {
doSomething();
}
//线程2
stop = true;
如果线程2改变stop的值,线程1一定会停止吗?不一定。当线程2更改了stop变量的值之后,但是还没来得及写入到主存当中,线程2转去做其它事情了,那么线程1由于不知道线程2对stop变量的更改,因此会一直循环下去。
关键字:volatile、synchronized、final 保证可见性
attention: 在count++中,可见性保证的是第四步
有序性 是指虚拟机在进行代码编译时,对于那些改变代码执行顺序之后不会对最终结果造成影响的代码,虚拟机不一定会按照我们写的代码的顺序来执行,有可能将他们重排序。实际上,对于有些代码进行重排序之后,虽然对变量的值没有造成影响,但有可能会出现线程安全问题。
int a = 0;
bool flag = false;
//线程1
public void write() {
a = 2; //1
flag = true; // 2
}
//线程2
public void multiply() {
if (flag) { //3
int ret = a * a;//4
}
}
write方法里的1和2做了重排序,线程1先对flag赋值为true,随后转执行到线程2,ret直接计算出结果,再到线程1,这时候a才赋值为2,很明显发生错误(正确:a=4;错误:a=0)
关键字:volatile、synchronized
volatile本身就包含了禁止指令重排的语义,而synchronized关键字是由“一个变量同一个时刻只允许一条线程对其操作”
-
为什么用线程池?解释下线程池的参数?
-
降低资源消耗:提高线程利用率,降低创建和销毁线程的消耗
-
提高响应速度:任务来了,直接有线程可用可执行,而不是先创建线程、再执行
-
提高线程的可管理性:线程是稀缺资源,使用线程池可以统一分配调优监控
首先通过ThreadFactory 生成出 corePoolSize大小的核心线程数,如果核心线程已经全部在执行任务,再有新的任务进来,就会把这些新的待执行任务放到workQueue中,直到这个队列被放满,此时,还再持续进入新任务,就会创建最多maxinumPoolSize-corePoolSize个线程用于处理队列中的任务。如果达到最大线程数maxinumPoolSize,线程池没有能力继续处理新提交的任务时,就会Handler(第二种策略)任务拒绝。如果高峰期一过,maxinumPoolSize-corePoolSize大小的线程闲置时间超过keepAliveTime,这些空闲线程就被销毁。
-
-
简述线程池处理流程
-
线程池中阻塞队列的作用?为什么是先添加任务到队列而不是先创建最大线程池?
-
一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,而阻塞队列通过阻塞可以保留住当前想要继续入队的任务。
阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放CPU资源
阻塞队列自带阻塞和唤醒的功能,不需要额外的处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活,不至于一直占用CPU资源
-
在创建新线程的时候,是要获取全局锁的,这个时候其它的就得阻塞,影响了整体效率
-
-
线程池中线程复用原理
线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过Thread创建线程时的一个线程必须对应一个任务的限制
在线程池中,同一个线程可以从阻塞队列中不断获取任务来执行,其核心原理在于线程池对Thread进行了封装,并不是每次执行任务都会调用Thread.start()来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停检查是否有任务被执行,如果有则直接执行,也就是调用任务中的run方法,将run方法当成一个普通的方法执行,通过这种方式只使用固定的线程就将所有任务的run方法串联起来。
-
spring是什么?
轻量级的开源的J2EE框架。它是一个容器框架,用来封装javabean(java对象),中间层框架(万能胶)可以起到一个连接作用,比如说把Structs和hibernate粘合在一起运行,可以让我们的企业开发更快、更简洁
spring是一个轻量级的控制反转(IOC)和面向切面(AOP)的容器框架
- 从大小与开销两方面而言Spring都是轻量级的
- 通过控制反转(IoC)的技术达到松耦合的目的
- 提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务(日志服务)进行内聚性的开发
- 包含并管理应用对象(Bean)的配置和生命周期,这个意义上是一个容器
- 将简单的组件配置、组合成为复杂的应用,这个意义上是一个框架
-
谈谈AOP的理解
每个controller不仅要实现核心的功能(业务逻辑功能),也会负责
一些日志、安全等功能,因此可能每个对象里面都有这些日志、安全的代码(横切关注点),导致重复冗余,因此AOP会把这些交叉业务逻辑(安全、日志、事务等)封装成一个切面,然后注入到目标对象(具体业务逻辑)中去。
-
谈谈你对IOC的理解
个人理解:Set集合中保存着包路径下的各个class文件,然后根据XML里面配置的bean节点、类的@repository、@service、@service、@controller、@component注解,通过反射创建对象放到map里面(IOC容器)。以前类之间的关系,譬如A类引用B类,即在A类中去主导 new B类对象,而控制反转,即我们将类之间的依赖关系交给了IOC容器,当A类需要引用B类时,通过autowired 、resourced等注解进行标识,IOC容器自动创建对象建立双发的联系。
-
BeanFactory和ApplicationContext有什么区别?
ApplicationContext是BeanFactory的子接口
因此,ApplicationContext提供了更完整的功能:
- 继承MessageSource,因此支持国际化
- 统一的资源文件访问方式
- 提供在监听器中注册bean的事件
- 同时加载多个配置文件
- 载入多个(有继承关系)上下文,使得每一个上下文都专注于一个特定的层次,比如应用的web。
-
Spring Bean的生命周期
- 扫描并解析类得到BeanDefinition
- 如果Bean有多个构造方法,则要推断构造方法
- 确定好构造方法后,进行实例化得到一个对象
- 对对象中的加了@Autowired注解的属性进行填充(依赖注入)
- 回调Aware方法(自定义一些操作),比如BeanNameAware,BeanFactoryAware
- 调用BeanPostProcessor的初始化前的方法
- 调用初始化方法
- 调用BeanPostProcessor的初始化后的方法,在这里会进行AOP
- 如果当前创建的Bean是单例的则会把bean放入单例池
- 使用bean
- Spring容器关闭时调用DisposableBean中destory()方法
-
Spring框架中的单例Bean是线程安全的么?不安全
总结:
- Spring中的Bean默认是单例模式,因此线程不安全
- 如果Bean有状态(实例变量、类变量),保存数据,需要开发人员自己保证线程安全,方法:“singleton”->“protopyte”、ThreadLocal、synchronized、lock、CAS
- 如果Bean无状态(方法),不保存数据,多线程调用里面的方法,会在内存中复制变量到自己线程的工作内存中,因此是安全的。
-
Spring框架中都用到了哪些设计模型
简单工厂:由一个工厂类根据传入的参数,动态决定应该创建哪一个产品类。
spring 中的BeanFactory就是简单工厂模式的体现,根据传入一个唯一的标识来获得Bean对象,但是否是在传入参数后创建还是传入参数前创建要根据具体情况来定。
工厂方法:
实现了FactoryBean接口的bean是一类叫做factory的bean。其特点是,spring会在使用getBean()调用获得该bean时,会自动调用该bean的getObject()方法,所以返回的不是factory这个bean,而是bean.getObject()方法的返回值
单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点
spring对单例的实现:spring中的单例模式完成了后半句话,即提供了全局的访问点BeanFactory。但没有从构造器级别去控制单例,这是因为spring管理的是任意的java对象。
适配器模式:
spring定义了一个适配接口,使得每一种Controller有一种对应的适配器实现类,让适配器代替controller执行相应的方法。这样在扩展controller时,只需要增加一个适配器类就完成了springMVC的扩展。
装饰器模式:动态地给一个对象添加一些额外的职责。就增加功能来说,Decorator模式相比生成子类更为灵活。
spring中用到的包装器模式在类名上有两种表现:一种是类名中含有wrapper,另外一种是类名中含有Decorator
动态代理:
切面在应用运行的时刻被切入。一般情况下,在织入切面时,AOP容器会为目标对象动态创建一个代理对象。springAOP就是以这种方式织入切面的。
-
Spring事务的实现方式和原理以及隔离级别
解释:
- 编程式,即调用类似于 rollback、commit这些api方法;神明式,使用@Transaction注解。
- @Transaction注解,就是开启事务,这个方法中的所有sql都会在一个事务中执行,统一成功或失败。
- 方法添加@Transaction注解后,spring会基于该方法的类生成一个代理对象作为bean,放入容器中,如果使用这个带有注解的方法,代理逻辑会先把事务的自动提交设置为false,然后再去执行原本的业务逻辑方法,如果业务逻辑方法没有异常(这个异常可以配置的,@Transaction中的rollback属性进行配置,默认会对RuntimeException和Error进行回滚),那么代理逻辑就会把事务进行提交,反之,事务回滚。
spring事务隔离级别就是数据库的隔离级别:外加一个默认级别
-
read uncommitted (未提交读)
-
read committed(提交读、不可重复读)
-
repeatable read (可重复读)
-
serializable(可串行化)
数据库的配置隔离级别是 Read committed,而spring配置的隔离级别是Repeatable Read,请问这时隔离级别是以哪一个为准? 以spring配置的为准,如果spring设置的隔离级别数据库不支持,则取决于数据库。
-
spring事务传播机制
多个事务方法互相调用时,事务如何在这些方法间传播
A方法调用B方法
REQUIRED(Spring默认的事务传播类型):如果A当前没有事务,则B自己创建一个事务,如果A当前存在事务,则B加入到A的事务中去。
SUPPORTS:A当前存在事务,B则加入到A的事务中,如果A当前没有事务,B就以非事务方法运行
MANDATORY:A当前存在事务,B则加入到A的事务中,如果A当前没有事务,则抛出异常
REQUIRED_NEW:如果A当前没有事务,则B创建一个自己的事务,互不相干;如果当前A存在事务,则挂起A的事务,B创建自己的事务。无论如何,B创建自己的事务
NOT_SUPPORTED:不管A的事务的有无,B都以非事务状态运行;如果A存在事务,则挂起该事务。
NEVER:B不使用事务,如果A当前存在事务,则抛出异常
NESTED:如果A当前存在事务,则B在嵌套事务中执行(A的事务为父事务,B的事务为子事务);如果A当前不存在事务,则和REQUIRED的操作一样,B创建一个事务。
和REQUIRED_NEW的区别 REQUIRED_NEW由于是新建一个事务且与原有事务无关,因此A的事务回滚,不会影响B的事务;NESTED中,A的为父事务,B的为子事务,即嵌套事务,父事务回滚时,子事务一定跟着回滚。 和REQUIRED的区别 REQUIRED中,A存在事务时,则A和B使用同一事务,因此B中出现异常时,由于共用事务,无论A是否catch其异常,事务都会回滚;而在NESTED中,B发生异常时,A可以catch其异常,这样子事务回滚,父事务不受影响。
-
spring事务什么时候会失效?
Spring事务的原理是AOP,进行了切面增强,那么失效的根本原因是这个AOP不起作用!常见情况如下:
-
发生自调用,类里面使用this调用本类的方法(this通常省略),此时这个this对象不是代理类,而是UserService对象本身。
解决方法,让那个this变成UserService的代理类即可
-
方法不是public的
@Transactional 只能适用于 public 的方法上,否则事务会失效,如果非要用于非 public 方法上,可以开启 ASpentJ代理模式
-
数据库不支持事务
-
代理对象没有被spring管理
-
异常被吃掉,无法抛出,事务不会回滚(或者抛出的异常没有被定义,默认为 RuntimeException)
-
-
什么是bean的自动装配,有哪些方式
在spring中,对象无需自己查找或创建与其关联的其它对象,由容器负责把需要相互协作的对象引用赋予各个对象,使用autowire来配置自动装载模式。
开启自动装配,只需要在xml配置文件
中定义“autowire”属性 <bean id="customer" class-"com.xxx.xxx.Customer" autowire=""/>
autowire属性有五种装配的方式:
-
no-缺省情况下,默认的方式是不进行自动装配的,通过手工设置ref来进行装配bean。
手动装配:以value或ref的方式明确指定属性值都是手动装配。 需要通过"ref"属性来连接bean
-
byName-根据bean的属性名称进行自动装配
Customer的属性名称是person,Spring会将bean id为person的bean通过setter方法进行自动装配 <bean id="customer" class="com.xxx.xxx.Customer" autowire="byname"/> <bean id="person" class="com.xxx.xxx.Person"/>
-
byType-根据bean的类型进行自动装配
Customer的属性person的类型为Person,Spring会将bean 类型为Person类型的bean通过setter方法进行自动装配 <bean id="customer" class="com.xxx.xxx.Customer" autowire="byType"/> <bean id="person" class="com.xxx.xxx.Person"/>
-
constructor-类似byType,不过是应用于构造器的参数。如果一个bean与构造器参数的类型相同,则进行自动装配,否则导致异常
Customer构造函数的参数person的类型为Person,Spring会将Person类型通过构造方法进行自动装配 <bean id="customer" class="com.xxx.xxx.Customer" autowire="constructor"/> <bean id="person" class="com.xxx.xxx.Person"/>
-
autodetect-如果有默认的构造器,则通过constructor方式进行自动装配,否则使用byType方式进行自动装配(spring 3.0+弃用)
@Autowired自动装配bean,可以在字段,setter方法、构造函数上使用。
-
-
Spring Boot、Spring MVC 和 Spring 有什么区别
Spring是一个IOC容器,用来管理Bean,使用依赖注入实现控制反转,可以很方便的整合各种框架,提供AOP机制弥补OOP的代码重复问题,更方便将不同类不同方法中的共同处理抽取成切面、自动注入给方法执行,比如日志、异常等
Spring MVC 是spring对web框架的一个解决方法,提供了一个总的前端控制器dispatchServlet,用来接受请求,然后定义了一套路由策略(url到handle的映射)及适配执行handle,将handle结果使用视图解析技术生成视图展现给前端。
Springboot是Spring提供的一个快速开发工具包,让程序员更方便、更快速的开发spring+springmvc应用,简化了配置(约定了默认配置),整合了一系列的解决方案(starter机制)、redis、mongodb、es,可以开箱即用。
-
SpringMVC的工作流程
- 用户发送请求到前端控制器 DispatchServlet
- DispatchServlet收到请求调用HandlerMapping处理器映射器。
- 处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器以及处理器拦截器(如果有则生成)一并返回给DispatchServlet。
- DispatchServlet调用HandlerAdapter处理器适配器。
- HandlerAdapter经过适配调用具体的处理器(Controller)
- Controller执行完成后返回 ModelAndView
- HandlerAdapter将controller执行结果ModelAndView返回给 DispatchServlet。
- DispatchServlet将ModelAndView传给ViewResolver.
- ViewResolver解析后返回具体View。
- DispatchServlet根据View进行渲染视图(即将模型数据填充至视图中)
- DispatchServlet响应客户
-
Spring MVC的主要组件
Handler:处理器。它直接应对着MVC中的C也就是Controller层,它的具体表现形式有很多,可以是类,也可以是方法。在Controller层中@RequestMapping标注的所有方法可以看成是一个Handler,只要可以实际处理请求就可以是Handler.(重点掌握前两个)
-
Spring Boot 自动配置原理
@Import + @Configuration + Spring spi
自动配置类由各个starter提供,使用@Configuration + @Bean定义配置类,放到META-INF/Spring.factories下
使用Spring spi扫描META-INF/spring.factories下的配置类
使用@Import导入自动配置类
EnableAutoConfiguration中是以<key, value>进行存储,value中,都是类的全路径,SpringFactoriesLoader.loadFactoryNames将读取这些类的全路径,并通过反射生成bean对象存到容器中。
如果这些bean对象的类被@Configuration修饰,那么类里面的被@Bean修饰的变量,也会变成bean对象。
-
如何理解Spring Boot 中的Starter
使用Spring + SpringMVC,如果需要引入mybatis等框架,需要到xml中定义mybatis需要的bean
starter就是定义一个starter的jar包,写一个@Configuration配置类,将这些bean定义在里面,然后在starter包的META-INF/spring.factories中写入该配置类,springboot会按照约定来加载该配置类
开发人员只需要将相应的starter包引入到pom文件中,进行相应的属性配置(使用默认配置时,不需要配置),就可以直接进行代码开发,使用对应的功能了,比如mybatis-spring-boot-starter,spring-boot-starter-redis
-
什么是嵌入式服务器?为什么使用嵌入式服务器
节省了下载安装tomcat,应用不需要再打包成war包,然后放入webapp目录中运行了
只需要一个安装了java的虚拟机,就可以直接在上面部署应用程序了
springboot内置了tomcat.jar,而main方法时会去启动tomcat,并利用tomcat的spi机制加载springmvc
-
mybatis的优缺点
优点5:即表的字段映射到entity的属性
缺点2:由于基于SQL语句编程,如果xml文件中使用了一些mysql支持的语法,再切换到oracle时,就需要进行修改。
-
MyBatis 与 Hibernate对比
国内:面向表结构设计,不使用面向对象
国外:面向对象开发
-
{}和${}的区别
{}是预编译处理、是占位符,${}是字符串替换、是拼接符
Mybatis 在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement来赋值;
Mybatis 在处理${}时,就是将其替换成变量的值,调用Statement来赋值;
{} 的变量替换是在DBMS中,变量替换后,#{}对应的变量自动加上单引号;
${} 的变量替换是在DBMS外,变量替换后,其对应的变量不会加上单引号;
使用#{}可以有效的防止SQL注入,提高系统安全性
-
简述Mybatis 的插件运行原理,如何编写一个插件
Mybatis 只支持针对 ParameterHandler、ResultSetHandler、StatementHandler、Executor这四种接口的插件,Mybatis使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler的invoke()方法,拦截那些你指定需要拦截的方法。
- Executor:代表执行器,由它调度StatementHandler、ParameterHandler、ResultSetHandler等来执行对应的SQL,其中StatementHandler是最重要的。
- StatementHandler:作用是使用数据库的Statement(PreparedStatement)执行操作,它是四大对象的核心,起到承上启下的作用,许多重要的插件都是通过拦截它来实现的。
- ParameterHandler:是用来处理SQL参数的。
- ResultSetHandler:是进行数据集(ResultSet)的封装返回处理的,它非常的复杂,好在不常用。
编写插件:实现Mybatis的 Interceptor 接口并覆写intercept()方法,然后在给插件编写注释,指定要拦截哪一个接口的哪些方法即可,在配置文件中配置编写的插件
@Signature 是起到一个方法重载的作用,因为method方法重载,因此需要指定args
@Component 将对象加载进IOC容器中。
invocation.proceed() 之前编写代码,即具体业务逻辑执行前进行处理,同理之后编写代码,即具体逻辑执行后进行处理。
-
索引基本原理
索引用来快速地寻找那些具有特定值的记录。如果没有索引,一般来说执行查询时遍历整张表。
索引的原理:就是把无序的数据变成有序的查询。
- 把创建了索引的列的内容进行排序
- 对排序结果生成倒排表
- 在排序表内容上拼上数据地址链
- 在查询的时候,先拿到倒排表内容,再取出数据地址链,从而拿到具体数据
-
mysql聚簇和非聚簇索引的区别
都是B+树的数据结构
-
聚簇索引:将数据存储与索引放到了一块,并且按照一定的顺序组织的,找到了索引也就找到了数据,数据的物理存放顺序与索引顺序是一致的,即:只要索引是相邻的,那么对应的数据一定也是相邻地存放在磁盘上。
-
非聚簇索引:叶子节点不存储数据、存储的是数据行地址,也就是说根据索引查找到数据行的位置再取磁盘查找数据,这个有点类似于一本书的目录,比如我要找第三章第一节,那么先在目录里面找,找到对应的页码再去找文章。
-
-
mysql索引的数据结构、各自优劣
索引的数据结构和具体存储引擎的实现有关,再MySQL中使用较多的索引有Hash索引,B+索引等,InnoDB存储引擎的默认索引实现为:B+树索引。对于哈希索引来说,底层的数据结构为哈希表,因此在绝大多数需求为单条记录查询(where id = '')的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择BTree索引。
B+树:
B+树是一个平衡的多叉树,从根节点到每个叶子节点的高度差值不超过1,而且同层级的节点间有指针相互(双向)链接。在B+树上的常规检索,从根节点到叶子节点的搜索效率基本相当,不会出现大幅度波动,而且基于索引的顺序扫描时,也可以利用双向指针快速左右移动,效率非常高。因此,B+树索引被广泛应用于数据库、文件系统等场景。
哈希索引:
哈希索引就是采用一定的哈希算法,把键值换算成新的哈希值,检索时不需要类似B+树那样从根节点到叶子节点逐级查找,只需要一次哈希算法即可立刻定位到相应的位置,速度非常快。
-
索引的设计原则
查询更快、占用空间更小
第9点,区分度,就是细粒度过于粗糙,导致查询处理的数量过多。
-
mysql锁的类型有哪些
基于锁的属性分类:共享锁、排他锁
基于锁的粒度分类:行级锁(INNODB)、表级锁(INNODB、MYiSAM)、页级锁(DBD引擎)、记录锁、间隙锁、临建锁。
基于锁的状态分类:意向共享锁、意向排他锁
-
mysql执行计划怎么看 !!!!
-
事务的基本特性和隔离级别 !!!
ACID:
Atomicity:原子性,指的是一个事务中的操作要么全部成功,要么全部失败
Consistency:一致性,指的是数据库总是从一个一致性的状态转换到另外一个一致性的状态。比如A转账给B1000块钱,假设A只有90块,支付之前我们数据库里的数据都是符合约束的,但是如果事务执行成功了,我们的数据库数据就破坏约束了,因此此事务不能成功,这里我们说事务提供了一致性的保证。
Isolation:隔离性,指的是一个事务的修改在最终提交前,对其它事务是不可见的。
duration:持久性,指的是一旦事务提交,所做的修改就会永久保存到数据库中。
隔离性的4个级别:
-
关心过业务系统里面的sql耗时吗?统计过慢查询吗?对慢查询都怎么优化过。
在业务系统中,除了使用主键进行的查询,其他的都会在测试库上测试其耗时,慢查询的统计主要由运维在做,会定期将业务中的慢查询反馈给我们。
慢查询的优化首先要搞明白慢的原因?是查询条件没有命中索引?是load了不需要的数据列?还是数据量太大?
因此优化主要针对这三个方面。
- 首先分析语句,看看是否load了额外的数据,可能是查询了多余的行并且抛弃掉了,可能是加载了许多结果中并不需要的列,对语句进行分析以及重写
- 分析语句的执行计划,然后获得其使用索引的情况,之后修改语句或者修改索引,使得语句尽可能的命中索引。
- 如果对语句的优化已经无法进行,可以考虑表中的数据量是否太大,如果是的话可以进行横向或者纵向的分表
-
ACID靠什么保证的
-
A原子性由undo log日志保证,它记录了需要回滚的日志信息,事务回滚时撤销已经执行的sql
-
C一致性有其它三大特性保证,程序代码保证业务上的一致性
-
I隔离性由MVCC来保证
-
D持久性由 内存+redo log 来保证,mysql修改数据同时在内存和redo log记录这次操作,宕机的时候可以从redo log恢复
InnoDB redo log 写盘,InnoDB 事务进入prepare 状态。 如果前面prepare 成功,binlog写盘,再继续将日志持久化到 binlog,如果持久化成功,那么 InnoDB 事务则进入 commit状态 (在 redo log 里面写一个 commit 记录)
-
redo log的刷盘会在系统空闲时进行
-
-
什么是MVCC !!!!
-
mysql主从同步原理
全同步复制
主库写入binlog后强制同步日志到从库,所有的从库都执行完成后才返回给客户端,但是很显然这个方式的话性能会受到严重影响。因为主库需要一直等待。
半同步复制
和全同步不同的是,半同步复制的逻辑是这样,从库写入日志成功后返回ACK确认给从库,主库收到至少一个从库的确认就认为写操作完成。
-
简述MyIsAM和InnoDB的区别
MyISAM:
- 不支持事务,但是每次查询都是原子的
- 支持表级锁,即每次操作是对整个表加锁
- 存储表的总行数;(select count(*) from table)很快
- 一个MYIISAM表有三个文件:索引文件、表结构文件、数据文件
- 采用非聚集索引,索引文件的数据域存储指向数据文件的指针。辅索引与主索引基本一致,但是辅索引不用保证唯一性。
InnoDB:
- 支持ACID的事务,支持事务的四种隔离级别
- 支持行级锁以外键约束;因此可以支持写并发
- 不存储总行数
- 一个InnoDB引擎存储在一个文件空间中(共享表空间,表的大小不受操作系统控制,一个表可能分布在多个文件里),也有可能为多个(设置独立表空间,表大小受操作系统文件大小限制,一般为2G),受操作系统文件大小的限制;
- 主键索引采用聚集索引(索引的数据域存储数据文件本身),辅索引的数据域存储主键的值;因此从辅助索引查找数据,需要先通过辅助索引找到主键值,再访问主键索引;最好使用自增主键,数据能够有序的插入在之后,防止插入数据时,为维持B+树结构,文件的大调整。
-
mySQL中索引类型以及对数据库性能的影响
普通索引:允许被索引的数据列包含重复的值,即对应着多行
唯一索引:可以保证数据记录的唯一性,对应一行数据,但实际存储和普通索引一致,B+树中只存储主键id
主键:是一种特殊的唯一索引,在一张表中只能定义一个主键索引,主键用于唯一标识一条记录,使用关键字PRIMARY KEY 来创建(聚集索引,B+书中存储实际数据)
联合索引:索引可以覆盖多个数据列,比如像INDEX(column A,column B)索引
全文索引:通过建立倒排索引,可以极大的提升检索效率,解决判断字段是否包含的问题(使用like效率会很低),是目前搜索引擎使用的一种关键技术。可以通过AlTER TABLE table_name ADD FULLTEXT(column);创建全文索引
优势:
索引可以极大的提升数据的查询速度。
通过使用索引,可以在查询过程中,使用优化隐藏器,提高系统的性能。
缺点:
会降低插入、删除、更新表的速度,因为在执行这些写操作时,还要操作索引文件。
索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚集索引,那么需要的空间就会更大,如果非聚集索引很多,一旦聚集索引改变,那么所有非聚集索引都跟着改变,因此非聚集索引指向聚集索引。
-
RDB 和 AOF机制
RDB:redis DateBase
在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
优点:
- 这个Redis数据库将只包含一个文件dump.rdb,方便持久化
- 容灾性好,方便备份
- 性能最大化,fork子进程来完成写操作,让主进程继续处理命令,所有IO最大化。使用单独子进程来进行持久化,主进程不会进行任何IO操作,保证了redis的高性能
- 相对于数据集大时,比AOF的启动效率更高。
缺点:
-
数据安全性低。RDB是间隔一段时间进行持久化,如果持久化之间redis发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的的时候
-
由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至1秒钟。
AOF:Append only File
以日志的形式记录服务器所处理的每一个写、删除操作,以文本的方式记录,可以打开文件看到详细的操作记录
优点
- 数据安全,Redis提供了3种与AOF文件的同步策略,即每秒同步、每修改同步和不同步。事实上,每秒同步也是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失。而每修改同步,我们可以将其视为同步持久化,即每次发送的数据变化都会被立即记录到磁盘中。
- 通过append模型写文件,即使中途服务器宕机也不会破环已经存在的内容,可以通过redis-check-aof工具解决数据一致性问题。
- AOF机制的rewrite模式。定期对AOF文件进行重写,以达到压缩的目的
缺点:
-
AOF文件比RDB文件大,且恢复速度慢
-
数据集大的时候,比rdb启动效率低
-
运行效率没有RDB高
总结
- AOF文件比RDB更新频率高,优先使用AOF还原数据
- AOF比RDB更安全也更大
- RDB性能比AOF好
- 如果两个都配了,优先加载AOF
-
Redis的过期键的删除策略
Redis是key-value数据库,我们可以设置Redis缓存的key的过期时间。Redis的过期策略是指当Redis中缓存的key过期了,Redis如何处理。
- 惰性过期:只有当访问一个key时,才会去判断该key是否已经过期,过期则清除。该策略可以最大化地节省CPU资源,却对内容非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用内存。
- 定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已经过期的key。该策略是前两者(CPU and 内存)的一个折衷方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
(expires字典会保存所有设置了过期时间的key和过期数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的unix时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键)
Redis中同时使用了惰性过期和定期过期两种过期策略。
-
Redis线程模型,单线程为什么快
文件事件处理器:多个Socket、IO多路复用程序、文件事件分派器以及事件处理器
流程:IO多路复用程序会监听多个Socket,会将Socket放入一个队列中排队,每次从队列中取出一个Socket给事件分派器,事件分派器把Socket给对应的事件处理器。
-
缓存雪崩、缓存穿透、缓存击穿
缓存雪崩是指缓存同一时间大面积失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量的请求而崩掉。
解决方案:
- 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
- 给每一个缓存数据增加相应的缓存标记,记录缓存是否失效,如果缓存标记失效,则更新数据缓存。
- 缓存预热,即在启动服务之前,先将热点数据加载进缓存中去
- 互斥锁,避免大量的请求对数据库的同一个键进行操作,每次只能一个请求进行操作,其它请求排队。
缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落在数据库上,造成数据库短时间内承受大量的请求而崩掉。
解决方案:
- 接口增加校验,如用户鉴权校验,id做基础校验,id<0的直接拦截(业务层进行规避)
- 从缓存取不到数据,在数据库中也没有取到,这时可以将key-value对改写为key-null,缓存有效时间设置短点。这样可以防止攻击用户反复用同一个id暴力攻击。
- 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没有读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据查不到从而查数据库。
解决方案
- 设置热点数据永不过期
- 加互斥锁
-
简述Redis事务实现
-
redis集群方案 !!!
-
redis主从复制的核心原理
-
CAP理论、BASE理论
Consistency:客户端,即并发访问时,当前请求能够获取其它请求更新过的数据,即相互可见;服务端,即分布式的多台服务器,其中一台服务器数据发生更改,其余的服务器也要进行数据同步。
Availability:即服务一直可用,并且响应时间正常。
Partition Tolerance:即分布式系统中,发生网络分区故障,节点之间无法通行,依旧能够向外提供一致性和可用性的服务。对于用户而言没有体验上的影响。
CP和AP:之所以无法同时满足强一致性和可用性,是因为当发生网络分区故障时,势必会出现服务器数据不同步,因此有两种方案:1.等故障恢复,然后数据同步,再提供向外的服务,这样就满足了一致性,但牺牲了可用性;2.即便故障未恢复,依旧向外提供服务,这样只能满足可用性。
-
负载均衡算法、类型
Nginx主要在应用层进行负载均衡,LVS在传输层进行负载均衡。
-
分布式架构下,Session共享有什么方案
cookie-session:单服务器的Session,进行登录时,第一次用户需要提交账户密码,然后Session会在服务器端存储该用户的登陆态,返回一个该用户的登陆态id给cookie进行存储,下次用户再进行访问,就会自动携带该用户的登陆态id与服务器Session中的进行对比,如果成功,就会自动登陆。
问题:分布式架构下,由于是由多台服务器组成一个分布式系统,因此用户的登陆态可能只是存储某台服务器中,如果下次该用户的请求分配到其它的服务器上,就需要重新登陆。
- 采用无状态服务,抛弃session,可以使用JWT或者token机制
- 因为cookie存在客户端,因此会有风险
- 会引发其它同步延迟以及同步失败等问题
- 这样导致以后该用户的请求,只会在指定的服务器上进行访问,违背了负载均衡的意义。
- 使用redis存储,因为所有的服务器都可以从redis中存取值。
-
简述你对RPC、RMI的理解
RPC:在本地调用远程函数,远程过程调用,可以跨语言实现 httpClient
RMI:远程方法调用,java中用于实现RPC的一种机制,RPC的java版本,是J2EE的网络机制调用,跨JVM调用对象的方法,面向对象的思维方式
-
分布式id生成方案
-
分布式锁解决方案
需要这个锁独立于每一个服务之外,而不在服务里面
数据库:利用主键冲突控制一次只有一个线程才能获取锁,非阻塞、不可重入、单点、失效时间。
Zookeeper分布式锁:
zk通过临时节点,解决了死锁问题,一旦客户端获取到锁之后突然挂掉(session连接断开),那么这个临时节点就会自动删除掉,其它客户端自动获取锁。临时顺序节点解决惊群效应
惊群效应:即一个线程获取锁再执行,另外多个线程在阻塞等待,当这个线程执行完成后,释放出锁,如果唤醒全部的阻塞线程,就称为惊群效应。
Redis分布式锁:setNx,单线程处理网络请求,不需要考虑并发安全性。
所有服务节点设置相同的key,返回为0,则锁获取失败
setnx 问题: 1.早期版本没有超时参数,需要单独设置,存在死锁问题(中途宕机) 2.后期版本提供加锁与设置时间原子操作,但是存在任务超时,锁自动释放,导致并发问题,加锁和释放锁不是同一线程问题。(value存每个线程的uuid,如果为该线程的uuid,才能释放锁)
删除锁:判断线程的唯一标识,再删除
可重入性及锁续期没有实现,通过redisson解决(类似AQS的实习,看门狗监听机制)
redlock:以上的机制都只操作单节点,即使Redis通过sentinel保证高可用,如果这个master节点由于某些原理发生了主从切换,那么就会出现锁丢失的情况(redis)同步设置可能数据丢失
。redlock从多个节点申请锁,当一半以上节点获取成功,锁才能算获取成功,redission有相应的实现。
-
分布式事务解决方案
XA规范:分布式事务规范,定义了分布式事务模型
四个角色:事务管理器(协调者TM)、资源管理器(参与者RM)、应用程序AP、通信资源管理器CRM
全局事务:一个横跨多个数据库的事务,要么全部提交、要么全部回滚
JTA事务是java对XA规范的实现,对应JDBC的单库事务
第一阶段(prepare):每个参与者执行本地事务但不提交,进入ready状态,并通知协调者已经准备就绪
第二阶段(commit):当协调者确认每个参与者都ready后,通知参与者进行commit操作;如果有参与者fail,则发送rollback命令,各参与者进行回滚
问题:
- 单点故障:一旦事务管理器出现故障,整个系统不可用(参与者都会阻塞)
- 数据不一致:在阶段二,如果事务管理器发送commit消息,由于网络发送异常,那么只有部分参与者接受到commit消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。
- 响应时间长:参与者和协调者资源都被锁住,提交或回滚之后才能释放
- 不确定性:当事务管理器发送commit之后,并且此时只有一个参与者收到了commit,那么当参与者与事务管理器同时宕机之后,重新选举的事务管理器无法确定该条消息是否提交成功。
三阶段协议:主要针对两阶段的优化,解决了2PC单点故障问题,但是性能问题和不一致问题依旧没有根本解决
TCC:是通过纯业务逻辑代码实现分布式事务解决;2PC,3PC则是通过数据库的commit,rollback来解决分布式事务。
-
如何实现接口的幂等性
场景:用户点击注册按钮,提交信息,由于网络等因素,后台没反应,因此用户提交了多次,如何保证后台在接受相同数据时,只会对数据库操作一次。
- 唯一id。每次操作,都根据操作和内容生成唯一的id,在执行之前先判断id是否存在,如果不存在则执行后续操作,并且保存到数据库或者redis等(还是会访问数据,对数据造成压力)
- 服务端提供发送token的接口,业务调用接口前先获取token,然后调用业务接口请求时,把token携带过去,服务器判断token是否已经存在redis中,未存在表示第一次请求,可以继续执行业务,执行业务完成后,最后需要把redis中的token删除。
- 建去重表。将业务中有唯一标识的自动保存到去重表,如果表中存在,则表示已经处理过了
- 版本控制。增加版本号,当版本号符合时,才能更新数据
- 状态控制。例如订单有状态 已支付、未支付、支付中、支付失败,当处于未支付的时候才能允许修改支付中。
-
简述ZAP协议
-
zk的数据模型和节点类型
-
简述zk的命名服务、配置管理、集群管理
-
Zookeeper watch机制
-
zk和eureka的区别
zk:CP设计(强一致性),目标是一个分布式的协调系统,用于进行资源的统一管理。
当节点crash后,需要进行leader的选举,在这个期间,zk服务是不可用的。
eureka:AP设计(高可用),目标是一个服务注册发现系统,专门用于微服务的服务发现注册。
Eureka各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而Eureka的客户端在向某个Eureka注册时如果发现连接失败,会自动切换至其它节点,只要一台Eureka节点还在,就能保证注册服务可用(保证可用性),只不过查询的信息可能不是最新的(不保证强一致性)
同时当eureka的服务端发现85%以上的服务都没有心跳的话,他就会认为自己的网络出了问题,就不会从服务列表中删除这些失去心跳的服务,同时eureka的客户端也会缓存服务信息。eureka对于服务注册发现来说是非常好的选择。
-
Spring Cloud和Dubbo的区别
底层协议:springcloud基于http协议,dubbo基于tcp协议,决定了dubbo的性能相对会比较好
注册中心:Spring Cloud使用的eureka,dubbo推荐使用zookeeper
模型定义:dubbo将一个接口定义为一个服务,springCloud则是将一个应用定义一个服务
springCloud是一个生态,而Dubbo是SpringCloud生态中关于服务调用一种解决方案(服务治理)
-
什么是Hystrix?简述实现机制
分布式容错框架
- 阻止故障的连锁反应,实现熔断
- 如果某个请求的服务优先级不高,且占用资源较多,就会快速失败,实现优雅降级
- 提供实时的监控和警告
资源隔离:线程隔离、信号量隔离
- 线程隔离:Hystrix会给每一个command分配一个单独的线程池,这样在进行单个服务调用的时候,就可以在独立的线程池里面进行,而不会对其它线程池造成影响
- 信号量隔离:客户端向依赖服务发起请求时,首先要获取一个信号量才能真正发起调用,由于信号量的数量有限,当并发请求量超过信号量个数时,后续的请求都会直接拒绝,进入fallback(降级)流程。信号量隔离主要是通过控制并发请求量,防止请求线程大面积阻塞,从而达到限流和防止雪崩的目的。
熔断和降级:调用服务失败后快速失败
熔断是为了防止异常不扩散,保证系统的稳定性
降级:编写好调用失败的补救逻辑,然后对服务直接停止运行,这样这些接口就无法正常调用,但又不至于直接报错,只是服务水平下降。
-
springcloud核心组件以及作用 !!!
解释:Ribbon做负载均衡,即某个eureka client 从 eureka server 获取目标服务的地址时,而该目标服务由多个服务机器组成,访问其中一台目标服务机器都能完成请求,因此 Ribbon控制这些目标服务的负载均衡,根据每个机器的资源情况,将eureka client的请求转发到目标服务机器上去。
Zuul也是作为一个 eureka client ,当前端访问后台时,Zuul会根据该请求从eureka server 上找到目标服务的地址,然后进行访问。
-
Dubbo的整体架构设计及分层
-
简述RabbitMQ的架构设计
-
RabbitMQ如何确保消息发送?消息接受?
-
RabbitMQ事务消息
通过对信道的设置实现,发送端
- channel.txSelect(); 通知服务器开启事务模式;服务端会返回Tx.Select-Ok
- channel.basicPublic;发送消息,可以是多条,可以是消息提交ack
- channel.txCommit();提交事务
- channel.txRollback();回滚事务
解释:在开启事务的情况下,生产者发送消息给服务端,服务端不会直接把它存到queue中,而是放在临时队列中,当生产者提交事务后,服务端才会把临时队列的消息存到Queue中。由于使用事务,会经常访问服务端,因此性能不如confirm模式
消费者使用事务:
- auto=false,手动提交ack,以事务提交或回滚为准
- auto=true,不支持事务,也就是说你即使在收到消息之后再回滚事务也是于事无补,队列已经把消息移除了
如果在事务中任意一个环节出现问题,就会抛出IoException异常,用户可以拦截异常进行事务回滚,或决定要不要重复消息
事务消息会降低rabbitmq的性能
-
RabbitMQ死信队列、延时队列
解释:死信队列和死信交换机,即等同于普通的Queue和exchange;延时队列,即用一个Queue+死信队列实现,当Queue的消息的存活时间超出了TTL,就会转到死信队列中,因此消费者只需要消费死信队列的消息即可
-
简述kafka架构设计
-
Kafka消息丢失的场景及解决方案 !!!
-
消息发送
解释:ack=0,即ack累加最大到0,意味着服务端不需要返回ack,因此无ack可用;ack=1,即ack累加最大到1,意味着只能lead写入成功返回ack,但是这个时候leader crash,fallower没来得及同步数据,数据就会丢失;ISR存的都是数据同步好的节点,OSR存的是数据落后于leader的节点,因此如果让OSR也参加选举,会造成数据丢失
-
消费
解释:先把offset提交后,再处理,如果在处理offset位置的数据发送异常,没有消费成功,以后也不会消费到这个消息了
-
broker的刷盘
减小刷盘间隔
解释:刷盘的间隙发生数据丢失
-
-
Kafka是pull?push?优劣势分析
pull模式:
- 根据consumer的消费能力,consumer进行数据拉取,可以控制速率
- 可以批量拉取、也可以单条拉取
- 可以设置不同的提交方式,实现不同的传输语义
缺点:如果kafka没有数据,consumer会一直拉取空循环,消耗资源
优点:通过参数设置,consumer拉取数据为空或者服务端的数据没有达到一定数量时进行阻塞
push模式:不会导致consumer循环等待,由服务端主动推送数据
缺点:速率固定,忽略了consumer的消费能力,可能导致拒绝服务或者网络拥塞等情况。
-
Kafka中的zk的作用(新版本的kafka中,zk的作用越来越小)
-
Kafka中(读写)高性能的原因
KafKa不基于内存,而是移动硬盘,因此消息堆积能力更强
顺序写:利用磁盘的顺序访问速度可以接近内存,kafka的消息都是append操作,partition是有序的,节省了磁盘的寻道时间,同时通过批量操作(累计到一定量再一次性写入),节省写入次数,partition物理上分为多个segment存储,方便删除。
传统:
- 读取磁盘文件数据到内核缓冲区
- 将内核缓冲区的数据copy到用户缓冲区
- 将用户缓冲区的数据copy到socket的发送缓冲区
- 将socket发送缓冲区中的数据发送到网卡、进行传输
零拷贝:
- 直接将内核缓冲区的数据发送到网卡传输
- 使用的是操作系统的指令支持
kafka不太依赖jvm,主要使用操作系统的pageCache,如果生产消费速率相当,则直接使用pageCache交换数据,不需要经过磁盘IO