概念
1.JVM是java虚拟机,用来执行字节码文件(二进制 class文件)的虚拟计算机。除了java,Scala,Groovy和Python等其他语言经过处理也可以转换成字节码文件。
2.JVM运行在操作系统上,和硬件没有任何关系。
跨平台原理:编译后的字节码文件和平台无关,在java虚拟机上运行。统一的class文件结构,就是jvm的基石。
JVM分类:
程序执行方式有三种,静态编译执行,动态编译执行,动态解释执行。
在java中,程序的执行以动态解释为主,动态编译为辅。(静态编译如C,直接编译成可执行文件exe)
机器码和字节码的区别:
机器码是CPU直接读取,速度快;字节码需要直译器转译后才能变成机器码。
JDK包括了编译器等开发工具和JRE
JRE包括了运行类库和JVM
JVM有两种运行方式 client和server Client启动快,运行慢。Server启动慢,运行快。
JVM流程 .java文件编译器解释为class文件,交给jvm执行引擎执行,执行时会用空间存储数据,就是JVM内存。
JVM内存主要为:堆,栈,方法区,本地方法区,程序技术器。
在Hotspot的演变过程中:
堆中分为老年代和年轻代,年轻代中分为eden区和存活区,区中分为s0和s1
新生成的对象在Eden区
触发Minor GC后幸存的对象存入s0,再次触发Minor GC后,eden区和s0的对象存入s1中,s0清空。
每次移动,递增计数器,超过默认值15 (通过 -XX:+MaxTenuringThreshold 设置),移动到老年代中,eden中没有足够内存分配,也会分配到老年代。
老年代靠major GC。
新生代的回收机制采用复制算法,老生代采用的回收算法是标记整理算法。
栈:创建线程时创建,存储栈帧,线程私有。栈桢在执行方法时创建,包括局部变量表,操作数栈,动态链接,方法出口等信息。栈中数据生命周期短,出栈即失效。栈超过虚拟机允许最大深度StackOverflow
堆:存储对象,线程共享。堆中数据声明周期长,由垃圾回收机制不定期回收。空间不够扩展申请不到足够的内存,oom
参考【Java】垃圾回收
作用区域:频繁发生在年轻代,较少发生在老年代,极少发生在方法区(永久代/元空间)
引用类型才需要垃圾回收,基本数据类型不需要。
内存泄漏:这个对象不再使用,但是GC没法回收。
垃圾回收分为标记和清除阶段
引用计数法
引用对象+1,引用失效-1。为0则认为可以进行回收。
优点:实现简单,垃圾容易辨识;判定效率高,回收没有延迟
缺点:
1.需要单独的字段存储计时器,增加空间开销
2.每次赋值都需要进行加减法,增加时间开销
3.无法处理循环引用的情况
可达性分析算法
通过被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,走过的路径被称为引用链。当一个对象到GC roots没有任何引用相连时,证明该对象不可用。
同样具备实现简单和执行高效的特点,能有效解决循环依赖的问题,防止内存泄漏的发生。
可达性分析必须在一个能保证一致性的环境下进行。这点也是导致GC必须进行"stop the world"的一个重要原因。
所谓GC roots根集合就是一组必须活跃的引用。可以是:
1.虚拟机栈中引用的对象。
2.静态变量引用的对象,除非类卸载,否则他的引用对象一直存在。
3.所有被同步锁持有的对象。(同步锁要是被销毁,同步就失效了)
JVM中常见的清除方法:1.标记清除法 2.标记复制法 3.标记压缩法
标记清除法:
把存活的对象进行标记,清除死亡对象。
当堆中有效空间被用完,就会stw。然后进行标记和清除。要把用户线程停止保持一致性,防止用户线程产生垃圾。
缺点:
1、效率不高,需要遍历
2、进行GC时需要停止整个应用程序,用户体验差。
3、清理出来的空闲空间不是连续的,会产生碎片。
标记复制法:
内存分为两块,每次只用其中的一块。垃圾回收时,将存活的对象复制到未使用的一块,清除不可达的对象。
年轻代S0和S1也是用的复制算法。
优点:1.没有标记和清除的过程,实现简单,运行高效;2.复制后能保证空间的连续性
缺点:损失一半空间。
适合回收对象多的场景,复制少。适用于年轻代。
标记压缩算法
1.第一阶段和标记清除算法相同,从根节点开始标记被引用对象。
2.第二阶段将所有的存活对象压缩到内存的一端,按顺序排放,之后清理边界外所有的空间。
标记压缩算法等同于标记清除算法加压缩。
优点:1.没有碎片 2.不会内存减半
缺点:
1.效率低,因为会进行整理压缩
2.移动对象时,如果对象被其他对象引用,需要调整引用的地址。
3.移动过程中会stw
分代收集算法:
不同生命周期对象采用不同的算法,提高效率。
1.年轻代:区域小,生存周期短,回收频繁。
采用复制算法,内存利用率不高,hotspot中两个survivor的设计得以缓解
2.老年代:区域大,生命周期长,回收不频繁。
采用标记清除或者标记清楚整理算法。
Old GC: 只收集 old gen 的 GC。只有垃圾收集器 CMS 的 concurrent collection 是这个模式
Mixed GC: 收集整个 young gen 以及部分 old gen 的 GC。只有垃圾收集器 G1 有这个模式
年轻代的gc称为Minor GC。新生代Eden区满的时候触发Minor GC
Full GC是回收整个堆。触发条件为:
1.System.gc
2.老年代空间不足
3.方法区空间不足
4.Minor GC后进入老年代的平均大小大于老年代的可用大小
5.由Eden区,from 区向to区复制,对象大于to的内存,也大于老年代的内存。
单例模式是静态的,生命周期长,如果中间引用了别的对象,那么这个对象一直不会被回收。
Major GC通常是跟full GC是等价的,收集整个GC堆,但也有说法是old GC。
参考【Code皮皮虾】带你盘点双亲委派机制【原理、优缺点】,以及如何打破它?
双亲委派机制是在JDK1.2后才引入的。
加载类时不直接加载,委托给自己的父类加载器,递归直到加载成功。否则自己加载。
目的:1.防止类的重复加载。2.避免核心类遭到修改
Java提供四种类加载器:
什么时候破坏这个机制?
JDBC
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql", "root", "0000");
获取连接时的DriverManager因为处于rt下,会被启动类加载器加载。
类加载时,会执行静态方法。其中会加载所有实现了Driver接口的实现类,但是这些实现类都是第三方提供的,启动类加载器无法加载,因此引入了ThreadContextClassLoader(线程上下文类加载器,默认情况下是AppClassLoader)来使用应用程序加载器,破坏双亲委派机制。
tomcat
比如tomcat web容器里面部署很多应用程序,但是每个应用依赖的第三方类库版本不同,但是类的全路径名可能相同。
双亲委派无法加载多个相同的class文件,因此tomcat给每个web容器单独同一个webAppClassLoader加载器。实现隔离性,优先加载Web应用自己定义的类,加载不到再交给CommonClassLoader加载,这和双亲委派机制恰好相反。
如何打破双亲委派机制?
1.自定义类加载器:继承ClassLoader,不想打破,只需要重写findClass,想打破,重写整个loadClass方法,设定自己的类加载逻辑。
2.使用线程上下文类加载器
public class Main {
public static void main(String[] args) {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
}
}
hashmap
1.7 数组+链表
1.8 数组+链表+红黑树
ConcurrentHashMap简介
hashmap线程不安全。
hashtable线程安全,方法直接加synchronize锁,性能低下。
concurrentHashMap
对数组增加了voliate关键字
1.7 segment+hashentry 分段锁
1.8 cas+synchronized
getSize
1.7获取三次,两次一致返回,三次拿不到加锁进行计算
1.8 获取basecount
put方法
计算哈希值;
当前map是否为空,为空先初始化;
判断哈希值所在位置是否有值,没值直接cas替换
有值判断key是否相等,相等不变
不相等判断是红黑树还是链表,进行循环,key相同替换,未找到相同的进行增加
16 张图带你搞懂 Java 数据结构
常见面试的查找和排序算法
面试高频考点 – 常见的排序算法(7种)
将数组放入堆,调整每一颗子树,使之变成大根堆。
O(n * logN) O(1) 不稳定
package com.ln.mybatis.sort;
import java.util.Comparator;
import java.util.PriorityQueue;
//求前k个最小的元素
public class sortTest {
public static void main(String[] args) {
int test[]={10,11,12,32,434,1,2,3,4,5,6,7,8,9,10,11,12,32,434,54645,6565,757,323,32};
int[] topk=topK(test,3);
for (int i = 0; i < topk.length; i++) {
System.out.println(topk[i]);
}
// System.out.println(topK(test,3));
}
// 找出数组中最小的元素,建立大根堆,然后对根节点进行比较,大则不放,小则替换
public static int[] topK(int[] arr,int k){
// 创建一个大小为k的大根堆
PriorityQueue<Integer> maxHeap =new PriorityQueue<>(k, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return 02-01;
}
});
for (int i = 0; i < arr.length; i++) {
if(i<k){
maxHeap.offer(arr[i]);
}else {
if(maxHeap.peek()>arr[i]){
maxHeap.poll();
maxHeap.offer(arr[i]);
}
}
}
int[] ret=new int[k];
for (int i = 0; i <k ; i++) {
ret[i]=maxHeap.poll();
}
return ret;
}
}
在使用完成之后需要remove掉,避免内存泄漏。ThreadLocal变量的key为弱引用,使用完成后TheadLocal没有使用的强引用后会释放,但是value是强引用,只要线程存活,一直存在强引用,需要通过remove删除Entry.线程池尤为严重。
public class SingleTonObj {
private static volatile SingleTonObj singleTonObj;
private SingleTonObj() {
}
public static SingleTonObj getObj() {
if (singleTonObj == null) {
synchronized (SingleTonObj.class) {
if (singleTonObj == null) {
singleTonObj = new SingleTonObj();
}
}
}
return singleTonObj;
}
}
深度剖析Java常量池
通过javap命令生成更可读的JVM字节码指令文件:javap -v Math.class
Class常量池可以理解为Class文件中的资源仓库。Class文件中除了包含版本,字段,方法,接口,还有一项信息就是常量池,用于存放编译器生成的各种字面量和符号引用。
int a=1 1为字面量,a为符号引用
字面量是指由字母,数字构成的字符串和数值常量,字面量只可以右值出现。
符号引用是编译原理中的概念,是相对于直接引用来说的,主要包括了以下三大类:
intern
1.6 在常量池中寻找equal()相等的字符串,存在则返回常量池中的引用。不存在就在永久代的常量池中新建一个实例,放入常量池中并返回。
1.7:不存在永久代,常量池存在返回,不存在可以直接指向堆上的实例。
> String s0="zhigan";
String s1="zhigan";
String s2="zhi" + "gan";
System.out.println( s0==s1 ); //true
System.out.println( s0==s2 );//true
字面量声明的字符串常量在编译期就能确定,会存储在常量池中,地址相同。
String s0="zhigan";
String s1=new String("zhigan");
String s2=“zhi”+new String("gan");
System . out . println ( s0 == s1 ); // false
System . out . println ( s0 == s2 ); // false
System . out . println ( s1 == s2 ); // false
new String() 的字符串不是常量,不能在编译期确定,不放入常量池,他们有自己的地址空间。
String a="a3.4";
String b="a"+3.4;
System . out . println ( a == b ); // true
jvm对于加号连接,在编译器就会进行优化,将常量字符串连接。
String a="ab";
String bb="b";
String b="a"+bb;
System . out . println ( a == b ); // false
在+中带有引用,JVM无法优化,因为引用无法在编译期确认,只能在程序运行是动态分配,并将连接后的新地址赋值给B,因此为false(如果bb是一个方法的返回结果,同样的原因)如果bb用final修饰,那么他在编译期会被解析为常量,比较的结果为true。
String s = "a" + "b" + "c" ; // 就等价于 String s = "abc";
String a = "a" ;
String b = "b" ;
String c = "c" ;
String s1 = a + b + c ;
s1这个就不一样,可以通过观察器JVM指令码发现s1的"+"操作会变成如下:
StringBuilder temp=new StringBuilder();
temp.append(a).append(b).append( c );
String s=temp.toString();
Java八大基本对象的包装类型除了两个浮点型,其他都有常量池。另外Byte,Short,Int,Long,Character这五种整形的包装类也只是对应值小于127才可以使用对象池。
参考 面试官:请详细说下synchronized的实现原理 - 知乎
概念:在多线程情况下,多个线程访问共享资源会出现问题,而synchronized关键字则是用来保证线程同步的
synchronized解决可见性的方式是,每次都清除工作内存,从主内存中重新获取。
synchronized可以保证并发编程的三大特性:原子性,可见性,有序性。
synchronized可以实现悲观锁,非公平锁,可重入锁,独占锁或者排它锁。
实现原理:
Java虚拟机是通过进入和退出Monitor对象来实现代码块同步和方法同步的,代码块同步使用的是monitorenter和 monitorexit 指令实现的,而方法同步是通过Access flags后面的标识来确定该方法是否为同步方法。
jDK1.6对synchronized做了哪些优化?
引入偏向锁和轻量级锁,随着竞争的激烈而升级。
引入偏向锁的目的:减少只有一个线程执行同步代码块时的性能消耗,即在没有其他线程竞争的情况下,一个线程获得了锁。
使用CAS操作将当前线程的ID记录到对象的Mark Word中。
引入轻量级锁的目的:在多线程交替执行同步代码块时(未发生竞争),避免使用互斥量(重量锁)带来的性能消耗。但多个线程同时进入临界区(发生竞争)则会使得轻量级锁膨胀为重量级锁。
将对象的Mark Word复制到当前线程的Lock Record中,并将对象的Mark Word更新为指向Lock Record的指针。
锁消除是指java编译时,消除不可能发生共享资源竞争的锁。
锁粗化是指java编译时,将不必要的重复加锁粗化到整个操作的外部。
for(int i=0;i<n;i++){
synchronized(lock){
}
}
//粗化后
synchronized(lock){
for(int i=0;i<n;i++){
}
}
参考粗谈synchronize和Lock锁的区别及使用场景
区别
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
接口方法
Lock lock = ...; //声明锁
lock.lock(); //获得锁
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
Lock lock = ...;
if(lock.tryLock()) {
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则直接做其他事情
}
synchronize和reentrantlock都是可重入锁。
就是一个线程不用释放,可以重复的获取一个锁n次,只是在释放的时候,也需要相应的释放n次。synchronize不用手动释放。
实现
非公平锁:如果同时还有另一个线程进来尝试获取,那么有可能会让这个线程抢先获取;
公平锁:如果同时还有另一个线程进来尝试获取,当它发现自己不是在队首的话,就会排到队尾,由队首的线程获取到锁。
参考准备用HashMap存1W条数据,构造时传10000还会触发扩容吗?存1000呢?
不指定容量,会随着数据的增加不断扩容,影响性能。
在指定调用容量的构造方法时,会重新调用另一个构造方法,传入默认的负载因子0.75
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
构造方法中初始化了两个成员变量。threadHold 扩容阈值和 loadFactor 负载因子。
tableSizeFor就是找到大于入参的2的整数次方,如传入10,会得到16.
设置容量为2的整数次方是为了减少哈希冲突。
推荐在集合初始化的过程中指定集合初始化大小为 ((需要存储的元素个数)/0.75)+1
参考 为什么HashMap中的键往往都使用String?
1.String重写了hashCode,两个不同引用的String类型,只要值相等,hashcode就相等,而非地址相等。(设计 hashCode() 时最重要的因素就是对同一个对象调用 hashCode() 都应该产生相同的值。)
2.String不可变,每当创建一个字符串对象,他的hashcode就被缓存下来,所以存储hashMap不用重新计算,相比于其他对象更快。
参考并发编程(三):线程池基本面试题(必背题目)
1. 作用:限制系统中执行线程的数量。
1.降低资源消耗:复用线程。
2.提高效率,提前创建,使用从中获取节省创建时间。
3.增加线程的可管理型。线程是稀缺资源,使用线程池可以进行统一分配,调优和监控。
2. 常见线程池:
1.SingleThreadExecutor
只有一个线程,保证任务按顺序执行(FIFO,LIFO,优先级)
任务队列为链表结构的有界队列
2.FixedThreadPool
定长线程池,超出等待。只有核心线程,执行完成后回收。
任务队列为链表结构的有界队列。
3.CachedThreadPool
超出回收空闲线程,没有则创建线程。核心线程固定,非核心线程无限。使用完成后闲置10分钟回收。任务队列为延时阻塞队列。
4.ScheduledThreadPool
定时执行任务线程池
无核心线程,非核心线程数量无限,执行完闲置 60s 后回收,任务队列为不存储元素的阻塞队列。
5.WorkStealingPool:一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用cpu数量的线程来并行执行。
3. 线程池中的几个重要参数
//TreadPoolExecutor(自定义参数线程池)(推荐使用)
public class ThreadPoolDemo {
public static void main(String[] args) {
//1. 使用ThreadPoolExecutor指定具体参数的方式创建线程池
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
2, //核心线程数
5, //池中允许的最大线程数
2, //空闲线程最大存活时间
TimeUnit.SECONDS, //秒
new ArrayBlockingQueue<>(10),//被添加到线程池中,但尚未被执行的任务
Executors.defaultThreadFactory(), //创建线程工厂,默认
new ThreadPoolExecutor.AbortPolicy()//,如何拒绝任务
);
//2. 执行具体任务
poolExecutor.submit(new MyRunnable());
poolExecutor.submit(new MyRunnable());
//3. 关闭线程池
poolExecutor.shutdown();
}
}
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"执行了");
}
}
4. 拒绝策略
任务不断过来,系统无法及时处理,就要拒绝。
5. execute和submit的区别
execute适用于不需要关注返回值的场景,只需要将线程丢到线程池中去执行就可以了。
submit方法适用于需要关注返回值的场景
6. 线程池的关闭
shutdownNow:对正在执行的任务全部发出interrupt(),停止执行,对还未开始执行的任务全部取消,并且返回还没开始的任务列表。
shutdown:当我们调用shutdown后,线程池将不再接受新的任务,但也不会去强制终止已经提交或者正在执行中的任务。
7. 线程数的选择
计算密集型:应为 cpu核数+1 减少上下文切换
即使当密集型的线程由于偶尔的内存页失效或其他原因导致阻塞时,这个额外的线程也能确保 CPU 的时钟周期不会被浪费,从而保证 CPU 的利用率。
IO密集型:
线程数 = CPU 核心数 * (1 + IO 耗时/ CPU 耗时)
IO比cpu慢,设置过少线程数会造成cpu资源的浪费。
等待时间越长,线程越多。
8. 线程池都有哪几种工作队列
1、ArrayBlockingQueue
是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
2、LinkedBlockingQueue
一个基于链表结构的阻塞队列,在未指定容量时,容量默认为 Integer.MAX_VALUE.此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列
3、SynchronousQueue
一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
4、PriorityBlockingQueue
一个具有优先级的无限阻塞队列。
5、DelayQueue:类似于PriorityBlockingQueue,是二叉堆实现的无界优先级阻塞队列。要求元素都实现 Delayed 接口,通过执行时延从队列中提取任务,时间没到任务取不出来。
9. 线程工作流程
提交任务,线程是否达到核心线程数,未达到,创建核心线程,否则下一步
查看任务队列是否已满,未满,放入任务队列,否则,进入下一步
线程是否到达最大线程数,未到,创建非核心线程执行任务,否则执行饱和策略,默认抛出异常。
随着任务的增加增加活跃线程数,活跃线程数=核心线程数
10.线程池优化
参考Java—优化 if-else 代码的 8 种方案
1.提前return,去除不必要的else
if(xx){
}
else{
return
}
--------
if(!xx){
return
}
2.使用三元表达式
3.使用枚举类
String OrderStatusDes;
if(orderStatus==0){
OrderStatusDes="订单未支付";
}else if(OrderStatus==1){
OrderStatusDes="订单已支付";
}else if(OrderStatus==2){
OrderStatusDes="已发货";
}
---------------------
String OrderStatusDes = OrderStatusEnum.0f(orderStatus).getDesc();
4.合并条件表达式
结果相同,合并表达式
5.使用Optional优化if,else
String str = "jay@huaxiao";
if(str != null) {
System.out.println(str);
} else{
System.out.println("Null");
}
-----------------------------
Optional<String> strOptional = Optional.of("jay@huaxiao");
strOptional.ifPresentOrElse(System.out::println, () -> System.out.println("Null"));
6.表驱动法
又称之为表驱动、表驱动方法。表驱动方法是一种使你可以在表中查找信息,而不必用很多的逻辑语句(if或case)来把它们找出来的方法。以下的demo,把map抽象成表,在map中查找信息,而省去不必要的逻辑语句。
if(param.equals(value1)) {
doAction1(someParams);
} else if(param.equals(value2)) {
doAction2(someParams);
} elseif(param.equals(value3)) {
doAction3(someParams);
}
---------------
// 这里泛型 ? 是为方便演示,实际可替换为你需要的类型
Map<?, Function<?> action> actionMappings = newHashMap<>();
// 初始化
actionMappings.put(value1, (someParams) -> { doAction1(someParams)});
actionMappings.put(value2, (someParams) -> { doAction2(someParams)});
actionMappings.put(value3, (someParams) -> { doAction3(someParams)});
// 省略多余逻辑语句
actionMappings.get(param).apply(someParams);
7.使用策略模式
例如支付场景下,支持多种支付方式
public class PaymentService {
CreditService creditService;
WeChatService weChatService;
AlipayService alipayService;
public void payment(PaymentType paymentType, BigDecimal amount) {
if (PaymentType.Credit == paymentType) {
creditService.payment();
} else if (PaymentType.WECHAT == paymentType) {
weChatService.payment();
} else if (PaymentType.ALIPAY == paymentType) {
alipayService.payment();
} else {
throw new NotSupportPaymentException("paymentType not support");
}
}
}
enum PaymentType {
Credit, WECHAT, ALIPAY;
}
作者:小黑说Java
链接:https://juejin.cn/post/7030976391596212255
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
这种不满足开闭原则(对修改关闭,对扩展开放),修改后需要对其他支付方式进行测试。
策略设计模式是一种行为设计模式。当在处理一个业务时,有多种处理方式,并且需要再运行时决定使哪一种具体实现时,就会使用策略模式。
抽象支付方式为一个策略接口
public interface PaymentStrategy {
public void payment(BigDecimal amount);
}
针对具体的支付方式做实现
public class CreditPaymentStrategy implements PaymentStrategy{
@Override
public void payment(BigDecimal amount) {
System.out.println("使用银行卡支付" + amount);
// 去调用网联接口
}
}
public class WechatPaymentStrategy implements PaymentStrategy{
@Override
public void payment(BigDecimal amount) {
System.out.println("使用微信支付" + amount);
// 调用微信支付API
}
}
重新实现支付服务paymentservice
public class PaymentService {
/**
* 将strategy作为参数传递给支付服务
*/
public void payment(PaymentStrategy strategy, BigDecimal amount) {
strategy.payment(amount);
}
}
策略模式优化后
public class StrategyTest {
public static void main(String[] args) {
PaymentService paymentService = new PaymentService();
// 使用微信支付
paymentService.payment(new WechatPaymentStrategy(), new BigDecimal("100"));
//使用支付宝支付
paymentService.payment(new AlipayPaymentStrategy(), new BigDecimal("100"));
}
}
在使用了策略模式之后,在我们的支付服务PaymentService中便不需要写复杂的if…else,如果需要新增加一种支付方式,只需要新增一个新的支付策略实现,这样就满足了开闭原则,并且对其他支付方式的业务逻辑也不会造成影响,扩展性很好。
数组:随机查询快,增删慢;内存连续;
链表:随机查询慢,增删快;空间分散,不需要连续;
数组固定大小,在编译期间分配内存;链表动态灵活,在执行或者运行时分配内存;
链表因为要存储上一个和下一个的引用元素,因此需要更多的内存;
链表内存利用率更高;
对于想要快速访问数据,不经常有插入和删除元素的时候,选择数组。
对于需要经常的插入和删除元素,而对访问元素时的效率没有很高要求的话,选择链表。
ArrayList,LinkedList,Vector的区别
ArrayList动态数组,默认容量10,扩容为1.5倍
LinkedList双向链表
LinkedList还实现了Deque接口,所以LinkedList还可以用作队列
Vector也是数组,线程安全,每次扩容一倍。
参考HashMap和Hashtable的区别
1.hashMap线程不安全
hashtable线程安全,用synchronized关键字实现
2.hashMap允许null作为键或者Value,HashTable会抛出异常
3.hashtbale使用的是key的hashcode,hashMap计算hash对key的hashcode进行了二次hash,以获得更好的散列值,然后对table数组长度取摸。
4. HsahMap在数组+链表的结构中引入了红黑树,Hashtable没有
5.HashMap初始容量为16,Hashtable初始容量为11
5. HsahMap扩容是当前容量翻倍,Hashtable是当前容量翻倍+1
6. HsahMap只支持Iterator遍历,Hashtable支持Iterator和Enumeration
使用场景:
@Autowired @Component @RestController @Cacheable @RequestMapping @Value @Bean @Import
@Autowired和@Resource的区别
A默认byType,可以通过@Qualify指定bean名称。是spring的注解,默认必须存在bean,可以用required=false设置
R默认ByName,有name和type两种属性,先找name,name没有匹配type。是java的注解。
新建 就绪 运行 阻塞 死亡
wait和sleep的区别
wait释放锁,需要notify唤醒,sleep不释放锁,自动唤醒。
run和start的区别
run直接运行,start是进入就绪状态。
造成可见性的原因是JAVA内存模型JMM,在java内存模型中,共享变量存放在主内存中,每个线程都有自己的工作内存,操作共享变量需要从主内存中获取,但是何时写回主内存不可预知,这就导致每个线程变量的操作是封闭的,其他线程不可见的。
CPU快内存慢,一般都用寄存器解决。
可以加synchronized关键字,进入synchronize代码块,会清除缓存,从主内存中获取共享变量,进行操作,刷新回主内存,然后释放锁。
效率低下,出现了缓存一致性协议,有MSI,MESI,MOSI等,最出名的是Intel的MESI协议,保证了每个缓存中使用的共享变量的副本是一致的。
使用volatile等于告诉CPU需要MESI协议和嗅探机制来保证可见性。
MESI机制:
1.Modify:当缓存中的数据被修改时,该缓存设置为M状态
2.Eclusive(独占):当只有一个缓存使用某行数据时,设置为E状态
3.Share(共享):当多个CPU有数据的缓存,该数据的缓存设置为S状态
4.Invalid(无效):当某个数据的缓存修改时,其他持有该数据的缓存更新为I状态
核心思想:CPU修改数据,发现该数据是共享变量,会发出通知让其他CPU将该变量的缓存置为无效状态,因此当其他CPU需要读取这个变量的时候,发现自己的缓存行是无效的,那么他就会重新读取。
监听和通知基于总线机制。总线嗅探机制就是一个监听器。
2.有volatile修饰的共享变量在写之前会多出一条lock指令
lock前缀会触发
1.将当前缓存行的数据写会主内存
2.这个写回操作会使其他CPU中缓存了该内存地址的数据无效。
JMM:
1.lock前缀会将线程工作内存中的缓存数据写回主内存
2.通过缓存一致性协议,其他线程如果工作内存中使用了该变量的值,就会失效
3.其他线程会重新从主内存获取新的值、
大量使用volatile会导致总线风暴。
volatile保证数据的可见性,但不保证数据操作的原子性。在多线程环境下,使用volatile变量是线程不安全的,可以使用锁机制或者原子类。
禁止指令重排
编译器不会对volatile读以及volatile后面的任务内存操作重排序。
通过内存屏障来实现。
1.通过hashcode方法获取hash值
2.在hash表中查找,如果不存在则添加成功。如果hash表中含有该值,则进行equals比较,相同添加失败,不同添加到已有对象链末尾。
浅拷贝 对象的引用变量还是指向原对象地址
深拷贝 对象的引用变量指向不同,会新建引用对象
一般都是浅拷贝,深拷贝的是实现方式:1.重写clone方法,克隆引用成员变量;2.字节流写入文件再读出来;3.构造函数传参等。
1.在条件字段加索引
2.创建临时表
3.在a表中加入b表的字段,单查a表
参考MySQL如何查看SQL查询是否用到了索引?
explain+sql
EXPLAIN select * from tb_brand where id='1';
type 性能
由好到坏:system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
至少range,最好ref
possible_keys 查询用到的索引,没有的话值为null
key 实际决定查询结果使用的索引,没有的话值为null
rows为执行查询时必须检查的行数
参考Oracle通过执行计划查看查询语句是否使用索引
explain plan for +sql
select * from table(dbms_xplan.display)
TABLE ACCESS FULL为全表扫描;
index range scan为索引范围扫描;
常见的索引类型扫描:
慢查询SQL
是指超过指定时间的sql。在mysql中默认关闭,手动开启
show variables like 'slow_query%';
show variables like 'long_query_time';
参数说明如下:
slow_query_log:慢查询开启状态
slow_query_log_file:慢查询日志存放的位置(一般设置为 MySQL 的数据存放目录)
long_query_time:查询超过多少秒才记录
mysql> set global slow_query_log=ON;
Query OK, 0 rows affected (0.05 sec)
mysql> set global long_query_time=0.01;
Query OK, 0 rows affected (0.00 sec)
超过指定时间就会记录在sql中
尽量避免用text类型,采用es或者oss存储
字段尽量设置非null,会影响索引稳定
记得写注释comment
数据重要的情况下采用innodb,而且支持事务操作
减少索引大小可以采用前缀索引,但是前缀索引不能消除group by ,order by带来排序开销
经验:新核心迁移,编码不一致,扩容 gbk 到utf-8
建立索引常用的规则
索引是帮助数据库高效获取数据的数据结构。
数据库除了维护数据,还维护着满足特定查找算法的数据结构,数据结构以某种方式指向引用数据,称为索引。
MySQL索引的数据结构
MySQL索引的数据结构
Oracle是B树,Mysql是B+树
参考面试官:你知道多少种索引?
索引,目录,提高查询效率。
索引常用的实现方式 B树,B+树 。hash也可以。
B树就是平衡树,时间复杂度为logn
B+树基于B树实现,是有序的。
为什么有B树,还要有B+树?
B+树的叶子节点会指向下一节点,遍历查找更快。
非叶子节点不存储数据,key更紧密,数据查询更稳定和迅速,因为更好的利用空间局部性原理。
为什么使用B+树?
数据库访问通过页,尽量减少IO操作次数,因此树的层级要尽可能的少。
B树相比于二叉树,B树的非叶子结点可以有多个子树,因此B树高度远远小于AVL树和红黑树,磁盘IO数大大减少。
B+树相比于B树
1.非叶子节点不存储数据,存储的数据更多,因此B+树的高度更低,更少的磁盘IO操作。由于每个节点存储的记录更多,对局部性原理的利用更好,缓存的命中率更高。
2.更适合范围查找:B+树只需要对链表进行遍历,但是B树需要找到查询下限,然后进行中序遍历,直到找到查询的下限。
3.更稳定的查询效率:B+树的查询复杂度稳定为树高,因为所有数据都在叶子节点。
哈希也可以,但是会有两个缺点:1.哈希冲突。2.哈希计算的是个值,无法进行范围查询。
Mysql的innodb使用的是B+树。
1,普通索引:普通索引是最基本的索引,它没有任何限制,值可以为空;仅加速查询。
2,唯一索引:唯一索引与普通索引类似,不同的就是:索引列的值必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一。
3,主键索引:主键索引是一种特殊的唯一索引,一个表只能有一个主键,不允许有空值。
4,组合索引:组合索引指在多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。使用组合索引时遵循最左前缀集合。
5,全文索引:应用场景为百度和淘宝的搜索框。关键字查找,不支持大小写,索引创建慢。mysql中的MyIsam支持,innnodb不支持。不推荐使用,一般用es实现。
6.聚集索引
7.辅助索引:根据条件查询出主键,通过主键返回聚集索引,根据聚集索引的主键排序,找到条件对应的数据
mysql默认引擎innodb分为两种索引,聚簇索引和非聚簇索引,每个索引对应一颗b+树,两者的区别主要是叶子节点存储的数据不同,聚簇索引存储行数据,非聚簇索引存储聚簇索引,因此需要进行第二次查询,称为回表查询。
聚簇索引一般是指主键索引,一张表只能有一个。查询效率高。
非聚簇索引也称二级索引,数量不限制,查询效率低。
聚簇索引数据和索引都在一块存储。非聚簇索引叶子节点指向存储节点的位置,数据和索引分开存储。
在innodb中,在聚簇索引上创建的索引称为辅助索引,像非聚簇索引,复合索引,前缀索引,唯一索引。辅助索引叶子结点不存储数据,存储聚簇索引,总是需要二次查找。
聚簇索引具有唯一性,因为索引和数据在一块存储。
表中行的物理顺序和索引中的行物理顺序是相同的。
聚簇索引默认是主键,如果没有主键,会选择一个非空的列来做聚簇索引(类似oracle中的rowid)。如果想指定非主键做聚簇索引,删除主键,设置聚簇索引,然后重新设置主键即可。
myisam使用非聚簇索引,主键叶子结点存储主键,辅助键索引存储辅助键,都指向表数据,对数据来说这两种键没有区别。索引树独立,无法通过辅助键访问主键。
聚簇索引优势
1.数据和索引叶子结点存储在一起,每页有多行数据,查询其中一条会加载到buffer(缓存器)中,访问同页其他行就不会访问磁盘直接返回,查询更快。
(一次iO读写会获取16k的资源,读取到的数据区域称为Page,b+树,一个叶子节点上有多条数据和索引值,因此数据在叶子节点上不需要重复查询,走缓存。只有页分裂,即数据不存在才重新申请IO,)
2.当行数据发生变化,只需维护聚簇索引。非聚簇索引叶子节点存储主键,更节约空间。
3.myisam使用非聚簇索引,地址凌乱,拿到地址,按照合适的算法进行IO读取,聚簇索引只需一次。
4.涉及大数据量的排序,全表扫描,count,非聚簇索引更快,因为索引小,这些操作都是在内存中完成的。
聚簇索引中物理顺序和索引顺序一致,因此建议使用自增主键,不用uuid。默认会在索引树的末尾增加主键值,对索引树的结构影响最小。索引紧凑,磁盘碎片少,效率高。
主键大小也会影响,因为辅助索引中存储的主键,导致索引存储内存增多。影响IO操作读取的数据量。
优点:
1.查询快,索引和数据在一块
2.主键排序和范围查找很快
缺点:
1.插入速度依赖于插入顺序,一般主键自增
2.更新主键代价高,需要移动行
3.二级索引访问需要进行两次索引查找
mylsam查询比innodb速度快的原因
1.innodb不会压缩索引 查询时需要缓存数据块,而mylsam数据和索引是分开的,可以压缩索引,在相同容量的内存加载更多的数据
2.innodb寻址要映射到块,再到行(个人猜想主要是查询行的版本号),MYISAM记录的直接是文件的OFFSET,定位比INNODB要快
(注释: SELECT InnoDB必须每行数据来保证它符合两个条件: 1、InnoDB必须找到一个行的版本,它至少要和事务的版本一样老(也即它的版本号不大于事务的版本号)。这保证了不管是事务开始之前,或者事务创建时,或者修改了这行数据的时候,这行数据是存在的。2、这行数据的删除版本必须是未定义的或者比事务版本要大。这可以保证在事务开始之前这行数据没有被删除)
3)INNODB还需要维护MVCC一致;虽然你的场景没有,但他还是需要去检查和维护MVCC (Multi-Version Concurrency Control)多版本并发控制
在test表上对列a,b,c建立联合索引
联合索引采用左前缀命中规则
从左到右匹配直到匹配终止条件。
终止条件为范围操作符,函数等不能应用索引的情况。
和顺序无关,mysql优化器会进行优化。
a‹› range索引
or 不使用索引
order by 没有影响
a= and b‹› 走索引
数据库事务是一个不可分割的数据库操作序列,也是数据库控制并发的基本单位,其执行结果为使数据库从一种状态到另一种状态
事务的特性 ACID
原子性 Atomicity:事务中的操作要么全部成功,要么全都失败
一致性 Consistency:事务执行前后,数据库中的数据具有一致性
隔离性 Isolation:多个事务之间不会相互影响
持久性 Durability:事务提交后,对数据库的改变是永久性的
脏读:读到了未提交的数据
不可重复读:两次读取到的数据内容不一致,这是update引起的
幻读:两次读取到的数据数量不一致,这是insert或者delete引起的
事务的隔离性
读取未提交:会发生脏读、不可重复度、幻读
读取已提交:避免脏读 Oracle默认
可重复读:避免脏读,不可重复读 Mysql默认
可串行化:最高隔离级别,事务顺序执行,不会相互影响
注意:
--mysql
start transaction; -- 开启事务
savepoint abc;--设置回滚点
rollback to abc;--回滚到abc
commit; -- 提交事务
rollback; -- 回滚事务
--查看MYSQL中事务是否自动提交
show variables like '%commit%';
--关闭自动提交
set autocommit = 0;-- 0:OFF 1:ON
set session transaction isolation level --隔离级别;
--eg: 设置事务隔离级别为:read uncommitted,read committed,repeatable read,serializable
set session transaction isolation level read uncommitted;
--查询当前事务隔离级别
select @@tx_isolation;
对行加锁,不能增删改,其他人也不能select for update
for update 使用场景:高并发情况下对数据有很强的要求
排他锁的申请前提,没有线程对结果集的数据进行排他锁和行级锁,否则会申请阻塞
共享更新锁的释放条件:1.commit 2.推出数据库 log off 3.程序停止运行
数据一致性
悲观锁:先锁后更 乐观锁:先更后比较 悲观锁适合频繁更新的情况,乐观锁适合频繁查询的情况
行锁和表锁
innoDb默认行级锁,指定主键是行级锁,否则是表级锁
for update仅适用于InnoDB,必须在实务块中才能生效(Begin Commit)
总结
1.innodb行锁是根据索引项加锁实现的,只有通过索引查询数据才会产生行锁,否则只会产生表锁
2.由于mysql的行锁是针对索引加的锁,不是对记录进行加锁,因此即便查询不同行的记录,但是索引相同也会发生锁冲突
3.当表有多个索引,不同的事务可以用不同的索引锁定不同的行。不论是使用主键索引,普通索引还是唯一索引,innodb都会使用行锁来进行加锁
4.即便查询条件中用到了索引,但是因为sql执行计划不一定采用索引,因此分析锁冲突时,需要检查sql执行计划,判断是否真的走了索引
oracle
select for update 当发现有人修改时,等待修改完成之后再去查询
for update nowait 发现有人修改,直接异常
for update wait 3 超过三秒未更新结束,抛出异常
insert,delete,update,select for update 未提交事务的情况下
alter table 修改表结构
truncate table清空表数据
mysql手动加锁
GET_LOCK()和RELEASE_LOCK()函数来获取和释放表锁。
锁表发生在并发而非并行。
减少锁表概率:缩短数据库事务开启和提交时间:具体:批量执行改为单个执行,优化sql执行速度。
什么时候发生幻读,如何解决?
幻读产生的条件:
在可重复读的隔离界别下,普通的查询都是快照读,查询不到别的事务新插入的数据,所以幻读是在当前读的状态下产生。
快照读:读取的是快照数据,不加锁的查询读取的都是快照数据
当前读:读取数据库最新数据,加锁的查询和增删改都会进行行当前读
解决幻读,innodb加入了间隙锁,间隙锁不仅可以锁住数据行的实体,也可以锁住数据行之间的间隙。
会造成并发下降。
解决方案:
1.降低为读取已提交,但是为了解决数据和日志不一样的问题,需要把binlog格式设置为row。
–日志不一样的原因:binlog日志是以commit顺序为准,如果第一个事务更新数据但未提交,第二个事务更新数据满足第一个事务中的更新条件但是提交了,日志中第二个日志在前,第一个日志在后。看着就是把第一个事务中的数据也更新了
RC情况下默认是statement,记录顺序是以commit方式提交的。
row会记录每一列之后的值。
2.判断有无数据,有则删除,无数据不删除,因为删除不存在的数据一定要加间隙锁
3.间隙锁只针对写锁,只要两个线程锁定的区间有交叉就会出现死锁。可以根据条件查询出所有主键,根据主键删除数据,这样只会加行锁。
间隙锁产生的条件:
执行当前读,where条件没有命中索引(命中索引加行锁,没有索引加表锁)
innodb_locks_unsafe_for_bin_log 默认false 启用间隙锁。
间隙锁和行锁的组合称为netxt-key lock 临间锁,每个next-key lock 临间锁 是前开后闭区间,n行数据,产生n+1个next-key 锁
数据 1,2,3 select * from table for update 产生四个next-key锁,(-∞,1],(1,2],(2,3],(4,+∞],此时另一个事务进行插入是无法插入的,避免了幻读的产生
间隙锁是在可重复读的隔离级别下生效。
只有可重复读的隔离界别的当前读才会出险幻读。
增加排他锁时,会对结果集的行增加间隙锁。
mysql共享锁 sql + lock in share mode
1.允许其他事务增加共享锁
2.不允许其他事务增加排他锁
3.多个事务共同添加,必须等待先执行的事务commit后才行。
排他锁 sql + for update
1.事务之间不允许其他排他锁和共享锁读取,修改更不允许
2.所有事务只有一个排他锁执行commit后,其他事务才可以执行
mvcc
多版本并发控制,解决读写时的线程安全问题,线程不用去争抢读写锁。
隔离性:通过加锁(当前读)和MVCC(快照读)实现。
一致性:通过undolog,redolog,隔离性共同实现。
mvcc的实现,基于undolog,版本链,readView。
在mysql存储过程中,会隐式的定义几个字段:
trx_id:事务id,每次进行一次事务操作,自增1
roll_pointer:回滚指针,用于找到上一个版本的数据,结合undolog进行回滚
使用select读取数据,这是时刻的数据会有多个版本,通过readview来判断能够读取哪个版本
readview中包含以下字段:
m_ids:活跃的事务id列表 活跃的事务是指还没有commit的事务
min_trx_id_:活跃事务中的最小值
max_trx_id_:下一个事务id
ctreator_trx_id:执行select读这个操作的事务id
readview如何判断哪个版本可用
trx_id==ctreator_trx_id 可以访问这个版本 :读取自己创建的记录
trx_id
min_trx_id_<=trx_id<=max_trx_id_: trx_id在当前事务在活跃事务中不可以访问这个版本,反之可以 :读取的事务id不在活跃列表可以读取
mvcc如何实现RC和RP的隔离级别
1.rc时,每个快照读,都会生成并获取最新的readview
2.rr时,只有在同一个事务的第一个快照读才会创建readview
幻读问题:
快照读:通过mvcc,rr的隔离界别解决了幻读问题,因为每次都是同一个readview
当前读:通过临键锁,rr隔离级别并不能解决幻读问题
mysql redo和undo
事务的原子性,隔离性由锁机制实现;
事务的一致性和持久性由redo和undo日志来保证
redo log是重做日志,保证持久性。
undo log是回滚日志,保证事务的一致性。
redo是记录尚未完成的操作,数据库崩溃则用其重做。
redo log分为两部分
innodb采用write ahead log (预先日志持久化策略),先写入日志,在写入磁盘
第一步:InnoDB 会先把记录从硬盘读入内存
第二部:修改数据的内存拷贝
第三步:生成一条重做日志并写入redo log buffer,记录的是数据被修改后的值
第四步:当事务commit时,将redo log buffer中的内容刷新到 redo log file,对 redo log file采用追加写的方式
第五步:定期将内存中修改的数据刷新到磁盘中(注意注意注意,不是从redo log file刷入磁盘,而是从内存刷入磁盘,redo log file只在崩溃恢复数据时才用),如果数据库崩溃,则依据redo log buffer、redo log file进行重做,恢复数据,这才是redo log file的价值所在
在commit时写入redo日志
为什么不直接写,而是先写日志?
磁盘写入时间长,先写入日志,日志只需要考虑修改的数据。
如何保证每次都写入redo log file?
没开启0_direct 直接写磁盘操作。每次写到内存,在刷到磁盘。
每次将redo buffer写入os cache 文件缓存,innodb都调用fsync操作将缓存写入redo log file。
buffer pool中的数据未刷新到磁盘,称为脏页。
redo log满时,会把脏页刷入磁盘。
除了redo满时,什么时候刷脏页?
系统内存不足,淘汰数据页为脏页时。
mysql认为空闲时。
mysql正常关闭前,会把所有的脏页刷到磁盘。
脏页刷入会带来性能问题吗?
在生产环境中,如果我们开启了慢 SQL 监控,你会发现偶尔会出现一些用时稍长的 SQL。**这是因为脏页在刷新到磁盘时可能会给数据库带来性能开销,**导致数据库操作抖动。
2.5 参数innodb_flush_log_at_trx_commit
上面提到的Force Log at Commit机制就是靠InnoDB存储引擎提供的参数innodb_flush_log_at_trx_commit来控制的
该参数控制 commit提交事务 时,如何将 redo log buffer 中的日志刷新到 redo log file 中。
1、当设置参数为1时,(默认为1,建议),表示事务提交时必须调用一次 fsync 操作,最安全的配置,保障持久性
2、当设置参数为2时,则在事务提交时只做 write 操作,只保证将redo log buffer写到系统的页面缓存中,不进行fsync操作,因此如果MySQL数据库宕机时 不会丢失事务,但操作系统宕机则可能丢失事务
3、当设置参数为0时,表示事务提交时不进行写入redo log操作,这个操作仅在master thread 中完成,而在master thread中每1秒进行一次重做日志的fsync操作,因此实例 crash 最多丢失1秒钟内的事务。(master thread是负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性)
undo log
用于记录更改的前一份copy
undo log的存储位置
在InnoDB存储引擎中,undo存储在回滚段(Rollback Segment)中,每个回滚段记录了1024个undo log segment,而在每个undo log segment段中进行undo 页的申请,在5.6以前,Rollback Segment是在共享表空间里的,5.6.3之后,可通过 innodb_undo_tablespace设置undo存储的位置。
purge线程的作用,清除undo页和清除page中带delete_bit的标识。数据删除只是打标记,并不是真的删除。
undo日志两种,insert和update
insert执行后就删除。
update对应delete和insert操作,需要提供mvcc操作,执行并不删除,等待purge线程操作。
undo log是逻辑日志。redo log是物理日志。
undo和rollback的区别,undo会使用rollback。
一张表修改要求比较高的事务处理选择InnoDB,查询要求比较高,选择MyISAM
MyBatis动态SQL 多条件查询(if、where、trim标签)
if where 判断非空
choose、when、otherwise标签 多条件选择
foreach 拼接list
1.基本数据类型:int,string,long,Date
#{value}或${value} 获取参数中的值
2.复杂数据类型:类或者Map
#{属性名}或{属性名} ,map中则是#{key}或
{key}
for each的collection属性
单个List<Long> idList,为list
单个array数组,属性为array
map中放array或者list时,属性为数组的key
java对象中放array或者list时,属性为对象的属性名
resultType
1.基本数据类型
2.pojo类型
一级缓存 sqlsession共享
二级缓存 同一个命名空间共享
resultType设置为java对象:
1.反射创建对象
2.同名的列赋值给同名的属性(通过反射,找到set方法赋值)
3.得到java对象,如果是集合放入list中
当列名和java对象属性名不一致时,使用resultMap建立结果映射
出现原因:
1.外键未加索引
在更新从表会对主表加写锁,在更新主表时,如果从表没有索引,会对整个表加锁。
容易发生死锁
2.用户1锁A,请求锁B,用户2锁B请求锁A
多表操作尽量按照相同的顺序进行处理。
避免方法
1.所有的update和delete必须走唯一索引
schema的操作
2.sql语句中不要有太复杂的关联操作,使用explain sql检查sql,全表扫描尽量优化为索引
3.把select放在update之前
4.避免事务中的用户相互等待。
现场解决方法:
1.重启系统
2.撤销进程,剥夺资源。终止参与死锁的进程,收回资源,从而解除死锁。
3.进程回退策略
级联删除和更新
ON DELETE CASCADE ON UPDATE CASCADE
Oracle不支持,只能用触发器实现
1.商品id 单价 数量 按照销售额排序 前十
PRODUCTIDNUMBER 商品数量
PRICE 商品单价
productid 商品id
select sum(PRODUCTIDNUMBER*PRICE) ,productid from TESTPRODUCT where rownum<11 Group BY productid ORDER BY sum(PRODUCTIDNUMBER*PRICE) ;
String List Hash Set ZSet
ZSet的实现方式:
ziplist压缩链表:
skiplist跳跃链表:不满足上述条件就采用跳表,具体来说组合了map和skiplist
n个节点,第一层索引为n/2,第二层索引为n/4,第k层索引为n/2^k
空间换时间的折半查找
redis排行榜 zset
添加
zadd key score value
ZADD broadcast:20210108231 1 lisi
加分
zincrby key increment member
ZINCRBY broadcast:20210108231 2 lisi
排名
ZRANGE broadcast:20210108231 0 -1 WITHSCORES
过期键淘汰策略是定期删除和惰性删除
定期删除是指:redis服务器定期操作redis.c/serverCron函数执行时,redis.c/activeExpireCycle会被调用。actvieExpire函数在规定时间内,分多次遍历服务期内的多个数据库,从过期字典中检查一部分key的过期时间并删除。
current_db记录当前检查数据库,函数处理2号数据库时间超限,返回后下次检查会从3号数据库开始检查。所有数据库检查完毕current_db重置为0,然后再次开启一轮的检查工作。
惰性删除是指用户请求,此时会检查过期,过期清除不会返回
redis一般用作缓存,多读少写,只支持乐观锁
Redis事务命令主要包括 WATCH, EXEC, DISCARD, MULTI。
事务使用MULTI开启,这时可以执行多条命令,Redis在这些事务中加入命令,当用户执行Exec命令时才真正的执行队列当中的命令。执行discard,丢弃队列中的命令。
Watch命令是Exec执行的条件,watch的key没有修改则执行事务,否则事务不会被执行。
Watch命令可以被调用多次,一个watch命令可以监控多个key。watch命令调用则开启监视功能,直到exec命令终止。
Redis的watch命令给事务CAS机制,如果key在执行exec前有变动,则整个事务被取消。
事务中,采用watch加锁,unwatch解锁,执行EXEC命令或者Discard命令后,锁自动释放,不需要进行unwatch操作。
参考:Redis实现分布式锁的7种方案
分布式锁需要保证:
1.互斥性:只能有一个客户端获得锁
2.安全性:锁只能被持有该锁的客户端删除
3.死锁:锁超时释放,避免死锁
4.容错:当redis部分节点宕机,客户端仍能获取锁和释放锁
1.一种实现
setnx k v; 成功返回1 失败返回0;
getset k v;返回key的旧值;
expire k secondsl;给key设置超时时间
del key [key …]
1.1setnx key 当前时间+过期时间
1.2. 失败获取锁失败,成功 expire 超时时间
1.3.执行业务 del key
问题:如果获取锁成功未设置超时时间的时候进行重启,会产生死锁。
(如果不是kill线程,而是shutdown可以用springBean的predestory注解,进行删除key操作)
优化:获取锁失败后,查看当前key的value值,如果不为空,并且当前时间大于该值,说明当前锁失效,通过getset获取值。如果getset获取的值,和一开始的旧值相同,则获取锁成功。
问题:多节点时间需要尽可能的保持一致。getset即便失败也会延长之前锁的时间。
2.使用lua脚本
保证setnx和expire的原子性
3.set扩展命令
SET key value[EX seconds][PX milliseconds][NX|XX]
问题:业务没执行完,锁就释放了;锁被别的线程误删。
4.SET EX PX NX + 校验唯一随机值,再删除
还是会存在业务没有执行完成,锁释放的问题。
5.Redisson框架
开源框架Redisson
加锁后启动一个watchdog线程,每隔10s检查是否持有锁,持有锁就延长,防止锁过期提前释放。
6.redlock+redisson
redis节点全为master,避免master加锁后未同步到slave然后宕机导致加锁失败的情况
按顺序向5个master节点请求加锁
根据设置的超时时间来判断,是不是要跳过该master节点。
如果大于等于3个节点加锁成功,并且使用的时间小于锁的有效期,即可认定加锁成功啦。
如果获取锁失败,解锁!
参考 深入学习redis 的线程模型
redis内部采用事件处理器是单线程的,因此redis叫做单线程模型。采用IO多路复用机制同时监听多个socket,将产生的事件的socket压缩到内存队列中,事件分派器根据事件不同的类型选择对应的事件处理器进行处理。
文件事件处理器的结构
线程模型
多个socket可能并发产生不同的操作,每个操作对应不同的事件,但是IO多路复用程序会监听多个socket,将产生事件的socket放入队列中排队,事件分派器每次从队列中取出一个socket,根据socket对应的事件类型交给对应的事件处理器进行处理。
建立连接
为什么redis效率高?
1.纯内存操作
2.核心是基于非阻塞的IO多路复用机制
3.C语言实现,语言更接近操作系统,执行速度相对会快
4.单线程反而避免了多线程的频繁上下文切换问题,预防了多线程可能产生的竞争问题。
redis的吞吐量:
单点TPS达到8万/秒,QPS达到10万/秒
qps是指每秒最大能接受的用户访问量,tps是指每秒钟最大能处理的请求数。
幂等性:多次操作结果一致
消息发送时会生成唯一id
消息的监听者第一次监听到消息并处理时,会将id存入redis,记录为处理中,以后的消息就不会进行重复处理
1.流量削峰(消息过多,处理有限) 落库采用异步消息的方式,减缓数据库压力
2.服务解耦(接口故障不影响生产和消费)消息发出去,发消息的应用down了,也不会影响后序微服务的消费
3.异步(提高响应速度,用户无感知)
参考高频面试之Eureka
注册中心,包括服务发现,治理等功能。
@EnableEurekaServer作为注册中心,@EnableEurekaClient作为服务的提供者或消费者。
一致性哈希相较于普通哈希具有更好的可扩展性和容错性。
哈希是对节点个数取模,在进行扩容或者节点下线时,需要重新映射所有数据。
一致性哈希是对2^32取模,将哈希值空间映射到虚拟的闭环上,称为哈希环。取模所得值,进行顺时针寻找到的第一个节点为哈希环中的位置。节点扩容和下线时只需要迁移相邻节点的数据。
当分布不均衡时,采用虚拟节点来解决问题。
1.生产者发送给服务器
acks机制 0 发送就成功,1 生产者收到leader分区的响应则认为成功 -1 当所有ISR中的副本全部收到消息,生产者才认为是成功的
2.服务器通过副本保存消息
3.消费者,关闭自动提交,在接收到消息,进行业务处理完毕后再提交偏移量
保存偏移量,从上一次消费的偏移量开始避免重复消费
消费者组消费指定分区消息,互不干扰
kafka生产者幂等性校验,生产者在发送消息时,会产生唯一序列号,发送消息会进行幂等性和重复校验
消费者自己做幂等性处理
参考Kafka的Rebalance机制可能造成的影响及解决方案
每当有新的消费者加入或者订阅的topic数发生变化,会触发rebalance(再均衡:在同一个消费组当中,分区的所有权从一个消费者转到另一个消费者)机制,Rebalance顾名思义就是重新均衡消费者消费。
过程如下:
1.所有消费者向coordinator发送请求,请求加入comsumer group。一旦所有成员都发送了请求,Coordinator会从中选择一个consumer作为leader,并将组员信息发给leader
2.leader分配消费方案,指定哪个消费者消费哪些topic的哪些分区。发给coordinator,coordinator发送给消费者,组内成员知道自己该消费哪些分区了。
coordinator:每个consumer group会选择一个服务器作为自己的coordinator,负责监控整个消费者组内各个分区的心跳,以及判断是否宕机和开启rebalance的
partition:每个topic分区,备份在不同的服务器上
如何选择coordinator?
对groupid进行hash,然后对_consumer_offsets的分区数量进行取模,默认分区数量为50,可以配置。
发生的时机:
1.分区数量增加。
2.对topic的订阅发生变化
3.消费者组成员的加入或者离开。
影响:1.重复消费;2.集群不稳定;3.影响消费速度
springcloud是一系列架构的有序集合。基于springboot简化分布式基础设置的开发,如服务发现,配置中心,路由zuul,消息总线,负载均衡fegin+ribbon,断路器hystrix,数据监控等。
1.创建springApplication对象,运行run方法;
通过类加载器加载classpath下所有的spring.factories配置文件,创建初始化对象;
创建环境对象environment,读取环境配置,如application.yml
2.创建程序上下文createApplicationContext,创建bean工厂对象
3.刷新上下文(启动核心),refreshContext(工厂对象配置,bean处理器配置,类的扫描,解析,bean的定义,bean类信息缓存,tomcat创建,bean实例化,动态代理对象创建)
4.通知监听者,启动程序完成
当需要将类加载到spring中,而类并不在spring启动类的包下,除了在启动类@Import,还可以在resource中新建META-INF目录,新建spring.factories。通过这种方式来加载非默认扫描路径,自己写的或者第三方的类。 通过Spring的spi扩展实现
参考springboot核心基础之spring.factories机制
IOC:控制反转,自己new对象改为由Spring注入,解耦。
AOP:面向切面编程,提取公共代码,进行增强处理,解耦。
参考 java动态代理
静态代理:代理类和被代理类实现相同的接口,代理类持有被代理对象,调用接口方法中进行前置和后置操作,调用被代理类的方法。
类已经写好,编译后就能生成class文件。
public interface Person {
//租房
public void rentHouse();
}
public class Renter implements Person{
@Override
public void rentHouse() {
System.out.println("租客租房成功!");
}
}
public class RenterProxy implements Person{
private Person renter;
public RenterProxy(Person renter){
this.renter = renter;
}
@Override
public void rentHouse() {
System.out.println("中介找房东租房,转租给租客!");
renter.rentHouse();
System.out.println("中介给租客钥匙,租客入住!");
}
}
public class StaticProxyTest {
public static void main(String[] args) {
Person renter = new Renter();
RenterProxy proxy = new RenterProxy(renter);
proxy.rentHouse();
}
}
动态代理:
代理类在程序运行时创建的代理方式被称为动态代理。动态代理相较于静态代理可以对代理类的所有函数进行统一管理,不需要对每个方法都写一遍。
通过reflect包下的proxy类和invocationHandler
生成jdk动态代理类和代理对象。
public interface Person {
//租房
public void rentHouse();
}
public class Renter implements Person{
@Override
public void rentHouse() {
System.out.println("租客租房成功!");
}
}
创建代理类实现invocationHandler接口,需要实现invoke方法,代理类中存在被代理对象。
调用代理类的方法都会调用invoke方法,在invoke中通过反射调用被代理对象的方法。
在执行被代理对象的方法调用前后增加自己的处理,这就是spring aop的主要原理。
public class RenterInvocationHandler<T> implements InvocationHandler{
//被代理类的对象
private T target;
public RenterInvocationHandler(T target){
this.target = target;
}
/**
* proxy:代表动态代理对象
* method:代表正在执行的方法
* args:代表调用目标方法时传入的实参
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
//代理过程中插入其他操作
System.out.println("租客和中介交流");
Object result = method.invoke(target, args);
return result;
}
}
public class ProxyTest {
public static void main(String[] args) {
//创建被代理的实例对象
Person renter = new Renter();
//创建InvocationHandler对象
InvocationHandler renterHandler = new RenterInvocationHandler<Person>(renter);
//创建代理对象,代理对象的每个执行方法都会替换执行Invocation中的invoke方法
Person renterProxy = (Person)Proxy.newProxyInstance(Person.class.getClassLoader(),new Class<?>[]{Person.class}, renterHandler);
renterProxy.rentHouse();
//也可以使用下面的方式创建代理类对象,Proxy.newProxyInstance其实就是对下面代码的封装
/*try {
//使用Proxy类的getProxyClass静态方法生成一个动态代理类renterProxy
Class> renterProxyClass = Proxy.getProxyClass(Person.class.getClassLoader(), new Class>[]{Person.class});
//获取代理类renterProxy的构造器,参数为InvocationHandler
Constructor> constructor = renterProxyClass.getConstructor(InvocationHandler.class);
//使用构造器创建一个代理类实例对象
Person renterProxy = (Person)constructor.newInstance(renterHandler);
renterProxy.rentHouse();
//
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}*/
}
}
JDK动态代理生成的代理类中的方法都会调用RenterInvocationHandler中的invoke方法,而invoke方法中调用了被代理对象的指定方法。
没有实现接口采用cglib代理
通过继承目标类,在子类中采用方法拦截的方式拦截所有父类方法的调用,然后加入自己需要的操作。因为使用的是继承,因此类不能为final。
创建被代理类
public class UserService {
public void getName(){
System.out.println("张三!");
}
}
创建代理工厂类ProxyFactory
public class ProxyFactory<T> implements MethodInterceptor {
private T target;
public ProxyFactory(T target) {
this.target = target;
}
// 创建代理对象
public Object getProxyInstance() {
// 1.cglib工具类
Enhancer en = new Enhancer();
// 2.设置父类
en.setSuperclass(this.target.getClass());
// 3.设置回调函数
en.setCallback(this);
return en.create();
}
//拦截方法
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy methodProxy) throws Throwable {
System.out.println("开始事务...");
// 执行目标对象的方法
Object result = method.invoke(target, args);
System.out.println("提交事务...");
return result;
}
}
JDK和CGLib动态代理都是实现SpringAOP的基础。如果加入容器的目标对象有实现接口,用动态代理,如果目标对象没有实现接口,用CGlib代理。
Spring AOP是动态代理,运行时增强
AspectJ属于编译增强
Spring AOP 只能在运行时织入,不需要单独编译,性能相比 AspectJ 编译织入的方式慢,而 AspectJ 只支持编译前后和类加载时织入,性能更好,功能更加强大。
Spring事务基于数据库事务
Spring隔离级别和数据库事务隔离级别一样,多了个默认,采用数据库默认的隔离界别
Spring事务的两种方式,编程式事务,声明式事务
编程式事务精确到代码级别,声明式事务精确到方法级别。
Spirng事务的传播机制
a调用b
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
//事务超时时间,超时回滚
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
//如果事务只读,可以利用事务的只读属性开启优化措施
boolean readOnly() default false;
}
//事务的回滚策略,指定异常回滚
@Transactional(rollbackFor= MyException.class)
//事务的回滚策略,指定异常不回滚
@Transactional(noRollbackFor= MyException.class)
ApplicaitonContext代表spring的IOC容器,负责实例化,配置,装配bean。
有@SpringBootApplication注解的启动类
该注解有@EnableAutoConfiguration(会导入AutoConfigurationImportSelector,这个类会将所有符合条件的@Configuration配置都进行加载),@SpringBootConfiguration(等同于@Configuration),@ComponmentScan(扫描,加载)这三个注解构成。不需要增加配置内容和扫描路径,只使用@EnableAutoConfiguration即可。
执行run()
服务构建
服务指SpringApplication对象,1.首先将资源加载器,主方法类放入内存中
2.逐一判断服务类是否存在,来确定web服务的类型,默认servlet(即基于servlet的web服务,如tomcat),还有响应式非阻塞服务reactive,如spring-webflux,还有什么都不用用的none。
3.加载初始化类,读取所有META-INF/spring.factory文件中的注册初始化(bootstrapRegistryInitializer),上下文初始化(ApplicationContextInitializer)和监听器(ApplicationListener)三种配置。
没有默认的注册初始化配置,spring-boot和spring-boot-autoconfiguration这两个工程中配置了7个上下文初始化和8个监听器。也可以自定义这三个配置。
4.通过运行栈stackTrace判断main方法所在的类,大概率是启动类本身
环境准备
2.1 new一个BootStrapcontext,逐一调用启动注册初始化器(bootstrapRegistryInitializer)中的初始化initialize方法
2.2 将java.awt.headless这个设置为true,表示缺少显示器,键盘等输入设备,也可以正常启动
2.3 启动运行监听器SpringApplicationRunListeners,发布启动事件,获取并加载spring-boot工程springfactories配置文件中的EventPublishingRunListener,在启动时会将8个监听器引入
2.4 prepareEnvironment
构造可配置环境ConfigurationEnvironment,根据不同的Web服务类型会构造不同的环境,默认servlet,构造之后会加载很多系统环境变量,如systemEnvironment,jvm系统属性systemProperties等在内的4组配置信息,将配置信息加载到propertySource的内存集合中。通过配置环境configureEnvironment方法将我们启动时传入的参数args进行设置,例如启动时传入的“开发/生产”环境哦配置都会在这一步进行加载,同时在propertySource集合的首个位置添加一个值为空的配置内容,configurationProperties后续使用
发布环境准备完成这个事件,8个监听器监听到事件,部分会进行相应处理注入环境配置后置处理监听器会加载spring.factories配置文件中的“环境配置后处理器”(监听器通过观察者模式设计,逐一串行执行)
可配置环境在过程中可能会有变化,通过更新保证匹配
将spring.beaninfo.ignore设为true,表示不加载Bean的元数据信息,同时打印Banner图
容器创建
createApplicationContext来创建容器
根据服务器类型创建容器ConfigurableApplicaitonContext,默认是Servlet,创建注解配置的servlet-web服务器容器,即AnnotionConfigServletWebServerApplicationContext,在这个过程中,会构造存放和构建bean实例的Bean工厂 DefaultListableBeanFactory
将这三个都放入容器中,通过prepareContext方法对容器中部分属性进行初始化了。
先用postProcessApplicaitonContext方法设置Bean名称生成器,资源加载器,类型转换器等,接着执行上下文初始化ApplicationContextInitializer,默认加载7个。发布容器准备完成监听事件后,陆续为容器注册启动参数,banner,bean引用策略和懒加载机制等等,通过bean定义加载器将启动类在内的资源加载到bean定义池beanDefinitionMap中,以便后续根据bean定义创建bean对象,然后发布资源加载完成事件
填充容器
生产自身提供的和自定义的bean对象,放入容器,也就是自动装配
在META-INF下,META-INF目录是提供jar文件描述信息的文件目录。
SpringFactories模仿java SPI机制
SPI机制是service provice interface,面向对象基于接口编程,如JDBC,java定义接口,数据库公司实现接口,换数据库不需要修改java代码。类似还有xml解析,日志模块的方案。
java SPI 就是为某个接口寻找服务的实现的机制。
对于在maven中引用的其他外部包加入容器的过程,需要用到spring.factories。
在SpringBoot启动时,SpringFactoriesLoader类会通过loadFactories或者loadFactoryNames寻找每个jar包下的META-INF/spring.factories,
AutoConfigurationImportSelector的selectImports方法返回的类名,来自spring.factories文件内的配置信息。通过该方法加载外部的Bean
Servlet中的过滤器Filter是实现了javax.servlet.Filter接口的服务器端程序,主要的用途是过滤字符编码、做一些业务逻辑判断等。
SpringMVC 中的Interceptor 拦截器的主要作用就是拦截用户的 url 请求,并在执行 handler 方法的前中后加入某些特殊请求,比如通过它来进行权限验证,或者是来判断用户是否登陆。
拦截器的实现方式:实现HandlerInterceptor 或者WebRequestInterceptor
过滤器:用于属性甄别,对象收集(不可改变过滤对象的属性和行为)
拦截器:用于对象拦截,行为干预(可以改变拦截对象的属性和行为)
过滤器是JavaWeb的三大组件之一,是实现Filter的java类。
过滤器实现对请求资源的过滤功能,在请求资源前,响应前进行操作。
过滤器主要用来如参数过滤、防止SQL注入、防止页面攻击、过滤敏感字符、解决网站乱码、空参数矫正、Token验证、Session验证、点击率统计等。
过滤器 Filter
init():该方法在容器启动初始化过滤器时被调用,它在Filter的整个生命周期只会被调用一次,这个方法必须执行成功,否则过滤器会不起作用。
doFilter():容器中的每一次请求都会调用该方法,FilterChain用来调用下一个过滤器Filter。
destroy():容器销毁时被调用。一般在方法中销毁或关闭资源,也只会被调用一次。
拦截器核心API
SpringMVC拦截器提供三个方法分别是preHandle、postHandle、afterCompletion,我们就是通过重写这几个方法来对用户的请求进行拦截处理的。
preHandle() :这个方法将在请求处理之前进行调用。「注意」:如果该方法的返回值为false ,将视为当前请求结束,不仅自身的拦截器会失效,还会导致其他的拦截器也不再执行。
postHandle():只有在 preHandle() 方法返回值为true 时才会执行。会在Controller 中的方法调用之后,DispatcherServlet 返回渲染视图之前被调用。「有意思的是」:postHandle() 方法被调用的顺序跟 preHandle() 是相反的,先声明的拦截器 preHandle() 方法先执行,而postHandle()方法反而会后执行。
afterCompletion():只有在 preHandle() 方法返回值为true 时才会执行,在整
个请求结束之后, DispatcherServlet 渲染了对应的视图之后执行。
一级缓存:实例化,初始化的对象
二级缓存:实例化,还未初始化的对象
三级缓存:对象工厂,用来创建二级缓存中的对象
AB相互依赖
实例化A,放入三级缓存,属性注入时发现需要B,去实例化B,发现需要依赖A,从缓存中依次查找,从三级缓存中删除,放入二级缓存,完成初始化后,将B放入一级缓存。接着初始化A,完成后删除二级缓存中的A,放入一级缓存。
使用三级缓存的原因是:保证使用的是同一个对象。
单用二级缓存,在二级缓存中放入一个普通Bean之后,BeanPostPorcessor生成代理对象覆盖,多线程环境,取不到一致的对象。
保证获取的是同一个对象。A3和A2都依赖A1,A1依赖A2,A3,在进行A1的依赖注入时,创建A2时,给A2注入了A1的代理对象,但是A3进行依赖注入时,如果不缓存A1的代理对象,aop会重新生成代理对象,导致单例被破坏
Spring IOC和AOP
SpringBoot 约定大于配置,快速开发
SopringCloud 微服务治理,提供解决方案
SpringBoot可以依赖SpringCloud开,SpringCloud不能离开SpringBoot,属于依赖关系。
参考 SpringBoot——SpringBoot使用过滤器Filter
1.注解实现
编写一个过滤器,在启动类上添加ServletComponentScan注解扫描过滤器的对应包
2.Spring Boot 的配置类实现
新增过滤器不添加注解,通过FilterRegistrationBean注册自己的过滤器
实现BeanFactoryAware ,ApplicationContextAware,ApplicationListener接口
获取spring容器对象
参考:Spring AOP代码实现:实例演示与注解全解
Aspect Oriented Programming 面向切面编程,用于处理代码中公共非业务逻辑,如日志,鉴权等。
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();//拿到方法
Object[] allArgs = joinPoint.getArgs();//拿到参数
@Aspect
@Component
public class LogAdvice {
// 定义一个切点:所有被GetMapping注解修饰的方法会织入advice
@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
// @Pointcut("execution(* com.example.demo.controller..*(..))")
private void logAdvicePointcut(){}
@Before("logAdvicePointcut()")
public void logAdvice(){
// 这里只是一个示例,你可以写任何处理逻辑
System.out.println("get请求的advice触发了");
}
}
cat 文件 | grep 关键字 | wc -l
参考 Linux系统中统计文件中某个字符出现次数命令详细教程
其实我不想了解,钱到位无所谓。
1.公司的业务是什么。
2.为什么会招,是有人离职?
3.目前最大的挑战是什么
3.年假,五险一金,
参考树、二叉树、完全二叉树、满二叉树的概念和性质
只有一个根节点;每个子树根节点只有一个前驱,可以有0或者多个后驱。
注意:子树之间不能有交集
一颗二叉树是一个节点的有序集合,该集合为空,或者由一个根节点加上两颗别称为左子树和右子树的二叉树组成。
1.二叉树不存在子节点大于2的度
2.二叉树有左右之分,次序不能颠倒,因此二叉树是有序树。
满二叉树
所有叶子结点都在最后一层。所有分支节点都有两个孩子。节点为2^k-1。k为层数。
完全二叉树
前n-1层是满的。不满的那层子从左往右是连续的。
①若规定根结点的层数为1,则一棵非空二叉树的第i层上最多有2i-1个结点。\n②若规定根结点的层数为1,则深度为h的二叉树的最大结点数为2h-1个。\n③对任何一棵二叉树,如果度为0的叶结点个数为n0,度为2的分支结点个数为n2,则有n0 = n2+1。(常用这个性质解选择题)\n④若规定根结点的层数为1,则具有N个结点的满二叉树的深度h = log2(N+1)。\n⑤对于具有N个结点的完全二叉树,如果按照从上至下、从左至右的数组顺序对所有结点从0开始编号,则对于序号为i的结点:\n1、若 i > 0,则该结点的父结点序号为:( i - 1) / 2;若 i = 0,则无父结点。\n2、若2i + 1 \u003C N,则该结点的左孩子序号为:2i + 1;若2i + 1 >= N,则无左孩子。\n3、若2i + 2 \u003C N,则该结点的右孩子序号为:2i + 2;若2i + 2 >= N,则无右孩子。
顺序存储一般用来存储完全二叉树,否则会造成空间浪费。现实只有堆会用数组存储。
链式存储,用链表表示二叉树。每个节点为左右指针域和数据域。
深度遍历
参考堆的应用 – Top-K问题(巨详细)
堆是二叉树,一般的二叉树用链表存储,用数组存储会浪费空间,但是堆是完全二叉树,用数组存储。
已知父节点下标n, 他的右节点为2n+2,左节点2n+1
已知子节点n,他的父节点为(n-1)/2
大根堆和小根堆
大根堆:根节点大于左右孩子节点
小根堆:根节点小于左右孩子节点
java提供这种数据结构,每次添加或者删除元素,都会变成小跟堆。
// 默认得到一个小根堆
PriorityQueue<Integer> smallHeap = new PriorityQueue<>();
smallHeap.offer(23);
smallHeap.offer(2);
smallHeap.offer(11);
System.out.println(smallHeap.poll());// 弹出2,剩余最小的元素就是11,会被调整到堆顶,下一次弹出
System.out.println(smallHeap.poll());// 弹出11
// 如果需要得到大根堆,在里面传一个比较器
PriorityQueue<Integer> BigHeap = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
top-K问题
数组,找出前三个最小元素
1.排序
2.放入小跟堆
3.在大根堆中放三个元素,循环数组每次往里放一个数据,弹出最大的数据,最后堆里就是结果。
堆的向下调整算法、堆的向上调整算法、堆的实现、Topk问题
exception.printStackTrace打印到流,流转换为字符,log对象输出到log文件里
提取业务处理类共性,抽象出父类,公共模块根据名称获取子类对象,向上转型为父类对象,进行调用,捕获异常
参考工厂模式
工厂模式属于创建型模式。
意图:定义一个创建接口的接口,让其子类字节决定实例化哪个类,工厂模式使其创建过程延迟到子类执行
主要解决:接口选择的问题
如何解决:让子类实现工厂接口,返回的也是也是一个抽象的产品
关键代码:创建过程在其子类实现
在任何需要生成复杂对象的地方,都可以使用工厂方法模式。简单对象,只需要new就能完成创建的对象,无需工厂模式。使用工厂模式,需要引入一个工厂类,增加系统的复杂度。
举例,抽象一个形状接口,圆,方块实现这个接口,定义一个工厂类提供给获取形状的方法,根据入参判断实例化圆/方块
public class AbstractFactoryPatternDemo {
public static void main(String[] args) {
//获取形状工厂
AbstractFactory shapeFactory = FactoryProducer.getFactory("SHAPE");
//获取形状为 Circle 的对象
Shape shape1 = shapeFactory.getShape("CIRCLE");
//调用 Circle 的 draw 方法
shape1.draw();
//获取形状为 Rectangle 的对象
Shape shape2 = shapeFactory.getShape("RECTANGLE");
//调用 Rectangle 的 draw 方法
shape2.draw();
//获取形状为 Square 的对象
Shape shape3 = shapeFactory.getShape("SQUARE");
//调用 Square 的 draw 方法
shape3.draw();
//获取颜色工厂
AbstractFactory colorFactory = FactoryProducer.getFactory("COLOR");
//获取颜色为 Red 的对象
Color color1 = colorFactory.getColor("RED");
//调用 Red 的 fill 方法
color1.fill();
//获取颜色为 Green 的对象
Color color2 = colorFactory.getColor("GREEN");
//调用 Green 的 fill 方法
color2.fill();
//获取颜色为 Blue 的对象
Color color3 = colorFactory.getColor("BLUE");
//调用 Blue 的 fill 方法
color3.fill();
}
}
xml到标准对象 thoughtworks.xstream 流
json到标准对象 hutools工具,底层反射调用set方法
javaBean
java语言中的可重用组件。
满足:1.类是公共的;2.有一个无参的公共构造器;3.有属性,且有对应的get和set方法。
其他开发者通过JSP,Servlet 其他JavaBean,applet程序或者应用来使用这些对象。用户可以认为JavaBean提供了一种随时随地复制粘贴的功能。
javaBean的任务就是一次性编写,任何地方执行,任何地方重用。
难点:堆排序,redis跳表
八股文我背了第一层,技术hr问第二层,给我讲解,完事推荐我可以看书:
深入理解java虚拟机 (周志明)
MySQL实战宝典(姜承尧)