有些图片没有显示,可以在百度网盘里边找到
链接:https://pan.baidu.com/s/1KnQHMjvwF41M4zmImUI8iQ
提取码:ltbu
小米
B+树
结构上
(1)B树中关键字集合分布在整棵树中,叶节点中不包含任何关键字信息,而B+树关键字集合分布在叶子结点中,非叶节点只是叶子结点中关键字的索引;
(2)B树中任何一个关键字只出现在一个结点中,而B+树中的关键字必须出现在叶节点中,也可能在非叶结点中重复出现;
性能上(也即为什么说B+树比B树更适合实际应用中操作系统的文件索引和数据库索引?)
(1)不同于B树只适合随机检索,B+树同时支持随机检索和顺序检索;
(2)B+树的磁盘读写代价更低。B+树的内部结点并没有指向关键字具体信息的指针,其内部结点比B树小,盘块能容纳的结点中关键字数量更多,一次性读入内存中可以查找的关键字也就越多,相对的,IO读写次数也就降低了。而IO读写次数是影响索引检索效率的最大因素。
(3)B+树的查询效率更加稳定。B树搜索有可能会在非叶子结点结束,越靠近根节点的记录查找时间越短,只要找到关键字即可确定记录的存在,其性能等价于在关键字全集内做一次二分查找。而在B+树中,顺序检索比较明显,随机检索时,任何关键字的查找都必须走一条从根节点到叶节点的路,所有关键字的查找路径长度相同,导致每一个关键字的查询效率相当。
(4)(数据库索引采用B+树的主要原因是,)B-树在提高了磁盘IO性能的同时并没有解决元素遍历的效率低下的问题。B+树的叶子节点使用指针顺序连接在一起,只要遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作(或者说效率太低)。
单例模式有几种各出现什么问题,缺点,优点
单例模式:单例模式的意思就是只有一个实例。单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类。
单例模式有三种:懒汉式单例,饿汉式单例,登记式单例。
(1)懒汉式单例
优点:第一次调用才初始化,避免内存浪费。
缺点:必须加锁synchronized 才能保证单例,(如果两个线程同时调用getInstance方法,会chuxia)但加锁会影响效率。
(2)饿汉式单例
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。
(3)登记式模式(holder)
内部类只有在外部类被调用才加载,产生SINGLETON实例;又不用加锁。此模式有上述两个模式的优点,屏蔽了它们的缺点,是最好的单例模式。
阿里
hashmap底层原理实现
概要:
HashMap在JDK1.8之前的实现方式 数组+链表,但是在JDK1.8后对HashMap进行了底层优化,改为了由 数组+链表+红黑树实现,主要的目的是提高查找效率。
如图所示:
JDK版本 实现方式 节点数>=8 节点数<=6
1.8以前 数组+单向链表 数组+单向链表 数组+单向链表
1.8以后 数组+单向链表+红黑树 数组+红黑树 数组+单向链表
HashMap
1.继承关系
public class HashMap
implements Map
2.常量&构造方法
//这两个是限定值 当节点数大于8时会转为红黑树存储
static final int TREEIFY_THRESHOLD = 8;
//当节点数小于6时会转为单向链表存储
static final int UNTREEIFY_THRESHOLD = 6;
//红黑树最小长度为 64
static final int MIN_TREEIFY_CAPACITY = 64;
//HashMap容量初始大小
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//HashMap容量极限
static final int MAXIMUM_CAPACITY = 1 << 30;
//负载因子默认大小
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//Node是Map.Entry接口的实现类
//在此存储数据的Node数组容量是2次幂
//每一个Node本质都是一个单向链表
transient Node
//HashMap大小,它代表HashMap保存的键值对的多少
transient int size;
//HashMap被改变的次数
transient int modCount;
//下一次HashMap扩容的大小
int threshold;
//存储负载因子的常量
final float loadFactor;
//默认的构造函数
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//指定容量大小
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//指定容量大小和负载因子大小
public HashMap(int initialCapacity, float loadFactor) {
//指定的容量大小不可以小于0,否则将抛出IllegalArgumentException异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//判定指定的容量大小是否大于HashMap的容量极限
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//指定的负载因子不可以小于0或为Null,若判定成立则抛出IllegalArgumentException异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
// 设置“HashMap阈值”,当HashMap中存储数据的数量达到threshold时,就需要将HashMap的容量加倍。
this.threshold = tableSizeFor(initialCapacity);
}
//传入一个Map集合,将Map集合中元素Map.Entry全部添加进HashMap实例中
public HashMap(Map extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
//此构造方法主要实现了Map.putAll()
putMapEntries(m, false);
}
3.Node单向链表的实现
//实现了Map.Entry接口
static class Node
final int hash;
final K key;
V value;
Node
//构造函数
Node(int hash, K key, V value, Node
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
//equals属性对比
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry,?> e = (Map.Entry,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
4.TreeNode红黑树实现
static final class TreeNode
TreeNode
TreeNode
TreeNode
TreeNode
boolean red; //是否是红树
TreeNode(int hash, K key, V val, Node
super(hash, key, val, next);
}
/**
* 根节点的实现
*/
final TreeNode root() {
for (TreeNode r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
...
5.Hash的计算实现
//主要是将传入的参数key本身的hashCode与h无符号右移16位进行二进制异或运算得出一个新的hash值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
延伸讲解
5.1.下面的做了一个例子讲解,经过hash函数计算后得到的key的hash值
hash计算.png
5.2那为什么要这么做呢?直接通过key.hashCode()获取hash不得了吗?为什么在右移16位后进行异或运算?
答案 : 与HashMap的table数组下计算标有关系
我们在下面讲解的put/get函数代码块中都出现了这样一段代码
//put函数代码块中
tab[i = (n - 1) & hash])
//get函数代码块中
tab[(n - 1) & hash])
我们知道这段代码是根据索引得到tab中节点数据,它是如何与hash进行与运算后得到索引位置呢! 假设tab.length()=1<<4
tab下标计算h计算.png
这样做的根本原因是当发生较大碰撞时也用树形存储降低了冲突。既减少了系统的开销
6.HashMap.put的源码实现
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//HashMap.put的具体实现
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node
//判定table不为空并且table长度不可为0,否则将从resize函数中获取
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//这样写法有点绕,其实这里就是通过索引获取table数组中的一个元素看是否为Nul
if ((p = tab[i = (n - 1) & hash]) == null)
//若判断成立,则New一个Node出来赋给table中指定索引下的这个元素
tab[i] = newNode(hash, key, value, null);
else { //若判断不成立
Node
//对这个元素进行Hash和key值匹配
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode) //如果数组中德这个元素P是TreeNode类型
//判定成功则在红黑树中查找符合的条件的节点并返回此节点
e = ((TreeNode
else { //若以上条件均判断失败,则执行以下代码
//向Node单向链表中添加数据
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//若节点数大于等于8
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//转换为红黑树
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e; //p记录下一个节点
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold) //判断是否需要扩容
resize();
afterNodeInsertion(evict);
return null;
}
梳理以下HashMap.put函数的执行过程
1.首先获取Node数组table对象和长度,若table为null或长度为0,则调用resize()扩容方法获取table最新对象,并通过此对象获取长度大小
2.判定数组中指定索引下的节点是否为Null,若为Null 则new出一个单向链表赋给table中索引下的这个节点
3.若判定不为Null,我们的判断再做分支
-3.1 首先对hash和key进行匹配,若判定成功直接赋予e
3.2 若匹配判定失败,则进行类型匹配是否为TreeNode 若判定成功则在红黑树中查找符合条件的节点并将其回传赋给e
3.3 若以上判定全部失败则进行最后操作,向单向链表中添加数据若单向链表的长度大于等于8,则将其转为红黑树保存,记录下一个节点,对e进行判定若成功则返回旧值
4.最后判定数组大小需不需要扩容
7.HashMap.get的源码实现
//这里直接调用getNode函数实现方法
public V get(Object key) {
Node
//经过hash函数运算 获取key的hash值
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node
Node
//判定三个条件 table不为Null & table的长度大于0 & table指定的索引值不为Null
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//判定 匹配hash值 & 匹配key值 成功则返回 该值
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//若 first节点的下一个节点不为Null
if ((e = first.next) != null) {
if (first instanceof TreeNode) //若first的类型为TreeNode 红黑树
//通过红黑树查找匹配值 并返回
return ((TreeNode
//若上面判定不成功 则认为下一个节点为单向链表,通过循环匹配值
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
//匹配成功后返回该值
return e;
} while ((e = e.next) != null);
}
}
return null;
}
梳理以下HashMap.get函数的执行过程
1.判定三个条件 table不为Null & table的长度大于0 & table指定的索引值不为Null
2.判定 匹配hash值 & 匹配key值 成功则返回 该值
3.若 first节点的下一个节点不为Null
3.1 若first的类型为TreeNode 红黑树 通过红黑树查找匹配值 并返回查询值
3.2若上面判定不成功 则认为下一个节点为单向链表,通过循环匹配值
8.HashMap扩容原理分析
//重新设置table大小/扩容 并返回扩容的Node数组即HashMap的最新数据
final Node
Node
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
//判定数组是否已达到极限大小,若判定成功将不再扩容,直接将老表返回
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//若新表大小(oldCap2)小于数组极限大小 并且 老表大于等于数组初始化大小
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//旧数组大小oldThr 经二进制运算向左位移1个位置 即 oldThr2当作新数组的大小
newThr = oldThr << 1; // double threshold
}
//若老表中下次扩容大小oldThr大于0
else if (oldThr > 0)
newCap = oldThr; //将oldThr赋予控制新表大小的newCap
else { //若其他情况则将获取初始默认大小
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//若新表的下表下一次扩容大小为0
if (newThr == 0) {
float ft = (float)newCap * loadFactor; //通过新表大小*负载因子获取
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr; //下次扩容的大小
@SuppressWarnings({“rawtypes”,“unchecked”})
Node
table = newTab; //将当前表赋予table
if (oldTab != null) { //若oldTab中有值需要通过循环将oldTab中的值保存到新表中
for (int j = 0; j < oldCap; ++j) {
Node
if ((e = oldTab[j]) != null) {//获取老表中第j个元素 赋予e
oldTab[j] = null; //并将老表中的元素数据置Null
if (e.next == null) //若此判定成立 则代表e的下面没有节点了
newTab[e.hash & (newCap - 1)] = e; //将e直接存于新表的指定位置
else if (e instanceof TreeNode) //若e是TreeNode类型
//分割树,将新表和旧表分割成两个树,并判断索引处节点的长度是否需要转换成红黑树放入新表存储
((TreeNode
else { // preserve order
Node
Node
Node
//通过Do循环 获取新旧索引的节点
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//通过判定将旧数据和新数据存储到新表指定的位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
//返回新表
return newTab;
}
梳理以下HashMap.resize函数的执行过程
1.判定数组是否已达到极限大小,若判定成功将不再扩容,直接将老表返回
2.若新表大小(oldCap2)小于数组极限大小&老表大于等于数组初始化大小 判定成功则 旧数组大小oldThr 经二进制运算向左位移1个位置 即 oldThr2当作新数组的大小
2.1. 若[2]的判定不成功,则继续判定 oldThr (代表 老表的下一次扩容量)大于0,若判定成功 则将oldThr赋给newCap作为新表的容量
2.2 若 [2] 和[2.1]判定都失败,则走默认赋值 代表 表为初次创建
3.确定下一次表的扩容量, 将新表赋予当前表
4.通过for循环将老表中德值存入扩容后的新表中
4.1 获取旧表中指定索引下的Node对象 赋予e 并将旧表中的索引位置数据置空
4.2 若e的下面没有其他节点则将e直接赋到新表中的索引位置
4.3 若e的类型为TreeNode红黑树类型
4.3.1 分割树,将新表和旧表分割成两个树,并判断索引处节点的长度是否需要转换成红黑树放入新表存储
4.3.2 通过Do循环 不断获取新旧索引的节点
4.3.3 通过判定将旧数据和新数据存储到新表指定的位置
最后返回值为 扩容后的新表。
9.HashMap 的treeifyBin讲解
final void treeifyBin(Node
int n, index; Node
//做判定 tab 为Null 或 tab的长度小于 红黑树最小容量
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
//则通过扩容,扩容table数组大小
resize();
//做判定 若tab索引位置下数据不为空
else if ((e = tab[index = (n - 1) & hash]) != null) {
//定义两个红黑树;分别表示头部节点、尾部节点
TreeNode
//通过循环将单向链表转换为红黑树存储
do {
//将单向链表转换为红黑树
TreeNode
if (tl == null) //若头部节点为Null,则说明该树没有根节点
hd = p;
else {
p.prev = tl; //指向父节点
tl.next = p; //指向下一个节点
}
tl = p; //将当前节点设尾节点
} while ((e = e.next) != null); //若下一个不为Null,则继续遍历
//红黑树转换后,替代原位置上的单项链表
if ((tab[index] = hd) != null)
hd.treeify(tab); // 构建红黑树,以头部节点定为根节点
}
}
TreeNode
return new TreeNode<>(p.hash, p.key, p.value, next);
}
梳理以下HashMap.treeifyBin函数的执行过程
1.做判定 tab 为Null 或 tab的长度小于红黑树最小容量, 判定成功则通过扩容,扩容table数组大小
2.做判定 若tab索引位置下数据不为空,判定成功则通过循环将单向链表转换为红黑树存储
2.1 通过Do循环将当前节点下的单向链表转换为红黑树,若下一个不为Null,则继续遍历
2.2 构建红黑树,以头部节点定为根节点
类加载机制
https://www.cnblogs.com/aspirant/p/7200523.html
Java线程中的锁机制
java包含两种锁机制:synchronized和java.util.concurrent.Lock
Lock接口实现类:ReenTrantLock
synchronized和java.util.concurrent.Lock的区别:
(1)java中的锁是基于对象的,比如线程A拥有了对象A的锁,线程B等待线程A释放对象A的锁,对于高并发的情况下,优先使用Lock。synchronized是java内置的关键字,Lock是一个接口
(2)synchronized会自动释放锁比如:当前拥有锁的线程执行完后会自动释放锁,或者发生异常时,JVM会去释放锁,而Lock不会,使用Lock建议在finally中调用unLock()方法去手动释放锁
(3)synchornized不能让等待锁的线程响应中断,而Lock可以
(4)synchornized不能判断是否获取到了锁,而Lock可以
一、乐观锁与悲观锁
悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
两种锁的使用场景
从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
乐观锁一般会使用版本号机制或CAS算法实现。
说起垃圾回收的场所,了解过JVM(Java Virtual Machine Model)内存模型的朋友应该会很清楚,堆是Java虚拟机进行垃圾回收的主要场所,其次要场所是方法区。
三、GC实现机制-Java虚拟机具体实现流程
我们都知道在Java虚拟机中进行垃圾回收的场所有两个,一个是堆,一个是方法区。在堆中存储了Java程序运行时的所有对象信息,而垃圾回收其实就是对那些“死亡的”对象进行其所侵占的内存的释放,让后续对象再能分配到内存,从而完成程序运行的需要。关于何种对象为死亡对象,在下一部分将做详细介绍。Java虚拟机将堆内存进行了“分块处理”,从广义上讲,在堆中进行垃圾回收分为新生代(Young Generation)和老生代(Old Generation);从细微之处来看,为了提高Java虚拟机进行垃圾回收的效率,又将新生代分成了三个独立的区域(这里的独立区域只是一个相对的概念,并不是说分成三个区域以后就不再互相联合工作了),分别为:Eden区(Eden Region)、From Survivor区(Form Survivor Region)以及To Survivor(To Survivor Region),而Eden区分配的内存较大,其他两个区较小,每次使用Eden和其中一块Survivor。Java虚拟机在进行垃圾回收时,将Eden和Survivor中还存活着的对象进行一次性地复制到另一块Survivor空间上,直到其两个区域中对象被回收完成,当Survivor空间不够用时,需要依赖其他老年代的内存进行分配担保。当另外一块Survivor中没有足够的空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老生代,在老生代中不仅存放着这一种类型的对象,还存放着大对象(需要很多连续的内存的对象),当Java程序运行时,如果遇到大对象将会被直接存放到老生代中,长期存活的对象也会直接进入老年代。如果老生代的空间也被占满,当来自新生代的对象再次请求进入老生代时就会报OutOfMemory异常。新生代中的垃圾回收频率高,且回收的速度也较快。就GC回收机制而言,JVM内存模型中的方法区更被人们倾向的称为永久代(Perm Generation),保存在永久代中的对象一般不会被回收。其永久代进行垃圾回收的频率就较低,速度也较慢。永久代的垃圾收集主要回收废弃常量和无用类。以String常量abc为例,当我们声明了此常量,那么它就会被放到运行时常量池中,如果在常量池中没有任何对象对abc进行引用,那么abc这个常量就算是废弃常量而被回收;判断一个类是否“无用”,则需同时满足三个条件:
(1)、该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
(2)、加载该类的ClassLoader已经被回收
(3)、该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足上述3个条件的无用类进行回收,这里说的是可以回收而不是必然回收。
大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC;同理,当老年代中没有足够的内存空间来存放对象时,虚拟机会发起一次Major GC/Full GC。只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full CG。
虚拟机通过一个对象年龄计数器来判定哪些对象放在新生代,哪些对象应该放在老生代。如果对象在Eden出生并经过一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将该对象的年龄设为1。对象每在Survivor中熬过一次Minor GC,年龄就增加1岁,当他的年龄增加到最大值15时,就将会被晋升到老年代中。虚拟机并不是永远地要求对象的年龄必须达到MaxTenuringThreshold才能晋升到老年代,如果在Survivor空间中所有相同年龄的对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。
四、GC实现机制-Java虚拟机如何实现垃圾回收机制
(1)、引用计数算法(Reference Counting)
给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的,这就是引用计数算法的核心。客观来讲,引用计数算法实现简单,判定效率也很高,在大部分情况下都是一个不错的算法。但是Java虚拟机并没有采用这个算法来判断何种对象为死亡对象,因为它很难解决对象之间相互循环引用的问题。
public class ReferenceCountingGC{
public Object object = null;
private static final int OenM = 1024 * 1024;
private byte[] bigSize = new byte[2 * OneM];
public static void testCG(){
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.object = null;
objB.object = null;
System.gc();
}
}
在上述代码段中,objA与objB互相循环引用,没有结束循环的判断条件,运行结果显示Full GC,就说明当Java虚拟机并不是使用引用计数算法来判断对象是否存活的。
(2)、可达性分析算法(Reachability Analysis)
这是Java虚拟机采用的判定对象是否存活的算法。通过一系列的称为“GC Roots"的对象作为起始点,从这些结点开始向下搜索,搜索所走过的路径称为饮用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。可作为GC Roots的对象包括:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象。本地方法栈JNI引用的对象。
在上图可以看到GC Roots左边的对象都有引用链相关联,所以他们不是死亡对象,而在GCRoots右边有几个零散的对象没有引用链相关联,所以他们就会别Java虚拟机判定为死亡对象而被回收。
五、GC实现机制-何为死亡对象?
Java虚拟机在进行死亡对象判定时,会经历两个过程。如果对象在进行可达性分析后没有与GC Roots相关联的引用链,则该对象会被JVM进行第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法,如果当前对象没有覆盖该方法,或者finalize方法已经被JVM调用过都会被虚拟机判定为“没有必要执行”。如果该对象被判定为没有必要执行,那么该对象将会被放置在一个叫做F-Queue的队列当中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它,在执行过程中JVM可能不会等待该线程执行完毕,因为如果一个对象在finalize方法中执行缓慢,或者发生死循环,将很有可能导致F-Queue队列中其他对象永久处于等待状态,甚至导致整个内存回收系统崩溃。如果在finalize方法中该对象重新与引用链上的任何一个对象建立了关联,即该对象连上了任何一个对象的引用链,例如this关键字,那么该对象就会逃脱垃圾回收系统;如果该对象在finalize方法中没有与任何一个对象进行关联操作,那么该对象会被虚拟机进行第二次标记,该对象就会被垃圾回收系统回收。值得注意的是finaliza方法JVM系统只会自动调用一次,如果对象面临下一次回收,它的finalize方法不会被再次执行。
六、再探GC实现机制-垃圾收集算法
(1)、标记-清楚算法(Mark-Sweep)
用在老生代中, 先对对象进行标记,然后清楚。标记过程就是第五部分提到的标记过程。值得注意的是,使用该算法清楚过后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
(2)、复制算法(Copying)
用在新生代中,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另外一块上,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可。
七、空间分配担保策略-GC过程中的内存担保机制
当出现大量对象在Minor GC后仍然存活的情况,就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。与生活中的银行贷款类似,老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会存活下来在实际完后才能内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。如果出现担保失败,就只好重新发起一次Full GC来进行内存的分配。
Java中的动态代理
代理模式是设计模式中非常重要的一种类型,而设计模式又是编程中非常重要的知识点,特别是在业务系统的重构中,更是有举足轻重的地位。代理模式从类型上来说,可以分为静态代理和动态代理两种类型。
今天我将用非常简单易懂的例子向大家介绍动态代理的两种类型,接着重点介绍动态代理的两种实现方式(Java 动态代理和 CGLib 动态代理),最后深入剖析这两种实现方式的异同,最后说说动态代理在我们周边框架中的应用。
在开始之前,我们先假设这样一个场景:有一个蛋糕店,它们都是使用蛋糕机来做蛋糕的,而且不同种类的蛋糕由不同的蛋糕机来做,这样就有:水果蛋糕机、巧克力蛋糕机等。这个场景用 Java 语言描述就是下面这样:
//做蛋糕的机器
public interface CakeMachine{
void makeCake();
}
//专门做水果蛋糕的机器
class FruitCakeMachine implements CakeMachine{
public void makeCake() {
System.out.println(“Making a fruit cake…”);
}
}
//专门做巧克力蛋糕的机器
public class ChocolateCakeMachine implements CakeMachine{
public void makeCake() {
System.out.printf(“making a Chocolate Cake…”);
}
}
//蛋糕店
public class CakeShop {
public static void main(String[] args) {
new FruitCakeMachine().makeCake(); //making a Fruit Cake…
new ChocolateCakeMachine().makeCake(); //making a Chocolate Cake…
}
}
上面的代码抽象出了一个 CakeMachine 接口,有各种蛋糕机(FruitCakeMachine、ChocolateCakeMachine 等)实现了该接口,最后蛋糕店(CakeShop)直接利用这些蛋糕机做蛋糕。
这样的一个例子真实地描述了实际生活中的场景。但生活中的场景往往是复杂多变的,假设这个时候来了一个顾客,他想要一个水果蛋糕,但他特别喜欢杏仁,希望在水果蛋糕上加上一层杏仁。这时候我们应该怎么做呢?
因为我们的蛋糕机只能做水果蛋糕(程序设定好了),没办法做杏仁水果蛋糕。最简单的办法是直接修改水果蛋糕机的程序,做一台能做杏仁水果蛋糕的蛋糕机。这种方式对应的代码修改也很简单,直接在原来的代码上进行修改,生成一台专门做杏仁水果蛋糕的机器就好了,修改后的 FruitCakeMachien 类应该是这样子:
//专门做水果蛋糕的机器,并且加上一层杏仁
class FruitCakeMachine implements CakeMachine{
public void makeCake() {
System.out.println(“making a Fruit Cake…”);
System.out.println(“adding apricot…”);
}
}
虽然上面这种方式实现了我们的业务需求。但是仔细想一想,在现实生活中如果我们遇到这样的一个需求,我们不可能因为一个顾客的特殊需求就去修改一台蛋糕机的硬件程序,这样成本太高!而且从代码实现角度上来说,这种方式从代码上不是很优雅,修改了原来的代码。根据代码圈中「对修改封闭、对扩展开放」的思想,我们在尝试满足新的业务需求的时候应该尽量少修改原来的代码,而是在原来的代码上进行拓展。
那我们究竟应该怎么做更加合适一些呢?我们肯定是直接用水果蛋糕机做一个蛋糕,然后再人工撒上一层杏仁啦。这其实就对应了即使模式中的代理模式,在这个业务场景中,服务员(代理人)跟顾客说没问题,可以做水果杏仁蛋糕,于是服务员充当了一个代理的角色,先让水果蛋糕机做出了水果蛋糕,之后再往上面撒了一层杏仁。在这个例子中,实际做事情的还是水果蛋糕机,服务员(撒杏仁的人)只是充当了一个代理的角色。
下面我们就来试着实现这样一个代理模式的设计。我们需要做的,其实就是设计一个代理类(FruitCakeMachineProxy),这个代理类就相当于那个撒上一层杏仁的人,之后让蛋糕店直接调用即可代理类去实现即可。
//水果蛋糕机代理
public class FruitCakeMachineProxy implements CakeMachine{
private CakeMachine cakeMachine;
public FruitCakeMachineProxy(CakeMachine cakeMachine) {
this.cakeMachine = cakeMachine;
}
public void makeCake() {
cakeMachine.makeCake();
System.out.println(“adding apricot…”);
}
}
//蛋糕店
public class CakeShop {
public static void main(String[] args) {
FruitCakeMachine fruitCakeMachine = new FruitCakeMachine();
FruitCakeMachineProxy fruitCakeMachineProxy = new FruitCakeMachineProxy(fruitCakeMachine);
fruitCakeMachineProxy.makeCake(); //making a Fruit Cake… adding apricot…
}
}
通过代理实现这样的业务场景,这样我们就不需要在原来的类上进行修改,从而使得代码更加优雅,拓展性更强。如果下次客人喜欢葡萄干水果蛋糕了了,那可以再写一个 CurrantCakeMachineProxy 类来撒上一层葡萄干,原来的代码也不会被修改。上面说的这种业务场景就是代理模式的实际应用,准确地说这种是静态代理。
业务场景的复杂度往往千变万化,如果有另外一个客人,他也想在巧克力蛋糕上撒一层杏仁,那我们岂不是也要再写一个代理类让他做同样的一件事情。如果有客人想在抹茶蛋糕上撒一层杏仁,有客人想在五仁蛋糕上撒一层杏仁……那我们岂不是要写无数个代理类?
其实在 Java 中早已经有了针对这种情况而设计的一个接口,专门用来解决类似的问题,它就是动态代理 —— InvocationHandler。
动态代理与静态代理的区别是静态代理只能针对特定一种类型(某种蛋糕机)做某种代理动作(撒杏仁),而动态代理则可以对所有类型(所有蛋糕机)做某种代理动作(撒杏仁)。
接下来我们针对这个业务场景做一个代码的抽象实现。首先我们分析一下可以知道这种场景的共同点是希望在各种蛋糕上都做「撒一层杏仁」的动作,所以我们就做一个杏仁动态代理(ApricotHandler)。
//杏仁动态代理
public class ApricotHandler implements InvocationHandler{
private Object object;
public ApricotHandler(Object object) {
this.object = object;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = method.invoke(object, args); //调用真正的蛋糕机做蛋糕
System.out.println("adding apricot...");
return result;
}
}
撒杏仁的代理写完之后,我们直接让蛋糕店开工:
public class CakeShop {
public static void main(String[] args) {
//水果蛋糕撒一层杏仁
CakeMachine fruitCakeMachine = new FruitCakeMachine();
ApricotHandler fruitCakeApricotHandler = new ApricotHandler(fruitCakeMachine);
CakeMachine fruitCakeProxy = (CakeMachine) Proxy.newProxyInstance(fruitCakeMachine.getClass().getClassLoader(),
fruitCakeMachine.getClass().getInterfaces(), fruitCakeApricotHandler);
fruitCakeProxy.makeCake();
//巧克力蛋糕撒一层杏仁
CakeMachine chocolateCakeMachine = new ChocolateCakeMachine();
ApricotHandler chocolateCakeApricotHandler = new ApricotHandler(chocolateCakeMachine);
CakeMachine chocolateCakeProxy = (CakeMachine) Proxy.newProxyInstance(chocolateCakeMachine.getClass().getClassLoader(),
chocolateCakeMachine.getClass().getInterfaces(), chocolateCakeApricotHandler);
chocolateCakeProxy.makeCake();
}
}
输出结果为:
making a Fruit Cake…
adding apricot…
making a Chocolate Cake…
adding apricot…
从输出结果可以知道,这与我们想要的结果是一致的。与静态代理相比,动态代理具有更加的普适性,能减少更多重复的代码。试想这个场景如果使用静态代理的话,我们需要对每一种类型的蛋糕机都写一个代理类(FruitCakeMachineProxy、ChocolateCakeMachineProxy、MatchaCakeMachineProxy等)。但是如果使用动态代理的话,我们只需要写一个通用的撒杏仁代理类(ApricotHandler)就可以直接完成所有操作了。直接省去了写 FruitCakeMachineProxy、ChocolateCakeMachineProxy、MatchaCakeMachineProxy 的功夫,极大地提高了效率。
看到这里,大家应该清楚为什么有了静态代理之后,还需要有动态代理了吧。静态代理只能针对某一种类型的实现(蛋糕机)进行操作,如果要针对所有类型的实现(所有蛋糕机)都进行同样的操作,那就必须要动态代理出马了。
如何使用动态代理?
参照上面的例子,我们可以知道要实现动态代理需要做两方面的工作。
首先需要新建一个类,并且这个类必须实现 InvocationHandler 接口。
//杏仁动态代理
public class ApricotHandler implements InvocationHandler{
private Object object;
public ApricotHandler(Object object) {
this.object = object;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = method.invoke(object, args); //调用真正的蛋糕机做蛋糕
System.out.println("adding apricot...");
return result;
}
}
在调用的时候使用 Proxy.newProxyInstance() 方法生成代理类。
public class CakeShop {
public static void main(String[] args) {
//水果蛋糕撒一层杏仁
CakeMachine fruitCakeMachine = new FruitCakeMachine();
ApricotHandler fruitCakeApricotHandler = new ApricotHandler(fruitCakeMachine);
CakeMachine fruitCakeProxy = (CakeMachine) Proxy.newProxyInstance(fruitCakeMachine.getClass().getClassLoader(),
fruitCakeMachine.getClass().getInterfaces(), fruitCakeApricotHandler);
fruitCakeProxy.makeCake();
}
最后直接使用生成的代理类调用相关的方法即可。
动态代理的几种实现方式
动态代理其实指的是一种设计模式概念,指的是通过代理来做一些通用的事情,常见的应用有权限系统、日志系统等,都用到了动态代理。
而 Java 动态代理只是动态代理的一种实现方式而已,动态代理还有另外一种实现方式,即 CGLib(Code Generation Library)。
Java 动态代理只能针对实现了接口的类进行拓展,所以细心的朋友会发现我们的代码里有一个叫 MachineCake 的接口。而 CGLib 则没有这个限制,因为 CGLib 是使用继承原有类的方式来实现代理的。
我们还是举个例子来说明 CGLib 是如何实现动态代理的吧。还是前面的例子:我们要做杏仁水果蛋糕、巧克力水果蛋糕、五仁巧克力蛋糕,这时候用代码描述是这样的。
首先我们需要写一个杏仁拦截器类,这个拦截器可以给做好的蛋糕加上杏仁。
public class ApricotInterceptor implements MethodInterceptor {
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
methodProxy.invokeSuper(o, objects);
System.out.println(“adding apricot…”);
return o;
}
}
接着直接让蛋糕店使用 CGLib 提供的工具类做杏仁水果蛋糕:
public class CakeShop {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(FruitCakeMachine.class);
enhancer.setCallback(new ApricotInterceptor());
FruitCakeMachine fruitCakeMachine = (FruitCakeMachine) enhancer.create();
fruitCakeMachine.makeCake();
}
}
上面的 enhancer.setSuperClass() 设置需要增强的类,而 enhancer.setCallback() 则设置需要回调的拦截器,即实现了 MethodInterceptor 接口的类。最后最后使用 enhancer.create() 生成了对应的增强类,最后输出结果为:
making a Fruit Cake…
adding apricot…
和我们预期的一样。如果要做一个杏仁巧克力蛋糕,那么直接让蛋糕店利用ApricotHandler 再做一个就可以了,它们的区别只是传入的增强类不同。
public class CakeShop {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(ChocolateCakeMachine.class);
enhancer.setCallback(new ApricotInterceptor());
ChocolateCakeMachine chocolateCakeMachine = (ChocolateCakeMachine) enhancer.create();
chocolateCakeMachine.makeCake();
}
}
可以看到,这里传入的增强类是 ChocolateCakeMachine,而不是之前的 FruitCakeMachine。
对比 Java 动态代理和 CGLib 动态代理两种实现方式,你会发现 Java 动态代理适合于那些有接口抽象的类代理,而 CGLib 则适合那些没有接口抽象的类代理。
Java动态代理的原理
从上面的例子我们可以知道,Java 动态代理的入口是从 Proxy.newInstance() 方法中开始的,那么我们就从这个方法开始边剖析源码边理解其原理。
其实通过这个方法,Java 替我们生成了一个继承了指定接口(CakeMachine)的代理类(ApricotHandler)实例。从 Proxy.newInstance() 的源码我们可以看到首先调用了 getProxyClass0 方法,该方法返回了一个 Class 实例对象,该实例对象其实就是 ApricotHandler 的 Class 对象。接着获取其构造方法对象,最后生成该 Class 对象的实例。其实这里最主要的是 getProxyClass0() 方法,这里面动态生成了 ApricotHandler 的 Class 对象。下面我们就深入到 getProxyClass0() 方法中去了解这里面做了什么操作。
getProxyClass0() 方法首先是做了一些参数校验,之后从 proxyClassCache 参数中取出 Class 对象。其实 proxyClassCache 是一个 Map 对象,缓存了所有动态创建的 Class 对象。从源码中的注释可以知道,如果从 Map 中取出的对象为空,那么其会调用 ProxyClassFactory 生成对应的 Class 对象。
在 ProxyClassFactory 类的源码中,最终是调用了 ProxyGenerator.genrateProxyClass() 方法生成了对应的 class 字节码文件。
到这里,我们已经把动态代理的 Java 源代码都解析完了,现在思路就很清晰了。Proxy.newProxyInstance(ClassLoader loader,Class>[] interfaces,InvocationHandler h) 方法简单来说执行了以下操作:
1、生成一个实现了参数 interfaces 里所有接口且继承了 Proxy 的代理类的字节码,然后用参数里的 classLoader 加载这个代理类。
2、使用代理类父类的构造函数 Proxy(InvocationHandler h) 来创造一个代理类的实例,将我们自定义的 InvocationHandler 的子类传入。
3、返回这个代理类实例,因为我们构造的代理类实现了 interfaces(也就是我们程序中传入的 fruitCakeMachine.getClass().getInterfaces() 里的所有接口,因此返回的代理类可以强转成 MachineCake 类型来调用接口中定义的方法。
CGLib动态代理的原理
因为 JVM 并不允许在运行时修改原有类,所以所有的动态性都是通过新建类来实现的,上面说到的 Java 动态代理也不例外。所以对于 CGLib 动态代理的原理,其实也是通过动态生成代理类,最后由代理类来完成操作实现的。
对于 CGLib 动态代理的实现,我并没有深入到源码中,而是通过查阅资料了解了其大概的实现原理。
首先,我们在使用的时候通过 enhancer.setSuperclass(FruitCakeMachine.class) 传入了需要增加的类,CGLib 便会生成一个继承了改类的代理类。
接着,我们通过 enhancer.setCallback(new ApricotInterceptor()) 传入了代理类对象,CGLib 通过组装两个类的结构实现一个静态代理,从而达到具体的目的。
而在 CGLib 生成新类的过程中,其使用的是一个名为 ASM 的东西,它对 Java 的 class 文件进行操作、生成新的 class 文件。如果你对 CGLib 的原理感兴趣,不妨看看这篇文章:从兄弟到父子:动态代理在民间是怎么玩的?
动态代理的应用
动态代理在代码界可是有非常重要的意义,我们开发用到的许多框架都使用到了这个概念。我所知道的就有:Spring AOP、Hibernate、Struts 使用到了动态代理。
Spring AOP。Spring 最重要的一个特性是 AOP(Aspect Oriented Programming 面向切面编程),利用 Spring AOP 可以快速地实现权限校验、安全校验等公用操作。而 Spring AOP 的原理则是通过动态代理实现的,默认情况下 Spring AOP 会采用 Java 动态代理实现,而当该类没有对应接口时才会使用 CGLib 动态代理实现。
Hibernate。Hibernate 是一个常用的 ORM 层框架,在获取数据时常用的操作有:get() 和 load() 方法,它们的区别是:get() 方法会直接获取数据,而 load() 方法则会延迟加载,等到用户真的去取数据的时候才利用代理类去读数据库。
Struts。Struts 现在虽然因为其太多 bug 已经被抛弃,但是曾经用过 Struts 的人都知道 Struts 中的拦截器。拦截器有非常强的 AOP 特性,仔细了解之后你会发现 Struts 拦截器其实也是用动态代理实现的。
总结
我们通过蛋糕店的不同业务场景介绍了静态代理和动态代理的应用,接着重点介绍了动态代理两种实现方式(Java 动态代理、CGLib 动态代理)的使用方法及其实现原理,其中还针对 Java 动态代理的源码进行了简单的分析。最后,我们介绍了动态代理在实际上编程中的应用(Spring AOP、Hibernate、Struts)。
https握手机制
在Http工作之前,Web浏览器通过网络和Web服务器建立链连接,该连接是通过Tcp来完成的,该协议和Ip共同组成了Internet,即著名的Tcp/Ip协议族,因此Internet也被称为Tcp/Ip网络,Http是比Tcp更高的应用层协议,一般Tcp接口的端口好是80。
Web浏览器想Web服务器发送请求命令,这个命令中包含:
Web服务器发送响应数据给Web浏览器,这个包含:
然后Web服务器关闭连接。
以上就是基本的http请求。
在这个过程中,http建立连接,Tcp经过了3次握手,下面我们来讲讲具体的3次握手的过程,首先我们来看一张图:
1:客户端发送了一个带有SYN的Tcp报文到服务器,这个三次握手中的开始。表示客户端想要和服务端建立连接。
2:服务端接收到客户端的请求,返回客户端报文,这个报文带有SYN和ACK标志,询问客户端是否准备好。
3:客户端再次响应服务端一个ACK,表示我已经准备好。
那么为什么要三次握手呢,有人说两次握手就好了。的确,为什么呢,这个可以从计算机网络中得到答案,举一个例子:已失效的连接请求报文段,
client发送了第一个连接的请求报文,但是由于网络不好,这个请求没有立即到达服务端,而是在某个网络节点中滞留了,知道某个时间才到达server,本来这已经是一个失效的报文,但是server端接收到这个请求报文后,还是会想client发出确认的报文,表示同意连接。假如不采用三次握手,那么只要server发出确认,新的建立就连接了,但其实这个请求是失效的请求,client是不会理睬server的确认信息,也不会向服务端发送确认的请求,但是server认为新的连接已经建立起来了,并一直等待client发来数据,这样,server的很多资源就没白白浪费掉了,采用三次握手就是为了防止这种情况的发生,server会因为收不到确认的报文,就知道client并没有建立连接。这就是三次握手的作用。
当终止协议的时候,tcp进行了4次握手,那这4次握手有是怎么回事呢?
由于Tcp连接是进行全双工工作的,因此每个方向都必须单独进行关闭,这个原则是当一方完成他的数据发送的时候就发送一个FIN来终止这个方向的连接,收到这个FIN意味着这个方向上没有数据的流动,一个TCP连接在收到这个FIN之后还能发送消息,首先执行关闭的一方进行主动的关闭,而另一方进行被动的关闭。
1:TCP发送一个FIN,用来关闭客户到服务端的连接。
2:服务端收到这个FIN,他发回一个ACK,确认收到序号为收到序号+1,和SYN一样,一个FIN将占用一个序号。
3:服务端发送一个FIN到客户端,服务端关闭客户端的连接。
4:客户端发送ACK报文确认,并将确认的序号+1,这样关闭完成。
那么为什么是4次挥手呢?
可能有人会有疑问,tcp我握手的时候为何ACK和SYN是一起发送。挥手的时候为什么是分开的时候发送呢,原因是TCP的全双工模式,接收到FIN意味着没有数据发送过来了,但是还可以继续发送数据。
3次握手过程的状态:
listener:这个很好理解,就是服务端的某个socket处于监听状态,可以接收连接了。
syn_send:当某个socket执行connect的时候,首先发送SYN报文,因此也进入了SYN_SEND状态,并等待服务端发送过来的报文,syn_send表示客户端已发送SYN报文。
syn_rcvd:这个状态与SYN_SEND状态差不多,表示接收了SYN报文,这个状态是服务器端的socket在建立tcp连接是的三次握手中的一个中间状态,很短暂,当客户端收到ACK报文的时候,表示连接确立,进入established状态。
4次挥手的状态:
FIN_WAIT_1: 这个状态要好好解释一下,其实FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。而这两种状态的区别是:FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET即进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状态,当然在实际的正常情况下,无论对方何种情况下,都应该马上回应ACK报文,所以FIN_WAIT_1状态一般是比较难见到的,而FIN_WAIT_2状态还有时常常可以用netstat看到。(主动方)
FIN_WAIT_2:上面已经详细解释了这种状态,实际上FIN_WAIT_2状态下的SOCKET,表示半连接,也即有一方要求close连接,但另外还告诉对方,我暂时还有点数据需要传送给你(ACK信息),稍后再关闭连接。(主动方)
TIME_WAIT: 表示收到了对方的FIN报文,并发送出了ACK报文,就等2MSL后即可回到CLOSED可用状态了。如果FIN_WAIT_1状态下,收到了对方同时带FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。(主动方)
CLOSING(比较少见): 这种状态比较特殊,实际情况中应该是很少见,属于一种比较罕见的例外状态。正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什么情况下会出现此种情况呢?其实细想一下,也不难得出结论:那就是如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接。
CLOSE_WAIT: 这种状态的含义其实是表示在等待关闭。怎么理解呢?当对方close一个SOCKET后发送FIN报文给自己,你系统毫无疑问地会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,实际上你真正需要考虑的事情是察看你是否还有数据发送给对方,如果没有的话,那么你也就可以close这个
SOCKET,发送FIN报文给对方,也即关闭连接。所以你在CLOSE_WAIT状态下,需要完成的事情是等待你去关闭连接。(被动方)
LAST_ACK: 这个状态还是比较容易好理解的,它是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,也即可以进入到CLOSED可用状态了。(被动方)
CLOSED: 表示连接
HTTP与TCP/IP区别?
TPC/IP协议是传输层协议,主要解决数据如何在网络中传输,而HTTP是应用层协议,主要解决如何包装数据。WEB使用HTTP协议作应用层协议,以封装HTTP 文本信息,然后使用TCP/IP做传输层协议将它发到网络上。
下面的图表试图显示不同的TCP/IP和其他的协议在最初OSI(Open System Interconnect)模型中的位置:
CA证书是什么?
CA(Certificate Authority)是负责管理和签发证书的第三方权威机构,是所有行业和公众都信任的、认可的。
CA证书,就是CA颁发的证书,可用于验证网站是否可信(针对HTTPS)、验证某文件是否可信(是否被篡改)等,也可以用一个证书来证明另一个证书是真实可信,最顶级的证书称为根证书。除了根证书(自己证明自己是可靠),其它证书都要依靠上一级的证书,来证明自己。
HTTP三次握手
HTTP(HyperText Transfer Protocol)超文本传输协议是互联网上应用最为广泛的一种网络协议。由于信息是明文传输,所以被认为是不安全的。而关于HTTP的三次握手,其实就是使用三次TCP握手确认建立一个HTTP连接。
如下图所示,SYN(synchronous)是TCP/IP建立连接时使用的握手信号、Sequence number(序列号)、Acknowledge number(确认号码),三个箭头指向就代表三次握手,完成三次握手,客户端与服务器开始传送数据。
第一次握手:客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
HTTPS握手过程
HTTPS在HTTP的基础上加入了SSL协议,SSL依靠证书来验证服务器的身份,并为浏览器和服务器之间的通信加密。具体是如何进行加密,解密,验证的,且看下图,下面的称为一次握手。
HTTPS和HTTP的区别
(1) https协议需要到ca申请证书或自制证书。
(2)http的信息是明文传输,https则是具有安全性的ssl加密。
(3)http是直接与TCP进行数据传输,而https是经过一层SSL(OSI表示层),用的端口也不一样,前者是80(需要国内备案),后者是443。
(4) http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
注意https加密是在传输层
https报文在被包装成tcp报文的时候完成加密的过程,无论是https的header域也好,body域也罢都是会被加密的。
当使用tcpdump或者wireshark之类的tcp层工具抓包,获取是加密的内容,而如果用应用层抓包,使用Charels(Mac)、Fildder(Windows)抓包工具,那当然看到是明文的。
PS:HTTPS本身就是为了网络的传输安全。
例子,使用wireshark抓包:
http,可以看到抓到是明文的:
https可以看到抓到是密文的:
HTTPS一般使用的加密与HASH算法如下:
非对称加密算法:RSA,DSA/DSS
对称加密算法:AES,RC4,3DES
HASH算法:MD5,SHA1,SHA256
volatile关键字
Volatile关键字的作用
(1)保证内存的可见性
(2)防止指令重排
注意:volatile并不保证原子性
内存可见性:Volatile保证可见性的原理是在每次访问变量时都会进行一次刷新,并且volatile字段的写操作先于读操作。
禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。
平安产险
集合类的方法
1.集合概述
(1)集合的由来
Java是面向对象的语言,而面向对象语言对事物的描述是通过对象体现的,为了方便对多个对象进行操作,我们必须把多个对象进行存储。已有的容器类型有:数组和StringBuffer。但是,StringBuffer的结果是一个字符串,不一定满足我们的要求,所以我们只能选择数组,这就是对象数组。而对象数组又不能适应变化的需求,因为数组的长度是固定的,此时,为了适应变化的需求,Java就提供了集合类。
(2)数组和集合区别
1)长度区别
数组的长度固定;集合长度可变
2)内容不同
数组存储的是同一种类型的元素;集合可以存储不同类型的元素
3)元素的数据类型问题
数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用类型。
(3)集合的分类
存储多个元素会有不同的需求,针对这些不同的需求,Java就提供了不同的集合类。下边列出了常用的集合类别:
Collection概述:它是Collection层次结构中的根接口。Collection表示一组对象,这些对象也称为collection的元素。一些collection允许有重复的元素,而另一些则不允许。一些collection是有序的,而另一些则是无序的。
List概述:它的类型也是接口,称为有序的collection(也称为序列)(注意:这里所说的有序不是说List集合中元素从小到大或者从大到小排列,而是说,元素“出”集合的顺序和“进”集合的顺序是一样的)。使用此接口的用户可以对列表中每个元素的插入位置进行精确地控制。用户可以根据元素的整数索引(在列表中的位置)访问元素,并搜索列表中的元素;与set不同,列表List通常允许重复的元素。
2.集合常用的方法
(1)Collection常用的方法
add(E e) ,添加元素
clear() ,暴力清除集合中所有元素
contains(Object o), 返回值类型:boolean。判断集合是否包含某个元素
isEmpty() ,返回值类型:boolean。如果此集合不包含元素,则返回true。
iterator() 迭代器。返回值类型:Iterator
size() 返回值类型:int。返回集合中的元素数
(2)List集合(列表)特有且常用的方法
添加功能
void add(int index,Object element):在指定位置添加元素
List list = new ArrayList();
list.add(“我”);
list.add(“爱”);
list.add(“你”);
list.add(1,“很”);//没有问题
//list.add(3.“很”)//没有问题
//list.add(10,“很”)//有问题,提示“越界添加”
System.out.println(list);//输出:[我,很,爱,你]//注意:在索引位置1处添加元素,并不是覆盖掉原来位置的元素
获取功能
Object get(int index):获取指定位置的元素
List list = new ArrayList();
list.add(“我”);
list.add(“爱”);
list.add(“你”);
//下面这段代码展示List中特有的遍历方法:应用size()和get()方法
for(int i = 0;i
System.out.println(s);
}
列表迭代器
ListIterator listIterator():List集合特有的迭代器。该迭代器继承了Iterator迭代器,所以,就可以直接使用hasNext()和next()方法。
特有功能:Object previous():获取上一个元素;boolean hasPrevious():判断是否有元素。
注意:ListIterator可以实现逆向遍历,但是必须先正向遍历,才能逆向遍历,所以一般无意义,不使用该方法进行集合的迭代。
下面这段代码展示了ListIterator的一个应用,同时该段代码也是一个关于ListIterator的一个面试题,值得学习
/*需求:有如下集合,请判断该集合里面是否包含“java”这个元素,如果有,就添加一个“love”元素
*
/
List list = new ArrayList();
list.add(“hello”);
list.add(“java”);
list.add(“world”);
/ 1,展示一个会出错的代码
* 出错提示:ConcurrentModificationException:当方法检测到对象的并发修改,但不允许这种修改时,抛出此异常。
* 也就是说:迭代器遍历元素的时候,通过集合是不能修改元素的
*/
Iterator it = list.iterator();
while(it.hasNext()){
String s = (String)it.next();
if(s.equals(“java”)){
list.add(“love”);
}
}
System.out.println(list);
/* 2,当遇到以上错误时,给出两种解决办法
* 首先展示第一种解决办法:使用ListIterator的add()方法
*/
ListIterator lit = list.listItertor();
while(lit.hasNext()){
String s = (String)lit.next();
if(s.equals("java")){
lit.add("love");//注意:此处是利用迭代器进行添加元素,刚添加的元素处于刚才迭代的元素的后面。
}
}
/* 3,给出另一种解决办法:
* 使用普通循环方法,即使用get()和size()的方法
*/
for(int i = 0;i
(3)List子类特点(面试题)
ArrayList:底层数据结构是数组,查询快,增删慢;线程不安全,效率高。
Vector:底层数据结构是数组,查询快,增删慢;线程安全,效率低。现在已不常用
LinkedList:底层数据结构是链表,查询慢,增删快。线程不安全,效率高。
(4)LinkedList的特有功能:
添加功能
public void addFirst(Object e)
public void addLast(Object e) //和add()功能一样,所以不常用此方法
获取功能
puclic Object getFirst()
public Object getLast()
删除功能
public Object removeFirst()
public Object removeLast()
3.集合应用案例
案例一:获取10个1-30之间的随机数,要求不能重复
import java.util.ArrayList;
import java.util.Random;
/*
* 需求:获取10个1-30之间的随机数,要求不能重复
* 分析:
* (1)创建随机数对象
* (2)创建存放随机数的集合
* (3)设定统计量
* (4)产生随机数,并判断集合中是否包含该随机数
* 是:不放入集合
* 否:放入集合,同时统计量++
* (5)遍历集合
*
*/
public class RandomDemo {
public static void main(String[] args) {
//创建随机数对象、创建存放随机数的集合
Random r = new Random();
ArrayList array = new ArrayList();
//设定统计量
int count = 0;
//产生随机数,并判断集合中是否包含该随机数
while(count < 10) {
int temp = r.nextInt(30) + 1;
if(!array.contains(temp)) {
array.add(temp);
count++;
}
}
//遍历集合
for(Integer x : array) {
System.out.println(x);
}
}
}
案例二:键盘录入多个数据,以0结束,要求在控制台输出所录入数据的最大值
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Scanner;
/**
* @author Manduner_TJU
* @version 创建时间:2018年5月27日下午5:06:25
*/
/*
*需求:键盘录入多个数据,以0结束,要求在控制台输出所录入数据的最大值
*分析:
* (1)创建键盘录入对象
* (2) 创建集合
* (3)录入数据,并存放入集合中
* (4)将集合转换为数组
* (5)将数组进行排序
* (6)输出数组的最大值
*
*/
public class ArrayListDemo {
public static void main(String[] args) {
//创建键盘录入对象、创建集合
Scanner sc = new Scanner(System.in);
ArrayList array = new ArrayList();
//录入数据,并存放入集合中
while(true) {
System.out.println("请输入数据:");
Integer i = sc.nextInt();
if(i != 0) {
array.add(i);
}else {
break;
}
}
//将集合转换为数组
Integer[] ii = new Integer[array.size()];
array.toArray(ii);
//将数组进行排序
Arrays.sort(ii);
//输出数组的最大值
System.out.println("数组的最大值是 " + ii[ii.length - 1]);
}
}
给你一个10亿的身份证,有的身份证有重复,查出其中重复的身份证(sql语句)
SELECT id FROM t GROUP BY id HAVING COUNT(id)>1
去掉其中重复的身份证
SELECT DISTINCT id FROM t ;
奇安信
Spring的启动过程
内网给外网发东西,经历的过程
内网和外网连接,这要看你的主动连接方(Client)和被动连接方(Server)各处于什么位置
(1)假设Server方在外网,Client方在内网,那么可以直接通过外网IP连接,不需要任何映射
(2)假设Server方在内网,Client方不论在其他内网还是在外网,都需要Server方的监听端口有被外网访问的权限,可以通过端口映射的方式实现
一个二叉树中序遍历的序列
左根右
GC—复制后地址如何确定
垃圾收集算法:
(1) 标记清除
首先标记出所有需要回收的对象
在标记完成后统一回收所有被标记的对象
不足:效率问题
空间问题:标记清除之后产生大量不连续的内存碎片
(2) 复制算法
将可用内存按容量大小划分为大小相等的两块,每次只使用其中的一块。当一块内存使用完了,就将还存活的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。
不足:只利用了一半内存
解决:将内存分为较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor
(3) 标记整理
首先标记出所有需要回收的对象
让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存-
(4) 分代收集
一般把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法
在老年代中,因为对象存活率高,没有额外空间对它进行分配担保,就必须采用”标记清除”或”标记整理”算法来进行回收
http1与http2的区别
二进制传输
http2采用二进制传输,相较于文本传输的http1来说更加安全可靠。
多路复用
http1一个连接只能提交一个请求,而http2可以同时处理无数个请求,可以降低连接的占用数量,进一步提升网络的吞吐量。
头部压缩
http2通过gzip与compress对头部进行压缩,并且在客户端与服务端各维护了一份头部索引表,只需要根据索引id就可以进行头部信息的传输,缩小了头部容量,间接提升了传输效率。
服务端推送
服务端可以主动推送资源给客户端,避免客户端花过多的时间逐个请求资源,这样可以降低整个请求的响应时间。
http与https的区别
(1)https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。
(2)http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
(3)http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
(4)http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
对称加密与非对称加密
对称加密: 加密和解密的秘钥使用的是同一个.
非对称加密: 与对称加密算法不同,非对称加密算法需要两个密钥:公开密钥(publickey)和私有密钥(privatekey)
动态规划
定义:动态规划算法是通过拆分问题,定义问题状态和状态之间的关系,使得问题能够以递推(或者说分治)的方式去解决。动态规划算法的基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。
基本思想与策略编辑:由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个二维数组中
数据库中事务的隔离级别(有一个两个线程,A-读,B-写,A读了两次,又读了两次,B看到四条数据)
一般的数据库,都包括以下四种隔离级别:
读未提交(Read Uncommitted)----产生问题: “脏读”、“不可重复读”、“幻读”
读提交(Read Committed)----产生问题: 只能避免“脏读”,并不能避免“不可重复读”和“幻读”。
可重复读(Repeated Read)----产生问题: “可重复读”能够有效的避免“不可重复读”,但却避免不了“幻读”
串行化(Serializable)----产生问题: “脏读”、“不可重复读”、“幻读”都可以被避免,但是执行效率奇差,性能开销也最大
为什么会出现“脏读”?因为没有“select”操作没有规矩。
为什么会出现“不可重复读”?因为“update”操作没有规矩。
为什么会出现“幻读”?因为“insert”和“delete”操作没有规矩。
线程和进程的区别
(1)进程是资源分配的最小单位,线程是程序执行的最小单位。
(2)进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。
(3)线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。
(4)但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。
生产者消费者模型
★简介
生产者消费者模式并不是GOF提出的23种设计模式之一,23种设计模式都是建立在面向对象的基础之上的,但其实面向过程的编程中也有很多高效的编程模式,生产者消费者模式便是其中之一,它是我们编程过程中最常用的一种设计模式。
在实际的软件开发过程中,经常会碰到如下场景:某个模块负责产生数据,这些数据由另一个模块来负责处理(此处的模块是广义的,可以是类、函数、线程、进程等)。产生数据的模块,就形象地称为生产者;而处理数据的模块,就称为消费者。
单单抽象出生产者和消费者,还够不上是生产者/消费者模式。该模式还需要有一个缓冲区处于生产者和消费者之间,作为一个中介。生产者把数据放入缓冲区,而消费者从缓冲区取出数据。大概的结构如下图。
为了不至于太抽象,我们举一个寄信的例子(虽说这年头寄信已经不时兴,但这个例子还是比较贴切的)。假设你要寄一封平信,大致过程如下:
1、你把信写好——相当于生产者制造数据
2、你把信放入邮筒——相当于生产者把数据放入缓冲区
3、邮递员把信从邮筒取出——相当于消费者把数据取出缓冲区
4、邮递员把信拿去邮局做相应的处理——相当于消费者处理数据
★优点
可能有同学会问了:这个缓冲区有什么用捏?为什么不让生产者直接调用消费者的某个函数,直接把数据传递过去?搞出这么一个缓冲区作甚?
其实这里面是大有讲究的,大概有如下一些好处。
◇解耦
假设生产者和消费者分别是两个类。如果让生产者直接调用消费者的某个方法,那么生产者对于消费者就会产生依赖(也就是耦合)。将来如果消费者的代码发生变化,可能会影响到生产者。而如果两者都依赖于某个缓冲区,两者之间不直接依赖,耦合也就相应降低了。
接着上述的例子,如果不使用邮筒(也就是缓冲区),你必须得把信直接交给邮递员。有同学会说,直接给邮递员不是挺简单的嘛?其实不简单,你必须得认识谁是邮递员,才能把信给他(光凭身上穿的制服,万一有人假冒,就惨了)。这就产生和你和邮递员之间的依赖(相当于生产者和消费者的强耦合)。万一哪天邮递员换人了,你还要重新认识一下(相当于消费者变化导致修改生产者代码)。而邮筒相对来说比较固定,你依赖它的成本就比较低(相当于和缓冲区之间的弱耦合)。
◇支持并发(concurrency)
生产者直接调用消费者的某个方法,还有另一个弊端。由于函数调用是同步的(或者叫阻塞的),在消费者的方法没有返回之前,生产者只好一直等在那边。万一消费者处理数据很慢,生产者就会白白糟蹋大好时光。
使用了生产者/消费者模式之后,生产者和消费者可以是两个独立的并发主体(常见并发类型有进程和线程两种,后面的帖子会讲两种并发类型下的应用)。生产者把制造出来的数据往缓冲区一丢,就可以再去生产下一个数据。基本上不用依赖消费者的处理速度。
其实当初这个模式,主要就是用来处理并发问题的。
从寄信的例子来看。如果没有邮筒,你得拿着信傻站在路口等邮递员过来收(相当于生产者阻塞);又或者邮递员得挨家挨户问,谁要寄信(相当于消费者轮询)。不管是哪种方法,都挺土的。
◇支持忙闲不均
缓冲区还有另一个好处。如果制造数据的速度时快时慢,缓冲区的好处就体现出来了。当数据制造快的时候,消费者来不及处理,未处理的数据可以暂时存在缓冲区中。等生产者的制造速度慢下来,消费者再慢慢处理掉。
为了充分复用,我们再拿寄信的例子来说事。假设邮递员一次只能带走1000封信。万一某次碰上情人节(也可能是圣诞节)送贺卡,需要寄出去的信超过1000封,这时候邮筒这个缓冲区就派上用场了。邮递员把来不及带走的信暂存在邮筒中,等下次过来时再拿走。
费了这么多口水,希望原先不太了解生产者/消费者模式的同学能够明白它是怎么一回事。接下来说说数据单元。
★啥是数据单元
何谓数据单元捏?简单地说,每次生产者放到缓冲区的,就是一个数据单元;每次消费者从缓冲区取出的,也是一个数据单元。对于前一个帖子中寄信的例子,我们可以把每一封单独的信件看成是一个数据单元。
不过光这么介绍,太过于简单,无助于大伙儿分析出这玩意儿。所以,后面咱们来看一下数据单元需要具备哪些特性。搞明白这些特性之后,就容易从复杂的业务逻辑中分析出适合做数据单元的东西了。
★数据单元的特性
分析数据单元,需要考虑如下几个方面的特性:
◇关联到业务对象
首先,数据单元必须关联到某种业务对象。在考虑该问题的时候,你必须深刻理解当前这个生产者/消费者模式所对应的业务逻辑,才能够作出合适的判断。
由于“寄信”这个业务逻辑比较简单,所以大伙儿很容易就可以判断出数据单元是啥。但现实生活中,往往没这么乐观。大多数业务逻辑都比较复杂,当中包含的业务对象是层次繁多、类型各异。在这种情况下,就不易作出决策了。
这一步很重要,如果选错了业务对象,会导致后续程序设计和编码实现的复杂度大为上升,增加了开发和维护成本。
◇完整性
所谓完整性,就是在传输过程中,要保证该数据单元的完整。要么整个数据单元被传递到消费者,要么完全没有传递到消费者。不允许出现部分传递的情形。
对于寄信来说,你不能把半封信放入邮筒;同样的,邮递员从邮筒中拿信,也不能只拿出信的一部分。
◇独立性
所谓独立性,就是各个数据单元之间没有互相依赖,某个数据单元传输失败不应该影响已经完成传输的单元;也不应该影响尚未传输的单元。
为啥会出现传输失败捏?假如生产者的生产速度在一段时间内一直超过消费者的处理速度,那就会导致缓冲区不断增长并达到上限,之后的数据单元就会被丢弃。如果数据单元相互独立,等到生产者的速度降下来之后,后续的数据单元继续处理,不会受到牵连;反之,如果数据单元之间有某种耦合,导致被丢弃的数据单元会影响到后续其它单元的处理,那就会使程序逻辑变得非常复杂。
对于寄信来说,某封信弄丢了,不会影响后续信件的送达;当然更不会影响已经送达的信件。
◇颗粒度
前面提到,数据单元需要关联到某种业务对象。那么数据单元和业务对象是否要一一对应捏?很多场合确实是一一对应的。
不过,有时出于性能等因素的考虑,也可能会把N个业务对象打包成一个数据单元。那么,这个N该如何取值就是颗粒度的考虑了。颗粒度的大小是有讲究的。太大的颗粒度可能会造成某种浪费;太小的颗粒度可能会造成性能问题。颗粒度的权衡要基于多方面的因素,以及一些经验值的考量。
还是拿寄信的例子。如果颗粒度过小(比如设定为1),那邮递员每次只取出1封信。如果信件多了,那就得来回跑好多趟,浪费了时间。
如果颗粒度太大(比如设定为100),那寄信的人得等到凑满100封信才拿去放入邮筒。假如平时很少写信,就得等上很久,也不太爽。
可能有同学会问:生产者和消费者的颗粒度能否设置成不同大小(比如对于寄信人设置成1,对于邮递员设置成100)。当然,理论上可以这么干,但是在某些情况下会增加程序逻辑和代码实现的复杂度。后面讨论具体技术细节时,或许会聊到这个问题。
好,数据单元的话题就说到这。希望通过本帖子,大伙儿能够搞明白数据单元到底是怎么一回事。下一个帖子,咱们来聊一下“基于队列的缓冲区”,技术上如何实现。
[2]:队列缓冲区
经过前面两个帖子的铺垫,今天终于开始聊一些具体的编程技术了。由于不同的缓冲区类型、不同的并发场景对于具体的技术实现有较大的影响。为了深入浅出、便于大伙儿理解,咱们先来介绍最传统、最常见的方式。也就是单个生产者对应单个消费者,当中用队列(FIFO)作缓冲。
关于并发的场景,在之前的帖子“进程还线程?是一个问题!”中,已经专门论述了进程和线程各自的优缺点,两者皆不可偏废。所以,后面对各种缓冲区类型的介绍都会同时提及进程方式和线程方式。
★线程方式
先来说一下并发线程中使用队列的例子,以及相关的优缺点。
◇内存分配的性能
在线程方式下,生产者和消费者各自是一个线程。生产者把数据写入队列头(以下简称push),消费者从队列尾部读出数据(以下简称pop)。当队列为空,消费者就稍息(稍事休息);当队列满(达到最大长度),生产者就稍息。整个流程并不复杂。
那么,上述过程会有什么问题捏?一个主要的问题是关于内存分配的性能开销。对于常见的队列实现:在每次push时,可能涉及到堆内存的分配;在每次pop时,可能涉及堆内存的释放。假如生产者和消费者都很勤快,频繁地push、pop,那内存分配的开销就很可观了。对于内存分配的开销,用Java的同学可以参见前几天的帖子“Java性能优化[1]”;对于用C/C++的同学,想必对OS底层机制会更清楚,应该知道分配堆内存(new或malloc)会有加锁的开销和用户态/核心态切换的开销。
那该怎么办捏?请听下文分解,关于“生产者/消费者模式[3]:环形缓冲区”。
◇同步和互斥的性能
另外,由于两个线程共用一个队列,自然就会涉及到线程间诸如同步啊、互斥啊、死锁啊等等劳心费神的事情。好在"操作系统"这门课程对此有详细介绍,学过的同学应该还有点印象吧?对于没学过这门课的同学,也不必难过,网上相关的介绍挺多的(比如"这里"),大伙自己去瞅一瞅。关于这方面的细节,咱今天就不多啰嗦了。
这会儿要细谈的是,同步和互斥的性能开销。在很多场合中,诸如信号量、互斥量等玩意儿的使用也是有不小的开销的(某些情况下,也可能导致用户态/核心态切换)。如果像刚才所说,生产者和消费者都很勤快,那这些开销也不容小觑啊。
这又该咋办捏?请听下文的下文分解,关于“生产者/消费者模式[4]:双缓冲区”。
◇适用于队列的场合
刚才尽批判了队列的缺点,难道队列方式就一无是处?非也。由于队列是很常见的数据结构,大部分编程语言都内置了队列的支持(具体介绍见"这里"),有些语言甚至提供了线程安全的队列(比如JDK 1.5引入的ArrayBlockingQueue)。因此,开发人员可以捡现成,避免了重新发明轮子。
所以,假如你的数据流量不是很大,采用队列缓冲区的好处还是很明显的:逻辑清晰、代码简单、维护方便。比较符合KISS原则。
★进程方式
说完了线程的方式,再来介绍基于进程的并发。
跨进程的生产者/消费者模式,非常依赖于具体的进程间通讯(IPC)方式。而IPC的种类名目繁多,不便于挨个列举(毕竟口水有限)。因此咱们挑选几种跨平台、且编程语言支持较多的IPC方式来说事儿。
◇匿名管道
感觉管道是最像队列的IPC类型。生产者进程在管道的写端放入数据;消费者进程在管道的读端取出数据。整个的效果和线程中使用队列非常类似,区别在于使用管道就无需操心线程安全、内存分配等琐事(操作系统暗中都帮你搞定了)。
管道又分命名管道和匿名管道两种,今天主要聊匿名管道。因为命名管道在不同的操作系统下差异较大(比如Win32和POSIX,在命名管道的API接口和功能实现上都有较大差异;有些平台不支持命名管道,比如Windows CE)。除了操作系统的问题,对于有些编程语言(比如Java)来说,命名管道是无法使用的。所以我一般不推荐使用这玩意儿。
其实匿名管道在不同平台上的API接口,也是有差异的(比如Win32的CreatePipe和POSIX的pipe,用法就很不一样)。但是我们可以仅使用标准输入和标准输出(以下简称stdio)来进行数据的流入流出。然后利用shell的管道符把生产者进程和消费者进程关联起来(没听说过这种手法的同学,可以看"这里")。实际上,很多操作系统(尤其是POSIX风格的)自带的命令都充分利用了这个特性来实现数据的传输(比如more、grep等)。
这么干有几个好处:
1、基本上所有操作系统都支持在shell方式下使用管道符。因此很容易实现跨平台。
2、大部分编程语言都能够操作stdio,因此跨编程语言也就容易实现。
3、刚才已经提到,管道方式省却了线程安全方面的琐事。有利于降低开发、调试成本。
当然,这种方式也有自身的缺点:
1、生产者进程和消费者进程必须得在同一台主机上,无法跨机器通讯。这个缺点比较明显。
2、在一对一的情况下,这种方式挺合用。但如果要扩展到一对多或者多对一,那就有点棘手了。所以这种方式的扩展性要打个折扣。假如今后要考虑类似的扩展,这个缺点就比较明显。
3、由于管道是shell创建的,对于两边的进程不可见(程序看到的只是stdio)。在某些情况下,导致程序不便于对管道进行操纵(比如调整管道缓冲区尺寸)。这个缺点不太明显。
4、最后,这种方式只能单向传数据。好在大多数情况下,消费者进程不需要传数据给生产者进程。万一你确实需要信息反馈(从消费者到生产者),那就费劲了。可能得考虑换种IPC方式。
顺便补充几个注意事项,大伙儿留意一下:
1、对stdio进行读写操作是以阻塞方式进行。比如管道中没有数据,消费者进程的读操作就会一直停在哪儿,直到管道中重新有数据。
2、由于stdio内部带有自己的缓冲区(这缓冲区和管道缓冲区是两码事),有时会导致一些不太爽的现象(比如生产者进程输出了数据,但消费者进程没有立即读到)。具体的细节,大伙儿可以看"这里"。
◇SOCKET(TCP方式)
基于TCP方式的SOCKET通讯是又一个类似于队列的IPC方式。它同样保证了数据的顺序到达;同样有缓冲的机制。而且这玩意儿也是跨平台和跨语言的,和刚才介绍的shell管道符方式类似。
SOCKET相比shell管道符的方式,有啥优点捏?主要有如下几个优点:
1、SOCKET方式可以跨机器(便于实现分布式)。这是主要优点。
2、SOCKET方式便于将来扩展成为多对一或者一对多。这也是主要优点。
3、SOCKET可以设置阻塞和非阻塞方法,用起来比较灵活。这是次要优点。
4、SOCKET支持双向通讯,有利于消费者反馈信息。
当然有利就有弊。相对于上述shell管道的方式,使用SOCKET在编程上会更复杂一些。好在前人已经做了大量的工作,搞出很多SOCKET通讯库和框架给大伙儿用(比如C++的ACE库、Python的Twisted)。借助于这些第三方的库和框架,SOCKET方式用起来还是比较爽的。由于具体的网络通讯库该怎么用不是本系列的重点,此处就不细说了。
虽然TCP在很多方面比UDP可靠,但鉴于跨机器通讯先天的不可预料性(比如网线可能被某傻X给拔错了,网络的忙闲波动可能很大),在程序设计上我们还是要多留一手。具体该如何做捏?可以在生产者进程和消费者进程内部各自再引入基于线程的"生产者/消费者模式"。这话听着像绕口令,为了便于理解,画张图给大伙儿瞅一瞅。
这么做的关键点在于把代码分为两部分:生产线程和消费线程属于和业务逻辑相关的代码(和通讯逻辑无关);发送线程和接收线程属于通讯相关的代码(和业务逻辑无关)。
这样的好处是很明显的,具体如下:
1、能够应对暂时性的网络故障。并且在网络故障解除后,能够继续工作。
2、网络故障的应对处理方式(比如断开后的尝试重连),只影响发送和接收线程,不会影响生产线程和消费线程(业务逻辑部分)。
3、具体的SOCKET方式(阻塞和非阻塞)只影响发送和接收线程,不影响生产线程和消费线程(业务逻辑部分)。
4、不依赖TCP自身的发送缓冲区和接收缓冲区。(默认的TCP缓冲区的大小可能无法满足实际要求)
5、业务逻辑的变化(比如业务需求变更)不影响发送线程和接收线程。
针对上述的最后一条,再多啰嗦几句。如果整个业务系统中有多个进程是采用上述的模式,那或许可以重构一把:在业务逻辑代码和通讯逻辑代码之间切一刀,把业务逻辑无关的部分封装成一个通讯中间件(说中间件显得比较牛X :-)。如果大伙儿对这玩意儿有兴趣,以后专门开个帖子聊。
[3]:环形缓冲区
前一个帖子提及了队列缓冲区可能存在的性能问题及解决方法:环形缓冲区。今天就专门来描述一下这个话题。
为了防止有人给咱扣上“过度设计”的大帽子,事先声明一下:只有当存储空间的分配/释放非常频繁并且确实产生了明显的影响,你才应该考虑环形缓冲区的使用。否则的话,还是老老实实用最基本、最简单的队列缓冲区吧。还有一点需要说明一下:本文所提及的“存储空间”,不仅包括内存,还可能包括诸如硬盘之类的存储介质。
★环形缓冲区 vs 队列缓冲区
◇外部接口相似
在介绍环形缓冲区之前,咱们先来回顾一下普通的队列。普通的队列有一个写入端和一个读出端。队列为空的时候,读出端无法读取数据;当队列满(达到最大尺寸)时,写入端无法写入数据。
对于使用者来讲,环形缓冲区和队列缓冲区是一样的。它也有一个写入端(用于push)和一个读出端(用于pop),也有缓冲区“满”和“空”的状态。所以,从队列缓冲区切换到环形缓冲区,对于使用者来说能比较平滑地过渡。
◇内部结构迥异
虽然两者的对外接口差不多,但是内部结构和运作机制有很大差别。队列的内部结构此处就不多啰嗦了。重点介绍一下环形缓冲区的内部结构。
大伙儿可以把环形缓冲区的读出端(以下简称R)和写入端(以下简称W)想象成是两个人在体育场跑道上追逐(R追W)。当R追上W的时候,就是缓冲区为空;当W追上R的时候(W比R多跑一圈),就是缓冲区满。
为了形象起见,去找来一张图并略作修改,如下:
从上图可以看出,环形缓冲区所有的push和pop操作都是在一个固定的存储空间内进行。而队列缓冲区在push的时候,可能会分配存储空间用于存储新元素;在pop时,可能会释放废弃元素的存储空间。所以环形方式相比队列方式,少掉了对于缓冲区元素所用存储空间的分配、释放。这是环形缓冲区的一个主要优势。
★环形缓冲区的实现
如果你手头已经有现成的环形缓冲区可供使用,并且你对环形缓冲区的内部实现不感兴趣,可以跳过这段。
◇数组方式 vs 链表方式
环形缓冲区的内部实现,即可基于数组(此处的数组,泛指连续存储空间)实现,也可基于链表实现。
数组在物理存储上是一维的连续线性结构,可以在初始化时,把存储空间一次性分配好,这是数组方式的优点。但是要使用数组来模拟环,你必须在逻辑上把数组的头和尾相连。在顺序遍历数组时,对尾部元素(最后一个元素)要作一下特殊处理。访问尾部元素的下一个元素时,要重新回到头部元素(第0个元素)。如下图所示:
使用链表的方式,正好和数组相反:链表省去了头尾相连的特殊处理。但是链表在初始化的时候比较繁琐,而且在有些场合(比如后面提到的跨进程的IPC)不太方便使用。
◇读写操作
环形缓冲区要维护两个索引,分别对应写入端(W)和读取端(R)。写入(push)的时候,先确保环没满,然后把数据复制到W所对应的元素,最后W指向下一个元素;读取(pop)的时候,先确保环没空,然后返回R对应的元素,最后R指向下一个元素。
◇判断“空”和“满”
上述的操作并不复杂,不过有一个小小的麻烦:空环和满环的时候,R和W都指向同一个位置!这样就无法判断到底是“空”还是“满”。大体上有两种方法可以解决该问题。
办法1:始终保持一个元素不用
当空环的时候,R和W重叠。当W比R跑得快,追到距离R还有一个元素间隔的时候,就认为环已经满。当环内元素占用的存储空间较大的时候,这种办法显得很土(浪费空间)。
办法2:维护额外变量
如果不喜欢上述办法,还可以采用额外的变量来解决。比如可以用一个整数记录当前环中已经保存的元素个数(该整数>=0)。当R和W重叠的时候,通过该变量就可以知道是“空”还是“满”。
◇元素的存储
由于环形缓冲区本身就是要降低存储空间分配的开销,因此缓冲区中元素的类型要选好。尽量存储值类型的数据,而不要存储指针(引用)类型的数据。因为指针类型的数据又会引起存储空间(比如堆内存)的分配和释放,使得环形缓冲区的效果打折扣。
★应用场合
刚才介绍了环形缓冲区内部的实现机制。按照前一个帖子的惯例,我们来介绍一下在线程和进程方式下的使用。
如果你所使用的编程语言和开发库中带有现成的、成熟的环形缓冲区,强烈建议使用现成的库,不要重新制造轮子;确实找不到现成的,才考虑自己实现。如果你纯粹是业余时间练练手,那另当别论。
◇用于并发线程
和线程中的队列缓冲区类似,线程中的环形缓冲区也要考虑线程安全的问题。除非你使用的环形缓冲区的库已经帮你实现了线程安全,否则你还是得自己动手搞定。线程方式下的环形缓冲区用得比较多,相关的网上资料也多,下面就大致介绍几个。
对于C++的程序员,强烈推荐使用boost提供的circular_buffer模板,该模板最开始是在boost 1.35版本中引入的。鉴于boost在C++社区中的地位,大伙儿应该可以放心使用该模板。
对于C程序员,可以去看看开源项目circbuf,不过该项目是GPL协议的,不太爽;而且活跃度不太高;而且只有一个开发人员。大伙儿慎用!建议只拿它当参考。
对于C#程序员,可以参考CodeProject上的一个示例。
◇用于并发进程
进程间的环形缓冲区,似乎少有现成的库可用。大伙儿只好自己动手、丰衣足食了。
适用于进程间环形缓冲的IPC类型,常见的有共享内存和文件。在这两种方式上进行环形缓冲,通常都采用数组的方式实现。程序事先分配好一个固定长度的存储空间,然后具体的读写操作、判断“空”和“满”、元素存储等细节就可参照前面所说的来进行。
共享内存方式的性能很好,适用于数据流量很大的场景。但是有些语言(比如Java)对于共享内存不支持。因此,该方式在多语言协同开发的系统中,会有一定的局限性。
而文件方式在编程语言方面支持很好,几乎所有编程语言都支持操作文件。但它可能会受限于磁盘读写(Disk I/O)的性能。所以文件方式不太适合于快速数据传输;但是对于某些“数据单元”很大的场合,文件方式是值得考虑的。
对于进程间的环形缓冲区,同样要考虑好进程间的同步、互斥等问题,限于篇幅,此处就不细说了。
生产/消费者问题是个非常典型的多线程问题,涉及到的对象包括“生产者”、“消费者”、“仓库”和“产品”。他们之间的关系如下:
① 生产者仅仅在仓储未满时候生产,仓满则停止生产。
② 消费者仅仅在仓储有产品时候才能消费,仓空则等待。
③ 当消费者发现仓库没产品可消费时候会通知生产者生产。
④ 生产者在生产出可消费产品时候,应该通知等待的消费者去消费。
用wait/notify/notifyAll实现和用Lock的Condition实现。
用wait/notify/notifyAll 实现生产者消费者模型:
方法一:用五个类来实现,分别为Produce(实现生产过程), Consumer(实现消费过程), ProduceThread(实现生产者线程),ConsumeThread(实现消费者线程),Main等。需要注意的是有两个地方。
① 用while判断当前list是否为空;
② 调用的是object的notifyAll()方法而不是notify()方法。
方法二:用四个类实现,分别为MyService(实现生产和消费过程用synchronized关键字实现同步),ProduceThread(实现生产者线程),ConsumeThread(实现消费者线程),Main。需要注意的也是方法一中的两个地方while和notifyAll()。
用Lock和Condition实现。共有四个类,分别是分别为MyService(实现生产和消费过程,用lock实现线程间同步),ProduceThread(实现生产者线程),ConsumeThread(实现消费者线程),Main。需要注意的也是方法一中的两个地方while和signalAll()。
方法一:
[java] view plain copy
public Object object;
public ArrayList list;//用list存放生产之后的数据,最大容量为1
public Produce(Object object,ArrayList list ){
this.object = object;
this.list = list;
}
public void produce() {
synchronized (object) {
/*只有list为空时才会去进行生产操作*/
try {
while(!list.isEmpty()){
System.out.println("生产者"+Thread.currentThread().getName()+" waiting");
object.wait();
}
int value = 9999;
list.add(value);
System.out.println("生产者"+Thread.currentThread().getName()+" Runnable");
object.notifyAll();//然后去唤醒因object调用wait方法处于阻塞状态的线程
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
[java] view plain copy
public Object object;
public ArrayList list;//用list存放生产之后的数据,最大容量为1
public Consumer(Object object,ArrayList list ){
this.object = object;
this.list = list;
}
public void consmer() {
synchronized (object) {
try {
/*只有list不为空时才会去进行消费操作*/
while(list.isEmpty()){
System.out.println("消费者"+Thread.currentThread().getName()+" waiting");
object.wait();
}
list.clear();
System.out.println("消费者"+Thread.currentThread().getName()+" Runnable");
object.notifyAll();//然后去唤醒因object调用wait方法处于阻塞状态的线程
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
[java] view plain copy
private Produce p;
public ProduceThread(Produce p){
this.p = p;
}
@Override
public void run() {
while (true) {
p.produce();
}
}
[java] view plain copy
private Consumer c;
public ConsumeThread(Consumer c){
this.c = c;
}
@Override
public void run() {
while (true) {
c.consmer();
}
}
[java] view plain copy
public static void main(String[] args) {
Object object = new Object();
ArrayList list = new ArrayList();
Produce p = new Produce(object, list);
Consumer c = new Consumer(object, list);
ProduceThread[] pt = new ProduceThread[2];
ConsumeThread[] ct = new ConsumeThread[2];
for(int i=0;i<2;i++){
pt[i] = new ProduceThread(p);
pt[i].setName("生产者 "+(i+1));
ct[i] = new ConsumeThread(c);
ct[i].setName("消费者"+(i+1));
pt[i].start();
ct[i].start();
}
}
方法二:
[java] view plain copy
public ArrayList list = new ArrayList();//用list存放生产之后的数据,最大容量为1
synchronized public void produce() {
try {
/*只有list为空时才会去进行生产操作*/
while(!list.isEmpty()){
System.out.println("生产者"+Thread.currentThread().getName()+" waiting");
this.wait();
}
int value = 9999;
list.add(value);
System.out.println("生产者"+Thread.currentThread().getName()+" Runnable");
this.notifyAll();//然后去唤醒因object调用wait方法处于阻塞状态的线程
}catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized public void consmer() {
try {
/*只有list不为空时才会去进行消费操作*/
while(list.isEmpty()){
System.out.println("消费者"+Thread.currentThread().getName()+" waiting");
this.wait();
}
list.clear();
System.out.println("消费者"+Thread.currentThread().getName()+" Runnable");
this.notifyAll();//然后去唤醒因object调用wait方法处于阻塞状态的线程
} catch (InterruptedException e) {
e.printStackTrace();
}
}
[java] view plain copy
private MyService p;
public ProduceThread(MyService p){
this.p = p;
}
@Override
public void run() {
while (true) {
p.produce();
}
}
[java] view plain copy
private MyService c;
public ConsumeThread(MyService c){
this.c = c;
}
@Override
public void run() {
while (true) {
c.consmer();
}
}
用Lock和Condition实现
[java] view plain copy
private ReentrantLock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
private boolean hasValue = false;
public void produce() {
lock.lock();
try {
/*只有list为空时才会去进行生产操作*/
while(hasValue == true){
System.out.println("生产者"+Thread.currentThread().getName()+" waiting");
condition.await();
}
hasValue = true;
System.out.println("生产者"+Thread.currentThread().getName()+" Runnable");
condition.signalAll();//然后去唤醒因object调用wait方法处于阻塞状态的线程
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
lock.unlock();
}
}
public void consmer() {
lock.lock();
try {
/*只有list为空时才会去进行生产操作*/
while(hasValue == false){
System.out.println("消费者"+Thread.currentThread().getName()+" waiting");
condition.await();
}
hasValue = false;
System.out.println("消费者"+Thread.currentThread().getName()+" Runnable");
condition.signalAll();//然后去唤醒因object调用wait方法处于阻塞状态的线程
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
lock.unlock();
}
}
[java] view plain copy
private MyService p;
public ProduceThread(MyService p){
this.p = p;
}
@Override
public void run() {
while (true) {
p.produce();
}
}
private MyService c;
public ConsumeThread(MyService c){
this.c = c;
}
@Override
public void run() {
while (true) {
c.consmer();
}
}
[java] view plain copy
public static void main(String[] args) {
MyService service = new MyService();
ProduceThread[] pt = new ProduceThread[2];
ConsumeThread[] ct = new ConsumeThread[2];
for(int i=0;i<1;i++){
pt[i] = new ProduceThread(service);
pt[i].setName("Condition 生产者 "+(i+1));
ct[i] = new ConsumeThread(service);
ct[i].setName("Condition 消费者"+(i+1));
pt[i].start();
ct[i].start();
}
}
public class TestFilter1 extends Filter {
@Override
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//在DispatcherServlet之前执行
System.out.println("############TestFilter1 doFilterInternal executed############");
filterChain.doFilter(request, response);
//在视图页面返回给客户端之前执行,但是执行顺序在Interceptor之后
System.out.println("############TestFilter1 doFilter after############");
}
}
(2)第二个过滤器:
public class TestFilter2 extends Filter {
@Override
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//在DispatcherServlet之前执行
System.out.println("############TestFilter2 doFilterInternal executed############");
filterChain.doFilter(request, response);
//在视图页面返回给客户端之前执行,但是执行顺序在Interceptor之后
System.out.println("############TestFilter2 doFilter after############");
}
}
(3)在web.xml中注册这两个过滤器:
testFilter1
com.scorpios.filter.TestFilter1
testFilter1
/
testFilter2
com.scorpios.filter.TestFilter2
testFilter2
/
再定义两个拦截器:
(4)第一个拦截器:
public class BaseInterceptor implements HandlerInterceptor{
/**
* 在DispatcherServlet之前执行
* */
public boolean preHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2) throws Exception {
System.out.println(“BaseInterceptor preHandle executed");
return true;
}
/
* 在controller执行之后的DispatcherServlet之后执行
* */
public void postHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, ModelAndView arg3) throws Exception {
System.out.println(“BaseInterceptor postHandle executed");
}
/
* 在页面渲染完成返回给客户端之前执行
* */
public void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3)
throws Exception {
System.out.println(”**BaseInterceptor afterCompletion executed”);
}
}
(5)第二个拦截器:
public class TestInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2) throws Exception {
System.out.println("**TestInterceptor preHandle executed");
return true;
}
public void postHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, ModelAndView arg3) throws Exception {
System.out.println("**TestInterceptor postHandle executed");
}
public void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3) throws Exception {
System.out.println("**TestInterceptor afterCompletion executed");
}
}
(6)、在SpringMVC的配置文件中,加上拦截器的配置:
mvc:interceptors
(7)、定义一个Controller控制器:
package com.scorpios.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class TestController {
@RequestMapping("/test")
public ModelAndView handleRequest(){
System.out.println("---------TestController executed--------");
return new ModelAndView(“test”);
}
}
(8)、测试结果:
启动测试项目,地址如下:http://www.localhost:8080/demo,可以看到控制台中输出如下:
这就说明了过滤器的运行是依赖于servlet容器,跟springmvc等框架并没有关系。并且,多个过滤器的执行顺序跟xml文件中定义的先后关系有关。
接着清空控制台,并访问:http://www.localhost:8080/demo/test,再次看控制台的输出:
从这个控制台打印输出,就可以很清晰地看到有多个拦截器和过滤器存在时的整个执行顺序了。当然,对于多个拦截器它们之间的执行顺序跟在SpringMVC的配置文件中定义的先后顺序有关。
四、总结
对于上述过滤器和拦截器的测试,可以得到如下结论:
(1)、Filter需要在web.xml中配置,依赖于Servlet;
(2)、Interceptor需要在SpringMVC中配置,依赖于框架;
(3)、Filter的执行顺序在Interceptor之前,具体的流程见下图;
(4)、两者的本质区别:拦截器(Interceptor)是基于Java的反射机制,而过滤器(Filter)是基于函数回调。从灵活性上说拦截器功能更强大些,Filter能做的事情,都能做,而且可以在请求前,请求后执行,比较灵活。Filter主要是针对URL地址做一个编码的事情、过滤掉没用的参数、安全校验(比较泛的,比如登录不登录之类),太细的话,还是建议用interceptor。不过还是根据不同情况选择合适的。
大华
死锁产生的条件以及解决方法
死锁:多个并发进程因争夺系统资源而产生相互等待的现象
本质原因:1)系统资源有限
2)进程推进顺序不合理
产生死锁(必要条件):1)互斥:一个资源每次只能被一个进程使用
2)请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
3)不可剥夺条件:进程已获得的资源,在未使用之前,不能被强行剥夺
4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
解决死锁的基本方法:
1) 预防死锁:资源一次性分配;可剥夺资源;资源有序分配法
2) 避免死锁:运行进程动态地申请资源
3) 检测死锁:为每个进程和每个资源指定唯一的号码,然后建立资源分配表和进程等待表
4) 解除死锁:剥夺资源—剥夺其它进程资源给死锁进程,以解除死锁
撤销进程—撤销死锁进程或撤销代价最小的进程直到资源足够可用
Java实现删除指定下的所有文件(递归思想)
使用File类delete方法的特点是,删除一个文件夹,这个文件夹下面不能有其他的文件和文件夹。因此,在删除一个指定的文件夹的时候,用到了递归的思想,需要反复调用删除一个文件夹内所有文件的方法,才能最后将指定的文件夹删除
public class DeleteDemo {
public static void main(String[] args) {
File f = new File(“D:\c.txt”);
deleteFile(f);
}
public static void deleteFile(File file) {
// 判断传递进来的是文件还是文件夹,如果是文件,直接删除,如果是文件夹,则判断文件夹里面有没有东西
if (file.isDirectory()) {
// 如果是目录,就删除目录下所有的文件和文件夹
File[] files = file.listFiles();
// 遍历目录下的文件和文件夹
for (File f : files) {
// 如果是文件,就删除
if (f.isFile()) {
System.out.println(“已经被删除的文件:” + f);
// 删除文件
f.delete();
} else if (file.isDirectory()) {
// 如果是文件夹,就递归调用文件夹的方法
deleteFile(f);
}
}
// 删除文件夹自己,如果它低下是空的,就会被删除
System.out.println(“已经被删除的文件夹:” + file);
file.delete();
return;// 文件夹被删除后,直接用return语句结束当次递归调用
}
// 如果是文件,就直接删除自己
System.out.println(“已经被删除的文件:” + file);
file.delete();
}
}
介绍下Spring中的AOP
https://www.cnblogs.com/xuyatao/p/8485851.html
Spring中都用到哪些设计模式
https://www.cnblogs.com/twoheads/p/9720105.html
跟谁学
ArrayList类底层用数组怎么实现增加,删除,获取元素
https://blog.csdn.net/qq_43527426/article/details/86544761
Object都有哪些方法?
https://blog.csdn.net/qq_30264689/article/details/81903031
wait(),notify(),notifyAll()如何配套使用?
wait(), notify(), notifyAll(),join(),sleep(),yield()等方法介绍
https://blog.csdn.net/zhaojun20161206/article/details/89381243
同城艺龙笔试
public class Main8 {
// 1.现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行
public static void main(String[] args) {
final Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t1");
}
});
final Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
// 引用t1线程,等待t1线程执行完
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2");
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
try {
// 引用t2线程,等待t2线程执行完
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t3");
}
});
t3.start();// 这里三个线程的启动顺序可以任意,大家可以试下!
t2.start();
t1.start();
}
}
乐信
算法遍历一次数组,遍历过程中,将每次遍历的数组元素按升序放到它相应的位置,那么这个元素必然>=它之前的元素。然后比较该元素与前一个元素是否相等。如果相等,则说明包含重复的元素。
class Solution {
public boolean containsDuplicate(int[] nums) {
for (int i = 1; i < nums.length;i++){
int j = i - 1;
int temp = nums[j+1];
while (j >= 0 && nums[j] > temp){
nums[j+1] = nums[j];
j–;
}
nums[j+1] = temp;
if (j >= 0 && nums[j] == nums[j+1]){
return true;
}
}
return false;
}
}