面试个人总结
- 1、基础
-
- 1.1 JVM
- 1.2 GC
- 1.3 集合
- 1.4 线程
-
- 1.4.1 创建线程的方法 4 或 5种
- 1.4.2 线程的生命周期
- 1.4.3 线程的交互 (join yeild sleep wait notify\notifyall)
- 1.4.4 线程安全 synchronized 和 lock
- 1.4.5 死锁
- 1.4.6 线程池 7个参数的含义
- 1.4.7 阻塞队列
- 1.4.8 拒绝策略
- 1.4.9 锁的分类 公平 非公平 乐观 悲观 轻重
- 1.4.10 锁的优化 锁消除 锁粗化 自旋 CAS
- 1.4.11 锁的过程 锁的升级过程(膨胀过程)
- 1.4.12 ThreadLocal 实现过程
- 1.4.13 Volatile 变量内存的可见性
- 1.4.14 AQS AbstractQueuedSynchronizer 锁生效的核心抽象类
- 1.4.15 synchronized 实现原理(monitor)
- 1.4.16 分布式锁
- 1.5 JDK新特性
- 1.6 设计模式
- 1.7 杂七杂八
- 2、框架
- 3、微服务
- 4、数据库
- 5、项目研发
1、基础
1.1 JVM
1.1.1 JVM五大区域
- 方法区(Method Area):
也叫永久区,jdk8以后叫元数据区,是线程共享,里面存储着被虚拟机加载的类信息,常量,静态变量。程序执行时,将字节码文件加载到方法区的常量池,存储一些和类有关的属性,方法区的清理也是通过垃圾回收,回收目标主要是常量池中废弃的常量和不再使用的类型。
- 虚拟机栈(VM Stack):
是线程私有的,生命周期和线程相同,它描述的是Java方法执行的内存模型,每一个虚拟机栈都有自己的栈针,每一个方法的执行都是入栈和出栈的过程(想到关于栈和队列的区别,有一个形象的例子,队列是先进先出 吃了拉,栈是先进后出 吃了吐 ),每一个栈中都有着局部变量表和操作数栈,局部变量表是变量值的存储空间,用于存放方法参数和方法内部定义的局部变量,操作数栈存储的数据与局部变量表一致,是通过弹栈和压栈来进行访问的,操作数栈可以理解为Java虚拟机栈中的一个用于计算的临时数据存储区。(Java的基本数据类型大部分都是存储在栈的,除了一些引用变量和引用类型。String不是基本据类型!基本数据类型就只有八个,数值型:byte,short,int,long,浮点型:float,double,字符型:char,布尔型:boolean。
- 本地方法栈(Native Method Stack):
跟虚拟机栈非常相似,也是线程私有的,不过虚拟机栈是针对Java方法,而本地方法栈是针对native方法,也就是底层方法。由于Java是跨平台语言,导致的它不得不牺牲一些对底层方法的控制,而要实现这些底层方法的控制,就需要用到native方法,而本地方法栈就是针对native方法的。
- 堆(Heap):
堆区用于存放所有new出来的对象和数组,是线程共享的,堆区中的对象和栈区中的对象往往是成对出现的,一般的程序是通过栈区的对象引用来访问堆区的对象,因为是共享的,意味着对个对象引用可以指向同一个对象,在没有引用变量的指向时,就会变成垃圾,不被使用,被垃圾回收机制清理。
- 程序计数器(Program Counter Register):
每一个线程内部都有自己的程序计数器,是相互独立的,保证每个线程的程序运行不会出错,因此程序计数器是线程私有的,在idea将Java代码运行时都会被转译成字节码文件,字节码文件是二进制文件,识别起来比较困难,所以在编译时用到计数器,为编译好的字节码添加行号,后期调用的时候就能按顺序分条执行。
1.1.2 新生代和老年代 (1:2)
新生代:年轻代主要存放新创建的对象,内存大小相对会比较小,其中的区域分为Eden:tosurvior:fromsurvior = 8:1:1
老年代:在逻辑上是连续的,主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁
(这部分在GC回收机制讲)
1.1.3 JVM加载类的过程
1、类的加载:
获取此类的二进制字节流,并将这个字节流所代表的的静态存储结构转化成为方法区的运行时数据结构,最后在Java堆中生成一个此类的class对象作为方法区数据的访问入口,相对于其他阶段,类的加载阶段是开发期可控性最强的阶段,我们可以通过定制不同的类加载器,也就是classloader来控制二进制字节流的获取方式。
2、类的连接:类的连接又分为 验证,准备,解析。
验证:而验证就是连接阶段的第一步,验证被加载的类是否有正确的结构,类数据是否符合虚拟机的要求,确保不会危害虚拟机安全,主要验证过程包括:文件格式验证,元数据验证,字节码验证以及符号引用验证。
准备:为类的静态变量(static
filed)在方法区分配内存,并赋默认初值(0值或null值)。关于准备阶段为类变量设置零值的唯一例外就是当这个类变量同时也被final修饰,那么在编译时,就会直接为这个常量赋上目标值。
解析:将类的二进制数据中的符号引用换为直接引用。
3、类的初始化:
类的初始化主要工作是为静态变量赋程序设定的初值。Java虚拟机规范规定了有4种情况必须立即对类进行初始化(加载,验证,准备必须在此之前完成)
1)当使用new关键字实例化对象时,当读取或者设置一个类的静态字段(被final修饰的除外)时,以及当调用一个类的静态方法时(比如构造方法就是静态方法),如果类未初始化,则需先初始化。
2)通过反射机制对类进行调用时,如果类未初始化,则需先初始化。 3)当初始化一个类时,如果其父类未初始化,先初始化父类。
4)用户指定的执行主类(含main方法的那个类)在虚拟机启动时会先被初始化。
除了上面这4种方式,所有引用类的方式都不会触发初始化,称为被动引用。 如:通过子类引用父类的静态字段,不会导致子类
初始化;通过数组定义来引用类,不会触发此类的初始化;引用类的静态常量不会触发定义常量的类的初始化,因为常量在编译阶段已经被放到常量池中了。
1.1.4 OOM异常
OutOfMemoryError jvm内存不足时抛出此错误
jvm内存模型分为5大部分,堆,栈,方法区,本地方法栈,程序计数器。其中这五大部分中,只有堆和方法区会发生GC垃圾回收,由此可见OOM问题很有可能就会出现在堆和方法区。
1)OutOfMemoryError :java heap space 堆内存溢出
这个OOM是最常见的,堆内存溢出。jvm堆是java存放对象的位置,当堆空间已经被占满到无法在创建新的对象时,就会抛出java.lang.OutOfMemoryError:java
heap space的错误。 原因: 1、创建了一个超级大的对象。 2、快速的创建海量的对象,直到最后来不及GC。
3、内存泄漏,一些对象创建之后没有释放,比如一些文件对象。
4、过度使用Finalizer终结器。finalizer终结方法,是每个类都会有的特殊方法,当其被jvm调用时,就能帮助jvm清理资源。但是它有个问题就是:不保证及时性。如果你给类提供finalizer执行的时候,其开始到被jvm回收的这段时间是不可预见的,且jvm会延迟执行finalizer,从而导致类一直无法被回收。如果量一大就会有堆内存溢出的风险。使用finalizer会使创建并销毁对象的时间变成原来的400~500倍。
解决方案: 使用-Xmx参数调高堆内存空间。大力出奇迹。对某些场景做限流,降低存在短期内创建海量对象的可能。排查是否出现内存泄漏。
2)OutOfMemoryError: Metaspace 元空间内存溢出
hotspot虚拟机在java8后,将原本的方法区(永久代)彻底移除,取而代之的是元空间,常量池与静态变量转移到了堆中储存,元空间中只存了类的信息(class定义,名称,方法),字节码文件等信息。
原因: 1、元空间已经不占JVM虚拟机空间,而是使用本地内存。
2、当元空间中加载的class数目太多或者体量太大时,占满元空间的时候,就会出现OutOfMemoryError: Metaspace
元空间内存溢出的错误。 解决方案:
-XX:MaxMetaspaceSize 配置更大的元空间,一般用于启动时就报错的情况。重启JVM虚拟机,可能是一些应用没有重启导致了加载多份class的原因。设置
-XX:+CMSClassUnloadingEnabled 和 -XX:+UseConcMarkSweepGC这两个参数允许 JVM卸载 class。因为JVM默认是不会卸载class的,但是可能会有程序动态创建太多class的情况出现。
3)OutOfMemoryError: Permgen space 永久代内存溢出
与上述元空间内存溢出类似,可使用-XX:MaxPermSize改动永久代大小。
4)OutOfMemoryError: GC overhead limit exceeded
当内存基本被耗尽,JVM虚拟机反复进行GC垃圾回收却几乎无法回收垃圾的时候,就会抛出这个错误。通俗的说就是,GC垃圾收集器反复反复进行垃圾回收却没啥用的时候,它就放弃治疗了。
解决办法:
其实与堆内存溢出情况类似,就算不出这个异常,在创建几个对象也会出现堆内存溢出的情况。因此解决方案可以参考1)堆内存溢出的解决方法。
1.1.5 JVM常用调优参数
-Xms2g:初始化推大小为 2g;
-Xmx2g:堆最大内存为 2g;
-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
-XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
–XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
-XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
-XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
-XX:+PrintGC:开启打印 gc 信息;
-XX:+PrintGCDetails:打印 gc 详细信息。
1.2 GC
1.2.1 可达性分析
判断对象是否可以被回收之引用计数法:
Java中,引用和对象是有关联的。如果要操作对象必须用引用进行。
因此,很显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。简单说,就是给对象添加一个引用计数器,每当一个地方引用它,计数器值加1,每当一个引用失效时,计数器减1。任何时刻计数器值为零的对象就是不可再被使用的对象,即可回收对象。
但这种方法难以解决对象之间的循环引用问题,所以Java使用了可达性分析方法:
原理:首先从GC Roots (GC Roots就是对象,而且是JVM确定当前绝对不能被回收的对象)作为起点开始,向下搜索,如果一个对象没有任何引用链相连,则判断为对象不可用,作为可回收对象。也即给定一个集合的引用作为根出发,通过应用关系遍历对象图,能够被遍历到的(可到达的)对象就被判定为存活,如果没有被遍历到就判定为死亡。
Java中可以作为GC Roots 的对象:
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中 native方法中引用的变量
1.2.2 Java中的4大引用
Java对象的引用包括:
强引用,软引用,弱引用,虚引用
Java中提供这四种引用的目的:
第一是让程序员通过代码的方式决定某些对象的生命周期。
第二是有利于jvm进行垃圾回收。
- 强引用
创建一个对象并把这个对象赋值给一个引用变量。强引用有引用指向时永远不会被垃圾回收,JVM宁愿抛出outofmemory错误也不会回收这种对象。
- 软引用
如果一个对象具有软引用,内存空间足够的情况下,就不会回收它,但如果内存空间不足了,就会回收,软引用可用来实现内存敏感的高速缓存,比如网页缓存,图片缓存等。
- 弱引用
弱引用用来描述非必须对象的,当jvm进行垃圾回收时,无论内存是否充足,都会被回收。
- 虚引用
虚引用和前面的软引用、弱引用不同,它不影响对象的生命周期,在java中用
java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。
1.2.3 GC回收算法
STW
在垃圾回收时,都会产生应用程序的停顿,停顿产生时,整个应用程序会被卡死,没有任何响应。
java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。
1、复制清除
复制(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,降低了内存的利用率,持续复制长生存期的对象则导致效率降低,还有在分配对象较大时,该种算法也存在效率低下的问题。不适用存活对象较多的场合,比如老年代。
2、标记清除
标记-清除(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
它的主要缺点有两个:一个是效率问题,标记和清除过程的效率都不高;另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
3、标记整理
复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法(老年代一般是存活时间较长的大对象)。
标记-整理(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,这种算法克服了复制算法的低效问题,同时克服了标记清除算法的内存碎片化的问题;
4、分代收集算法
“分代收集”(Generational Collection)算法,是一种划分的策略,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。
1.2.4 GC回收器
收集算法是jvm内存回收过程中具体的、通用的方法,垃圾收集器是jvm内存回收过程中具体的执行者,即各种GC算法的具体实现。
1、串行垃圾回收器 Serial收集器
串行收集器是最古老,最稳定以及效率高的收集器,通过持有应用程序所有的线程进行工作,它为单线程环境设计,只能使用一个单独的线程进行垃圾回收,通过冻结所有应用程序进行工作,可能会产生较长的停顿,只使用一个线程去回收。
新生代、老年代使用串行回收;新生代复制算法、老年代标记-压缩;垃圾收集的过程中会Stop The World(服务暂停)
2、并行垃圾回收器 ParNew收集器
它是JVM的默认垃圾回收器。与串行垃圾回收器不同,它使用多线程进行垃圾回收。相似的是,当执行垃圾回收的时候它也会冻结所有的应用程序线程,其实就是Serial收集器的多线程版本
新生代并行,老年代串行;新生代复制算法、老年代标记-压缩
3、并发标记扫描垃圾回收器 CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。
从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于“标记-清除”算法实现的
优点:并发收集、低停顿
缺点:产生大量空间碎片、并发阶段会降低吞吐量
4、G1垃圾回收器 G1 收集器
G1是目前技术发展的最前沿成果之一,G1垃圾回收器适用于堆内存很大的情况,他将堆内存分割成不同的区域,并且并发的对其进行垃圾回收。G1也可以在回收内存之后对剩余的堆内存空间进行压缩。并发扫描标记垃圾回收器在STW情况下压缩内存。G1垃圾回收会优先选择第一块垃圾最多的区域
与CMS收集器相比G1收集器有以下特点:
1、空间整合,G1收集器采用标记整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。
2、可预测停顿,这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
1.2.5 FullGC 和 MajorGC,Minor GC
Minor GC 又称为新生代GC :
指的是发生在新生代的垃圾回收,因为Java对象大多都具有朝生夕灭的特性,因此Minor GC(采用复制算法)非常频繁,一般回收速度也比较快。
Minor GC触发条件:
Eden区满时,触发MinorGC,即申请一个对象时,发现Eden区不够用了,在Minor GC时会把存活的对象复制到tosurvior 区,如果tosurvior空间不够的话,就利用担保机制进入老年代
担保机制:(在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间。如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlerPromotionFailure设置是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小。如果大于,将尝试着进行一次Monitor GC)
Major GC 老年代GC
是指发生在老年代的垃圾回收,出现了Major GC 经常会伴随至少一次的Minor GC (并非绝对,在Parallel
Scavenge收集器中就有直接进行Full GC的策略选择过程) Major GC的速度一般会比Minor GC慢10倍以上。
Full GC 是清理整个堆空间—包括年轻代和老年代
Full GC 触发条件:
- 通过调用system.gc()方法会建议JVM进行FullGC,因为是建议并不一定会进行,但是大多数情况下还是会进行FullGC
- 老年代空间不足,JVM会进行Major GC,如果Major GC完后空间还是不足,就会抛出java.lang.OutOfMemoryError: Java heap space异常。
- 方法区空间不足(方法区又叫“永久代”),jvm会进行FullGC,如果Full GC完后空间还是不足,就会抛出java.lang.OutOfMemoryError: PermGen space异常。
- 担保机制将新生代无法存储的对象放入老年代,老年代空间也无法存储,担保失败,进行FullGC
- CMS垃圾收集器进行GC的时候会产生浮动垃圾,浮动垃圾就是进行GC过程中产生的垃圾,占用了空间,没有被GC,导致空间不足,JVM虚拟机进行Major GC。
- 如果分配的对象太大,老年代剩余的空间足够,但是连续空间不够,此时进行FullGC
1.2.6 防止FullGC
1、System.gc()方法的调用
调用此方法是建议JVM进行Full GC,只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full
GC的频率,也即增加了间歇性停顿的次数。可通过设置参数-XX:+ DisableExplicitGC来禁止RMI调用System.gc。
2、老年代空间不足
a:连续空间不足:
当有大对象进入老年代时,老年代的连续空间碎片放不下,此时会发生fullgc,fullgc后仍然放不下,就会抛出内存溢出错误
解决方法:1、尽量不要创建这种大对象。2、万一创建了就再新生代多保存一段时间(增大默认参数:15),尽量在新生代被回收掉。
b:总空间不足:
通常经过minor GC之后晋升到老年代的对象大于老年代剩余空间的容纳量就会进行Full GC,为了由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时,先做一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。
3、方法区空间不足
方法区中存放着一些class信息,常量,静态变量等数据,当系统中要加载的类,反射的类和要调用的方法教多时,方法区可能会被占满,就会执行FullGC,Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息:
java.lang.OutOfMemoryError: PermGen space
为避免方法区占满,可采用增大方法区空间或使用CMS GC
4、CMS GC日志出现promotion failed和concurrent mode failure
采用CMS进行老年代GC时,尤其注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。promotion failed是在进行Minor GC时,survivor space放不下、对象只能放入老年代,而此时老年代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足造成的(有时候“空间不足”是CMS GC时当前的浮动垃圾过多导致暂时性的空间不足触发FullGC)。
对措施为:增大survivor space和老年代空间,或调低触发并发GC的比率。
1.2.7 GC调优
1、将转移到老年代的对象数量降到最少
2、减少full gc的执行时间
1.3 集合
Set、List、Map的联系和区别
一、结构特点
1、list 和 set 是存储单列数据的集合,map是存储双列数据的集合;
2、list中存储的数据是有序的,并且值允许重复。map中存储的数据是无序的,他的键不允许重复,但值是允许重复的。set中存储的数据是无序 的,并且不容许重复,但元素在集合中的我在是由元素的hashcode决定的,即位置是固定的(set集合是根据hashcode来进行数据存储的,所以位置是固定的,但这个位置不是用户可以控制的,所以对于用户来说set中的元素还是无序的)。
二、实现类
1、list接口有三个实现类:
1.1 LinkList:基于链表的实现,链表内存是散列的,增删快,查找慢;
1.2 ArrayList:基于数组的实现,非线程安全,效率高,增删慢;
1.3 Vector :基于数组的实现,线程安全,增删慢,查找慢;
2、Map接口有四个实现类:
2.1HashMap:基于hash表的Map接口实现,非线程安全,高效,支持null键和null值;
2.2HashTable:线程安全,低效,不支持 null 值和 null 键;
2.3 LinkedHashMap:是 HashMap 的一个子类,保存了记录的插入顺序;
2.4 SortMap 接口:TreeMap,能够把它保存的记录根据键排序,默认是键值的升序排序;
3、Set接口有两个实现类:
3.1 HashSet:底层是由 Hash Map 实现,不允许集合中有重复的值,使用该方式时需要重写 equals()和 hash;
3.2 LinkedHashSet:继承于 HashSet,同时又基于 LinkedHashMap 来进行实现,底层使用的是 LinkedHashMap
三、区别
- List 集合中对象按照索引位置排序,可以有重复对象,允许按照对象在集合中的索引位置检索对象,例如通过list.get(i)方法来获取集合中的元素;
- Map 中的每一个元素包含一个键和一个值,成对出现,键对象不可以重复,值对象可以重复;
- Set 集合中的对象不按照特定的方式排序,并且没有重复对象,但它的实现类能对集合中的对象按照特定的方式排序,例如 Tree Set 类,可以按照默认顺序,也可以通过实现Java.util.Comparator< Type >接口来自定义排序方式。
四、补充
HashMap 是线程不安全的,HashMap 是一个接口,是 Map的一个子接口,是将键映射到值得对象,不允许键值重复,允许空键和空值;由于非线程安全, HashMap的效率要较 HashTable 的效率高一些.
HashTable 是线程安全的一个集合,不允许 null 值作为一个 key 值或者 Value 值;
HashTable 是 sychronize(同步化),多个线程访问时不需要自己为它的方法实现同步
HashMap在被多个线程访问的时候需要自己为它的方法实现同步;
1.3.1 ArrayList
ArrayList的默认容量是10,扩容因子为1,阙值是10,当达到阙值是,进行扩容1.5倍+1。
1.8之后增加了函数式编程
函数式编程的准则:不依赖于外部的数据,而且也不改变外部数据的值,而是返回一个新的值给你。
ArrayList实现了List接口,与他同级的还有LinkedList,ArrayList的底层是数组结构,故查询快,增删慢,因为查询的话直接用的下标直接索引,速度非常快,而增删的话每次都要移动非常多的元素,比较麻烦。一般增删的话都用LinkedList,他的增删是previous和next的断开和重组(previous是该节点的上一个节点,next是下一个节点)
ArrayList是非线程安全的,这也就导致了多线程操作可能会出现溢出的情况。
1.3.2 HashMap 1.7和1.8 扩容 冲突 长度>8 ->红黑树
相对于其他的对象存储器,hashmap可以理解为多了一层指向关系,用指定的key来寻找对应的value。
初始化hashmap,提供了有参和无参构造,无参构造中,容器默认数组大小initialCapacity为16,扩容因子loadFactor为0.75,阈值为12.
hashmap使用“懒扩容”,只会在调用put的时候才进行判断,然后进行扩容。需要注意的是,每次扩容之后,都需要重新计算Entry在数组中的位置
PUT方法:
第一步:通过 HashMap 自己提供的hash 算法算出当前 key 的hash 值
第二步:通过计算出的hash 值去调用 indexFor 方法计算当前对象应该存储在数组的几号位置
第三步:判断size 是否已经达到了当前阈值,如果没有,继续;如果已经达到阈值,则先进行数组扩容,将数组长度扩容为原来的2倍。
第四步:将当前对应的 hash,key,value封装成一个 Entry,去数组中查找当前位置有没有元素,如果没有,放在这个位置上;如果此位置上已经存在链表,那么遍历链表,如果链表上某个节点的 key 与当前key 进行 equals 比较后结果为 true,则把原来节点上的value 返回,将当前新的 value替换掉原来的value,如果遍历完链表,没有找到key 与当前 key equals为 true的,就把刚才封装的新的 Entry中next 指向当前链表的始节点,也就是说当前节点现在在链表的第一个位置,简单来说即,先来的往后退。
哈希冲突:
通过hash算法算出的当前key的位置已经有一个key了,实际中冲突是不可避免的,只能通过改进哈希函数的性能来减少冲突。哈希计算就是努力的把比较大的数据存放到相对较小的空间中。
解决哈希冲突的四种方法:
1、开放地址法
按顺序决定哈希值时,如果某数据已经存在,通过随机函数随机生成一个数,在原来哈希值的基础上加上随机数,直至不发生哈希冲突。
2、拉链法(默认)
对于相同的哈希值,使用链表进行连接。使用数组存储每一个链表。
3、建立公共溢出区
建立公共溢出区存储所有哈希冲突的数据。
4、再哈希法
对于冲突的哈希值再次进行哈希处理,直至没有哈希冲突。
1.3.3 ConCurrentHashMap 1.7和1.8 分布式锁
HashMap :先说HashMap,HashMap是线程不安全的,在并发环境下,可能会形成环状链表(扩容时可能造成,具体原因自行百度google或查看源码分析),导致get操作时,cpu空转,所以,在并发环境中使用HashMap是非常危险的。
HashTable : HashTable和HashMap的实现原理几乎一样,差别无非是1.HashTable不允许key和value为null;2.HashTable是线程安全的。但是HashTable线程安全的策略实现代价却太大了,简单粗暴,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差。
HashTable性能差主要是由于所有操作需要竞争同一把锁,而如果容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这样便可以有效地提高并发效率。这就是ConcurrentHashMap所采用的”分段锁“思想。
分段锁:
Java7中基本设计理念就是切分成多个Segment块,默认是16个,也就是说并发度是16,可以初始化时显式指定,后期不能修改,每个Segment里面可以近似看成一个HashMap,每个Segment块都有自己独立的ReentrantLock锁,所以并发操作时每个Segment互不影响;
Java8中锁方面:由分段锁(Segment继承自ReentrantLock)升级为CAS+synchronized实现;
数据结构层面:将Segment变为了Node,每个Node独立,原来默认的并发度16,变成了每个Node都独立,提高了并发度;
hash冲突:1.7中发生hash冲突采用链表存储,1.8中先使用链表存储,后面满足条件后会转换为红黑树来优化查询;
1.3.4 Hashtable 和 HashMap 的区别
1.两者的存储结构和对冲突的处理是一致的
2.hashtable在不指定容量的情况下,默认容量是11,而hashmap默认容量是16,hashtable不要求底层数组的容量为2的整数次幂,而hashmap要求为2的整数次幂(因为可以有效的降低下标index值的冲突,从而满足哈希算法均匀分布的原则)
3.hashtable在计算hash值的索引用的是%运算(求摸运算)而hashmap用的是&运算(按位与运算)
4.hashtable扩容时为2倍+1,而hashmap为2倍
5.hashtable的key和value都不允许为空,而hashmap的两者都可以为空,但key只能一个为空,value可以很多,但是如果在 Hashtable中有类似 put( null, null)的操作,编译同样可以通过,因为 key和 value都是Object类型,但运行时会抛出 NullPointerException异常。
1.3.5 HashSet 的实现过程 重写hashcoed和equals
HashSet:
HashSet中不允许有重复元素,这是因为HashSet是基于HashMap实现的,HashSet中的元素都存放在HashMap的key上面,而value中的值都是统一的一个private static final Object PRESENT = new Object();。HashSet跟HashMap一样,都是一个存放链表的数组。
HashSet是Set接口的典型实现,HashSet按照Hash算法来存储集合中的元素,存在以下特点: 不能保证元素的顺序,元素是无序的
HashSet不是同步的,需要外部保持线程之间的同步问题 集合元素值允许为null
HashSet的原理:
HashSet的add方法调用HashMap的put()方法实现
如果键已存在,那HashMap.put()返回旧值,添加失败
如果键不存在,那HashMap.put()返回null,添加成功
remove()
删除方法调用的是map.remove()方法,
remove()方法找到指定的key,返回key对应的value,而对于Hashset来说,他的value都是PRESENT.
特征:
Hashset底层是由Hashmap实现的,所以hashset也是无序的,在1.7之前hashmap是数组+链表结构,1.8之后是数组+链表+红黑树结构,hashset也是。
重写hashcode和equals是为了判断是否为同一个对象
首先先判断两个对象的hashcode是否相同,如果不同直接为两个不同的对象,如果相同,再去进行equals比较,如果equals也相同,才真正相同。
1.3.6 TreeSet实现原理 红黑二叉树 比较器接口(comparable 和 comparator)
TreeSet的默认顺序并不是按照装入集合的先后排序的。而是按照字符的顺序。
TreeSet 集合类是Set的子类,固有保持数据不重复的属性,除此之外还有一个独有的功能就是排序
TreeSet中底层的实现算法二叉树:
二叉树的实现就是说 第一个进来的数据 (对象)作为根 然后随后进来的对象依次与根进行比较 如果小于对象就放左边 如果大于对象就放右边,如果相等就不存储
1、实现Comparator的接口
重写compare方法 (由于每个类都是Object的子类 所以不需要重写equals方法)
2、 Comparbale接口:
实现这个接口就要重写 public int compareTo(Object o)方法 返回值是一个int值 对于这个方法有如下特性
前提保证返回值为固定值
1.当返回值为0 表示不存入集合
2.当返回值为正数 表示存入集合(功能为按照你存的顺序 正向存储)
3.当返回值为负数 表示存入集合(功能为按照你存的顺序 逆向存储)
下面谈两种比较方法的区别
一丶自然排序(Comparable)
1.TreeSet中的add方法会自动把传入的对象提升为Comparable类型(如果没有提升的话 就会报出类型转换异常)
2.调用传入对象中的comparaTo()方法和集合中已有的对象作比较
3.根据comparaTo()方法的返回值的结果进行存储二丶比较器Comparator< E >
1.创建TreeSet对象的时候可以传入一个比较器Comparator< E >
2.如果传入Comparator< E >d的子类对象,那么TreeSet就会按照比较器中的比较顺序排序
3.add()方法是自动调用Comparator< E >中compare方法
两种方式的区别
1.自然排序 TreeSet的无参构造
2.比较器排序 TreeSet的有参构造(参数为Comparator< E >)
1.4 线程
1.4.1 创建线程的方法 4 或 5种
1、继承Thread类创建线程
1、定义thread类的子类,并重写该类的run()方法,该方法的方法体就是线程需要完成的任务,run()方法也称为线程执行体。
2、创建了thread子类的实例,就是创建了线程对象
3、启动线程,就是调用线程的start()方法。
public class MyThread extends Thread{
//继承thread类
public void run(){
//重写run()方法
}
}
public class Main{
public static void main(String[] args){
new MyThread().start();//创建并启动线程
}
}
2、实现Runnable接口创建线程
1、定义Runnable接口的实现类,一样要重写run()方法,这个run()方法和Thread中的run()方法一样是线程的执行体。
2、创建Runnable实现类的实例,并用这个实例作为Thread的target来创建Thread对象,这个Thread对象才是真正的线程对象。
3、第三部依然是通过调用线程对象的start()方法来启动线程
public class MyThread2 implements Runnable{
//实现Runnable接口
public void run(){
//重写run方法
}
}
public class Main{
public static void main(String[] args){
//创建线程并启动
MyThread2 myThread2 = new MyThread2;
Thread thread = new Thread(myThread)
thread().start();
//或者 new Thread(new MyThread2()).start();
}
}
3、使用callable和Future创建线程
和Runnable接口不同,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大。call方法可以有返回值,call方法可以声明抛出异常。
1、创建Callable接口的实现类,并实现call方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。
2、使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call方法的返回值。
3、使用FutureTask对象来作为Thread对象的target创建并启动线程(因为FutureTesk实现了Runnable接口)
4、调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
public class Main{
public static void main(String[] args){
MyThread3 th = new MyThread3();
//使用Lambda 表达式 创建Callable对象
//使用FutureTesk类来包装Callable对象
FutureTask<Integer> furture = new FutureTask<Integer>(
(Callable<Integer>)()->{
return 5;
}
);
new Thread(future,"有返回值的线程").start();//实质上还是Callable对象来创建并启动线程
try{
System.out.println("子线程的返回值:" + future.get());//get方法会阻塞,直到线程执行结束才返回
}catch(Exception e){
e,printStackTrace();
}
}
}
4、使用线程池 如Executor框架
1.5 后引用的Executor框架的最大优点是把任务的提交和执行解耦,要执行任务的人只需要把Task描述清楚,然后提交即可,这个Task是怎么被执行的,被谁执行的,什么时候执行的,提交的人就不用关心了。具体点讲,提交一个Callable对象给ExecutorService(如最常用的线程池ThreadPoolExecutor),将得到一个Future对象的get方法,等待执行结果就好了。
//Executor执行Runnable任务
通过Executors的以上四个静态工厂方法获得 ExecutorService实例,而后调用该实例的execute(Runnable command)方法即可。一旦Runnable任务传递到execute()方法,该方法便会自动在一个线程上,execute会首先在线程池中选一个已有空闲线程来执行任务,如果线程池中没有空闲线程,它便会创建一个新的线程来执行任务。
//Executor执行Callable任务
在Java 5之后,任务分两类:一类是实现了Runnable接口的类,一类是实现了Callable接口的类。两者都可以被ExecutorService执行,但是Runnable任务没有返回值,而Callable任务有返回值。并且Callable的call()方法只能通过ExecutorService的submit(Callable task) 方法来执行,并且返回一个 Future,是表示任务等待完成的 Future。
1.4.2 线程的生命周期
1、新建状态(new)
当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时仅由JVM为其分配内存,并初始化其成员变量的值。
2、就绪状态(runnable)
当线程对象调用了start方法之后,该线程就处于就绪状态,Java虚拟机会为其创建方法调用栈和线程计数器,等待调度运行
3、运行状态(running)
如果处于就绪状态的线程获取到了CPU的使用权,开始执行run方法的线程执行体,则该线程处于运行状态。
4、阻塞状态(blocked)
阻塞状态是指线程因为某种原因放弃了对CPU的使用权,也即让出了CPU
timeslice,暂时停止运行。直到线程进入可运行状态才有机会再次获取CPU timeslice,转到运行状态。阻塞的情况分三种:
(1)等待阻塞 (o.wait -> 等待队列) 运行状态的线程执行o.wait方法,jvm会把该线程放入等待队列(waitting queue)中。
(2)同步阻塞 (lock -> 锁池)
运行状态下的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则jvm会把该线程放入锁池(lock pool)中。
(3)其他阻塞
(sleep、join) 运行状态下,线程执行Thread.sleep / t.join()方法时,或者发出了I/O请求时,JVM会把该线程设置为阻塞状态。当sleep状态超时、join()等待线程终止或者超时,或者I/O处理完毕时,线程重新转入就绪状态。
5、线程死亡(dead)
正常结束:run()或call()方法执行完成,线程正常结束。 异常结束:线程抛出一个未捕获的Exception 或 Error。
调用stop:直接调用该线程的stop()方法来结束该线程。(该方法容易导致死锁)
1.4.3 线程的交互 (join yeild sleep wait notify\notifyall)
wait()
wait()方法是object的方法,是在资源角度阻塞线程,也就是让线程放弃对资源的占用
sleep()
sleep()方法是线程的方法,是在线程的角度阻塞线程,无法操作对象锁,所以线程阻塞时锁住了某个对象,那么这个对象的锁将不会被释放。
notify()
notify() 和
nitifyAll()方法是唤醒被wait()方法置于waiting状态的线程。notify()方法是唤醒一个等待资源的线程,如果有多个,则随机唤醒一个,进入就绪状态,重新竞争锁。
notifyAll()
notifyAll()唤醒所有等待资源的线程。
yield()
yield()方法把CPU让给同等优先级或者更高优先级的线程。(注意:其他线程不会立马进入运行状态,只是给其他线程提供竞争的机会)。
join()
join()方法会使当前线程等待调用join()方法的线程结束后才能继续执行
1.4.4 线程安全 synchronized 和 lock
三个概念:
1、原子性:一个操作不可中断,要不全部执行成功要不全部执行失败
2、可见性:一个线程对某个共享数据进行修改后,其他线程能够立即得知这个修改
3、有序性:程序执行的顺序按照代码的先后顺序进行执行,执行程序为了提高性能,编译器和处理器常常会对指令进行重排序
作用: (1)确保线程互斥的访问同步代码 (2)保证共享变量的修改能够及时可见 (3)有效解决重排序问题
类别 | synchronized | lock |
---|---|---|
存在层次 | Java关键字 | 是一个接口 |
锁的释放 | 1、获取锁的线程执行完同步代码后自动释放 2、线程发生异常,jvm会让线程释放锁 | 手动释放,必须在finally中释放锁,不然容易造成死锁,lock.unlock |
锁的获取 | 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程就会一直等待 | lock有多种获取锁的方式,如lock,trylock |
锁的状态 | 无法判断,只能阻塞 | 可以判断 |
锁的类型 | 可重入,非公平,不可中断 | 可重入,可公平,可中断 |
锁的功能 | 功能单一 | API丰富,trylock;tryLock(long time, TimeUnit unit);可避免死锁 |
锁的修饰 | 类,方法,代码块 | 块范围 |
1.4.5 死锁
产生死锁id必要条件:
互斥条件:进程要求对所分配的资源进行排他性控制,即在一段时间内某资源仅为一进程所占用
请求和保存条件:当进程因请求资源而阻塞是,对咦获得的资源保存不放
不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放
环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。
如何避免死锁:
1、以确定的顺序获得锁
如果必须获取多个锁,那么在设计的时候需要充分考虑不同线程之前获得锁的顺序。
2、超时放弃
当使用synchronized关键词提供的内置锁时,只要线程没有获得锁,那么就会永远等待下去,然而Lock接口提供了boolean tryLock(long time, TimeUnit unit) throws InterruptedException方法,该方法可以按照固定时长等待锁,因此线程可以在获取锁超时以后,主动释放之前已经获得的所有的锁。通过这种方式,也可以很有效地避免死锁。
1.4.6 线程池 7个参数的含义
线程池的顶级接口:Executor
(1)newFixedThreadPool(int nThreads)
创建一个固定长度的线程池,每次提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模不再变化当线程发生未预期的错误而结束时,线程池会补充一个新的线程。
(2)newCachedThreadPool()
创建一个可缓冲的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,当需求增加,自动添加新线程,规模没有限制
(3)newSingleThreadExecutor()
这是一个单线程的executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它;它的特点是能确保依照任务在队列中的顺序来串行执行。
(4)newScheduledThreadPool(int corePoolSize)
创建一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于timer。
使用线程池的好处:
- 减少对象的创建,消亡的开销,提高性能
- 有效控制最大并发线程数,提高资源的使用率,同时避免过多的资源竞争,避免堵塞
- 提供定时执行,定期执行,单线程,并发数控制等。
线程池的7个参数:
1、线程池核心线程大小 corePoolSize
线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会
被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。
2、线程池最大线程数量 maximumPoolSize
一个任务被提交到线程池以后,首先会找有没有空闲存活线程,如果有则直接执行,如果没有则会缓存到工作队列(后面会介绍)中,如果工作队列满了,才会创建一个新线程,然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。
3、线程池空闲线程存活时间 keepAliveTime
一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定
4、空现线程存活时间单位 unit
keepAliveTime的计量单位
5、工作队列 workQueue
新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列:
6、拒绝策略 handler
当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的,jdk中提供了4中拒绝策略:
①CallerRunsPolicy该策略下,在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务。
②AbortPolicy
该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。
③DiscardPolicy
该策略下,直接丢弃任务,什么都不做。
④DiscardOldestPolicy
该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列
7、线程工厂 threadFactory
创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等
1.4.7 阻塞队列
阻塞队列(BlockingQueue) 是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
JDK7提供了7个阻塞队列。分别是
ArrayBlockingQueue : 一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue :
一个由链表结构组成的有界阻塞队列。 PriorityBlockingQueue : 一个支持优先级排序的无界阻塞队列。
DelayQueue: 一个使用优先级队列实现的无界阻塞队列。
SynchronousQueue: 一个不存储元素的阻塞队列。
LinkedTransferQueue: 一个由链表结构组成的无界阻塞队列。
LinkedBlockingDeque:
一个由链表结构组成的双向阻塞队列。
1.4.8 拒绝策略
1、CallerRunsPolicy:
线程调用运行该任务的 execute 本身。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。 这个策略显然不想放弃执行任务。但是由于池中已经没有任何资源了,那么就直接使用调用该execute的线程本身来执行。(开始我总不想丢弃任务的执行,但是对某些应用场景来讲,
很有可能造成当前线程也被阻塞。如果所有线程都是不能执行的,很可能导致程序没法继续跑了。需要视业务情景而定吧。)
2、AbortPolicy:
处理程序遭到拒绝将抛出运行时 ,这种策略直接抛出异常,丢弃任务。(jdk默认策略,队列满并线程满时直接拒绝添加新任务,并抛出异常,所以说有时候放弃也是一种勇气,为了保证后续任务的正常进行,丢弃一些也是可以接收的,记得做好记录)
3、DiscardPolicy:
不能执行的任务将被删除 这种策略和AbortPolicy几乎一样,也是丢弃任务,只不过他不抛出异常。
4、DiscardOldestPolicy:
如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程)
1.4.9 锁的分类 公平 非公平 乐观 悲观 轻重
公平锁(ReentrantLock和ReentrantReadWriteLock):
按等待获取锁的线程的等待时间进行回去,等待时间长的具有优先获取锁的权利。
优点:所有线程都能得到资源,不会饿死在队列中。
缺点:吞吐量会下降,队列里除了第一个线程其他线程都会阻塞,CPU唤醒阻塞线程的开销会很大。
非公平锁:
多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会变高,CPU也不必唤醒所有线程,减少唤醒线程数量。
缺点:可能会导致队列中的线程一直没有获取到锁,或者长时间获取不到,导致饿死。
可重入锁:
可重入锁又叫递归锁,是指同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。列如:
对于Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁,其名字是ReentrantLock重新进入锁。
对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。
synchronized void setA() throws Exception{
Thread.sleep(1000);
setB();
}
synchronized void setB() throws Exception{
setB();
}
上面的代码就是一个可重入锁的特点,如果不是可重入锁的话,setB可能不会被当前线程执行,造成死锁。
独享锁:独享锁是指该锁一次只能被一个线程所持有。
共享锁:是指该锁可被多个线程所持有。对于reentrantlock 而言,它是独享锁。但是对于lock另一个实现类readwritelock,它是共享锁,其写锁是独享锁。
对于synchronized而言,当然是独享锁。互斥锁在Java中的具体实现就是ReentrantLock 读写锁在Java中的具体实现就是ReadWriteLock
乐观锁和悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,它也会认为修改了。因此对于同一个数据的并发操作,悲观锁采取加锁的形式,悲观的认为,不加锁一定会出现问题。
乐观锁认为对于同一个数据的并发操作,是不会发生修改的,在更新数据的时候,会采用尝试更新。乐观锁认为,不加锁的并发操作是没有事情的。
悲观锁在Java中的使用,就是利用各种锁。
乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。
分段锁:
分段锁其实是一种锁的设计,并不是具体的一种锁,对于concurrenthashmap来说,其实并发的实现就是通过分段锁的形式来实现高效的并发操作。
ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap的结构,即内部拥有一个Entry数组,数组中的每一个元素又是一个链表,同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
1.4.10 锁的优化 锁消除 锁粗化 自旋 CAS
1、自旋锁
互斥同步进入阻塞状态的开销都很大,应该尽量避免。大多数情况下,共享数据的锁定状态持续时间很短。自旋锁的思想是让一个线程在请求一个共享数据的锁时执行自循环(自旋)一段时间,不让出CPU,如果在这段时间内能获取到锁,就可以避免进入阻塞状态。
但是,很容易想象到这种情况,如果锁会被线程占用很长时间,那么进行忙循环操作占用 CPU
时间就会造成很大的性能开销,所以自旋锁只适用于共享数据的锁定状态很短的场景。
2、锁消除
锁消除是一种更为彻底的优化,在 JIT 编译时,对运行上下文进行扫描,去除不可能存在共享资源竞争的锁。
3锁粗化
原则上,我们都知道在加同步锁的时候,尽可能的将同步块的作用范围限制在尽量小的范围 锁粗化的思想就是扩大加锁范围,避免反复的加锁和解锁。
CAS:
即compare and swap 或者 compare and set,,中文叫做“比较并交换”,它是一种思想、一种算法。
在多线程的情况下,各个代码的执行顺序是不能确定的,所以为了保证并发安全,我们可以使用互斥锁。而 CAS 的特点是避免使用互斥锁,当多个线程同时使用 CAS 更新同一个变量时,只有其中一个线程能够操作成功,而其他线程都会更新失败。不过和同步互斥锁不同的是,更新失败的线程并不会被阻塞,而是被告知这次由于竞争而导致的操作失败,但还可以再次尝试。
CAS是由CPU硬件实现,所以执行相当快, 涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作,即不断的重试。
1.4.11 锁的过程 锁的升级过程(膨胀过程)
偏向锁/轻量级锁/重量级锁
这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
偏向所锁,轻量级锁都是乐观锁,重量级锁是悲观锁。
1.4.12 ThreadLocal 实现过程
ThreadLocal是一个线程内部的存储类,可以指定线程内存储数据,并且该数据只有指定线程能够获取到。
其大致意思就是,threadlocal提供了线程内部的存储变量的能力,这些变量不同之处在于每一个线程能够读取到的变量是对应相互独立的,通过set和get方法就可以得到当前线程对应的值。
每一个线程都有持有一个threadlocalmap对象,如果使用的threadlocals对象已经存在map中,则直接使用。如果没有存在,就新创建一个threadlocalmap并赋值给threadlocals变量。
ThreadLocal 和 synchronized都是为了解决线程中共享变量访问冲突问题。
不同的是,synchronized是通过线程等待,牺牲时间来解决访问冲突。ThreadLocal则是通过每个线程单独一份存储空间,牺牲空间来解决访问冲突,并且相比于synchronized,threadlocal具有线程隔离的效果,只有在线层内才能获取到对应的值,线程外则不能访问到想要的值。
ThreadLocal在没有外部强引用时,发生 GC
时会被回收,但Entry对象和value并没有被回收,因此如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,从而发生内存泄露。既然已经发现有内存泄露的隐患,自然有应对的策略。在调用ThreadLocal的get方法时会自动清除ThreadLocalMap中key为null的Entry对象
1.4.13 Volatile 变量内存的可见性
锁提供了两种特性:互斥和可见
互斥即一个共享资源只能有一个线程在使用,
可见即一个线程对共享资源进行修改,对于其他线程是可见的。
在Java中,为了保证多线程读写数据时保证数据的一致性,可以采用两种方式:
同步:如用synchronized关键字,或者使用锁对象
使用volatile关键字:用一句话概括volatile,它能够使变量在值发生改变时能尽快地让其他线程知道。
volatile:
首先,我们要先意思到有这样的现象,编译器为了加快程序运行的速度,对一些变量的写操作会先在寄存器会CPU缓存上进行,最后才写入内存。而在这个过程中,变量在寄存器上的新值对其他线程是不可见的。所以有了volatile。
当对volatile标记的变量进行修改时,会将在其他寄存器或缓存中的修改之前的变量清除,然后重新读取。
volatile 和 synchronized
1、 volatile本质上是在告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取,而synchronized则是锁定当前变量,只有在当前线程可以访问该变量,其他线程被阻塞。
2、volatile是变量级别,而synchronized可以使用在变量和方法。
3、volatile仅能实现可见性,而synchronized可以保证变量的可见性和原子性等。
4、volatile不会造成线层阻塞,而synchronized可能会造成线程阻塞。
5、当一个域的值依赖于它之前的值时,volatile就无法工作了,如n=n+1,n++等。如果某个域的值受到其他域的值的限制,那么volatile也无法工作,如Range类的lower和upper边界,必须遵循lower<=upper的限制。
什么是内存可见性?:
我和你在一家公司上班,是领座的同事,到了晚上了需要开灯才能继续进行工作,这个时候我将灯打开了,按照通常的情况下,你也能马上发现灯亮了。也就是说当某个线程正在使用对象状态,而另一个线程在同时修改该状态,需要确保当一个线程修改了对象状态后,其他线程能够 看到发生的状态变化。
什么是可见性错误?:
接着拿我开灯的事情来说,就是当我把灯打开了,你的世界里还是黑的,你并不知道开灯了,这是不符合客观规律的。所以可见性错误是指当读操作与写操作在不同的线程中执行时,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。
解决可见性错误的方法:
1、通过同步锁来保证大家都能看到灯目前最新的状况 2、使用轻量级关键字:volatile。
1.4.14 AQS AbstractQueuedSynchronizer 锁生效的核心抽象类
AQS原理
AQS:AbstractQuenedSynchronizer抽象的队列式同步器。是除了java自带的synchronized关键字之外的锁机制。
AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒 时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。 CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队 列实例,仅存在节点之间的关联关系。
AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。 用大白话来说,AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态 符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。
注意:AQS是自旋锁:
在等待唤醒的时候,经常会使用自旋(while(!cas()))的方式,不停地尝试获取锁,直到被其他线程获取成功
AQS 定义了两种资源共享方式:
1.Exclusive:独占,只有一个线程能执行,如ReentrantLock
2.Share:共享,多个线程可以同时执行,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier
.
不同的自定义的同步器争用共享资源的方式也不同。
1.4.15 synchronized 实现原理(monitor)
在多线程访问共享资源的时候,经常会带来可见性和原子性的安全问题,为了解决这类线程安全的问题,Java提供了同步机制、互斥锁机制,这个机制保证了在同一时刻只有一个线程能访问共享资源。这个机制的保障来源于监视锁Monitor,每个对象都拥有自己的监视锁Monitor。
先来举个例子,我们可以把监视锁理解为包含A房间的建筑物,这个A房间同一时间只能有一个客人(线程),这个房间放了许多的东西(数据和代码)。如果一个客人想要进入A房间,他首先需要在走廊(entry set)排队等候(自旋)。调度器将基于某个标准(比如FIFO)来选择排队的客人进入房间。如果因为某些原因,这个客人因为其他事情无法脱身(线程被挂起),那么它将被送到另一个房间,B房间(wait set),专门用来等待的房间,B房间可以在稍后再次进入A房间。
如上面所说,这个建筑屋中一共有三个场所,监视器是一个用来监视这些线程进入特殊的房间的,他的义务是保证(同一时间)只有一个线程可以访问被保护的数据和代码。
Monitor其实是一种同步工具,也可以说是一种同步机制,它通常被描述为一个对象,主要特点是:
对象的所有方法都被“互斥”的执行。好比一个Monitor只有一个运行“许可”,任一个线程进入任何一个方法都需要获得这个“许可”,离开时把许可归还。
通常提供singal机制:允许正持有“许可”的线程暂时放弃“许可”,等待某个谓词成真(条件变量),而条件成立后,当前进程可以“通知”正在等待这个条件变量的线程,让他可以重新去获得运行许可。
1.4.16 分布式锁
分布式锁
1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行。 2、高可用的获取锁与释放锁。 3、高性能的获取锁和释放锁。
4、具备可重入性。 5、具备锁失效机制,防止死锁。 6、具备非阻塞锁特性,即没有获取到锁将直接返回
目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直都是一个比较重要的话题。在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。有的时候,我们需要保证一个方法在同一时间内只能被同一个线程执行。
1、基于数据库实现的分布式锁:
基于数据库实现的核心思想是:在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,插入成功则获取锁,执行完成后删除对应的行数据,释放锁。
2、基于Redis的实现方式:
Redission - 高性能分布式锁
lock实现加锁,unlock实现释放锁
使用命令介绍:
SETNX key val:当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么也不做,返回0.
expire key timeout:为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。
delete key:删除key
过程:
1)获取锁的时候,使用setnx来加锁
2)并使用expire命令来给锁添加一个超时时间,超过这个时间则自动释放锁,锁的val’值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
3)释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。
3、基于ZooKeeper的实现方式
。。。。。。。。略
1.5 JDK新特性
1.5.1 JDK8的新特性
hashmap 红黑树:
原来的hashMap采用的数据结构是哈希表(数组+链表),hashMap默认大小是16,一个0-15索引的数组,如何往里面存储元素,首先调用元素的hashcode方法,计算出哈希码值,经过哈希算法算成数组的索引值,如果对应的索引处没有元素,直接存放,如果有对象在,那么比较它们的equals方法比较内容。
如果内容一样,后一个value会将前一个value的值覆盖,如果不一样
在1.7的时候,后加的放在前面,形成一个链表,形成了碰撞,在某些情况下如果链表 无限下去,那么效率极低,碰撞是避免不了的,(加载因子:0.75,数组扩容)达到总容量的75%,就进行扩容,但是无法避免碰撞的情况发生。
在1.8之后,在数组+链表+红黑树来实现hashmap,当碰撞的元素个数大于8时 & 总容量大于64,会有红黑树的引入除了添加之后,效率都比链表高,1.8之后链表新进元素加到末尾
ConcurrentHashMap (锁分段机制):
concurrentLevel,jdk1.8采用CAS算法(无锁算法,不再使用锁分段),数组+链表中也引入了红黑树的使用
lambda表达式 :
为引入Lambda表达式,Java8新增了java.util.funcion包,里面包含常用的函数接口,这是Lambda表达式的基础,Java集合框架也新增部分接口,以便与Lambda表达式对接。 lambda表达式本质上是一段匿名内部类,也可以是一段可以传递的代码
增强for循环等
1.6 设计模式
1.工厂模式:$(‘XX’),创建商品【扩展:商品折扣】
2.单例模式:购物车
3.装饰器模式:打点统计
4.观察者模式:网页事件【btn多个cck事件】, promise [then】
5.状态模式:添加到购物车&从购物车删除
6.模板方法模式:渲染有统一的方法,内部包含了各模块渲染【算法骨架:先init内容、再init按钮、再init渲染】
7.代理模式:打折商品的处理
spring中的BeanFactory就是简单工厂模式的体现,根据传入一个唯一的标识来获得bean对象,但是否是在传入参数后创建还是传入参数前创建这个要根据具体情况来定
Spring实现这一AOP功能的原理就使用代理模式(1、JDK动态代理。2、CGLib字节码生成技术代理。)对类进行方法级别的切面增强
Builder模式 :SqlSessionFactoryBuilder
在Mybatis中有两个地方用到单例模式,ErrorContext和LogFactory,其中ErrorContext是用在每个线程范围内的单例,用于记录该线程的执行环境错误信息,而LogFactory则是提供给整个Mybatis使用的日志工厂,用于获得针对项目配置好的日志对象。
代理模式可以认为是Mybatis的核心使用的模式,正是由于这个模式,我们只需要编写Mapper.java接口,不需要实现,由Mybatis后台帮我们完成具体SQL的执行。
1.7 杂七杂八
1.7.1 Java中创建(实例化)对象的五种方式
1、用new语句创建对象,这是最常见的创建对象的方法。
2、通过工厂方法返回对象,如:String str = String.valueOf(23);
3、运用反射手段,调用java.lang.Class或java.lang.reflect.Constructor类的newInstance()实例方法。如:Object obj =Class.forName("java.lang.Object").newInstance();
4、调用对象的clone()方法。
5、通过I/O流(包括反序列化),如运用反序列化手段,调用java.io.ObjectInputStream对象的readObject()方法。
1.7.2 ==和equals
其实他们俩最大的区别就是使用目的不同,
==主要用于基础类型的比较,只要基础类型的值相等那么就是true,在比较引用的时候 ==是 比较引用的地址。
equals主要用在引用对象的比较上,不重写的情况下和 ==一样,重写了就看自己的需要了。
== 如果是基本数据类型的比较,比的是他们的值是否相等 如果是引用型数据类型比较,比较的是所指对象的地址是否相同
equals (不能作为基本类型数据的比较,继承的是object类,比较的是是否是同一个对象)如果没有对equals进行重写,比较的是所指对象的地址是否相同,和==一样 如果对equals进行重写,比较的是所指向的对象的内容
1.7.3 反射
Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法。对于任意一个对象,都能够调用它的任意一个方法和属性。这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制,
1、调用运行时类本身的.class属性
Class class = String.class;
2、通过运行时类的对象获取
Person p = new Person(); Class clazz = p.getClass();
3、通过Class的静态方法获取:体现反射的动态性
String className = “java.util.commons”;
Class clazz = Class.forName(className);
1.7.4 注解
元注解的作用就是负责注解其他注解
1、@Target
作用:用于描述注解的修饰对象(注解、方法、类、接口等)
2、@Documented
作用:用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。Documented是一个标记注解,没有成员。
3、@Retention:
作用:表示需要在什么级别保存该注释信息,描述注解的生命周期(运行,编译,源文件)
4、@Inherited:
作用:阐述了某个被标记的注解的类型是被继承的
自定义注解:
使用@interface自定义注解时,自动继承了java.lang.annotation.Annotation接口,由编译程序自动完成其他细节。在定义注解时,不能继承其他的注解或接口。@interface用来声明一个注解,其中的每一个方法实际上是声明了一个配置参数。方法的名称就是参数的名称,返回值类型就是参数的类型(返回值类型只能是基本类型、Class、String、enum)。可以通过default来声明参数的 默认值。
1.7.5 BIO NIO AIO
同步,异步,阻塞,非阻塞:
同步 :
自己亲自出马持银行卡到银行取钱(使用同步IO时,Java自己处理IO读写);
异步 :
委托一小弟拿银行卡到银行取钱,然后给你(使用异步IO时,Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS(银行卡和密码),OS需要支持异步IO操作API);
阻塞 :
ATM排队取款,你只能等待(使用阻塞IO时,Java调用会一直阻塞到读写完成才返回);
非阻塞 :
柜台取款,取个号,然后坐在椅子上做其它事,等号广播会通知你办理,没到号你就不能去,你可以不断问大堂经理排到了没有,大堂经理如果说还没到你就不能去(使用非阻塞IO时,如果不能读写Java调用会马上返回,当IO事件分发器会通知可读写时再继续进行读写,不断循环直到读写完
成)
BIO是一个连接一个线程。
NIO是一个请求一个线程。
AIO是一个有效请求一个线程。
Java BIO :
同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
Java NIO :
同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
NIO 通过Channel(通道) 进行读写。
Java AIO(NIO.2) :
异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都 是由OS先完成了再通知服务器应用去启动线程进行处理,
异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
BIO、NIO、AIO适用场景分析:
BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。
1.7.6 TCP 和 UDP
TCP是面向连接的,保证可靠的数据传输,每次建立连接都需要经历三次握手,数据传输完成都需要经历4次挥手断开连接,由于TCP是面向连接的所以只能用于端到端的通信。
UDP是面向无连接的通讯协议,每次发送数据不需要建立连接,因此可以用于广播发送并不局限于端到端。
1.7.7 TCP的三次握手四次挥手
1.进行三次握手,首先向服务器发送一个syn报文,其中syn=1,seq number=1022(随机);
2.服务器接收到syn报文,根据syn=1判断客户端请求建立连接,并返回一个syn报文,为第一次握手, 其中ack number=1023(客户端seq number+1),seq number=2032(随机),syn=1,ack=1;
3.客户端根据服务器的syn报文,确认其ack number是否与上一次发送的seq number+1相等,且ack=1,确认正确,则回应一个ack报文,为第二次握手, 即ack number=2033(服务器seq
number+1),ack=1;
4.服务器根据接收到的ack报文,确认ack number是否与上一次发送的seq number+1相等,并且ack=1,确认正确,则建立连接, 进入Established状态,为第三次握手。
①TCP是一种精致的,可靠的字节流协议。
②在TCP编程中,三路握手一般由客户端(Client)调用Connent函数发起。
③TCP3次握手后数据收发通道即打开(即建立了连接)。
所谓四次挥手(Four-Way Wavehand)即终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。在socket编程中,这一过程由客户端或服务端任一方执行close来触发。
1.7.8 TCP的粘包和拆包
对于什么是粘包、拆包问题,我想先举两个简单的应用场景:
客户端和服务器建立一个连接,客户端发送一条消息,客户端关闭与服务端的连接。
客户端和服务器建立一个连接,客户端连续发送两条消息,客户端关闭与服务端的连接。
第一种情况:
服务端一共读到两个数据包,第一个包包含客户端发出的第一条消息的完整信息,第二个包包含客户端发出的第二条消息,那这种情况比较好处理,服务器只需要简单的从网络缓冲区去读就好了,第一次读到第一条消息的完整信息,消费完再从网络缓冲区将第二条完整消息读出来消费。
第二种情况:
服务端一共就读到一个数据包,这个数据包包含客户端发出的两条消息的完整信息,这个时候基于之前逻辑实现的服务端就蒙了,因为服务端不知道第一条消息从哪儿结束和第二条消息从哪儿开始,这种情况其实是发生了TCP粘包。
第三种情况:
服务端一共收到了两个数据包,第一个数据包只包含了第一条消息的一部分,第一条消息的后半部分和第二条消息都在第二个数据包中,或者是第一个数据包包含了第一条消息的完整信息和第二条消息的一部分信息,第二个数据包包含了第二条消息的剩下部分,这种情况其实是发送了TCP拆,因为发生了一条消息被拆分在两个包里面发送了,同样上面的服务器逻辑对于这种情况是不好处理的。
tcp为提高性能,发送端会将需要发送的数据发送到缓冲区,等待缓冲区满了之后,再将缓冲中的数据发送到接收方。同理,接收方也有缓冲区这样的机制,来接收数据。
发生TCP粘包、拆包主要是由于下面一些原因:
应用程序写入的数据大于套接字缓冲区大小,这将会发生拆包。
应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包。
进行mss(最大报文长度)大小的TCP分段,当TCP报文长度-TCP头部长度>mss的时候将发生拆包。接收方法不及时读取套接字缓冲区数据,这将发生粘包。
如何解决拆包粘包:
既然知道了tcp是无界的数据流,且协议本身无法避免粘包,拆包的发生,那我们只能在应用层数据协议上,加以控制
使用带消息头的协议、消息头存储消息开始标识及消息长度信息,服务端获取消息头的时候解析出消息长度,然后向后读取该长度的内容。
设置定长消息,服务端每次读取既定长度的内容作为一条完整消息。 设置消息边界,服务端从网络流中按消息编辑分离出消息内容。
1.7.9 00P的理解(特征)
继承
子类继承父类的属性和方法,并可以添加新的属性和方法对部分属性和方法进行重写,继承增加了代码的可重用性,而且只支持单继承
封装
将一个类的使用和实现分开,只保留接口与外部联系,或者说只公开了一部分开发人员使用的方法,于是开发人员只需要管子这个类如何使用,而不用关心它是如何实现的,实现了代码模块的松耦合。
多态
子类继承了父类中的属性和方法,并对其中部分进行重写。于是多个子类中虽然具有同一个方法,但是这些子类调用实例化方法后可以获得完全不同的结果,这就 是多态,增加了软件的灵活性。
1.7.10 七层协议
2、框架
2.1 Spring
2.1.1 创建对象的方式
这里采用xml配置,分别演示三种创建Bean的方式
先创建一个Bean User 类,三种方式都是为了得到这个User对象
public class User{}
1、采用默认的无参构造创建实例Bean
XML:
<!-- 默认的无参构造 -->
<bean id="user" class = "pojo.User"></bean>
测试:
@Test
public void testUser(){
}
2、采用普通工厂创建实例Bean
工厂类:
/**
* @author lyuf
* @date 2020/9/27 11:00
*/
public class UserFactory {
public UserService getUserService() {
return new UserServiceImpl();
}
}
XML:
<!--普通工厂创建对象-->
<bean id="userFactory" class="com.lyuf.factory.UserFactory"></bean>
<bean id="userService" class="com.lyuf.factory.UserFactory" factory-bean="userFactory" factory-method="getUserService"></bean>
3、采用静态工厂创建实例Bean
配置工厂类:
/**
* @author lyuf
* @date 2020/9/27 11:01
*/
public class UserStaticFactory {
public static UserService getUserService() {
return new UserServiceImpl();
}
}
XML:
<bean id="userService" class="com.lyuf.factory.UserStaticFactory" factory-method="getUserService"></bean>
2.1.2 属性注入的方式
1、接口注入
public class Person implements InjectFinder {
private Head head;
}
public interface InjectFinder {
void injection(Head head);
}
@Test
public void TestDo() {
Person person = new Person();
person.injection(new Head());
person.doSomething();
}
2.构造方法注入
public class Person {
private Hand hand;
private Footer footer;
private Head head;
public Person(Hand hand, Footer footer, Head head) {
this.hand = hand;
this.footer = footer;
this.head = head;
}
}
public class Person {
private Hend hend;
public Person() {
head = new Head();
}
}
//通过Person 的构造函数,向Person 传递了一个Hand对象。
//这意味着两个对象之间的耦合变低了。
//Person类不需要知道 Hand 的具体实现,只要继承了原始 Hand 类,任何类型 Hand 都符合要求
3.setget注入
public class Person {
private Hend hend;
public Person() {
head = new Head();
}
public void setHead(Head head) {
this.head = head;
}
}
4.注解注入
基于注解的组件(比如使用 @Component , @Controller 等)或者在配置了 @Configuration 的类上面使用 @Bean 的方法
2.1.3 IOC和DI
ioc是控制翻转,不是什么技术,而是一种思想。
正常情况下我们创建一个A类,A类中需要依赖其他的外部资源C,所以A类需要主动去寻找C资源。而如今有了spring,我们只需要告诉spring,我们需要C,spring会帮你创建或者找到这个C,并想打针一样注入到A类中,这就是DI(依赖注入)。
IoC容器需要具备两个基本的功能:
通过描述管理Bean , 包括发布和获取Bean;
在Spring 的定义中,它要求所有的IoC 容器都需要实现接口BeanFactory
IoC的一个重点是在系统运行中,动态的向某个对象提供它所需要的其他对象。这一点是通过DI(Dependency Injection,依赖注入)来实现的。比如对象A需要操作数据库,以前我们总是要在A中自己编写代码来获得一个Connection对象,有了 spring我们就只需要告诉spring,A中需要一个Connection,至于这个Connection怎么构造,何时构造,A不需要知道。在系统运行时,spring会在适当的时候制造一个Connection,然后像打针一样,注射到A当中,这样就完成了对各个对象之间关系的控制。A需要依赖 Connection才能正常运行,而这个Connection是由spring注入到A中的,依赖注入的名字就这么来的。那么DI是如何实现的呢? Java 1.3之后一个重要特征是反射(reflection),它允许程序在运行的时候动态的生成对象、执行对象的方法、改变对象的属性,spring就是通过反射来实现注入的。
2.1.4 IOC的创建过程(Spring Bean的生命周期)
1、Bean的创建 (我们常说的new),在容器中寻找bean的定义信息,并将其实例化
2、属性注入(IOC注入),使用依赖注入,根据bean中的信息来注入属性值
3、调用BeanNameAware的setBeanName,工厂通过setbeanname来传递bean的id
4、调用BeanFactoryAware的setBeanFactory,通过setBeanFactory方法让bean传入到工厂中
5、如果这个Bean已经实现了ApplicationContextAware接口,会调用setApplicationContext(ApplicationContext)方法,传入Spring上下文
6、如果这个Bean关联了BeanPostProcessor接口,将会调用postProcessBeforeInitialization(Object
obj, String s)方法,用作是Bean内容的更改
7、如果Bean在Spring配置文件中配置了init-method属性会自动调用其配置的初始化方法。(初始化)
8、如果这个Bean关联了BeanPostProcessor接口,将会调用postProcessAfterInitialization(Object
obj, String s)方法
9、当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean这个接口,会调用那个其实现的destroy()方法;
10、最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法。(摧毁)
2.1.5 AOP(动态代理)
AOP,即面向切面编程。是对OOP的一种补充,OOP是纵向的而AOP是横向的。
优点:
1、降低模块之间的耦合度。
2、使系统容易拓展
3、更好的代码复用
AOP的相关概念:
例如:方法增强
切面(Aspect):类中多个方法得到增强
连接点(JoinPoint):方法执行的某个特定的位置,调用前,调用后,方法抛出异常后等
通知(Advice):增强的代码
前置通知[Before advice]:在连接点前面执行,
正常返回通知[After returning advice]:在连接点正常执行完成后执行,
异常返回通知[After throwing advice]:在连接点抛出异常后执行。
返回通知[After (finally) advice]:在连接点执行完成后执行,
环绕通知[Around advice]:环绕通知围绕在连接点前后,
切点(PointCut):相当于类中的某个的方法
织入(Weaving):将增强的代码应用到某个方法上的过程
引介(Introduction):引介是一种特殊的增强,它为类添加一些属性和方法。这样,即使一个业务类原本没有实现某个接口,通过引介功能,可以动态的未该业务类添加接口的实现逻辑,让业务类成为这个接口的实现类。
AOP代理(AOP Proxy):在Spring AOP中有两种代理方式,JDK动态代理和CGLIB代理。
2.1.6 Spring三级缓存(循环依赖)
singletonObject:完成初始化的单例对象的cache(一级缓存)
earlySingletonObject:完成实例化但是未初始化的cache(二级缓存)
singletonFactories:进入实例化阶段的单例对象工厂的cache(三级缓存)
2.1.7 scope的值
<!--无参构造创建对象-->
<!-- scope="singleton" 相同的Action对象-->
<!-- scope="prototype" 可以保证当有请求的时候都创建一个Action对象-->
1、bean 的属性
2、作用控制对象的有效范围(单例,多例)
3、标签对应的对象默认是单例
4、无论多少次都是同一个对象
2.1.8 Spring Bean的初始化顺序 :@Order @DependOn
在类上添加@Order注解,order值越小,优先级越高
在不调整bean的定义顺序和强加依赖的情况下,可以通过depend-on属性来设置当前bean的依赖于哪些bean,那么可以保证depend-on指定的bean在当前bean之前先创建好,销毁的时候在当前bean之后销毁。
epend-on使用方式:
<bean id="bean1" class="" depend-on="bean2,bean3,bean4" />
2.1.9 @Autowire和@Resource区别
注解@Autowire和@Resource,我们都知道都可以用来装配bean,将对象加载到容器之中,但是他们具体有什么区别,工作当中大家也没怎么注意到,现在简单说明一下这俩个注解的区别。
@AutoWire默认按照类型装配,默认情况下它要求依赖对象必须存在,如果允许为null,可以设置它required属性为false
@Resource默认按照名称装配,当找不到与名称匹配的bean才会按照类型装配。可以通过name属性指定,如果没有指定name属 性,当注解标注在字段上,即默认取字段的名称作为bean名称寻找依赖对象,当注解标注在属性的setter方法上,即默认取属性名作为bean名称寻找 依赖对象.
2.2 SpringMVC
2.2.1 SpringMVC的运行流程(实现原理)
springMVC工作流程是从前端控制器dispatcherServlet开始的,是整个流程的中心,dispatcherServlet完成了对HTTPS请求的拦截和分发处理
1、客户端client发送请求URL到Tomcat服务器
2、servlet容器初始化,调用filter,对用户的请求进行预处理,servlet容器会创建特定于这个请求的servletRequest对象和servletResponse对象,然后将请求发送给servlet(首次调用servlet,容器会调用servlet的init方法)
3、dispatcherServlet对请求进行拦截,调用service方法对请求进行解析(doGet和doPost分别处理get和post请求,并将ServletRequest,ServletResponse强转为HttpRequest和HttpResponse)
4、然后从容器中取出所有的handleMapping实例,并遍历,找到一个可以处理该请求的映射器,映射器的作用就是根据当前请求找到对应的处理器handle,并将处理器和一堆拦截器封装到handleExecutionChain对象中,然后向dispatcherServlet返回一个处理器链。
5、dispatcherServlet拿到对象后,再次遍历所有的处理器适配器handleAdapter,寻找一个支持整这个处理器的处理器适配器,因为只有处理器适配器才知道如何使用这个处理器请求(实际上就是对controller的定位,使用哪个controller)
6、dispatcherServlet将控制权交给处理器适配器,处理器适配器将http请求传递给控制器controller,控制器完成请求处理后,调用方法,返回一个modelandview(逻辑视图路径)给handleAdaptor,然后处理器适配器再返回给dispatcherServlet。
7、dispatcherServlet遍历所有的视图解析器,将modelandview传给视图解析器,得到一个确定的视图(物理视图),然后再返回给dispatcherServlet,dispatcherServlet再调用render方法对视图进行渲染
8、最后将视图传递给Tomcat服务器,服务器再返回给客户端,servlet容器执行destory()方法释放资源。
2.2.2 Restful @PathVariable
@pathvariable 是用来赋予请求URL中的动态参数,即将URL中的变量映射到接口方法的参数上。
2.2.3 @ResponseBody @RequestBody @RequestParam
@responsebody 注解的作用是将controller方法返回的对象转化为指定的格式之后,写入到response的body区,通常用来返回json数据或者是xml
@requestparam 这个一般是用在ajax传输数据里,没有声明contentType时,为默认,另外使用form表格提交数据就只能使用@requestparam接收
@RequestBody 主要用来接收前端传递给后端的json字符串中的数据,因为get方法无请求体,故使用@requestbody接收数据时不能用get传输,而是用POST方式进行提交。在后端的同一个接收方法里,@RequestBody与@RequestParam()可以同时使用,@RequestBody最多只能有一个,而@RequestParam()可以有多个。
注:一个请求,只有一个RequestBody;一个请求,可以有多个RequestParam。
2.3 Mybatis
Mybatis是用映射的方式,将XML表中的MySQL命令与数值发送至数据库中,从而得到相应的表
2.3.1 Mybatis动态SQL
select 标签
id :唯一的标识符.
parameterType:传给此语句的参数的全路径名或别名 例:com.test.poso.User 或 user
resultType :语句返回值类型或别名。注意,如果是集合,那么这里填写的是集合的泛型,而不是集合本身(resultType 与 resultMap 不能并用)
insert 标签
id :唯一的标识符
parameterType:传给此语句的参数的全路径名或别名 例:com.test.poso.User
2.3.2 Mybatis $和#区别
1、数据类型匹配
#:会进行预编译,而且进行类型匹配
$:不会进行类型匹配
2、实现方式
#:用于变量的替换
$: 用于字符串的拼接
1)变量的传递,必须使用#,使用#{}就等于使用了PrepareStatement这种占位符的形式,提高效率。可以防止sql注入等等问题。#方式一般用于传入添加,修改的值或查询,删除的where条件 id值
select * from t_user where name = #{param}
(2)$ 只是只是简单的字符串拼接,要特别小心sql注入问题,对应非变量部分,只能用$。 $ 方式一般用于传入数据库对象,比如这种group by 字段 ,order by 字段,表名,字段名等没法使用占位符的就需要使用$ {}
select count(*), from t_user group by KaTeX parse error: Expected 'EOF', got '#' at position 17: …param} (3)能同时使用#̲和的时候,最好用#。
sql注入问题:SQL注入就是将原本的SQL语句的逻辑结构改变,使得SQL语句的执行结果和原本开发者的意图不一样。
2.3.3 Mybatis批处理
批量处理即对多条数据进行sql操作,如批量更新,插入,新增。
之前采取过很low的方式,就是在dao层进行循环,对每条数据进行操作。这样效果可以实现,但是频繁连接数据库,性能,效率上非常不好。mybatis支持参数为list的操作,这样连接数据库就一次,把循环的语句写入到sql语句中,这样效率会高很多。
<insert id="addResource" parameterType="java.util.List">
insert into resource (object_id, res_id, res_detail_value,
res_detail_name)
values
<foreach collection="list" item=" ResourceList " index="index" separator=",">
(#{
ResourceList.objectId,jdbcType=VARCHAR},
#{
ResourceList.resId,jdbcType=VARCHAR},
#{
ResourceList.resDetailValue,jdbcType=VARCHAR},
#{
ResourceList.resDetailName,jdbcType=VARCHAR}
)
</foreach>
</insert>
2.3.4 Mybatis实现过程(实现原理)
基于mybatis的核心类 SQLSessionfactory
1、创建SQLSessionfactorybuilder对象,调用build方法读取并解析配置文件(XML文件),返回一个SQLSessionfactory对象
2、SQLSessionfactory创建一个SQLSession对象,没有手动开启的话,事务会默认开启。
3、然后调用SQLSession中的api,传入statement和参数(mybatis dao xml 映射),通过statement id找到对应的预处理对象。
4、调用jdbc执行sql语句,封装结果返回。
SQLSession会调用内部存放的executor执行器来对数据库进行操作
SQLSession.getMapper()方法实现动态代理
2.3.5 补 JDK代理和CGlib代理
在Java中许多框架的底层都是基于动态代理来实现的,比如AOP,mybatis动态生成数据库操作类。在Java中有两种动态代理的方法。一种是JDK原生的动态代理,一种是基于CGlib的代理方式。
什么是代理模式
为其他对象提供一种代理以控制对这个对象的访问
什么是动态代理
与静态代理在程序运行前就已经存在的方式不同,动态代理指的是代理类是在运行时才被创建出来的。相比于静态代理,动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类的函数。
- JDK实现方式产生的代理类是接口的实现,也就是说serviceProxy是可以赋值给IService的,但是不能赋值给ServiceImpl。对应Cglib则使用的继承机制,具体说被代理类和代理类是继承关系,所以代理类是可以赋值给被代理类的,如果被代理类有接口,那么代理类也可以赋值给接口。
- JDK代理只能对接口进行代理,Cglib则是对实现类进行代理。
- Cglib采用的是继承,所以不能对final修饰的类进行代理。
- JDK采用反射机制调用委托类的方法,Cglib采用类似索引的方式直接调用委托类方法;
从 jdk6 到 jdk7、jdk8 ,动态代理的性能得到了显著的提升,与cglib的性能上已经差别不大
2.4 SpringBoot
2.4.1 自动装配原理(实现过程)
@SpringBootApplication
组合注解:
元注解:
@Target(ElementType.TYPE) @Target表示注解的修饰目标,ElementType指作用目标的类型:
@Retention(RetentionPolicy.RUNTIME)
代表注解的保留位置,其中的RetentionPolicy代表保留规则: @Documented
说明该注解将被包含在javadoc中,生成的文档会有API注解,标记作用 @Inherited 说明子类可以继承父类中的该注解
其他注解:
@springbootconfiguration :它也是一个组合注解,是对@configuration的包装,表明这是个配置类
@EnableAutoConfiguratin :该注解表示开启自动装配,我们前面提过,在SpringBoot中没有配置Tomcat容器(包括端口)、dispatcherServlet等MVC组件加载信息,web.xml等。使其都是指定了该注解后,springboot帮我们初始化的,当我们启动了@EnableAutoConfiguratin 注解,启动自动配置后,该注解会根据项目依赖的jar包自动配置项目的配置项。
如:我们添加了spring-boot-starter-web的依赖,项目中也就会引入SpringMVC的依赖,
Spring boot就会自动配置tomcat和SpringMVC:
@ConpontScan 该注解会自动扫描路径下面的所有@Controller、@Service、@Repository、@Component 的类,不配置包路径的话,在Spring Boot中默认扫描@SpringBootApplication所在类的同级目录以及子目录下的相关注解。
2.4.2 .SpringBoot多环境配置
application-dev.properties 对应开发环境
application-test.properties 对应测试环境
application-pro.properties 对应生产环境
2.5 RabbitMQ
2.5.1 消息模式 P2P Worker Pub/Sub(exchange 4种)
消息队列中间件,是分布式系统中的重要组件;主要解决异步处理、应用解耦、流量削峰等问题,从而实现高性能,高可用,可伸缩和最终一致性的架构。
使用较多的消息队列产品:RabbitMQ,RocketMQ,ActiveMQ,ZeroMQ,Kafka 等。
异步处理:
用户注册后,需要发送验证邮箱和手机验证码。 将注册信息写入数据库,发送验证邮件,发送手机,三个步骤全部完成后,返回给客户端。
传统:
客户端<-> 注册信息写入数据库 -> 发送注册邮件 -> 发送注册短信
现在:
客户端 <-> 注册信息写入数据库 -> 写入消息队列 ->异步 [发送注册邮件,发送注册短信]
应用解耦:
场景:订单系统需要通知库存系统。
如果库存系统异常,则订单调用库存失败,导致下单失败。
原因:订单系统和库存系统耦合度太高。
传统:
用户 <-> 订单系统 - 调用库存接口 -> 库存系统
现在:
用户 <-> 订单系统 - 写入 -> 消息队列 <- 订阅 - 库存系统
订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户,下单成功。
库存系统:订阅下单的消息,获取下单信息,库存系统根据下单信息,再进行库存操作。
假如:下单的时候,库存系统不能正常运行,也不会影响下单,因为下单后,订单系统写入消息队列就不再关心其他的后续操作了,实现了订单系统和库存系统的应用解耦。
所以,消息队列是典型的“生产者-消费者“模型。
Producer 消息生产者,即生产方客户端,将消息发送给MQ
Consumer 消息消费者,即消费者客户端,接收MQ发送的信息
Broker 消息队列服务进程,此服务包括两部分:exchange、queue
Exchange 消息队列交换机,交换机按照一定的规则将消息路由转发到队列,对消息进行过滤。
Queue 消息队列,存储消息,消息到达队列并将消息转发给消费方。
1、简单队列:
一个生产者,一个默认的交换机,一个队列,一个消费者
2、work queue 工作模式
workqueue 是一个较为基础的工作模式,其工作模式是多个消费者共同消费同一个队列中的信息,启动多个消费者。一个生产者,一个默认的交换机,一个队列,两个消费者,默认采用公平分发
3、Publish/Subscribe发布订阅模式
一个生产者,一个交换机,两个队列,两个消费者
使用该模式需要借助交换机,生产者将消息发送到交换机,再通过交换机到达队列.
有四种交换机:direct/topic/headers/fanout,默认交换机是direct,发布与订阅的实现使用第四个交换器类型fanout
使用交换机时,每个消费者有自己的队列,生产者将消息发送到交换机(X),每个队列都要绑定到交换机
4、Routing 路由模式
一个生产者,一个交换机,两个队列,两个消费者
生产者将消息发送到direct交换机(路由模式需要借助直连交换机实现),在绑定队列和交换机的时候有一个路由key,生产者发送的消息会指定一个路由key,那么消息只会发送到相应key相同的队列,接着监听该队列的消费者消费消息。也就是让消费者有选择性的接收消息。
5、主题模式
一个生产者,一个交换机,两个队列,两个消费者又称通配符模式(可以理解为模糊匹配,路由模式相当于精确匹配)
使用直连交换机可以改善我们的系统,但是它仍有局限性,它不能实现多重条件的路由。
在消息系统中,我们不仅想要订阅基于路由键的队列,还想订阅基于生产消息的源。这时候可以使用topic交换机。
使用主题交换机时不能采用任意写法的路由键,路由键的形式应该是由点分割的有意义的单词。例如"goods.stock.info"等。路由key最多255字节。
*号代表一个单词
#号代表0个或多个单词
2.5.2 rabbitMQ消息丢失
如图,生产者P向队列中生产消息,C1和C2消费队列中的消息,默认情况下,RabbitMQ会平均的分发消费给C1C2(Round-robin dispatching),假设一个任务的执行时间非常长,在执行过程中,客户端挂了(连接断开),那么,该客户端正在处理且未完成的消息,以及分配给它还没来得及执行的消息,都将丢失。因为默认情况下,RabbitMQ分发完消息后,就会从内存中把消息删除掉。
2.5.3 RabbitMQ消息确认机制
rabbitmq提供了消息监听器,即listener来接收消息的投递状态
—— 这个listener指的是producer端的,而消息确认的确认涉及到两种状态,confirm和return
通过将channel设置成confirm模式来实现;
2.6 ElasticSearch
2.6.1 倒排索引
索引这东西,一般是用于提高查询效率的。
举个最简单的例子,已知有5个文本文件,需要我们去查某个单词位于哪个文本文件中,最直观的做法就是挨个加载每个文本文件中的单词到内存中,然后用for循环遍历一遍数组,直到找到这个单词。这种做法就是正向索引的思路。
–正向索引是通过文档去查找单词,反向索引则是通过单词去查找文档,我们建立一个索引库,里面的key是关键字,value是每个文档的id
–而倒排索引,是通过分词策略,形成了词和文章的映射关系表,这种词典+映射表即为倒排索引
–倒排索引源于实际应用中需要根据属性的值来查找记录,这种索引表中的每一项都包括一个属性值和具有该属性值的各记录的地址。由于不是由记录来确定属性值,而是由属性值来确定记录的位置,因而称为倒排索引(inverted
index)。
–电商网站中的检索功能,通常是用户搜索关键词,然后需求就是根据关键词来返回商品的动态地址,就是词条中所说的这个由属性值来确定记录的位置。
–倒排在构建索引的时候较为耗时且维护成本较高,但是搜索耗时短,
(1)空间占用小。通过对词典中单词前缀和后缀的重复利用,压缩了存储空间;
(2)查询速度快。O(len(str))的查询时间复杂度
2.6.2 分词
把输入的文本块按照一定的策略进行分解,并建立倒排索引。
默认ES使用standard analyzer,可以自己配置。
拼音分词器
ik 中文分词器
2.6.3 项目使用 数据同步
在Kibana中建立索引
有了数据后,我们就需要对数据进行检索操作。根据实际开发需要,往往我们需要支持包含但不限于以下类型的检索:
1)精确匹配,类似mysql中的 “=”操作;
2)模糊匹配,类似mysql中的”like %关键词% “查询操作;
3)前缀匹配;
4)通配符匹配;
5)正则表达式匹配;
6)跨索引匹配;
7)提升精读匹配。
3、微服务
3.1.1 微服务的理解
微服务是一种架构风格,一个大型复杂软件应用由一个或多个微服务组成。系统中的各个微服务可被独立部署,各个微服务之间是松耦合的。每个微服务仅关注于完成一件任务并很好地完成该任务。在所有情况下,每个任务代表着一个小的业务能力。
微服务架构的核心目标是把复杂问题简单化,通过服务划分,把一个完整的系统拆分成多个高内聚、低耦合的小的子系统。使每个子系统可以独立的运行、升级和测试。然后再通过一些集成手段将这些子系统组合在一起,对外提供完整功能的过程。所以我们对微服务设计的过程就是对系统如何做拆分和集成的过程。
3.1.2 微服务解决方案(Spring Cloud)
3.1.3 各大组件(5大组件)网关
网关:路由转发 + 过滤器
服务注册中心
服务注册中心:调用和被调用的信息维护
配置中心
配置中心:配置设置,动态更新 application.peoperties
链路追踪
链路追踪:分析调用链路耗时
负载均衡
负载均衡:分发负载
熔断
熔断:保护自己和被调用方
SpringCloud系列之Nacos
开关类 使用注解@EnableDiscoveryClient
微服中的服务容错Sentinel
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
1.Sentinel的作用(1.流量控制 2.熔断降级 3.自适应保护)
2.流量控制的策略(单机:QPS或者线程数 最大阈值,集群:QPS或线程数,单机均摊、总体阈值)
3.熔断降级策略(RT(Response Time)响应时间,异常响应的比例,异常响应的数量,还需要设置熔断的间隔时间/秒)
微服务中的链路跟踪Sleuth
Sleuth为Spring Cloud实现了分布式跟踪解决方案。它在整个分布式系统中能跟踪一个用户请求的过程(包括数据采集,数据传输,数据存储,数据分析,数据可视化),捕获这些跟踪数据,就能构建微服务的整个调用链的视图,这是调试和监控微服务的关键工具。
Zipkin是一个分布式跟踪系统。它有助于收集解决服务体系结构中的延迟问题所需的时序数据。功能包括该数据的收集和查找。
服务的负载均衡之Ribbon
Ribbon是一个客户端的负载均衡器,它提供对大量的HTTP和TCP客户端的访问控制,可以实现服务的远程调用和服务的负载均衡协调。
核心作用:1.负载均衡 实现服务集群的策略调用 2.实现服务的远程调用
3.1.4 组件的实现原理(1-2个)
3.1.5 分布式锁
1.4.16
3.1.6 分布式事务
简单的说,就是一次大操作由不同小操作组成,这些小操作分布在不同服务器上,分布式事务需要保证这些小操作要么全部成功,要么全部失败。
你上淘宝买东西,需要先扣钱,然后商品库存-1吧。但扣款和库存分别属于两个服务,这两个服务中间要经过网络、网关、主机等一系列中间层,万一任何一个地方出了问题,比如网络抖动、突发异常等待,都会导致不一致,比如扣款成功了,但是库存没-1,就会出现超卖的现象,而这就是分布式事务需要解决的问题。
2PC(二阶段提交)
而事务管理器作为全局的调度者,负责各个本地资源的提交和回滚
3PC(三阶段提交)
1、引入超时机制。同时在协调者和参与者中都引入超时机制。
2、在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与
3.1.7 Dubbo相关(协议、注册中心、超时、服务容错)
3.1.8 SpringCloud+Dubbo
4、数据库
4.1 Mysql
4.1.1 存储引擎(InnoDB+Myisma)
InnoDB:数据库的首选引擎,支持事务编程,支持事务安全表(ACID),支持锁定和外键,灾难恢复性好,实现了缓冲,不仅能缓冲索引,还能缓冲数据,会自动创建索引来加快数据的读取,被用在许多高性能大型数据库站点上。
Myisma:具有较高的插入和查询速度,不支持事务,表级锁,并发性,可以获得更小的索引和更快的查询速度,可以把数据和索引分别放在不同的文件。
memory:将表中的数据存储在内存中,为查询和引用其他表数据提供快速访问,varchar类型有固定
的长度,浪费空间,服务器重启后数据易丢失
4.1.2 SQL执行过程
from–where–group by–having–select–order by
from:需要从哪个数据表检索数据
where:过滤表中数据的条件
group by:如何将上面过滤出的数据分组
having:对上面已经分组的数据进行过滤的条件
select:查看结果集中的哪个列,或列的计算结果
order by :按照什么样的顺序来查看返回的数据
4.1.3 慢查询(获取需要优化的SQL语句)
MySQL的慢查询,全名是慢查询日志,是MySQL提供的一种日志记录,用来记录在MySQL中响应时间超
过阀值的语句。
slow_query_log:是否开启慢查询日志,1表示开启,0表示关闭。
4.1.4 SQL优化
1 使用外键
2 使用索引
3 开启事务
4 锁定表
5 用join代替子查询
6 使用连表查询
7 选取合适的字段属性
8 进行sql语句优化
1、 把数据、日志、索引放到不同的I/O设备上,增加读取速度,以前可以将Tempdb应放在RAID0上,SQL2000不在支持。数据量(尺寸)越大,提 高I/O越重要.
2、纵向、横向分割表,减少表的尺寸(sp_spaceuse)
3、升级硬件
4、根据查询条件,建立索引,优化 索引、优化访问方式,限制结果集的数据量。注意填充因子要适当(最好是使用默认值0)。索引应该尽量小,使用字节数小的列建索引好(参照索引的创建),不 要对有限的几个值的字段建单一索引如性别字段
5、提高网速;
6、扩大服务器的内存,Windows 2000和SQL server 2000能支持4-8G的内存。
7、增加 服务器CPU个数;但是必须明白并行处理串行处理更需要资源例如内存。使用并行还是串行程是MsSQL自动评估选择的。单个任务分解成多个任务,就可以在 处理器上运行。例如耽搁查询的排序、连接、扫描和GROUP BY字句同时执行,SQL SERVER根据系统的负载情况决定最优的并行等级,复杂的需要消耗大量的CPU的查询最适合并行处理。但是更新操作 UPDATE,INSERT,DELETE还不能并行处理。
8、如果是使用like进行查询的话,简单的使用index是不行的,但是全文索 引,耗空间。
4.1.5 索引(使用、生效、底层原理(BTree B-Tree B+Tree))
MySQL官方对于索引的定义为:索引是帮助MySQL高效获取数据的数据结构。
树形结构:由根节(root)、分支(branches)、叶(leaves)三级节点组成,其中分支节点可以有多层。
在InnoDB中,表数据文件本身就是按B+Tree组织的一个索引结构,这棵树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。
每个叶子结点都指向下一个叶子结点,这是一个链表的形式。
所以,B+树实际上是树结构和链表结构的结合。
① 这就是排序对索引有效的原因。
② B+树的高度是由数据量决定的。
Btree索引的限制:
a) 如果不是按照索引的最左列开始查找,则无法使用索引;
b) 不能跳过索引中的列;
c) 如果查询中有某个列的范围查询,则其右边所有列都无法使用索引优化查找。
4.1.6 事务(ACID 隔离级别 胀读、虚读、不可重复读)
ACID:
原子性 : 整个过程是连续的,要么全部成功,要不全部失败
隔离性 : 事务允许多个用户对同一个数据进行并发访问,而不破坏数据
一致性 : 事务必须保持和系统处于一致的状态下
持久性 : 一个成功的事务将永久改变数据库中的状态,固化
问题:
1 脏读 : A读取了事务B更新的数据,结果B回滚了,导致A读取的数据和现有数据不同
2 不可重复读 :A在多次读取数据的过程中,B改变了此数据,导致A多次读取的数据不相同
3 幻读 :多次查询后,结果集中的个数不一样,如:A在为数据分ABCDE等级时,B在数据中又插入了一条数据,导致最后多出来一条数据。
隔离级别:
脏读 不可重复读 幻读
读未提交 read-uncommitted X X X
读已提交 read-committed √ X X
可重复读 repeatable-read √ √ X
串行读 serializable √ √ √
4.1.7 Mysql函数和自定义函数
用户自定义函数是一种对MySql扩展的途径,其用法与内置函数相同。
CREATE FUNCTION f1()
RETURNS VARCHAR(20)
RETURN"HELLO";
SELECT f1();
4.1.7 分库和分表
当一张表随着时间和业务的发展,库里表的数据量会越来越大,,数据操作也会随之越来越大。
一台机器的承载能力是有限的,达到了这个量后,数据的处理能力就会受限制,这时候就用到了分库和分表
垂直切分
表按照功能模块,关系密切程度划分出来,部署到不同的库上,
比如:建立用户数据库 商品数据库 分别存储不同的数据
垂直分库使原本在同一数据库中的表拆分到不同数据库(节点)中,操作不同数据库中的表要使用分布式事务,事务的使用变得复杂。另外,跨库的多表关联查询性能较差。
垂直分表使原本一张表中的字段拆分到多张表中,不同表之间的操作需要多表关联查询,连接查询性能较差
垂直切分提升了单表查询的性能,但增加了多表关联查询的次数。
水平切分
垂直切分只是减少了单表的字段数,但并没有减少单表的记录数。水平切分是将单表中的记录拆分到多张表中。
当一个表中的数据量过大时,我们可以把该表的数据按照某种规则,存储相同的表在不同的库中
库内分表 将拆分出来的新表放在同一数据库中
分库分表 将拆分出来的新表放在不同的数据库中
库内分表只解决了单表记录数过多的问题,但拆分出来的表竞争同一个机器的CPU、内存、网络IO,并没有减轻数据库的存储压力。
如果数据库是因为表太多而造成海量数据,并且项目的各项业务逻辑划分清晰、低耦合,那么规则简单明了、容易实施的垂直切分必是首选。
而如果数据库中的表并不多,但单表的数据量很大、或数据热度很高,这种情况之下就应该选择水平切分,水平切分比垂直切分要复杂一些,它将原本逻辑上属于一体的数据进行了物理分割,除了在分割时要对分割的粒度做好评估,考虑数据平均和负载平均,后期也将对项目人员及应用程序产生额外的数据管理负担。
4.2 Oracle
4.3 Redis
4.3.1 Redis为什么快
Redis的高并发和快速原因很多,总结一下几点:
- Redis是纯内存数据库,一般都是简单的存取操作,线程占用的时间很多,时间的花费主要集中在IO上,所以读取速度快。
- 再说一下IO,Redis使用的是非阻塞IO,IO多路复用,使用了单线程来轮询描述符,将数据库的开、关、读、写都转换成了事件,减少了线程切换时上下文的切换和竞争。
阻塞IO模型最传统的一种IO模型,即在读写数据过程中会发生阻塞现象。
当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态
当用户线程发起一个read操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。
多路复用IO模型是目前使用得比较多的模型。Java NIO实际上就是多路复用IO。
在多路复用IO模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket读写事件进行时,才会使用IO资源,所以它大大减少了资源占用。
- Redis采用了单线程的模型,保证了每个操作的原子性,也减少了线程的上下文切换和竞争。
- 另外,数据结构也帮了不少忙,Redis全程使用hash结构,读取速度快,还有一些特殊的数据结构,对数据存储进行了优化,如压缩表,对短数据进行压缩存储,再如,跳表,使用有序的数据结构加快读取的速度。
- 还有一点,Redis采用自己实现的事件分离器,效率比较高,内部采用非阻塞的执行方式,吞吐能力比较大。
4.3.2 Redis常用的数据类型
1.字符串(String):最常用的,一般用于存储一个值
2.列表(List):使用list结构实现栈和队列结构
3.集合(Set) :交集,差集和并集的操作
4.有序集合(sorted set) :排行榜,积分存储等操作
5.哈希(Hash):存储一个对象数据的
4.3.3 Redis 穿透、雪崩、倾斜等
缓存穿透(查不到)
概念:当用户去查询数据的时候,发现redis内存数据库中没有,于是向持久层数据库查询,发现也没有,于是查询失败,当用户过多时,缓存都没有查到,于是都去查持久层数据库,这会给持久层数据库造成很大的压力,此时相当于出现了缓存穿透。
解决方案:
1.布隆过滤器:是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的压力.
2.缓存空对象:当存储层查不到时,即使返回的空对象也将其缓存起来,同时设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护后端数据.
缓存击穿(访问量大,缓存过期)
指对某一个key的频繁访问,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就会直接请求数据库,就像在一个屏障上凿开了一个洞,例如微博由于某个热搜导致宕机.其实就是:当某个key在过期的瞬间,有大量的请求并发访问,这类数据一段是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并回写缓存,导致数据库瞬间压力过大。
解决方案:
1.设置热点数据永不过期:从缓存层面上来说,不设置过期时间,就不会出现热点key过期后产生的问题.
2.添加互斥锁:使用分布式锁,保证对每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可,这种方式将高并发的压力转移到了分布式锁上,对分布式锁也是一种极大的考验.
缓存雪崩
指在某一个时间段,缓存集中过期失效或Redis宕机导致的,例如双十一抢购热门商品,这些商品都会放在缓存中,假设缓存时间为一个小时,一个小时之后,这些商品的缓存都过期了,访问压力瞬间都来到了数据库上,此时数据库会产生周期性的压力波峰,所有的请求都会到达存储层,存储层的调用量暴增,造成存储层挂掉的情况.
其实比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网,因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,此时的数据库还是可以顶住压力的,而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,有可能瞬间就把服务器压垮.
解决方案:
1.配置Redis的高可用:其实就是搭建集群环境,有更多的备用机.
2.限流降级:在缓存失效后,通过加锁或者队列来控制读服务器以及写缓存的线程数量,比如对某个key只允许一个线程查询数据和写缓存,其他线程等待.
3.数据预热:在项目正式部署之前,把可能用的数据预先访问一边,这样可以把一些数据加载到缓存中,在即将发生大并发访问之前手动触发加载缓存中不同的key,设置不同的过期时间,让缓存失效的时间尽量均衡.
缓存倾斜
指某一台redis服务器压力过大而导致该服务器宕机.
4.3.4 Redis集群方案
4.3.5 Redis失效策略
主动式过期:
redis会将所有设置了过期时间的key放到一个独立的字典中,以后会定时扫描这个字典,将过期的key删除。redis默认每秒进行10次过期扫描,扫描不会扫描所有的key,而是采用一种贪心的策略来进行:
- 从过期字典中随机选择20个key。
- 将20个key中过期的key删除。
- 如果过期的key的比例超过1/4,redis认为过期的key相对较多,会再次从步骤1依次执行。 同时为了保证扫描不会出现过度循环影响性能,redis会限制每次扫描的时间上限为25ms。
被动式过期:
redis定时扫描任务可能不会将过期的key完全删除,此时redis会用一种懒惰策略来被动式的更新过期的key,即当这个key被实际访问时,redis会检查这个key的过期状态,如果过期,那么将这个key删除。
4.3.6 Redis淘汰策略
redis内存数据数据集大小升到一定大的时候,就会实行数据淘汰策略(回收策略)。
1,volatile-lru:从已设置过期时间的哈希表(server.db[i].expires)中随机挑选多个key,然后在选到的key中用lru算法淘汰最近最少使用的数据
2,allkey-lru:从所有key的哈希表(server.db[i].dict)中随机挑选多个key,然后再选到的key中利用lru算法淘汰最近最少使用的数据
3,volatile-ttl:从已设置过期时间的哈希表(server.db[i].expires)中随机挑选多个key,然后在选到的key中选择过期时间最小的数据淘汰掉。
4,volatile-random:从已设置过期时间的哈希表(server.db[i].expires)中随机挑选key淘汰掉。
5,allkey-random:从所有的key的哈希表(server.db[i].dict)中随机挑数据淘汰
6,no-eviction(驱逐):内存达到上限,不淘汰数据。
4.3.7 RESP协议
4.3.8 Redis的String类型的优化
4.3.9 Redis的事务
- MULTI
用于标记事务块的开始。Redis会将后续的命令逐个放入队列中,然后才能使用EXEC命令原子化地执行这个命令序列。 - EXEC
在一个事务中执行所有先前放入队列的命令,然后恢复正常的连接状态。
当使用WATCH命令时,只有当受监控的键没有被修改时,EXEC命令才会执行事务中的命令,这种方式利用了检查再设置(CAS)的机制。
这个命令的运行格式如下所示:
EXEC
这个命令的返回值是一个数组,其中的每个元素分别是原子化事务中的每个命令的返回值。 当使用WATCH命令时,如果事务执行中止,那么EXEC命令就会返回一个Null值。 - DISCARD
清除所有先前在一个事务中放入队列的命令,然后恢复正常的连接状态。
如果使用了WATCH命令,那么DISCARD命令就会将当前连接监控的所有键取消监控。
这个命令的运行格式如下所示:
DISCARD
这个命令的返回值是一个简单的字符串,总是OK。 - WATCH
当某个事务需要按条件执行时,就要使用这个命令将给定的键设置为受监控的。
这个命令的运行格式如下所示:
WATCH key [key …]
这个命令的返回值是一个简单的字符串,总是OK。
对于每个键来说,时间复杂度总是O(1)。 - UNWATCH
清除所有先前为一个事务监控的键。
如果你调用了EXEC或DISCARD命令,那么就不需要手动调用UNWATCH命令。
这个命令的运行格式如下所示:
UNWATCH
这个命令的返回值是一个简单的字符串,总是OK。
时间复杂度总是O(1)。
4.3.10 Redis实现分布式锁
为什么Redis可以方便地实现分布式锁
1、Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系。
2、Redis的SETNX命令可以方便的实现分布式锁。
setNX(SET if Not eXists)
语法:SETNX key value
返回值:设置成功,返回 1 ;设置失败,返回 0 。
当且仅当 key 不存在时将 key 的值设为 value,并返回1;若给定的 key 已经存在,则 SETNX 不做任何动作,并返回0。
综上所述,可以通过setnx的返回值来判断是否获取到锁,并且不用担心并发访问的问题,因为Redis是单线程的,所以如果返回1则获取到锁,返回0则没获取到。当业务操作执行完后,一定要释放锁,释放锁的逻辑很简单,就是把之前设置的key删除掉即可,这样下次又可以通过setnx该key获取到锁了。
4.3.11 Redis主从复制
主从复制是指将一台Redis服务器的数据,复制到其它的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。
默认情况下,每台Redis服务器都是主节点,且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。
主从复制的作用:
1.数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
2.故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复,但实际上是一种服务的冗余.
3.负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
4.高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础.
4.3.12 Redis哨兵模式
当主服务器宕机后,并且我们并没有及时发现,这时候就可能会出现数据丢失或程序无法运行。此时,redis的哨兵模式就派上用场了,可以用它来做redis的高可用
功能作用:
1.监控(monitoring):Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。
2.提醒(Notifation):当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。
3.自动故障转移(Automatic failover):当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作, 它会将失效主服务器的其中一个从服务器升级为新的主服务器, 并让失效主服务器的其他从服务器改为复制新的主服务器;
当客户端试图连接失效的主服务器时, 集群也会向客户端返回新主服务器的地址, 使得集群可以使用新主服务器代替失效服务器。
5、项目研发
5.1.1 Git的分支(test -master-bug)
5.1.2 Git冲突(代码冲突、分支冲突)
5.1.3 Maven(常用命令、聚合、依赖的范围、依赖冲突)
5.1.4 Nginx(负载均衡 负载均衡算法(策略))
5.1.5 接口测试(PostMan+Swagger)+单元测试(方法)+性能测试(Jmeter)
5.1.6 项目性能指标:QPS TPS DAU MAU RT
转载:https://blog.csdn.net/qq_45598161/article/details/115111282