String,StringBuffer与StringBuilder的区别??
String 字符串常量
StringBuffer 字符串变量(线程安全)
StringBuilder 字符串变量(非线程安全)
sleep
是Thread
中的方法,线程暂停,让出CPU,但是不释放锁
wait()
是Object
中的方法, 调用次方法必须让当前线程必须拥有此对象的monitor(即锁),执行之后 线程阻塞,让出CPU, 同时也释放锁; 等待期间不配拥有CPU执行权, 必须调用notify/notifyAll
方法唤醒,(notify是随机唤醒) 唤醒并不意味着里面就会执行,而是还是需要等待分配到CPU才会执行;
clone
是浅拷贝;只克隆了自身对象和对象内实例变量的地址引用,使用它需要实现接口Cloneable
;
使用ObjectStream
进行深度克隆; 先将对象序列化;然后再反序列化;
public static <T extends Serializable> T deepClone(T t) throws CloneNotSupportedException {
// 保存对象为字节数组
try {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
try(ObjectOutputStream out = new ObjectOutputStream(bout)) {
out.writeObject(t);
}
// 从字节数组中读取克隆对象
try(InputStream bin = new ByteArrayInputStream(bout.toByteArray())) {
ObjectInputStream in = new ObjectInputStream(bin);
return (T)(in.readObject());
}
}catch (IOException | ClassNotFoundException e){
CloneNotSupportedException cloneNotSupportedException = new CloneNotSupportedException();
e.initCause(cloneNotSupportedException);
throw cloneNotSupportedException;
}
}
TL用于保存本地线程的值, 每个
Thread
都有一个threadLocals
属性,它是一个ThreadLocalMap
对象,本质上是一个Entry
数组;Entry
是k-v结构; 并且是WeakReference
弱引用, K存的是Thread
对象,Value是设置的值; 那么每个线程就可以读自己设置的值了;
会发生内存泄漏
ThreadLocalMap使用ThreadLocal的弱引用作为key
,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。
key是弱引用好歹还可以 GC掉key的对象;强引用则不行
使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
InheritableThreadLocal
基础ThreadLocal
; 他跟ThreadLocal
区别是 可以传递值给子线程; 每个Thread
都有一个inheritableThreadLocals
属性, 创建子线程的时候,把把父线程的Entry
数组 塞到子线程的Entry数组
中; 所以就实现了父子线程的值传递; 注意如果Value是一个非基本类型的对象, 父子线程指向的是相同的引用; 子线程如果修改了值,父线程也是会修改的;
线程不安全:
如果说线程本地变量是只读变量不会受到影响,但是如果是可写的,那么任意子线程针对本地变量的修改都会影响到主线程的本地变量
线程池中可能失效:
在使用线程池的时候,ITL会完全失效,因为父线程的TLMap是通过Thread的init
方法的时候进行赋值给子线程的,而线程池在执行异步任务时可能不再需要创建新的线程了,因此也就不会再传递父线程的TLMap给子线程了
阿里开源的
transmittable-thread-local
可以很好的解决 在线程池情况下,父子线程值传递问题;TransmittableThreadLocal
继承了InheritableThreadLocal
, 简单的原理就是TTL 中的holder持有的是当前线程内的所有本地变量,被包装的run方法执行异步任务之前,会使用replay进行设置父线程里的本地变量给当前子线程,任务执行完毕,会调用restore恢复该子线程原生的本地变量
1.在JDK1.7中,当并发执行扩容操作时会造成环形链和数据丢失的情况。(链表的头插法 造成环形链)
2.在JDK1.8中,在并发执行put操作时会发生数据覆盖的情况。(元素插入时使用的是尾插法)
HashMap在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的。
JDK1.7和JDK1.8中HashMap为什么是线程不安全的
- JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法,那么他们为什么要这样做呢?因为JDK1.7是用单链表进行的纵向延伸,当采用头插法就是能够提高插入的效率,但是也会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。
- 扩容后数据存储位置的计算方式也不一样:1. 在JDK1.7的时候是直接用hash值和需要扩容的二进制数进行&(这里就是为什么扩容的时候为啥一定必须是2的多少次幂的原因所在,因为如果只有2的n次幂的情况时最后一位二进制数才一定是1,这样能最大程度减少hash碰撞)(hash值 & length-1)
Hashmap的结构,1.7和1.8有哪些区别
提高检索时间,在链表长度大于8的时候,将后面的数据存在红黑树中,以加快检索速度。复杂度变成O(logn)
可以看出JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发,从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中
synchronized+CAS+HashEntry+红黑树
,相对而言
- JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)
- JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了
- JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档
- JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock; 因为粒度降低了
在JDK1.5中,synchronized是性能低效的。因为这是一个重量级操作,它对性能大的影响是阻塞的是实现,挂起 线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来了很大的压力
javaSE1.6引入了偏向锁,轻量级锁(自旋锁)后,synchronized和ReentrantLock两者的性能就差不多了
锁可以升级, 但不能降级. 即: 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁是单向的.
偏向锁
: HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得; 偏向锁是四种状态中最乐观的一种锁:从始至终只有一个线程请求某一把锁。
偏向锁的获取
: 当一个线程访问同步块并成功获取到锁时,会在对象头和栈帧中的锁记录字段里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,直接进入
偏性锁的撤销:
偏向锁使用了一种等待竞争出现才释放锁的机制,所以当其他线程竞争偏向锁时,持有偏向锁的线程才会释放偏向锁,并将锁膨胀为轻量级锁(持有偏向锁的线程依然存活的时候)
多个线程在不同的时间段请求同一把锁,也就是说没有锁竞争。
加锁:
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
解锁:
轻量级锁解锁时, 会使用原子的CAS操作将当前线程的锁记录替换回到对象头, 如果成功, 表示没有竞争发生; 如果失败, 表示当前锁存在竞争, 锁就会膨胀成重量级锁.
Java线程的阻塞以及唤醒,都是依靠操作系统来完成的,这些操作将涉及系统调用,需要从操作系统 的用户态切换至内核态,其开销非常之大。
锁粗化:
锁粗化就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成为一个范围更大的锁
锁消除:
锁消除即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程, 那么可以认为这段代码是线程安全的,不必要加锁
在HotSpot虚拟机中, 对象在内存中的布局分为三块区域: 对象头, 示例数据和对其填充.
对象头中包含两部分: MarkWord 和 类型指针.
多线程下synchronized的加锁就是对同一个对象的对象头中的MarkWord中的变量进行CAS操作
对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm实现,Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放
- 代码块同步: 通过使用
monitorenter
和monitorexit
指令实现的.- 同步方法:
ACC_SYNCHRONIZED
修饰
ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。
- 等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。通过lock.lockInterruptibly()来实现这个机制。
- 公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。
- 锁绑定多个条件,一个ReentrantLock对象可以同时绑定对个对象。ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
如果两个对象相同(即:用 equals 比较返回true),那么它们的 hashCode 值一定要相同
如果两个对象的 hashCode 相同,它们并不一定相同(即:用 equals 比较返回 false
为了提供程序效率 通常会先进性hashcode
的比较,如果不同,则就么有必要equals
比较了;
JAVA 内存泄露详解(原因、例子及解决)
Java中关于内存泄漏出现的原因以及如何避免内存泄漏
我们知道,对象都是有生命周期的,有的长,有的短,如果长生命周期的对象持有短生命周期的引用,就很可能会出现内存泄露
下面给出一个 Java 内存泄漏的典型例子,
Vector v = new Vector(10);
for (int i = 0; i < 100; i++) {
Object o = new Object();
v.add(o);
o = null;
}
在这个例子中,我们循环申请Object对象,并将所申请的对象放入一个 Vector 中,如果我们仅仅释放引用本身,那么 Vector 仍然引用该对象,所以这个对象对 GC 来说是不可回收的。因此,如果对象加入到Vector 后,还必须从 Vector 中删除,最简单的方法就是将 Vector 对象设置为 null。
v = null
ThreadLocal使用不当也可能泄漏
在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写 - 读内存中的公共状态来隐式进行通信。Java 的并发采用的是共享内存模型
线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory)
从 JDK5 开始,java 使用新的 JSR -133 内存模型,提出了 happens-before 的概念
如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在 happens-before 关系
这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间
- 程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。
- 监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。
- volatile 变量规则:对一个 volatile 域的写,happens- before 于任意后续对这个 volatile 域的读。
- 传递性:如果 A happens- before B,且 B happens- before C,那么 A happens- before C。
注意,两个操作之间具有 happens-before 关系,并不意味着前一个操作必须要在后一个操作之前执行!
可见性。对一个 volatile 变量的读,总是能看到(任意线程)对这个 volatile 变量最后的写入。
原子性:对任意单个 volatile 变量的读 / 写具有原子性,但类似于 volatile++ 这种复合操作不具有原子性。
volatile 写的内存语义
:当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量刷新到主内存
volatile 读的内存语义:
当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量
为了实现 volatile 的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
JMM 采取保守策略
在每个 volatile 写操作的前面插入一个 StoreStore 屏障。
在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。
在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。
在每个 volatile 读操作的后面插入一个 LoadStore 屏障。
通过反编译可以看到,有volatile变量修饰的遍历,会有一个lock前缀的指令,lock前缀的指令在多核处理器下会引发了两件事情
将当前处理器缓存行的数据会写回到系统内存。
这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。
主要追对的是 Java堆 和 方法区 ;
java栈、程序计数器、本地方法栈都是线程私有的,线程生就生,线程灭就灭,栈中的栈帧随着方法的结束也会撤销,内存自然就跟着回收了。所以这几个区域的内存分配与回收是确定的,我们不需要管的。但是java堆和方法区则不一样,我们只有在程序运行期间才知道会创建哪些对象,所以这部分内存的分配和回收都是动态的。一般我们所说的垃圾回收也是针对的这一部分。
- 引用计数法 :给一个对象添加引用计数器,每当有个地方引用它,计数器就加1;引用失效就减1。
好了,问题来了,如果我有两个对象A和B,互相引用,除此之外,没有其他任何对象引用它们,实际上这两个对象已经无法访问,即是我们说的垃圾对象。但是互相引用,计数不为0,导致无法回收,所以还有另一种方法:- 可达性分析算法:以根集对象为起始点进行搜索,如果有对象不可达的话,即是垃圾对象。这里的根集一般包括java栈中引用的对象、方法区常良池中引用的对象
- 标记-清除(Mark-sweep):标记清除算法分为两个阶段,标记阶段和清除阶段。标记阶段任务是标记出所有需要回收的对象,清除阶段就是清除被标记对象的空间。
优缺点:实现简单,容易产生内存碎片
- 复制(Copying)将可用内存划分为大小相等的两块,每次只使用其中的一块。当进行垃圾回收的时候了,把其中存活对象全部复制到另外一块中,然后把已使用的内存空间一次清空掉。
优缺点:不容易产生内存碎片;可用内存空间少;存活对象多的话,效率低下。
- 标记-整理(Mark-Compact)
先标记存活对象,然后把存活对象向一边移动,然后清理掉端边界以外的内存
优缺点
:不容易产生内存碎片;内存利用率高;存活对象多并且分散的时候,移动次数多,效率低下- 分代收集算法(目前大部分JVM的垃圾收集器所采用的算法)
年轻代(Young Generation)
的回收算法 (回收主要以Copying为主)
年老代(Old Generation
)的回收算法(回收主要以Mark-Compact为主)
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法区(1.8之后改为元空间)空间不足
(4)创建大对象,比如数组,通过Minor GC后,进入老年代的平均大小大于老年代的可用内存
(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
年轻代:
是所有新对象产生的地方。年轻代被分为3个部分——Enden区和两个Survivor区(From和to)当Eden区被对象填满时,就会执行Minor GC。并把所有存活下来的对象转移到其中一个survivor区(假设为from区)。Minor GC同样会检查存活下来的对象,并把它们转移到另一个survivor区(假设为to区)。这样在一段时间内,总会有一个空的survivor区。经过多次GC周期后,仍然存活下来的对象会被转移到年老代内存空间。通常这是在年轻代有资格提升到年老代前通过设定年龄阈值来完成的。需要注意,Survivor的两个区是对称的,没先后关系,from和to是相对的。
年老代:
在年轻代中经历了N次回收后仍然没有被清除的对象,就会被放到年老代中,可以说他们都是久经沙场而不亡的一代,都是生命周期较长的对象。对于年老代和永久代,就不能再采用像年轻代中那样搬移腾挪的回收算法,因为那些对于这些回收战场上的老兵来说是小儿科。通常会在老年代内存被占满时将会触发Full GC,回收整个堆内存。
持久代:用于存放静态文件,比如java类、方法等。持久代对垃圾回收没有显著的影响
GC里边在JVM当中是使用的ROOT算法,ROOT算法 也就是根; 只要看这个两个对象有没有挂在 根 上, 挂在根上了 就不会被回收; 没有挂在根上就会回收;
- 方法区中的静态属性
- 方法区的中的常量
- 虚拟机中的局部变量
- 本地方法栈中JNI
Cms与G1的优缺点
CMS垃圾回收器:
- 初始标记(CMS-initial-mark) ,会导致STW(stop-the-world);
- 并发标记(CMS-concurrent-mark),与用户线程同时运行
- 预清理(CMS-concurrent-preclean),与用户线程同时运行
- 可被终止的预清理(CMS-concurrent-abortable-preclean) 与用户线程同时运行;
- 重新标记(CMS-remark) ,会导致STW; 这个阶段会导致第二次stop the word,该阶段的任务是完成标记整个年老代的所有的存活对象。
这个阶段,重新标记的内存范围是整个堆,包含_young_gen和_old_gen。为什么要扫描新生代呢,因为对于老年代中的对象,如果被新生代中的对象引用,那么就会被视为存活对象,即使新生代的对象已经不可达了
- 并发清除(CMS-concurrent-sweep),与用户线程同时运行;
CMS垃圾回收器的优化
1.减少重新标记remark阶段停顿
一般CMS的GC耗时 80%都在remark阶段,如果发现remark阶段停顿时间很长,可以尝试添加该参数:-XX:+CMSScavengeBeforeRemark
在执行remark操作之前先做一次ygc,目的在于减少ygen对oldgen的无效引用,降低remark时的开销。
G1垃圾回收器
区别一: 使用范围不一样
CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用
G1收集器收集范围是老年代和新生代。不需要结合其他收集器使用
区别二: STW的时间
CMS收集器以最小的停顿时间为目标的收集器。
G1收集器可预测垃圾回收的停顿时间(建立可预测的停顿时间模型)
区别三: 垃圾碎片
CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片
G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型。类加载和连接的过程都是在运行期间完成的。
1):本地编译好的class中直接加载
2):网络加载:java.net.URLClassLoader可以加载url指定的类
3):从jar、zip等等压缩文件加载类,自动解析jar文件找到class文件去加载util类
4):从java源代码文件动态编译成为class文件
- 类加载的生命周期:加载(Loading)–>验证(Verification)–>准备(Preparation)–>解析(Resolution)–>初始化(Initialization)–>使用(Using)–>卸载(Unloading)
加载
a)加载阶段的工作
i.通过一个类的全限定名来获取定义此类的二进制字节流。
ii.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
iii.在java堆中生成一个代表这个类的java.lang.Class对象,做为方法区这些数据的访问入口。
b)加载阶段完成之后二进制字节流就按照虚拟机所需的格式存储在方区去中
。
验证
这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求。
准备
准备阶段是正式为变量
分配内存并设置初始值
,这些内存都将在方法区中进行分配,这里的变量仅包括类标量不包括实例变量
。
解析
解析是虚拟机将常量池的符号引用替换为直接引用的过程。
初始化
初始化阶段是执行类构造器()方法的过程
a. Bootstrap ClassLoader/启动类加载器
主要负责jdk_home/lib目录下的核心 api 或 -Xbootclasspath 选项指定的jar包装入工作.
b. Extension ClassLoader/扩展类加载器
主要负责jdk_home/lib/ext目录下的jar包或 -Djava.ext.dirs 指定目录下的jar包装入工作
c. System ClassLoader/系统类加载器
主要负责java -classpath/-Djava.class.path所指的目录下的类与jar包装入工作.
d. User Custom ClassLoader/用户自定义类加载器(java.lang.ClassLoader的子类)
在程序运行期间, 通过java.lang.ClassLoader的子类动态加载class文件, 体现java动态实时类装入特性.
JVM在加载类时默认采用的是双亲委派机制, 先往上 让上层加载器去加载
在Java中,一个类用其完全匹配类名(fully qualified class name)作为标识,这里指的完全匹配类名包括包名和类名。但
在JVM中一个类用其全名和一个加载类ClassLoader的实例作为唯一标识,不同类加载器加载的类将被置于不同的命名空间.
所以是不相同的
Class.forName(String name)默认会使用
调用类的类加载器来进行类加载
在不指定父类加载器的情况下,默认采用系统类加载器(AppClassLoader);
- 一般尽量不要覆写已有的loadClass(…)方法中的委派逻辑; 这样做极有可能引起系统默认的类加载器不能正常工作
一 是可以直接调用ClassLoader.getSystemClassLoader()或者其他方式获取到系统类加载器(系统类加载器和扩展类加载器本身都派生自URLClassLoader),调用
URLClassLoader中的getURLs()方法可以获取到
;
二 是可以直接通过获取系统属性java.class.path 来查看当前类路径上的条目信息 ,System.getProperty("java.class.path")
ClassLoader就是遵循双亲委派模型最终调用启动类加载器的类加载器
Class.forName()方法实际上也是调用的CLassLoader来实现的;在这个forName0方法中的第二个参数被默认设置为了true,这个参数代表是否对加载的类进行初始化,设置为true时会类进行初始化,代表会执行类中的静态代码块,以及对静态变量的赋值等操作。
Class.forName 默认会进行初始化,执行静态代码块;有参数可以设置
无法找到目标类
通常加载方式 Class.forName / ClassLoader.loadClass ;
导致原因:1、类名拼写错误或者没有拼写完整类名
2,没有导入相应的jar包
类加载过程有几个阶段
读取:找到.class文件,读取
链接:校验读取的class文件是否符合规范
初始化:载入静态资源 静态块 产生一个Class对象
首先Exception和Error都是继承于Throwable 类,在 Java 中只有 Throwable 类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。
Exception是java程序运行中可预料的异常情况,咱们可以获取到这种异常,并且对这种异常进行业务外的处理。
Error是java程序运行中不可预料的异常情况,这种异常发生以后,会直接导致JVM不可处理或者不可恢复的情况。所以这种异常不可能抓取到,比如OutOfMemoryError、NoClassDefFoundError等。
内存泄漏
是指对象实例在新建和使用完毕后,仍然被引用,没能被垃圾回收释放,一直积累,直到没有剩余内存可用。如果内存泄露,我们要找出泄露的对象是怎么被GC ROOT引用起来,然后通过引用链来具体分析泄露的原因。分析内存泄漏的工具有:Jprofiler,visualvm等。
内存溢出
是指当我们新建一个实力对象时,实例对象所需占用的内存空间大于堆的可用空间。
栈(JVM Stack)存放主要是栈帧( 局部变量表, 操作数栈 , 动态链接 , 方法出口信息 )的地方。注意区分栈和栈帧:栈里包含栈帧。与线程栈相关的内存异常有两个:
a)、StackOverflowError(方法调用层次太深,内存不够新建栈帧)
b)、OutOfMemoryError(线程太多,内存不够新建线程)
如果出现了内存溢出问题,这往往是程序本生需要的内存大于了我们给虚拟机配置的内存,这种情况下,我们可以采用调大-Xmx来解决这种问题
通过jstack分析问题
1、利用top名称查看哪个java进程占用了较多的cpu资源;
2、通过top -Hp pid可以查看该进程下各个线程的cpu使用情况;
3.通过top -Hp命令定位到cpu占用率较高的线程tid之后,继续使用jstack pid命令查看当前java进程的堆栈状态
4.然后将刚刚找到的tid转换成16进制,在 jstack -pid里面的堆栈信息里面找到对应的线程信息
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
主要是Executor的一些方法创建的线程池的对了长度都非常大,容易堆积大量的请求,从而导致OOM
下面是ThreadPoolExecutor最核心的构造方法参数:
1)corePoolSize
核心线程池的大小
2)maximumPoolSize
最大线程池大小,当队列满了 就会创建新线程直至最大
3)keepAliveTime
线程池中超过corePoolSize数目的空闲线程最大存活时间;可以allowCoreThreadTimeOut(true)使得核心线程超出有效时间也关闭
4)TimeUnit keepAliveTime
的时间单位
5)workQueue
阻塞任务队列
6)threadFactory
新建线程工厂,可以自定义工厂
7)RejectedExecutionHandler
当提交任务数超过maximumPoolSize+workQueue之和时,任务会交给RejectedExecutionHandler来处理
重点讲解
corePoolSize,maximumPoolSize,workQueue三者之间的关系
1)当线程池小于corePoolSize时,新提交的任务会创建一个新线程执行任务,即使线程池中仍有空闲线程。
2)当线程池达到corePoolSize时,新提交的任务将被放在workQueue中,等待线程池中的任务执行完毕
3)当workQueue满了,并且maximumPoolSize > corePoolSize时,新提交任务会创建新的线程执行任务
4)当提交任务数超过maximumPoolSize,新任务就交给RejectedExecutionHandler来处理
5)当线程池中超过 corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程
6)当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭
RejectedExecutionHandler拒绝策略
1、
AbortPolicy策略
:该策略会直接抛出异常,阻止系统正常工作;
2、CallerRunsPolicy策略
:如果线程池的线程数量达到上限,该策略会把任务队列中的任务放在调用者线程当中运行;
3、DiscardOledestPolicy策略
:该策略会丢弃任务队列中最老的一个任务,也就是当前任务队列中最先被添加进去的,马上要被执行的那个任务,并尝试再次提交;
4、DiscardPolicy策略
:该策略会默默丢弃无法处理的任务,不予任何处理。当然使用此策略,业务场景中需允许任务的丢失;
也可以自己扩展RejectedExecutionHandler接口
workQueue任务队列
- 直接提交队列:设置为
SynchronousQueue
队列,提交的任务不会被保存,总是会马上提交执行- 有界的任务队列:有界的任务队列可以使用
ArrayBlockingQueue
实现- 无界的任务队列:有界任务队列可以使用
LinkedBlockingQueue
实现- 优先任务队列:优先任务队列通过
PriorityBlockingQueue
实现,它其实是一个特殊的无界队列,PriorityBlockingQueue队列可以自定义规则根据任务的优先级顺序先后执行
内存中value的偏移量
long valueOffset = Unsafe.getUnsafe().objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
通过本地方法
Unsafe.getUnsafe().objectFieldOffset
获取 值 在内存中的偏移量;然后又通过本地方法unsafe.compareAndSwapInt
去更新数据; 如果内存中的值跟期望中的值一样则 修改成update;
如线程1从内存X中取出A,这时候另一个线程2也从内存X中取出A,并且线程2进行了一些操作将内存X中的值变成了B,然后线程2又将内存X中的数据变成A,这时候线程1进行CAS操作发现内存X中仍然是A,然后线程1操作成功。虽然线程1的CAS操作成功,但是整个过程就是有问题的。比如链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化
所以JAVA中提供了AtomicStampedReference/AtomicMarkableReference
来处理会发生ABA问题的场景,主要是在对象中额外再增加一个标记来标识对象是否有过变更
冒泡算法、选择排序、插入排序、希尔排序、归并排序、快速排序
Dubbo缺省协议采用单一长连接和NIO异步通讯
适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况
- client一个线程调用远程接口,生成一个唯一的ID(比如一段随机字符串,UUID等),Dubbo是使用AtomicLong从0开始累计数字的
- 将打包的方法调用信息(如调用的接口名称,方法名称,参数值列表等),和处理结果的回调对象callback,全部封装在一起,组成一个对象object
- 向专门存放调用信息的全局ConcurrentHashMap里面put(ID, object)
- 将ID和打包的方法调用信息封装成一对象connRequest,使用IoSession.write(connRequest)异步发送出去
- 当前线程再使用callback的get()方法试图获取远程返回的结果,在get()内部,则使用synchronized获取回调对象callback的锁, 再先检测是否已经获取到结果,如果没有,然后调用callback的wait()方法,释放callback上的锁,让当前线程处于等待状态。
- 服务端接收到请求并处理后,将结果(此结果中包含了前面的ID,即回传)发送给客户端,客户端socket连接上专门监听消息的线程收到消息,分析结果,取到ID,再从前面的ConcurrentHashMap里面get(ID),从而找到callback,将方法调用结果设置到callback对象里。
- 监听线程接着使用synchronized获取回调对象callback的锁(因为前面调用过wait(),那个线程已释放callback的锁了),再notifyAll(),唤醒前面处于等待状态的线程继续执行(callback的get()方法继续执行就能拿到调用结果了),至此,整个过程结束。
Service 业务层:业务代码的接口与实现
config 配置层:对外配置接口,以 ServiceConfig, ReferenceConfig 为中心,可以直接初始化配置类,也可以通过 Spring 解析配置生成配置类。
proxy 服务代理层:服务接口透明代理
registry 注册中心层:封装服务地址的注册与发现
cluster 路由层:封装多个提供者的路由及负载均衡
monitor 监控层:RPC 调用次数和调用时间监控
能,本地有保存一份数据;
在 Dubbo 的最新版本,默认使用 Netty4 的版本
当然你也可以通过SPI 选择Netty3 Mina Grizzly
【重要】Hessian2 :基于 Hessian 实现的序列化拓展。dubbo:// 协议的默认序列化方案
Dubbo :Dubbo 自己实现的序列化拓展
还有Kryo 、FST、JSON、NativeJava、CompactedJava
Failover Cluster[默认]:
失败自动重试其他服务的策略。
Failover Cluster :
失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries=“2” 来设置重试次数(不含第一次)。
Failfast Cluster:
快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。
Failsafe Cluster:
失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作
Failback Cluster:
失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。
Forking Cluster:
并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks=“2” 来设置最大并行数。
Broadcast Cluster:
广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。
- Dubbo 原生自带的服务降级功能
- 引入支持服务降级的组件 比如 Alibaba Sentinel
- Dubbo 原生自带的限流功能,通过 TpsLimitFilter 实现,仅适用于服务提供者
- 引入支持限流的组件 例如
Sentine
举个栗子,我给大家说个最简单的回答思路:
- 上来你的服务就得
去注册中心注册
吧,你是不是得有个注册中心,保留各个服务的信心,可以用 zookeeper 来做,对吧。- 然后你的消费者需要去
注册中心
拿对应的服务信息吧,对吧,而且每个服务可能会存在于多台机器上。- 接着你就该发起一次请求了,咋发起?当然是基于
动态代理
了,你面向接口获取到一个动态代理,这个动态代理就是接口在本地的一个代理,然后这个代理会找到服务对应的机器地址。- 然后找哪个机器发送请求?那肯定得有个
负载均衡
算法了,比如最简单的可以随机轮询是不是。- 接着找到一台机器,就可以跟它发送请求了,第一个问题咋发送?你可以说用
netty
了,nio 方式;第二个问题发送啥格式数据?你可以说用 hessian序列化协议
了,或者是别的,对吧。然后请求过去了。- 服务器那边一样的,需要针对你自己的服务生成一个动态代理,
监听某个网络端口了
,然后代理你本地的服务代码。接收到请求的时候,就调用对应的服务代码,对吧。
这就是一个最最基本的 rpc 框架的思路,先不说你有多牛逼的技术功底,哪怕这个最简单的思路你先给出来行不行?
这里写链接内容
- 每个事务,会分配全局唯一的递增id(
zxid,64位:epoch + 自增 id
),每次一个leader被选出来,它都会有一 个新的epoch,标识当前属于那个leader的统治时期。低32位用于递增计数
Zookeeper的核心是原子广播,这个机制保证了各个Server之间的同步。实现这个机制的协议叫做Zab协议。Zab协议有两种模式,它们分别是恢复模式(选主)和广播模式(同步)。当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数Server完成了和leader的状态同步以后,恢复模式就结束了。状态同步保证了leader和Server具有相同的系统状态。leader选举是保证分布式数据一致性的关键。
当zk集群中的一台服务器出现以下两种情况之一时,就会开始leader选举。
(1)服务器初始化启动。
(2)服务器运行期间无法和leader保持连接。
而当一台机器进入leader选举流程时,当前集群也可能处于以下两种状态。
(1)集群中本来就已经存在一个leader。
(2)集群中确实不存在leader。
首先第一种情况,通常是集群中某一台机器启动比较晚,在它启动之前,集群已经正常工作,即已经存在一台leader服务器。当该机器试图去选举leader时,会被告知当前服务器的leader信息,它仅仅需要和leader机器建立连接,并进行状态同步即可。
开始选举
sid:
即server id,用来标识该机器在集群中的机器序号。
zxid:
即zookeeper事务id号。
ZooKeeper状态的每一次改变, 都对应着一个递增的Transaction id,,该id称为zxid.,由于zxid的递增性质, 如果zxid1小于zxid2,,那么zxid1肯定先于zxid2发生。创建任意节点,或者更新任意节点的数据, 或者删除任意节点,都会导致Zookeeper状态发生改变,从而导致zxid的值增加。
以(sid,zxid)的形式来标识一次投票信息。
(1)初始阶段,都会给自己投票。
(2)当接收到来自其他服务器的投票时,都需要将别人的投票和自己的投票进行pk,规则如下:
优先检查zxid。zxid比较大的服务器优先作为leader。如果zxid相同的话,就比较sid,sid比较大的服务器作为leader。
客户端watcher 可以监控节点的数据变化以及它子节点的变化,一旦这些状态发生变化,zooKeeper服务端就会通知所有在这个节点上设置过watcher的客户端 ,从而每个客户端都很快感知,它所监听的节点状态发生变化,而做出对应的逻辑处理。
watch对节点的监听事件是一次性的
- 数据发布与订阅
- 命名服务:作为分布式命名服务,命名服务是指通过指定的名字来获取资源或者服务的地址,利用ZooKeeper创建一个全局的路径,这个路径就可以作为一个名字,指向集群中的集群,提供的服务的地址,或者一个远程的对象等等。
- 配置管理
- 集群管理: 所谓集群管理就是:是否有机器退出和加入、选举master。
- 分布式锁
- 分布式队列:
生产者通过在queue节点下创建顺序节点来存放数据,消费者通过读取顺序节点来消费数据。
- 创建临时顺序节点
- 判断自己是不是最小值,是则获取了锁
- 用watch自己前面的一个节点;如果前面的节点删除了,则节点收到通知之后,立马判断自己是不是最小的节点,如果是则获取锁;如果不是,则watch它前面的一个节点
每个watch只会通知一次,锁具有顺序性,并且watch自己前面的一个节点是为了避免羊群效应
家所熟知的 Redis 确实是单线程模型,
指的是执行 Redis 命令的核心模块是单线程的
,而不是整个 Redis 实例就一个线程,Redis 其他模块还有各自模块的线程的。
Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。
因为文件事件分派器队列的消费是单线程的
`,所以Redis才叫单线程模型。
一般来说 Redis 的瓶颈并不在 CPU,而在内存和网络。如果要使用 CPU 多核,可以搭建多个 Redis 实例来解决。
其实,Redis 4.0 开始就有多线程的概念了,比如 Redis 通过多线程方式在后台删除对象
、以及通过 Redis 模块实现的阻塞命令等。
内存不够的话,可以加内存或者做数据结构优化和其他优化等
但网络的性能优化才是大头
,网络 IO 的读写在 Redis 整个执行期间占用了大部分的 CPU 时间,如果把网络处理这部分做成多线程处理方式,那对整个 Redis 的性能会有很大的提升。Redis 的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程
- 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。
- 数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的;
- 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;(但是redis6.0已经开始使用多线程了,不过是在网络层面)
- 使用多路 I/O 复用模型,非阻塞 IO;
Redis 提供两种持久化机制 RDB(默认) 和 AOF 机制:
数据结构丰富,除了支持string类型的value外还支持hash、set、zset、list等数据结构。
RDB:
是Redis DataBase缩写快照,RDB是Redis默认的持久化方式。按照一定的时间将内存的数据以快照的形式保存到硬盘中,对应产生的数据文件为dump.rdb。通过配置文件中的save参数来定义快照的周期。
优点:
- 只有一个文件 dump.rdb,方便持久化。
- 容灾性好,一个文件可以保存到安全的磁盘。
- 性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO 最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 redis 的高性能
- 相对于数据集大时,比 AOF 的启动效率更高。
缺点:- 数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候)
AOF:持久化
是将Redis执行的每次写命令记录到单独的日志文件中,当重启Redis会重新将持久化的日志中文件恢复数据;当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复。
优点:
- 数据安全,aof 持久化可以配置 appendfsync 属性,有 always,每进行一次 命令操作就记录到 aof 文件中一次。
- 通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。
- AOF 机制的 rewrite 模式。AOF 文件没被 rewrite 之前(文件过大时会对命令 进行合并重写),可以删除其中的某些命令(比如误操作的 flushall))
缺点:
- AOF 文件比 RDB 文件大,且恢复速度慢。
- 数据集大的时候,比 rdb 启动效率低。
- 如果需要达到很高的数据安全性,应该同时使用两种持久化功能。在这种情况下,当 Redis 重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。
- 如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失,那么你可以只使用RDB持久化。
- 有很多用户都只使用AOF持久化,但并不推荐这种方式,因为定时生成RDB快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比AOF恢复的速度要快,除此之外,使用RDB还可以避免AOF程序的bug。
如果Redis被当做缓存使用,
使用一致性哈希实现动态扩容缩容。
如果Redis被当做一个持久化存储使用,必须使用固定的keys-to-nodes映射关系,节点的数量一旦确定不能变化。否则的话(即Redis节点需要动态变化的情况),必须使用可以在运行时进行数据再平衡的一套系统,而当前只有Redis集群可以做到这样。
惰性删除:
惰性删除不会去主动删除数据,而是在访问数据的时候,再检查当前键值是否过期,如果过期则执行删除并返回 null 给客户端,如果没有过期则返回正常信息给客户端。定期删除:
Redis会周期性的随机测试一批设置了过期时间的key并进行处理。测试到的已过期的key将被删除。
如果一个数据在最近没有被访问到,那么在未来被访问的可能性也很小,因此当空间满的时候,最久没有被访问的数据最先被置换(淘汰)
LRU算法通常通过双向链表来实现,添加元素的时候,直接插入表头,访问元素的时候,先判断元素是否在链表中存在,如果存在就把该元素移动至表头
淘汰的时候 把队尾的一些删掉;
Redis基于Reactor模式开发了网络事件处理器
,这个处理器被称为文件事件处理器(file event handler)。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。
因为文件事件分派器队列的消费是单线程的
,所以Redis才叫单线程模型
Redis Cluster是一种服务端Sharding技术,3.0版本开始正式提供。Redis Cluster并没有使用一致性hash,而是采用slot(槽)的概念,一共分成
16384个槽
。将请求发送到任意节点,接收到请求的节点会将查询请求发送到正确的节点上执行
方案说明
- 通过哈希的方式,将数据分片,每个节点均分存储一定哈希槽(哈希值)区间的数据,默认分配了16384 个槽位
- 每份数据分片会存储在多个互为主从的多节点上
- 数据写入先写主节点,再同步到从节点(支持配置为阻塞同步)
- 同一分片多个节点间的数据不保持一致性
- 读取数据时,当客户端操作的key没有分配在该节点上时,redis会返回转向指令,指向正确的节点
- 扩容时时需要需要把旧节点的数据迁移一部分到新节点
在 redis cluster 架构下,每个 redis 要放开两个端口号,比如一个是 6379,另外一个就是 加1w 的端口号,比如 16379。 16379 端口号是用来进行节点间通信的,也就是 cluster bus 的东西,cluster bus 的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus 用了另外一种二进制的协议,gossip 协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。
优点:
无中心架构,支持动态扩容,对业务透明
具备Sentinel的监控和自动Failover(故障转移)能力
客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可
高性能,客户端直连redis服务,免去了proxy代理的损耗
缺点:
运维也很复杂,数据迁移需要人工干预
只能使用0号数据库
不支持批量操作(pipeline管道操作)
sentinel,中文名是哨兵。哨兵是 redis 集群机构中非常重要的一个组件,主要有以下功能:
集群监控:负责监控 redis master 和 slave 进程是否正常工作。
消息通知:如果某个 redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员
故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。
配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。
哨兵用于实现 redis 集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。
故障转移时,判断一个 master node 是否宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题。
哨兵至少需要 3 个实例,来保证自己的健壮性。
哨兵 + redis 主从的部署架构,是不保证数据零丢失的
,只能保证 redis 集群的高可用性。
例如开源的:
Twemproxy
Codis
三级缓存
三级 啥都还没有干,给了工厂
singletonFactories : 单例对象工厂的cache
二级,完成了构造函数但是还没有注意依赖
earlySingletonObjects :提前暴光的单例对象的Cache
一级 加载好的bean
singletonObjects:单例对象的cache
看上面
添加链接描述
工厂模式的升级 IOC 依赖注入 控制反转
(1)也许有人说,IoC和工厂模式不是一样的作用吗,用IoC好象还麻烦一点。 举个例子,如果用户需求发生变化,要把Chinese类修改一下。那么前一种工厂模式,就要更改Factory类的方法,并且重新编译布署。而IoC只需 要将class属性改变一下,并且由于IoC利用了Java反射机制,这些对象是动态生成的,这时我们就可以热插拨Chinese对象(不必把原程序停止 下来重新编译布署,java特性 需要重新编译)
(2)也许有人说,即然IoC这么好,那么我把系统所有对象都用IoC方式来生成。 注意,IoC的灵活性是有代价的:设置步骤麻烦、生成对象的方式不直观、反射比正常生成对象在效率上慢一点。因此使用IoC要看有没有必要,我认为比较通用的判断方式是:用到工厂模式的地方都可以考虑用IoC模式。
select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
但select,poll,epoll本质上都是同步I/O
,因为他们都需要在读写事件就绪后自己负责进行读写
,也就是说这个读写过程是阻塞的
,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
select:
时间复杂度O(n),它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部)select具有O(n)的无差别轮询复杂度,
同时处理的流越多,无差别轮询时间就越长。遍历 ;有最大连接数的限制,在FD_SETSIZE宏定义
poll:
时间复杂度O(n),poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.但是它没有最大连接数的限制,原因是它是基于链表来存储的
epoll
时间复杂度O(1),epoll可以理解为event poll,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的 ;虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接
epoll的优点:
1、没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口);
2、效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;
即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。
3. 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。
Java BIO 就是传统的 Java IO 编程,同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不作任何事情会造成不必要的线程开销。
BIO 方式适用于连接数比较小且固定的架构
Java NIO 全称 Java non-blocking IO,NIO
同步非阻塞
有三大核心部分:Channel(管道)、Buffer(缓冲区)、Selector(选择器)
。NIO 以块的方式处理数据
,块 I/O 的效率比流 I/O 高很多
NIO 是面向缓冲区编程的
。数据读取到了一个它稍微处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞的高伸缩性网络
- BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多。
- BIO 是阻塞的,而 NIO 是非阻塞的。
- BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道事件(比如连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。
NIO比传统的BIO核心区别就是,NIO采用的是多路复用的IO模型,普通的IO用的是阻塞的IO模型
在win下用select 在linux下用epoll
- 通道是双向的可以进行读写,而流是单向的只能读,或者写。
- 通道可以实现异步读写数据。
- 通道可以从缓冲区读取数据,也可以写入数据到缓冲区。
DK 7 引入了 Asynchronous I/O,即 AIO。在进行 I/O 编程中,通常用到两种模式:Reactor 和 Proactor 。Java 的 NIO 就是 Reactor,当有事件触发时,服务器端得到通知,进行相应的处理。
AIO 叫做异步非阻塞的 I/O,引入了异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才会启动线程,特点就是先由操作系统完成后才通知服务端程序启动线程去处理,一般用于连接数较多且连接时长较长的应用。
每个客户端发起连接请求都会交给acceptor,acceptor根据事件类型交给线程handler处理,但是由于在同一线程中,容易产生一个handler阻塞影响其他的情况。
这里使用了单线程进行接收客户端的连接,采用了NIO的线程池用来处理客户端对应的IO操作,由于客户端连接较多,有时会一个线程对应处理多个连接。
这里将接收客户端请求后采用线程池进行处理,服务端用于接收客户端连接的不再是个1个单独的NIO线程,而是一个独立的NIO线程池。Acceptor接收到客户端TCP连接请求处理完成后(可能包含接入认证等),将新创建的SocketChannel注册到IO线程池(sub reactor线程池)的某个IO线程上,由它负责SocketChannel的读写和编解码工作。Acceptor线程池仅仅只用于客户端的登陆、握手和安全认证,一旦链路建立成功,就将链路注册到后端subReactor线程池的IO线程上,由IO线程负责后续的IO操作。
产生粘包和拆包问题的主要原因是,
操作系统在发送TCP数据的时候,底层会有一个缓冲区
,例如1024个字节大小,如果一次请求发送的数据量比较小,没达到缓冲区大小,TCP则会将多个请求合并为同一个请求进行发送,这就形成了粘包问题;如果一次请求发送的数据量比较大,超过了缓冲区大小,TCP就会将其拆分为多次发送,这就是拆包,也就是将一个大的包拆分为多个小包进行发送
- 发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。
- 发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
- 可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。
流量控制、熔断降级、系统负载保护
服务隔离机制:
线程池隔离或者信号量隔离机制
线程池隔离:
每个接口都有自己独立的线程池维护我们的请求,每个线程池互不影响,就是每个接口有独立使用一个线程池,缺点:占用服务器内存非常大
信号量隔离:
设置允许我们的某个接口有一个阈值的线程数量去处理接口,如果超出改线程数量则拒绝访问,有点类似服务限流
数据库更新某条记录为加锁状态, update 锁状态=加锁 from table where 锁状态=没加锁; 返回影响行数=0表示被别人加锁了就不能加了;
- 互斥性。在任意时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
加锁
主要回答
NX
意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;
增加requestId,谁加的锁必须谁解锁
设置过期时间
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
解锁
1.判断是不是自己加的锁
2.是的话删掉锁
3.使用lua实现上面两步骤
保持原子性! 否则可能出现删除别人的锁的情况;
比如: A判断了是自己的锁,然后准备去删除这个锁,突然锁过期了,B这时候成功加锁了,那么A再执行删除操作的时候就会删掉B的锁
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
原子性(Atomicity)
是通过 undo log 来实现的
一致性(Consistency)
是通过 redo log 来实现的
隔离型(Isolation)
是通过 (读写锁+MVCC)来实现的
持久性(Durability)
READ UNCOMMITED (未提交读)
READ COMMITED (提交读)
REPEATABLE READ (可重复读)
SERIALIZABLE (可重复读)
Mysql 默认隔离级别是REPEATABLE READ (可重复读)
; 但是他存在幻读的问题
;也就是读取范围记录的时候,可能有其他事物插入了数据导致读取的不一致;
但是InnoDB
解决了幻读问题; 通过MVCC 多版本并发控制
解决了幻读问题; 具体是通过加锁,Next-Key Lock :行锁和间隙锁组合起来就叫Next-Key Lock。
InnoBD通过在每行的后面包车2个隐藏列实现,一个保存行的创建事件,一个是过期时间(或删除时间),当然存储的并不是时机的时间,而是系统版本号; 每开一个新事务,系统版本号都会自增;
undo log
存在内存中的数据;用于记录数据被修改前的信息,为了在发生错误时回滚之前的操作,需要将之前的操作都记录下来,然后在发生错误时才可以回滚。
REDO LOG
Write Ahead Log(WAL)策略Write Ahead Log(WAL)策略 先写日志
mysql 为了提升性能不会把每次的修改都实时同步到磁盘,而是会先存到Boffer Pool(缓冲池)里头
,把这个当作缓存来用。然后使用后台线程去做缓冲池和磁盘之间的同步
。
那么问题来了,如果还没来的同步的时候宕机或断电了怎么办?
所以引入了redo log来记录已成功提交事务的修改信息,并且会把redo log持久化到磁盘,系统重启之后在读取redo log恢复最新数据。
总结: redo log是用来恢复数据的 用于保障,已提交事务的持久化特性
既然 redo log也要刷盘 为什么不直接刷修改的数据到磁盘呢?
- redo_log 存储的是顺序刷盘,而修改数据的刷盘是随机I/O; 前者更快
- r组提交 Group Commit,redo log 和 binlog 都具有组提交特性,在刷盘时通过等待一段时间来收集多个事务日志同时进行刷盘
行锁(Record Lock):
锁直接加在索引记录上面,锁住的是key。
间隙锁(Gap Lock):
锁定索引记录间隙,确保索引记录的间隙不变。间隙锁是针对事务隔离级别为可重复读或以上级别而已的。
Next-Key Lock :
行锁和间隙锁组合起来就叫Next-Key Lock。
- B+索引
- 哈希索引
- 空间数据索引
- 全文索引
哈希索引: 基于哈希表的实现
哈希索引质保函哈希值和行指针,不存储字段
不是顺序存储,无法排序
访问哈希索引的数据非常快
哈希冲突多的话,一些索引维护操作代价也会很高
InnoDB有一个特殊功能叫 自适应哈希索引
,当InnoDb注意到某些索引值被使用非常频繁,它会在内存中基于B-TREE索引之上在创建一个哈希索引; 完全是自动行为用户无法控制
1.索引列不能是表达式的一部分,也不能是函数的参数
例如: 下面索引会失效,
select a from table where actor_id +1 = 5;
2.前缀索引.当字段里有很长字符串的列(TEXT,长的VARCHER等…),在前几个字符串里加索引,这就是前缀索引。
alter table table_name add key(column_name(length));
为多列字段建立一个索引,称之为联合索引,联合索引需要遵从最左前缀原则
多个单列索引在多条件查询时优化器会选择最优索引策略,可能只用一个索引,也可能将多个索引全用上! 但多个单列索引底层会建立多个B+索引树,比较占用空间,也会浪费一定搜索效率,所以索引建议最好建联合索引
覆盖索引:一个辅助索引包含了查询结果的数据就叫做覆盖索引,即从辅助索引中就可以得到查询结果,而不需要从聚集索引中查询
- 文件很大,不可能全部存储在内存中,故要存储到磁盘上
- 索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数(为什么使用B-/+Tree,还跟磁盘存取原理有关。)
- 局部性原理与磁盘预读,预读的长度一般为页(page)的整倍数,(在许多操作系统中,页得大小通常为4k)
- 数据库系统巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,默认16k,这样每个节点只需要一次I/O就可以完全载入,(由于节点中有两个数组,所以地址连续)。而红黑树这种结构,h明显要深的多。由于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性
- InnoDB的主键索引 ,MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址。而在InnoDB中,表数据文件本身就是按B+Tree组织的一个索引结构,这棵树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引,所以必须有主键,如果没有显示定义,自动为生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整形
- InnoDB的辅助索引(Secondary Index, 也就是非主键索引)也会包含主键列,比如名字建立索引,内部节点 会包含名字,叶子节点会包含该名字对应的主键的值,如果主键定义的比较大,其他索引也将很
- MyISAM引擎使用B+Tree作为索引结构,索引文件叶节点的data域存放的是数据记录的地址,指向数据文件中对应的值,每个节点只有该索引列的值
- MyISAM主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求key是唯一的,辅助索引可以重复
MyISAM
InnoDB
叶子节点都保存着完整的数据
MySQL之 索引下推
索引条件下推(Index Condition Pushdown),简称ICP。MySQL5.6新添加,用于优化数据的查询。
当你不使用ICP,通过使用非主键索引(普通索引or二级索引)进行查询,存储引擎通过索引检索数据,然后返回给MySQL服务器,服务器再判断是否符合条件。
使用ICP,当存在索引的列做为判断条件时,MySQL服务器将这一部分判断条件传递给存储引擎,然后存储引擎通过判断索引是否符合MySQL服务器传递的条件,只有当索引符合条件时才会将数据检索出来返回给MySQL服务器。
分区:
一张大表进行分区后,他还是一张表,不会变成二张表,但是他存放数据的区块变多了;突破磁盘I/O瓶颈,想提高磁盘的读写能力
分表:
多个表;单表的并发能力提高了,磁盘I/O性能也提高了
分区和分表的测重点不同,分表重点是存取数据时,如何提高mysql并发能力上;而分区呢,如何突破磁盘的读写能力,从而达到提高mysql性能的目的。
分库:
单机性能不够,分成多个库提升性能;
分表分库:
垂直切分,即将表按照功能模块、关系密切程度划分出来,部署到不同的库上水平切分,当一个表中的数据量过大时,我们可以把该表的数据按照某种规则
其实也没有什么,我在看Nacos源码的时候,里面有一个类
TaskManager
; 有一个提供者-消费者模型 有bug;我给顺手改掉了;
就拿
多版本并行开发解决方案
这个项目来说; 这这里碰到的问题还挺多的
- dubbo的服务路由
- kafka的隔离问题;这个需要对kafka很熟悉; 解决这个问题的时候 先想到能不能创建多个
分区
然后每个版本只能消费自己的分区;后来发现不行, 因为一个消息,可能会有 稳定版本的服务消费+ 迭代版本的消费;这个就被pass了;改了改成了通过 kafka的 header传递版本号 详见kafka的服务复用与隔离设计方案- 遇到了ThreadLocal在线程池的情况下会有父子值传递的问题; 后来用的阿里的TTl来解决的;也花了点时间去看他的源码的实现方案
- 如何无侵入的使用上面的jar包也是碰到的一个问题; 我不想把jar包放到仓促里面;然后让每个项目都要去依赖我这个dubbo jar包;那样太不友好了;为了解决这个问题也是找了很久的资料;后来是通过在项目打包后 启动之前把这个jar包放到启动包里面;如果是war包;就直接把
jar
放到lib文件夹;如果是 jar包则 通过jar 的-uf0
先解压包再放进去 再重新打包;我写的dubbo扩展jar包如何无侵入的给别人使用- 还有就是 我们项目中用的是自研的网关;每个项目启动的时候都会去注册;那我们这个不是多版本吗;需要注册的时候也要把版本号注册到网关中去, 那不可能让每个项目都去改注册的应用名吧?所以0侵入;我用
java-agent
来给注册的应用名加上了版本号;最终使用的时候,程序中是0感知的;
有序集合 ZSET 用命令 ZREMRANGEBYSCORE key min max
线程不停的去循环查询score;
确定
1.浪费资源 性能
2.可靠性得不到保障
3.也不支持消息删除
等等…
优点
1.BLPOP 阻塞的消费,不会一直循环浪费资源
2. 高可用性:支持单机,支持集群
3. 支持消息删除:业务费随时删除指定消息
4. 实时性: 允许存在一定时间内的秒级误差
缺点
- 存在消息消费失败的可能, 当消息消费失败,重试的时候,redis挂了;重试若干次之后会打印 异常日志;人工处理
- 使用kill-9的时候可能会有部分消息未被消费
TABLE 存放 具体数据结构
LIST 存放等待消费的队列
ZSET 存放延时消息的时间戳
- 搬运线程去将 ZSET的到期数据移动到 对应的TOPIC 的LIST中 一个topic一个list
- 每个topic的List都会有一个监听线程去批量获取List中的待消费数据;获取到的数据全部扔给这个Topic的消费线程池
- 消息线程池执行会去Redis_Delay_Table查找数据结构,返回给回调接口,执行回调方法;
以上所有操作,都是基于Lua脚本做的操作,Lua脚本执行的优点在于,批量命令执行具有原子性,事务性, 并且降低了网络开销,毕竟只有一次网络开销;
避免资源浪费,一直循环空转; BLPOP
但是也不能一直不停的去执行操作,如果list已经没有数据了去操作也没有任何意义,不然就太浪费资源了,幸好List中有一个BLPOP阻塞原语,如果list中有数据就会立马返回,如果没有数据就会一直阻塞在那里,直到有数据返回,可以设置阻塞的超时时间,超时会返回NULL;
第一次去获取N个待消费的任务扔进到消费线程池中;如果获取到了0个,那么我们就立马用BLPOP来阻塞,等有元素的时候 BLPOP就返回数据了,下次就可以尝试去LrangeAndLTrim一次了. 通过BLPOP阻塞,我们避免了频繁的去请求redis,并且更重要的是提高了实时性;
批量获取的待消费数量是多少?有什么讲究 (Semaphore信号量)避免线程池来不及消费 宕机丢失任务风险
执行上面的一次获取N个元素是不定的,这个要看线程池的maxPoolSize 最大线程数量; 因为避免消费的任务过多而放入线程池的阻塞队列, 放入阻塞队列有宕机丢失任务的风险,关机重启的时候还要讲阻塞队列中的任务重新放入List中增加了复杂性;
所以我们每次LrangeAndLTrim获取的元素不能大于当前线程池可用的线程数; 这样的一个控制可用用信号量Semaphore来做
搬运线程的处理 总不能一直循环的去搬运吧
机器记录最近的一个需要搬运的score ;到了时间自动唤醒 搬运线程;如果没有需要搬运的就睡眠 ;
- 用
update 库存=库存-1 from 表 where 库存>0;
判断是否修改成功,修改成功表示有库存,否则就没有; 因为update语句行级锁,一个一个执行; 然后程序判断修改的影响杭州是否=1;则往下走;不是则回滚; 这样就保证了肯定不会超卖;这是乐观锁
但是上面的会导致DB过载,所有的流量都去DB了不合适,所以再提出解决方案- 将库存缓存起来; 用redis缓存库存, 为了避免
缓存击穿
,提前预热缓存,为了正常的在redis减库存,使用lua+redis
来执行判断+-1库存的操作
;因为lua原子性;- 流量还是过大, 再加上限流; 然后描述
Sentinel的简单原理
,什么漏桶算法和令牌桶算法
都描述一遍- 防止单用户恶意刷脚本还有线程安全问题(单个用户只能领取一个的判断),加上根据用户id维度加一个
分布式锁
- 如果库存很大的情况下,在DB层面的压力还是会很大;
热点问题
解决方案: 分散热点; 让库存量减小,分散成多个商品; 和分成多个时间段;- 还可以异步化, 非关键操作异步处理
- 静态资源使用CDN进行服务分发
- 服务降级
CPU
内存使用率
磁盘IO
缓存穿透:
缓存和数据库中都没有数据,一直请求;导致DB压力过大;
解决方案
: ①.数据库中也没有的数据,这是可以将k-v对写成k-null,缓存的时机可以设置短一点例如20秒;可以防止请求反复攻击同一个id;缓存击穿:
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力
解决方案
:①.像秒杀场景先缓存预热,别让秒杀一瞬间很多请求穿透到了DB;
②.设置热点数据永不过期缓存雪崩:
在同一时刻缓存中的大部分数据都过期了,导致DB一下子请求过大,导致数据库宕机
解决方案:
①.过期时间设置随机,防止同一时间大量数据过期
漏桶算法:
能强行限制数据的传输速率;水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,如果要让自己的系统不被打垮,用令牌桶
令牌桶算法:
的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。如果保证别人的系统不被打垮,用漏桶算法
限流工具类RateLimiter
零拷贝指的是,从一个存储区域到另一个存储区域的copy任务
没有CPU参与
;
零拷贝通常用于网络文件传输,以减少CPU消耗和内存带宽占用,减少用户空间(用户可以操作的内存缓存区域)与CPU内核空间(CPU可以操作的内存缓存区域及寄存器)的拷贝过程,减少用户上下文(用户状态环境)与CPU内核上下文(CPU内核状态环境)间的切换,提高系统效率
直接内存存取; 我们知道 ,硬件和软件之间的数据传输可以通过使用
DMA
来进行,DMA 进行数据传输的过程中几乎不需要 CPU 参与,这样就可以把 CPU 解放出来去做更多其他的事情; 但是当数据需要在用户地址空间的缓冲区和 Linux 操作系统内核的页缓存之间进行传输的时候,并没有类似 DMA 这种工具可以使用
发生4次空间切换(1、4、5、7),发生4次copy(3、4、5、6),其中有2次CPU(4、5)参与
应用程序跟操作系统共享这个缓冲区(地址映射)
用户空间可以修改数据Memory Mapped Files :简称mmap,简单描述其作用就是:将磁盘文件映射到内存, 用户通过修改内存就能修改磁盘文件。
调用mmap来替代read()来减少拷贝次数
;应用程序调用了 mmap() 之后,数据会先通过 DMA 拷贝到操作系统内核的缓冲区中去。接着,应用程序跟操作系统共享这个缓冲区
,这样,操作系统内核和应用程序存储空间就不需要再进行任何的数据拷贝操作。应用程序调用了 write() 之后,操作系统内核将数据从原来的内核缓冲区中拷贝到与 socket 相关的内核缓冲区中。接下来,数据从内核 socket 缓冲区拷贝到协议引擎中去,这是第三次数据拷贝操作。
缺点: 其他的进程截断
使用 mma()p 其实是存在潜在的问题的。当对文件进行了内存映射,然后调用 write() 系统调用,如果此时
其他的进程截断
了这个文件,那么 write() 系统调用将会被总线错误信号 SIGBUS 中断,因为此时正在执行的是一个错误的存储访问。这个信号将会导致进程被杀死
解决: 可以通过内核对文件加读或者写的租借锁
第二种方法是通过文件租借锁来解决这个问题的,这种方法相对来说更好一些。我们可以通过内核对文件加读或者写的租借锁,当另外一个进程尝试对用户正在进行传输的文件进行截断的时候,内核会发送给用户一个实时信号:RT_SIGNAL_LEASE 信号,这个信号会告诉用户内核破坏了用户加在那个文件上的写或者读租借锁,那么 write() 系统调用则会被中断,并且进程会被 SIGBUS 信号杀死,返回值则是中断前写的字节数,errno 也会被设置为 success。文件租借锁需要在对文件进行内存映射之前设置
不仅减少了数据拷贝操作,它也减少了上下文切换
但是用户空间不可修改数据为了简化用户接口,同时还要继续保留 mmap()/write() 技术的优点:减少 CPU 的拷贝次数,Linux 在版本 2.1 中引入了 sendfile() 这个系统调用。
sendfile() 系统调用不需要将数据拷贝或者映射到应用程序地址空间中去,所以 sendfile() 只是适用于应用程序地址空间不需要对所访问数据进行处理的情况
局限性:
- sendfile() 局限于基于文件服务的网络应用程序,比如 web 服务器。据说,在 Linux 内核中实现 sendfile() 只是为了在其他平台上使用 sendfile() 的 Apache 程序。
- 由于网络传输具有异步性,很难在 sendfile () 系统调用的接收端进行配对的实现方式,所以数据传输的接收端一般没有用到这种技术。
- 基于性能的考虑来说,sendfile () 仍然需要有一次从文件到 socket 缓冲区的 CPU 拷贝操作,这就导致页缓存有可能会被传输的数据所污染。
不拷贝内容,只拷贝描述符(带地址和偏移量)
为啥叫 收集拷贝? 待传输的数据可以分散在存储的不同位置上,而不需要在连续存储中存放。这样一来,从文件中读出的数据就根本不需要被拷贝到 socket 缓冲区中去,而只是需要将缓冲区描述符传到网络协议栈中去,之后其在缓冲区中建立起数据包的相关结构,然后通过 DMA 收集拷贝功能将所有的数据结合成一个网络数据包
写时复制是计算机编程中的一种优化策略,它的基本思想是这样的:如果有多个应用程序需要同时访问同一块数据,那么可以为这些应用程序分配指向这块数据的指针,在每一个应用程序看来,它们都拥有这块数据的一份数据拷贝,当其中一个应用程序需要对自己的这份数据拷贝进行修改的时候,就需要将数据真正地拷贝到该应用程序的地址空间中去,也就是说,该应用程序拥有了一份真正的私有数据拷贝,这样做是为了避免该应用程序对这块数据做的更改被其他应用程序看到。这个过程对于应用程序来说是透明的,如果应用程序永远不会对所访问的这块数据进行任何更改,那么就永远不需要将数据拷贝到应用程序自己的地址空间中去。这也是写时复制的最主要的优点。
Linux 中的零拷贝技术
零拷贝
Java NIO,提供了一个 MappedByteBuffer 类可以用来实现内存映射。
FileChannel.transferTo()/transferFrom()。
- 模板模式: 直接做开放平台的时候,要接入很多OTA平台. 每家要求还不一样;入参出参都还不一样 ;基本的操作就是 鉴权 ,业务过滤 ,处理业务,返回参数; 这个就是一个典型的模板模式, 抽象出来方法;让子类去实现自己的方法
- 策略模式: 在叫单之前有很多逻辑校验;因为不同业务类型要校验的不一样; 这里就用上了策略模式,
一场HttpClient调用未关闭流引发的问题
- jps 查询Jvm进程号
- jstack -l 22741 查询线程栈信息
- 通过栈信息可以发现,基本上所有的线程都被阻塞了,都在wait;
- 重点看 MyJobExecutor-" 开头的线程都在wait同一个lock,并且代码发生的地方是 HttpUtil.doGet 方法;
- 查看代码发现好像是没有关闭 流
- netstat -anp | grep 进程号发现很多 CLOSE_WAIT的状态 ;确定是流未关闭
- 然后又发现, 不对,按上面的将应该只影响上面的方法体,为什么其他的job也停止了呢?
- 然后发现 执行job的线程池的丢弃策略为
ThreadPoolExecutor.CallerRunsPolicy()
; 线程阻塞了之后,就会调用主线程去执行;进而阻塞了其他的job
//允许异步执行 Schedule
@EnableAsync
@Component
public class TestSchedule {
private static final Logger LOGGER = LoggerFactory.getLogger(LotterySchedule.class);
// 使用线程池myAsync来执行这个Job
@Async("myAsync")
@Scheduled(cron = "0/1 * * * * ?")
public void testDoGet(){
LOGGER.info("\ntestDoGet:"+Thread.currentThread());
//业务代码:里面调用了 String json = HttpUtil.doGet(url);来调用第三方接口
HttpUtil.doGet("www.baidu.com")
}
//这里没有用异步执行,单线程执行
@Scheduled(cron = "0/1 * * * * ?")
public void testPrint(){
LOGGER.info("\ntestPrint:"+Thread.currentThread());
}
}
@Configuration
@EnableAsync
public class ExecutorConfig {
/** Set the ThreadPoolExecutor's core pool size. */
private int corePoolSize = 10;
/** Set the ThreadPoolExecutor's maximum pool size. */
private int maxPoolSize = 25;
/** Set the capacity for the ThreadPoolExecutor's BlockingQueue. */
private int queueCapacity = 10;
@Bean
public Executor myAsync() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setThreadNamePrefix("MyJobExecutor-");
// rejection-policy:当pool已经达到max size的时候,如何处理新任务
// CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}