- 抽象类可以有构造方法,但是不能被实例化。不一定只有抽象方法,相反有抽象方法的一定是抽象类。
- 抽象类和接口的区别
- 接口可以继承接口,并且可以多继承,但类只能单继承
- 抽象类必须有某一个子类实现所有的方法。
- 接口中只能有方法的声明,静态方法和default方法,但抽象类可以有普通方法。
- 抽象类的变量是普通变量,接口中是公共的常量。
- 外部类不能是静态的,因为如果是静态的,那么随着应用启动该类就会被加载,如果根本没有用过该类,那么就会造成内存浪费,这样是不合理的。
- 内部类,成员内部类可以访问外部类所有的方法和成员变量,不能有静态的方法和成员变量
- 静态内部类,只能访问外部类的静态方法和成员变量
- 匿名内部类,没有类名,没有关键字,也没有继承实现的修饰,类的定义和实例化同时进行。前提条件,必须继承或实现一个接口。比如Runable的匿名内部类实现,Thread的匿名内部类实现。
- Try-Catch-Finally中,如果在finally修改返回值不会生效,但是如果直接return会覆盖try中的return。
- 类初始化的过程
- 创建实例前需要先加载并初始化该类
- 子类初始化前需要初始化父类
- 类初始化执行静态类变量显示赋值代码和静态代码块,从上到下执行。
- 实例化的初始化过程
- 非静态实例变量显示赋值代码和非静态代码块从上到下执行,对应的构造器方法最后执行。
- 被重写的非静态方法的this指的是正在创建的对象,所以执行子类的重写方法。
- 方法的传参机制和特殊类的不变性
- 基本数据类型传递的是数据值,引用数据类型传递的是地址值。
- String,包装类不可变,所以在方法中指向了新的对象,所以原数据仍然无法修改。
- JAVA的四大特性(3.17 阿里一面题)
- 抽象:abstract修饰,父类为子类提供一些属性和行为,子类根据业务需求实现具体的行为。如果子类没有实现所有的抽象方法,那子类也是抽象类。
- 封装:private修饰,对外提供set,get方法。把对象的属性和行为结合为一个独立的整体,尽可能隐藏对象的内部实现细节。
- 继承:extends,子类继承父类的属性,并能根据自己的需求扩展出新的属性和行为,提高代码复用性。
- 多态:接口实现,继承父类进行方法重写,在同一个类中进行方法重载。不修改代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态。多态有三个必要条件
- 要有继承
- 要有重写
- 父类引用指向子类对象
- Collection接口下有:List,Set,Queue,sortedSet
- Map接口下:HashMap,HashTable,TreeMap,IdentityHashMap,WeakHashMap
- B+ Tree 1.
- 红黑树
- 都只能在构造器的第一行,且不能同时使用。
- this调用的是重载的构造器,super调用的是父类被子类重写的方法。
- this和super都指的是对象,所以不能在static环境中使用。
- Exception和Error的区别:
- 都继承自Throwable
- Error指的是较为严重的,正常情况不应该出现的错误,如OutOfMemoryError,StackOverflowError,不应当捕获
- Exception指的是可以预料到的意外情况,可以捕获处理
- 运行时异常(不受检查的异常)和一般异常的区别
- 受检查的异常:编译时被强制检查的异常如ClassNotFountException,IOException
- 不受检查的异常,在编码过程中可以避免的逻辑错误,如空指针异常,不需要在编译时强制检查。
- 常见的几种运行时异常
- NullPointerException 空指针
- ClassCastException 类型转换
- NumberFormatException 数字类型转换
- IndexOutOfBoundsException 越界
- throw和throws的区别
- throw:在方法中手动抛出异常的方法,如果方法体内不处理,需要在方法上声明throws
- throws:方法声明时标明可能产生的所有异常,不做任何处理直接向上层传
- 异常使用的注意点
- 不要捕获Exception这样的通用异常
- 不要catch到异常后不处理
- 借助日志记录异常
- 不要try-catch一大段代码,因为会造成额外的性能开销
- Integer在-128到127内时,使用缓存机制,是同一个对象,否则是不同对象不能直接==判断数值。
- Integer在自增等操作后是一个新的对象。
- 判断数值相等时需要使用equals或拆箱为基本数据类型int比较。
- String是final的,所以具有不可变性,对于字符串操作会产生新的String对象。
- StringBuffer本质是一个线程安全的可修改的字符串序列,由于加入线程安全,会带来额外性能损耗。
- StringBuilder是没有线程安全的StringBuffer。
- String重写了equals方法,所以可以使用equals判断两个字符串内容是否一致。equals方法先判断是否为字符串,然后逐个比较。
- 重写指子类中重写父类的方法,实现的功能不同。要求必须满足两同两小一大。
- 访问修饰符要大于等于父类。
- 方法名和参数相同。
- 返回值是引用数据类型要小于等于父类,基本数据类型的返回值必须相同。
- 抛出的异常更小。
- 重载指方法名相同,参数类型和类型不同。
- final方法,静态方法,private等子类不可见方法不可被重写。
- IO和NIO的区别
- IO:面向流,阻塞
- NIO:面向缓冲区,非阻塞,选择器。通道负责打开连接,需要获取通道和容纳数据的缓冲区,操作缓冲区对数据进行处理。
- 缓冲区Buffer
- 底层就是数组,用于存储不同数据类型的数据
- 根据数据类型不同,提供了相应类型的缓冲区。(boolean除外)
- 存取数据的核心方法:put() get() flip() 分别对应存,取,读写模式切换
- 缓冲区的四个核心属性
- capacity:容量,一旦声明不能改变
- limit:界限,表示缓冲区可以操作数据的大小
- position:缓冲区中正在操作数据的位置
- mark:标记用于标识position的位置,可以通过reset()恢复到mark的位置
- 直接缓冲区,非直接缓冲区
- 非直接缓冲区:allocate() 将缓冲区建立在jvm的内存中,需要从用户空间拷贝到内核地址空间。
- 直接缓冲区:allocateDirect() 将缓冲区建立在物理内存内存中,由于不需要拷贝所以可以提高效率。但是当写到物理内存映射文件中后,程序无法再管理数据,而是由操作系统处理。分配销毁开销大,不容易控制。
- 通道
- 通道表示IO源于目标打开的连接,通道类似于传统的流,只不过通道本身不能访问数据,只能和Buffer交互。
- 传统方式中,由CPU处理所有的IO接口,当出现大量数据请求后,CPU占用极高,性能下降。DMA是内存和IO接口中的一个直接存储器,DMA负责IO操作,CPU不需要干预IO操作,也就是IO流,但存在总线冲突问题。因为DMA还是需要向CPU申请资源,大量请求仍然会出现性能下降。
- DMA由通道替代,通道是完全独立的处理器,专门用于IO操作,有一套自己的命令。
- 通道间传输,transformTo/From()
- 分散读取,将通道中的数据分散到多个缓冲区中
- 聚集写入,将多个缓冲区的数据聚集到通道中
- 管道
- 管道是两个线程间的单向数据连接,source读通道,sink写通道
- NIO的非阻塞
- 传统IO在服务端需要等待从内核空间保存到用户地址空间完成后,线程才会继续,否则会阻塞。解决方法对于每个客户端开启一个独立线程。
- NIO加入了选择器,选择器会把每一个通道都注册到选择器上,监控每个通道的IO状况,比如读,写,连接,接收。当某个通道上的数据准备就绪时,选择器才会分配服务端的线程运行。
- NIO非阻塞针对的是网络传输,所以FileChannel无法监控
- HashMap允许null的value或者key,但是HashTable不允许。
- HashMap继承自AbstractMap。HashTable继承自Dictionary。1.7之前实现一样,都是数组+链表。
- HashMap的默认大小是16,默认负载因子为0.75。链表长度大于8且容量大于等于64时会转为红黑树,少于6会转回链表。总容量小于64时,认为是由于格子太少造成的冲突过多,所以进行扩容。
- HashTable在方法上添加了Synchronize,实现了线程安全,但锁住了整个HashTable,效率比较低。
- HashMap在JDK1.8时引入了红黑树。
- HashMap在计算哈希时,(当前的容量-1)&(哈希值) 是存放的位置。基本上就是哈希值本身,所以可以使得数据尽可能分散。
- JDK1.7中,当扩容时,需要去计算每个数据的新位置,然后更新数据。这里使用的是头插法,当出现第一个线程刚拿到next,还没来得及放入新的位置时时间片用完,第二个线程完成了链表的逆置,这时候切回第一个线程就会导致死循环。
- JDK1.8中,在put操作时,当出现两个线程同时往一个位置放东西时,如果一个线程还没放进去时间片就用完了切换到线程二,线程二完成后线程一再操作时会覆盖线程二的数据。
- HashSet的底层是HashMap。相当于是一个只有key的HashMap,value是一个假的Object。
- 1.8版本之前,采用分段锁,将数据分成一段一段后,给每一段数据配置一把锁。Segment锁使用的时ReentrantLock,segments本身就是一个哈希表,每个segment守护一个HashEntry里的元素,要对其修改时要先获得他对应的Segment锁。
- 1.8之后,ConcurrentHashMap抛弃了segment分段锁,采用了CAS+synchronized保证并发安全。CAS就是Compare and Swap,先比较在交换,主要用于空位置的赋值,是否有其他线程在扩容的判断。1.8之后,锁的是每一个node,由于在如此细分的情况下,不容易出现极大的并发,那么synchronized不会 转为重量级锁,省去了线程切换上下文的时间。
- 为什么与length-1做与运算,因为length要求都是2的幂次方,length-1的二进制是全1的,这样做与运算结果等同于HashCode后几位的值,只要输入的数据是分布均匀的,hash的结果就是均匀分布。
- LinkedBlockingQueue是一个基于链表结构的可选是否有界的阻塞队列,不允许null,线程安全
- PriorityQueue是非线程安全的,PriorityBlockingQueue是线程安全的。
- LinkedList也是一种队列。
- ArrayList是自扩容的动态数组实现,LinkList是双向链表实现。所以性能上随机访问Array比较好,指定位置删除添加Link好。由于内存连续,所以遍历上ArrayList也比较快。
- ArrayList线程不安全,多线程插入数据时会出问题。可以使用Collections.synchronizedList,CopyOnWriteArrayList解决。
- 默认容量为10,扩容时为当前容量*1.5
- 指定位置新增,从指定位置开始复制,然后放到指定位置+1,再将数据放到指定位置
- arraylist在设置了初始化大小后仍然不能直接set到某个位置,因为他基于elementData大小而不是数组的大小。
- vector把所有的方法加了synchronized,和Collections.synchronizedList一样。
- Collections.synchronizedList写的效率高,CopyOnWriteArrayList有一个复制操作所以效率低。
- CopyOnWriteArrayList的容器可以实现在读取的时候其他线程修改数据,因为他把要读取的和写的容器分开,实现了读写分离,不须加锁,所以读的效率更高。
- CopyOnWriteArrayList存在读写一致性问题,因为读的是保存的旧数据,所以存在其他线程修改,不能立刻读到新数据。
- &&具有短路功能,&当左右不是boolean数据时为位运算符,表示按位与运算。
-
JVM结构
-
类加载器 (阿里一面题)
- 加载.class文件,并创建对应的class文件。
- 四种类加载器:根加载器,扩展类加载器,系统类加载器,还有用户自定义加载器。
- 双亲委派:从根加载器开始,逐层向下找,也就是优先使用高层的,父类的类加载器,减少重复加载,保证安全。比如自己写的String类不会被加载,保证源码安全。
-
本地方法栈
- Native是一个关键字,代表本地方法,用来与非JAVA程序交互。比如Thread的启动底层就是本地方法。
- 有声明,无实现,标记Native的方法信息保存在 Native Method Stack。
- 各个线程不共享。
-
PC寄存器
- 记录了方法之间的调用和执行情况,用来存储指向下一条指令的地址。它是当前执行的字节码的行号指示器。
- 各个线程不共享。
-
方法区
- 所有线程共享。
- 存储了每一个类的结构信息。如常量池,字段和方法数据,构造函数和普通方法的字节码内容。
- 方法区是规范,不同虚拟机的实现不同。java 7 是永久代,java 8是元空间。
- 实例变量存在堆内存中,和方法区无关。
- JVM规范将方法区描述为堆的一个逻辑部分,但他还有别名Non-Heap,目的是和堆区分开。
-
JAVA栈 (阿里一面题)
- 线程不共享。
- 栈管运行,堆管存储。
- 栈不存在垃圾回收,因为线程结束占内存也就释放。
- 8种基本类型变量+对象的引用变量+实例方法都是在函数的栈内存中分配。
- 每个方法执行的同时会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息,栈的大小和具体的JVM实现有关。
-
堆 (阿里一面题)
- 各线程共享。
- 新生代:默认和老年代比例为1:2。测试中发现实际 新生代=Eden+Survivor0,并不是生代=Eden+Survivor0+Survivor1。
- 伊甸区(Eden):满了会开启YGC(Minor GC),和幸存者区的比例为8:1:1。
- 幸存者0区:又称From区。
- 幸存者1区:又称To区。
- Minor GC:复制->清空->互换,两个幸存者区解决了碎片化。
- 当Eden区满时会触发第一次GC,把活着的对象拷贝到From区。
- 当Eden再次满时会扫描Eden和From区,对这两个区域进行垃圾回收,把这次存活的对象拷贝到to区。
- 清空Eden和From区的对象。
- From和To区交换,对象交换15次仍然存活,就会进入老年代。所以会保证每次To区域都为空。
- 老年代:满了,会开启FGC(Major GC),多次FGC后,仍无法腾出空间,触发OOM。
- 元空间/永久代:
- 永久代或元空间是方法区的一个实现。
- 用于存放JDK自身携带的Class,Interface的元数据,是常驻内存区域。
- 永久代使用的是JVM的堆内存,java8以后替换为元空间,元空间不在虚拟机内,而是使用本机物理内存。
- jinfo -flags PID 查看运行的java程序的JVM参数
-Xms 初始分配大小,默认为物理内存的“1/64”
-Xmx 最大分配内存,默认为物理内存的“1/4" -XX:SurvivorRatio = Ratio ,设置Eden和一个Survivor的比例。
-XX:MaxTenuringThreshold = num 设置对象在新生代中的存活次数,默认是15且最大时15,15是由JVM的年龄计数用的是0000决定的。
-XX:MetaspaceSize = num 设置元空间大小
-XX:+PrintGCDetails 打印GC日志 - 强弱软虚引用
- 强引用:平常new的对象的引用都是强引用,宁愿抛出OOM也不会回收
- 软引用:softReference,内存充足不会被回收
- 弱引用:weakReference,会直接回收,weakHashMap利用了弱引用可以解决缓存占用空间的问题。
- 虚引用:要和引用队列联合使用,跟踪对象被垃圾回收的状态。我们可以在这个对象被回收后收到一个系统通知。
-
GC
- 两种判断对象是否死亡的算法
- 引用计数法
- 给对象添加一个引用计数器,每当有一个地方引用它,计数器+1,引用失效,计数器减1,计数器为0,则认为对象不再被使用。
- 优点:实现简单,判定高校,可以解决大部分问题。
- 缺点: 难以解决循环引用,由于需要维护计数器,所以开销较大。
- 可达性分析
- 又称传递跟踪算法,主流的判定对象是否存活的算法。
- 通过一系列"GC Roots"对象作为起点,开始向下搜索,搜索走过的路径被称为引用链,当某个对象到GC Roots不可达时,则该对象不可用。
- GC Roots对象包括:
- 虚拟机栈(堆栈中本地变量表)所引用的对象。也就是说在虚拟机栈中该对象的生命周期还没有结束。
- 方法区中类静态属性引用的对象,也就是使用了static关键字,保存在了共有的方法区中。
- 方法区常量引用的对象。
- 本地方法栈中引用的对象。
- 优点是准确和严谨,缺点是实现复杂,耗时,分析过程需要GC停顿,被称为Stop The World。
- 引用计数法
- 四种垃圾收集算法 (阿里一面题)
- 分代收集算法:目前大部分垃圾收集器采用的算法,是下面三种方法的结合。
- 年轻代特点是区域相对较小,对象存活率较低,用复制算法。
- 老年代特点是区域相对较大,对象存活率较高,用标记清除和压缩混合实现的算法。
- 复制算法
- 年轻代中采用的算法
- 就是上面提到了复制操作。
- 优点不会产生内存碎片。缺点是费空间,浪费了一半的内存。并且如果Eden区存活率高,那么时间上的损耗也比较大。
- 标记清除
- 老年代中采用的算法。
- 第一次扫描先标记出要回收的对象,然后第二次扫描统一回收这些对象。
- 两次扫描耗时,会产生内存碎片。而老年代的对象往往又占据较大内存,这就导致可能会经常找不到足够大的连续空间,触发下一次GC。
- 标记整理
- 在标记清除的基础上,将存活对象滑动到连续区域,处理内存碎片问题。
- 可以和标记清除结合,多次GC后再压缩。
- 分代收集算法:目前大部分垃圾收集器采用的算法,是下面三种方法的结合。
- 七种垃圾收集器
- 新生代:
- Serial 串行
- 针对新生代,采用复制算法。 2,单线程收集且垃圾收集时必须暂停所有工作线程。
- 稳定,效率高,但会造成较长时间的停顿。
- 配合Serial Old使用
- ParNew 并行
- 多线程收集,相当于是Serial收集器的多线程版本。
- 其他特点和Serial一样。
- 配合CMS使用
- Parallel Scavenge 并行 默认垃圾回收期
- 又被称为吞吐量收集器。
- 新生代收集器,采用复制算法,多线程收集。
- 他的关注点与其他收集器不同,他的目的是达到一个可控制的吞吐量。也就是控制用户代码在CPU总消耗中的比值。(用户代码时间/(用户代码时间+垃圾收集时间))
- 适用场景:高吞吐量为目标,减少垃圾收集时间。
- 自适应调节停顿时间。
- 配合Parallel Old使用
- Serial 串行
- 老年代:
- Serial Old(MSC) 串行
- 是Serial的老年代版本
- 采用标记-整理(标记-清除,标记-压缩)方法,单线程收集,需要暂停所有用户线程。
- CMS 并发
- 并发标记清理。
- 基于标记-清除,不进行压缩操作,产生内存碎片。可以设置参数"-XX:+CMSFullGCsBeforeCompaction",在多次FGC后执行一次压缩操作。
- 目的是获取最短的回收停顿时间,并发收集。
- 出现异常情况将会使用Serial Old。
- 4个阶段
- 初始标记,stw,标记一下GC Roots关联的对象
- 并发标记,和用户线程并发,进行跟踪过程,标记全部对象
- 重新标记,stw,标记由于用户程序运行导致标记产生变动的记录,做一次修正
- 并发清除,和用户线程并发,清除工作。
- 优点:并发收集停顿低
- 缺点:对CPU压力大,内存碎片,必须要在老年代内存用尽之前完成垃圾回收。
- JDK14中被删除
- Parallel Old
- 多线程的标记-整理算法。
- 配合Parallel Scavenge,解决注重吞吐量和CPU资源敏感的场景。
- Serial Old(MSC) 串行
- 整堆收集器:
- G1 面向服务器端,从1.7u4开始支持,目标时取代CMS
- 并行与并发
- G1新增了一个Humongous区,存放大对象,直接分配在老年代。
- Young gc还是会导致停顿。
- 分代收集,收集范围包括新生代和老年代。但这里的分代和Java堆的内存布局不一样。Eden,Survivo和Tenured的内存区域不再是连续的了,而是一个个大小一样的region。
- 整体上看是标记整理,从局部来看是基于复制的。不会产生碎片,有利于长时间运行。每个区域不再独立的属于某一代了,而是会根据状态变化。所以不存在内存碎片了。
- 可预测的停顿,实现了高吞吐量。可以明确指定M毫秒内,垃圾收集消耗的时间不超过N毫秒。
- 不需要更大的堆内存
- 四步回收
- 初始标记
- 并发标记,只有这一步时并发的
- 最终标记
- 筛选回收:根据时间来进行价值最大的回收,可以使用MaxGCPauseMillis指定JVM将尽可能将停顿时间小于这个时间,但不保证。
- ZGC : 目标是低停顿,低延迟
- 达到了任意堆内存大小的停顿时间会控制在10毫秒以内
- 使用了读屏障,染色指针和内存多重映射等技术。
- G1 面向服务器端,从1.7u4开始支持,目标时取代CMS
- 新生代:
- 两种判断对象是否死亡的算法
-
JMM
- 线程对变量的操作必须要在工作内存中,首先要把变量拷贝到线程自己的工作空间,然后对变量进行操作,操作完成后再将变量写会主内存。
- 可见性。由于工作内存和主内存分开,所以某个线程修改完某个变量后,在其他线程中,未必能观察到变量的修改。volatile修饰的变量具备对其他线程的可见通知性,因为他会立即更新到主内存,synchronized和final也能实现可见性。
- 原子性。操作不可分割,同时成功或同时失败。被synchronized关键字或其他锁包括起来的操作也可以认为是原子的。可以用Atomic解决。
- 有序性。java会对一些指令进行指令重排,volatile和synchronized可以保证程序的有序性,保证了指令不进行重排。
- 接口中只有一个方法声明时,可以使用Lambda表达式。
- 接口中可以有方法的实现,default修饰或者是静态方法。
- 接口中可以有方法的实现,所以如果是实现的方法不影响Lambda使用。
- 什么是JUC
- java.util.concurrent的缩写,java在并发编程中使用的工具类。
- 主要有三个部分java.util.concurrent,java.util.concurrent.atomic,java.util.concurrent.locks。
- wait和sleep的区别
- wait会释放锁,sleep不会释放锁。
- 线程的六种状态
- new 新创建的线程,还没调用start
- RUNNABLE 就绪和运行都属于RUNNABLE,调用start后就进入就绪状态
- BLOCKED 线程阻塞与锁
- WAITING 等待其他线程,比如通知或中断
- TIMED_WAITING 指定时间后悔自行返回
- TERMINATED 执行完毕
- 多线程的四种方式
- 继承Thread,重写Run方法
- 实现Runnable接口,重写run方法,可以用于处理同一资源以及多继承。
- 实现Callable接口,重写call方法,可以拿到返回值,允许抛出异常。配合FutureTask使用,拿到返回值的get方法是阻塞的。
- 使用线程池,减少创建线程的时间,降低资源消耗,控制并发线程的数量,可以有返回值。
- synchronized 和 ReentrantLock
- synchronized可以用来同步方法或代码块:
- 同步方法:给方法增加synchronized关键字,可以使静态方法,也可以是非静态方法,但不能是抽象方法。
- 同步代码块,通过锁定一个指定的对象,来对同步代码进行同步。
- 同步是高开销操作,减少使用同步方法,同步关键代码即可。
- ReentrantLock:可重入锁,代码通过lock()方法获取锁,调用unlock释放锁。
- JDK1.5前synchronized的性能较低,JDK1.6后性能基本一致。
- 为什么1.6后synchronized性能提高了,因为引入了锁升级
- 锁的4种状态:无锁,偏向锁,轻量级锁,重量级锁
- 偏向锁的升级,当线程1访问代码并拿到锁后,会在对象头和堆栈中记录偏向锁的threadID,偏向锁不会主动释放锁,如果有线程继续获取锁,比较当前线程的threadID是否一致,如果不一致,则产看线程1是否存活,如果线程1存活且持有这个对象,那么 升级为轻量级锁,否则让设置为无锁状态,有新线程持有偏向锁。
- 轻量级锁不会阻塞其他竞争锁的线程,而是采用自旋让他等待锁的释放,如果自旋达到一定次数后,线程1还没释放,那且这时候又有新的线程来竞争锁,那么这时候轻量级锁会升级为重量级锁,防止CPU空转。
- 锁可以升级,但不能降级。
- ReentrantLock可以配合多个condition的signal方法实现指定唤醒。
- 类中有多个synchronized方法,synchronized锁的是当前的实例对象this,所以同一时刻只能有一个线程能调用其中一个synchronized方法。
- 当锁上的是静态方法,锁的是当前Class对象,所以即使是多个实例,也只能有一个调用静态方法。
- 普通方法上锁和静态方法上锁,两者不会互相影响,因为锁的对象不一样,一个是当前模板一个是对象示例,
- synchronized可以用来同步方法或代码块:
- 多线程中为防止虚假唤醒,避免使用if,使用while。
- notify只会唤醒一个等待线程,notifyAll会唤醒所有的等待线程,notify使用不当会导致死锁,所以更推荐使用notifyAll。
- CountDownLatch,当一个或多个线程调用await时,线程会阻塞,当其他线程调用countDown时,计数器会减一,值为0时,被await阻塞的线程会被唤醒。
- CyclicBarrier,和CountDLatch相对,做加法,达到输入值后会执行定义好的方法。
- Semaphore,信号量主要定义了两种操作,acquire和release。调用acquire时,如果信号量能成功获取到则继续执行,否则就等待。release则会将信号量加1,然后唤醒等待的线程。
- ReadWriteLock,读-读操作是可以共存的,读-写,写-写是不可以共存的,会破坏读写一致性,ReadWriteLock解决该问题。
- 阻塞队列
- ArrayBlockingQueue 基于数组结构的有界阻塞队列,FIFO
- LinkedBlockingQueue 基于链表结构的可设置有界的阻塞队列 FIFO,put时使用了ReentrantLock保证线程安全。
- SynchronousQueue 不存储元素的阻塞队列,每个插入操作要等到另一个线程调用移除操作。
- 线程池
- Executors.newFixedThreadPool() 固定线程数的线程池
- Executors.newSingleThreadExecutor() 只有一个线程的线程池
- Executors.newCachedThreadPool() 动态调整的线程池
- 三种方法底层调用的都是ThreadPoolExecutor,他的实现是阻塞队列。
- 七大参数
- corePoolSize:常驻核心线程数,
- maximumPoolSize:线程池中的最大线程数
- keepAliveTime:空闲线程存活时间,超过这个存活时间,多余线程会被销毁。
- unit:时间单位
- BlockingQueue workQueue:阻塞队列,被提交但还没有被执行的任务
- ThreadFactory threadFactory:线程池中工作线程的线程工厂
- RejectedExecutionHandler handler:拒绝策略,当超过最大线程数时如何拒绝新的请求,最大数为maximumPoolSize+队列大小。
- 四种拒绝模式
- AbortPolicy:直接抛出异常阻止系统正常运行。
- CallerRunsPolicy:不会抛出异常也不会抛弃任务。将任务回退到调用者。
- DiscardPolicy:直接丢弃无法处理的任务
- DiscardOldestPolicy:抛弃等待最久的任务,就是最先入队列的那几个任务。
- 实际使用中都不使用,而是自己通过ThreadPoolExecutor自定义。
- CAS
- 比较并交换。先从主内存拿到最新数据并比较,如果和期望值一直返回true,否则返回false;
- 主要由unsafe类实现,unsafe方法都是native修饰,所以可以直接调用操作系统底层资源执行相应的任务。
- ABA问题,一个线程将数据修改成功后又修改了回来。解决方法,添加版本号。使用AtomicStampedReference多维护一个版本号。
- 由于自旋的存在,可能由于一直尝试不成功,会给CPU带来较大开销。(这个线程在操作前总有线程先修改)
- 只能保证一个变量的原子操作,多个变量还是需要加锁。
- 锁
- 公平锁按照先来后到,非公平锁高优先级的可能会优先执行,非公平锁的吞吐量比公平锁大。
- ReentrantLock默认为非公平锁,Synchronized也是非公平锁。
- 可重入锁(递归锁).就是线程可以进入仍和一个他已经拥有锁所同步的代码块。Synchronized和ReentrantLock都是可重入锁。
- 自旋锁。用循环替代堵塞,CAS。
- 读写锁。ReadWriteLock,保证读-写,写-写的并发安全。
- volatile底层实现
- 被volatile修饰的变量转换为汇编指令后会加入一个lock前缀指令
- lock前缀指令相当于是一个内存屏障,内存屏障是一组处理指令,用来实现对内存操作的顺序限制。
- 死锁
- 为什么会产生死锁
- 系统资源不足
- 资源分配不当
- 线程运行顺序不合适
- 死锁的四个必要条件
- 互斥条件,分配的资源排他性使用,一次只能被一个线程占用
- 请求与保持联系,线程获得了资源,有提出了新的资源请求,而这个资源已被其他线程占有,此时线程阻塞,但资源继续占有
- 不可剥夺,线程获得资源后,没有使用完不会被抢占,只能自己释放
- 循环等待,p1等待p2,p2也等待p1
- 死锁的处理
- 可以考虑不去显示的取锁,用信号量去控制。
- 给信号量设置超时时间,超过超时时间没有取得锁,就跳出等待。
- 为什么会产生死锁
- Redis是一种分布式的内存数据库,索引从0开始,默认有16个库,Select命令切换。
- 五大数据类型
- String,Redis的最基本类型,可以包含仍和数据,比如图片和序列化对象,一个字符串value最多可以是512M。
- List,简单的字符串列表,底层是一个链表。
- Set,无序的string集合,是通过HashTable实现的。
- Hash,一个string类型的field和value映射表。
- Zset,有序的string集合,每个元素关联到一个double类型的分数。成员是唯一的,分数是可以重复的。
- bitmap 新特性
- Key的基础知识
- keys * 显示当前库下的所有key
- move key db 移动某个key到指定库
- expire key 秒钟 设定某key过期时间
- ttl key -1表示永不过期 -2表示已过期
- type key 查看key是什么类型
- exists key,判断某个key是否存在,存在为1,没有为0
- 对于已有值的key,重复赋值会覆盖。
- String的基础知识
- set/get/del/append/strlen 看名知用途
- INCR/DECR 自增1/自减1 INCRBY/DECRBY 指定大小的加减,只能用于数字
- get/setrange 获得/设置指定区间的数据
- setex set的同时设置存活时间
- setnx key不存在才set
- mset/mget/msetnx m代表more 多值赋值,msetnx必须要全部都不存在才能正常插入
- getset 先get再set
- List的基础知识
- lpush/rpush/lrange lpush和rpush区别就是lpush往左边插,可以看做是头插法,rpush是尾插法。
- lpop/rpop/lindex/llen 看名知用途
- lrem key n value 删除n个指定value,如果value数少于n则有多少删多少
- ltrim key 截取后再赋值给key
- rpoplpush 源列表 目的列表 源列表rpop后lpush到目的列表
- lset key index value 根据下标设置值
- linsert key before/after v1 v2 多个v1只会插在第一个前后
- 底层是链表,头尾插入效率高,对中间元素操作效率较差,因为查询是O(n)的。
- Set的基础知识
- sadd/smembers/sismember 添加/查询/检查
- scard 获取集合里的元素个数
- srem key value 删除某个元素
- srandmember key 随机出几个数
- spop 随机出栈
- smove key1 key2 n 将key1中的n赋给key2
- sdiff/sinter/sunion 差集/交集/并集
- Hash的基础知识
- kv模式不变,但v变成了键值对
- hset/hget/hmset/hmget/hgetall/hdel
- hkeys/hvals 拿到所有的key或val
- hincrby/hincrbyfloat
- hsetnx
- Zset的基础知识
- zadd/zrange
- zrangebyscore
- withscores
- ( 代表不包含
- limit index step 添加返回限制
- zrem key value 删除元素
- zcard/zcount/zrank/zscore 统计个数/带区间的统计/获得指定value下标/获得分数
- zrevrange/zrevrangebyscore 翻转
- Redis的持久化
- rdb Redis DataBase
- 指定的时间间隔内将内存中的数据集快照到硬盘。
- redis会单独创建(复制)一个子进程进行持久化,先将数据写入到临时文件,持久化过程结束后,用临时文件替换上次持久化的文件。
- RDB方式比AOF的方式更高效,但最后一次持久化的数据可能丢失。
- 数据保存在dump.rdb,快照可以自己设置触发机制,规则为在N秒内发生过M次修改。
- 人为FlushAll,ShutDown时会自动执行快照保存,重启时会从指定文件加载数据。
- save命令可以手动创建快照,阻塞的。bgsave在后台异步进行快照。
- stop-writes-on-bgsave-error 表示保存出错时是否停止数据写入,设置为no则代表不在乎数据一致性,或者有其他方法修复
- rdbchecksum 存储快照后,增加数据校验
- rdbcompression 存储快照后,是否进行压缩存储
- 优点,适合大规模的数据恢复,缺点就是最后一次时间段可能还没有触发保存,那么这部分数据就丢失了,还有由于fork进程,需要两倍的内存空间。
- conf修改中主要在snapshot区域。
- aof Append only file
- 已日志形式记录写操作,只追加不改写文件,redis启动后会读取该文件重新构建数据。
- conf修改中主要在Append only model,默认是关闭的。
- 如果存在aof那么优先加载aof,可以使用Redis-check-aof--fix修复
- Appendfsync
- always:每次数据变更都会记录,性能差但是数据完整性好
- Everysec:异步操作,每秒记录,一秒内宕机会有数据丢失
- no: 不会自动同步
- Rewrite,为了避免文件过大,当超过阈值时会触发重写机制,启动内容的压缩,只保留可以恢复数据的最小数据集。
- aof恢复效率低于rdb,保存的文件远大于rdb。
- 建议同时开启两张持久化方式
- RDB文件作为后备用途,只要15分钟备份一次就可以了。
- AOF最恶劣情况的数据丢失不超过两秒,但带来了持续的IO操作压力。AOF rewrite会造成阻塞,为减少rewrite的次数,AOF需要修改默认的重写基础大小和默认超过原大小100%重写。
- rdb Redis DataBase
- Redis的事务
- 一个事务中的所有命令会被序列化,按顺序的穿行执行而不会被其他命令插入。Redis对事务是部分支持,命令错误才会全部取消执行。
- DISCARD:放弃事务
- EXEC:提交事务
- MULTI:开启事务,如果语句错误就全部失败,如果是语句正确但运行失败,那不会影响其他语句。
- UNWATCH:出现了别人的修改后要取消监控,在重新进行一次WATCH监控。
- WATCH key:相当于加了一个乐观锁,出现别人的修改后,整个事务队列的操作都无法执行。
- 乐观锁:自己维护一个version字段,更新过后就会修改version,保证不会被其他线程的修改覆盖。
- 悲观锁:往往是互斥锁,只有一人能处理资源,极大地影响并发性。
- 发布和订阅
- 进程间的消息通信模式:发送者发送消息,订阅者接受消息。
- 主从复制,读写分离
- 主机数据更新后,根据配置和策略,自动同步到备机的master/slaver,Master以写为主,Slave以读为主
- 配从不配主。
- 从库配置:slaveof 主库IP 主库端口
- 第一次连接是全量复制,后面是增量复制,复制会有延时。
- 一主多仆
- 一个master多个slaver。
- master负责写,slaver只负责读。
- 主机挂了后,从机原地待命,并不会自己转为主机,主机重连后可以直接连上。
- 从机挂了后,需要重新配置连接,除非写进配置文件。
- 主机压力较大。
- 薪火相传
- 部分从机也指向从机,降低主机的压力。
- 被从机连接的从机,身份上仍然是slaver。
- 反客为主
- slaveof no one
- 使当前数据库停止与其他数据库同步,转成主数据库
- 从库需要重新指向新的主库。
- 哨兵模式
- 反客为主的自动版,主机挂掉后,投票选出新主机,主机重连后变为从机。
- 新增sentinel.conf,用于配置监控主机挂掉后的投票数
- 使用redis-sentinel工具加载配置文件。
- 哨兵是一个独立的进程,烧饼通过发送命令,等待Redis服务器响应,监控运行的多个Redis实例。
- 检测到master宕机后,投票选出新出及后通过发布订阅模式通知其他从机修改配置。
- 正常业务流程
- 先读cache,如果数据命中返回cache数据
- 如果没有命中,则去查db,将db读出来后放入缓存。
- 缓存击穿
- 一个存在的key,在缓存过期的一瞬间,大量请求击穿打到DB,造成瞬间DB请求压力过大。
- 采用互斥锁,没有拿到锁的线程自旋重新进入请求。
- 缓存穿透
- 恶意访问一个不存在的key,缓存不起作用,直接打到db,造成大流量时DB挂掉。
- 设置一个存活时间较短的null值缓存,保证不会直接打到数据库。
- 缓存雪崩
- 大量的key设置了相同的过期时间,导致缓存在同一时间全部失效。
- 缓存过期时间设置随机。
- MySql的存储引擎
- InnoDB
- 支持外键
- 支持事务
- 行锁,适合高并发
- 不仅缓存索引还要缓存真实数据,对内存要求较高,数据文件本身就是索引文件。
- 表空间大
- 关注点在于事务的控制
- InnoDB数据文件本身按主键聚集,叶节点包含了完整的数据记录,这种索引叫做聚集索引。
- MyISAM
- 不支持外键
- 不支持事务
- 表锁
- 缓存只缓存索引
- 表空间小
- 关注点在于性能,也就是查询更快
- MyISAM的索引文件只保存数据记录的地址,MyISAM的主索引和辅助索引,在结构上没有任何区别,每次查询是先查出指定key的data域,然后以data域的值为地址,读取相应数据记录,这种叫做非聚集索引。
- InnoDB
- SQL慢的主要原因
- 查询语句写的烂
- 索引失效
- 关联查询太多join(设计不合理或不得已)
- 服务器调优
- Join查询
- A INNER JOIN B ON A.key=B.key A和B的交集,只有两者的公有部分
- A LEFT JOIN B ON A.key=B.key 全A,A的独有部分的B表数据补null
- A RIGHT JOIN B ON A.key=B.key 全B,B的独有部分的A表数据补null
- A LEFT JOIN B ON A.key=B.key where B.key is NULL A独占,只查出A独有的部分
- A RIGHT JOIN B ON A.key=B.key where A.key is NULL B独占,只查出B独有的部分
- A FULL OUTER JOIN B ON A.key=B.key A+B ,MySQL不支持这样的语法,可以用UNION实现
- A FULL OUTER JOIN B ON A.key=B.key where A.Key is NULL or B.key is NULL A独占+B独占
- 索引 (阿里一面题 谈谈你对索引的理解)
- 索引是一种帮助MySql高效获取数据的数据结构,可以简单理解为排好序的快速查找数据结构。
- 聚集索引,次要索引,覆盖索引,复合索引,前缀索引,唯一索引默认都是B+树索引。
- 优势:
- 提高数据检索的效率,降低数据库的IO成本。
- 降低数据排序的成本,降低了CPU的消耗。
- 劣势:
- 索引保存了准建和索引字段,所以索引列也要占空间。
- 索引能提高查询速度,同时会降低更新表的速度。MySQL在更新操作时,不仅要保存数据,还要更新索引。
- 索引需要花时间研究最优的索引,和业务环境有紧密联系。
- 单值索引:一个索引只包含单个列,一个表可以有多个单列索引。
- 主键索引:特殊的唯一索引,不允许有空值,
- 唯一索引:索引列的值必须唯一,但允许有空值。
- 复合索引:一个索引有多个列,必须要按照顺序才能有效,也就是最左前缀原则。
- 聚集索引:叶子节点存放的是整行的数据,直接通过这个聚集索引的键值找到某行。Innodb通过主键聚集数据,如果没有定义主键,会选择第一个非空的唯一索引替代。
- 覆盖索引:select的数据列只需要从索引中就能取得,不必读取数据行,也就是查询列被所建的索引覆盖。
- 创建索引的场景
- 主键自动建立唯一索引
- 频繁查询的字段应该创建索引。
- 查询中与其他表关联的字段,外键关系建立索引。
- 高并发下倾向于创建组合索引。
- 查询中排序的字段,排序字段若通过索引去访问,将提高排序速度。
- 查询中统计或者分组的字段,因为这两个操作都和排序有关。
- 不创建索引的场景
- 表记录太少
- 经常增删改的表
- 重复较多且分布平均,索引的选择性是指索引列中不同值的数目与表中记录数的比。
- where条件里用不到的字段不建立索引。
- Explain的使用
- id:表示查询中执行select子句或操作表的顺序。id相同执行顺序由上至下。id值越大,优先级越高,优先执行,所以要用小表驱动大表。
- select_type : 代表查询的类型
- SIMPLE:不包含子查询和UNION
- PRIMARY:复杂查询中的最外层查询
- SUBQUERY:select或where中的子查询
- DERIVED:FROM中的子查询的临时表
- UNION:出现在UNION之后或包含UNION的子查询的最外层select。
- 从UNION表中获取结果的结果集。
- type:访问类型,最好到最差:system>const>eq_ref>ref>range>index>all,至少能达到range,最好能达到ref。
- possible_keys 可能应用到的索引,不一定被实际使用
- key:实际使用的索引。如果使用了覆盖索引,则该索引值出现在key中。
- key_len:索引中使用的字节数,可通过该列计算查询中使用的索引的长度。在不损失精确性的情况下,长度越短越好。
- ref:显示索引的哪一列被使用,并显示使用的是常量还是表数据。
- rows:每张表有多少行被优化器查询。
- extra:
- using filesort:会对数据使用一个外部的排序,而不是按照表内的索引。
- using tempory:使用了临时表保存中间结果,如order by,group by,极大影响性能。
- using index:使用了覆盖索引,避免了扫描数据行。
- using join buffer:多表连接时使用到的缓存
- 索引优化 2. 双表分析:连接时都是相反的加索引,因为搜索的重点在于连接的表(左连接右表,右连接左表),所以索引要加在搜索重点的表。 3. 最佳左前缀法则:如果索引了多列,要遵循最佳左前缀法则,就是要从索引的最左前列开始,并且不能跳过中间的列。 4. 不能在索引列上做任何计算。 5. 范围查询会导致范围后的索引全部失效。可以考虑跳过排序字段建立索引,就是将可能要排序的字段放在索引的最后。 6. 减少使用select*,只查询索引的字段效率会好一些,覆盖索引。 7. 使用不等于符号会导致全表扫描。 8. 涉及到is not null判断无法使用索引。 9. like通配符%放左边会导致从当前开始的索引失效,放右边会比较好。如果左边一定要有%,使用覆盖索引优化。 10. 字符串不加''会导致索引失效 11. or会导致索引失效。 12. order by的索引无法用于查找,但如果顺序一致可以用于排序,否则会导致fileSort. 13. group by 分组之前必排序,所以会有临时表产生。规则和order by基本一致。
- 索引条件下推
- 对于一些无法使用复合索引过滤的情况,使他们在index filter阶段使用索引进行过滤,无需回表进行table filter。
- 比如 SELECT * FROM people WHERE zipcode='95054' AND lastname LIKE '%etrunia%' AND address LIKE '%Main Street%'; 这个查询语句中,lastname和address无法使用索引进行直接过滤,需要检索表中所有zipcode满足的数据行进行筛选,有了ICP之后,可以在index filter阶段使用索引进行过滤,在查询整张表前进行过滤。
- RPC
- Remote Procedure Call 远程过程调用
- 核心模块:通讯,序列化
- 工作过程,A服务器调用B服务器的一个方法
- A服务器发起调用,客户端部分先建立和B服务器的连接
- 将参数序列化,并将调用信息发送给服务器。
- 服务器将参数反序列化,并调用本地服务
- 处理后,将结果序列化,发送给客户端
- 客户端将结果反序列化,得到最后的调用结果
- dubbo
- 面向接口代理,屏蔽调用的底层细节
- 智能负载均衡,提高系统吞吐量
- 注册中心维护了一个服务清单,支持服务的自动注册和发现
- 通过配置不同路由规则,实现灰度发布
- 可视化的服务治理与运维工具
- dubbo的应用架构
- Registry:provider向容器登记,consumer订阅所需要的服务,当服务发生改变后,会通知Consumer。推荐使用Zookeeper。
- Consumer:消费者模块
- Provider:提供服务的模块
- Container:框架容器
- Monitor:监控中心,监控服务状态
- 高可用场景
- zookeeper宕机与dubbo直连
- zookeeper宕机后,不会影响使用,但是会丢失部分采样数据
- 服务提供者和服务消费者通过本地缓存通讯。
- 即使没有监控中心,也可以通过直连dubbo的方式调用服务
- 但宕机后无法在注册新的服务。
- 负载均衡
- Random LoadBalance:基于权重的随机负载均衡,通过设置权重,调整请求分布。访问无序,随机,但大体上复合权重概率,默认使用此方法
- RoundRobin LoadBalance:基于权重的轮询负载均衡,保证请求访问的顺序。
- Least Active LoadBalance:最少活跃数负载均衡机制,总是选择处理最快的服务器。
- ConsistenHash LoadBalance:一致性hash负载均衡机制,计算方法名+参数的哈希,确定访问哪一台。
- 服务降级
- 当服务器压力剧增时,对一些服务和页面有策略地不处理或换简单方式处理,保证核心服务正常运作。
- 两种策略:
- 直接返回null
- 调用失败后返回null,不会抛出异常或错误
- 集群容错
- Failover Cluster 失败时自动切换,尝试其他服务器,通常用于读操作
- Failfast Cluster:只发起一次调用,通常用于写操作
- Failsafe Cluster:出现异常时,直接忽略,通常用于写入日志等操作
- Failback Cluster:失败后自动回复,后台记录失败请求,定时重发,通常用于消息通知
- Forking Cluster:并行调用,只要一个成功就返回
- Broadcast Cluster:广播调用所有提供者,一个失败就认为失败,用于缓存更新等操作
- 整合Hystrix,提高容错能力。(springcloud默认使用)
- zookeeper宕机与dubbo直连
- dubbo原理
- dubbo封装了RPC的内部细节,只需要以本地调用方式调用服务,就能得到服务返回的结果
- 通信只用到了Netty,netty是基于nio的一个异步时间驱动的网络应用程序框架。
- 底层可以认为是netty+动态代理
- 通过BeanDefinitionParser解析标签,每个标签对应一个beanClass。
- 底层看不懂了,以后再说吧
- Bean的基础知识
- 三种创建方法
- 在spring的配置文件中使用bean标签,配以id和class属性后,且没有其他属性和标签。该方法要求必须要有默认构造函数。
- 使用普通工厂中的方法创建对象,并存入spring容器。
- 使用工厂中的静态方法创建对象,并存入spring容器。
- scope标签:singleton单例的,prototype多例的,request作用于请求范围,session作用于会话范围。
- 生命周期:
- 单例对象:和容器相同
- 多例对象:在使用时出生,使用过程中活着,由GC进行回收,spring不处理。
- 依赖注入:只需要在配置文件中说明,spring就能管理类之间的依赖关系。
- 构造函数注入:添加constructor-arg标签,优点在于注入数据时必须的操作,否则无法创建,缺点是改变了实例方法,造成参数的浪费。
- set方法注入:添加标签property,优点在于创建对象没有明确限制,可以直接使用默认构造函数,缺点在于如果某个成员必须有值,获取对象时可能出现值的缺失。
- 复杂数据类型,List结构和Map结构,List使用标签list array set,Map结构使用map,props。
- 三种创建方法
- IoC
- 基本概念
- 控制反转,依赖注入
- 依赖注入的三种方式
- 构造方法注入
- setter方法注入
- 接口注入,不提倡的一种方式。
- Spring的IoC容器 BeanFactory
- BeanFactory和ApplicationContext
- BeanFactory,基础性IoC容器,提供完整的IoC服务支持,如果灭有特殊指定,默认采用延迟初始化策略。
- ApplicationContext在BeanFactory上构建,是相对比较高级的容器实现,提供了其他高级特性,比如事件发布,国际化信息支持。对象在该类型容器启动之后,默认全部初始化并绑定完成。
- 对象注册和依赖绑定方式
- 直接编码方式
- 外部配置文件方式
- 注解方式 @Autowired,将告知spring容器需要为当前对象注入哪些依赖对象,@Component配合classpath-scanning功能使用,告知该类需要被加载
- 手写IOC:构造一个用于创建Bean对象的工厂 1. 首先需要一个配置文件来配置我们的service和dao配置内容,唯一标识为全限定类名 2. 读取配置文件内容,反射创建对象,解决对具体实现类的依赖。 3. 工厂类加载时,将读取的内容涉及到的对象全部保存到map中,实现单例。
- BeanFactory和ApplicationContext
- 基本概念
- AOP
- 应用场景:日志记录,权限验证,效率检查,事务管理
- 什么是AOP:传统OOP编程是自上而下的,AOP可以抽取出业务流程中共用的代码,在对源代码无入侵性的情况下,实现功能的加强。
- Aspect:切面通常是一个类,定义切入点和通知
- 连接点 JointPoint:程序执行过程中的明确的点,一般是方法的调用
- Pointcut切入点:带有通知的连接点,主要体现为书写切入点表达式
- AOP代理,代理就是对象的加强。
- 手写AOP:动态代理 (阿里一面题)
- 动态代理:字节码随用随创建,随用随加载,第一种可以使用Proxy类中的newProxyInstance方法第二种使用cglib。
- 方法参数:
- ClassLoader:类加载器,用于加载代理对象字节码,使代理对象和被代理对象用同样的类加载器代理谁就写谁
- Class[]: 字节码数组,让代理对象和被代理对象有相同的方法。
- InvocationHandler:用于提供增强的代码
- proxy:构造一个代理工厂,通过向工厂注入原始的对象,用动态代理的方式重写get方法,将代理对象放回容器。在invoke方法中,增加前置,后置,异常,最终四种方法。
- cglib:可以代理非接口对象。
- 七大原则
- 单一职责原则:一个类应当只负责一项职责。
- 降低类的复杂性,一个类只负责一项职责。
- 提高代码可读性。
- 降低变更带来的风险。
- 通常情况下,我们应当严格遵守单一职责,除非代码逻辑极其简单,类中方法数量足够少。
- 接口隔离原则:不应依赖不需要的接口,一个类对一个类的依赖应该建立在最小的接口上
- 依赖倒转原则
- 高层模块不应依赖底层模块,应当依赖其抽象 2, 抽象不应依赖细节,细节应该依赖抽象
- 面向接口编程
- 里式替换原则
- 所有引用基类的地方必须能透明的使用其子类
- 子类中尽量不要重写父类的方法
- 通过聚合,组合,依赖来解决问题,将原本的继承关系去掉,而去继承一个更通俗的基类。
- 开闭原则
- 类,模块,函数应该对扩展开放,对修改关闭。用抽象构建框架,用实现扩展细节。
- 软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
- 迪米特法则
- 最少知道原则
- 尽量将逻辑封装在类的内部,除了对外提供的public方法,不对外泄漏任何信息。
- 合成复用原则
- 尽量使用合成/聚合的方式,而不是使用继承
- 单一职责原则:一个类应当只负责一项职责。
- 单例模式
- 饿汉式:线程安全,反射不安全,反序列化不安全
- 登记式:又称静态内部类式,线程安全,防止反射攻击,反序列化不安全
- 枚举式:线程安全,支持序列化,反序列化安全,防止反射攻击
- 懒汉式:线程不安全,延迟加载,可以加同步解决线程安全,效率低,加双捡锁优化,volatile避免由于指令重排出现的空对象。
- ThreadLocal:在各个线程内式单例的,不同线程不保证单例
- CAS:无锁乐观策略,线程安全
- 工厂模式
- 简单工厂模式,又叫静态工厂方法模式,通过专门定义一个类来负责创建其他类的实例。不符合OCP原则,
- 工厂方法模式,又被称为多态工厂模式,定义一个创建产品对象的工厂接口,将实际创建工作推迟到子类。
- 抽象工厂:任何工厂类都必须实现此接口
- 具体工厂负责实例化产品对象
- 抽象角色,负责描述公共接口
- 具体角色,工厂创建的实例对象
- 抽象工厂模式
- 抽象工厂对应于产品结构,具体工厂对应产品族。
- 原型模式
- 由原型对象自身创建目标对象。
- 目标对象是原型对象的一个克隆。
- 根据对象克隆深度不同,有浅克隆和深克隆。浅克隆不会克隆对象,只会克隆对象的引用。
- Cloneable 只有实现这个接口才能被克隆。
- 建造者模式
- 为对象的创建而设计的模式
- 创建的是一个复合属性的对象
- 关注对象的各部分的创建过程,不同的工厂对产品的属性有不同的创建方法。
- 装饰模式
- 避免出现类爆炸
- 又称包装模式,是继承的一个替换方案,通过对客户端透明的方式来扩展对象的功能。
- 抽象组件角色:一个抽象接口,是被装饰类和装饰类的父接口
- 具体组件角色:包含一个组件的引用,并定义了与抽象组件一致的接口
- 具体装饰角色:为抽象装饰角色的实现类,负责具体的装饰。
- IO中的FilterInputStream使用到了装饰者模式。
- 策略模式
- 主要用于平滑的处理算法的切换。
- Strategy:算法的抽象
- Concrete Strategy:各种算法的具体实现
- context:策略的外部封装类,或者说策略的容器。
- 常见搭配工厂模式,享元模式使用。
- 适配器模式
- 将某个类转换成客户期望的另一个借口标识,主要目的是兼容性。使得接口不匹配不能一起工作的两个类可以协同工作,别名为包装器。
- 主要分三种:类适配器,对象适配器,接口适配器
- 用户看不到被适配者,是解耦的。
- 类适配器
- 类适配器需要继承src类,这是一个缺点,并且dst必须是接口,有一定局限性
- src类的方法会在Adapter中暴露出来,增加了使用成本
- 对象适配器
- 根据合成复用原则,通过聚合关系,去掉继承。
- 接口适配器模式
- 默认适配器模式或缺省适配器模式
- 不需要全部实现接口方法时,可先设计一个抽象类实现接口,并为每个方法提供一个孔方法,抽象类子类可以选择的覆盖某些方法
- 适用于一个接口不想用其所有的方法的情况。
- SpringMVC中的HandlerAdapter中就用到了适配器模式。
- 桥接模式
- 将实现和抽象放在两个不同的类层次中,使两个层次可以独立的改变
- JDBC使用到了桥接模式, 通过分离数据库种类和操作,实现不同数据库的具体操作。
- 组合模式
- 又叫部分整体模式,创建了对象组的树形结构,将对象组合成树状结构的层次关系
- 目的是单个对象和组合对象的访问具有一致性。
- HashMap中使用到了组合模式,Map是一个抽象的构建,HashMap相当于是一个中间构建,Node是叶子结点。
- 外观模式
- 也叫过程模式,目标在于为子系统的一组接口提供一个一致的界面,用于屏蔽内部自系统的细节,使得调用端只需跟这个接口发生调用,无需关心子系统的内部细节。
- myBatis中configuration中创建MetaObject时,使用到了外观模式。
- 当子系统足够复杂时才考虑使用外观模式,否则不应使用外观模式。
- 享元模式
- 经典应用场景是池技术,共享对象,解决内存浪费的问题。
- 内部状态是可以共享的信息,而外部状态是对象依赖的一个标记,随着环境改变,不可共享。
- Integer,String等都可以看做是享元模式。
- 代理模式
- 主要有三种,静态代理,动态代理,Cglib代理
- 动态代理:需要有顶层接口才能使用,但是在只有顶层接口的时候也可以使用,常见是mybatis的mapper文件是代理。
- 模板模式
- 钩子方法:默认不做任何事,子类可以视情况要不要覆盖他,可以用于模板方法中,控制某些方法不使用。
- IOC容器的初始化时,使用到了模板方法。
- 主要用于抽取类似任务中相同的操作,将不同的步骤延迟到子类中,使得子类可以不改变算法的结构,就可以重新定义该算法的某些特定步骤。
- 属于行为型模式。
-
为什么要用MQ
- 解决耦合调用的问题
- 提供一个异步模型
- 抵御洪峰流量,消峰平谷。
-
MQ的缺点
- 可用性较低,如果MQ宕机,会对业务造成影响
- 复杂度提高了,如何保证没有重复消费,如何解决消息丢失,如何保证顺序性
- 一致性问题
-
工作流程
- 启动NameServer,监听端口,等到Broker,Producer,Consumer,相当于是一个路由控制中心
- Broker启动,跟所有的NameServer保持长连接,定时发送心跳包,心跳包中包含当前Broker信息以及存储所有Topic信息,注册成功后,NameServer就有Topic和Broker的映射关系
- 收发消息前,先创建Topic,指定存储再哪些Broker上。
- Producer发送消息,启动时先和NameServer中的一台建立长连接,并从NameServer中获取当前Topic存在哪些broker上,轮询从队列列表中选择一个队列,然后于Broker建立长连接,从而向Broker发消息
- Consumer和Producer类似,与一台NameServer建立长连接,获取当前订阅Topic存在哪些Broker上,然后建立连接,消费信息。
-
几种MQ的比较
- ActiveMQ
- 开发语言:JAVA
- 单机吞吐量:万级
- 时效性:ms级
- 可用性:高 主从架构
- RabbitMQ
- 开发语言:erlang
- 单机吞吐量:万级
- 时效性:us级
- 可用性:高 主从架构
- RocketMQ
- 开发语言:JAVA
- 单机吞吐量:10万级
- 时效性:ms级
- 可用性:非常高 分布式架构
- kafka
- 开发语言:scala
- 单机吞吐量:10万级
- 时效性:ms级
- 可用性:非常高 分布式架构
- ActiveMQ
-
启动RocketMQ
- 启动 nameServer bin/mqnamesrv
- 启动 broker,bin/mqbroker 需要调整默认虚拟机大小
- 使用自带的脚本进行测试
-
RocketMQ集群搭建
- 角色介绍
- Producer:消息的发送者
- Consumer:消息的接收者
- Broker:暂存和传输信息,通过name确定分组,通过id确定主从
- NameServer:管理Broker
- Topic:区分消息的种类,每一个发送者可以发送多个Topic消息,一个接收者可以订阅多个Topic消息
- Message Queue:相当于Topic的分区,用于并行的发送和接受消息。
- GroupID,代表了不同的生产组或者消费组。同一个GroupID最好订阅相同的Topic+Tag,避免在负载均衡时,产生不明确的结果,造成消息的丢失。
- 集群模式
- 单master模式:一旦Broker重启或宕机,会导致整个服务不可用。
- 多master模式:集群中无slaver,全部是master
- 优点:配置简单,单master宕机或重启维护对应用无影响,性能高
- 缺点:当即期间,未被消费的消息在机器恢复前不可被订阅,消息的实时性会受到影响。
- 多Master多Slave(异步):采用异步复制方式,当落盘后就回复producer,然后再复制
- 优点:消息丢失少,消息实时性不受影响,
- 缺点:master宕机后,可能会丢失少量信息,主备有短暂的消息延迟
- 多Master多Slave(同步):采用同步复制方式,当落盘并复制完成后再回复producer。
- 优点:消息无延迟,可用性非常高
- 缺点:性能比异步复制略低,主节点宕机后,备机不能自动切换为主机
- 角色介绍
-
RocketMq-console
- 可视化工具,git clone后打包成jar包后运行即可使用
-
消息发送
- 消息发送者步骤:
- 创建生产者producer,并定制生产者组名
- 指定Nameserver地址
- 启动producer
- 创建消息对象,指定topic,tag和消息体
- 发送消息
- 关闭生产者
- 消息消费者步骤
- 创建消费者Consumer,制定消费者组名
- 制定Nameserver地址
- 订阅Topic和Tag
- 制定回调函数,处理消息
- 启动消费者
- 同步消息:消息的生产者发送消息后,线程会阻塞的等待MQ回传结果。用于短信通知或重要的消息通知。
- 异步消息:生产者不等待MQ回传,用于对响应时间敏感的场景,即发送端不能容忍长时间等待Broker的响应。通过回调函数拿到发送结果
- 单向消息:不关心发送结果的场景,比如日志。使用sendOneway
- 消息发送者步骤:
-
消息消费
- 负载均衡:默认方式,多个消费者消费队列信息,每个消费者处理的信息不同
- 广播模式:每个消费者消费的消息都是相同的。
-
顺序消息
- 对于一些业务,比如订单的处理,我们需要保证局部的有序。创建,付款,推送,完成必须是有序的。所以我们需要将这些内容发送到同一个队列中,单线程的去消费,保证消息的有序。
- 通过传递业务标识(ID),提供选择器,实现发送到同一队列。
-
延迟消息
- 提交消息后,设置延迟时间,消息将会在延迟时间后发送。rocketMQ设定了18个等级的延迟时间,最长2h。
- 使用方法:setDelayTimeLevel()
-
批量消息
- send参数改为集合,不能是延迟消息。
- 超过4M时需要分割消息。
-
过滤消息
- 可以使用Tag方式进行过滤。
- 也可以添加用户属性,生产者通过MessageSelector.bySql()来过滤。
-
事务消息
- 发送消息,此时为半消息
- 服务端响应消息,写入结果
- 根据发送消息执行本地食物,此时半消息对业务不可见,本地逻辑不执行
- 根据本地是无状态执行commit或者rollback
- 事务消息有三种状态:提交,回滚和中间,中间状态要回查队列来确定状态
- 事务消息不支持延时消息和批量消息,单个消息检查次数限制为15次,如果超过该次数,消息会被丢弃
- 事务性消息可能被不止一次被检查或消费。
-
高级功能
- 消息存储
- 由于高可用的要求,数据需要做持久化存储
- 可以使用数据库存储,但如果DB出现故障,MQ消息无法落盘,就会导致线上故障。而且容易出现IO瓶颈。
- 主流方法使用文件系统保存,采用消息刷盘至文件系统做持久化。
- 消息存储时由于随机写和顺序写速度差异极大,所以RocketMQ写消息时使用顺序写,保证消息存储的速度。
- 消息发送时,避免用户态和内核态之间的数据复制,采用零拷贝(MappedByteBuffer)实现。RocketMQ单个commitlog大小因此限制为1G。 4.消息存储结构
- CommitLog:存储消息元数据
- ConsumerQueue:存储消息的CommitLog索引
- IndexFile:索引文件,提供通过key或时间区间来查询消息的方法。
- 刷盘机制
- 同步刷盘:返回写成功状态时,消息已经被写入磁盘,通知刷盘线程,等待刷盘完成,唤醒响应生产者的线程
- 异步刷盘:返回写成功状态时,消息可能只被写入了内存,当内存中的消息量积累到一定程度,统一出发磁盘动作。
- 高可用性
- NameServer:通过构建集群,保证高可用。NameServer是无状态的,不需要特别配置
- Broker:多主多从的集群,master的id=0,slaver的id>0,slaver只负责读,Producer只会和Master连接写入消息。Consumer默认从Master读,当Matser不可用或繁忙,会自动切换到Slaver读取消息。
- 消息发送时,topic会指向多个Broker组上,及时一个Broker挂了,其他的Broker也能继续处理。
- 主从复制:
- 同步复制:Master和Slaver都写成功后才会反馈客户端,保证数据安全,但系统吞吐量下降
- 异步复制:Master写成功,就会向客户端反馈,通过异步方式,同步slaver的数据。
- 通常采用异步刷盘和主从同步复制,异步刷盘保证吞吐量,同步复制保证数据不丢失。
- 负载均衡
- Producer负载均衡,默认会轮训所有的message queue,以达到消息平均落在不同的queue上,而queue会散落在不同的broker上,所以消息就会发送到不同的broker。
- Consumer负载均衡
- 集群模式:每个consumer实例平均分配每个consume queue。
- 广播模式:没有负载均衡,所有consumer都要消费消息。
- 消息重试
- 顺序消息消费失败后,RocketMQ会自动不断进行消息重试(间隔1秒),此时会出现消息被阻塞的情况。
- 无序消息的重试
- 普通消息,定时消息,延时消息,事务消息都是无序消息,当消费失败时,可以通过设置返回状态达到消息重试的结果。
- RocketMQ默认允许每条消息最多被重试16次,并且间隔会主键增大,Message ID不会改变。
- 消费后返回ReconsumeLater,null或者抛出异常,都能触发重试
- 死信队列
- 死信消息不会再被消费者消费,3天后悔自动删除
- 死信队列对应一个GroupID,而不是对应单个消费者实例。
- 可以通过控制台或者设置特别的消费者来重发和消费死信消息。
- 消费幂等
- 消费者在接收到相同的消息后,需要根据唯一key对消息做幂等处理。
- 发送时消息重复:比如网络闪断或客户端宕机,服务端对客户端应答失败。
- 投递时消息重复,消费者已经完成业务处理,当客户端给服务端反馈时网络闪断,为保证消息至少被消费一次,服务端将会在网络恢复后再次尝试投递之前被处理过的消息。
- 负载均衡时消息重复,会触发Rebalance,此时消费者可能会受到重复消息。
- 最好设置独立的业务标识,因为MessageId可能冲突(重复).
- 消息存储
- 传输层的TCP的三次握手和四次挥手
- 序列号seq:占4个字节,用于标记数据段的顺序
- 确认号ack:占4个字节,期待收到对方下一个报文段的第一个数据字节的序号,
- 确认ACK,占1为,仅当ACK=1时,确认号字段才有效。
- 同步SYN,连接建立时用于同步序号,当SYN=1,ACK=0时,表示这是一个连接请求的报文段,若同意链接,则在响应报文中使得SYN=1,ACK=1.因此SYN=1表示这是一个连接请求或连接接受报文,握手完成后被设置为0.
- 终止FIN,用于释放一个连接,FIN=1表示发送方的报文数据已经发送完毕,要求释放连接。
- 三次握手,连接确认
- 第一次握手:客户端发送SYN=1,初始序列号seq=x给服务器,并进入SYN_SENT状态,等待服务器确认。
- 第二次握手:服务器收到请求报文后,如果同意连接,发送报文SYN=1,ACK=1,服务器的初始序列号seq=y,确认号ack=x+1给客户端。服务器进入SYN_RECV状态。
- 第三次握手:客户端收到确认后,还要向服务器确认,发送ACK=1,序列号seq=x+1,确认号ack=y+1给服务器,都进入ESTABLISHED状态,建立连接,完成三次握手。
- 四次挥手
- 第一次挥手,客户端发出连接释放报文,FIN=1,seq=u,客户端进入FIN-WAIT-1状态。
- 第二次挥手,服务器收到连接释放报文,发送确认报文,ACK=1,ack=u+1,seq=v,服务端进入CLOSE-WAIT状态。
- 客户端收到服务器的确认请求后,客户端进入FIN-WAIY-2状态,等待服务器发送链接释放报文,在收到之前还需要接收服务器发送的最后的数据。
- 第三次挥手,服务器向客户端发送连接释放报文,FIN=1,ack=u+1,seq=w,服务器进入LAST-ACK状态,等待客户端确认。
- 第四次挥手,客户端收到连接释放报文后,发出确认,ACK=1,ack=w+1,seq=u+1,客户端进入TIME-WAIT状态,这时TCP连接还没释放,要经过2*MSL的时间,当客户端撤销相应的TCB,才进入CLOSED状态。
- 服务器收到客户端确认后,立即进入CLOSED。撤销TCB后,结束了此次的TCP连接。
- 为什么连接需要三次,而关闭需要四次
- 连接时,Server收到请求连接报文后,可以直接发送SYN+ACK报文,ACK用于应答,SYN用于同步。
- 关闭时,Server很可能不会立即关闭SOCKET,所以需要先回复一个ACK报文,告诉Client我收到了你的FIN报文,等到所有报文发送完,才能发送FIN报文。
- 为什么TIME_WAIT状态需要2MSL才能返回到CLOSE
- 我们需要假想网络是不可靠的,最后一个ACK可能丢失,TIME_WAIT状态就是用于重发可能丢失的ACK报文。
- Server如果没有收到ACK,将重复发送FIN片段,Client如果再次受到FIN,那么会重发ACK并重新计时。2MSL是一个发送一个回复所需要的最大时间,如果超过这个时间,就推断已经被接收,结束TCP。
- 为什么不能用两次握手
- 如果Server的应答在传输中被丢失了,那么会出现Server认为已经连接,但Client认为连接还未建立,会忽略Server的任何数据,只等待连接应答。
- 连接建立后,客户端出现问题会怎么办
- TCP设有保活计时器,如果客户端出现故障,服务器不会一直等下去。服务器每次收到客户端请求后都会重新复位计时器。
- 如果长时间,一般为2小时,服务器一直没有收到数据,服务器会发送一个探测报文,每隔75秒发送一次,如果10次还没有反应,服务器就认为客户端出故障了,关闭连接。
- TCP和UDP的区别
- UDP
- 面向无连接,不需要三次握手建立连接,想发数据直接就可以发送了,不会对数据报文进行任何的拆分和拼接。
- 有单播,多播,广播的功能,不止支持一对一的传输方式,同样支持多对多,多对一,一对多。
- 传输方式,UDP是面向报文的,对报文不合并不拆分。
- 不可靠性,通讯不需要连接,想发就发,收到什么数据就传输什么数据,不关心对方是否收到了正确的数据,这样的情况肯定不可靠。优点在于没有拥塞控制,一直会以恒定的速度发送数据,对实时性要求高的场景就需要使用UDP。
- 首部开销小,仅8字节,传输数据报文时是很高效的。
- TCP
- 面向连接,必须要用三次握手建立连接。
- 仅支持单播传输,也就是支持点对点的数据传输。
- 传输方式,TCP是面向字节流,TCP是在不保留报文边界的情况下以字节流方式进行传输。
- 可靠传输。对于接收结果都会回一个相应的确认,如果在合理的往返时延内未收到确认,那么对应的数据会被重传。
- 提供拥塞控制,网络情况拥塞时,TCP会减少向网络注入数据的速率和数量。
- TCP提供全双工通信。TCP允许通信双方的应用程序在任何时间都能发送数据,因为TCP的两端都有缓存,用来临时存放双向通信的数据。
- 首部最小20字节,最大60字节。
- UDP