面向Java面试
2019-11-13 13:42:37 3 举报2. Iterator:迭代器,可以通过迭代器遍历集合中的数据
3. Map:是映射表的基础接口
因为这个数组是动态扩展的,并不是所有的空间都被使用,
因此就不需要所有的内容都被序列化。
通过重写序列化和反序列化方法(readObject、writeObject),
使得可以只序列化数组中有内容的那部分数据。
private transient Object[] elementData;
而自定义类的对象是不可以的,
自 己定义的类必须实现Comparable接口,
并且覆写相应的compareTo()函数,
才可以正常使 用。
要返回相应的值才能使TreeSet按照一定的规则来排序
如果该对象小于、等于或大于指定对象,
则分别返回负整 数、零或正整数。
LinkedHashSet 底层使用 LinkedHashMap 来保存所有元素,
它继承HashSet,其所有的方法 操作上又与HashSet相同,
因此LinkedHashSet 的实现上非常简单,
只提供了四个构造方法,并 通过传递一个标识参数,
调用父类的构造器,
底层构造一个 LinkedHashMap 来实现,
在相关操 作上与父类HashSet的操作相同,
直接调用父类HashSet的方法即可。
大多数情况下可以直接定位到它的值,
因而具有很快 的访问速度,
但遍历顺序却是不确定的。
HashMap最多只允许一条记录的键为null,
允许多条记 录的值为 null。
HashMap 非线程安全,
即任一时刻可以有多个线程同时写 HashMap,
可能会导 致数据的不一致
int hash = hash(key);
int i = indexFor(hash, table.length);
计算 hash 值
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
确定桶下标的最后一步是将 key 的 hash 值对桶个数取模:hash % capacity,
如果能保证 capacity 为 2 的 n 次方,那么就可以将这个操作转换为位运算、优化执行效率。
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
static int indexFor(int h, int length) {
return h & (length - 1);
}
位运算的代价比求模运算小的多,因此在进行这种计算时用位运算的话能带来更高的性能(结果是等价的):
index = hash % length;
index = hash & (length - 1);
计算过程:令 x = 1 << 4,即 x 为 2 的 4 次方,它具有以下性质:
x : 00010000
x - 1 : 00001111
令一个数 y 与 x-1 做与运算,可以去除 y 位级表示的第 4 位以上数:
y : 10110010
x - 1 : 00001111
y & (x - 1) : 00000010
这个性质和 y 对 x 取模效果是一样的:
y : 10110010
x : 00010000
y % x : 00000010
put方法的代码如下:
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//如果发现key已经在链表中存在,则修改并返回旧的值
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
//如果遍历链表没发现这个key,则会调用以下代码
modCount++;
addEntry(hash, key, value, i);
return null;
}
阅读源码发现,如果遍历链表都没法发现相应的key值的话,
则会调用addEntry方法在链表添加一个Entry,
重点就在于addEntry方法是如何插入链表的,addEntry方法源码如下:
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
这里构造了一个新的Entry对象(构造方法的最后一个参数传入了当前的Entry链表),
然后直接用这个新的Entry对象取代了旧的Entry链表,
可以猜测这应该是头插法,为了进一步确认这个想法,
我们再看一下Entry的构造方法:
Entry( int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
从构造方法中的next=n可以看出确实是把原本的链表直接链在了新建的Entry对象的后边,
可以断定是插入头部。
put方法的代码如下:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
继续进入putVal方法(先不要着急去读它):
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
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;
}
}
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;
}
在jdk1.8中当链表长度大于8是会被转化成红黑树,
所以源码看起来比jdk1.6要复杂不少,
大量的if-else判断是为了处理红黑树的情况的,
与链表插入相关的核心代码只有如下几行:
for (int binCount = 0; ; ++binCount) {
//e是p的下一个节点
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;
}
//如果key在链表中已经存在,则退出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
//如果key在链表中已经存在,则修改其原先的key值,并且返回老的值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
代码我一行行加了注释,其中链表插入的代码是:
//e是p的下一个节点
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;
}
从这段代码中可以很显然地看出当到达链表尾部(即p是链表的最后一个节点)时,
e被赋为null,会进入这个分支代码,
然后会用newNode方法建立一个新的节点插入尾部。
结论:jdk1.8中是插入的是链表尾部
以至于在并发场景下导致链表成环的问题。
而在jdk1.8中采用尾插入法,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。
但没有解决死锁
https://juejin.im/post/5ba457a25188255c7b168023#heading-18
https://www.cnblogs.com/xrq730/p/5037299.html
T1线程放入key A、B、C、D、E。
在T1线程中A、B、C Hash值相同,
于是形成一个链接,假设为A->C->B,
而D、E Hash值不同,
于是容量不足,需要新建一个更大尺寸的hash表,
然后把数据从老的Hash表中,迁移到新的Hash表中(rehash)。
这时T2线程闯进来了,T1暂时挂起,T2进程也准备放入新的key,
这时也发现容量不足,也rehash一把。
rehash之后原来的链表结构假设为C->A,之后T1进程继续执行,
链接结构为A->C,这时就形成A.next=C,C.next=A的环形链表。
一旦取值进入这个环形链表就会陷入死循环。
能够把它保存的记录根据键排序,
默认是按键值的升序排序,
也可以指定排序的比较器,
当用Iterator遍历TreeMap时,得到的记录是排过序的。
如果使用排序的映射,建议使用TreeMap。
在使用 TreeMap 时,
key 必须实现 Comparable 接口
或者在构造 TreeMap 传入自定义的 Comparator,
否则会在运行时抛出java.lang.ClassCastException类型的异常。
保存了记录的插入顺序,
在用 Iterator 遍历 LinkedHashMap时,
先得到的记录肯定是先插入的,
也可以在构造时带参数,按照访问次序排序。
并且是线程安全的,
任一时间只有一个线程能写 Hashtable,
并发性不如 ConcurrentHashMap
JDK 1.6中,采用分离锁的方式,在读的时候,部分锁;写的时候,完全锁。
而在JDK 1.7、1.8中,读的时候不需要锁的,写的时候需要锁的。
并且JDK 1.8中在为了解决Hash冲突,采用红黑树解决。
它内部细分了若干个小的 HashMap,
称之为段(Segment)。
默认情况下 一个ConcurrentHashMap被进一步细分为16个段,既就是锁的并发度。
如果需要在 ConcurrentHashMap 中添加一个新的表项,
并不是将整个 HashMap 加锁,
而是首 先根据hashcode得到该表项应该存放在哪个段中,
然后对该段加锁,并完成put操作。
在多线程 环境中,如果多个线程同时进行put操作,
只要被加入的表项不存放在同一个段中,则线程间可以 做到真正的并行。
是由 Segment数组结构和
HashEntry数组结构组成或者Node 数组,红黑树组成
Segment 继承了可 重入锁 ReentrantLock,
在 ConcurrentHashMap 里扮演锁的角色,
HashEntry 则用于存储键值 对数据。
一个 ConcurrentHashMap 里包含一个 Segment 数组,
Segment 的结构和 HashMap 类似,是一种数组和链表结构,
一个Segment里包含一个HashEntry 数组,
每个HashEntry是 一个链表结构的元素,
每个 Segment守护一个HashEntry数组里的元素,
当对HashEntry数组的 数据进行修改时,必须首先获得它对应的Segment锁。
但是因为它支持并发操作,所以要复杂一 些。
整个 ConcurrentHashMap 由一个个 Segment 组成,
Segment 代表”部分“或”一段“的 意思,
所以很多地方都会将其描述为分段锁。
注意,行文中,我很多地方用了“槽”来代表一个 segment。
Segment 通过继承 ReentrantLock 来进行加锁,
所以每次需要加锁的操作锁住的是一个 segment,
这样只要保证每 个 Segment 是线程安全的,
也就实现了全局的线程安全。
而mapFunction:key -> map.computeIfAbsent("BBBB", key2 -> 42);
却进入了一个死循环,永远都不会返回。
因此整个代码的执行就被锁住了,
但是这算死锁吗?似乎和死锁的定义不太一样!
总之,为了避免这个问题,
在JDK1.8中使用ConcurrentHashMap时,
不要在computeIfAbsent的lambda函数中再去执行更新其它节点value的操作。
https://blog.csdn.net/lx1848/article/details/81256443
怎么翻译不重要,理解它。
默认是 16, 也就是说 ConcurrentHashMap 有 16 个 Segments,
所以理论上,这个时候,最多可以同时支 持 16 个线程并发写,
只要它们的操作分别分布在不同的 Segment 上。
这个值可以在初始化的时 候设置为其他值,
但是一旦初始化以后,它是不可以扩容的。
再具体到每个 Segment 内部,其实 每个 Segment 很像之前介绍的 HashMap,
不过它要保证线程安全,所以处理起来要麻烦些。
transient int count;
在执行 size 操作时,需要遍历所有 Segment 然后把 count 累计起来。
ConcurrentHashMap 在执行 size 操作时先尝试不加锁,
如果连续两次不加锁操作得到的结果一致,那么可以认为这个结果是正确的。
尝试次数使用 RETRIES_BEFORE_LOCK 定义,该值为 2,retries 初始值为 -1,因此尝试次数为 3。
如果尝试的次数超过 3 次,就需要对每个 Segment 加锁。
static final int RETRIES_BEFORE_LOCK = 2;
public int size() {
// Try a few times to get accurate count. On failure due to
// continuous async changes in table, resort to locking.
final Segment<K,V>[] segments = this.segments;
int size;
boolean overflow; // true if size overflows 32 bits
long sum; // sum of modCounts
long last = 0L; // previous sum
int retries = -1; // first iteration isn't retry
try {
for (;;) {
// 超过尝试次数,则对每个 Segment 加锁
if (retries++ == RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
ensureSegment(j).lock(); // force creation
}
sum = 0L;
size = 0;
overflow = false;
for (int j = 0; j < segments.length; ++j) {
Segment<K,V> seg = segmentAt(segments, j);
if (seg != null) {
sum += seg.modCount;
int c = seg.count;
if (c < 0 || (size += c) < 0)
overflow = true;
}
}
// 连续两次得到的结果一致,则认为这个结果是正确的
if (sum == last)
break;
last = sum;
}
} finally {
if (retries > RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
segmentAt(segments, j).unlock();
}
}
return overflow ? Integer.MAX_VALUE : size;
}
内部仍有 Segment,但只是保证序列化时的兼容性,已不具作用。由于不再使用 Segment,初始化操作简化为 lazy-load 形式;
数据存储利用 volatile 保证可见性;
使用 CAS 操作,在特定场景下进行无锁并发操作(高并发场景下性能一般);
使用 Unsafe、LongAdder 之类底层手段,进行极端情况的优化。
存储节点:key 定义为 final,val、next 都定义为 volatile 保证可见性。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
// …
}
对于 put 操作:
final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh; K fk; V fv;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 利用 CAS 去进行无锁线程安全操作,如果 bin 是空的
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break;
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else if (onlyIfAbsent // 不加锁,进行检查
&& fh == hash
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null)
return fv;
else {
V oldVal = null;
synchronized (f) {
// 细粒度的同步修改操作...
}
}
// Bin 超过阈值,进行树化
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
对于初始化操作:
在一个 initTable 实现,利用 volatile 的 sizeCtl 作为互斥手段,
如果发生竞争性的初始化,就 spin (自旋锁)在哪里,等待条件回复;
否则利用 CAS 设置排他标志。如果成功则进行初始化,否则重试;
当 bin 为空时,同样是没有必要锁定,而以 CAS 操作放置;
同步逻辑上使用 synchronized(性能已被优化,且可以减少内存消耗);
更多细节实现通过 Unsafe 进行优化,如 tabAt 利用 getObjectAcquire 避免间接调用的开销。
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
// 如果发现冲突,进行 spin 等待
if ((sc = sizeCtl) < 0)
Thread.yield();
// CAS 成功返回 true,则进入真正的初始化逻辑
else if (U.compareAndSetInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
对于 size 操作:
也是采用分治的方法进行计数,然后做求和处理:
求和处理是通过 CounterCell 实现,而 CounterCell 操作,是基于 LongAdder 进行的(略)。
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}
内部使用 HashEntry 的数组,与 HashMap 类似,哈希值相同的值使用链表存放;
HashEntry 内部使用 volatile 修饰的值保证可见性,
也利用不可变对象的机制改进利用 Unsafe 提供的底层能力。
对于 get 操作,保证的是可见性,所以没有同步的逻辑。
public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
int h = hash(key.hashCode());
// 利用位操作替换普通数学运算
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
// 以 Segment 为单位,进行定位
// 利用 Unsafe 直接进行 volatile access
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
// 省略
}
return null;
}
对于 put 操作,首先通过二次哈希避免冲突,
再以 Unsafe 调用方式直接获取的 Segment,进行线程安全的 put 操作:
获取重入锁:要确保数据一致性(Segment 本身是基于 ReentrantLock 的扩展实现),
在并发修改期间,相应的 Segment 是被锁定的;
重复性扫描:确定 Key 值是否已经存在于数组,进而决定执行更新或插入操作;
扩容:和 HashMap 类似,可能发生扩容问题,但也是针对 Segment 进行扩容。
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// scanAndLockForPut 会去查找是否有 key 相同 Node
// 无论如何,确保获取锁
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
// 更新已有 value...
}
else {
// 放置 HashEntry 到特定位置,如果超过阈值,进行 rehash
// ...
}
} finally {
unlock();
}
return oldValue;
}
对于 size 操作,则涉及分离锁的副作用,ConcurrentHashMap 则采用重试机制,
如没有监控到变化(modCount)则直接返回,
否则再获取锁(初始化操作也要锁定所有 Segment)。
有两个构造器,一个是(Integer.MAX_VALUE),无边界
另一个是(int capacity),有边界;
数据存储特点(单个对象或键值对,是否需要排序、去重等)
是否保证线程安全,或安全的实现方式
底层数据结构及其空间占用情况
更新操作是否受元素位置影响
是否支持快速随机访问、效率如何
......
主要包括 Collection 和 Map 两种,Collection 存储对象的集合,Map 存储键值对的映射表:
常量
抽象方法
String TYPE_NAME = "java seven interface";
int TYPE_AGE = 20;
void method01();
void method02(String arg);
void method03(String arg1,int arg2);
}
常量
抽象方法
默认方法
静态方法
String TYPE_NAME = "java seven interface";
int TYPE_AGE = 20;
String TYPE_DES = "java seven interface description";
default void method01(String msg){
//TODO
}
default void method02(){
//TODO
}
// Any other abstract methods
void method03();
void method04(String arg);
...
String method05();
}
// Interfaces now allow static methods
static Defaulable create(Supplier< Defaulable > supplier ) {
return supplier.get();
}
}
而不会破坏实现这个接口的已有类的兼容性,
也就是说不会强迫实现接口的类实现默认方法。
默认方法和抽象方法的区别是抽象方法必须要被实现,默认方法不是。
作为替代方式,接口可以提供一个默认的方法实现,
所有这个接口的实现类都会通过继承得到这个方法
由于同一个方法可以从不同接口引入,自然而然的会有冲突的现象,规则如下:
1)一个声明在类里面的方法优先于任何默认方法
2)优先选取最具体的实现
从接口里不能提供对equals,hashCode或toString的默认实现。
因为若可以会很难确定什么时候该调用接口默认的方法。
如果一个类实现了一个方法,那总是优先于默认的实现的。
一旦所有接口的实例都是Object的子类,
所有接口实例都已经有对equals/hashCode/toString等方法非默认 实现。
因此,一个在接口上的这些默认方法都是没用的,它也不会被编译。
(简单地讲,每一个java类都是Object的子类,
也都继承了它类中的equals/hashCode/toString方法,
那么在类的接口上包含这些默认方法是没有意义的,它们也从来不会被编译。)
1)都是抽象类型;
2)都可以有实现方法(以前接口不行);
3)都可以不需要实现类或者继承者去实现所有方法,(以前不行,现在接口中默认方法不需要实现者实现)
不同点:
1)抽象类不可以多重继承,接口可以(无论是多重类型继承还是多重行为继承);
2)抽象类和接口所反映出的设计理念不同。
其实抽象类表示的是"is-a"关系,接口表示的是"like-a"关系;
3)接口中定义的变量默认是public static final 型,且必须给其初值,
所以实现类中不能重新定义,也不能改变其值;
抽象类中的变量默认是 friendly 型,其值可以在子类中重新定义,也可以重新赋值。
如果一个类、类属变量及方法不以public,protected,private这三种修饰符来修饰,
它就是friendly类型的,那么包内的任何类都可以访问它,
而包外的任何类都不能访问它(包括包外继承了此类的子类),
因此,这种类、类属变量及方法对包内的其他类是友好的,开放的,
而对包外的其他类是关闭的。
常量
抽象方法
默认方法
静态方法
私有方法
私有静态方法
String TYPE_NAME = "java seven interface";
int TYPE_AGE = 20;
String TYPE_DES = "java seven interface description";
default void method01(){
//TODO
}
default void method02(String message){
//TODO
}
private void method(){
//TODO
}
// Any other abstract methods
void method03();
void method04(String arg);
...
String method05();
}
具体体现在重写(Override)、重载(Overload)、向上转型。
将类型由原来的具体的类型参数化
泛型的目的是实现Java的类型安全。
用于编译器在编译期进行类型检查,因此代码经过编译之后就不存在了。
消除强制类型转换,增强代码可读性,减少错误率。
运行时擦除
在使用/调用时传入具体的类型(类型实参)
类型通配符上限通过形如Box<? extends Number>
类型通配符下限为Box<? super Number>
除了在类名后面添加了类型参数声明部分。
和泛型方法一 样,
泛型类的类型参数声明部分也包含一个或多个类型参数,
参数间用逗号隔开。
一个泛型参数, 也被称为一个类型变量,
是用于指定一个泛型类型名称的标识符。
因为他们接受一个或多个参数,
这些类被称为参数化的类或参数化的类型。
该方法在调用时可以接收不同类型的参数。
根据传递给泛型方法的参数 类型,
编译器适当地处理每一个方法调用。
2. <? super T>表示该通配符所代表的类型是T类型的父类
代 替 具 体 的 类 型 参 数 。
例 如 List<?> 在逻辑上是 List<String>,
List<Integer> 等所有List<具体类型实参>的父类。
基本上都是在编译器这个层次来实现的。
在生成的 Java 字节代码中是不包含泛 型中的类型信息的。
使用泛型的时候加上的类型参数,
会被编译器在编译的时候去掉。
这个过程就称为类型擦除。
newInstance() 获取类实例,
getMethod(..) 获取方法,
使用 invoke(..) 进行方法调用,
通过 setAccessible 修改私有变量/方法的访问限制。
2.获取属性/方法的时候有无“Declared”的区别是,
带有 Declared 修饰的方法或属性,
可以获取本类的所有方法或属性(private 到 public),
但不能获取到父类的任何信息;
非 Declared 修饰的方法或属性,
只能获取 public 修饰的方法或属性,
并可以获取到父类的信息,比如 getMethod(..)和getDeclaredMethod(..)。
通过getClass() -> 示例:new PeopleImpl().getClass()
直接获取.class -> 示例:PeopleImpl.class
getSuperclass():获取类的父类;
newInstance():创建实例对象;
getFields():获取当前类和父类的public修饰的所有属性;
getDeclaredFields():获取当前类(不包含父类)的声明的所有属性;
getMethod():获取当前类和父类的public修饰的所有方法;
getDeclaredMethods():获取当前类(不包含父类)的声明的所有方法;
public static void main(String[] args) {
Class myClass = Class.forName("example.PeopleImpl");
// 调用静态(static)方法
Method getSex = myClass.getMethod("getSex");
getSex.invoke(myClass);
}
复制代码
静态方法的调用比较简单,
使用 getMethod(xx) 获取到对应的方法,
直接使用 invoke(xx)就可以了。
Class myClass = Class.forName("example.PeopleImpl");
Object object = myClass.newInstance();
Method method = myClass.getMethod("sayHi",String.class);
method.invoke(object,"老王");
普通非静态方法调用,需要先获取类实例,通过“newInstance()”方法获取,
getMethod 获取方法,可以声明需要传递的参数的类型。
Class myClass = Class.forName("example.PeopleImpl");
Object object = myClass.newInstance();
Method privSayHi = myClass.getDeclaredMethod("privSayHi");
privSayHi.setAccessible(true); // 修改访问限制
privSayHi.invoke(object);
除了“getDeclaredMethod(xx)”可以看出,
调用私有方法的关键是设置 setAccessible(true) 属性,
修改访问限制,这样设置之后就可以进行调用了。
但是这种方法要求 该Class对象对应的类有默认的空构造器。
再调用Constructor对象的newInstance() 方法来创建 Class对象对应类的实例,
通过这种方法可以选定构造方法创建实例。
所以 JVM 无法对这些代码进行优化。
因此反射操作的效率要比那些非反射操作低得多。
应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。
如果一个程序必须在有安全限制的环境中运行,
如 Applet,那么这就是个问题了。
所以使用反射可能会导致意料之外的副作用,
导致代码功能失调并破坏可移植性。
反射代码破坏了抽象性,
因此当平台发生改变的时候,
代码的行为就有可能也随着变化。
参数:
deprecation: 使用了过时的类或方法时的警告
unchecked: 执行了未检查的转换时的警告
fallthrough: 当Switch程序块直接通往下一种情况而没有Break时的警告
path: 在类路径,源文件路径等中有不存在的路径时的警告
serial: 当在可序列化的类上缺少serialVersionUID定义时的警告
all: 关于以上所有情况的警告
(Java8新增)
1、CONSTRUCTOR:用于描述构造器
2、FIELD:用于描述字段
3、LOCAL_VARIABLE:用于描述局部变量
4、METHOD:用于描述方法
5、PACKAGE:用于描述包
6、PARAMETER:用于描述参数
7、TYPE:用于描述类、接口(包括注解类型) 或enum声明
如:@Target(ElementType.TYPE)
1、SOURCE:在源文件中有效(即源文件保留)
2、CLASS:在class文件中有效(即class保留)
3、RUNTIME:在运行时有效(即运行时保留)
如:@Retention(RetentionPolicy.RUNTIME)
(Java8新增)
String
Class
enum
Annotation
以上类型的数组
Constructor
Field
Method
Package
getAnnotations: 返回程序该元素上存在的所有注解
isAnnotationPresent: 判断该程序元素上是否包含制定类型的注解,存在返回true, 否则返回fase
getDeclaredAnnotations: 返回存在于此元素上的所有注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitProvider {
/**供应商编号*/
public int id() default -1;
/*** 供应商名称*/
public String name() default "";
/** * 供应商地址*/
public String address() default "";
}
public class Apple {
@FruitProvider(id = 1, name = "陕西红富士集团", address = "陕西省西安市延安路")
private String appleProvider;
public void setAppleProvider(String appleProvider) {
this.appleProvider = appleProvider;
}
public String getAppleProvider() {
return appleProvider;
}
}
public class FruitInfoUtil {
public static void getFruitInfo(Class<?> clazz) {
String strFruitProvicer = "供应商信息:";
Field[] fields = clazz.getDeclaredFields();//通过反射获取处理注解
for (Field field : fields) {
if (field.isAnnotationPresent(FruitProvider.class)) {
FruitProvider fruitProvider = (FruitProvider) field.getAnnotation(FruitProvider.class);
//注解信息的处理地方
strFruitProvicer = " 供应商编号:" + fruitProvider.id() + " 供应商名称:"
+ fruitProvider.name() + " 供应商地址:"+ fruitProvider.address();
System.out.println(strFruitProvicer);
}
}
}
}
public static void main(String[] args) {
FruitInfoUtil.getFruitInfo(Apple.class);
/***********输出结果***************/
// 供应商编号:1 供应商名称:陕西红富士集团 供应商地址:陕西省西安市延
}
}
和 ObjectInputStream
对对象进行序列化及反序列化
自定义序列化策略
不仅取决于类路径和功能代码是否一致,
一个非常重要的一点是两个 类的序列化 ID 是否一致
(就是 private static final long serialVersionUID)
序列化子父类说明
可以阻止该变量被序列化到文件中,
在被反序列 化后,transient 变量的值被设为初始值,
如 int 型的是 0,对象型的是 null
对象中有一些数据是敏感的,
比如密码字符串 等,希望对该密码字段在序列化时,
进行加密,而客户端如果拥有解密的密钥,
只有在 客户端进行反序列化时,
才可以对密码进行读取,
这样可以一定程度保证序列化对象的 数据安全
兼容性最好,但性能一般且不支持跨语言。
建议设置 serialVersionUID 字段值,
否则编译器编译器会根据类的内部实现 ,
包括类名、接口名、方法和属性等来自动生成 ,
源代码重新编译后可能会变化。
private static final long serialVersionUID = 362498820763181265L;
修改类时需要根据兼容性决定是否修改 serialVersionUID 的值,
只有在不兼容升级时才修改,避免反序列化混乱。
自描述序列化类型。不依赖外部描述文件或接口定义 ,
用一个字节表示常用基础类型,极大缩短二进制流。
语言无关,支持脚本语言。
协议简单,比 Java 原生序列化高效。
Hessian 会把复杂对象所有属性存储在一个 Map 中进行序列化。
所以在父类、子类存在同名成员变量的情况下,Hessian 序列化时,
先序列化子类,然后序列化父类,
因此反序列化结果会导致子类同名成员变量被父类的值覆盖。
在反序列化时需要重新提供。
但可读性更好,方便调试。
我们需要理解的是这实际上复制的是引用,
也就是 说a1和a2指向的是同一个对象。
因此,当a1变化的时候,a2 里面的成员变量也会跟 着变化。
如果字段是值类型的, 那么对该字段执行复制;
如果该字段是引用类型的话,则复制引用但不复制引用的对象。
因此,原始对象及其副本引用同一个对象。
常常可以先使对象实现Serializable接口,
然后把对 象(实际上只是对象的一个拷贝)写到一个流里,
再从流里读出来,便可以重建对象。
根据定义的方式不同,
内部类分为静态内部类,
成员内部类,
局部内部类,
匿名内部类四种。
private static int a;
private int b;
public static class Inner {
public void print() {
System.out.println(a);
}
}
}
2. 静态内部类和一般类一致,可以定义静态变量、方法,构造方法等。
3. 其它类使用静态内部类需要使用“外部类.静态内部类”方式,
如下所示:Out.Inner inner = new Out.Inner();inner.print();
4. Java集合类HashMap内部就有一个静态内部类Entry。
Entry是HashMap存放元素的抽象, HashMap 内部维护 Entry 数组用了存放元素,
但是 Entry 对使用者是透明的。
像这种和外部 类关系密切的,且不依赖外部类实例的,
都可以使用静态内部类。
private static int a;
private int b;
public class Inner {
public void print() {
System.out.println(a);
System.out.println(b);
}
}
}
成员内部类不能定义静态方法和变量(final 修饰的 除外)。
这是因为成员内部类是非静态的,
类初始化的时候先初始化静态成员,
如果允许成员内 部类定义静态变量,
那么成员内部类的静态变量初始化顺序是有歧义的。
private static int a;
private int b;
public void test(final int c) {
final int d = 1;
class Inner {
public void print() {
System.out.println(c);
}
}
}
}
如果一个类只在某个方法中使用,
则可以考虑使用局部类。
(要继承一个父类或者实现一个接口、
直接使用 new 来生成一个对象的引用)
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public abstract int fly();
}
public class Test {
public void test(Bird bird){
System.out.println(bird.getName() + "能够飞 " + bird.fly() + "米");
}
public static void main(String[] args) {
Test test = new Test();
test.test(new Bird() {
public int fly() {
return 10000;
}
public String getName() {
return "大雁";
}
});
}
}
当然也仅能只继承一个父类或者实现一 个接口。
同时它也是没有class关键字,
这是因为匿名内部类是直接使用new来生成一个对象的引 用。
// 成员内部类
private class InstanceInnerClass {}
// 静态内部类
static class StaticInnerClass {}
public static void main(String[] args) {
// 匿名内部类
(new Thread() {}).start();
(new Thread() {}).start();
// 方法内部类
class MethodClass1 {}
class MethodClass2 {}
}
}
应用程序不会抛出该类对象。
如果 出现了这样的错误,除了告知用户,剩下的就是尽力使程序安全的终止。
NullPointerException、
ConcurrentModificationException、
IndexOutOfBoundsException、
ClassCastException、
NoSuchElementException
一般是外部错误,这种异常都发生在编译阶段,
Java 编译器会强 制程序去捕获此类异常,
即会出现要求你把这段可能出现异常的程序进行 try catch,
该类异常一 般包括几个方面:
1. 试图在文件尾部读取数据
2. 试图打开一个错误格式的URL
3. 试图根据给定的字符串查找class对象,而这个字符串表示的类并不存在
一个throws,
还有一种系统自动抛异常
1. throws 用在函数上,后面跟的是异常类,可以跟多个;而 throw 用在函数内,后面跟的 是异常对象。
throws 用来声明异常,让调用者只知道该功能可能出现的问题,
可以给出预先的处理方 式;
throw抛出具体的问题对象,执行到throw,功能就已经结束了,
跳转到调用者,并 将具体的问题对象抛给调用者。
也就是说 throw 语句独立存在时,
下面不要定义其他语 句,因为执行不到。
throws 表示出现异常的一种可能性,
并不一定会发生这些异常;
throw 则是抛出了异常,
执行throw则一定抛出了某种异常对象。
两者都是消极处理异常的方式,
只是抛出或者可能抛出异常,
但是不会由函数去处理异 常,
真正的处理异常由函数的上层调用处理。
从 JDK 1.7 开始,可以使用 Paths 和 Files 代替 File。
递归地列出一个目录下的所有文件:
public static void listAllFiles(File dir) {
if (dir == null || !dir.exists()) {
return;
}
if (dir.isFile()) {
System.out.println(dir.getName());
return;
}
for (File file : dir.listFiles()) {
listAllFiles(file);
}
}
使用 InputStream、OutputStream 读取或写入字节,如操作图片文件等。
public static void copyFile(String src, String dist) throws IOException {
FileInputStream in = new FileInputStream(src);
FileOutputStream out = new FileOutputStream(dist);
byte[] buffer = new byte[20 * 1024];
int cnt;
// read() 最多读取 buffer.length 个字节
// 返回的是实际读取的个数
// 返回 -1 的时候表示读到 eof,即文件尾
while ((cnt = in.read(buffer, 0, buffer.length)) != -1) {
out.write(buffer, 0, cnt);
}
in.close();
out.close();
}
Java I/O 使用了装饰者模式来实现。以 InputStream 为例,
InputStream 是抽象组件;
FileInputStream 是 InputStream 的子类,属于具体组件,提供了字节流的输入操作;
FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能。例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。
实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可。
FileInputStream fileInputStream = new FileInputStream(filePath);
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
DataInputStream 装饰者提供了对更多数据类型进行输入的操作,比如 int、double 等基本类型。
使用 Reader、Writer 操作字符(如 1个 char = 8 bit),
增加了字符编码解码等功能,
适用于从文件中读取信息。
编码就是把字符转换为字节,而解码是把字节重新组合成字符。
如果编码和解码过程使用不同的编码方式那么就出现了乱码。
GBK 编码中,中文字符占 2 个字节,英文字符占 1 个字节;
UTF-8 编码中,中文字符占 3 个字节,英文字符占 1 个字节;
UTF-16be 编码中,中文字符和英文字符都占 2 个字节。
UTF-16be 中的 be 指的是 Big Endian,也就是大端。
相应地也有 UTF-16le,le 指的是 Little Endian,也就是小端。
Java 的内存编码使用双字节编码 UTF-16be,这不是指 Java 只支持这一种编码方式,
而是说 char 这种类型使用 UTF-16be 进行编码。
char 类型占 16 位,也就是两个字节,
Java 使用这种双字节编码是为了让一个中文或者一个英文都能使用一个 char 来存储。
String 可以看成一个字符序列,
可以指定一个编码方式将它编码为字节序列,
也可以指定一个编码方式将一个字节序列解码为 String。
String str1 = "中文";
byte[] bytes = str1.getBytes("UTF-8");
String str2 = new String(bytes, "UTF-8");
System.out.println(str2);
在调用无参数 getBytes() 方法时,
默认的编码方式不是 UTF-16be。
双字节编码的好处是可以使用一个 char 存储中文和英文,
而将 String 转为 bytes[] 字节数组就不再需要这个好处,
因此也就不再需要双字节编码。
getBytes() 的默认编码方式与平台有关,一般为 UTF-8。
byte[] bytes = str1.getBytes();
不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符。
但是在程序中操作的通常是字符形式的数据,
因此需要提供对字符进行操作的方法。
InputStreamReader 实现从字节流解码成字符流;
OutputStreamWriter 实现字符流编码成为字节流。
public static void readFileContent(String filePath) throws IOException {
FileReader fileReader = new FileReader(filePath);
BufferedReader bufferedReader = new BufferedReader(fileReader);
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
// 装饰者模式使得 BufferedReader 组合了一个 Reader 对象
// 在调用 BufferedReader 的 close() 方法时会去调用 Reader 的 close() 方法
// 因此只要一个 close() 调用即可
bufferedReader.close();
}
对于 Socket 服务端,在传统 IO 中,
给每个接入的客户端请求都创建一个线程处理会导致巨大的开销(线程频繁创建、切换),
尽管可以通过线程池来管理工作线程,
但当连接数急剧上升时这种方式也无法避免线程上下文切换的巨大开销:
用于表示网络上的硬件资源,即 IP 地址;
没有公有的构造函数,只能通过静态方法来创建实例。
InetAddress.getByName(String host);
InetAddress.getByAddress(byte[] address);
ServerSocket:服务器端类
Socket:客户端类
服务器和客户端通过 InputStream 和 OutputStream 进行输入输出。
一个简单的 Socket 服务器实现:
public class DemoServer extends Thread {
private ServerSocket serverSocket;
public int getPort() {
return serverSocket.getLocalPort();
}
public void run() {
try {
// 服务端启动 ServerSocket,自动绑定一个空闲端口
serverSocket = new ServerSocket(0);
while (true) {
// 调用 accept 方法,阻塞等待客户端连接
Socket socket = serverSocket.accept();
RequestHandler requestHandler = new RequestHandler(socket);
requestHandler.start();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (serverSocket != null) {
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
};
}
}
}
public static void main(String[] args) throws IOException {
DemoServer server = new DemoServer();
server.start();
// 利用 Socket 模拟一个客户端,只进行连接、读取和打印
try (Socket client = new Socket(InetAddress.getLocalHost(), server.getPort())) {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
bufferedReader.lines().forEach(s -> System.out.println(s));
}
}
}
// 简化实现,不做读取,直接发送字符串
class RequestHandler extends Thread {
private Socket socket;
RequestHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try (PrintWriter out = new PrintWriter(socket.getOutputStream());) {
out.println("Hello world!");
out.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Datagram:使用 UDP 协议实现网络通信,其中:
DatagramSocket:通信类
DatagramPacket:数据包类
NIO 在 JDK 1.4 引入,是多路复用、同步非阻塞 IO:
新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的,弥补了原来的 I/O 的不足,提供了高速的、面向块的 I/O;
NIO 常常被叫做非阻塞 IO,主要是因为 NIO 在网络通信中的非阻塞特性被广泛使用。
I/O 以流的方式处理数据,
而 NIO 以块的方式处理数据。
面向流:一次处理一个字节数据。
为流式数据创建过滤器非常容易,链接几个过滤器,
以便每个过滤器只负责复杂处理机制的一部分。
缺点是 I/O 通常相当慢;
面向块:一次处理一个数据块,
按块处理数据比按流处理数据要快得多。
但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。
java.io.* 已经以 NIO 为基础重新实现,
可以利用 NIO 的一些特性。
例如 java.io.* 包中的一些类包含以块的形式读写数据的方法,
这使得即使在面向流的系统中,处理速度也会更快。
Charset.defaultCharset().encode("Hello world!"));
类似 Linux 系统的文件描述符(FileDescriptor),
是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。
与流的不同之处在于,
流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),
而通道是双向的,可以用于读、写或者同时用于读写。
DatagramChannel 通过 UDP 读写网络中数据。
SocketChannel 通过 TCP 读写网络中数据。
ServerSocketChannel 可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。
高效的数据容器,
数据进出通道都需要经过缓冲区。
本质上是一个数组,
但提供了对数据的结构化访问,
而且还可以跟踪系统的读/写进程。
除了布尔类型,
所有原始类型都提供了相应的 Buffer 实现(XxxBuffer)。
NIO 实现了 IO 多路复用中的 Reactor 模型,
一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去监听多个通道 Channel 上的事件,
从而让一个线程就可以处理多个事件。
通过配置监听的通道 Channel 为非阻塞,
那么当 Channel 上的 IO 事件还未到达时,
就不会进入阻塞状态一直等待,
而是继续轮询其它 Channel,
找到 IO 事件已经到达的 Channel 执行;
因为创建和切换线程的开销很大,
因此使用一个线程来处理多个事件而不是一个线程处理一个事件,
对于 IO 密集型的应用具有很好地性能。
public static void fastCopy(String src, String dist) throws IOException {
/* 获得源文件的输入字节流 */
FileInputStream fin = new FileInputStream(src);
/* 获取输入字节流的文件通道 */
FileChannel fcin = fin.getChannel();
/* 获取目标文件的输出字节流 */
FileOutputStream fout = new FileOutputStream(dist);
/* 获取输出字节流的文件通道 */
FileChannel fcout = fout.getChannel();
/* 为缓冲区分配 1024 个字节 */
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
while (true) {
/* 从输入通道中读取数据到缓冲区中 */
int r = fcin.read(buffer);
/* read() 返回 -1 表示 EOF */
if (r == -1) {
break;
}
/* 切换读写 */
buffer.flip();
/* 把缓冲区的内容写入输出文件中 */
fcout.write(buffer);
/* 清空缓冲区 */
buffer.clear();
}
}
或者利用 transferTo 或 transferFrom 方法实现(可能更快,更能利用操作系底层细节,避免不必要的拷贝和上下文切换):
public static void copyFileByChannel(File source, File dest) throws IOException {
try (
FileChannel sourceChannel = new FileInputStream(source).getChannel();
FileChannel targetChannel = new FileOutputStream(dest).getChannel();
){
for (long count = sourceChannel.size() ;count>0 ;) {
long transferred = sourceChannel.transferTo(sourceChannel.position(), count, targetChannel);
sourceChannel.position(sourceChannel.position() + transferred);
count -= transferred;
}
}
}
Selector selector = Selector.open();
否则使用选择器就没有任何意义了,
因为如果通道在某个事件上被阻塞,
那么服务器就不能响应其它事件,
必须等待这个事件处理完毕才能去处理其它事件
(而且注册也不允许,会抛出 ILLegalBlockingModeException 异常)。
ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
ssChannel.register(selector, SelectionKey.OP_ACCEPT); // 请求接入
在将通道注册到选择器上时,还需要指定要注册的具体事件,主要有以下几类:
SelectionKey.OP_CONNECT
SelectionKey.OP_ACCEPT
SelectionKey.OP_READ
SelectionKey.OP_WRITE
它们在 SelectionKey 的定义如下:
public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;
// 可以看出每个事件可以被当成一个位域,从而组成事件集整数,例如:
// int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
int num = selector.select();
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
// ...
} else if (key.isReadable()) {
// ...
}
keyIterator.remove();
}
并且服务器端有可能需要一直监听事件,
因此服务器端处理事件的代码一般会放在一个死循环内。
while (true) {
int num = selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
// ...
} else if (key.isReadable()) {
// ...
}
keyIterator.remove();
}
}
只有在 select 阶段是阻塞的(等待至少一个客户端请求接入),
可避免大量客户端连接时频繁线程切换的问题。
public class NIOServer {
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
ServerSocket serverSocket = ssChannel.socket();
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
serverSocket.bind(address);
while (true) {
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();
// 服务器会为每个新连接创建一个 SocketChannel
SocketChannel sChannel = ssChannel1.accept();
sChannel.configureBlocking(false);
// 这个新连接主要用于从客户端读取数据
sChannel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
SocketChannel sChannel = (SocketChannel) key.channel();
System.out.println(readDataFromSocketChannel(sChannel));
sChannel.close();
}
keyIterator.remove();
}
}
}
private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
StringBuilder data = new StringBuilder();
while (true) {
buffer.clear();
int n = sChannel.read(buffer);
if (n == -1) {
break;
}
buffer.flip();
int limit = buffer.limit();
char[] dst = new char[limit];
for (int i = 0; i < limit; i++) {
dst[i] = (char) buffer.get(i);
}
data.append(dst);
buffer.clear();
}
return data.toString();
}
}
public class NIOClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 8888);
OutputStream out = socket.getOutputStream();
String s = "hello world";
out.write(s.getBytes());
out.close();
}
}
提供全面的异步文件 IO 和文件系统访问支持(java.nio.file);
基于异步 Channel 的 IO(java.nio.channels);
在 Windows 上通过 IOCP、在 Linux 上通过 epoll 实现。
AsynchronousServerSocketChannel serverSock = AsynchronousServerSocketChannel.open().bind(sockAddr);
// 为异步操作指定 CompletionHandler 回调函数
serverSock.accept(serverSock, new CompletionHandler<>() {
@Override
public void completed(AsynchronousSocketChannel sockChannel, AsynchronousServerSocketChannel serverSock) {
serverSock.accept(serverSock, this);
// 另外一个 write(sock,CompletionHandler{})
sayHelloWorld(sockChannel, Charset.defaultCharset().encode
("Hello World!"));
}
// 省略其他路径处理方法...
});
它可以比常规的基于流或者基于通道的 I/O 快得多。
向内存映射文件写入可能是危险的,
只是改变数组的单个元素这样的简单操作,
就可能会直接修改磁盘上的文件。
修改数据与将数据保存到磁盘是没有分开的。
下面代码行将文件的前 1024 个字节映射到内存中,
map() 方法返回一个 MappedByteBuffer,它是 ByteBuffer 的子类。
因此,可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,
操作系统会在需要时负责执行映射。
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
异步:不等待当前调用返回,而通过事件、回调等机制实现任务间的次序关系。
非阻塞:不过 IO 操作是否结束,直接返回,相应操作在后台继续处理。
if (filePath == null) {
return null;
}
try {
byte[] b = Files.readAllBytes(Paths.get(filePath));
return Base64.getEncoder().encodeToString(b);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public static String decryptByBase64(String base64, String filePath) {
if (base64 == null && filePath == null) {
return "生成文件失败,请给出相应的数据。";
}
try {
Files.write(Paths.get(filePath), Base64.getDecoder().decode(base64), StandardOpenOption.CREATE);
} catch (IOException e) {
e.printStackTrace();
}
return "success";
}
注:该资源类必须要直接或间接实现 java.lang.AutoCloseable 接口,该接口只包含一个close方法
函数式编程关心数据的映射,命令式编程关心解决问题的步骤。
或使用@FunctionalInterface注解的接口。
@FunctionalInterface注解和@Override用法类似,
都是在编译期进行检查
Stream 的 map 方法接受Function<T, R> 作为参数,
filter 使用Predicate<T>作为参数,
forEach 使用Consumer<T>作为参数。
在IDE中可以使用 @FunctionalInterface 注解来验证该要求是否满足
① this关键字指向不同:匿名类中this指代当前匿名类对象,
而lambda表达式中this指向包含它的类的对象。
② 编译方式不同:Java编译器将lambda表达式编译成类的私有方法,
使用了Java 7的 invokedynamic 字节码指令来动态绑定这个方法。
注意:重复注解机制本身必须用@Repeatable注解。
增加了TYPE_PARAMETER、TYPE_USE两个枚举值,
扩展了注解的使用范围
最终操作返回一特定类型的计算结果,
而中间操作返回Stream本身,
这样就可以将多个操作依次串起来,形成链式调用。
Collectors.toSet()、
Collectors.groupingBy()(分组)、
Collectors.partitioningBy()(可替代if...else语句,进行条件划分)等方法
IntStream、FloatStream、
LongStream、DoubleStream等,
其中包括 range、rangeClosed、iterate 和 limit等方法
可用来简化foreach循环代码,
以及处理集合元素的计算、排序与统计等操作。
parallelStream(创建并行流对象)、
filter(过滤元素)、
map(映射)、
reduce、
collect(收集)、
count(计数)等方法
它表示默认格式(yyyy-MM-dd)的日期,
我们可以使用now()方法得到当前时间,
也可以提供输入年份、月份和日期的输入参数来创建一个LocalDate实例。
该类为now()方法提供了重载方法,我们可以传入ZoneId来获得指定时区的日期。
public void testLocalDate() {
//Current Date
LocalDate today = LocalDate.now();
System.out.println("Current Date=" + today);
//Creating LocalDate by providing input arguments
LocalDate firstDay_2014 = LocalDate.of(2014, Month.JANUARY, 1);
System.out.println("Specific Date=" + firstDay_2014);
//Try creating date by providing invalid inputs
//LocalDate feb29_2014 = LocalDate.of(2014, Month.FEBRUARY, 29);
//Exception in thread "main" java.time.DateTimeException:
//Invalid date 'February 29' as '2014' is not a leap year
//Current date in "Asia/Kolkata", you can get it from ZoneId javadoc
LocalDate todayKolkata = LocalDate.now(ZoneId.of("Asia/Kolkata"));
System.out.println("Current Date in IST=" + todayKolkata);
//java.time.zone.ZoneRulesException: Unknown time-zone ID: IST
//LocalDate todayIST = LocalDate.now(ZoneId.of("IST"));
//Getting date from the base date i.e 01/01/1970
LocalDate dateFromBase = LocalDate.ofEpochDay(365);
System.out.println("365th day from base date= " + dateFromBase);
LocalDate hundredDay2014 = LocalDate.ofYearDay(2014, 100);
System.out.println("100th day of 2014=" + hundredDay2014);
}
Current Date=2018-05-29
Specific Date=2014-01-01
Current Date in IST=2018-05-29
365th day from base date= 1971-01-01
100th day of 2014=2014-04-10
它的实例代表一个符合人类可读格式的时间,
默认格式是hh:mm:ss.zzz。像LocalDate一样,
该类也提供了时区支持,
同时也可以传入小时、分钟和秒等输入参数创建实例
public void testLocalTime() {
//Current Time
LocalTime time = LocalTime.now();
System.out.println("Current Time=" + time);
//Creating LocalTime by providing input arguments
LocalTime specificTime = LocalTime.of(12, 20, 25, 40);
System.out.println("Specific Time of Day=" + specificTime);
//Try creating time by providing invalid inputs
//LocalTime invalidTime = LocalTime.of(25,20);
//Exception in thread "main" java.time.DateTimeException:
//Invalid value for HourOfDay (valid values 0 - 23): 25
//Current date in "Asia/Kolkata", you can get it from ZoneId javadoc
LocalTime timeKolkata = LocalTime.now(ZoneId.of("Asia/Kolkata"));
System.out.println("Current Time in IST=" + timeKolkata);
//java.time.zone.ZoneRulesException: Unknown time-zone ID: IST
//LocalTime todayIST = LocalTime.now(ZoneId.of("IST"));
//Getting date from the base date i.e 01/01/1970
LocalTime specificSecondTime = LocalTime.ofSecondOfDay(10000);
System.out.println("10000th second time= " + specificSecondTime);
}
Current Time=19:09:39.656
Specific Time of Day=12:20:25.000000040
Current Time in IST=16:39:39.657
10000th second time= 02:46:40
它表示一组日期-时间,默认格式是yyyy-MM-dd-HH-mm-ss.zzz。
它提供了一个工厂方法,
接收LocalDate和LocalTime输入参数,
创建LocalDateTime实例。
public void testLocalDateTime() {
//Current Date
LocalDateTime today = LocalDateTime.now();
System.out.println("Current DateTime=" + today);
//Current Date using LocalDate and LocalTime
today = LocalDateTime.of(LocalDate.now(), LocalTime.now());
System.out.println("Current DateTime=" + today);
//Creating LocalDateTime by providing input arguments
LocalDateTime specificDate = LocalDateTime.of(2014, Month.JANUARY, 1, 10, 10, 30);
System.out.println("Specific Date=" + specificDate);
//Try creating date by providing invalid inputs
//LocalDateTime feb29_2014 = LocalDateTime.of(2014, Month.FEBRUARY, 28, 25,1,1);
//Exception in thread "main" java.time.DateTimeException:
//Invalid value for HourOfDay (valid values 0 - 23): 25
//Current date in "Asia/Kolkata", you can get it from ZoneId javadoc
LocalDateTime todayKolkata = LocalDateTime.now(ZoneId.of("Asia/Kolkata"));
System.out.println("Current Date in IST=" + todayKolkata);
//java.time.zone.ZoneRulesException: Unknown time-zone ID: IST
//LocalDateTime todayIST = LocalDateTime.now(ZoneId.of("IST"));
//Getting date from the base date i.e 01/01/1970
LocalDateTime dateFromBase = LocalDateTime.ofEpochSecond(10000, 0, ZoneOffset.UTC);
System.out.println("10000th second time from 01/01/1970= " + dateFromBase);
}
Current DateTime=2018-05-29T19:10:00.353
Current DateTime=2018-05-29T19:10:00.353
Specific Date=2014-01-01T10:10:30
Current Date in IST=2018-05-29T16:40:00.353
10000th second time from 01/01/1970= 1970-01-01T02:46:40
它以Unix时间戳的形式存储日期时间
public void testTimestampForInstant() {
//Current timestamp
Instant timestamp = Instant.now();
System.out.println("Current Timestamp = " + timestamp);
//Instant from timestamp
Instant specificTime = Instant.ofEpochMilli(timestamp.toEpochMilli());
System.out.println("Specific Time = " + specificTime);
//Duration example
Duration thirtyDay = Duration.ofDays(30);
System.out.println(thirtyDay);
}
如:加/减天数、周数、月份数,等等。
还有其他的工具方法能够使用TemporalAdjuster调整日期,
并计算两个日期间的周期。
public void testDateTool() {
LocalDate today = LocalDate.now();
//Get the Year, check if it's leap year
System.out.println("Year " + today.getYear() + " is Leap Year? " + today.isLeapYear());
//Compare two LocalDate for before and after
System.out
.println("Today is before 01/01/2015? " + today.isBefore(LocalDate.of(2015, 1, 1)));
//Create LocalDateTime from LocalDate
System.out.println("Current Time=" + today.atTime(LocalTime.now()));
//plus and minus operations
System.out.println("10 days after today will be " + today.plusDays(10));
System.out.println("3 weeks after today will be " + today.plusWeeks(3));
System.out.println("20 months after today will be " + today.plusMonths(20));
System.out.println("10 days before today will be " + today.minusDays(10));
System.out.println("3 weeks before today will be " + today.minusWeeks(3));
System.out.println("20 months before today will be " + today.minusMonths(20));
//Temporal adjusters for adjusting the dates
System.out.println(
"First date of this month= " + today.with(TemporalAdjusters.firstDayOfMonth()));
LocalDate lastDayOfYear = today.with(TemporalAdjusters.lastDayOfYear());
System.out.println("Last date of this year= " + lastDayOfYear);
Period period = today.until(lastDayOfYear);
System.out.println("Period Format= " + period);
System.out.println("Months remaining in the year= " + period.getMonths());
}
之后再解析一个字符串,得到日期时间对象,这些都是很常见的。
public void testFormat() {
//Format examples
LocalDate date = LocalDate.now();
//default format
System.out.println("Default format of LocalDate=" + date);
//specific format
System.out.println(date.format(DateTimeFormatter.ofPattern("d::MMM::uuuu")));
System.out.println(date.format(DateTimeFormatter.BASIC_ISO_DATE));
LocalDateTime dateTime = LocalDateTime.now();
//default format
System.out.println("Default format of LocalDateTime=" + dateTime);
//specific format
System.out.println(dateTime.format(DateTimeFormatter.ofPattern("d::MMM::uuuu HH::mm::ss")));
System.out.println(dateTime.format(DateTimeFormatter.BASIC_ISO_DATE));
Instant timestamp = Instant.now();
//default format
System.out.println("Default format of Instant=" + timestamp);
//Parse examples
LocalDateTime dt = LocalDateTime.parse("27::五月::2014 21::39::48",
DateTimeFormatter.ofPattern("d::MMM::uuuu HH::mm::ss"));
System.out.println("Default format after parsing = " + dt);
}
可配合Lambda表达式使用,使代码更简洁美观,
包括(工厂方法of、ofNullable)、filter、map、orElse、get等方法
isPresent() :判断容器中是否有值。
ifPresent(Consume lambda) :容器若不为空则执行括号中的Lambda表达式。
T get() :获取容器中的元素,若容器为空则抛出NoSuchElement异常。
T orElse(T other) :获取容器中的元素,若容器为空则返回括号中的默认值
Base64.getDecoder().decode(str)
实现了CompletionStage和Future接口
使用take、poll方法从队列中取出任务获得执行结果
但是高并发也就意味着CAS的失败次数会增多,
失败次数的增多会引起更多线程的重试,
最后导致AtomicLong的效率降低。
你需要编写一个方法,然后给它一个File,
它就会告诉你文件是不是隐藏的。
幸好,File类里面有一个叫作isHidden的方法。
我们可以把它看作一个函数,
接受一个File,返回一个布尔值。
但要用它做筛选,你需要把它包在一个FileFilter对象里,
然后传递给File.listFiles方法
public boolean accept(File file) {
return file.isHidden();
}
});
因此只需用Java 8的方法引用::语法
(即“把这个方法作为值”)将其传给listFiles方法
在Java 8里写下
File::isHidden的时候,你就创建了一个方法引用,你同样可以传递它
还有一个变量inventory保存着一个Apples的列表。
你可能想要选出所有的绿苹果,并返回一个列表。
通常我们用筛选(filter)一词来表达这个概念。
List
for (Apple apple: inventory){
if ("green".equals(apple.getColor())) {
result.add(apple);
}
}
return result;
}
List
for (Apple apple: inventory){
if (apple.getWeight() > 150) {
result.add(apple);
}
}
return result;
}
这样可以避免filter方法出现重复的代码。
return "green".equals(apple.getColor());
}
public static boolean isHeavyApple(Apple apple) {
return apple.getWeight() > 150;
}
//Predicate 从库中import即可
public interface Predicate
boolean test(T t);
}
static List
List
for (Apple apple: inventory){
if (p.test(apple)) {
result.add(apple);
}
}
return result;
}
filterApples(inventory, Apple::isGreenApple);
filterApples(inventory, Apple::isHeavyApple);
//引入Lambda
//代替isGreenApple(Apple apple)函数
filterApples(inventory, (Apple a) -> "green".equals(a.getColor()) );
//代替isHeavyApple(Apple apple)函数
filterApples(inventory, (Apple a) -> a.getWeight() > 150 );
你得用for-each循环一个个去迭代元素,然后再处理元素。
我们把这种数据迭代的方法称为外部迭代。
相反,有了Stream API,你根本用不着操心循环的事情。
数据处理完全是在库内部进行的。
我们把这种思想叫作内部迭代。
default void sort(Comparator c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator
for (Object e : a) {
i.next();
i.set((E) e);
}
}
}
把颜色作为参数
List
List
List
List
你考虑的是苹果,需要根据Apple的某些属性
(比如它是绿色的吗?重量超过150克吗?)
来返回一个boolean值。
我们把它称为谓词(即一个返回boolean值的函数)。
让我们定义一个接口来对选择标准建模
boolean test (Apple apple);
}
换句话说,你把filterApples方法的行为参数化了
了filterApples方法的新行为。
你还可以将List类型抽象化,从而超越你眼前要处理的问题
public interface Comparator
public int compare(T o1, T o2);
}
用sort方法表现出不同的行为。
inventory.sort(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()) );
public interface Runnable{
public void run();
}
Thread t = new Thread(new Runnable() {
public void run(){
System.out.println("Hello world");
}
});
Thread t = new Thread(() -> System.out.println("Hello world"));
简洁地表示可传递的匿名函数的一种方式:
它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表
但和方法一样,Lambda有参数列表、函数主体、返回类型,还可能有可以抛出的异常列表。
4,5无效
并把整个表达式作为函数式接口的实例
一个抽象方法run的函数式接口
我们将这种抽象方法叫作函数描述符。
因为它只有一个叫作run的抽象方法,这个方法什么也不接受,什么也不返回(void)。
Lambda表达式的签名要和函数式接口的抽象方法一样
r.run();
}
process( () -> System.out.println("This is awesome!!") );
将打印“This is awesome!!”。
Lambda表达式 ()-> System.out.println("This is awesome!!")不接受参数且返回void
如果你用@FunctionalInterface定义了一个接口,
而它却不是函数式接口的话,编译器将返回一个提示原因的错误。
以便它可以利用BufferedReader执行不同的行为
br.readLine() + br.readLine());
你需要创建一个能匹配 BufferedReader -> String,
还可以抛出IOException异常的接口。
让我们把这一接口叫作 BufferedReaderProcessor
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
…
}
为函数式接口的抽象方法提供实现,
并且将整个表达式作为函数式接口的一个实例。
对得到的BufferedReaderProcessor对象调用process方法执行处理
并以不同的方式处理文件
String oneLine = processFile((BufferedReader br) -> br.readLine());
处理两行:
String twoLines = processFile((BufferedReader br) -> br.readLine() + br.readLine());
定义了一个名叫test的抽象方法,
它接受泛型T对象,并返回一个boolean。
在需要表示一个涉及类型T的布尔表达式时,
就可以使用这个接口。
它接受泛型T的对象,没有返回(void)。
你如果需要访问类型T的对象,
并对其执行某些操作,就可以使用这个接口。
并对其中每个元素执行操作。
使用这个forEach方法,并配合Lambda来打印列表中的所有元素。
定义了一个叫作apply的方法,
它接受一个泛型T的对象,
并返回一个泛型R的对象
如果你需要定义一个Lambda,
将输入对象的信息映射
到输出,
就可以使用这个接口
(比如提取苹果的重量,或把字符串映射为它的长度)
以将一个String列表映射到包含每个String长度的Integer列表。
中Lambda表达式需要的类型称为目标类型。
第二,要求它是Predicate
第三,Predicate
第四,test方法描述了一个函数描述符,它可以接受一个Apple,并返回一个boolean。
最后,filter的任何实际参数都必须匹配这个要求。
同一个Lambda表达式就可以与不同的函数式接口联系起来,
只要它们的抽象方法签名能够兼容。
这两个接口都代表着什么也不接受且返回一个泛型T的函数
Callable<Integer>
//第二个赋值的目标类型是PrivilegedAction<Integer>
PrivilegedAction<Integer>
ToIntBiFunction<Apple, Apple>
BiFunction<Apple, Apple, Integer>
它就和一个返回void的函数描述符兼容(当然需要参数列表也兼容)。
例如,两行都是合法的,
尽管List的add方法返回了一个boolean,
而不是Consumer上下文(T -> void)所要求的void
Predicate<String>
// Consumer返回了一个void
Consumer<String>
推断出用什么函数式接口来配合Lambda表达式,
这意味着它也可以推断出适合Lambda的签名,
因为函数描述符可以通过目标类型来得到。
这样做的好处在于,
编译器可以了解Lambda表达式的参数类型,
这样就可以在Lambda语法中省去标注参数类型
例如,可以创建一个Comparator对象
(不是参数,而是在外层作用域中定义的变量),
就像匿名类一样。 它们被称作捕获Lambda。
Runnable r = () -> System.out.println(portNumber);
但局部变量必须显式声明为final,或事实上是final。
换句话说,Lambda表达式只能捕获指派给它们的局部变量一次。
(注:捕获实例变量可以被看作捕获最终局部变量this。)
实例变量都存储在堆中,
而局部变量则保存在栈上。
而且Lambda是在一个线程中使用的,
则使用Lambda的线程,
可能会在分配该变量的线程将这个变量收回之后,去访问该变量。
因此,Java在访问自由局部变量时,
实际上是在访问它的副本,
而不是访问原始变量。
如果局部变量仅仅赋值一次那就没有什么区别了——因此就有了这个限制。
(这种模式会阻碍很容易做到的并行处理)。
并像Lambda一样传递它们。
在一些情况下,比起使用Lambda表达式,
它们似乎更易读,感觉也更自然。
如果一个Lambda代表的只是“直接调用这个方法”,
那最好还是用名称来调用它,而不是去描述如何调用它。
但是,显式地指明方法的名称,你的代码的可读性会更好。
(假设你有一个局部变量expensiveTransaction用于存放Transaction类型的对象,
它支持实例方法getValue,那么你就可以写expensiveTransaction::getValue)。
而这个对象本身是Lambda的一个参数。
例如,Lambda表达式 (String s) -> s.toUppeCase()可以写作String::toUpperCase
你在Lambda中调用一个已经存在的外部对象中的方法。
例如,Lambda表达式 ()->expensiveTransaction.getValue()可以写作expensiveTransaction::getValue
和父类调用(super-call)的一些特殊形式的方法引用。
List的sort方法需要一个Comparator作为参数。
Comparator描述了
一个具有(T, T) -> int签名的函数描述符。
你可以利用String类中的compareToIgnoreCase
方法定义一个Lambda表达式(注意compareToIgnoreCase是String类中预先定义的)
str.sort((s1, s2) -> s1.compareToIgnoreCase(s2));
str.sort(String::compareToIgnoreCase);
你可以利用它的名称和关键字new来创建它的一个引用:
ClassName::new。它的功能与指向静态方法的引用类似。
它适合Supplier的签名() -> Apple。可以这样做
那么它就适合Function接口的签名,可以这样写
map方法传递给了Apple的构造函数,得到了一个具有不同重量苹果的List
那么它就适合BiFunction接口的签名,可以这样写
可以创建一个giveMeFruit方法,
给它一个String和一个Integer,
它就可以创建出不同重量的各种水果
比如Color(int, int, int),使用构造函数引用呢?
那么在这个例子里面就是Color::new。
但是需要与构造函数引用的签名匹配的函数式接口。
但是语言本身并没有提供这样的函数式接口,
可以自己创建一个
R apply(T t, U u, V v);
}
TriFunction<Integer, Integer, Integer, Color>
inventory.sort(comparing(Apple::getWeight));
sort方法的签名是这样的:
void sort(Comparator c)
这就是在Java中传递策略的方式:
它们必须包裹在一个对象里。
我们说sort的行为被参数化了:
传递给它的排序策略不同,其行为也会不同。
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
}
inventory.sort(new AppleComparator());
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
});
在需要函数式接口的地方可以使用Lambda表达式。
我们回顾一下:函数式接口就是仅仅定义一个抽象方法的接口。
抽象方法的签名(称为函数描述符)描述了Lambda表达式的签名。
苹果具体代表的就是(Apple, Apple) -> int。
-> a1.getWeight().compareTo(a2.getWeight())
);
它可以接受一个Function来提取Comparable键值,并生成一个Comparator对象
现在传递的Lambda只有一个参数:Lambda说明了如何从苹果中提取需要比较的键值
Comparator
inventory.sort(comparing((a) -> a.getWeight()));
可以用方法引用让代码更简洁(假设静态导入了java.util.Comparator.comparing)
比如,可以让两个谓词之间做一个or操作,组合成一个更大的谓词。
而且,还可以让一个函数的结果成为另一个函数的输入。
(毕竟,这违背了函数式接口的定义啊!)
窍门在于,即将介绍的方法都是默认方法,
也就是说它们不是抽象方法。
根据提取用于比较的键值的Function来返回一个Comparator,
如下所示:
Comparator<Apple>
接口有一个默认方法reversed可以使给定的比较器逆序。
因此仍然用开始的那个比较器,
只要修改一下前一个例子就可以对苹果按重量递减排序
哪个苹果应该排在前面呢?
可能需要再提供一个Comparator来进一步定义这个比较。
可能想要按原产国排序。
thenComparing方法就是做这个用的。
它接受一个函数作为参数(就像comparing方法一样),
如果两个对象用第一个Comparator比较之后是一样的,
就提供第二个Comparator。
可以重用已有的Predicate来创建更复杂的谓词。
从左向右确定优先级的。因此,a.or(b).and(c)可以看作(a || b) && c
Function接口为此配了andThen和compose两个默认方法,
它们都会返回Function的一个实例
它先对输入应用一个给定函数,
再对输出应用另一个函数
另一个函数g给数字乘2,
你可以将它们组合成一个函数h,先给数字加1,再给结果乘2
先把给定的函数用作compose的参数里面给的那个函数,
然后再把函数本身用于结果
而andThen则意味着g(f(x))
(通过查询语句来表达,而不是临时编写一个实现)。
并按照卡路里排序
可以访问特定元素类型的一组有序值。
素(如ArrayList 与 LinkedList)。
filter、sorted和map。集合讲的是数据,流讲的是计算
请注意,从有序集合生成流时会保留原有的顺序。
由列表生成的流,其元素顺序与列表一致
以及函数式编程语言中的常用操作,
如filter、map、reduce、find、match、sort等。
流操作可以顺序执行,也可并行执行。
流水线的操作可以看作对数据源进行数据库式查询。
它包含数据结构中目前所有的值——集合中的每个元素都得先算出来才能添加到集合中。
(可以往集合里加东西或者删东西,但是不管什么时候,
集合中的每个元素都是放在内存里的,元素都得先算出来才能成为集合的一部分。)
只有在消费者要求的时候才会计算值
(用管理学的话说这就是需求驱动,甚至是实时制造)
你可以从原始数据源那里再获得一个新的流来重新遍历一遍,就像迭代器一样
(这里假设它是集合之类的可重复的源,如果是I/O通道就没戏了)。
相反,Streams库使用内部迭代——它帮你把迭代做了,
还把得到的流值存在了某个地方,
你只要给出一个函数说要干什么就可以了。
重要的是,除非流水线上触发一个终端操作,
否则中间操作不会执行任何处理——它们很懒。
这是因为中间操作一般都可以合并起来,
在终端操作时一次性全部处理。
其结果是任何不是流的值,比如List、Integer,甚至void。
它会对源中的每道菜应用一个Lambda。
把System.out.println传递给forEach,
并要求它打印出由menu生成的流中的每一个Dish
一个中间操作链,形成一条流的流水线;
一个终端操作,执行流水线,并能生成结果。
接受一个谓词(一个返回boolean的函数)作为参数,
并返回一个包括所有符合谓词的元素的流。
(根据流所生成元素的hashCode和equals方法实现)的流
所需的长度作为参数传递给limit。
如果流是有序的,则最多会返回前n个元素。
这种情况下,limit的结果不会以任何顺序排列。
如果流中元素不足n个,则返回一个空流。
请注意,limit(n)和skip(n)是互补的!
menu.stream()
.filter(d -> d.getType() == Dish.Type.MEAT)
.limit(2)
.collect(toList());
Stream API也通过map和flatMap方法提供了类似的工具
这个函数会被应用到每个元素上,
并将其映射成一个新的元素
(使用映射一词,是因为它和转换类似,
但其中的细微差别在于它是“创建一个新版本”而不是去“修改”)。
来提取流中菜肴的名称
.map(Dish::getName)
.collect(toList());
你想要返回另一个列表,
显示每个单词中有几个字母。
应用的函数应该接受一个单词,并返回其长度。
给map传递一个方法引用String::length来解决这个问题
List
.map(String::length)
.collect(toList());
.map(Dish::getName)
.map(String::length)
.collect(toList());
起来成为一个流
例如,给定单词列表["Hello","World"],你想要返回列表["H","e","l", "o","W","r","d"]
把每个单词映射成一张字符表,
然后调用distinct来过滤重复的字符。
.map(word -> word.split(""))
.distinct()
.collect(toList());
因此,map返回的流实际上是Stream<String[]>
你真正想要的是用Stream<String>
有一个叫作Arrays.stream()的方法可以接受
一个数组并产生一个流,
Stream
所有使用map(Arrays::stream)时生成的单个流都被合并起来,即扁平化为一个流。
例如,给定[1, 2, 3, 4,5],应该返回[1, 4, 9, 16, 25]。
并返回该数字平方的Lambda来解决这个问题。
List squares =
numbers.stream()
.map(n -> n * n)
.collect(toList());
给定列表[1, 2, 3]和列表[3, 4],应该返回[(1, 3), (1, 4), (2, 3), (2, 4), (3, 3), (3, 4)]。
但这样会返回一个Stream-
你需要让生成的流扁平化,以得到一个Stream
这正是flatMap所做的
List<Integer>
List<int[]>
numbers1.stream()
.flatMap(i -> numbers2.stream()
.map(j -> new int[]{i, j})
)
.collect(toList());
因为在flatMap操作后,你有了一个代表数对的int[]流,
所以只需要一个谓词来检查总和是否能被3整除就可以了
List
List
numbers1.stream()
.flatMap(i ->
numbers2.stream()
.filter(j -> (i + j) % 3 == 0)
.map(j -> new int[]{i, j})
)
.collect(toList());
Stream API通过allMatch、anyMatch、noneMatch、findFirst和findAny方法提供了这样的工具
System.out.println("The menu is (somewhat) vegetarian friendly!!");
}
但它会看看流中的元素是否都能匹配给定的谓词
.allMatch(d -> d.getCalories() < 1000);
.noneMatch(d -> d.getCalories() >= 1000);
例如,假设你需要对一个用and连起来的大布尔表达式求值。
不管表达式有多长,你只需找到一个表达式为false,
就可以推断整个表达式将返回false,所以用不着计算整个表达式。
你可以结合使用filter和findAny方法来实现这个查询
menu.stream()
.filter(Dish::isVegetarian)
.findAny();
这样就不用返回众所周知容易出问题的null了。
我们在第3章介绍了Consumer函数式接口;
它让你传递一个接收T类型参数,
并返回void的Lambda表达式。
(比如由List或排序好的数据列生成的流)。
对于这种流,你可能想要找到第一个元素。
为此有一个findFirst方法,它的工作方式类似于findany。
Optional
someNumbers.stream()
.map(x -> x * x)
.filter(x -> x % 3 == 0)
.findFirst(); // 9
将流中所有元素反复结合起来,得到一个值,比如一个Integer。
这样的查询可以被归类为归约操作(将流归约成一个值)。
用函数式编程语言的术语来说,这称为折叠(fold),
因为你可以将这个操作看成把一张长长的纸(你的流)反复折叠成一个小方块,
而这就是折叠操作的结果。
用加法运算符反复迭代来得到结果。
通过反复使用加法,
把一个数字列表归约成了一个数字。
int sum = 0;
for (int x : numbers) {
sum += x;
}
总和变量的初始值,在这里是0;
将列表中所有元素结合在一起的操作,在这里是+
reduce操作对这种重复应用的模式做了抽象。
可以像下面这样对流中所有的元素求和
//起始值0很重要
一个初始值,这里是0;
一个BinaryOperator<T>
这里我们用的是lambda (a, b) -> a + b。
//起始值1很重要
//起始值0很重要
reduce操作无法返回其和,
因为它没有初始值。
这就是为什么结果被包裹在一个Optional对象里,
以表明和可能不存在。
reduce接受两个参数:
一个初始值
一个Lambda来把两个流元素结合起来并产生一个新值
Lambda是一步步用加法运算符应用到流中每个元素上的,
需要给定两个元素能够返回最大值的Lambda,
reduce操作会考虑新值和流中下一个元素,
并产生一个新的最大值,直到整个流消耗完
.map(d -> 1)
.reduce(0, (a, b) -> a + b);
IntStream、DoubleStream和LongStream,
分别将流中的元素特化为int、long和double,
从而避免了暗含的装箱成本
这些方法和前面说的map方法的工作方式一样,
只是它们返回的是一个特化流,而不是Stream<T>
(每个int都会装箱成一个Integer),
可以使用boxed方法
也分别有一个Optional原始类型特化版本:
OptionalInt、OptionalDouble和OptionalLong
两个可以用于IntStream和LongStream的静态方法,
帮助生成这种范围:
range和rangeClosed。
这两个方法都是第一个参数接受起始值,
第二个参数接受结束值。
但range是不包含结束值的,而rangeClosed则包含结束值。
Stream<String>
例如,一个很有用的方法是Files.lines,它会返回一个由指定文件中的各行构成的字符串流
这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。由iterate
和generate产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去!一般来说,
应该使用limit(n)来对这种流加以限制,以避免打印无穷多个值。
但generate不是依次
对每个新生成的值应用函数的。
它接受一个Supplier<T>
对流调用
collect方法将对流中的元素触发一个归约操作(由Collector来参数化)
List<Transaction>
元素分组
元素分区
但凡要把流中所有的项目合并成一个结果时就可以用。
这个结果可以是任何类型,
可以复杂如代表一棵树的多级映射,
或是简单如一个整数
数一数菜单里有多少种菜
来计算流中的最大或最小值。
这两个收集器接收一个Comparator参数来比较流中的元素。
并把它传递给Collectors.maxBy
Comparator.comparingInt(Dish::getCalories);
Optional<Dish>
menu.stream()
.collect(maxBy(dishCaloriesComparator));
它可接受一个把对象映射为求和所需int的函数,并返回一个收集器;
该收集器在传递给普通的collect方法后即执行我们需要的汇总操作
而且希望只需一次操作就可以完成。
在这种情况下,可以使用summarizingInt工厂方法返回的收集器。
并得到菜肴热量总和、平均值、最大值和最小值
menu.stream().collect(summarizingInt(Dish::getCalories));
它提供了方便的取值(getter)方法来访问结果。
打印menuStatisticobject会得到以下输出:
IntSummaryStatistics{count=9, sum=4300, min=120,
average=477.777778, max=800}
把对流中每一个对象应用toString方法得到的所有字符串连接成一个字符串。
joining在内部使用了StringBuilder来把生成的字符串逐个追加起来
那你无需用提取每一道菜名称的函数来对原流做映射就能够得到相同的结果
String shortMenu = menu.stream().collect(joining());
porkbeefchickenfrench friesriceseason fruitpizzaprawnssalmon
Map<
{FISH=[prawns, salmon], OTHER=[french fries, rice, season fruit, pizza],
MEAT=[pork, beef, chicken]}
它提取了流中每一道Dish的Dish.Type。
我们把这个Function叫作分类函数,
因为它用来把流中的元素分成不同的组。
热量400到700卡路里的菜划为“普通”(normal),
高于700卡路里的划为“高热量”(fat)。
它除了普通的分类函数之外,还可以接受collector类型的第二个参数。
那么要进行二级分组的话,我们可以把一个内层groupingBy传递给外层groupingBy,
并定义一个为流中项目分类的二级标准
但进一步说,传递给第一个groupingBy的第二个收集器可以是任何类型,
而不一定是另一个groupingBy。
普通的单参数groupingBy(f)(其中f是分类函数)
实际上是groupingBy(f, toList())的简便写法
可以传递counting收集器作为groupingBy收集器的第二个参数
groupingBy(Dish::getType, counting()));
{MEAT=3, FISH=2, OTHER=4}
按照菜的类型分类
menu.stream()
.collect(groupingBy(Dish::getType,
maxBy(comparingInt(Dish::getCalories))));
以包装了该类型中热量最高的Dish的Optional<Dish>
{FISH=Optional[salmon], OTHER=Optional[pizza], MEAT=Optional[pork]}
这个收集器相当于旧收集器的一个包装,collect操作的最后一步就是将返回值用转换函数做一个映射。
在这里,被包起来的收集器就是用maxBy建立的那个,
而转换函数Optional::get则把返回的Optional中的值提取出来。
前面已经说过,这个操作放在这里是安全的,
因为reducing收集器永远都不会返回Optional.empty()。
{FISH=salmon, OTHER=pizza, MEAT=pork}
将会对分到同一组中的所有流元素执行进一步归约操作
不过这次是对每一组Dish求和
menu.stream().collect(groupingBy(Dish::getType,
summingInt(Dish::getCalories)));
这个方法接受两个参数:
一个函数对流中的元素做变换,
另一个则将变换的结果对象收集起来。
其目的是在累加之前对每个输入元素应用一个映射函数,
这样就可以让接受特定类型元素的收集器适应不同类型的对象。
菜单中都有哪些CaloricLevel
menu.stream().collect(
groupingBy(Dish::getType, mapping(
dish -> { if (dish.getCalories() <= 400) return CaloricLevel.DIET;
else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT; },
toSet() )));
生成的CaloricLevel流传递给一个toSet收集器,
它和toList类似,不过是把流中的元素累积到一个Set而不是List中,
以便仅保留各不相同的值
得到这样的Map结果
{OTHER=[DIET, NORMAL], MEAT=[DIET, NORMAL, FAT], FISH=[DIET, NORMAL]}
例如,你可以给它传递一个构造函数引用来要求HashSet
menu.stream().collect(
groupingBy(Dish::getType, mapping(
dish -> { if (dish.getCalories() <= 400) return CaloricLevel.DIET;
else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT; },
toCollection(HashSet::new) )));
它称分区函数。分区函数返回一个布尔值,这意味着得到的分组Map的键类型是Boolean,
于是它最多可以分为两组——true是一组,false是一组
然后把结果收集到另外一个List中也可以获得相同的结果
menu.stream().filter(Dish::isVegetarian).collect(toList());
你可以使用两个筛选操作来访问partitionedMenu这个Map中false键的值:
一个利用谓词,一个利用该谓词的非。
可以传递第二个收集器
int candidateRoot = (int) Math.sqrt((double) candidate);
return IntStream.rangeClosed(2, candidateRoot)
.noneMatch(i -> candidate % i == 0);
}
只要创建一个包含这n个数的流,
用刚刚写的isPrime方法作为谓词,
再给partitioningBy收集器归约
return IntStream.rangeClosed(2, n).boxed()
.collect(
partitioningBy(candidate -> isPrime(candidate)));
}
将Stream<T>
也就是一个无参数函数,
在调用时它会创建一个空的累加器实例,
供数据收集过程使用。
比如我们的ToListCollector,
在对空流执行操作的时候,
这个空的累加器也代表了收集过程的结果。
在我们的ToListCollector中,
supplier返回一个空的List
- > supplier() {
return () -> new ArrayList<T>
}
public Supplier<
- List<T>
return ArrayList::new;
}
当遍历到流中第n个元素时,
这个函数执行时会有两个参数:
保存归约结果的累加器(已收集了流中的前 n1 个项目),
还有第n个元素本身。
该函数将返回void,因为累加器是原位更新,
即函数的执行改变了它的内部状态以体现遍历的元素的效果。
这个函数仅仅会把当前项目添加至已经遍历过的项目的列表
- List<T>
return (list, item) -> list.add(item);
}
public BiConsumer<List<T>
- , T> accumulator() {
return List::add;
}
以便将累加器对象转换为整个集合操作的最终结果。
累加器对象恰好符合预期的最终结果,
因此无需进行转换。所以finisher方法只需返回identity函数
- List<T>
return Function.identity();
}
至少从逻辑上看可以按图进行。
实践中的实现细节可能还要复杂一点,
一方面是因为流的延迟性质,
可能在collect操作之前还需要完成其他中间操作的流水线,
另一方面则是理论上可能要进行并行归约。
它定义了对流的各个子部分进行并行处理时,
各个子部分归约所得的累加器要如何合并。
只要把从流的第二个部分收集到的项目列表加到遍历第一部分时得到 的列表后面就行了
- > combiner() {
return (list1, list2) -> {
list1.addAll(list2);
return list1; }
}
直到定义流是否需要进一步拆分的一个条件为非
(如果分布式工作单位太小,
并行计算往往比顺序计算要慢,
而且要是生成的并行任务比处理器内核数多很多的话就毫无意义了)。
将所有的部分结果两两合并。
这时会把原始流每次拆分时得到的子流对应的结果合并起来。
它定义了收集器的行为——尤其是关于流是否可以并行归约,
以及可以使用哪些优化的提示。
Characteristics是一个包含三个项目的枚举
且该收集器可以并行归约流。
如果收集器没有标为UNORDERED,那它仅在用于无序数据源时才可以并行归约。
这种情况下,累加器对象将会直接用作归约过程的最终结果。
这也意味着,将累加器A不加检查地转换为结果R是安全的
2、增加forRemoval属性,表示是否会在未来版本移除,默认为false
如果有多个变量,则用分号隔开
InputStreamReader reader = new InputStreamReader(System.in);
OutputStreamWriter writer = new OutputStreamWriter(System.out);
try (reader ; writer) {
writer.write(reader.read());
} catch (Exception e) {
e.printStackTrace();
}
将会在以后的发布版本中用来替换旧的HttpURLConnection API。
其中不仅包含旧的 HttpURLConnection API 功能,
另外还加入了对 HTTP/2 和 WebSocket 的支持。
其中包含了 Flow.Publisher、Flow.Subscriber、Flow.Subscription 和 F low.Processor 4 个核心接口
CompletableFuture 类的异步机制可以在 ProcessHandle.onExit 方法退出时执行操作
见 java.lang.StackWalker 类
比如 System.out.println("Hello World") 仅仅一行代码就可以实现Hello World的输出
将待执行的程序代码写到以.jsh为后缀名的文件中,
使用jshell [filename]命令执行
目的是为了更节省存储空间,提高性能。
同样地,StringBuilder 和 StringBuffer 底层也进行了改变,
具体见 java.lang.AbstractStringBuilder 源码。
latin1和ISO用一个byte标识,
UTF-16用两个byte标识。
Java 9会自动识别用哪个编码,
当数据用到1byte,就会使用iSO或者latin1编码,
当空间数据满足2byte的时候,自动使用utf-16编码,节省了很多空间。
2、输出的Javadoc文档兼容 HTML5 标准;
3、每个 Javadoc 页面都包含有关 JDK 模块类或接口来源的信息。
使用新的module-path来解决类路径(classpath)管理的不足。
提供了更强大的代码封装性:
在Java 9 中,模块之间的关系被称为“可读性”(readability),
实际代码中一个类型对于另外一个类型的调用被称为“可访问性“(accessablity),
即private、默认、protected、public访问修饰符。
可访问性的前提是可读性。因此public访问修饰符不再意味着具有可访问性了。
模块声明中可以包含零个或多个模块语句。
包括以下五种类型:
1、(模块)导出语句(exports statement)
2、(模块)打开语句(opens statement)
3、(模块)导入语句(requires statement)
4、(服务)使用语句(uses statement)
5、(服务)提供语句(provides statement)
exports com.example.test;
requires main;
}
当程序使用new关键字创建了一个线程之后,
该线程就处于新建状态,
此时仅由JVM为其分配 内存,
并初始化其成员变量的值
当线程对象调用了start()方法之后,
该线程处于就绪状态。
Java虚拟机会为其创建方法调用栈和 程序计数器,等待调度运行。
阻塞状态是指线程因为某种原因放弃了cpu 使用权,
也即让出了cpu timeslice,暂时停止运行。
直到线程进入可运行(runnable)状态,
才有机会再次获得cpu timeslice 转到运行(running)状 态。
JVM会把该线程放入等待队列(waitting queue) 中。
若该同步锁被别的线程占用,
则JVM会把该线 程放入锁池(lock pool)中。
执行Thread.sleep(long ms)或t.join()方法,
或者发出了I/O请求时,
JVM会把该线程置为阻塞状态。
当sleep()状态超时、join()等待线程终止
或者超时、或者I/O 处理完毕时,
线程重新转入可运行(runnable)状态。
然而,常常有些线程是伺服线程。
它们需要长时间的 运行,
只有在外部某些条件满足的情况下,
才能关闭这些线程。
使用一个变量来控制循环,
例如: 最直接的方法就是设一个boolean类型的标志,
并通过设置这个标志为true或false来控制while 循环是否退出
如使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时,
会使线程处于阻塞状态。
当调用线程的 interrupt()方法时,
会抛出 InterruptException 异常。
阻塞中的那个方法抛出这个异常,
通过代码捕获该异常,然后 break 跳出循环状态,
从而让 我们有机会结束这个线程的执行。
通常很多人认为只要调用 interrupt 方法线程就会结束,
实 际上是错的,
一定要先捕获InterruptedException异常之后通过break来跳出循环,
才能正 常结束run方法。
使用isInterrupted()判断线程的中断标志来退出循环。
当使用 interrupt()方法时,中断标志就会置true,
和使用自定义的标志来控制循环是一样的道理。
但是stop方法是很危险的,就象突然关 闭计算机电源,
而不是按正常程序关机一样,可能会产生不可预料的结果,
不安全主要是:
thread.stop()调用之后,
创建子线程的线程就会抛出 ThreadDeath的错误,
并且会释放子 线程所持有的所有锁。
一般任何进行加锁的代码块,
都是为了保护数据的一致性,
如果在调用 thread.stop()后
导致了该线程所持有的所有锁的突然释放(不可控制),
那么被保护数据就有可能呈 现不一致性,
其他线程在使用这些被破坏的数据时,
有可能导致一些很奇怪的应用程序错误。
因 此,并不推荐使用stop方法来终止线程。
调用该方法的线程进入WAITING 状态,
只有等待另外线程的通知或被中断才会返回,
需要注意的 是调用wait()方法后,会释放对象的锁。
因此,wait方法一般用在同步方法或同步代码块中。
Object 类中的 notify() 方法,
唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象 上等待,
则会选择唤醒其中一个线程,
选择是任意的,并在对实现做出决定时发生,
线程通过调 用其中一个 wait() 方法,
在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,
才能继 续执行被唤醒的线程,
被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞 争。
类似的方法还有 notifyAll() ,唤醒在此监视器上等待的所有线程。
sleep 导致当前线程休眠,
与 wait 方法不同的是 sleep 不会释放当前占有的锁,
sleep(long)会导致 线程进入TIMED-WATING状态,
而wait()方法会导致当前线程进入WATING状态
yield 会使当前线程让出 CPU 执行时间片,
与其他线程一起重新竞争 CPU 时间片。
一般情况下, 优先级高的线程有更大的可能性成功竞争得到 CPU 时间片,
但这又不是绝对的,有的操作系统对 线程优先级并不敏感。
中断一个线程,其本意是给这个线程一个通知信号,
会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)。
1. 调用 interrupt()方法并不会中断一个正在运行的线程。
也就是说处于 Running 状态的线 程并不会因为被中断而被终止,
仅仅改变了内部维护的中断标识位而已。
2. 若调用 sleep()而使线程处于 TIMED-WATING 状态,
这时调用 interrupt()方法,会抛出 InterruptedException,
从而使线程提前结束TIMED-WATING状态。
3. 许多声明抛出InterruptedException的方法(如Thread.sleep(long mills方法)),
抛出异 常前,都会清除中断标识位,所以抛出异常后,
调用 isInterrupted()方法将会返回 false。
4. 中断状态是线程固有的一个标识位,
可以通过此标识位安全的终止线程。
比如,你想终止 一个线程thread的时候,
可以调用thread.interrupt()方法,
在线程的run方法内部可以 根据thread.isInterrupted()的值来优雅的终止线程
在当前线程中调用一个线程的 join() 方法,
则当前线程转为阻塞 状态,
回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸。
很多情况下,主线程生成并启动了子线程,
需要用到子线程返回的结果,
也就是需要主线程需要 在子线程结束后再结束,
这时候就要用到 join() 方法。
System.out.println(Thread.currentThread().getName() + "线程运行开始!");
Thread6 thread1 = new Thread6();
thread1.setName("线程B");
thread1.join();
System.out.println("这时 thread1执行完毕之后才能执行主线程");
并且这个线程并不属于程序中不可或缺的部分。
因此,当所有的非后台线程结束时,程序也就终止了,
同时会杀死进程中的所有后台线程。反过来说,
只要有任何非后台线程还在运行,程序就不会终止。
必须在线程启动之前调用setDaemon()方法,才能把它设置为后台线程。
引用处理守护进程、
GC守护进程、
服务守护进程、
编译守护进程、
windows下的监听Ctrl+break的守护进程
如果全部的User Thread已经撤离,
Daemon 没有可服务的线程,JVM撤离。
sleep是Thread的方法
sleep不需要释放获得锁,也不会释放对象
sleep不需要被唤醒
让出cpu该其他线程,但是他的监控状态依然 保持者,
当指定的时间到了又会自动恢复运行状态。
这时无需等待 run 方法体代码执行完毕, 可以直接继续执行下面的代码。
这时此线程是处于就绪状态, 并没有运 行。
线程就进入了运行状态,
开始运 行run函数当中的代码。
Run方法运行结束, 此线程终止。然后CPU再调度其它线程。
而线程数大于给程序分配的CPU数量时,
为了让各个线程都有执行的机会,就需要轮转使用CPU。
不同的线程切换使用CPU发生的切换数据等
CPU 给每个任务都服务一定的时间,
然后把当前任务的状态保存 下来,
在加载下一任务的状态后,
继续服务下一任务,任务的状态保存及再加载,
这段过程就叫做 上下文切换。
时间片轮转的方式使多个任务在同一颗CPU上执行变成了可能。
寄存器通过对常用值(通常是运算的中间值)的快速访问来提高计算机程序运行的速 度。
用于表明指令序列中 CPU 正在执行的位置,
存的值为正在执行的指令 的位置或者下一个将要被执行的指令的位置,
具体依赖于特定的系统。
在 CPU 上对于进程(包括线程)进行切换,
上下 文切换过程中的信息是保存在进程控制块(PCB, process control block)中的。
PCB还经常被称 作“切换桢”(switchframe)。
信息会一直保存到CPU的内存中,直到他们被再次使用。
2. 在内存中检索下一个进程的上下文并将其在 CPU 的寄存器中恢复。
3. 跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程在程序 中。
2. 当前执行任务碰到 I/O阻塞,调度器将此任务挂起,继续下一任务;
3. 多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一任务;
4. 用户代码挂起当前任务,让出CPU时间;
5. 硬件中断;
每次去拿数据的时候都认为 别人不会修改,所以不会上锁,
但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,
采取在写时先读出当前版本号,
然后加锁操作(比较跟上一次的版本号,如果一样则更新),
如果失败则要重复读-比较-写的操作。
java 中的乐观锁基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,
比较当前值跟传入 值是否一样,一样则更新,否则失败。
每次去拿数据的时候都认为别人 会修改,
所以每次在读写数据的时候都会上锁,
这样别人想读写这个数据就会block直到拿到锁。
java中的悲观锁就是Synchronized,
AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,
才会转换为悲观锁,如RetreenLock。
如果持有锁的线程能在很短时间内释放锁资源,
那么那些等待竞争锁 的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,
它们只需要等一等(自旋),
等持有锁的线程释放锁后即可立即获取锁,
这样就避免用户线程和内核的切换的消耗。
线程自旋是需要消耗 cpu 的,说白了就是让 cpu 在做无用功,
如果一直获取不到锁,那线程 也不能一直占用cpu自旋做无用功,
所以需要设定一个自旋等待的最大时间。
如果持有锁的线程执行的时间超过自旋等待的最大时间仍没有释放锁,
就会导致其它争用锁 的线程在最大等待时间内还是获取不到锁,
这时争用线程会停止自旋进入阻塞状态。
自旋锁尽可能的减少线程的阻塞,
这对于锁的竞争不激烈,
且占用锁时间非常短的代码块来说性能能大幅度的提升,
因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,
这些操作会 导致线程发生两次上下文切换!
但是如果锁的竞争激烈,
或者持有锁的线程需要长时间占用锁执行同步块,
这时候就不适合 使用自旋锁了,
因为自旋锁在获取锁前一直都是占用 cpu 做无用功,
占着 XX 不 XX,同时有大量 线程在竞争一个锁,
会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,
其它需要cpu的线程又不能获取到cpu,造成cpu的浪费。
所以这种情况下我们要关闭自旋锁;
引入了适应性自旋锁 )
自旋锁的目的是为了占着 CPU 的资源不释放,等到获取到锁立即进行处理。
但是如何去选择 自旋的执行时间呢?如果自旋执行时间太长,
会有大量的线程处于自旋状态占用 CPU 资源,进而 会影响整体系统的性能。
因此自旋的周期选的格外重要!
JVM 对于自旋周期的选择,jdk1.5 这个限度是一定的写死的,
在 1.6 引入了适应性自旋锁,适应 性自旋锁意味着自旋的时间不在是固定的了,
而是由前一次在同一个锁上的自旋时间以及锁的拥 有者的状态来决定,
基本认为一个线程上下文切换的时间是最佳的一个时间,
同时 JVM 还针对当 前 CPU 的负荷情况做了较多的优化,
如果平均负载小于 CPUs 则一直自旋,如果有超过(CPUs/2) 个线程正在自旋,
则后来线程直接阻塞,
如果正在自旋的线程发现 Owner 发生了变化则延迟自旋 时间
(自旋计数)或进入阻塞,如果 CPU 处于节电模式则停止自旋,
自旋时间的最坏情况是 CPU 的存储延迟(CPU A存储了一个数据,到CPU B得知这个数据直接的时间差),
自旋时会适当放 弃线程优先级之间的差异。
自旋锁的开启
JDK1.6中
-XX:+UseSpinning开启;
-XX:PreBlockSpin=10 为自旋次数;
JDK1.7后,去掉此参数,由jvm控制;
他属于独占式的悲观锁,同时属于可重 入锁。
Synchronized
作用范围
1. 作用于方法时,锁住的是对象的实例(this);
2. 当作用于静态方法时,锁住的是Class实例,
又因为Class的相关数据存储在永久代PermGen (jdk1.8 则是 metaspace),
永久代是全局共享的,因此静态方法锁相当于类的一个全局锁,
会锁住所有调用该方法的线程;
3. synchronized 作用于一个对象实例时,
锁住的是所有以该对象为锁的代码块。
它有多个队列, 当多个线程一起访问某个对象监视器的时候,
对象监视器会将这些线程存储在不同的容器中。
Synchronized
核心 组件
1) Wait Set:调用wait方法被阻塞的线程被放置在这里;
2) Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
3) Entry List:Contention List中那些有资格成为候选资源的线程被移动到Entry List中;
4) OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck;
5) Owner:当前已经获取到所资源的线程被称为Owner;
6) !Owner:当前释放锁的线程。
但是并发情况下, ContentionList会被大量的并发线程进行CAS访问,
为了降低对尾部元素的竞争,
JVM会将 一部分线程移动到EntryList中作为候选竞争线程。
2. Owner 线程会在 unlock 时,
将 ContentionList 中的部分线程迁移到 EntryList 中,
并指定 EntryList中的某个线程为OnDeck线程(一般是最先进去的那个线程)。
3. Owner 线程并不直接把锁传递给 OnDeck 线程,
而是把锁竞争的权利交给 OnDeck,
OnDeck需要重新竞争锁。
这样虽然牺牲了一些公平性,
但是能极大的提升系统的吞吐量,
在 JVM中,也把这种选择行为称之为“竞争切换”。
4. OnDeck线程获取到锁资源后会变为Owner线程,
而没有得到锁资源的仍然停留在EntryList 中。
如果Owner线程被wait方法阻塞,
则转移到WaitSet队列中,
直到某个时刻通过notify 或者notifyAll唤醒,
会重新进去EntryList中。
5. 处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,
该阻塞是由操作系统 来完成的(Linux内核下采用pthread_mutex_lock内核函数实现的)。
6. Synchronized是非公平锁。
Synchronized在线程进入ContentionList时,
等待的线程会先 尝试自旋获取锁,
如果获取不到就进入 ContentionList,
这明显对于已经进入队列的线程是不公平的,
还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁 资源。
参考:https://blog.csdn.net/zqz_zqz/article/details/70233767
7. 每个对象都有个 monitor 对象,
加锁就是在竞争 monitor 对象,
代码块加锁是在前后分别加 上monitorenter和monitorexit指令来实现的,
方法加锁是通过一个标记位ACC_SYNCHRONIZED来判断的
8. synchronized 是一个重量级操作,
需要调用操作系统相关接口,
性能是低效的,有可能给线 程加锁消耗的时间比有用操作消耗的时间更多。
9. Java1.6,synchronized进行了很多的优化,
有适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,
效率有了本质上的提高。
在之后推出的 Java1.7 与 1.8 中,
均对该关键字的实现机理做了优化。
引入了偏向锁和轻量级锁。
都是在对象头中有标记位,不需要经过操作系统加锁。
10. 锁可以从偏向锁升级到轻量级锁,
再升级到重量级锁。这种升级过程叫做锁膨胀;
11. JDK 1.6中默认是开启偏向锁和轻量级锁,
可以通过-XX:-UseBiasedLocking来禁用偏向锁。
是一种可重入锁,除了能完 成 synchronized 所能完成的所有工作外,
还提供了可响应中断锁、可轮询锁请求、定时锁等 避免多线程死锁的方法。
1. void lock():
执行此方法时, 如果锁处于空闲状态, 当前线程将获取到锁.
相反, 如果锁已经 被其他线程持有, 将禁用当前线程, 直到当前线程获取到锁.
2. boolean tryLock():
如果锁可用, 则获取锁, 并立即返回 true, 否则返回 false.
该方法和 lock()的区别在于, tryLock()只是"试图"获取锁,
如果锁不可用, 不会导致当前线程被禁用,
当前线程仍然继续往下执行代码.
而 lock()方法则是一定要获取到锁,
如果锁不可用, 就一 直等待, 在未获得锁之前,
当前线程并不继续向下执行.
3. void unlock():
执行此方法时, 当前线程将释放持有的锁.
锁只能由持有者释放, 如果线程 并不持有锁, 却执行该方法,
可能导致异常的发生.
4. Condition newCondition():
条件对象,获取等待通知组件。
该组件和当前的锁绑定, 当前线程只有获取了锁,
才能调用该组件的 await()方法,
而调用后,当前线程将缩放锁。
5. getHoldCount() :
查询当前线程保持此锁的次数,
也就是执行此线程执行lock方法的次 数。
6. getQueueLength():
返回正等待获取此锁的线程估计数,
比如启动 10 个线程,1 个 线程获得锁,此时返回的是9
7. getWaitQueueLength:
(Condition condition)返回等待与此锁相关的给定条件的线 程估计数。
比如 10 个线程,用同一个 condition 对象,
并且此时这 10 个线程都执行了 condition对象的 await方法,
那么此时执行此方法返回10
8. hasWaiters(Condition condition):
查询是否有线程等待与此锁有关的给定条件 (condition),
对于指定condition对象,有多少线程执行了condition.await方法
9. hasQueuedThread(Thread thread):
查询给定线程是否等待获取此锁
10. hasQueuedThreads():
是否有线程等待此锁
11. isFair():
该锁是否公平锁
12. isHeldByCurrentThread():
当前线程是否保持锁锁定,
线程的执行 lock 方法的前后分别是false和true
13. isLock():
此锁是否有任意线程占用
14. lockInterruptibly():
如果当前线程未被中断,获取锁
15. tryLock():
尝试获得锁,仅在调用时锁未被线程占用,获得锁
16. tryLock(long timeout TimeUnit unit):
如果锁在给定等待时间内没有被另一个线程保持, 则获取该锁。
与synchronized会被 JVM 自动解锁机制不同,
ReentrantLock 加锁后需要手动进行解锁。
为了避免程序出 现异常而无法正常解锁的情况,
使用 ReentrantLock 必须在 finally 控制块中进行解锁操 作。
2. ReentrantLock相比synchronized的优势是可中断、公平锁、多个锁。
这种情况下需要 使用ReentrantLock。
2. Condition类的signal方法和Object类的 notify方法等效
3. Condition类的signalAll方法和Object类的notifyAll方法等效
4. ReentrantLock类可以唤醒指定条件的线程,而Object的唤醒是随机的
tryLock(long timeout,TimeUnit unit),可以增加时间限制,
如果超过该时间段还没获得锁,返回false
2. lock能获得锁就返回true,不能的话一直等待获得锁
3. lock 和 lockInterruptibly,如果两个线程分别执行这两个方法,
但此时中断这两个线程, lock不会抛出异常,
而lockInterruptibly会抛出异常。
ReentrantLock在构造函数中提供了是否公平锁的初始化方式来定义公平锁。
ReentrantLock 在构造函数中提供了 是否公平锁的初始化方式,默认为非公平锁。
非公平锁实际执行的效率要远远超出公平锁,除非程序有特殊需要,否则最常用非公平锁的分配机制。
2. Java中的synchronized是非公平锁,ReentrantLock 默认的lock()方法采用的是非公平锁
内层递归函数仍然有获取该锁的代码,但不受 影响。
在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁。
在读的地方使用读锁,在写的地方使用写锁,灵活控制,
如 果没有写锁的情况下,读是无阻塞的,
在一定程度上提高了程序的执行效率。
读写锁分为读锁和写 锁,多个读锁不互斥,读锁与写锁互斥,
这是由jvm自己控制的,你只要上好相应的锁即可。
如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁
写锁
如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。
总之,读的时候上 读锁,写的时候上写锁!
Java 中 读 写 锁 有 个 接 口 java.util.concurrent.locks.ReadWriteLock ,
也 有 具 体 的 实 现 ReentrantReadWriteLock。
独占锁模式下,每次只能有一个线程能持有锁,
ReentrantLock 就是以独占方式实现的互斥锁。
独占锁是一种悲观保守的加锁策略,
它避免了读/读冲突,
如果某个只读线程获取锁,
则其他读线 程都只能等待,
这种情况下就限制了不必要的并发性,
因为读操作并不会影响数据的一致性。
共享锁则允许多个线程同时获取锁,并发访问 共享资源,
如:ReadWriteLock。
共享锁则是一种 乐观锁,它放宽了加锁策略,
允许多个执行读操作的线程同时访问共享资源。
1. AQS的内部类Node定义了两个常量SHARED和EXCLUSIVE,
他们分别标识 AQS队列中等 待线程的锁获取模式。
2. java的并发包中提供了ReadWriteLock,读-写锁。
它允许一个资源可以被多个读操作访问,
或者被一个 写操作访问,但两者不能同时进行。
但是监视器锁本质又 是依赖于底层的操作系统的Mutex Lock来实现的。
而操作系统实现线程之间的切换这就需要从用 户态转换到核心态,
这个成本非常高,状态之间的转换需要相对比较长的时间,
这就是为什么 Synchronized 效率低的原因。
因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为 “重量级锁”。
JDK中对Synchronized做的种种优化,
其核心都是为了减少这种重量级锁的使用。
JDK1.6 以后,为了减少获得锁和释放锁所带来的性能消耗,
提高性能,引入了“轻量级锁”和 “偏向锁”。
锁升级
随着锁的竞争,锁可以从偏向锁升级到轻量级锁,
再升级的重量级锁(但是锁的升级是单向的, 也就是说只能从低到高升级,不会出现锁的降级)。
“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。
但是,首先需要强调一点的是, 轻量级锁并不是用来代替重量级锁的,
它的本意是在没有多线程竞争的前提下,
减少传统的重量 级锁使用产生的性能消耗。
在解释轻量级锁的执行过程之前,先明白一点,
轻量级锁所适应的场 景是线程交替执行同步块的情况,
如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀 为重量级锁。
而且总是由同一线 程多次获得。
偏向锁的目的是在某个线程获得锁之后,
消除这个线程锁重入(CAS)的开销,
看起 来让这个线程得到了偏护。
引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级 锁执行路径,
因为轻量级锁的获取及释放依赖多次 CAS 原子指令,
而偏向锁只需要在置换 ThreadID的时候依赖一次CAS原子指令
(由于一旦出现多线程竞争的情况就必须撤销偏向锁,
所 以偏向锁的撤销操作的性能损耗必须小于节省下来的 CAS 原子指令的性能消耗)。
上面说过,轻 量级锁是为了在线程交替执行同步块时提高性能,
而偏向锁则是在只有一个线程执行同步块时进 一步提高性能。
只用在有线程安全要求的程序上加锁
将大对象(这个对象可能会被很多线程访问),
拆成小对象,大大增加并行度,降低锁竞争。
降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。
最最典型的减小锁粒度的案例就是 ConcurrentHashMap。
最常见的锁分离就是读写锁ReadWriteLock,
根据功能进行分离成读锁和写锁,
这样读读不互 斥,读写互斥,写写互斥,即保证了线程安全,
又提高了性能,具体也请查看[高并发Java 五] JDK并发包。
读写分离思想可以延伸,只要操作互不影响,锁就可以分离。
比如 LinkedBlockingQueue 从头部取出,从尾部放数据
通常情况下,为了保证多线程间的有效并发,
会要求每个线程持有锁的时间尽量短,
即在使用完 公共资源后,应该立即释放锁。
但是,凡事都有一个度,
如果对同一个锁不停的进行请求、同步 和释放,
其本身也会消耗系统宝贵的资源,反而不利于性能的优化 。
锁消除是在编译器级别的事情。
在即时编译器时,如果发现不可能被共享的对象,
则可以消除这 些对象的锁操作,多数是因为程序员编码不规范引起。
很容易出现问题。
为了避免这种情况出现,
我们要保证线程 同步互斥,
就是指并发执行的多个线程,
在同一时间内只允许一个线程访问共享数据。
Java 中可 以使用synchronized关键字来取得一个对象的同步锁。
线程调度, 上下文切换 : 上下文切换过程并不廉价
线程安全问题都是由共享变量(全局变量及静态变量)引起的。
共享变量的一致性和正确性。
多个线程访问共享变量,有线程安全问题。
存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。
需要关注原子性、可见性、有序性
而锁解决了原子性的 问题,理想情况下我们希望做到“同步”和“互斥”。
这么设计可以很容易做到 同步,只要在方法上加”synchronized“
共享数据作为这个类的成员变量,
每个线程对共享数 据的操作方法也封装在外部类,
以便实现对数据的各个操作的同步和互斥,
作为内部类的各 个Runnable对象调用外部类的这些方法。
既有可见性又有原子性(非我及彼),可见性是一定的,原子性是看情况的。
对象类型和原生类型都是可见性,原生类型是原子性。
变量可见性、禁止重排序
用来确保将变量的更新操作通知到其他 线程。
volatile 变量具备两种特性,
volatile变量不会被缓存在寄存器或者对其他处理器不可见的 地方,
因此在读取volatile类型的变量时总会返回最新写入的值。
其一是保证该变量对所有线程可见,
这里的可见性指的是当一个线程修改了变量的值,
那么新的 值对于其他线程是可以立即获取的。
volatile 禁止了指令重排。
更轻量级的同步锁
在访问volatile变量时不会执行加锁操作,
因此也就不会使执行线程阻塞,
因此volatile变量是一 种比sychronized关键字更轻量级的同步机制。
volatile适合这种场景:一个变量被多个线程共 享,线程直接给这个变量赋值。
每个线程先从内存拷贝变量到CPU缓存中。
如果计算机有 多个CPU,
每个线程可能在不同的CPU上被处理,
这意味着每个线程可以拷贝到不同的 CPU cache 中。
而声明变量是 volatile 的,
JVM 保证了每次读变量都从内存中读,
跳过 CPU cache 这一步。
值得说明的是对volatile变量的单次读/写操作可以保证原子性的,如long和double类型变量,
但是并不能保证i++这种操作的原子性,因为本质上i++是读、写两次操作。
在某些场景下可以 代替Synchronized。
但是 volatile不能完全取代Synchronized的位置,
只有在一些特殊的场景下,才能适用volatile。
总的来说,必须同时满足下面两个条件才能保证在并发环境的线程安 全:
(1)对变量的写操作不依赖于当前值(比如 i++),或者说是单纯的变量赋值(boolean flag = true)。
(2)该变量没有包含在具有其他变量的不变式中,也就是说,不同的volatile变量之间,
不 能互相依赖。只有在状态真正独立于程序内其他内容时才能使用 volatile。
另一个是 WriteLock(互斥,串行)
1.7:ForkJoinPool
Executors.newCachedThreadPool()
Executors.newFixedThreadPool()
Executors.newScheduledThreadPool()
Executors.newWorkStealingPool(int parallelism):这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序;
return new ThreadPoolExecutor(nThreads,nThreads,0L,TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
它是一种固定大小的线程池;
corePoolSize和maximunPoolSize都为用户设定的线程数量nThreads;
keepAliveTime为0,意味着一旦有多余的空闲线程,就会被立即停止掉;但这里keepAliveTime无效;
阻塞队列采用了LinkedBlockingQueue,它是一个无界队列;
由于阻塞队列是一个无界队列,因此永远不可能拒绝任务;
由于采用了无界队列,实际线程数量将永远维持在nThreads,因此maximumPoolSize和keepAliveTime将无效。
return new ThreadPoolExecutor(0,Integer.MAX_VALUE,60L,TimeUnit.MILLISECONDS,
new SynchronousQueue<Runnable>());
}
它是一个可以无限扩大的线程池;
它比较适合处理执行时间比较小的任务;
corePoolSize为0,maximumPoolSize为无限大,意味着线程数量可以无限大;
keepAliveTime为60S,意味着线程空闲时间超过60S就会被杀死;
采用SynchronousQueue装等待的任务,这个阻塞队列没有存储空间,这意味着只要有请求到来,
就必须要找到一条工作线程处理他,如果当前没有空闲的线程,那么就会再创建一条新的线程。
return new ThreadPoolExecutor(1,1,0L,TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
它只会创建一条工作线程处理任务;
采用的阻塞队列为LinkedBlockingQueue;
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory, handler);
}
它接收SchduledFutureTask类型的任务,有两种提交任务的方式:
scheduledAtFixedRate
scheduledWithFixedDelay
SchduledFutureTask接收的参数:
time:任务开始的时间
sequenceNumber:任务的序号
period:任务执行的时间间隔
它采用DelayQueue存储等待的任务
DelayQueue内部封装了一个PriorityQueue,它会根据time的先后时间排序,若time相同则根据sequenceNumber排序;
DelayQueue也是一个无界队列;
工作线程的执行过程:
工作线程会从DelayQueue取已经到期的任务去执行;
执行结束后重新设置任务的到期时间,再次放回DelayQueue
return new ForkJoinPool
(Runtime.getRuntime().availableProcessors(),ForkJoinPool.defaultForkJoinWorkerThreadFactory,null, true);
}
public ThreadPoolExecutor( int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
核心线程的数量,线程池初始化后,每接到一个任务就会创建一个线程来执行任务,直到当前的线程数目到达corePoolSize,此时新的任务将会进入queue中,只有当queue满了之后,maximunPoolSize才发挥作用
核心线程被保存在pool中,即使线程处于闲置状态也不会被回收,除非allowCoreThreadTimeOut被设置,从名字可以看出这是用来控制核心线程是否可以超时被回收的一个参数。
Ps核心线程可以理解为工厂的长工
pool中所允许的最大线程数。线程池的queue满了之后,如果还有新的任务到来,此时如果线程数目小于maximumPoolSize,则会新建线程来执行任务。
Ps 非核心线程可以理解为工厂的短工 最大值=maximumPoolSize-corePoolSize
线程空闲的时间,默认情况该参数只针对”短工”有效(短工空闲太久就要被辞退),只有当配置allowCoreThreadTimeOut时该参数才对”长工”生效
keepAliveTime的单位
上文提到的queue,用来保存等待执行的任务的阻塞队列
线程工厂,可以用户自己配置,默认的ThreadFactory 1.给线程命名 2.将线程设置为非守护线程 3.优先级设置为NORM
拒绝策略:当线程数=maximumPoolSize 且 queue已满 这时候新提交的任务会被拒绝(消费者已达到max,而待消费的任务也达到max)
1.AbortPolicy:直接抛出异常,默认策略;
2.CallerRunsPolicy:用调用者所在的线程来执行任务;
3.DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
4.DiscardPolicy:直接丢弃任务;
需要的时候从池中获取线程不用自行创建,
使用完毕不需要销毁线程而是放回池中,
从而减少创建和销毁线程对象的开销。
线程池做的工作主要是控制运行的线程的数量,
处理过程中将任务放入队列,
然后在线程创建后 启动这些任务,
如果线程数量超过了最大数量超出数量的线程排队等候,
等其它线程执行完毕,
再从队列中取出任务来执行。
他的主要特点为:线程复用;控制最大并发数;管理线程。
当调用start启动线程时Java虚拟机会调用该类的 run 方法。
那么该类的 run() 方法中就是调用了 Runnable 对象的 run() 方法。
我们可以继承重写 Thread 类,
在其 start 方法中添加不断循环调用传递过来的 Runnable 对象。
这就是线程池的实 现原理。
循环方法中不断获取 Runnable 是用 Queue 实现的,
在获取下一个 Runnable 之前可以 是阻塞的。
2. 工作线程:线程池中的线程
3. 任务接口:每个任务必须实现的接口,用于工作线程调度其运行
4. 任务队列:用于存放待处理的任务,提供一种缓冲机制
该框架中用到了 Executor,Executors,
ExecutorService,ThreadPoolExecutor ,
Callable和Future、FutureTask这几个类。
任务队列是作为参数传进来的。
不过,就算队列里面 有任务,线程池也不会马上执行它们。
2. 当调用 execute() 方法添加一个任务时,
线程池会做如下判断:
a) 如果正在运行的线程数量小于 corePoolSize,
那么马上创建线程运行这个任务;
b) 如果正在运行的线程数量大于或等于 corePoolSize,
那么将这个任务放入队列;
c) 如果这时候队列满了,
而且正在运行的线程数量小于 maximumPoolSize,
那么还是要 创建非核心线程立刻运行这个任务;
d) 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,
那么线程池 会抛出异常RejectExecutionException。
3. 当一个线程完成任务时,
它会从队列中取下一个任务来执行。
4. 当一个线程无事可做,
超过一定的时间(keepAliveTime)时,
线程池会判断,如果当前运 行的线程数大于 corePoolSize,
那么这个线程就被停掉。
所以线程池的所有任务完成后,
它 最终会收缩到 corePoolSize 的大小。
在阻塞队列中,线程阻塞有这样的两种情况
消费者端的所有线程都会被自动阻塞(挂起),
直到有数据放 入队列。
生产者端的所有线程都会被自动阻塞(挂起),
直到队列中有 空的位置,线程被自动唤醒。
2. LinkedBlockingQueue :由链表结构组成的有界阻塞队列。
3. PriorityBlockingQueue :支持优先级排序的无界阻塞队列。
4. DelayQueue:使用优先级队列实现的无界阻塞队列。
5. SynchronousQueue:不存储元素的阻塞队列。
6. LinkedTransferQueue:由链表结构组成的无界阻塞队列。
7. LinkedBlockingDeque:由链表结构组成的双向阻塞队
用数组实现的有界阻塞队列。
此队列按照先进先出(FIFO)的原则对元素进行排序。
默认情况下 不保证访问者公平的访问队列,
所谓公平访问队列是指阻塞的所有生产者线程或消费者线程,
当 队列可用时,可以按照阻塞的先后顺序访问队列,
即先阻塞的生产者线程,可以先往队列里插入 元素,
先阻塞的消费者线程,可以先从队列里获取元素。
通常情况下为了保证公平性会降低吞吐 量。
我们可以使用以下代码创建一个公平的阻塞队列:
ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true);
基于链表的阻塞队列,同ArrayListBlockingQueue类似,
此队列按照先进先出(FIFO)的原则对 元素进行排序。
而 LinkedBlockingQueue 之所以能够高效的处理并发数据,
还因为其对于生产者 端和消费者端分别采用了独立的锁来控制数据同步,
这也意味着在高并发的情况下生产者和消费 者可以并行地操作队列中的数据,
以此来提高整个队列的并发性能。
LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE)。
是一个支持优先级的无界队列。
默认情况下元素采取自然顺序升序排列。
可以自定义实现 compareTo()方法来指定元素进行排序规则,
或者初始化 PriorityBlockingQueue 时,
指定构造 参数Comparator来对元素进行排序。
需要注意的是不能保证同优先级元素的顺序。
是一个支持延时获取元素的无界阻塞队列。
队列使用PriorityQueue来实现。队列中的元素必须实 现 Delayed 接口,
在创建元素时可以指定多久才能从队列中获取当前元素。
只有在延迟期满时才 能从队列中提取元素。
我们可以将DelayQueue运用在以下应用场景:
1. 缓存系统的设计:可以用 DelayQueue 保存缓存元素的有效期,
使用一个线程循环查询 DelayQueue,
一旦能从DelayQueue中获取元素时,表示缓存有效期到了。
2. 定时任务调度:使用 DelayQueue 保存当天将会执行的任务和执行时间,
一旦从 DelayQueue 中获取到任务就开始执行,
从比如 TimerQueue 就是使用 DelayQueue 实现的。
是一个不存储元素的阻塞队列。
每一个 put 操作必须等待一个 take 操作,
否则不能继续添加元素。
SynchronousQueue 可以看成是一个传球手,
负责把生产者线程处理的数据直接传递给消费者线 程。
队列本身并不存储任何元素,非常适合于传递性场景,
比如在一个线程中使用的数据,传递给 另 外 一 个 线 程 使 用 ,
SynchronousQueue 的 吞 吐 量 高 于 LinkedBlockingQueue 和 ArrayBlockingQueue。
是一个由链表结构组成的无界阻塞 TransferQueue 队列。
相对于其他阻塞队列, LinkedTransferQueue多了 tryTransfer和transfer方法。
1. transfer 方法:
如果当前有消费者正在等待接收元素(消费者使用 take()方法或带时间限制的 poll()方法时),
transfer 方法可以把生产者传入的元素立刻 transfer(传输)给消费者。
如果没有消费者在等待接收元素,transfer 方法会将元素存放在队列的 tail 节点,
并等到该元素 被消费者消费了才返回。
2. tryTransfer 方法。则是用来试探下生产者传入的元素是否能直接传给消费者。
如果没有消费 者等待接收元素,则返回 false。
和 transfer 方法的区别是 tryTransfer 方法无论消费者是否 接收,方法立即返回。
而transfer方法是必须等到消费者消费了才返回。
对于带有时间限制的tryTransfer(E e, long timeout, TimeUnit unit)方法,
则是试图把生产者传 入的元素直接传给消费者,
但是如果没有消费者消费该元素则等待指定的时间再返回,
如果超时 还没消费元素,则返回false,
如果在超时时间内消费了元素,则返回true。
是一个由链表结构组成的双向阻塞队列。
所谓双向队列指的你可以从队列的两端插入和移出元素。
双端队列因为多了一个操作队列的入口,
在多线程同时入队时,也就减少了一半的竞争。
相比其 他的阻塞队列,
LinkedBlockingDeque 多了 addFirst,addLast,offerFirst,offerLast, peekFirst,peekLast 等方法,
以 First 单词结尾的方法,表示插入,获取(peek)或移除双端队 列的第一个元素。
以 Last 单词结尾的方法,表示插入,获取或移除双端队列的最后一个元素。
另 外插入方法add等同于addLast,移除方法remove 等效于removeFirst。
但是take方法却等同 于takeFirst,不知道是不是Jdk的bug,
使用时还是用带有First和Last后缀的方法更清楚。
在初始化 LinkedBlockingDeque 时可以设置容量防止其过渡膨胀。
另外双向阻塞队列可以运用在 “工作窃取”模式中。
可以实现让一组线程等待至某个状态之后再全部同时执行。
用来挂起当前线程,
直至所有线程都到达 barrier 状态再同时执行后续任 务;
让这些线程等待至一定的时间,
如果还有 线程没有到达barrier状态就直接让到达barrier的线程执行后续任务。
它可以实现类似计数器的功能。
CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;
而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时 执行;
另外,CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。
其中有 acquire() 和 release() 两种方法,arg 都等于 1。
acquire() 会抛出 InterruptedException,同时从 sync.acquireSharedInterruptibly(arg:1)可以看出是读模式(shared);
release()中可以计数,可以控制数量,permits可以传递N个数量。
它可以设定一个阈值,基于此,多个线程竞争获取许可信 号,
做完自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。
Semaphore 可以用来 构建一些对象池,资源池之类的,比如数据库连接池
我们也可以创建计数为 1 的 Semaphore,
将其作为一种类似互斥锁的机制,这也叫二元信号量, 表示两种互斥状态。
使用方法也与之类似,通过 acquire()与 release()方法来获得和释放临界资源。
经实测,Semaphone.acquire()方法默认为可响应中断锁,
与 ReentrantLock.lockInterruptibly()作用效果一致,
也就是说在等待临界资源的过程中可以被 Thread.interrupt()方法中断。
此外,Semaphore也实现了可轮询的锁请求与定时锁的功能,
除了方法名tryAcquire 与tryLock 不同,其使用方法与ReentrantLock几乎一致。
Semaphore也提供了公平与非公平锁的机制,也 可在构造函数中进行设定。
Semaphore的锁释放操作也由手动进行,
因此与ReentrantLock一样,
为避免线程因抛出异常而 无法正常释放锁的情况发生,
释放锁的操作也必须在finally代码块中完成。
常见的还有 AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference 等,
他们的实现原理相同, 区别在与运算对象类型的不同。
还可以通过 AtomicReference<V>将一个对象的所 有操作转化成原子操作。
我们知道,在多线程程序中,诸如++i 或 i++等运算不具有原子性,是不安全的线程操作之一。
通常我们会使用 synchronized 将该操作变成一个原子操作,
但 JVM 为此类操作特意提供了一些 同步类,使得使用更方便,
且使程序运行效率变得更高。
通过相关资料显示,通常AtomicInteger 的性能是ReentantLock的好几倍。
所以每一个线程都可以独立地改变自己的副本,
而不会影响其它线程所对应的副本。
ThreadLocal 的经典使用场景是数据库连接和 session 管理等。
这种变量在线程的生命周期内起作用,
减少同一个线程内多个函数或 者组件之间一些公共变量的传递的复杂度。
1. 每个线程中都有一个自己的 ThreadLocalMap 类对象,
可以将线程自己的对象保持到其中,
各管各的,线程可以正确的访问到自己的对象。
2. 将一个共用的 ThreadLocal 静态实例作为 key,
将不同对象的引用保存到不同线程的 ThreadLocalMap中,
然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取 得自己线程保存的那个对象,
避免了将这个对象作为参数传递的麻烦。
3. ThreadLocalMap其实就是线程里面的一个属性,
它在Thread类中定义
ThreadLocal.ThreadLocalMap threadLocals = null;
下次同一线程访问同步区则偏向该线程,
无需再执行CAS操作。 单次CAS
都会执行CAS,若失败则膨胀为重量级锁。
每次CAS
都会执行CAS,若失败则阻塞。
每次CAS+阻塞
用来实现分组唤醒需要唤醒的线程们,
而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程
通过lock.lockInterruptibly()来实现这个机制。
运行时数据区(Runtime Data Area)
执行引擎(Execution Engine)
本地库接口(Native Interface)
jvm首先把字节码通过 类加载器(ClassLoader)
把文件加载到运行时数据区(Runtime Data Area) ,
而字节码文件是jvm的一套指令集规范,
并不能直接交个底层操作系统去执行,
因此需要 执行引擎(Execution Engine)
将字节码翻译成底层系统指令再交由CPU去执行,
而这个过程中需要调用本地库接口(Native Interface)来实现整个程序的功能,
这就是这4个主要组成部分的职责与功能。
存放的是对象实例本身以及数组
对年轻代的垃圾回收称为“Minor GC”,采用的是复制清除算法、并行收集器。
为使JVM更好的管理堆内存中对象的分配及回收,
年轻代又被分为三个区域:Eden、From Survivor、To Survivor。
注意:年轻代可用内存空间是Eden区+一个Survivor区,另一个Survivor区则保持空闲状态,
其中Eden区与一个Survivor区大小比例可以通过 -XX:SurvivorRatio参数进行设置,
默认为8,表示Eden区与一个Survivor区大小比值是8:1:1。
MinorGC采用复制算法。
1 : 首先,把Eden和ServivorFrom区域中存活的对象复制到ServicorTo区域
(如果有对象的年龄以及达到了老年的标准,则复制到老年代区),
同时把这些对象的年龄+1(如果 ServicorTo不够位置了就放到老年区);
2 : 然后,清空Eden和ServicorFrom中的对象;
3 : 最后,ServicorTo和ServicorFrom互换,原ServicorTo成为下一次GC时的ServicorFrom 区。
因此可以认为老年代中存放的都是生命周期较长的对象。
对老年代中对象的垃圾回收称为“Full GC”,采用的是标记-清除算法。
老年代的对象比较稳定,所以 MajorGC 不会频繁执行。
在进行 MajorGC 前一般都先进行 了一次 MinorGC,
使得有新生代的对象晋身入老年代,导致空间不够用时才触发。
当无法找到足 够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。
MajorGC 采用标记清除算法:
首先扫描一次所有老年代,标记出存活的对象,然后回收没 有标记的对象。
MajorGC的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减 少内存损耗,
我们一般需要进行合并或者标记出来方便下次直接分配。
当老年代也满了装不下的 时候,就会抛出OOM(Out of Memory)异常。
2)Perm Gen不位于堆内存中,而是属于方法区,由虚拟机直接分配,但可以通过-XX:PermSize -XX:MaxPermSize 等参数调整其大小。
-Xms初始堆大小
-Xmn设置新生代大小
-Xss设置栈大小
存储已经被虚拟机加载的类、常量、静态变量、编译器编译后的字节码
其大小跟项目的规模、类、方法的量有关。
永久代的对象垃圾回收发生在Full GC过程中。
所以这 也导致了永久代的区域会随着加载的Class的增多而胀满,最终抛出OOM异常。
元空间 的本质和永久代类似,
元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用 本地内存。
因此,默认情况下,元空间的大小仅受本地内存限制。
类的元数据放入 native memory,
字符串池和类的静态变量放入 java 堆中,
这样可以加载多少类的元数据就不再由 MaxPermSize控制, 而由系统的实际可用空间来控制。
在方法区中还包含有运行时常量池,它是每一个类或接口的常量池的运行时表示形式,
在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。
当然并非Class文件常量池中的内容才能进入运行时常量池,
在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法
用来标识执行的是哪条指令。
用来标识执行的是哪条指令。
每个线程都有自己独立的程序计数器,并且相互之间是独立的。
1、如果线程执行的是非native方法,则程序计数器中保存的是当前需要执行的指令的地址;
2、如果线程执行的是native方法,则程序计数器中的值是undefined。
存放:
局部变量表
操作数栈
动态链接
方法出口
StackOverflowError、OutOfMemoryError
在栈帧中包括
局部变量表(Local Variables)、
操作数栈(Operand Stack)、
指向当前方法所属的类的运行时常量池(运行时常量池的概念在方法区部分会谈到)的引用(Reference to runtime constant pool)
以及方法返回地址(Return Address)。
它是用C++语言写的。 由JVM启动,然后初始化sun.misc.Launcher,
sun.misc.Launcher初始化Extension ClassLoader、App ClassLoader。
1、加载:类加载器从类的.class文件读取二进制流到内存,并为之创建java.lang.Class对象,作为访问Class文件中的各种数据的入口,如反射机制。
2、连接:把类的二进制数据存储到虚拟机的各个内存区域中。
① 验证:目的是确保当前Class文件中的内容符合JVM规范的要求,并且不会危害虚拟机自身安全。主要包括文件格式验证、元数据验证、字节码验证、符号引用验证。
② 准备:为静态变量分配内存并设置类变量的默认值(如int、float的默认值是0,引用类型默认值是null。
特别地,对于final修饰的静态变量,如final static int a = 20则在此阶段结束a的值为20)。
③ 解析:将常量池中的符号引用替换为直接引用。
3、初始化:为静态变量赋予正确的初始值。如对于代码 static int i = 20,准备阶段结束后i的值是0,此阶段结束后i的值才变为20。
这个阶段会在内存中生成一个代表这个类的java.lang.Class对象,
作为方法区这个类的各种数据的入口。
注意这里不一定非得要从一个Class文件获取,
这里既 可以从ZIP包中读取(比如从jar包和war包中读取),
也可以在运行时计算生成(动态代理),
也可以由其它文件生成(比如将JSP文件转换成对应的Class类)。
验证魔数 0xCAFEBABE
版本号
元数据验证
字节码是否安全
符号引用验证
为静态变量分配内存并设置静态变量的初始值
即在方法区中分配这些变量所使用的内存空间。
注意这里所说的初始值概念,比如一个类变量定义为:
public static int v = 8080;
实际上变量v在准备阶段过后的初始值为0而不是8080,
将v赋值为8080的 put static 指令是 程序被编译后,
存放于类构造器<client>方法之中。 但是注意如果声明为:
public static final int v = 8080;
在编译阶段会为v生成ConstantValue属性,
在准备阶段虚拟机会根据ConstantValue属性将v 赋值为8080。
常量池中的符号引用替换为直接引用
符号引用就是class文件中 的:
1. CONSTANT_Class_info
2. CONSTANT_Field_info
3. CONSTANT_Method_info
等类型的常量
invokedynamic每次都会重新引用,其他执行会缓存
引用的目标并不一定要已经加载到内存中。
各种虚拟机实现的内存布局可以各不相同,
但是它们能接受的符号引用必须是一致的,
因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
相对偏移量或是一个能间接定位到目标的句柄。
如果有了直接引用,那引用的目标必定已经在内存中存在。
除了在加载阶段可以自定义类加载 器以外,
其它操作都由JVM主导。
到了初始阶段,才开始真正执行类中定义的Java程序代码。
<client>方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。
虚拟机会保证子<client>方法执行之前,父类 的<client>方法已经执行完毕,
如果一个类中没有对静态变量赋值也没有静态语句块,
那么编译 器可以不为这个类生成<client>()方法。
1. 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
2. 定义对象数组,不会触发该类的初始化。
3. 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触 发定义常量所在的类。
4. 通过类名获取Class对象,不会触发类的初始化。
5. 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,
其实这个参数是告诉虚拟机,是否要对类进行初始化。
6. 通过ClassLoader默认的loadClass方法,也不会触发初始化动作
1、主要是保证了安全性,避免用户自己编写的类动态替换Java的一些核心类,比如 java.lang.String;
2、避免了类的重复加载,因为在JVM中只有类名和加载类的ClassLoader都一样才认为是同一个类(即使是相同的class文件被不同的ClassLoader加载也被认为是不同的类)
java中的ClassLoader详解Java9 之前
https://blog.csdn.net/briblue/article/details/54973413
Bootstrap ClassLoader
最顶层的加载类,主要加载核心类库,
%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。
另外需要注意的是可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录。
比如java -Xbootclasspath/a:path被指定的文件追加到默认的bootstrap路径中。
Extention ClassLoader
扩展的类加载器,
加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。
还可以加载-D java.ext.dirs选项指定的目录。
Appclass Loader也称为SystemAppClass 加载当前应用的classpath的所有类。
我可以先告诉你答案
Bootstrap CLassloder
Extention ClassLoader
AppClassLoader
public class Launcher {
private static Launcher launcher = new Launcher();
private static String bootClassPath =
System.getProperty("sun.boot.class.path");
public static Launcher getLauncher() {
return launcher;
}
private ClassLoader loader;
public Launcher() {
// Create the extension class loader
ClassLoader extcl;
try {
extcl = ExtClassLoader.getExtClassLoader();
} catch (IOException e) {
throw new InternalError(
"Could not create extension class loader", e);
}
// Now create the class loader to use to launch the application
try {
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader", e);
}
//设置AppClassLoader为线程上下文类加载器,这个文章后面部分讲解
Thread.currentThread().setContextClassLoader(loader);
}
/*
* Returns the class loader used to launch the main application.
*/
public ClassLoader getClassLoader() {
return loader;
}
/*
* The class loader used for loading installed extensions.
*/
static class ExtClassLoader extends URLClassLoader {}
/**
* The class loader used for loading from java.class.path.
* runs in a restricted security context.
*/
static class AppClassLoader extends URLClassLoader {}
Launcher初始化了ExtClassLoader和AppClassLoader。
Launcher中并没有看见BootstrapClassLoader,但通过System.getProperty("sun.boot.class.path")得到了字符串bootClassPath,这个应该就是BootstrapClassLoader加载的jar包路径。
System.out.println(System.getProperty("sun.boot.class.path"));
得到的结果是:
C:\Program Files\Java\jre1.8.0_91\lib\resources.jar;
C:\Program Files\Java\jre1.8.0_91\lib\rt.jar;
C:\Program Files\Java\jre1.8.0_91\lib\sunrsasign.jar;
C:\Program Files\Java\jre1.8.0_91\lib\jsse.jar;
C:\Program Files\Java\jre1.8.0_91\lib\jce.jar;
C:\Program Files\Java\jre1.8.0_91\lib\charsets.jar;
C:\Program Files\Java\jre1.8.0_91\lib\jfr.jar;
C:\Program Files\Java\jre1.8.0_91\classes
可以看到,这些全是JRE目录下的jar包或者是class文件。
* The class loader used for loading installed extensions.
*/
static class ExtClassLoader extends URLClassLoader {
static {
ClassLoader.registerAsParallelCapable();
}
/**
* create an ExtClassLoader. The ExtClassLoader is created
* within a context that limits which files it can read
*/
public static ExtClassLoader getExtClassLoader() throws IOException
{
final File[] dirs = getExtDirs();
try {
// Prior implementations of this doPrivileged() block supplied
// aa synthesized ACC via a call to the private method
// ExtClassLoader.getContext().
return AccessController.doPrivileged(
new PrivilegedExceptionAction<ExtClassLoader>() {
public ExtClassLoader run() throws IOException {
int len = dirs.length;
for (int i = 0; i < len; i++) {
MetaIndex.registerDirectory(dirs[i]);
}
return new ExtClassLoader(dirs);
}
});
} catch (java.security.PrivilegedActionException e) {
throw (IOException) e.getException();
}
}
private static File[] getExtDirs() {
String s = System.getProperty("java.ext.dirs");
File[] dirs;
if (s != null) {
StringTokenizer st =
new StringTokenizer(s, File.pathSeparator);
int count = st.countTokens();
dirs = new File[count];
for (int i = 0; i < count; i++) {
dirs[i] = new File(st.nextToken());
}
} else {
dirs = new File[0];
}
return dirs;
}
......
}
System.out.println(System.getProperty("java.ext.dirs"));
结果如下:
C:\Program Files\Java\jre1.8.0_91\lib\ext;C:\Windows\Sun\Java\lib\ext
* The class loader used for loading from java.class.path.
* runs in a restricted security context.
*/
static class AppClassLoader extends URLClassLoader {
public static ClassLoader getAppClassLoader(final ClassLoader extcl)
throws IOException
{
final String s = System.getProperty("java.class.path");
final File[] path = (s == null) ? new File[0] : getClassPath(s);
return AccessController.doPrivileged(
new PrivilegedAction<AppClassLoader>() {
public AppClassLoader run() {
URL[] urls =
(s == null) ? new URL[0] : pathToURLs(path);
return new AppClassLoader(urls, extcl);
}
});
}
......
}
System.out.println(System.getProperty("java.class.path"));
结果:
D:\workspace\ClassLoaderDemo\bin
这个路径其实就是当前java工程目录bin,里面存放的是编译生成的class文件。
比如加载Test.class是由AppClassLoader完成,
那么AppClassLoader也有一个父加载器,怎么样获取呢?
很简单,通过getParent方法。
System.out.println("ClassLoader is:"+cl.toString());
System.out.println("ClassLoader\'s parent is:"+cl.getParent().toString());
运行结果如下:
ClassLoader is:sun.misc.Launcher$AppClassLoader@73d16e93
ClassLoader's parent is:sun.misc.Launcher$ExtClassLoader@15db9742
这个说明,AppClassLoader的父加载器是ExtClassLoader。那么ExtClassLoader的父加载器又是谁呢?
System.out.println("ClassLoader is:"+cl.toString());
System.out.println("ClassLoader\'s parent is:"+cl.getParent().toString());
System.out.println("ClassLoader\'s grand father is:"+cl.getParent().getParent().toString());
运行如果:
ClassLoader is:sun.misc.Launcher$AppClassLoader@73d16e93
Exception in thread "main" ClassLoader's parent is:sun.misc.Launcher$ExtClassLoader@15db9742
java.lang.NullPointerException
at ClassLoaderTest.main(ClassLoaderTest.java:13)
cl = int.class.getClassLoader();
System.out.println("ClassLoader is:"+cl.toString());
运行一下,却报错了
Exception in thread "main" java.lang.NullPointerException
at ClassLoaderTest.main(ClassLoaderTest.java:15)
提示的是空指针,意思是int.class这类基础类没有类加载器加载?
当然不是!
int.class是由Bootstrap ClassLoader加载的。
static class ExtClassLoader extends URLClassLoader {}
static class AppClassLoader extends URLClassLoader {}
可以看见ExtClassLoader和AppClassLoader同样继承自URLClassLoader,
但上面一小节代码中,为什么调用AppClassLoader的getParent()代码会得到ExtClassLoader的实例呢?
先从URLClassLoader说起,这个类又是什么?
先上一张类的继承关系图
public abstract class ClassLoader {
// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent;
// The class loader for the system
// @GuardedBy("ClassLoader.class")
private static ClassLoader scl;
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
...
}
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
public final ClassLoader getParent() {
if (parent == null)
return null;
return parent;
}
public static ClassLoader getSystemClassLoader() {
initSystemClassLoader();
if (scl == null) {
return null;
}
return scl;
}
private static synchronized void initSystemClassLoader() {
if (!sclSet) {
if (scl != null)
throw new IllegalStateException("recursive invocation");
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
if (l != null) {
Throwable oops = null;
//通过Launcher获取ClassLoader
scl = l.getClassLoader();
try {
scl = AccessController.doPrivileged(
new SystemClassLoaderAction(scl));
} catch (PrivilegedActionException pae) {
oops = pae.getCause();
if (oops instanceof InvocationTargetException) {
oops = oops.getCause();
}
}
if (oops != null) {
if (oops instanceof Error) {
throw (Error) oops;
} else {
// wrap the exception
throw new Error(oops);
}
}
}
sclSet = true;
}
}
}
我们可以看到getParent()实际上返回的就是一个ClassLoader对象parent,
parent的赋值是在ClassLoader对象的构造方法中,它有两个情况:
由外部类创建ClassLoader时直接指定一个ClassLoader为parent。
由getSystemClassLoader()方法生成,
也就是在sun.misc.Laucher通过getClassLoader()获取,
也就是AppClassLoader。直白的说,
一个ClassLoader创建时如果没有指定parent,
那么它的parent默认就是AppClassLoader。
我们主要研究的是ExtClassLoader与AppClassLoader的parent的来源,
正好它们与Launcher类有关,
我们上面已经粘贴过Launcher的部分代码。
public class Launcher {
private static URLStreamHandlerFactory factory = new Factory();
private static Launcher launcher = new Launcher();
private static String bootClassPath =
System.getProperty("sun.boot.class.path");
public static Launcher getLauncher() {
return launcher;
}
private ClassLoader loader;
public Launcher() {
// Create the extension class loader
ClassLoader extcl;
try {
extcl = ExtClassLoader.getExtClassLoader();
} catch (IOException e) {
throw new InternalError(
"Could not create extension class loader", e);
}
// Now create the class loader to use to launch the application
try {
//将ExtClassLoader对象实例传递进去
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader", e);
}
public ClassLoader getClassLoader() {
return loader;
}
static class ExtClassLoader extends URLClassLoader {
/**
* create an ExtClassLoader. The ExtClassLoader is created
* within a context that limits which files it can read
*/
public static ExtClassLoader getExtClassLoader() throws IOException
{
final File[] dirs = getExtDirs();
try {
// Prior implementations of this doPrivileged() block supplied
// aa synthesized ACC via a call to the private method
// ExtClassLoader.getContext().
return AccessController.doPrivileged(
new PrivilegedExceptionAction<ExtClassLoader>() {
public ExtClassLoader run() throws IOException {
//ExtClassLoader在这里创建
return new ExtClassLoader(dirs);
}
});
} catch (java.security.PrivilegedActionException e) {
throw (IOException) e.getException();
}
}
/*
* Creates a new ExtClassLoader for the specified directories.
*/
public ExtClassLoader(File[] dirs) throws IOException {
super(getExtURLs(dirs), null, factory);
}
}
}
我们需要注意的是
ClassLoader extcl;
extcl = ExtClassLoader.getExtClassLoader();
loader = AppClassLoader.getAppClassLoader(extcl);
代码已经说明了问题AppClassLoader的parent是一个ExtClassLoader实例。
ExtClassLoader并没有直接找到对parent的赋值。
它调用了它的父类也就是URLClassLoder的构造方法并传递了3个参数。
public ExtClassLoader(File[] dirs) throws IOException {
super(getExtURLs(dirs), null, factory);
}
对应的代码
public URLClassLoader(URL[] urls, ClassLoader parent,
URLStreamHandlerFactory factory) {
super(parent);
}
答案已经很明了了,ExtClassLoader的parent为null。
上面张贴这么多代码也是为了说明AppClassLoader的parent是ExtClassLoader,
ExtClassLoader的parent是null。这符合我们之前编写的测试代码。
不过,细心的同学发现,
还是有疑问的我们只看到ExtClassLoader和AppClassLoader的创建,
那么BootstrapClassLoader呢?
还有,ExtClassLoader的父加载器为null,
但是Bootstrap CLassLoader却可以当成它的父加载器这又是为何呢?
我们继续往下进行。
所以它并不是一个JAVA类,也就是无法在java代码中获取它的引用,
JVM启动时通过Bootstrap类加载器加载rt.jar等核心jar包中的class文件,
之前的int.class,String.class都是由它加载。
然后呢,我们前面已经分析了,
JVM初始化sun.misc.Launcher并创建Extension ClassLoader和AppClassLoader实例。
并将ExtClassLoader设置为AppClassLoader的父加载器。
Bootstrap没有父加载器,但是它却可以作用一个ClassLoader的父加载器。
比如ExtClassLoader。
这也可以解释之前通过ExtClassLoader的getParent方法获取为Null的现象。
具体是什么原因,很快就知道答案了。
是通过“委托模式”进行的,
它首先判断这个class是不是已经加载成功,
如果没有的话它并不是自己进行查找,
而是先通过父加载器,然后递归下去,
直到Bootstrap ClassLoader,
如果Bootstrap classloader找到了,
直接返回,如果没有找到,则一级一级返回,
最后到达自身去查找这些对象。这种机制就叫做双亲委托。
大家可以看到2根箭头,
蓝色的代表类加载器向上委托的方向,
如果当前的类加载器没有查询到这个class对象已经加载就请求父加载器(不一定是父类)进行操作,
然后以此类推。直到Bootstrap ClassLoader。
如果Bootstrap ClassLoader也没有加载过此class实例,
那么它就会从它指定的路径中去查找,
如果查找成功则返回,
如果没有查找成功则交给子类加载器,
也就是ExtClassLoader,这样类似操作直到终点,
也就是我上图中的红色箭头示例。
用序列描述一下:
一个AppClassLoader查找资源时,
先看看缓存是否有,缓存有从缓存中获取,
否则委托给父加载器。
递归,重复第1步的操作。
如果ExtClassLoader也没有加载过,
则由Bootstrap ClassLoader出面,
它首先查找缓存,如果没有找到的话,
就去找自己的规定的路径下,也就是sun.mic.boot.class下面的路径。
找到就返回,没有找到,让子加载器自己去找。
Bootstrap ClassLoader如果没有查找成功,
则ExtClassLoader自己在java.ext.dirs路径中去查找,
查找成功就返回,查找不成功,
再向下让子加载器找。
ExtClassLoader查找不成功,AppClassLoader就自己查找,
在java.class.path路径下查找。找到就返回。
如果没有找到就让子类找,如果没有子类会怎么样?抛出各种异常。
上面的序列,详细说明了双亲委托的加载流程。
我们可以发现委托是从下向上,
然后具体查找过程却是自上至下。
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
执行findLoadedClass(String)去检测这个class是不是已经加载过了。
执行父加载器的loadClass方法。
如果父加载器为null,则jvm内置的加载器去替代,也就是Bootstrap ClassLoader。
这也解释了ExtClassLoader的parent为null,但仍然说Bootstrap ClassLoader是它的父加载器。
如果向上委托父加载器没有加载成功,则通过findClass(String)查找。
那么loadClass()又会调用resolveClass(Class)这个方法来生成最终的Class对象。
我们可以从源代码看出这个步骤。
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检测是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//父加载器不为空则调用父加载器的loadClass
c = parent.loadClass(name, false);
} else {
//父加载器为空则调用Bootstrap Classloader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//父加载器没有找到,则调用findclass
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//调用resolveClass()
resolveClass(c);
}
return c;
}
}
这些类加载器都只是加载指定的目录下的jar包或者资源。
如果在某种情况下,我们需要动态加载一些东西呢?
比如从D盘某个文件夹加载一个class文件,
或者从网络上下载class主内容然后再进行加载,这样可以吗?
如果要这样做的话,需要我们自定义一个classloader。
复写它的findClass()方法。
在findClass()方法中调用defineClass()
它能将class二进制内容转换成Class对象,
如果不符合要求的会抛出各种异常。
那么它的parent默认就是AppClassLoader。
上面说的是,如果自定义一个ClassLoader,
默认的parent父加载器是AppClassLoader,
因为这样就能够保证它能访问系统内置加载器加载成功的class文件。
public class Test {
public void say(){
System.out.println("Say Hello");
}
}
然后将它编译过年class文件Test.class放到D:\lib这个路径下
在findClass()方法中定义了查找class的方法,然后数据通过defineClass()生成了Class对象
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class DiskClassLoader extends ClassLoader {
private String mLibPath;
public DiskClassLoader(String path) {
// TODO Auto-generated constructor stub
mLibPath = path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// TODO Auto-generated method stub
String fileName = getFileName(name);
File file = new File(mLibPath,fileName);
try {
FileInputStream is = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int len = 0;
try {
while ((len = is.read()) != -1) {
bos.write(len);
}
} catch (IOException e) {
e.printStackTrace();
}
byte[] data = bos.toByteArray();
is.close();
bos.close();
return defineClass(name,data,0,data.length);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return super.findClass(name);
}
//获取要加载 的class文件名
private String getFileName(String name) {
// TODO Auto-generated method stub
int index = name.lastIndexOf('.');
if(index == -1){
return name+".class";
}else{
return name.substring(index+1)+".class";
}
}
}
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class ClassLoaderTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
//创建自定义classloader对象。
DiskClassLoader diskLoader = new DiskClassLoader("D:\\lib");
try {
//加载class文件
Class c = diskLoader.loadClass("com.frank.test.Test");
if(c != null){
try {
Object obj = c.newInstance();
Method method = c.getDeclaredMethod("say",null);
//通过反射调用Test类的say方法
method.invoke(obj, null);
} catch (InstantiationException | IllegalAccessException
| NoSuchMethodException
| SecurityException |
IllegalArgumentException |
InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
到3个主要的JDK自带的类加载器
到自定义的ClassLoader
它们的关联部分就是路径,也就是要加载的class或者是资源的路径。
BootStrap ClassLoader、ExtClassLoader、AppClassLoader都是加载指定路径下的jar包。
如果我们要突破这种限制,实现自己某些特殊的需求,我们就得自定义ClassLoader,
自已指定加载的路径,可以是磁盘、内存、网络或者其它。
是因为它们是真实存在的类,
而且遵从”双亲委托“的机制。
而ContextClassLoader其实只是一个概念。
public class Thread implements Runnable {
/* The context ClassLoader for this thread */
private ClassLoader contextClassLoader;
public void setContextClassLoader(ClassLoader cl) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("setContextClassLoader"));
}
contextClassLoader = cl;
}
public ClassLoader getContextClassLoader() {
if (contextClassLoader == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
ClassLoader.checkClassLoaderPermission(contextClassLoader,
Reflection.getCallerClass());
}
return contextClassLoader;
}
}
contextClassLoader只是一个成员变量,
通过setContextClassLoader()方法设置,
通过getContextClassLoader()设置。
每个Thread都有一个相关联的ClassLoader,默认是AppClassLoader。
并且子线程默认使用父线程的ClassLoader除非子线程特别设置。
package com.frank.test;
public class SpeakTest implements ISpeak {
@Override
public void speak() {
// TODO Auto-generated method stub
System.out.println("Test");
}
}
它生成的SpeakTest.class文件放置在D:\\lib\\test目录下。
另外ISpeak.java代码
package com.frank.test;
public interface ISpeak {
public void speak();
}
然后,我们在这里还实现了一个SpeakTest.java
package com.frank.test;
public class SpeakTest implements ISpeak {
@Override
public void speak() {
// TODO Auto-generated method stub
System.out.println("I\' frank");
}
}
它生成的SpeakTest.class文件放置在D:\\lib目录下。
然后我们还要编写另外一个ClassLoader,
DiskClassLoader1.java这个ClassLoader的代码和DiskClassLoader.java代码一致,
我们要在DiskClassLoader1中加载位置于D:\\lib\\test中的SpeakTest.class文件。
DiskClassLoader1 diskLoader1 = new DiskClassLoader1("D:\\lib\\test");
Class cls1 = null;
try {
//加载class文件
cls1 = diskLoader1.loadClass("com.frank.test.SpeakTest");
System.out.println(cls1.getClassLoader().toString());
if(cls1 != null){
try {
Object obj = cls1.newInstance();
//SpeakTest1 speak = (SpeakTest1) obj;
//speak.speak();
Method method = cls1.getDeclaredMethod("speak",null);
//通过反射调用Test类的speak方法
method.invoke(obj, null);
} catch (InstantiationException | IllegalAccessException
| NoSuchMethodException
| SecurityException |
IllegalArgumentException |
InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
DiskClassLoader diskLoader = new DiskClassLoader("D:\\lib");
System.out.println("Thread "+Thread.currentThread().getName()+" classloader: "+Thread.currentThread().getContextClassLoader().toString());
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Thread "+Thread.currentThread().getName()+" classloader: "+Thread.currentThread().getContextClassLoader().toString());
// TODO Auto-generated method stub
try {
//加载class文件
// Thread.currentThread().setContextClassLoader(diskLoader);
//Class c = diskLoader.loadClass("com.frank.test.SpeakTest");
ClassLoader cl = Thread.currentThread().getContextClassLoader();
Class c = cl.loadClass("com.frank.test.SpeakTest");
// Class c = Class.forName("com.frank.test.SpeakTest");
System.out.println(c.getClassLoader().toString());
if(c != null){
try {
Object obj = c.newInstance();
//SpeakTest1 speak = (SpeakTest1) obj;
//speak.speak();
Method method = c.getDeclaredMethod("speak",null);
//通过反射调用Test类的say方法
method.invoke(obj, null);
} catch (InstantiationException | IllegalAccessException
| NoSuchMethodException
| SecurityException |
IllegalArgumentException |
InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}).start();
我们可以得到如下的信息:
DiskClassLoader1加载成功了SpeakTest.class文件并执行成功。
子线程的ContextClassLoader是AppClassLoader。
AppClassLoader加载不了父线程当中已经加载的SpeakTest.class内容。
Thread.currentThread().setContextClassLoader(diskLoader1);
继续改动代码:
Thread.currentThread().setContextClassLoader(diskLoader);
分别加载了自己路径下的SpeakTest.class文件,
并且它们的类名是一样的com.frank.test.SpeakTest,
但是执行结果不一样,因为它们的实际内容不一样。
系统内置的ClassLoader通过双亲委托来加载指定路径下的class和资源。
可以自定义ClassLoader,一般覆盖findClass()方法。
ContextClassLoader与线程相关,可以获取和设置,可以绕过双亲委托的机制。
同时类加载机制也进行了调整,
Java9中的类加载器,
变化仅仅是ExtClassLoader消失了且多了PlatformClassLoader,
JVM规范里5.3 Creation and Loading部分详细描述了类加载
一类是由虚拟机提供的启动类加载器,
另一类是由用户自定义的类加载器,
注意数组的创建不是类加载器创建的,
而是由虚拟机直接创建的。
initiating loader是加载并初始化
而是二进制名称和defining其的类加载器的组合
增加了Layer(层)的概念,
用Layer表示模块集,其实Layer和类加载器是对应的,
将启动类加载器加载的模块归一Layer,
用户自定义类加载器加载的模块归到另一Layer,
Layer也有委托的概念
synchronized (getClassLoadingLock(cn)) {
// check if already loaded
Class<?> c = findLoadedClass(cn);
if (c == null) {
// find the candidate module for this class
LoadedModule loadedModule = findLoadedModule(cn);
if (loadedModule != null) {
// package is in a module
BuiltinClassLoader loader = loadedModule.loader();
if (loader == this) {
if (VM.isModuleSystemInited()) {
c = findClassInModuleOrNull(loadedModule, cn);
}
} else {
// delegate to the other loader
c = loader.loadClassOrNull(cn);
}
} else {
// check parent
if (parent != null) {
c = parent.loadClassOrNull(cn);
}
// check class path
if (c == null && hasClassPath() && VM.isModuleSystemInited()) {
c = findClassOnClassPathOrNull(cn);
}
}
}
if (resolve && c != null)
resolveClass(c);
return c;
}
}
首先如果当前类已经加载了则直接返回,
如果没加载,则根据名称找到对应的模块有没有加载,
如果对应模块没有加载,则委派给父加载器去加载。
如果对应模块已经加载了,则委派给对应模块的加载器去加载,
这里需要注意下,
在模块里即使使用java.lang.Thread#setContextClassLoader方法改变当前上下文的类加载器,
或者在模块里直接使用非当前模块的类加载器去加载当前模块里的类,
最终使用的还是加载当前模块的类加载器。
Initialize the system class. Called after thread initialization.
java.lang.System#initPhase1
阶段2
Invoked by VM. Phase 2 module system initialization.Only classes in java.base can be loaded in this phase.
java.lang.System#initPhase2
阶段3
Invoked by VM. Phase 3 is the final system initialization:
1. set security manager
2. set system class loader
3. set TCCL
java.lang.System#initPhase3
Java9中分成了3个阶段,
阶段2是模块化的初始化工作,
主要是boot layer的加载,
bootlayer里包含的是平台系统依赖的一些模块,
阶段3是访问控制设置,类加载器的状态变更等,
Java9里的获取系统类加载器时是根据不同的initLevel来做安全校验,
Level为4是表示系统初始化ok了,
应用调用此方法获取AppClassLoader时校验反射安全,
而虚拟机在0-2的状态里则不校验,
直接返回AppClassLoader。
// initializing the system class loader
VM.initLevel(3);
// system class loader initialized
ClassLoader scl = ClassLoader.initSystemClassLoader();
// set TCCL
Thread.currentThread().setContextClassLoader(scl);
// system is fully initialized
VM.initLevel(4);
public static ClassLoader getSystemClassLoader() {
initSystemClassLoader();
if (scl == null) {
return null;
}
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(scl, Reflection.getCallerClass());
}
return scl;
}
public static ClassLoader getSystemClassLoader() {
switch (VM.initLevel()) {
case 0:
case 1:
case 2:
// the system class loader is the built-in app class loader during startup
return getBuiltinAppClassLoader();
case 3:
String msg = "getSystemClassLoader should only be called after VM booted";
throw new InternalError(msg);
case 4:
// system fully initialized
assert VM.isBooted() && scl != null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(scl, Reflection.getCallerClass());
}
return scl;
default:
throw new InternalError("should not reach here");
}
}
每个ClassLoader都有一个nameToModule,
是用于记录当前ClassLoader加载的模块,
一个模块里的类只会由一个ClassLoader来加载。
nameToModule是一个MAP,name是模块名,
和packageToModule不同。
package test;
public class Car {
public Car(String brand) {
this.brand = brand;
}
private String brand;
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public void run() {
System.out.println("run....");
}
}
package test;
import java.lang.reflect.InvocationTargetException;
public class ClassLoaderTest {
public static void main(String[] args) {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
try {
/** Java9之前的使用方式 */
Class clazz = loader.loadClass("test.Car");
Object obj = clazz.newInstance();
Car car = (Car) obj;
car.run();
} catch (ClassNotFoundException e) {
System.err.println(e);
} catch (InstantiationException e) {
System.err.println(e);
} catch (IllegalAccessException e) {
System.err.println(e);
}
try {
/** Java9的使用方式 */
Class clazz = loader.loadClass("test.Car");
Object obj = clazz.getDeclaredConstructor(String.class).newInstance("Benz");
Car car = (Car) obj;
car.run();
} catch (ClassNotFoundException e) {
System.err.println(e);
} catch (NoSuchMethodException e) {
System.err.println(e);
} catch (SecurityException e) {
System.err.println(e);
} catch (InstantiationException e) {
System.err.println(e);
} catch (IllegalAccessException e) {
System.err.println(e);
} catch (IllegalArgumentException e) {
System.err.println(e);
} catch (InvocationTargetException e) {
System.err.println(e);
}
}
}
运行结果如下:
java.lang.InstantiationException: test.Car
run....
这种按构造方法实例化的方式应该是比较方便的技能了,
而且可以看出异常也更细分了。
如果Car类没有构造方法,两种方式都可以运行,
但明显Java9的这种方式更强大,
原先的方式自然会被申明废弃了。
主要包括两个步骤:
找到需要回收的目标(即无用的对象)
将该区域的内存空间释放掉
由于JVM中的程序计数器、虚拟机栈、本地方法栈都是随线程而生随线程而灭,
栈帧随着方法的进入和退出做入栈和出栈操作,实现了自动的内存清理,
因此,内存垃圾回收主要集中于堆内存和方法区中。
当一个对象被强引用变量引用时,它处于可达状态,
它是不可能被垃圾回收机制回收的,
即使该对象以后永远都不会被用到JVM也不会回收。
因此强引用是造成Java内存泄漏的主要原因之 一。
对于只有软引用的对象来说,
当系统内存足够时它 不会被回收,
当系统内存空间不足时它会被回收。
软引用通常用在对内存敏感的程序中。
它比软引用的生存期更短,对于只有弱引用的对象 来说,
只要垃圾回收机制一运行,
不管JVM的内存空间是否足够,
总会回收该对象占用的内存。
它不能单独使用,必须和引用队列联合使用。
虚引用的主要作用是跟踪对象被垃圾回收的状态。
当gc(垃圾回收线程)准备回收一个对象时,
如果发现它还仅有软引用(或弱引用,或虚引用)指向它,
就会在回收该对象之前,把这个软引用(或弱引用,或虚引用)加入到与之关联的引用队列(ReferenceQueue)中。
如果一个软引用(或弱引用,或虚引用)对象本身在引用队列中,就说明该引用对象所指向的对象被回收了。
当软引用(或弱引用,或虚引用)对象所指向的对象被回收了,
那么这个引用对象本身就没有价值了,
如果程序中存在大量的这类对象(注意,我们创建的软引用、弱引用、虚引用对象本身是个强引用,不会自动被gc回收),
就会浪费内存。因此我们这就可以手动回收位于引用队列中的引用对象本身。
那么当 Entry 的 key 不再被使用(即,引用对象不可达)且被 GC 后,
那么该 Entry 就会进入到 ReferenceQueue 中。
当我们调用WeakHashMap 的get和put方法会有一个副作用,
即清除无效key对应的Entry。
首先会从引用队列中取出一个Entry对象,
然后在HashMap中查找这个Entry对象的位置,
最后把这个 Entry 从 HashMap中删除,
这时key和value对象都被回收了。
重复这个过程直到队列为空。
最后说明一点,WeakHashMap是线程安全的。
当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。即为不可达对象,所以会被判定为是 可以回收的对象。
* 虚拟机栈(栈帧中的局部变量表)中引用的对象
* 方法区中类静态属性引用的对象
* 方法区中常量引用的对象
* 本地方法栈中JNI(即native方法)引用的对象
Serial 是一个单线程的收集器,它不但只会使用一个 CPU 或一条线程去完成垃圾收集工作,
并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。
Serial 垃圾收集器虽然在收集垃圾过程中需要暂停所有其他的工作线程,
但是它简单高效,对于限 定单个 CPU 环境来说,没有线程交互的开销,
可以获得最高的单线程垃圾收集效率,
因此 Serial 垃圾收集器依然是java虚拟机运行在Client模式下默认的新生代垃圾收集器。
除了使用多线程进行垃圾收集之外,其余的行为和Serial收集器完全一样,
ParNew垃圾收集器在垃圾收集过程中同样也 要暂停所有其他的工作线程。
ParNew 收集器默认开启和 CPU 数目相同的线程数,
可以通过-XX:ParallelGCThreads 参数来限 制垃圾收集器的线程数。
【Parallel:平行的】 ParNew虽然是除了多线程外和Serial收集器几乎完全一样,
但是ParNew垃圾收集器是很多java 虚拟机运行在Server模式下新生代的默认垃圾收集器。
-XX:SurvivorRatio
-XX:PretenureSizeThreshold
-XX:HandlePromotionFailure
-XX:ParallelGCThreads
也是一个多线程的垃 圾收集器,
它重点关注的是程序达到一个可控制的吞吐量
(Thoughput,CPU 用于运行用户代码 的时间/CPU 总消耗时间,
即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),
高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,
主要适用于在后台运算而不需要太多交互的任务。
自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个重要区别
-XX:MaxGCPauseMillis
-XX:GCTimeRatio
-XX:UseAdaptiveSizePolicy
它同样是个单线程的收集器,使用标记-整理算法,
这个收集器也主要是运行在Client模式的java虚拟机默认的年老代垃圾收集器。
在Server模式下,主要有两个用途:
1. 在JDK1.5之前版本中与新生代的Parallel Scavenge收集器搭配使用。
2. 作为年老代中使用CMS收集器的后备垃圾收集方案。
使用多线程的标记-整理算法,在JDK1.6 才开始提供。
在 JDK1.6 之前,新生代使用 ParallelScavenge 收集器只能搭配年老代的 Serial Old 收集器,
只能保证新生代的吞吐量优先,无法保证整体的吞吐量,
Parallel Old 正是为了在年老代同样提供吞吐量优先的垃圾收集器,
如果系统对吞吐量要求比较高,
可以优先考虑新生代 Parallel Scavenge 和年老代Parallel Old收集器的搭配策略。
其最主要目标是获取最短垃圾回收停顿时间,
它使用多线程的标记-清除算法。
最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。
并发标记 进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
重新标记 为了修正在并发标记期间,
因用户程序继续运行而导致标记产生变动的那一部分对象的标记 记录,
仍然需要暂停所有的工作线程。
并发清除 清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。
由于耗时最长的并 发标记和并发清除过程中,
垃圾收集线程可以和用户现在一起并发工作,
所以总体上来看 CMS收集器的内存回收和用户线程是一起并发地执行。
G1收集器两个最突出的改进是:
1. 基于标记-整理算法,不产生内存碎片。
2. 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,
并且跟踪这些区域 的垃圾收集进度,同时在后台维护一个优先级列表,
每次根据所允许的收集时间,优先回收垃圾 最多的区域。
区域划分和优先级区域回收机制,
确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。
才能被GC回收,
也就是该Class被卸载(unload)
2. 加载该类的ClassLoader实例已经被GC
3. 该类的java.lang.Class对象没有在任何地方被引用
public class TestClassUnLoad {
public static void main(String[] args) throws Exception {
SimpleURLClassLoader loader = new SimpleURLClassLoader();
// 用自定义的加载器加载A
Class clazzA = loader.load("testjvm.testclassloader.A");
Object a = clazzA.newInstance();
// 清除相关引用
a = null; //清除该类的实例
clazzA = null; //清除该class对象的引用
loader = null; //清楚该类的ClassLoader引用
// 执行一次gc垃圾回收
System.gc();
System.out.println("GC over");
}
}
当Sample类被加载、连接和初始化后,它的生命周期就开始了。
当代表Sample类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,
Sample类在方法区内的数据也会被卸载,从而结束Sample类的生命周期。
由此可见,一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。
另一方面,一个Class对象总是会引用它的类加载器。
调用Class对象的getClassLoader()方法,就能获得它的类加载器。
由此可见,Class实例和加载它的加载器之间为双向关联关系。
在Object类中定义了getClass()方法,
这个方法返回代表对象所属类的Class对象的引用。
此外,所有的Java类都有一个静态属性class,它引用代表这个类的Class对象。
在虚拟机的生命周期中,始终不会被卸载。
前面介绍过,
Java虚拟机自带的类加载器包括根类加载器、扩展类加载器和系统类加载器。
Java虚拟机本身会始终引用这些类加载器,
而这些类加载器则会始终引用它们所加载的类的Class对象,
因此这些Class对象始终是可触及的。
由用户自定义的类加载器加载的类是可以被卸载的。
而objClass变量则直接引用它。
如果程序运行过程中,将上图左侧三个引用变量都置为null,
此时Sample对象结束生命周期,MyClassLoader对象结束生命周期,
代表Sample类的Class对象也结束生命周期,
Sample类在方法区内的二进制数据被卸载。
当再次有需要时,会检查Sample类的Class对象是否存在,
如果存在会直接使用,不再重新加载;
如果不存在Sample类会被重新加载,
在Java虚拟机的堆区会生成一个新的代表Sample类的Class实例
(可以通过哈希码查看是否是同一个实例)。
-xx:+PrintGDDetails
-XX:+PrintGCTimeStamps
-XX:PrintGCApplicationStoppedTime
-Xloggc:gc.log
-m 显示main函数参数
-gc:监视java堆使用情况
-flag 查看/设置 flag
jamp -histo pid
jmap -dump:format=b,file=1.log pid
jmap -dump:live,format=b,file=xxx [pid]
-heap 显示java堆详细信息
-dump 生成堆快照 jmap -dump:format=b,file=test.bin 13940
-l 出堆栈信息外,附加锁信息
E eden空间使用率
O 旧生代空间使用率
P持久代使用率
YGC monor GC执行次数
YGCT GC消耗时间
FGC full GC执行次数
FGCT full GC消耗时间
GCT Minor GC+FULL GC执行消耗的时间
Client模式启动速度较快,
Server模式启动较慢;
但是启动进入稳定期长期运行之后Server模式的程序运行速度比Client要快很多。
这是因为Server模式启动的JVM采用的是重量级的虚拟机,对程序采用了更多的优化;
而Client模式启动的JVM采用的是轻量级的虚拟机。
所以Server启动慢,但稳定后速度比Client远远要快。
-Xmx设置堆的最大空间大小。
-XX:NewSize设置新生代最小空间大小。
-XX:MaxNewSize设置新生代最大空间大小。
-XX:PermSize设置永久代最小空间大小。
-XX:MaxPermSize设置永久代最大空间大小。
-Xss设置每个线程的堆栈大小。
https://gceasy.io/
2016-07-05T10:43:18.093+0800: 25.395:
[GC [PSYoungGen: 274931K->10738K(274944K)] 371093K->147186K(450048K), 0.0668480 secs]
[Times: user=0.17 sys=0.08, real=0.07 secs]
Full GC回收日志:
2016-07-05T10:43:18.160+0800: 25.462:
[Full GC
[PSYoungGen: 10738K->0K(274944K)]
[ParOldGen: 136447K->140379K(302592K)] 147186K->140379K(577536K)
[PSPermGen: 85411K->85376K(171008K)], 0.6763541 secs
]
[Times: user=1.75 sys=0.02, real=0.68 secs]
https://mp.weixin.qq.com/s/_9o4mcJ3Kqc6117KbTWpig
[Full GC (System.gc())
[PSYoungGen: 6963K->0K(229376K)]
[ParOldGen: 8K->6793K(262144K)] 6971K->6793K(491520K),
[Metaspace: 5160K->5160K(1056768K)], 0.0085889 secs
]
[Times: user=0.03 sys=0.01, real=0.01 secs]
2019-06-15T09:40:08.520-0800:
[Full GC (System.gc())
[PSYoungGen: 5184K->0K(229376K)]
[ParOldGen: 6793K->6090K(262144K)] 11977K->6090K(491520K),
[Metaspace: 5168K->5168K(1056768K)], 0.0085573 secs
]
[Times: user=0.01 sys=0.00, real=0.00 secs]
最常见的方法还是借助btrace跟踪是哪里调用了System.gc()
Java heap space
2、可能存在内存泄露,导致在多次GC之后,还是无法找到一块足够大的内存容纳当前对象。
2、通过jmap命令,把堆内存dump下来,使用mat工具分析一下,检查是否存在内存泄露的问题
3、如果没有找到明显的内存泄露,使用 -Xmx 加大堆内存
4、还有一点容易被忽略,检查是否有大量的自定义的 Finalizable 对象,也有可能是框架内部提供的,考虑其存在的必要性
java.lang.OutOfMemoryError: Metaspace
JDK8后,元空间替换了永久代,元空间使用的是本地内存,还有其它细节变化:
字符串常量由永久代转移到堆中
和永久代相关的JVM参数已移除
可能原因有如下几种:
1、在Java7之前,频繁的错误使用String.intern()方法
2、运行期间生成了大量的代理类,导致方法区被撑爆,无法卸载
3、应用长时间运行,没有重启
没有重启 JVM 进程一般发生在调试时,如下面 tomcat 官网的一个 FAQ:
Why does the memory usage increase when I redeploy a web application?
That is because your web application has a memory leak.
A common issue are “PermGen” memory leaks.
They happen because the Classloader (and the Class objects it loaded) cannot be recycled unless some requirements are met ().
They are stored in the permanent heap generation by the JVM,
and when you redeploy a new class loader is created, which loads another copy of all these classes.
This can cause OufOfMemoryErrors eventually.
(*) The requirement is that all classes loaded by this classloader should be able to be gc’ed at the same time.
2、检查代码中是否存在大量的反射操作
3、dump之后通过mat检查是否存在大量由于反射生成的代理类
4、放大招,重启JVM
Sun 官方对此的定义:
超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。
2、添加参数 -XX:-UseGCOverheadLimit 禁用这个检查,
其实这个参数解决不了内存问题,
只是把错误的信息延后,
最终出现 java.lang.OutOfMemoryError: Java heap space。
3、dump内存,检查是否存在内存泄露,如果没有,加大内存。
2、线程总数也受到系统空闲内存和操作系统的限制,检查是否该系统下有此限制:
/proc/sys/kernel/pid_max
/proc/sys/kernel/thread-max
maxuserprocess(ulimit -u)
/proc/sys/vm/maxmapcount
在为数组分配内存之前,JVM 会执行一项检查。
要分配的数组在该平台是否可以寻址(addressable),
如果不能寻址(addressable)就会抛出这个错误。
1、swap 分区大小分配不足;
2、其他进程消耗了所有的内存。
1、其它服务进程可以选择性的拆分出去
2、加大swap分区大小,或者加大机器内存大小
和之前的方法栈溢出不同,
方法栈溢出发生在 JVM 代码层面,
而本地方法溢出发生在JNI代码或本地方法处。
spring mvc 是只是spring 处理web层请求的一个模块。
Spring 框架就像一个家族,有众多衍生产品例如 boot、security、jpa等等。
但他们的基础都是Spring Framework的 ioc和 aop。
ioc 提供了依赖注入的容器。 aop 解决了面向横切面的编程,然后在此两者的基础上实现了其他延伸产品的高级功能。
Spring MVC是基于 Servlet 的一个 MVC 框架 主要解决 WEB 开发的问题,
因为 Spring 的配置非常复杂,各种XML、 JavaConfig 处理起来比较繁琐。
于是为了简化开发者的使用,从而创造性地推出了Spring boot,约定优于配置,简化了spring的配置流程。
1、在Servlet容器(Tomcat,Jetty。。。)启动后,会创建一个ServletContext(整个Web应用的上下文);
2、由于ContextLoaderListener实现了ServletContextListener,
因此会在ServletContext创建完成后,其中的contextInitialized方法会自动被调用;
contextInitialized方法中将会通过ServletContext实例的getParameter()方法找到Spring配置文件位置,
然后根据其中的内容为Spring创建一个根上下文(WebApplicationContext,即通常所说的IOC容器):
3、将WebApplicationContext作为ServletContext的一个属性放进去,
名称是WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
不用显式的创建对象,而是把创建对象的工作交给了Ioc容器,
在使用的时候,不用去new一个对象,而是从容器中直接拿
<bean id="hello" class="com.maven.Hello"><constructor-arg ref="text" /></bean>
2.A首先完成了初始化的第一步,并且将自己提前曝光到singletonFactories中(这步是关键)
3.A发现自己依赖对象B,此时就尝试去get(B),发现B还没有被create,所以走create流程
4.
B在初始化第一步的时候发现自己依赖了对象A,于是尝试get(A),
尝试一级缓存singletonObjects(肯定没有,因为A还没初始化完全),
尝试二级缓存earlySingletonObjects(也没有),
尝试三级缓存singletonFactories,
由于A通过ObjectFactory将自己提前曝光了,
所以B能够通过ObjectFactory.getObject拿到A对象
5.B拿到A对象后顺利完成了初始化阶段1、2、3,
完全初始化之后将自己放入到一级缓存singletonObjects中
6.返回A中,A此时能拿到B的对象顺利完成自己的初始化阶段2、3,
最终A也完成了初始化,进去了一级缓存singletonObjects中
7.由于B拿到了A的对象引用,所以B类中的A对象完成了初始化。
当我们获取到对象的引用时,对象的field或者或属性是可以延后设置的。
实际上就是调用对应的构造方法构造对象,
此时只是调用了构造方法,
spring xml中指定的property并没有进行populate
这步对spring xml中指定的property进行populate
或者AfterPropertiesSet方法
会发生循环依赖的步骤集中在第一步和第二步。
有且只存在一个对象,很容易想到这个对象应该存在Cache中,
Spring大量运用了Cache的手段,
在循环依赖问题的解决过程中甚至使用了“三级缓存”。
Spring首先从singletonObjects(一级缓存)中尝试获取,
如果获取不到并且对象在创建中,
则尝试从earlySingletonObjects(二级缓存)中获取,
如果还是获取不到并且允许从singletonFactories通过getObject获取,
则通过singletonFactory.getObject()(三级缓存)获取
将singletonObject放入到earlySingletonObjects,
其实就是将三级缓存提升到二级缓存中!
单例对象此时已经被创建出来的。
这个对象已经被生产出来了,
虽然还不完美(还没有进行初始化的第二步和第三步),
但是已经能被人认出来了(根据对象引用能定位到堆中的对象),
所以Spring此时将这个对象提前曝光出来让大家认识,让大家使用。
配合Java的对象引用原理,
比较完美地解决了某些情况下的循环依赖问题!
不在代码里直接组装组件和服务,但是要在配置文件里描述哪些组件需要哪些服务,
之后一个容器(IOC容器)负责把他们组装起来。
这个方法标志IoC容器的正式启动.
指对BeanDefinition的资源定位过程。
通俗地讲,就是找到定义Javabean信息的XML文件,
并将其封装成Resource对象。
它由ResourceLoader通过统一的Resource接口来完成,
这个Resource对各种形式的BeanDef-inition的使用都提供了统一接口。
对于这些BeanDefinition的存在形式,相信大家都不会感到陌生。
比如,
在文件系统中的Bean定义信息可以使用FileSystemResource来进行抽象;
在类路径中的Bean定义信息可以使用前面提到的ClassPathResource来使用,
等等。
这个定位过程类似于容器寻找数据的过程,就像用水桶装水先要把水找到一样。
把用户定义好的Javabean表示为IoC容器内部的数据结构,
这个容器内部的数据结构就是BeanDefinition。
而这个容器内部的数据结构就是BeanDefinition。
下面介绍这个数据结构的详细定义。
具体来说,
这个BeanDefinition实际上就是POJO对象在IoC容器中的抽象,
通过这个BeanDefinition定义的数据结构,
使IoC容器能够方便地对POJO对象也就是Bean进行管理。
这个注册过程把载入过程中解析得到的BeanDefinition向IoC容器进行注册。
通过分析,我们可以看到,
在IoC容器内部将BeanDefinition注入到一个HashMap中去,
IoC容器就是通过这个HashMap来持有这些BeanDefinition数据的.
在Spring IoC的设计中,
Bean定义的载入和依赖注入是两个独立的过程
如果是,则去对应缓存中查找,
没有查找到的话则新建实例并保存。
如果不是单例,则直接新建实例(createBeanInstance)
Only valid in web-aware Spring ApplicationContext.
2. 设置属性值;
3. 如果实现了BeanNameAware接口,调用setBeanName设置Bean的ID或者Name;
4. 如果实现BeanFactoryAware接口,调用setBeanFactory 设置BeanFactory;
5. 如果实现ApplicationContextAware,调用setApplicationContext设置ApplicationContext
6. 调用BeanPostProcessor的预先初始化方法;
7. 调用InitializingBean的afterPropertiesSet()方法;
8. 调用定制init-method方法;
9. 调用BeanPostProcessor的后初始化方法;
按Bean文件和Bean的定义顺序按bean的装载顺序(即使加载多个spring文件时存在id覆盖)
“设置属性值”(第2步)时,遇到ref,则在“实例化”(第1步)之后先加载ref的id对应的bean
AbstractFactoryBean的子类,在第6步之后,会调用createInstance方法,之后会调用getObjectType方法
BeanFactoryUtils类也会改变Bean的加载顺序
究竟是按什么样的顺序加载呢?
Spring项目在部署时,
究竟创建了多少各beanFactory呢?
按什么顺序创建?
该文件内一般会配置spring-config和spring-mvc。
按顺序加载对应的xml文件。
那么这些servlet会按照定义的顺序执行,
但一定是在默认servlet之后,springmvc之前执行,
并且,若这些servlet都会分别对应一个ApplicationContext,
当然也意味着分别拥有一个beanFactory。
这些ApplicationContext(包括springmvc的那个),
他们的parent ApplicationContext均是默认servlet对应的那个ApplicationContext(Root ApplicationContext)。
默认的servlet,是通过参数contextConfigLocation来指定一个xml文件。
因此,若springmvc里的某个Controller尝试通过auto wire注解来注入servlet-test里面的service,
那么在运行时会抛出”Could not autowire field …”异常,
因为spring从springmvc那个servlet中的beanFactory(包括其父beanFactory)中找不到对应的bean。
1)BeanFactoryPostProcessor类的bean;
2)BeanPostProcessor类的bean;
3)普通bean,包括import进来的(bean标签和scan标签指定的);的顺序进行加载。
同类型的bean按照定义顺序加载。
所有bean默认是单例的。
因此,对于BeanFactoryPostProcessor和BeanPostProcessor类型的bean,
即使被放置在最后面,也会先加载。
<bean>标签生成的bean的默认id是:包名.类名#数字,例如qk.spring.beanFactory.service1.TestService1#0
如果component-scan和bean标签生成的bean有冲突(即bean的id相同),
并且都是单例(默认是单例),那么不会重复创建,
只保留最先创建出来的那个,同一个属性的话,
后续的会覆盖前面的。
见(org.springframework.context.support.AbstractApplicationContext)
(见AbstractAutowireCapableBeanFactory#initializeBean(…)。
里面分为AbstractAutowireCapableBeanFactory#applyBeanPostProcessorsBeforeInitialization(…)
和AbstractAutowireCapableBeanFactory#postProcessObjectFromFactoryBean(…))
ApplicationContextAwareProcessor(见AbstractApplicationContext#prepareBeanFactory(…)),
ServletContextAwareProcessor见(AbstractRefreshableWebApplicationContext#postProcessBeanFactory(…));
2)BeanPostProcessor#postProcessBeforeInitialization(…);
3)设置property;
4)InitializingBean#afterPropertiesSet();
5)BeanPostProcessor#postProcessAfterInitialization(…);
6)FactoryBean#getObject()的顺序构造bean实例。
doGetBean对加载bean的不同情况进行拆分处理,
并做了部分准备工作
如果是原型同时bean又正在创建,
说明是循环依赖,那直接抛异常,
spring不尝试解决原型的循环依赖
这边的调用是这个分支(当然这边一样没有类型)
既使用解决循环依赖的ObjectFactory也使用prototypeCreation的标记
如果不一致,
这边还需要委托TypeConverter进行类型装换
@Override
public Object getBean(String name) throws BeansException {
return doGetBean(name, null, null, false);
}
protected <T> T doGetBean(
final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
throws BeansException {
// 获取原始的bean name,去除&,解决alias问题
final String beanName = transformedBeanName(name);
Object bean;
// 尝试从缓存中获取bean
// Eagerly check singleton cache for manually registered singletons.
Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null && args == null) {
if (logger.isDebugEnabled()) {
if (isSingletonCurrentlyInCreation(beanName)) {
// ...
}
else {
logger.debug("Returning cached instance of singleton bean '" + beanName + "'");
}
}
// 如果从缓存中或得bean,还需要判断是否是FactoryBean,并调用getObejct
bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
}
else {
// 如果是原型scope,这边又是正在创建,说明有循环依赖,而原型的循环依赖Spring是不解决的
// Fail if we're already creating this bean instance:
// We're assumably within a circular reference.
if (isPrototypeCurrentlyInCreation(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}
// 如果当前容器没有配置bean,那么去父容器查找
// Check if bean definition exists in this factory.
BeanFactory parentBeanFactory = getParentBeanFactory();
if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
// Not found -> check parent.
String nameToLookup = originalBeanName(name);
if (args != null) {
// Delegation to parent with explicit args.
return (T) parentBeanFactory.getBean(nameToLookup, args);
}
else {
// No args -> delegate to standard getBean method.
return parentBeanFactory.getBean(nameToLookup, requiredType);
}
}
// 如果不是类型检查,这边需要标记类正在创建
if (!typeCheckOnly) {
markBeanAsCreated(beanName);
}
try {
// 实例化类之前,先去容器中获取配置的bean信息,这边需要将之前的GenericBeanDefinition转化为RootBeanDefinition
// 同时如果父bean的话,需要合并到子bean
final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
checkMergedBeanDefinition(mbd, beanName, args);
// Guarantee initialization of beans that the current bean depends on.
String[] dependsOn = mbd.getDependsOn();
if (dependsOn != null) {
for (String dependsOnBean : dependsOn) {
if (isDependent(beanName, dependsOnBean)) {
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
"Circular depends-on relationship between '" + beanName + "' and '" + dependsOnBean + "'");
}
// 解决依赖
registerDependentBean(dependsOnBean, beanName);
getBean(dependsOnBean);
}
}
// 创建单例的实例
// Create bean instance
if (mbd.isSingleton()) {
// 单例情况下,为解决循环依赖,在实例化之前,先新建一个ObjectFactory实例
sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() {
@Override
public Object getObject() throws BeansException {
try {
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
// Explicitly remove instance from singleton cache: It might have been put there
// eagerly by the creation process, to allow for circular reference resolution.
// Also remove any beans that received a temporary reference to the bean.
destroySingleton(beanName);
throw ex;
}
}
});
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
// 创建原型实例
else if (mbd.isPrototype()) {
// It's a prototype -> create a new instance.
Object prototypeInstance = null;
try {
beforePrototypeCreation(beanName);
prototypeInstance = createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
}
// 创建其他scope的实例
else {
String scopeName = mbd.getScope();
final Scope scope = this.scopes.get(scopeName);
if (scope == null) {
throw new IllegalStateException("No Scope registered for scope '" + scopeName + "'");
}
try {
// 还是先创建ObejctFactory,只是这边没有处理
Object scopedInstance = scope.get(beanName, new ObjectFactory<Object>() {
@Override
public Object getObject() throws BeansException {
beforePrototypeCreation(beanName);
try {
return createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
}
});
bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
}
catch (IllegalStateException ex) {
throw new BeanCreationException(beanName,
"Scope '" + scopeName + "' is not active for the current thread; " +
"consider defining a scoped proxy for this bean if you intend to refer to it from a singleton",
ex);
}
}
}
catch (BeansException ex) {
cleanupAfterBeanCreationFailure(beanName);
throw ex;
}
}
// 这边需要对实例进行类型校验,如果与requiredType不一致,需要委托TypeConverter尝试类型转换
// Check if required type matches the type of the actual bean instance.
if (requiredType != null && bean != null && !requiredType.isAssignableFrom(bean.getClass())) {
try {
return getTypeConverter().convertIfNecessary(bean, requiredType);
}
catch (TypeMismatchException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Failed to convert bean '" + name + "' to required type [" +
ClassUtils.getQualifiedName(requiredType) + "]", ex);
}
throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
}
}
return (T) bean;
}
逻辑是定义在DefaultSingletonBeanRegistry中,
它是AbstractBeanFactory的父类,主要职责是共享实例的注册.
这边虽然定义的是singleton,但是实际使用的时候,处理prototype,
其他scope均使用了这边进行缓存.
这边主要是需要理解
singletonObjects,
earlySingletonObjects,
singletonFactories,
registeredSingletons这4个变量.
缓存的是实例
缓存的也是实例,只是这边的是为解决循环依赖而提早暴露出来的实例,其实是ObjectFactory
缓存的是为解决循环依赖而准备的ObjectFactory
Set of registered singletons, containing the bean names in registration order
上面三个变量,任意一个添加了,这边都会添加bean name,标记已经注册
就是一个bean如果在其中任意一个变量中就,不会存在在另一变量中.这三个变量用于记录一个bean的不同状态.
如果bean已经添加到singletonObjects中,那么earlySinletonObjects和singltonFactories都不会考虑
singltonFactories中的bean 通过 ObjectFactory的getObject实例化后,添加到earlySingletonObjects(三级缓存升二级缓存)
/**
* 添加实例化的bean
* Add the given singleton object to the singleton cache of this factory.
* <p>To be called for eager registration of singletons.
* @param beanName the name of the bean
* @param singletonObject the singleton object
*/
protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
this.singletonObjects.put(beanName, (singletonObject != null ? singletonObject : NULL_OBJECT));
this.singletonFactories.remove(beanName);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
/**
* 为解决单例的循环依赖,这边注册ObjectFactory
* Add the given singleton factory for building the specified singleton
* if necessary.
* <p>To be called for eager registration of singletons, e.g. to be able to
* resolve circular references.
* @param beanName the name of the bean
* @param singletonFactory the factory for the singleton object
*/
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(singletonFactory, "Singleton factory must not be null");
synchronized (this.singletonObjects) {
if (!this.singletonObjects.containsKey(beanName)) {
this.singletonFactories.put(beanName, singletonFactory);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
}
/**
* 清除实例
* Remove the bean with the given name from the singleton cache of this factory,
* to be able to clean up eager registration of a singleton if creation failed.
* @param beanName the name of the bean
* @see #getSingletonMutex()
*/
protected void removeSingleton(String beanName) {
synchronized (this.singletonObjects) {
this.singletonObjects.remove(beanName);
this.singletonFactories.remove(beanName);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.remove(beanName);
}
}
/**
* 获取实例时,调用ObejctFactory的getObject 获取实例
* Return the (raw) singleton object registered under the given name.
* <p>Checks already instantiated singletons and also allows for an early
* reference to a currently created singleton (resolving a circular reference).
* @param beanName the name of the bean to look for
* @param allowEarlyReference whether early references should be created or not
* @return the registered singleton object, or {@code null} if none found
*/
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
其返回的对象不是指定类的一个实例,
而是该FactoryBean的getObject方法所返回的对象。
创建出来的对象是否属于单例由isSingleton中的返回决定。
则需要在<bean>中提供大量的配置信息。
配置方式的灵活性是受限的,
这时采用编码的方式可能会得到一个简单的方案。
Spring为此提供了一个org.springframework.bean.factory.FactoryBean的工厂类接口,
用户可以通过实现该接口定制实例化Bean的逻辑。
FactoryBean接口对于Spring框架来说占有重要的地位,
Spring自身就提供了70多个FactoryBean的实现。
但是它是一个能生产对象的工厂Bean,
它的实现和工厂模式及修饰器模式很像。
是一个IOC容器或者叫对象工厂,
它里面存着很多的bean。
一个方法的执行。
或者是一个异常的处理。
而 PointCut 是一个描述信息,它修饰的是 JoinPoint ,通过 PointCut ,我们就可以确定哪些 JoinPoint 可以被织入 Advice
然后,Advice 在查询到 JoinPoint 上执行逻辑。
Spring AOP 使用一个 Advice 作为拦截器,在 JoinPoint “周围”维护一系列的拦截器。
After Returning - 这些类型的 Advice 在连接点方法正常执行后执行,并使用 @AfterReturning 注解标记进行配置。
After Throwing - 这些类型的 Advice 仅在 JoinPoint 方法通过抛出异常退出并使用 @AfterThrowing 注解标记配置时执行。
After Finally - 这些类型的 Advice 在连接点方法之后执行,无论方法退出是正常还是异常返回,并使用 @After 注解标记进行配置。
Around - 这些类型的 Advice 在连接点之前和之后执行,并使用 @Around 注解标记进行配置
目标对象也被称为 Advised Object
注意, Advised Object 指的不是原来的对象,而是织入 Advice 后所产生的代理对象。
Advice + Target Object = Advised Object = Proxy
JDK代理是不需要第三方库支持的,
只需要JDK环境就可以进行代理,使用条件
2)使用Proxy.newProxyInstance产生代理对象;
3)被代理的对象必须要实现接口;
2)通过Proxy.getProxyClass获得动态代理类;
3)通过反射机制获得代理类的构造方法,方法签名为getConstructor(InvocationHandler.class);
4)通过构造函数获得代理对象并将自定义的InvocationHandler实例对象作为参数传入;
5)通过代理对象调用目标方法;
public interface IHello {
void sayHello();
}
public class HelloImpl implements IHello {
@Override
public void sayHello() {
System.out.println("Hello world!");
}
}
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class MyInvocationHandler implements InvocationHandler {
/** 目标对象 */
private Object target;
public MyInvocationHandler(Object target){
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("------插入前置通知代码-------------");
// 执行相应的目标方法
Object rs = method.invoke(target,args);
System.out.println("------插入后置处理代码-------------");
return rs;
}
}
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
/**
* 使用JDK动态代理的五大步骤:
* 1.通过实现InvocationHandler接口来自定义自己的InvocationHandler;
* 2.通过Proxy.getProxyClass获得动态代理类
* 3.通过反射机制获得代理类的构造方法,方法签名为getConstructor(InvocationHandler.class)
* 4.通过构造函数获得代理对象并将自定义的InvocationHandler实例对象传为参数传入
* 5.通过代理对象调用目标方法
*/
public class MyProxyTest {
public static void main(String[] args)
throws NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
// =========================第一种==========================
// 1、生成$Proxy0的class文件
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
// 2、获取动态代理类
Class proxyClazz = Proxy.getProxyClass(IHello.class.getClassLoader(),IHello.class);
// 3、获得代理类的构造函数,并传入参数类型InvocationHandler.class
Constructor constructor = proxyClazz.getConstructor(InvocationHandler.class);
// 4、通过构造函数来创建动态代理对象,将自定义的InvocationHandler实例传入
IHello iHello1 = (IHello) constructor.newInstance(new MyInvocationHandler(new HelloImpl()));
// 5、通过代理对象调用目标方法
iHello1.sayHello();
// ==========================第二种=============================
/**
* Proxy类中还有个将2~4步骤封装好的简便方法来创建动态代理对象,
*其方法签名为:newProxyInstance(ClassLoader loader,Class<?>[] instance, InvocationHandler h)
*/
IHello iHello2 = (IHello) Proxy.newProxyInstance(IHello.class.getClassLoader(), // 加载接口的类加载器
new Class[]{IHello.class}, // 一组接口
new MyInvocationHandler(new HelloImpl())); // 自定义的InvocationHandler
iHello2.sayHello();
}
}
newProxyInstance()方法帮我们执行了生成代理类----获取构造器----生成代理对象这三步;
生成代理类: Class<?> cl = getProxyClass0(loader, intfs);
获取构造器: final Constructor<?> cons = cl.getConstructor(constructorParams);
生成代理对象: cons.newInstance(new Object[]{h});
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
// 如果h为空直接抛出空指针异常,之后所有的单纯的判断null并抛异常,都是此方法
Objects.requireNonNull(h);
// 拷贝类实现的所有接口
final Class<?>[] intfs = interfaces.clone();
// 获取当前系统安全接口
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// Reflection.getCallerClass返回调用该方法的方法的调用类;loader:接口的类加载器
// 进行包访问权限、类加载器权限等检查
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}
/*
* Look up or generate the designated proxy class.
* 译: 查找或生成指定的代理类
*/
Class<?> cl = getProxyClass0(loader, intfs);
/*
* Invoke its constructor with the designated invocation handler.
* 译: 用指定的调用处理程序调用它的构造函数。
*/
try {
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}
/*
* 获取代理类的构造函数对象。
* constructorParams是类常量,作为代理类构造函数的参数类型,常量定义如下:
* private static final Class<?>[] constructorParams = { InvocationHandler.class };
*/
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
// 根据代理类的构造函数对象来创建需要返回的代理类对象
return cons.newInstance(new Object[]{h});
} catch (IllegalAccessException|InstantiationException e) {
throw new InternalError(e.toString(), e);
} catch (InvocationTargetException e) {
Throwable t = e.getCause();
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new InternalError(t.toString(), t);
}
} catch (NoSuchMethodException e) {
throw new InternalError(e.toString(), e);
}
}
Class<?>... interfaces) {
// 接口数不得超过65535个,这么大,足够使用的了
if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}
// If the proxy class defined by the given loader implementing
// the given interfaces exists, this will simply return the cached copy;
// otherwise, it will create the proxy class via the ProxyClassFactory
// 译: 如果缓存中有代理类了直接返回,否则将由代理类工厂ProxyClassFactory创建代理类
return proxyClassCache.get(loader, interfaces);
}
get方法中Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
subKeyFactory调用apply,具体实现在ProxyClassFactory中完成。
// 检查指定类型的对象引用不为空null。当参数为null时,抛出空指针异常。
Objects.requireNonNull(parameter);
// 清除已经被GC回收的弱引用
expungeStaleEntries();
// 将ClassLoader包装成CacheKey, 作为一级缓存的key
Object cacheKey = CacheKey.valueOf(key, refQueue);
// lazily install the 2nd level valuesMap for the particular cacheKey
// 获取得到二级缓存
ConcurrentMap<Object, Supplier<V>> valuesMap = map.get(cacheKey);
// 没有获取到对应的值
if (valuesMap == null) {
ConcurrentMap<Object, Supplier<V>> oldValuesMap
= map.putIfAbsent(cacheKey,
valuesMap = new ConcurrentHashMap<>());
if (oldValuesMap != null) {
valuesMap = oldValuesMap;
}
}
// create subKey and retrieve the possible Supplier<V> stored by that
// subKey from valuesMap
// 根据代理类实现的接口数组来生成二级缓存key
Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
// 通过subKey获取二级缓存值
Supplier<V> supplier = valuesMap.get(subKey);
Factory factory = null;
// 这个循环提供了轮询机制, 如果条件为假就继续重试直到条件为真为止
while (true) {
if (supplier != null) {
// supplier might be a Factory or a CacheValue<V> instance
// 在这里supplier可能是一个Factory也可能会是一个CacheValue
// 在这里不作判断, 而是在Supplier实现类的get方法里面进行验证
V value = supplier.get();
if (value != null) {
return value;
}
}
// else no supplier in cache
// or a supplier that returned null (could be a cleared CacheValue
// or a Factory that wasn't successful in installing the CacheValue)
// lazily construct a Factory
if (factory == null) {
// 新建一个Factory实例作为subKey对应的值
factory = new Factory(key, parameter, subKey, valuesMap);
}
if (supplier == null) {
// 到这里表明subKey没有对应的值, 就将factory作为subKey的值放入
supplier = valuesMap.putIfAbsent(subKey, factory);
if (supplier == null) {
// successfully installed Factory
// 到这里表明成功将factory放入缓存
supplier = factory;
}
// 否则, 可能期间有其他线程修改了值, 那么就不再继续给subKey赋值, 而是取出来直接用
// else retry with winning supplier
} else {
// 期间可能其他线程修改了值, 那么就将原先的值替换
if (valuesMap.replace(subKey, supplier, factory)) {
// successfully replaced
// cleared CacheEntry / unsuccessful Factory
// with our Factory
// 成功将factory替换成新的值
supplier = factory;
} else {
// retry with current supplier
// 替换失败, 继续使用原先的值
supplier = valuesMap.get(subKey);
}
}
}
}
implements BiFunction<ClassLoader, Class<?>[], Class<?>>
{
// prefix for all proxy class names
// 统一代理类的前缀名都以$Proxy
private static final String proxyClassNamePrefix = "$Proxy";
// next number to use for generation of unique proxy class names
// 使用唯一的编号给作为代理类名的一部分,如$Proxy0,$Proxy1等
private static final AtomicLong nextUniqueNumber = new AtomicLong();
@Override
public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
for (Class<?> intf : interfaces) {
/*
* Verify that the class loader resolves the name of this
* interface to the same Class object.
* 验证指定的类加载器(loader)加载接口所得到的Class对象(interfaceClass)是否与intf对象相同
*/
Class<?> interfaceClass = null;
try {
interfaceClass = Class.forName(intf.getName(), false, loader);
} catch (ClassNotFoundException e) {
}
if (interfaceClass != intf) {
throw new IllegalArgumentException(
intf + " is not visible from class loader");
}
/*
* Verify that the Class object actually represents an
* interface.
* 验证该Class对象是不是接口
*/
if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException(
interfaceClass.getName() + " is not an interface");
}
/*
* Verify that this interface is not a duplicate.
* 验证该接口是否重复
*/
if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
throw new IllegalArgumentException(
"repeated interface: " + interfaceClass.getName());
}
}
// 声明代理类所在包
String proxyPkg = null; // package to define proxy class in
int accessFlags = Modifier.PUBLIC | Modifier.FINAL;
/*
* Record the package of a non-public proxy interface so that the
* proxy class will be defined in the same package. Verify that
* all non-public proxy interfaces are in the same package.
* 验证所有非公共的接口在同一个包内;公共的就无需处理
*/
for (Class<?> intf : interfaces) {
int flags = intf.getModifiers();
if (!Modifier.isPublic(flags)) {
accessFlags = Modifier.FINAL;
String name = intf.getName();
int n = name.lastIndexOf('.');
// 截取完整包名
String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
if (proxyPkg == null) {
proxyPkg = pkg;
} else if (!pkg.equals(proxyPkg)) {
throw new IllegalArgumentException(
"non-public interfaces from different packages");
}
}
}
if (proxyPkg == null) {
// if no non-public proxy interfaces, use com.sun.proxy package
/*如果都是public接口,那么生成的代理类就在com.sun.proxy包下如果报java.io.FileNotFoundException: com\sun\proxy\$Proxy0.class
(系统找不到指定的路径。)的错误,就先在你项目中创建com.sun.proxy路径*/
proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
}
/*
* Choose a name for the proxy class to generate.
* nextUniqueNumber 是一个原子类,确保多线程安全,防止类名重复,类似于:$Proxy0,$Proxy1......
*/
long num = nextUniqueNumber.getAndIncrement();
// 代理类的完全限定名,如com.sun.proxy.$Proxy0.calss
String proxyName = proxyPkg + proxyClassNamePrefix + num;
/*
* Generate the specified proxy class.
* 生成类字节码的方法(重点)
*/
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);
try {
return defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
/*
* A ClassFormatError here means that (barring bugs in the
* proxy class generation code) there was some other
* invalid aspect of the arguments supplied to the proxy
* class creation (such as virtual machine limitations
* exceeded).
*/
throw new IllegalArgumentException(e.toString());
}
}
}
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags);
ProxyGenerator gen = new ProxyGenerator(name, interfaces, accessFlags);
// 真正生成字节码的方法
final byte[] classFile = gen.generateClassFile();
// 如果saveGeneratedFiles为true 则生成字节码文件,所以在开始我们要设置这个参数
// 当然,也可以通过返回的bytes自己输出
if (saveGeneratedFiles) {
java.security.AccessController.doPrivileged( new java.security.PrivilegedAction<Void>() {
public Void run() {
try {
int i = name.lastIndexOf('.');
Path path;
if (i > 0) {
Path dir = Paths.get(name.substring(0, i).replace('.', File.separatorChar));
Files.createDirectories(dir);
path = dir.resolve(name.substring(i+1, name.length()) + ".class");
} else {
path = Paths.get(name + ".class");
}
Files.write(path, classFile);
return null;
} catch (IOException e) {
throw new InternalError( "I/O exception saving generated file: " + e);
}
}
});
}
return classFile;
}
/* ============================================================
* Step 1: Assemble ProxyMethod objects for all methods to generate proxy dispatching code for.
* 步骤1:为所有方法生成代理调度代码,将代理方法对象集合起来。
*/
//增加 hashcode、equals、toString方法
addProxyMethod(hashCodeMethod, Object.class);
addProxyMethod(equalsMethod, Object.class);
addProxyMethod(toStringMethod, Object.class);
// 获得所有接口中的所有方法,并将方法添加到代理方法中
for (Class<?> intf : interfaces) {
for (Method m : intf.getMethods()) {
addProxyMethod(m, intf);
}
}
/*
* 验证方法签名相同的一组方法,返回值类型是否相同;意思就是重写方法要方法签名和返回值一样
*/
for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
checkReturnTypes(sigmethods);
}
/* ============================================================
* Step 2: Assemble FieldInfo and MethodInfo structs for all of fields and methods in the class we are generating.
* 为类中的方法生成字段信息和方法信息
*/
try {
// 生成代理类的构造函数
methods.add(generateConstructor());
for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
for (ProxyMethod pm : sigmethods) {
// add static field for method's Method object
fields.add(new FieldInfo(pm.methodFieldName,
"Ljava/lang/reflect/Method;",
ACC_PRIVATE | ACC_STATIC));
// generate code for proxy method and add it
// 生成代理类的代理方法
methods.add(pm.generateMethod());
}
}
// 为代理类生成静态代码块,对一些字段进行初始化
methods.add(generateStaticInitializer());
} catch (IOException e) {
throw new InternalError("unexpected I/O Exception", e);
}
if (methods.size() > 65535) {
throw new IllegalArgumentException("method limit exceeded");
}
if (fields.size() > 65535) {
throw new IllegalArgumentException("field limit exceeded");
}
/* ============================================================
* Step 3: Write the final class file.
* 步骤3:编写最终类文件
*/
/*
* Make sure that constant pool indexes are reserved for the following items before starting to write the final class file.
* 在开始编写最终类文件之前,确保为下面的项目保留常量池索引。
*/
cp.getClass(dotToSlash(className));
cp.getClass(superclassName);
for (Class<?> intf: interfaces) {
cp.getClass(dotToSlash(intf.getName()));
}
/*
* Disallow new constant pool additions beyond this point, since we are about to write the final constant pool table.
* 设置只读,在这之前不允许在常量池中增加信息,因为要写常量池表
*/
cp.setReadOnly();
ByteArrayOutputStream bout = new ByteArrayOutputStream();
DataOutputStream dout = new DataOutputStream(bout);
try {
// u4 magic;
dout.writeInt(0xCAFEBABE);
// u2 次要版本;
dout.writeShort(CLASSFILE_MINOR_VERSION);
// u2 主版本
dout.writeShort(CLASSFILE_MAJOR_VERSION);
cp.write(dout); // (write constant pool)
// u2 访问标识;
dout.writeShort(accessFlags);
// u2 本类名;
dout.writeShort(cp.getClass(dotToSlash(className)));
// u2 父类名;
dout.writeShort(cp.getClass(superclassName));
// u2 接口;
dout.writeShort(interfaces.length);
// u2 interfaces[interfaces_count];
for (Class<?> intf : interfaces) {
dout.writeShort(cp.getClass(
dotToSlash(intf.getName())));
}
// u2 字段;
dout.writeShort(fields.size());
// field_info fields[fields_count];
for (FieldInfo f : fields) {
f.write(dout);
}
// u2 方法;
dout.writeShort(methods.size());
// method_info methods[methods_count];
for (MethodInfo m : methods) {
m.write(dout);
}
// u2 类文件属性:对于代理类来说没有类文件属性;
dout.writeShort(0); // (no ClassFile attributes for proxy classes)
} catch (IOException e) {
throw new InternalError("unexpected I/O Exception", e);
}
return bout.toByteArray();
}
String var3 = var1.getName(); //方法名
Class[] var4 = var1.getParameterTypes(); //方法参数类型数组
Class var5 = var1.getReturnType(); //返回值类型
Class[] var6 = var1.getExceptionTypes(); //异常类型
String var7 = var3 + getParameterDescriptors(var4); //方法签名
Object var8 = (List)this.proxyMethods.get(var7); //根据方法签名却获得proxyMethods的Value
if(var8 != null) { //处理多个代理接口中重复的方法的情况
Iterator var9 = ((List)var8).iterator();
while(var9.hasNext()) {
ProxyGenerator.ProxyMethod var10 = (ProxyGenerator.ProxyMethod)var9.next();
if(var5 == var10.returnType) {
/*归约异常类型以至于让重写的方法抛出合适的异常类型,我认为这里可能是多个接口中有相同的方法,而这些相同的方法抛出的异常类 型又不同,所以对这些相同方法抛出的异常进行了归约*/
ArrayList var11 = new ArrayList();
collectCompatibleTypes(var6, var10.exceptionTypes, var11);
collectCompatibleTypes(var10.exceptionTypes, var6, var11);
var10.exceptionTypes = new Class[var11.size()];
//将ArrayList转换为Class对象数组
var10.exceptionTypes = (Class[])var11.toArray(var10.exceptionTypes);
return;
}
}
} else {
var8 = new ArrayList(3);
this.proxyMethods.put(var7, var8);
}
((List)var8).add(new ProxyGenerator.ProxyMethod(var3, var4, var5, var6, var2, null));
/*如果var8为空,就创建一个数组,并以方法签名为key,proxymethod对象数组为value添加到proxyMethods*/
}
import com.lanhuigu.spring.proxy.jdk.IHello;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
public final class $Proxy0 extends Proxy
implements IHello // 继承了Proxy类和实现IHello接口
{
// 变量,都是private static Method XXX
private static Method m1;
private static Method m3;
private static Method m2;
private static Method m0;
// 代理类的构造函数,其参数正是是InvocationHandler实例,Proxy.newInstance方法就是通过通过这个构造函数来创建代理实例的
public $Proxy0(InvocationHandler paramInvocationHandler)
throws
{
super(paramInvocationHandler);
}
// 以下Object中的三个方法
public final boolean equals(Object paramObject)
throws
{
try
{
return ((Boolean)this.h.invoke(this, m1, new Object[] { paramObject })).booleanValue();
}
catch (RuntimeException localRuntimeException)
{
throw localRuntimeException;
}
catch (Throwable localThrowable)
{
throw new UndeclaredThrowableException(localThrowable);
}
}
// 接口代理方法
public final void sayHello()
throws
{
try
{
this.h.invoke(this, m3, null);
return;
}
catch (RuntimeException localRuntimeException)
{
throw localRuntimeException;
}
catch (Throwable localThrowable)
{
throw new UndeclaredThrowableException(localThrowable);
}
}
public final String toString()
throws
{
try
{
return ((String)this.h.invoke(this, m2, null));
}
catch (RuntimeException localRuntimeException)
{
throw localRuntimeException;
}
catch (Throwable localThrowable)
{
throw new UndeclaredThrowableException(localThrowable);
}
}
public final int hashCode()
throws
{
try
{
return ((Integer)this.h.invoke(this, m0, null)).intValue();
}
catch (RuntimeException localRuntimeException)
{
throw localRuntimeException;
}
catch (Throwable localThrowable)
{
throw new UndeclaredThrowableException(localThrowable);
}
}
// 静态代码块对变量进行一些初始化工作
static
{
try
{
// 这里每个方法对象 和类的实际方法绑定
m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
m3 = Class.forName("com.lanhuigu.spring.proxy.jdk.IHello").getMethod("sayHello", new Class[0]);
m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
return;
}
catch (NoSuchMethodException localNoSuchMethodException)
{
throw new NoSuchMethodError(localNoSuchMethodException.getMessage());
}
catch (ClassNotFoundException localClassNotFoundException)
{
throw new NoClassDefFoundError(localClassNotFoundException.getMessage());
}
}
}
在动态代理中InvocationHandler是核心,每个代理实例都具有一个关联的调用处理程序(InvocationHandler)。
对代理实例调用方法时,将对方法调用进行编码并将其指派到它的调用处理程序(InvocationHandler)的invoke()方法。
所以对代理方法的调用都是通InvocationHadler的invoke来实现中,而invoke方法根据传入的代理对象,
方法和参数来决定调用代理的哪个方法。
方法签名如下:
invoke(Object Proxy,Method method,Object[] args)
从反编译源码分析调用invoke()过程:
从反编译后的源码看$Proxy0类继承了Proxy类,同时实现了IHello接口,即代理类接口,
所以才能强制将代理对象转换为IHello接口,然后调用$Proxy0中的sayHello()方法。
throws
{
try
{
this.h.invoke(this, m3, null);
return;
}
catch (RuntimeException localRuntimeException)
{
throw localRuntimeException;
}
catch (Throwable localThrowable)
{
throw new UndeclaredThrowableException(localThrowable);
}
}
this就是$Proxy0对象;
m3就是m3 = Class.forName("com.lanhuigu.spring.proxy.jdk.IHello").getMethod("sayHello", new Class[0]);
即是通过全路径名,反射获取的目标对象中的真实方法加参数。
h就是Proxy类中的变量protected InvocationHandler h;
所以成功的调到了InvocationHandler中的invoke()方法,但是invoke()方法在我们自定义的MyInvocationHandler
中实现,MyInvocationHandler中的invoke()方法:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("------插入前置通知代码-------------");
// 执行相应的目标方法
Object rs = method.invoke(target,args);
System.out.println("------插入后置处理代码-------------");
return rs;
}
可以看出,MyInvocationHandler中invoke第一个参数为$Proxy0(代理对象),第二个参数为目标类的真实方法,
第三个参数为目标方法参数,因为sayHello()没有参数,所以是null。
到这里,我们真正的实现了通过代理调用目标对象的完全分析,至于InvocationHandler中的invoke()方法就是
最后执行了目标方法。到此完成了代理对象生成,目标方法调用。
所以,我们可以看到在打印目标方法调用输出结果前后所插入的前置和后置代码处理。
只是他在运行期间生成的代理对象是针对目标类扩展的子类
它可以在运行期扩展Java类与实现Java接口。Hibernate用它来实现PO(Persistent Object 持久化对象)字节码的动态生成。
CGLIB是一个强大的高性能的代码生成包。它广泛的被许多AOP的框架使用,例如Spring AOP为他们提供
方法的interception(拦截)。CGLIB包的底层是通过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类。
除了CGLIB包,脚本语言例如Groovy和BeanShell,也是使用ASM来生成java的字节码。当然不鼓励直接使用ASM,
因为它要求你必须对JVM内部结构包括class文件的格式和指令集都很熟悉。
public class HelloService {
public HelloService() {
System.out.println("HelloService构造");
}
/**
* 该方法不能被子类覆盖,Cglib是无法代理final修饰的方法的
*/
final public String sayOthers(String name) {
System.out.println("HelloService:sayOthers>>"+name);
return null;
}
public void sayHello() {
System.out.println("HelloService:sayHello");
}
}
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* 自定义MethodInterceptor
*/
public class MyMethodInterceptor implements MethodInterceptor{
/**
* sub:cglib生成的代理对象
* method:被代理对象方法
* objects:方法入参
* methodProxy: 代理方法
*/
@Override
public Object intercept(Object sub, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("======插入前置通知======");
Object object = methodProxy.invokeSuper(sub, objects);
System.out.println("======插入后者通知======");
return object;
}
}
import net.sf.cglib.core.DebuggingClassWriter;
import net.sf.cglib.proxy.Enhancer;
public class Client {
public static void main(String[] args) {
// 代理类class文件存入本地磁盘方便我们反编译查看源码
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "D:\\code");
// 通过CGLIB动态代理获取代理对象的过程
Enhancer enhancer = new Enhancer();
// 设置enhancer对象的父类
enhancer.setSuperclass(HelloService.class);
// 设置enhancer的回调对象
enhancer.setCallback(new MyMethodInterceptor());
// 创建代理对象
HelloService proxy= (HelloService)enhancer.create();
// 通过代理对象调用目标方法
proxy.sayHello();
}
}
* Copyright 2002,2003 The Apache Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.sf.cglib.proxy;
/**
* General-purpose {@link Enhancer} callback which provides for "around advice".
* @author Juozas Baliuka <a href="mailto:baliuka@mwm.lt">baliuka@mwm.lt</a>
* @version $Id: MethodInterceptor.java,v 1.8 2004/06/24 21:15:20 herbyderby Exp $
*/
public interface MethodInterceptor
extends Callback
{
/**
* All generated proxied methods call this method instead of the original method.
* The original method may either be invoked by normal reflection using the Method object,
* or by using the MethodProxy (faster).
* @param obj "this", the enhanced object
* @param method intercepted Method
* @param args argument array; primitive types are wrapped
* @param proxy used to invoke super (non-intercepted method); may be called
* as many times as needed
* @throws Throwable any exception may be thrown; if so, super method will not be invoked
* @return any value compatible with the signature of the proxied method. Method returning void will ignore this value.
* @see MethodProxy
*/
public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args,
MethodProxy proxy) throws Throwable;
}
1)obj表示增强的对象,即实现这个接口类的一个对象;
2)method表示要被拦截的方法;
3)args表示要被拦截方法的参数;
4)proxy表示要触发父类的方法对象;
在上面的Client代码中,通过Enhancer.create()方法创建代理对象,create()方法的源码
* Generate a new class if necessary and uses the specified
* callbacks (if any) to create a new object instance.
* Uses the no-arg constructor of the superclass.
* @return a new instance
*/
public Object create() {
classOnly = false;
argumentTypes = null;
return createHelper();
}
使用的父类的参数的构造方法来实例化父类的部分。核心内容在createHelper()中,源码如下
preValidate();
Object key = KEY_FACTORY.newInstance((superclass != null) ? superclass.getName() : null,
ReflectUtils.getNames(interfaces),
filter == ALL_ZERO ? null : new WeakCacheKey<CallbackFilter>(filter),
callbackTypes,
useFactory,
interceptDuringConstruction,
serialVersionUID);
this.currentKey = key;
Object result = super.create(key);
return result;
}
通过newInstance()方法创建EnhancerKey对象,作为Enhancer父类AbstractClassGenerator.create()方法
创建代理对象的参数。
try {
ClassLoader loader = getClassLoader();
Map<ClassLoader, ClassLoaderData> cache = CACHE;
ClassLoaderData data = cache.get(loader);
if (data == null) {
synchronized (AbstractClassGenerator.class) {
cache = CACHE;
data = cache.get(loader);
if (data == null) {
Map<ClassLoader, ClassLoaderData> newCache = new WeakHashMap<ClassLoader, ClassLoaderData>(cache);
data = new ClassLoaderData(loader);
newCache.put(loader, data);
CACHE = newCache;
}
}
}
this.key = key;
Object obj = data.get(this, getUseCache());
if (obj instanceof Class) {
return firstInstance((Class) obj);
}
return nextInstance(obj);
} catch (RuntimeException e) {
throw e;
} catch (Error e) {
throw e;
} catch (Exception e) {
throw new CodeGenerationException(e);
}
}
abstract protected Object nextInstance(Object instance) throws Exception;
在子类Enhancer中实现,实现源码如下:
EnhancerFactoryData data = (EnhancerFactoryData) instance;
if (classOnly) {
return data.generatedClass;
}
Class[] argumentTypes = this.argumentTypes;
Object[] arguments = this.arguments;
if (argumentTypes == null) {
argumentTypes = Constants.EMPTY_CLASS_ARRAY;
arguments = null;
}
return data.newInstance(argumentTypes, arguments, callbacks);
}
第一个参数为代理对象的构成器类型,第二个为代理对象构造方法参数,第三个为对应回调对象。
最后根据这些参数,通过反射生成代理对象,源码如下:
* Creates proxy instance for given argument types, and assigns the callbacks.
* Ideally, for each proxy class, just one set of argument types should be used,
* otherwise it would have to spend time on constructor lookup.
* Technically, it is a re-implementation of {@link Enhancer#createUsingReflection(Class)},
* with "cache {@link #setThreadCallbacks} and {@link #primaryConstructor}"
*
* @see #createUsingReflection(Class)
* @param argumentTypes constructor argument types
* @param arguments constructor arguments
* @param callbacks callbacks to set for the new instance
* @return newly created proxy
*/
public Object newInstance(Class[] argumentTypes, Object[] arguments, Callback[] callbacks) {
setThreadCallbacks(callbacks);
try {
// Explicit reference equality is added here just in case Arrays.equals does not have one
if (primaryConstructorArgTypes == argumentTypes ||
Arrays.equals(primaryConstructorArgTypes, argumentTypes)) {
// If we have relevant Constructor instance at hand, just call it
// This skips "get constructors" machinery
return ReflectUtils.newInstance(primaryConstructor, arguments);
}
// Take a slow path if observing unexpected argument types
return ReflectUtils.newInstance(generatedClass, argumentTypes, arguments);
} finally {
// clear thread callbacks to allow them to be gc'd
setThreadCallbacks(null);
}
}
将其反编译后代码如下:
import java.lang.reflect.Method;
import net.sf.cglib.core.ReflectUtils;
import net.sf.cglib.core.Signature;
import net.sf.cglib.proxy.*;
public class HelloService$$EnhancerByCGLIB$$4da4ebaf extends HelloService
implements Factory
{
private boolean CGLIB$BOUND;
public static Object CGLIB$FACTORY_DATA;
private static final ThreadLocal CGLIB$THREAD_CALLBACKS;
private static final Callback CGLIB$STATIC_CALLBACKS[];
private MethodInterceptor CGLIB$CALLBACK_0; // 拦截器
private static Object CGLIB$CALLBACK_FILTER;
private static final Method CGLIB$sayHello$0$Method; // 被代理方法
private static final MethodProxy CGLIB$sayHello$0$Proxy; // 代理方法
private static final Object CGLIB$emptyArgs[];
private static final Method CGLIB$equals$1$Method;
private static final MethodProxy CGLIB$equals$1$Proxy;
private static final Method CGLIB$toString$2$Method;
private static final MethodProxy CGLIB$toString$2$Proxy;
private static final Method CGLIB$hashCode$3$Method;
private static final MethodProxy CGLIB$hashCode$3$Proxy;
private static final Method CGLIB$clone$4$Method;
private static final MethodProxy CGLIB$clone$4$Proxy;
static void CGLIB$STATICHOOK1()
{
Method amethod[];
Method amethod1[];
CGLIB$THREAD_CALLBACKS = new ThreadLocal();
CGLIB$emptyArgs = new Object[0];
// 代理类
Class class1 = Class.forName("com.lanhuigu.spring.proxy.cglib.HelloService$$EnhancerByCGLIB$$4da4ebaf");
// 被代理类
Class class2;
amethod = ReflectUtils.findMethods(new String[] {
"equals", "(Ljava/lang/Object;)Z", "toString", "()Ljava/lang/String;", "hashCode", "()I", "clone", "()Ljava/lang/Object;"
}, (class2 = Class.forName("java.lang.Object")).getDeclaredMethods());
Method[] = amethod;
CGLIB$equals$1$Method = amethod[0];
CGLIB$equals$1$Proxy = MethodProxy.create(class2, class1, "(Ljava/lang/Object;)Z", "equals", "CGLIB$equals$1");
CGLIB$toString$2$Method = amethod[1];
CGLIB$toString$2$Proxy = MethodProxy.create(class2, class1, "()Ljava/lang/String;", "toString", "CGLIB$toString$2");
CGLIB$hashCode$3$Method = amethod[2];
CGLIB$hashCode$3$Proxy = MethodProxy.create(class2, class1, "()I", "hashCode", "CGLIB$hashCode$3");
CGLIB$clone$4$Method = amethod[3];
CGLIB$clone$4$Proxy = MethodProxy.create(class2, class1, "()Ljava/lang/Object;", "clone", "CGLIB$clone$4");
amethod1 = ReflectUtils.findMethods(new String[] {
"sayHello", "()V"
}, (class2 = Class.forName("com.lanhuigu.spring.proxy.cglib.HelloService")).getDeclaredMethods());
Method[] 1 = amethod1;
CGLIB$sayHello$0$Method = amethod1[0];
CGLIB$sayHello$0$Proxy = MethodProxy.create(class2, class1, "()V", "sayHello", "CGLIB$sayHello$0");
}
final void CGLIB$sayHello$0()
{
super.sayHello();
}
public final void sayHello()
{
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if(this.CGLIB$CALLBACK_0 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
if(var10000 != null) {
// 调用拦截器
var10000.intercept(this, CGLIB$setPerson$0$Method, CGLIB$emptyArgs, CGLIB$setPerson$0$Proxy);
} else {
super.sayHello();
}
}
......
......
}
intercept()方法由自定义MyMethodInterceptor实现,所以,最后调用MyMethodInterceptor中
的intercept()方法,从而完成了由代理对象访问到目标对象的动态代理实现。
如果要被代理的对象不是实现类,那么Spring会强制使用CGLib来实现动态代理。
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope
{
/**
* 部门表的别名
*/
public String deptAlias() default "";
/**
* 用户表的别名
*/
public String userAlias() default "";
}
* 数据过滤处理
*
* @author app
*/
@Aspect
@Component
public class DataScopeAspect
{
/**
* 全部数据权限
*/
public static final String DATA_SCOPE_ALL = "1";
/**
* 自定数据权限
*/
public static final String DATA_SCOPE_CUSTOM = "2";
/**
* 部门数据权限
*/
public static final String DATA_SCOPE_DEPT = "3";
/**
* 部门及以下数据权限
*/
public static final String DATA_SCOPE_DEPT_AND_CHILD = "4";
/**
* 仅本人数据权限
*/
public static final String DATA_SCOPE_SELF = "5";
// 配置织入点
@Pointcut("@annotation(com.app.framework.aspectj.lang.annotation.DataScope)")
public void dataScopePointCut()
{
}
@Before("dataScopePointCut()")
public void doBefore(JoinPoint point) throws Throwable
{
handleDataScope(point);
}
protected void handleDataScope(final JoinPoint joinPoint)
{
// 获得注解
DataScope controllerDataScope = getAnnotationLog(joinPoint);
if (controllerDataScope == null)
{
return;
}
// 获取当前的用户
LoginUser loginUser = SpringUtils.getBean(TokenService.class).getLoginUser(ServletUtils.getRequest());
SysUser currentUser = loginUser.getUser();
if (currentUser != null)
{
// 如果是超级管理员,则不过滤数据
if (!currentUser.isAdmin())
{
dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(),
controllerDataScope.userAlias());
}
}
}
/**
* 数据范围过滤
*
* @param joinPoint 切点
* @param user 用户
* @param alias 别名
*/
public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias)
{
StringBuilder sqlString = new StringBuilder();
for (SysRole role : user.getRoles())
{
String dataScope = role.getDataScope();
if (DATA_SCOPE_ALL.equals(dataScope))
{
sqlString = new StringBuilder();
break;
}
else if (DATA_SCOPE_CUSTOM.equals(dataScope))
{
sqlString.append(StringUtils.format(
" OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias,
role.getRoleId()));
}
else if (DATA_SCOPE_DEPT.equals(dataScope))
{
sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
}
else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope))
{
sqlString.append(StringUtils.format(
" OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )",
deptAlias, user.getDeptId(), user.getDeptId()));
}
else if (DATA_SCOPE_SELF.equals(dataScope))
{
if (StringUtils.isNotBlank(userAlias))
{
sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId()));
}
else
{
// 数据权限为仅本人且没有userAlias别名不查询任何数据
sqlString.append(" OR 1=0 ");
}
}
}
if (StringUtils.isNotBlank(sqlString.toString()))
{
BaseEntity baseEntity = (BaseEntity) joinPoint.getArgs()[0];
baseEntity.setDataScope(" AND (" + sqlString.substring(4) + ")");
}
}
/**
* 是否存在注解,如果存在就获取
*/
private DataScope getAnnotationLog(JoinPoint joinPoint)
{
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method != null)
{
return method.getAnnotation(DataScope.class);
}
return null;
}
}
@DataScope(deptAlias = "d", userAlias = "u")
public List
{
return userMapper.selectUserList(user);
}
编程事务
声明事务
如果在dao层,回滚的时候只能回滚到当前方法,但一般我们的service层的方法都是由很多dao层的方法组成的
如果在dao层,commit的次数会过多
记录之前的版本,允许回滚
实现事务的原子性,要支持回滚操作,在某个操作失败后,回滚到事务执行之前的状态。
事务开始和结束之间的中间状态不会被其他事务看到
事务的一致性决定了一个系统设计和实现的复杂度,也导致了事务的不同隔离级别。
此种情况会存在一个不一致窗口,
指的是读操作可以读到最新值的一段时间。
事务更新一份数据,最终一致性保证在没有其他事务更新同样的值的话,
最终所有的事务都会读到之前事务更新的最新值。
如果没有错误发生,不一致窗口的大小依赖于:通信延迟,系统负载等。
适当的破坏一致性来提升性能与并行度 例如:最终一致~=读未提交。
使用后端数据库默认的隔离级别,
MySQL的默认隔离级别高于Oracle的默认隔离级别
Oracle 默认采用的 READ_COMMITTED隔离级别.
Mysql 默认采用的 REPEATABLE_READ隔离级别 。
允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
所做的修改也会对事务内的查询做出影响,
这种级别显然很不安全。
但是在表对某行进行修改时,会对该行加上行共享锁
允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
只有在事务提交后,才会对另一个事务产生影响,
并且在对表进行修改时,
会对表数据行加上行共享锁
对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,
可以阻止脏读和不可重复读,但幻读仍有可能发生。
其中一个事务修改数据对另一个事务不会造成影响,
即使修改的事务已经提交也不会对另一个事务造成影响。
在事务中对某条记录修改,
会对记录加上行共享锁,
直到事务结束才会释放。
最高的隔离级别,完全服从ACID的隔离级别。
所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,
也就是说,该级别可以防止脏读、不可重复读以及幻读。
但是这将严重影响程序的性能。通常情况下也不会用到该级别。
其他事务对该表将只能进行读操作,
而不能进行写操作。
每一次的事务提交后就会保证不会丢失
分别是DataSource、TransactionManager和代理机制这三部分,
无论哪种配置方式,一般变化的只是代理机制这部分。
DataSource、TransactionManager这两部分只是会根据数据访问方式有所变化,
比如使用Hibernate进行数据访问时,
DataSource实际为SessionFactory,
TransactionManager的实现为HibernateTransactionManager。
如JdbcTempalte和HibernateTemplate是一样的方法。
它使用回调方法,把应用程序从处理取得和释放资源中解脱出来。
如同其他模板,TransactionTemplate是线程安全的。
Object result = tt.execute(
new TransactionCallback(){
public Object doTransaction(TransactionStatus status){
updateOperation();
return resultOfUpdateOperation();
}
}
); // 执行execute方法进行事务管理
使用TransactionCallback()可以返回一个值。
如果使用TransactionCallbackWithoutResult则没有返回值。
//定义一个某个框架平台的TransactionManager,如JDBC、Hibernate
dataSourceTransactionManager.setDataSource(this.getJdbcTemplate().getDataSource()); // 设置数据源
DefaultTransactionDefinition transDef = new DefaultTransactionDefinition(); // 定义事务属性
transDef.setPropagationBehavior(DefaultTransactionDefinition.PROPAGATION_REQUIRED); // 设置传播行为属性
TransactionStatus status = dataSourceTransactionManager.getTransaction(transDef); // 获得事务状态
try {
// 数据库操作
dataSourceTransactionManager.commit(status);// 提交
} catch (Exception e) {
dataSourceTransactionManager.rollback(status);// 回滚
}
默认情况下,事务只有遇到运行期异常时才会回滚,
而在遇到检查型异常时不会回滚,也可以由用户自己定义
通过PlatformTransactionManager这个接口,
Spring为各个平台如JDBC、Hibernate等都提供了对应的事务管理器,
但是具体的实现就是各个平台自己的事情了。
所有依赖于它的对象都会得到通知被制动更新,如Spring中listener的实现--ApplicationListener。
读取bean配置文档,
管理bean的加载、实例化,
控制bean的生命周期,
维护bean之间的依赖关系
除了提供BeanFactory所具有的功能外,
还提供了更完整的框架功能
②统一的资源文件访问方式。
③提供在监听器中注册bean的事件。
④同时加载多个配置文件。
⑤载入多个(有继承关系)上下文 ,
使得每一个上下文都专注于一个特定的层次,比如应用的web层。
但是有的时候写在变量上会报空指针异常NPE,
然后通过写在构造器上就解决了此问题
@Autowired
private A a;
private final String prefix = a.getExcelPrefix();
........
}
这种方式会报错
private final String prefix;
@Autowired
public Test(A a) {
this.prefix= a.getExcelPrefix();
}
........
}
这样写就不报错了
但报错的原因是加载顺序的问题,
@autowired写在变量上的注入要等到类完全加载完,
才会将相应的bean注入,
而变量是在加载类的时候按照相应顺序加载的,
所以变量的加载要早于@autowired变量的加载,
那么给变量prefix 赋值的时候所使用的a,其实还没有被注入,
所以报空指针,
而使用构造器就在加载类的时候将a加载了,
这样在内部使用a给prefix 赋值就完全没有问题。
如果不使用构造器,那么也可以不给prefix 赋值,
而是在接下来的代码使用的地方,
通过a.getExcelPrefix()进行赋值,
这时的对a的使用是在类完全加载之后,
即a被注入了,所以也是可以的。
@Autowired一定要等本类构造完成后,
才能从外部引用设置进来。
所以@Autowired的注入时间一定会晚于构造函数的执行时间。
但在初始化变量的时候就使用了还没注入的bean,所以导致了NPE。
如果在初始化其它变量时不使用这个要注入的bean,
而是在以后的方法调用的时候去赋值,是可以使用这个bean的,
因为那时类已初始化好,即已注入好了。
才会将相应的bean注入,
而变量是在加载类的时候按照相应顺序加载的,
所以变量的加载要早于@autowired变量的加载,
那么给变量prefix 赋值的时候所使用的a,其实还没有被注入,
所以报空指针,
而使用构造器就在加载类的时候将a加载了,
这样在内部使用a给prefix 赋值就完全没有问题。
有利于分工
复用
好扩展,好维护
(2) 寻找处理器:由DispatcherServlet控制器查询一个或多个HandlerMapping,找到处理请求的Controller。
(3) 调用处理器:DispatcherServlet将请求提交到Controller。
(4)(5)调用业务处理和返回结果:Controller调用业务逻辑处理后,返回ModelAndView。
(6)(7)处理视图映射并返回模型: DispatcherServlet查询一个或多个ViewResoler视图解析器,找到ModelAndView指定的视图。
(8) Http响应:视图负责将结果显示到客户端。
用于类上,表示类中的所有响应请求都是以该地址作为父路径
通过@Autowired消除set、get方法
通过适当的HttpMessageConverter转换为指定格式后,
写入到Response对象的body数据区
在监听中会有contextInitialized(ServletContextEvent args) 初始化方法,
启动Web应用时,系统调用Listener的该方法
在该方法中获得ServletContext application = ServletContextEvent.getServletContext();
context-param的值 = application.getInitParameter("context-param的键")
得到这个context-param的值之后,就可以有一些操作了
用于处理Controller中的异常,Exception ex参数即Controller抛出的异常
返回值类型是ModelAndView,可以通过这个返回值来设置异常时显示的页面
那么它会接受并处理由Controller(或其任何子类)中的@RequestMapping方法抛出的异常
简化应用配置,更容易使用spring。方便搭建项目或构建一个微服务。
简化配置:封装常用套件,比如mybatis、hibernate、redis、mongodb等
自动管理依赖。
部署简单:内嵌Web容器,如 Tomcat
缺点是集成度较高,使用过程中不太容易了解底层。
spring.profiles.active= dev
或者在启动时java -jar xxx.jar -D spring.profiles.active= prod
多个配置文件:
application-dev.properties
application-prod.properties
application-qas.properties
1、只支持单个属性值读取
2、只支持简单数据类型,如String、Boolean及数值类型
1、支持实体属性自动封装;
2、支持复杂数据类型的解析,比如数组、列表List
spring-boot-starter-parent模块
2、创建独立的spring应用程序main方法运行
3、嵌入Tomcat无需部署war包,直接打成jar包nohup java -jar – & 启动就好
4、简化了maven的配置
4、自动配置spring添加对应的starter自动化配置
1、spring-boot-starter-web(嵌入Tomcat和web开发需要的servlet和jsp支持)
2、spring-boot-starter-data-jpa(数据库支持)
3、spring-boot-starter-data-Redis(Redis支持)
4、spring-boot-starter-data-solr(solr搜索应用框架支持)
5、mybatis-spring-boot-starter(第三方mybatis集成starter)
组合了 @SpringBootConfiguration 注解,实现配置文件的功能。
@EnableAutoConfiguration:
打开自动配置的功能,也可以关闭某个自动配置的选项,
如关闭数据源自动配置功能:
@SpringBootApplication
(exclude = { DataSourceAutoConfiguration.class })。
@ComponentScan:Spring组件扫描。
@SpringBootApplication
= (默认属性)@@SpringBootConfiguration
+ @EnableAutoConfiguration
+ @ComponentScan。
@SpringBootApplication
public class ApplicationMain {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
提到@Configuration就要提到他的搭档@Bean。
使用这两个注解就可以创建一个简单的spring配置类,
可以用来替代相应的xml配置文件。
@Configuration的注解类标识这个类可以使用Spring IoC容器作为bean定义的来源。
@Bean注解告诉Spring,一个带有@Bean的注解方法将返回一个对象,
该对象应该被注册为在Spring应用程序上下文中的bean。
<beans>
<bean id = "car" class="com.test.Car">
<property name="wheel" ref = "wheel"></property>
</bean>
<bean id = "wheel" class="com.test.Wheel"></bean>
</beans>
@Configuration
public class Conf {
@Bean
public Car car() {
Car car = new Car();
car.setWheel(wheel());
return car;
}
@Bean
public Wheel wheel() {
return new Wheel();
}
}
能够自动配置spring的上下文,
试图猜测和配置你想要的bean类,
通常会自动根据你的类路径和你的bean定义自动配置。
会自动扫描指定包下的全部标有@Component的类,并注册成bean,
当然包括@Component下的子注解@Service,@Repository,@Controller。
前提是你已经添加了jar依赖项,
如果spring-boot-starter-web已经添加Tomcat和SpringMVC,
这个注释就会自动假设您在开发一个web应用程序并添加相应的spring配置,
会自动去maven中读取每个starter中的spring.factories文件,
该文件里配置了所有需要被创建spring容器中bean
2、在main方法中加上@SpringBootApplication和@EnableAutoConfiguration
Spring boot 的所有自动化配置的实现都在 spring-boot-autoconfigure 依赖中,
通过@EnableAutoConfiguration 核心注解初始化,
并扫描 ClassPath 目录中自动配置类对应依赖。
并对对应的组件依赖按一定规则获取默认配置并自动初始化所需要的 Bean。
先答为什么需要自动配置?
顾名思义,自动配置的意义是利用这种模式代替了配置 XML 繁琐模式。
以前使用 Spring MVC ,需要进行配置组件扫描、调度器、视图解析器等,
使用 Spring Boot 自动配置后,只需要添加 MVC 组件即可自动配置所需要的 Bean。
所有自动配置的实现都在 spring-boot-autoconfigure 依赖中,
包括 Spring MVC 、Data 和其它框架的自动配置。
接着答spring-boot-autoconfigure 依赖的工作原理?
spring-boot-autoconfigure 依赖的工作原理很简单,
通过 @EnableAutoConfiguration 核心注解初始化,
并扫描 ClassPath 目录中自动配置类对应依赖。
比如工程中有木有添加 Thymeleaf 的 Starter 组件依赖。
如果有,就按按一定规则获取默认配置并自动初始化所需要的 Bean。
ioc的思想最核心的地方在于,
资源不由使用资源的双方管理,
而由不使用资源的第三方管理,
这可以带来很多好处:
第一,资源集中管理,实现资源的可配置和易管理。
第二,降低了使用资源双方的依赖程度,也就是我们说的耦合度。
注解 @EnableAutoConfiguration, @Configuration, @ConditionalOnClass 就是自动配置的核心,
首先它得是一个配置文件,其次根据类路径下是否有这个类去自动配置。
2、根据spring.factories配置加载AutoConfigure
3、根据@Conditional注解的条件,进行自动配置并将bean注入到Spring Context
2、使用JavaConfig有助于避免使用XML
3、避免大量的maven导入和各种版本冲突
4、通过提供默认值快速开始开发
5、没有单独的web服务器需要,这就意味着不再需要启动Tomcat、Glassfish或其他任何东西
6、需要更少的配置,因为没有web.xml文件。
只需添加用@Configuration注释的类,然后添加用@Bean注释的方法,
Spring将自动加载对象并像以前一样对其进行管理。
甚至可以将@Autowired添加到bean方法中,
以使用Spring自动装入需要的依赖关系中
内嵌了如Tomcat,Jetty和Undertow这样的容器,也就是说可以直接跑起来,用不着再做部署工作了。
无需再像Spring那样搞一堆繁琐的xml文件的配置;
可以自动配置Spring;
提供了一些现有的功能,如度量工具,表单数据验证以及一些外部配置这样的一些第三方功能;
提供的POM可以简化Maven的配置;
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.2.0</version>
</dependency>
在mybatis的接口中 添加@Mapper注解
在application.yml配置数据源信息
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
2)导入spring-boot-dependencies项目依赖
一、首先,需要xml中进行少量的配置来启动Java配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.2.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd">
<context:component-scan base-package="SpringStudy.Model">
</context:component-scan>
</beans>
二、定义一个配置类
用@Configuration注解该类,等价 与XML中配置beans;用@Bean标注方法等价于XML中配置bean。
package SpringStudy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import SpringStudy.Model.Counter;
import SpringStudy.Model.Piano;
@Configuration
public class SpringConfig {
@Bean
public Piano piano(){
return new Piano();
}
@Bean(name = "counter")
public Counter counter(){
return new Counter(12,"Shake it Off",piano());
}
}
三、基础类代码
Counter:
package SpringStudy.Model;
public class Counter {
public Counter() {
}
public Counter(double multiplier, String song,Instrument instrument) {
this.multiplier = multiplier;
this.song = song;
this.instrument=instrument;
}
private double multiplier;
private String song;
@Resource
private Instrument instrument;
public double getMultiplier() {
return multiplier;
}
public void setMultiplier(double multiplier) {
this.multiplier = multiplier;
}
public String getSong() {
return song;
}
public void setSong(String song) {
this.song = song;
}
public Instrument getInstrument() {
return instrument;
}
public void setInstrument(Instrument instrument) {
this.instrument = instrument;
}
}
Piano类
package SpringStudy.Model;
public class Piano {
private String name="Piano";
private String sound;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSound() {
return sound;
}
public void setSound(String sound) {
this.sound = sound;
}
}
四、调用测试类
package webMyBatis;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import SpringStudy.Model.Counter;
public class SpringTest {
public static void main(String[] args) {
//ApplicationContext ctx = new ClassPathXmlApplicationContext("spring/bean.xml");// 读取bean.xml中的内容
ApplicationContext annotationContext = new AnnotationConfigApplicationContext("SpringStudy");
Counter c = annotationContext.getBean("counter", Counter.class);// 创建bean的引用对象
System.out.println(c.getMultiplier());
System.out.println(c.isEquals());
System.out.println(c.getSong());
System.out.println(c.getInstrument().getName());
}
}
注意:如果是在xml中配置beans和bean的话,或者使用自动扫描调用的话,代码为
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring/bean.xml");// 读取bean.xml中的内容
Counter c = ctx.getBean("counter", Counter.class);// 创建bean的引用对象
五、运行结果
12.0
false
Shake it Off
Piano
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.extern.slf4j.Slf4j;
/**
* netty服务端处理器
**/
@Slf4j
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
/**
* 客户端连接会触发
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("Channel active......");
}
/**
* 客户端发消息会触发
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("服务器收到消息: {}", msg.toString());
ctx.write("你也好哦");
ctx.flush();
}
/**
* 发生异常触发
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;
/**
* netty服务初始化器
**/
public class ServerChannelInitializer extends ChannelInitializer
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//添加编解码
socketChannel.pipeline().addLast("decoder", new StringDecoder(CharsetUtil.UTF_8));
socketChannel.pipeline().addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));
socketChannel.pipeline().addLast(new NettyServerHandler());
}
}
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.net.InetSocketAddress;
/**
*
* 服务启动监听器
**/
@Component
@Slf4j
public class NettyServer {
public void start(InetSocketAddress socketAddress) {
//new 一个主线程组
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
//new 一个工作线程组
EventLoopGroup workGroup = new NioEventLoopGroup(200);
ServerBootstrap bootstrap = new ServerBootstrap()
.group(bossGroup, workGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ServerChannelInitializer())
.localAddress(socketAddress)
//设置队列大小
.option(ChannelOption.SO_BACKLOG, 1024)
// 两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文
.childOption(ChannelOption.SO_KEEPALIVE, true);
//绑定端口,开始接收进来的连接
try {
ChannelFuture future = bootstrap.bind(socketAddress).sync();
log.info("服务器启动开始监听端口: {}", socketAddress.getPort());
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//关闭主线程组
bossGroup.shutdownGracefully();
//关闭工作线程组
workGroup.shutdownGracefully();
}
}
}
import com.example.demo.netty.server.demo.NettyServer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.net.InetSocketAddress;
@SpringBootApplication
public class ServerApplication {
public static void main(String[] args) {
SpringApplication application = new SpringApplication(ServerApplication.class);
application.run(args);
//启动服务端
NettyServer nettyServer = new NettyServer();
nettyServer.start(new InetSocketAddress("127.0.0.1", 8090));
}
}
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("客户端Active .....");
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("客户端收到消息: {}", msg.toString());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
public class NettyClientInitializer extends ChannelInitializer
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast("decoder", new StringDecoder());
socketChannel.pipeline().addLast("encoder", new StringEncoder());
socketChannel.pipeline().addLast(new NettyClientHandler());
}
}
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class NettyClient {
public void start() {
EventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap()
.group(group)
//该参数的作用就是禁止使用Nagle算法,使用于小数据即时传输
.option(ChannelOption.TCP_NODELAY, true)
.channel(NioSocketChannel.class)
.handler(new NettyClientInitializer());
try {
ChannelFuture future = bootstrap.connect("127.0.0.1", 8090).sync();
log.info("客户端成功....");
//发送消息
future.channel().writeAndFlush("你好啊");
// 等待连接被关闭
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
group.shutdownGracefully();
}
}
}
import com.example.netty.client.demo.NettyClient;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import java.util.HashMap;
import java.util.Map;
@SpringBootApplication
public class ClientApplication {
public static void main(String[] args) {
SpringApplication application = new SpringApplication(ClientApplication.class);
application.run(args);
//启动netty客户端
NettyClient nettyClient = new NettyClient();
nettyClient.start();
}
@Bean
public TomcatServletWebServerFactory updatePort(){
return new TomcatServletWebServerFactory(8888);
}
}
函数listen(int socketfd,int backlog)用来初始化服务端可连接队列,
服务端处理客户端连接请求是顺序处理的,
所以同一时间只能处理一个客户端连接,多个客户端来的时候,
服务端将不能处理的客户端连接请求放在队列中等待处理,
backlog参数指定了队列的大小
这个参数表示允许重复使用本地地址和端口,
比如,某个服务器进程占用了TCP的80端口进行监听,
此时再次监听该端口就会返回错误,使用该参数就可以解决问题,
该参数允许共用该端口,这个在服务器程序中比较常使用,
比如某个进程非正常退出,
该程序占用的端口可能要被占用一段时间才能允许其他进程使用,
而且程序死掉以后,内核一需要一定的时间才能够释放此端口,
不设置SO_REUSEADDR就无法正常使用该端口。
该参数用于设置TCP连接,当设置该选项以后,连接会测试链接的状态,
这个选项用于可能长时间没有数据交流的连接。
当设置该选项以后,如果在两小时内没有数据的通信时,
TCP会自动发送一个活动探测数据报文
ChannelOption.SO_RCVBUF参数对应于套接字选项中的SO_RCVBUF
这两个参数用于操作接收缓冲区和发送缓冲区的大小,
接收缓冲区用于保存网络协议站内收到的数据,直到应用程序读取成功,
发送缓冲区用于保存发送数据,直到发送成功。
Linux内核默认的处理方式是当用户调用close()方法的时候,函数返回,
在可能的情况下,尽量发送数据,不一定保证会发生剩余的数据,
造成了数据的不确定性,使用SO_LINGER可以阻塞close()的调用时间,
直到数据完全发送
该参数的使用与Nagle算法有关,
Nagle算法是将小的数据包组装为更大的帧然后进行发送,
而不是输入一次发送一次,
因此在数据包不足的时候会等待其他数据的到了,
组装成大的数据包进行发送,虽然该方式有效提高网络的有效负载,但是却造成了延时,
而该参数的作用就是禁止使用Nagle算法,使用于小数据即时传输,
于TCP_NODELAY相对应的是TCP_CORK,
该选项是需要等到发送的数据量最大的时候,一次性发送数据,适用于文件传输。
值为False时,连接自动关闭;
为True时,触发ChannelInboundHandler的userEventTriggered()方法,
事件为ChannelInputShutdownEvent。
子线程才是真正监听和接受请求的,
closeFuture()是开启了一个channel的监听器,
负责监听channel是否关闭的状态,
如果监听到channel关闭了,子线程才会释放,
syncUninterruptibly()让主线程同步等待子线程结果
包括服务注册与发现,配置中心,全链路监控,服务网关,负载均衡,熔断器等组件。
spring.application.name=app_wechat_service #指定config server中配置的application名称
spring.cloud.config.uri=http://172.18.209.19:9088 #指定config server的地址
spring.cloud.config.profile=dev #指定当前运行的环境
@Scheduled(cron="${wx.token.sync.cron}") //从config server中配置的服务为 app_wechat_service,属性名为wx.token.sync.cron中读取数据
oauth -> 授权框架, 用在使用第三方账号登录的情况(比如使用weibo, qq, github登录某个app)
-> 一定需要token,JWT是其中的一个
--> InMemoryTokenStore
--> JdbcTokenStore
--> JSON Web Token (JWT)
jwt
-> 认证协议, 基于Token的身份认证是无状态的,服务器或者Session中不会存储任何用户信息。
-> 用在前后端分离, 需要简单的对后台API进行保护时使用
-> 优点
-> 缺点
-->不能很容易的撤销一个access token,
因此一般用该方式存储的token的有效期很短,
并且在刷新token的时候之前的token会被废除
-->token很长,
因为它里面存了很多关于用户凭证的信息。
JwtTokenStore不会真的存储数据,
它不持久化任何数据
apache shiro
它是一个强大的,高度自定义的认证和访问控制框架。
Authentication(认证)
和 Authorization(授权,也叫访问控制)
认证就是你是谁
授权就是你可以做什么
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用方法级别的权限认证
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserDetailsService myUserDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 允许所有用户访问"/"和"/index.html"
http.authorizeRequests()
.antMatchers("/", "/index.html").permitAll()
.anyRequest().authenticated() // 其他地址的访问均需验证权限
.and()
.formLogin()
.loginPage("/login.html") // 登录页
.failureUrl("/login-error.html").permitAll()
.and()
.logout()
.logoutSuccessUrl("/index.html");
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserService userService;
/**
* 授权的时候是对角色授权,而认证的时候应该基于资源,而不是角色,因为资源是不变的,而用户的角色是会变的
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = userService.getUserByName(username);
if (null == sysUser) {
throw new UsernameNotFoundException(username);
}
List
for (SysRole role : sysUser.getRoleList()) {
for (SysPermission permission : role.getPermissionList()) {
authorities.add(new SimpleGrantedAuthority(permission.getCode()));
}
}
return new User(sysUser.getUsername(), sysUser.getPassword(), authorities);
}
}
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Cacheable(cacheNames = "authority", key = "#username")
@Override
public SysUser getUserByName(String username) {
return userDao.selectByName(username);
}
}
@Repository
public class UserDao {
private SysRole admin = new SysRole("ADMIN", "管理员");
private SysRole developer = new SysRole("DEVELOPER", "开发者");
{
SysPermission p1 = new SysPermission();
p1.setCode("UserIndex");
p1.setName("个人中心");
p1.setUrl("/user/index.html");
SysPermission p2 = new SysPermission();
p2.setCode("BookList");
p2.setName("图书列表");
p2.setUrl("/book/list");
SysPermission p3 = new SysPermission();
p3.setCode("BookAdd");
p3.setName("添加图书");
p3.setUrl("/book/add");
SysPermission p4 = new SysPermission();
p4.setCode("BookDetail");
p4.setName("查看图书");
p4.setUrl("/book/detail");
admin.setPermissionList(Arrays.asList(p1, p2, p3, p4));
developer.setPermissionList(Arrays.asList(p1, p2));
}
public SysUser selectByName(String username) {
log.info("从数据库中查询用户");
if ("zhangsan".equals(username)) {
SysUser sysUser = new SysUser("zhangsan", "$2a$10$EIfFrWGINQzP.tmtdLd2hurtowwsIEQaPFR9iffw2uSKCOutHnQEm");
sysUser.setRoleList(Arrays.asList(admin, developer));
return sysUser;
}else if ("lisi".equals(username)) {
SysUser sysUser = new SysUser("lisi", "$2a$10$EIfFrWGINQzP.tmtdLd2hurtowwsIEQaPFR9iffw2uSKCOutHnQEm");
sysUser.setRoleList(Arrays.asList(developer));
return sysUser;
}
return null;
}
}
用户zhangsan可以查看所有的,而lisi只能查看图书列表,不能添加不能查看详情。
public class LoginController {
// Login form
@RequestMapping("/login.html")
public String login() {
return "login.html";
}
// Login form with error
@RequestMapping("/login-error.html")
public String loginError(Model model) {
model.addAttribute("loginError", true);
return "login.html";
}
}
@RequestMapping("/book")
public class BookController {
@PreAuthorize("hasAuthority('BookList')")
@GetMapping("/list.html")
public String list() {
return "book/list";
}
@PreAuthorize("hasAuthority('BookAdd')")
@GetMapping("/add.html")
public String add() {
return "book/add";
}
@PreAuthorize("hasAuthority('BookDetail')")
@GetMapping("/detail.html")
public String detail() {
return "book/detail";
}
}
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
/**
* 个人中心
*/
@PreAuthorize("hasAuthority('UserIndex')")
@GetMapping("/index")
public String index() {
return "user/index";
}
@RequestMapping("/hi")
@ResponseBody
public String hi() {
SysUser sysUser = userService.getUserByName("zhangsan");
return sysUser.toString();
}
}
@Slf4j
@ControllerAdvice
public class ErrorController {
@ExceptionHandler(Throwable.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public String exception(final Throwable throwable, final Model model) {
log.error("Exception during execution of SpringSecurity application", throwable);
String errorMessage = (throwable != null ? throwable.getMessage() : "Unknown error");
model.addAttribute("errorMessage", errorMessage);
return "error";
}
}
//https://github.com/freew01f/securing-spring-boot-with-jwts
@RestController
@EnableAutoConfiguration
public class DemoApplication {
// main函数,Spring Boot程序入口
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
// 根目录映射 Get访问方式 直接返回一个字符串
@RequestMapping("/")
Map
// 返回map会变成JSON key value方式
Map
map.put("content", "hello freewolf~");
return map;
}
}
"content": "hello freewolf~"
}
import org.json.JSONObject;
public class JSONResult{
public static String fillResultString(Integer status, String message, Object result){
JSONObject jsonObject = new JSONObject(){{
put("status", status);
put("message", message);
put("result", result);
}};
return jsonObject.toString();
}
}
message - 一般显示错误信息
result - 结果集
class UserController {
// 路由映射到/users
@RequestMapping(value = "/users", produces="application/json;charset=UTF-8")
public String usersList() {
ArrayList
add("freewolf");
add("tom");
add("jerry");
}};
return JSONResult.fillResultString(0, "", users);
}
@RequestMapping(value = "/hello", produces="application/json;charset=UTF-8")
public String hello() {
ArrayList
return JSONResult.fillResultString(0, "", users);
}
@RequestMapping(value = "/world", produces="application/json;charset=UTF-8")
public String world() {
ArrayList
return JSONResult.fillResultString(0, "", users);
}
}
"result": [
"freewolf",
"tom",
"jerry"
],
"message": "",
"status": 0
}
先通过申请一个JWT(JSON Web Token读jot),
然后通过这个访问/users,才能拿到数据。
通过使用HMAC(Hash-based Message Authentication Code)计算信息摘要,
也可以用RSA公私钥中的私钥进行签名。这个根据业务场景进行选择。
我们将引入一个安全设置类WebSecurityConfig,
这个类需要从WebSecurityConfigurerAdapter类继承。
import com.example.demo.demo.jwt.CustomAuthenticationProvider;
import com.example.demo.demo.jwt.JWTAuthenticationFilter;
import com.example.demo.demo.jwt.JWTLoginFilter;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// 设置 HTTP 验证规则
@Override
protected void configure(HttpSecurity http) throws Exception {
// 关闭csrf验证
http.csrf().disable()
// 对请求进行认证
.authorizeRequests()
// 所有 / 的所有请求 都放行
.antMatchers("/").permitAll()
// 所有 /login 的POST请求 都放行
.antMatchers(HttpMethod.POST, "/login").permitAll()
// 添加权限检测
.antMatchers("/hello").hasAuthority("AUTH_WRITE")
// 角色检测
.antMatchers("/world").hasRole("ADMIN")
// 所有请求需要身份认证
.anyRequest().authenticated()
.and()
// 添加一个过滤器 所有访问 /login 的请求交给 JWTLoginFilter 来处理 这个类处理所有的JWT相关内容
.addFilterBefore(new JWTLoginFilter("/login", authenticationManager()),
UsernamePasswordAuthenticationFilter.class)
// 添加一个过滤器验证其他请求的Token是否合法
.addFilterBefore(new JWTAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 使用自定义身份验证组件
auth.authenticationProvider(new CustomAuthenticationProvider());
}
}
我们设置所有人都能访问/和POST方式访问/login,
其他的任何路由都需要进行认证。
然后将所有访问/login的请求,
都交给JWTLoginFilter过滤器来处理。
一个负责存储用户名密码,
另一个是一个权限类型,负责存储权限和角色。
public class AccountCredentials {
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
import org.springframework.security.core.GrantedAuthority;
public class GrantedAuthorityImpl implements GrantedAuthority {
private String authority;
public GrantedAuthorityImpl(String authority) {
this.authority = authority;
}
public void setAuthority(String authority) {
this.authority = authority;
}
@Override
public String getAuthority() {
return this.authority;
}
}
import com.example.demo.demo.jwt.JSONResult;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
import java.util.List;
public class TokenAuthenticationService {
static final long EXPIRATIONTIME = 432_000_000; // 5天
static final String SECRET = "P@ssw02d"; // JWT密码
static final String TOKEN_PREFIX = "Bearer"; // Token前缀
static final String HEADER_STRING = "Authorization";// 存放Token的Header Key
static void addAuthentication(HttpServletResponse response, String username) {
// 生成JWT
String JWT = Jwts.builder()
// 保存权限(角色)
.claim("authorities", "ROLE_ADMIN,AUTH_WRITE")
// 用户名写入标题
.setSubject(username)
// 有效期设置
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATIONTIME))
// 签名设置
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
// 将 JWT 写入 body
try {
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_OK);
response.getOutputStream().println(JSONResult.fillResultString(0, "", JWT));
} catch (IOException e) {
e.printStackTrace();
}
}
static Authentication getAuthentication(HttpServletRequest request) {
// 从Header中拿到token
String token = request.getHeader(HEADER_STRING);
if (token != null) {
// 解析 Token
Claims claims = Jwts.parser()
// 验签
.setSigningKey(SECRET)
// 去掉 Bearer
.parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
.getBody();
// 拿用户名
String user = claims.getSubject();
// 得到 权限(角色)
List
// 返回验证令牌
return user != null ?
new UsernamePasswordAuthenticationToken(user, null, authorities) :
null;
}
return null;
}
}
这里简单写了,这个类就是提供密码验证功能,
在实际使用时换成自己相应的验证逻辑,
从数据库中取出、比对、赋予用户相应权限。
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import java.util.ArrayList;
// 自定义身份认证验证组件
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 获取认证的用户名 & 密码
String name = authentication.getName();
String password = authentication.getCredentials().toString();
// 认证逻辑
if (name.equals("admin") && password.equals("123456")) {
// 这里设置权限和角色
ArrayList
authorities.add( new GrantedAuthorityImpl("ROLE_ADMIN") );
authorities.add( new GrantedAuthorityImpl("AUTH_WRITE") );
// 生成令牌
Authentication auth = new UsernamePasswordAuthenticationToken(name, password, authorities);
return auth;
}else {
throw new BadCredentialsException("密码错误~");
}
}
// 是否可以提供输入类型的认证服务
@Override
public boolean supports(Class authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
attemptAuthentication - 登录时需要验证时候调用
successfulAuthentication - 验证成功后调用
unsuccessfulAuthentication - 验证失败后调用,这里直接灌入500错误返回,由于同一JSON返回,HTTP就都返回200了
import com.fasterxml.jackson.databind.ObjectMapper;
import org.json.JSONObject;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JWTLoginFilter extends AbstractAuthenticationProcessingFilter {
public JWTLoginFilter(String url, AuthenticationManager authManager) {
super(new AntPathRequestMatcher(url));
setAuthenticationManager(authManager);
}
@Override
public Authentication attemptAuthentication(
HttpServletRequest req, HttpServletResponse res)
throws AuthenticationException, IOException, ServletException {
// JSON反序列化成 AccountCredentials
AccountCredentials creds = new ObjectMapper().readValue(req.getInputStream(), AccountCredentials.class);
// 返回一个验证令牌
return getAuthenticationManager().authenticate(
new UsernamePasswordAuthenticationToken(
creds.getUsername(),
creds.getPassword()
)
);
}
@Override
protected void successfulAuthentication(
HttpServletRequest req,
HttpServletResponse res, FilterChain chain,
Authentication auth) throws IOException, ServletException {
TokenAuthenticationService.addAuthentication(res, auth.getName());
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_OK);
response.getOutputStream().println(JSONResult.fillResultString(500, "Internal Server Error!!!", JSONObject.NULL));
}
}
它拦截所有需要JWT的请求,
然后调用TokenAuthenticationService类的静态方法去做JWT验证
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
public class JWTAuthenticationFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain filterChain)
throws IOException, ServletException {
Authentication authentication = TokenAuthenticationService
.getAuthentication((HttpServletRequest)request);
SecurityContextHolder.getContext()
.setAuthentication(authentication);
filterChain.doFilter(request,response);
}
}
先程序启动 - main函数
注册验证组件 - WebSecurityConfig 类 configure(AuthenticationManagerBuilder auth)方法,这里我们注册了自定义验证组件
设置验证规则 - WebSecurityConfig 类 configure(HttpSecurity http)方法,这里设置了各种路由访问规则
初始化过滤组件 - JWTLoginFilter 和 JWTAuthenticationFilter 类会初始化
"result": "eyJhbGciOiJIUzUxMiJ9.eyJhdXRob3JpdGllcyI6IlJPTEVfQURNSU4sQVVUSF9XUklURSIsInN1YiI6ImFkbWluIiwiZXhwIjoxNDkzNzgyMjQwfQ.HNfV1CU2CdAnBTH682C5-KOfr2P71xr9PYLaLpDVhOw8KWWSJ0lBo0BCq4LoNwsK_Y3-W3avgbJb0jW9FNYDRQ",
"message": "",
"status": 0
}
自定义身份认证验证组件,进行身份认证 - CustomAuthenticationProvider 类 authenticate 方法
验证成功 - JWTLoginFilter 类 successfulAuthentication 方法
生成JWT - TokenAuthenticationService 类 addAuthentication方法
"result":["freewolf","tom","jerry"],
"message":"",
"status":0
}
验证JWT - TokenAuthenticationService 类 getAuthentication 方法
访问Controller
对于GrantedAuthority接口实现类来说是不区分是Role还是Authority,
二者区别就是如果是hasAuthority判断,就是判断整个字符串,判断hasRole时,
系统自动加上ROLE_到判断的Role字符串上,
也就是说hasRole("CREATE")和hasAuthority('ROLE_CREATE')是相同的。
利用这些可以搭建完整的RBAC体系。
其存储作用域为 Session,
当 Session flush 或 close 之后,
该 Session 中的所有 Cache 就将清空,
默认打开一级缓存。
一般通过自定义缓存Redis、Memcached等实现
默认也是采用 PerpetualCache,HashMap 存储,
不同在于其存储作用域为 Mapper(Namespace),
并且可自定义存储源,如 Ehcache,redis。
默认不打开二级缓存,要开启二级缓存,
使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),
在它的映射文件中配置<cache/>
https://juejin.im/post/592c08292f301e006c60cae2
接口共有以下五个方法
void putObject(Object key, Object value):将查询结果塞入缓存。
Object getObject(Object key):从缓存中获取被缓存的查询结果。
Object removeObject(Object key):从缓存中删除对应的key、value。只有在回滚时触发。
一般我们也可以不用实现,具体使用方式请参考:org.apache.ibatis.cache.decorators.TransactionalCache。
void clear():发生更新时,清除缓存。
int getSize():可选实现。返回缓存的数量。
ReadWriteLock getReadWriteLock():可选实现。用于实现原子性的缓存操作。
private static final Logger logger = LoggerFactory.getLogger(RedisCache.class);
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final String id; // cache instance id
private RedisTemplate redisTemplate;
private static final long EXPIRE_TIME_IN_MINUTES = 30; // redis过期时间
public RedisCache(String id) {
if (id == null) {
throw new IllegalArgumentException("Cache instances require an ID");
}
this.id = id;
}
@Override
public String getId() {
return id;
}
/**
* Put query result to redis
*
* @param key
* @param value
*/
@Override
@SuppressWarnings("unchecked")
public void putObject(Object key, Object value) {
RedisTemplate redisTemplate = getRedisTemplate();
ValueOperations opsForValue = redisTemplate.opsForValue();
opsForValue.set(key, value, EXPIRE_TIME_IN_MINUTES, TimeUnit.MINUTES);
logger.debug("Put query result to redis");
}
/**
* Get cached query result from redis
*
* @param key
* @return
*/
@Override
public Object getObject(Object key) {
RedisTemplate redisTemplate = getRedisTemplate();
ValueOperations opsForValue = redisTemplate.opsForValue();
logger.debug("Get cached query result from redis");
return opsForValue.get(key);
}
/**
* Remove cached query result from redis
*
* @param key
* @return
*/
@Override
@SuppressWarnings("unchecked")
public Object removeObject(Object key) {
RedisTemplate redisTemplate = getRedisTemplate();
redisTemplate.delete(key);
logger.debug("Remove cached query result from redis");
return null;
}
/**
* Clears this cache instance
*/
@Override
public void clear() {
RedisTemplate redisTemplate = getRedisTemplate();
redisTemplate.execute((RedisCallback) connection -> {
connection.flushDb();
return null;
});
logger.debug("Clear all the cached query result from redis");
}
@Override
public int getSize() {
return 0;
}
@Override
public ReadWriteLock getReadWriteLock() {
return readWriteLock;
}
private RedisTemplate getRedisTemplate() {
if (redisTemplate == null) {
redisTemplate = ApplicationContextHolder.getBean("redisTemplate");
}
return redisTemplate;
}
}
2.我们使用Spring封装的redisTemplate来操作Redis。
网上所有介绍redis做二级缓存的文章都是直接用jedis库,但是笔者认为这样不够Spring Style,
而且,redisTemplate封装了底层的实现,未来如果我们不用jedis了,我们可以直接更换底层的库,而不用修改上层的代码。
更方便的是,使用redisTemplate,我们不用关心redis连接的释放问题,否则新手很容易忘记释放连接而导致应用卡死。
3. 需要注意的是,这里不能通过autowire的方式引用redisTemplate,
因为RedisCache并不是Spring容器里的bean。所以我们需要手动地去调用容器的getBean方法来拿到这个bean。
4. 我们采用的redis序列化方式是默认的jdk序列化。所以数据库的查询对象(比如Product类)需要实现Serializable接口。
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.wooyoo.learning.dao.mapper.ProductMapper">
<!-- 开启基于redis的二级缓存 -->
<cache type="com.wooyoo.learning.util.RedisCache"/>
<select id="select" resultType="Product">
SELECT * FROM products WHERE id = #{id} LIMIT 1
</select>
<update id="update" parameterType="Product" flushCache="true">
UPDATE products SET name = #{name}, price = #{price} WHERE id = #{id} LIMIT 1
</update>
</mapper>
并且在update语句中,我们设置flushCache为true,
这样在更新product信息时,能够自动失效缓存(本质上调用的是clear方法)。
list<name> names = mapper.selectlike(wildcardname);
<select id=”selectlike”>
select * from foo where bar like #{value}
</select>
list<name> names = mapper.selectlike(wildcardname);
<select id=”selectlike”>
select * from foo where bar like "%"#{value}"%"
</select>
请问,这个Dao接口的工作原理是什么?
Dao接口里的方法,参数不同时,方法能重载吗?
接口的全限名,就是映射文件中的namespace的值;
接口的方法名,就是映射文件中Mapper的Statement的id值;
接口方法内的参数,就是传递给sql的参数。
Mapper接口是没有实现类的,
当调用接口方法时,接口全限名+方法名拼接字符串作为key值,
可唯一定位一个MapperStatement。
在Mybatis中,每一个<select>、<insert>、<update>、<delete>标签,
都会被解析为一个MapperStatement对象。
举例:com.mybatis3.mappers.StudentDao.findStudentById,
可以唯一找到namespace为com.mybatis3.mappers.StudentDao
下面 id 为 findStudentById 的 MapperStatement。
因为是使用 全限名+方法名 的保存和寻找策略。
Mapper 接口的工作原理是JDK动态代理,
Mybatis运行时会使用JDK动态代理为Mapper接口生成代理对象proxy,
代理对象会拦截接口方法,
转而执行MapperStatement所代表的sql,然后将sql执行结果返回。
在一对多的时候引入了collection节点,
不过都是在resultMap里面配置
它是针对ResultSet结果集执行的内存分页,而非物理分页,
可以在sql内直接书写带有物理分页的参数来完成物理分页功能,
也可以使用分页插件来完成物理分页。
实现自定义插件,在插件的拦截方法内拦截待执行的sql,
然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。
举例:select * from student,
拦截sql后重写为:select t.* from (select * from student)t limit 0,10
<!--association 一对一关联查询 -->
<select id="getClass" parameterType="int" resultMap="ClassesResultMap">
select * from class c,teacher t where c.teacher_id=t.t_id and c.c_id=#{id}
</select>
<resultMap type="com.lcb.user.Classes" id="ClassesResultMap">
<!-- 实体类的字段名和数据表的字段名映射 -->
<id property="id" column="c_id"/>
<result property="name" column="c_name"/>
<association property="teacher" javaType="com.lcb.user.Teacher">
<id property="id" column="t_id"/>
<result property="name" column="t_name"/>
</association>
</resultMap>
<select id="getClass2" parameterType="int" resultMap="ClassesResultMap2">
select * from class c,teacher t,student s where c.teacher_id=t.t_id and c.c_id=s.class_id and c.c_id=#{id}
</select>
<resultMap type="com.lcb.user.Classes" id="ClassesResultMap2">
<id property="id" column="c_id"/>
<result property="name" column="c_name"/>
<association property="teacher" javaType="com.lcb.user.Teacher">
<id property="id" column="t_id"/>
<result property="name" column="t_name"/>
</association>
<collection property="student" ofType="com.lcb.user.Student">
<id property="id" column="s_id"/>
<result property="name" column="s_name"/>
</collection>
</resultMap>
</mapper>
通过在resultMap里面配置association节点配置一对一的类就可以完成;
去再另外一个表里面查询数据,也是通过association配置,
但另外一个表的查询通过select属性配置。
通过在resultMap里面的collection节点配置一对多的类就可以完成;
去再另外一个表里面查询数据,也是通过配置collection,
但另外一个表的查询通过select节点配置。
然后把接口里面的方法和SQL语句绑定,
我们直接调用接口方法就可以,
这样比起原来了SqlSession提供的方法我们可以有更加灵活的选择和设置。
一种是通过注解绑定,
就是在接口的方法上面加上 @Select、@Update等注解,
里面包含Sql语句来绑定;
另外一种就是通过xml里面写SQL来绑定,
在这种情况下,要指定xml映射文件里面的namespace必须为接口的全路径名。
当Sql语句比较简单时候,用注解绑定,
当SQL语句比较复杂时候,用xml绑定,一般用xml绑定的比较多。
ParameterHandler、
ResultSetHandler、
StatementHandler、
Executor这4种接口的插件,
Mybatis使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,
每当执行这4种接口对象的方法时,就会进入拦截方法,
具体就是InvocationHandler的invoke()方法,
当然,只会拦截那些你指定需要拦截的方法。
实现Mybatis的Interceptor接口并复写intercept()方法,
然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,记住,别忘了在配置文件中配置你编写的插件。
association指的就是一对一,
collection指的就是一对多查询。
在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。
它的原理是,
使用CGLIB创建目标对象的代理对象,
当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),
拦截器invoke()方法发现a.getB()是null值,
那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,
然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。
这就是延迟加载的基本原理。
当然了,不光是Mybatis,几乎所有的包括Hibernate,支持延迟加载的原理都是一样的。
作用范围:Executor的这些特点,都严格限制在SqlSession生命周期范围内。
SimpleExecutor:每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。
ReuseExecutor:执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,
用完后,不关闭Statement对象,而是放置于Map<String, Statement>内,供下一次使用。
简言之,就是重复使用Statement对象。
BatchExecutor:执行update(没有select,JDBC批处理不支持select),
将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),
它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,
等待逐一执行executeBatch()批处理。与JDBC批处理相同。
<select id=”selectorder” parametertype=”int” resultetype=”me.gacl.domain.order”>
select order_id id, order_no orderno ,order_price price form orders where order_id=#{id};
</select>
<select id="getOrder" parameterType="int" resultMap="orderresultmap">
select * from orders where order_id=#{id}
</select>
<resultMap type=”me.gacl.domain.order” id=”orderresultmap”>
<!–用id属性来映射主键字段–>
<id property=”id” column=”order_id”>
<!–用result属性来映射非主键字段,property为实体类属性名,column为数据表中的属性–>
<result property = “orderno” column =”order_no”/>
<result property=”price” column=”order_price” />
</reslutMap>
通过LAST_INSERT_ID()获取刚插入记录的自增主键值,
在insert语句执行后,执行select LAST_INSERT_ID()就可以获取自增主键。
<insert id="insertUser" parameterType="cn.itcast.mybatis.po.User">
<selectKey keyProperty="id" order="AFTER" resultType="int">
select LAST_INSERT_ID()
</selectKey>
INSERT INTO USER(username,birthday,sex,address) VALUES(#{username},#{birthday},#{sex},#{address})
</insert>
<!-- oracle
在执行insert之前执行select 序列.nextval() from dual取出序列最大值,将值设置到user对象 的id属性
-->
<insert id="insertUser" parameterType="cn.itcast.mybatis.po.User">
<selectKey keyProperty="id" order="BEFORE" resultType="int">
select 序列.nextval() from dual
</selectKey>
INSERT INTO USER(id,username,birthday,sex,address) VALUES( 序列.nextval(),#{username},#{birthday},#{sex},#{address})
</insert>
使用@param注解:来命名参数
public interface usermapper {
user selectuser(@param(“username”) string username,
@param(“hashedpassword”) string hashedpassword);
}
<select id=”selectuser” resulttype=”user”>
select id, username, hashedpassword
from some_table where username = #{username} and hashedpassword = #{hashedpassword}
</select>
#{0},#{1}方式
//对应的xml,#{0}代表接收的是dao层中的第一个参数,#{1}代表dao层中第二参数,更多参数一致往后加即可。
<select id="selectUser"resultMap="BaseResultMap">
select * fromuser_user_t whereuser_name = #{0} anduser_area=#{1}
</select>
//映射文件的命名空间.SQL片段的ID,就可以调用对应的映射文件中的SQL
/**
* 由于我们的参数超过了两个,而方法中只有一个Object参数收集
* 因此我们使用Map集合来装载我们的参数
*/
Map<String, Object> map = new HashMap();
map.put("start", start);
map.put("end", end);
return sqlSession.selectList("StudentID.pagination", map);
}catch(Exception e){
e.printStackTrace();
sqlSession.rollback();
throw e;
}finally{
MybatisUtil.closeSqlSession();
}
<!--分页查询-->
<select id="pagination" parameterType="map" resultMap="studentMap">
/*根据key自动找到对应Map集合的value*/
select * from students limit #{start},#{end};
</select>
都有哪些动态sql?
能简述一下动态sql的执行原理不?
Mybatis提供了9种动态sql标签:trim | where | set | foreach | if | choose | when | otherwise | bind。
其执行原理为,使用OGNL从sql参数对象中计算表达式的值,根据表达式的值动态拼接sql,以此来完成动态sql的功能。
如果没有配置namespace的话,那么相同的id就会导致覆盖了。
请问,这个Dao接口的工作原理是什么?
Dao接口里的方法,参数不同时,方法能重载吗?
接口的全限名,就是映射文件中的namespace的值,
接口的方法名,就是映射文件中MappedStatement的id值,
接口方法内的参数,就是传递给sql的参数。
接口全限名+方法名拼接字符串作为key值,可唯一定位一个MappedStatement
可以唯一找到namespace为com.mybatis3.mappers.StudentDao下面id = findStudentById的MappedStatement。
在Mybatis中,每一个<select>、<insert>、<update>、<delete>标签,都会被解析为一个MappedStatement对象。
Dao接口的工作原理是JDK动态代理,
Mybatis运行时会使用JDK动态代理为Dao接口生成代理proxy对象,
代理对象proxy会拦截接口方法,转而执行MappedStatement所代表的sql,
然后将sql执行结果返回。
② Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql 的parameterType的类型相同;
③ Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同;
④ Mapper.xml文件中的namespace即是mapper接口的类路径。
使用此种方法需要编写mapper接口,mapper接口实现类、mapper.xml文件。
<mappers>
<mapper resource="mapper.xml文件的地址" />
<mapper resource="mapper.xml文件的地址" />
</mappers>
(3)实现类集成SqlSessionDaoSupport
mapper方法中可以this.getSqlSession()进行数据增删改查。
(4)spring 配置
<bean id=" " class="mapper接口的实现">
<property name="sqlSessionFactory" ref="sqlSessionFactory"></property>
</bean>
如果mapper.xml和mappre接口的名称相同且在同一个目录,
这里可以不用配置
<mappers>
<mapper resource="mapper.xml文件的地址" />
<mapper resource="mapper.xml文件的地址" />
</mappers>
①mapper.xml中的namespace为mapper接口的地址
②mapper接口中的方法名和mapper.xml中的定义的statement的id保持一致
③Spring中定义
<bean id="" class="org.mybatis.spring.mapper.MapperFactoryBean">
<property name="mapperInterface" value="mapper接口地址" />
<property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>
mapper.xml中的namespace为mapper接口的地址;
mapper接口中的方法名和mapper.xml中的定义的statement的id保持一致;
如果将mapper.xml和mapper接口的名称保持一致则不用在sqlMapConfig.xml中进行配置。
注意mapper.xml的文件名和mapper的接口名称保持一致,且放在同一个目录
(3)配置mapper扫描器:
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="mapper接口包地址"></property>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
(4)使用扫描器后从spring容器中获取mapper的实现对象。
@MapperScan("com.app.ghy.wechat.mapper")
public class AppWechatServiceApplication{
Logger logger = Logger.getLogger(AppWechatServiceApplication.class);
public static void main(String[] args) {
SpringApplication.run(AppWechatServiceApplication.class, args);
}
}
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface BaseMapper {
@Select("SELECT FUNC_SYS_SEQ(#{keyname}) as keyid ")
//参数名为keyname, 使用数据库中定义的函数 FUNC_SYS_SEQ
public String createKeyIDByCustom(@Param("keyname") String keyname);
}
--学生表
CREATE TABLE `Student`(
`s_id` VARCHAR(20),
`s_name` VARCHAR(20) NOT NULL DEFAULT '',
`s_birth` VARCHAR(20) NOT NULL DEFAULT '',
`s_sex` VARCHAR(10) NOT NULL DEFAULT '',
PRIMARY KEY(`s_id`)
);
CREATE TABLE `Course`(
`c_id` VARCHAR(20),
`c_name` VARCHAR(20) NOT NULL DEFAULT '',
`t_id` VARCHAR(20) NOT NULL,
PRIMARY KEY(`c_id`)
);
CREATE TABLE `Teacher`(
`t_id` VARCHAR(20),
`t_name` VARCHAR(20) NOT NULL DEFAULT '',
PRIMARY KEY(`t_id`)
);
CREATE TABLE `Score`(
`s_id` VARCHAR(20),
`c_id` VARCHAR(20),
`s_score` INT(3),
PRIMARY KEY(`s_id`,`c_id`)
);
insert into Student values('01' , '赵雷' , '1990-01-01' , '男');
insert into Student values('02' , '钱电' , '1990-12-21' , '男');
insert into Student values('03' , '孙风' , '1990-05-20' , '男');
insert into Student values('04' , '李云' , '1990-08-06' , '男');
insert into Student values('05' , '周梅' , '1991-12-01' , '女');
insert into Student values('06' , '吴兰' , '1992-03-01' , '女');
insert into Student values('07' , '郑竹' , '1989-07-01' , '女');
insert into Student values('08' , '王菊' , '1990-01-20' , '女');
--课程表测试数据
insert into Course values('01' , '语文' , '02');
insert into Course values('02' , '数学' , '01');
insert into Course values('03' , '英语' , '03');
--教师表测试数据
insert into Teacher values('01' , '张三');
insert into Teacher values('02' , '李四');
insert into Teacher values('03' , '王五');
--成绩表测试数据
insert into Score values('01' , '01' , 80);
insert into Score values('01' , '02' , 90);
insert into Score values('01' , '03' , 99);
insert into Score values('02' , '01' , 70);
insert into Score values('02' , '02' , 60);
insert into Score values('02' , '03' , 80);
insert into Score values('03' , '01' , 80);
insert into Score values('03' , '02' , 80);
insert into Score values('03' , '03' , 80);
insert into Score values('04' , '01' , 50);
insert into Score values('04' , '02' , 30);
insert into Score values('04' , '03' , 20);
insert into Score values('05' , '01' , 76);
insert into Score values('05' , '02' , 87);
insert into Score values('06' , '01' , 31);
insert into Score values('06' , '03' , 34);
insert into Score values('07' , '02' , 89);
insert into Score values('07' , '03' , 98);
select a.s_id
from Score a join Score b on a.s_id = b.s_id and a.s_score >b.s_score
where a.c_id = '01' and b.c_id= '02';
select a.s_id
from (select * from Score where c_id = '01') as a
join (select * from Score where c_id='02') as b
on a.s_id = b.s_id
where a.s_score > b.s_score;
from Score
group by s_id
having avg(s_score)>60;
(select avg(score) score , sid from sc GROUP BY sid)t1
left join student t2 on t1.sid = t2.sid
where t1.score >'60'
student.s_id AS '学号',
student.s_name AS '姓名',
count(score.c_id) AS '选课数',
sum(score.s_score) AS '总成绩
from
student join score
on student.s_id = score.s_id
group by student.s_id, student.s_name
left join sc t2 on t1.sid = t2.sid
GROUP BY t1.sid,t1.sname
from Teacher
where t_name like '张%';
from Teacher
where t_name like '张%';
from Student
where s_id not in
(
select s_id from Score join Course on Score.c_id = Course.c_id
join Teacher on Course.t_id = Teacher.t_id
where t_name = '张三'
);
select s_id, s_name
from Student
where s_id in
(select s_id from Score join Course on Score.c_id = Course.c_id
join Teacher on Course.t_id = Teacher.t_id
where t_name = '张三');
select Student.s_id, s_name
from
Student
JOIN Score on Student.s_id = Score.s_id
JOIN Course on Score.c_id = Course.c_id
JOIN Teacher on Teacher.t_id = Course.t_id
where t_name='张三';
(select 1 from
(select sid from course t1
left join sc t2 on t1.cid = t2.cid
where t1.tid = (select tid from teacher where tname = '张三'))t
where t.sid = student.sid)
并且也学过编号为“02”的课程的学生的学号、姓名
select s_id, s_name
from Student
where s_id in
(select a.s_id from
(select s_id from Score where c_id = '01') as a
join (select s_id from Score where c_id ='02') as b
on a.s_id= b.s_id);
select s_id,s_name
from Student
where s_id in
(select s_id from Score where c_id = '01')
AND s_id in
(select s_id from Score where c_id = '02')
select a.s_id,a.s_name
from Student a JOIN Score b ON a.s_id=b.s_id
JOIN Score c ON a.s_id=c.s_id
where b.c_id='01' and c.c_id='02'
and b.s_id=c.s_id ;
from Score
where c_id = '02';
from Student
where s_id NOT IN (SELECT s_id FROM Score where s_score >=60)
select Student.s_id, Student.s_name
from Student join Score on Score.s_id = Student.s_id
group by s_id, s_name
having count(Score.c_id) < (select count(c_id) from Course);
select s_id, s_name
from Student
where s_id IN (SELECT s_id FROM Score group by s_id
having count(c_id)<(select count(c_id) from Course))
所学课程相同的学生的学号和姓名
from Student a join Score b on a.s_id= b.s_id
where c_id in
(select c_id from Score where s_id = '01')
and a.s_id<>'01';
from Score
where c_id in
(select c_id from Score where s_id='01')
and s_id <> '01'
group by s_id
having count(c_id)=(select count(c_id) from Score where s_id='01');
(select avg(s_score) as t, Score.c_id from Score
join Course on Score.c_id= Course.c_id
join Teacher on Teacher.t_id= Course.t_id
where t_name ='张三' group by c_id) as b#张三老师教的课与平均分
on a.c_id= b.c_id
set a.s_score= b.t;
from Student a join Score b on a.s_id=b.s_id
where c_id in (select c_id from Score where s_id='02')
and a.s_id <> '02'
group by a.s_id
having count(c_id)=(select count(c_id) from Score where s_id='02');
where c_id in
(select c_id from Course join Teacher on Course.t_id=Teacher.t_id
where t_name ='张三');
“数据库”(c_id='04')、“企业管理”(c_id='01')、
“英语”(c_id='06')三门的课程成绩,
按如下形式显示:
学生ID,数据库,企业管理,英语,有效课程数,有效平均分
(case when c_id='04' then s_score else NULL end) as '数据库',
(case when c_id='01' then s_score else NULL end) as '企业管理',
(case when c_id='06' then s_score else NULL end) as '英语',
count(c_id) as 有效课程数,
avg(s_score) as 有效平均分
from Score
group by s_id
order by avg(s_score) DESC;
max(s_score) as 最高分,
min(s_score) as 最低分
from Score
group by c_id;
concat(
(select count(b.s_score)
from Score b where b.s_score>=60
and a.c_id=b.c_id)/(select count(b.s_score)
from Score b where a.c_id=b.c_id)*100,'%'
) as '及格百分数'
from Score a join Course c
on a.c_id=c.c_id
group by a.c_id,c_name
order by 平均成绩, 及格百分数 DESC;
sum(case when c_id = 01 then 1 else 0 end), # 计算课程 01 的平均分
100*
sum(case when c_id = 01 and s_score >= 60 then 1 else 0 end) /
sum(case when c_id = 01 then 1 else 0 end) # 计算课程 01 的及格率
from score;
from Teacher
join Course on Teacher.t_id=Course.t_id
join Score on Course.c_id=Score.c_id
group by Teacher.t_id,Teacher.t_name,Course.c_name
order by avg(Score.s_score) DESC;
学生ID学生姓名企业管理马克思UML数据库平均成绩
sum(case when s_score between 85 and 100 then 1 else 0 end) as '[100-85]',
sum(case when s_score >=70 and s_score<85 then 1 else 0 end) as '[85-70]',
sum(case when s_score>=60 and s_score<70 then 1 else 0 end) as '[70-60]',
sum(case when s_score<60 then 1 else 0 end) as '[<60]'
from Score a join Course b on a.c_id=b.c_id
group by a.c_id,c_name;
SELECT s_id ,avgscore,
(SELECT COUNT(*) FROM
(SELECT s_id,AVG(s_score)AS avgscore FROM Score GROUP BY s_id)AS b
WHERE b.avgscore>a.avgscore)+1 as RANK
FROM (select s_id,avg(S_score) as avgscore from Score group by s_id)AS a
order by avgscore desc;
WHERE (SELECT COUNT(*) FROM Score b WHERE a.c_id=b.c_id AND a.s_score
from Score
group by c_id;
select Student.s_id,s_name, count(c_id)
from Student join Score on Student.s_id= Score.s_id
group by s_id,s_name
having count(c_id)=2;
select s_id,s_name
from Student
WHERE s_id IN (SELECT s_id FROM Score GROUP BY s_id having count(c_id)=2)
from Student
where s_name like '%风%';
select a.s_id, a.s_name,a.s_birth,a.s_sex
from Student AS a JOIN Student AS b
ON a.s_name=b.s_name
WHERE a.s_id<>b.s_id
select s_name,count(*)
from Student
group by s_name
having count(*)>1
select s_name
from Student
where s_birth like '1990%';
select s_name
from Student
where year(s_birth)=1990;
from Student a join Score b on a.s_id=b.s_id
group by a.s_id,s_name
having avg(b.s_score) >85;
from Score
group by c_id
order by avg(s_score),c_id DESC;
from Student a join Score b on a.s_id=b.s_id
join Course c on c.c_id=b.c_id
where c_name='数学'
and s_score <60;
c_name as 课程名称
from Student a join Score b on a.s_id=b.s_id
join Course c on b.c_id=c.c_id
from Student a join Score b on at.s_id=b.s_id
join Course c on b.c_id=c.c_id
where s_score >70
from Score
where s_score<60
order by c_id;
select a.s_id as 学号 ,s_name as 姓名
from Student a join Score b on a.s_id=b.s_id
where c_id='03'
and s_score>80;
select s_id as 学号 ,s_name as 姓名
from Student
where s_id IN(SELECT s_id FROM Score WHERE c_id='03'
and s_score>80);
from Student a join Score b on a.s_id=b.s_id
join Course c on c.c_id=b.c_id
join Teacher d on d.t_id=c.t_id
where t_name='张三'
order by s_score DESC
limit 1;
from Score a join Course b on a.c_id=b.c_id
group by a.c_id;
from Score a join Score b
on a.s_id=b.s_id and a.c_id<> b.c_id
where a.s_score=b.s_score;
Course a join Score b on a.c_id=b.c_id
join Student c on c.s_id=b.s_id
WHERE (SELECT COUNT(*) FROM Score d WHERE a.c_id=d.c_id AND b.s_score
要求输出课程号和选修人数,
查询结果按人数降序排序,
若人数相同,按课程号升序排序
from Score
group by c_id
having count(s_id)>5
order by count(s_id) DESC,c_id;
from Score
group by s_id
having count(c_id)>=2;
select a.s_id, s_name,s_birth, s_sex
from Student a join Score b on a.s_id=b.s_id
group by a.s_id
having count(Score.c_id)=(select count(distinct c_id) from Score);
select * from Student
where s_id in(select s_id from Score GROUP BY s_id
having count(c_id)=(select count(distinct c_id) from Score))
where s_id not in
(select s_id from Score join Course on Score.c_id=Course.c_id
join Teacher on Teacher.t_id=Course.t_id
where t_name = '张三');
from Score
where s_score <60
group by s_id
having count(c_id)>=2;
from Score
where c_id='04'
and s_score <60
order by s_score DESC;
where s_id='02'
and c_id='01';
where加条件筛选,最后再利用sid和学生表join拿出所有信息
(select t1.sid,s1,s2 from
(select Sid,score s1 from sc where Cid = '01')t1
left join
(select Sid,score s2 from sc where Cid = '02')t2
on t1.sid = t2.sid
where t1.s1 > t2.s2 )t3
left join student st on st.sid = t3.sid
然后令他们等于同一个人就行了,满足就select出来
(select * from sc where cid = '01')t1
join (select * from sc where cid = '02')t2
on t1.sid = t2.sid
(select * from sc where cid = '01')t1
left join (select * from sc where cid = '02')t2
on t1.sid = t2.sid
where t1.cid = '02'
and
not exists (select 1 from sc where cid = '01' and sid = t1.sid)
再以秒相减
对于不同的bugStatus,使用不同的具体策略修改
程序启动时加载配置
lombok
但要给对象赋值时,使用Builder模式,省去了setter()方法
private static SingletonDemo instance;
private SingletonDemo(){
}
public static SingletonDemo getInstance(){
if(instance==null){
instance=new SingletonDemo();
}
return instance;
}
}
private static SingletonDemo instance;
private SingletonDemo(){
}
public static synchronized SingletonDemo getInstance(){
if(instance==null){
instance=new SingletonDemo();
}
return instance;
}
}
private static SingletonDemo instance=new SingletonDemo();
private SingletonDemo(){
}
public static SingletonDemo getInstance(){
return instance;
}
}
private static class SingletonHolder{
private static SingletonDemo instance=new SingletonDemo();
}
private SingletonDemo(){
System.out.println("Singleton has loaded");
}
public static SingletonDemo getInstance(){
return SingletonHolder.instance;
}
}
INSTANCE;
public void otherMethods(){
System.out.println("Something");
}
}
public static void main(String[] args){
SingletonDemo.INSTANCE.otherMethods();
}
}
private static SingletonDemo instance;
private SingletonDemo(){
System.out.println("Singleton has loaded");
}
public static SingletonDemo getInstance(){
if(instance==null){
synchronized (SingletonDemo.class){
if(instance==null){
instance=new SingletonDemo();
}
}
}
return instance;
}
}
private volatile static Singleton singleton = null;
private Singleton() { }
public static Singleton getInstance() {
if (singleton== null) {
synchronized (Singleton.class) {
if (singleton== null) {
singleton= new Singleton();
}
}
}
return singleton;
}
}
private static final ThreadLocal
new ThreadLocal
@Override
protected Singleton initialValue() {
return new Singleton();
}
};
/**
* Get the focus finder for this thread.
*/
public static Singleton getInstance() {
return tlSingleton.get();
}
// enforce thread local access
private Singleton() {}
}
* 更加优美的Singleton, 线程安全的
*/
public class Singleton {
/** 利用AtomicReference */
private static final AtomicReference
/**
* 私有化
*/
private Singleton(){
}
/**
* 用CAS确保线程安全
*/
public static final Singleton getInstance(){
for (;;) {
Singleton current = INSTANCE.get();
if (current != null) {
return current;
}
current = new Singleton();
if (INSTANCE.compareAndSet(null, current)) {
return current;
}
}
}
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2);
}
}
https://www.jianshu.com/p/d243e1aa13ce
比较相邻的元素。如果第一个比第二个大,就交换他们两个。
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
针对所有的元素重复以上的步骤,除了最后一个。
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
for (int i = 0; i < data.length - 1; i++) {// 控制趟数
for (int j = 0; j < data.length - 1 -i; j++) {
if (data[j] > data[j + 1]) {
int tmp = data[j];
data[j] = data[j + 1];
data[j + 1] = tmp;
}
}
}
}
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
重复第二步,直到所有元素均排序完毕。
public int[] sort(int[] sourceArray) throws Exception {
int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
// 总共要经过 N-1 轮比较
for (int i = 0; i < arr.length - 1; i++) {
int min = i;
// 每轮需要比较的次数 N-i
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[min]) {
// 记录目前能找到的最小值元素的下标
min = j;
}
}
// 将找到的最小值和i位置所在的值进行交换
if (i != min) {
int tmp = arr[i];
arr[i] = arr[min];
arr[min] = tmp;
}
}
return arr;
}
}
将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。
(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
public int[] sort(int[] sourceArray) throws Exception {
// 对 arr 进行拷贝,不改变参数内容
int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
// 从下标为1的元素开始选择合适的位置插入,因为下标为0的只有一个元素,默认是有序的
for (int i = 1; i < arr.length; i++) {
// 记录要插入的数据
int tmp = arr[i];
// 从已经排序的序列最右边的开始比较,找到比其小的数
int j = i;
while (j > 0 && tmp < arr[j - 1]) {
arr[j] = arr[j - 1];
j--;
}
// 存在比其小的数,插入
if (j != i) {
arr[j] = tmp;
}
}
return arr;
}
}
选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
按增量序列个数 k,对序列进行 k 趟排序;
每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。
仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
public int[] sort(int[] sourceArray) throws Exception {
// 对 arr 进行拷贝,不改变参数内容
int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
int gap = 1;
while (gap < arr.length) {
gap = gap * 3 + 1;
}
while (gap > 0) {
for (int i = gap; i < arr.length; i++) {
int tmp = arr[i];
int j = i - gap;
while (j >= 0 && arr[j] > tmp) {
arr[j + gap] = arr[j];
j -= gap;
}
arr[j + gap] = tmp;
}
gap = (int) Math.floor(gap / 3);
}
return arr;
}
}
申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
设定两个指针,最初位置分别为两个已经排序序列的起始位置;
比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
重复步骤 3 直到某一指针达到序列尾;
将另一序列剩下的所有元素直接复制到合并序列尾。
public int[] sort(int[] sourceArray) throws Exception {
// 对 arr 进行拷贝,不改变参数内容
int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
if (arr.length < 2) {
return arr;
}
int middle = (int) Math.floor(arr.length / 2);
int[] left = Arrays.copyOfRange(arr, 0, middle);
int[] right = Arrays.copyOfRange(arr, middle, arr.length);
return merge(sort(left), sort(right));
}
protected int[] merge(int[] left, int[] right) {
int[] result = new int[left.length + right.length];
int i = 0;
while (left.length > 0 && right.length > 0) {
if (left[0] <= right[0]) {
result[i++] = left[0];
left = Arrays.copyOfRange(left, 1, left.length);
} else {
result[i++] = right[0];
right = Arrays.copyOfRange(right, 1, right.length);
}
}
while (left.length > 0) {
result[i++] = left[0];
left = Arrays.copyOfRange(left, 1, left.length);
}
while (right.length > 0) {
result[i++] = right[0];
right = Arrays.copyOfRange(right, 1, right.length);
}
return result;
}
}
从数列中挑出一个元素,称为 “基准”(pivot);
重新排序数列,所有元素比基准值小的摆放在基准前面,
所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。
在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
public int[] sort(int[] sourceArray) throws Exception {
// 对 arr 进行拷贝,不改变参数内容
int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
return quickSort(arr, 0, arr.length - 1);
}
private int[] quickSort(int[] arr, int left, int right) {
if (left < right) {
int partitionIndex = partition(arr, left, right);
quickSort(arr, left, partitionIndex - 1);
quickSort(arr, partitionIndex + 1, right);
}
return arr;
}
private int partition(int[] arr, int left, int right) {
// 设定基准值(pivot)
int pivot = left;
int index = pivot + 1;
for (int i = index; i <= right; i++) {
if (arr[i] < arr[pivot]) {
swap(arr, i, index);
index++;
}
}
swap(arr, pivot, index - 1);
return index - 1;
}
private void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
创建一个堆 H[0……n-1];
把堆首(最大值)和堆尾互换;
把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
重复步骤 2,直到堆的尺寸为 1。
public int[] sort(int[] sourceArray) throws Exception {
// 对 arr 进行拷贝,不改变参数内容
int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
int len = arr.length;
buildMaxHeap(arr, len);
for (int i = len - 1; i > 0; i--) {
swap(arr, 0, i);
len--;
heapify(arr, 0, len);
}
return arr;
}
private void buildMaxHeap(int[] arr, int len) {
for (int i = (int) Math.floor(len / 2); i >= 0; i--) {
heapify(arr, i, len);
}
}
private void heapify(int[] arr, int i, int len) {
int left = 2 * i + 1;
int right = 2 * i + 2;
int largest = i;
if (left < len && arr[left] > arr[largest]) {
largest = left;
}
if (right < len && arr[right] > arr[largest]) {
largest = right;
}
if (largest != i) {
swap(arr, i, largest);
heapify(arr, largest, len);
}
}
private void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
花O(n)的时间扫描一下整个序列 A,获取最小值 min 和最大值 max
开辟一块新的空间创建新的数组 B,长度为 ( max - min + 1)
数组 B 中 index 的元素记录的值是 A 中某元素出现的次数
最后输出目标整数序列,具体的逻辑是遍历数组 B,输出相应元素以及对应的个数
public int[] sort(int[] sourceArray) throws Exception {
// 对 arr 进行拷贝,不改变参数内容
int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
int maxValue = getMaxValue(arr);
return countingSort(arr, maxValue);
}
private int[] countingSort(int[] arr, int maxValue) {
int bucketLen = maxValue + 1;
int[] bucket = new int[bucketLen];
for (int value : arr) {
bucket[value]++;
}
int sortedIndex = 0;
for (int j = 0; j < bucketLen; j++) {
while (bucket[j] > 0) {
arr[sortedIndex++] = j;
bucket[j]--;
}
}
return arr;
}
private int getMaxValue(int[] arr) {
int maxValue = arr[0];
for (int value : arr) {
if (maxValue < value) {
maxValue = value;
}
}
return maxValue;
}
}
设置固定数量的空桶。
把数据放到对应的桶中。
对每个不为空的桶中数据进行排序。
拼接不为空的桶中数据,得到结果
private static final InsertSort insertSort = new InsertSort();
public int[] sort(int[] sourceArray) throws Exception {
// 对 arr 进行拷贝,不改变参数内容
int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
return bucketSort(arr, 5);
}
private int[] bucketSort(int[] arr, int bucketSize) throws Exception {
if (arr.length == 0) {
return arr;
}
int minValue = arr[0];
int maxValue = arr[0];
for (int value : arr) {
if (value < minValue) {
minValue = value;
} else if (value > maxValue) {
maxValue = value;
}
}
int bucketCount = (int) Math.floor((maxValue - minValue) / bucketSize) + 1;
int[][] buckets = new int[bucketCount][0];
// 利用映射函数将数据分配到各个桶中
for (int i = 0; i < arr.length; i++) {
int index = (int) Math.floor((arr[i] - minValue) / bucketSize);
buckets[index] = arrAppend(buckets[index], arr[i]);
}
int arrIndex = 0;
for (int[] bucket : buckets) {
if (bucket.length <= 0) {
continue;
}
// 对每个桶进行排序,这里使用了插入排序
bucket = insertSort.sort(bucket);
for (int value : bucket) {
arr[arrIndex++] = value;
}
}
return arr;
}
/**
* 自动扩容,并保存数据
*
* @param arr
* @param value
*/
private int[] arrAppend(int[] arr, int value) {
arr = Arrays.copyOf(arr, arr.length + 1);
arr[arr.length - 1] = value;
return arr;
}
}
将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零
从最低位开始,依次进行一次排序
从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列
public int[] sort(int[] sourceArray) throws Exception {
// 对 arr 进行拷贝,不改变参数内容
int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
int maxDigit = getMaxDigit(arr);
return radixSort(arr, maxDigit);
}
/**
* 获取最高位数
*/
private int getMaxDigit(int[] arr) {
int maxValue = getMaxValue(arr);
return getNumLenght(maxValue);
}
private int getMaxValue(int[] arr) {
int maxValue = arr[0];
for (int value : arr) {
if (maxValue < value) {
maxValue = value;
}
}
return maxValue;
}
protected int getNumLenght(long num) {
if (num == 0) {
return 1;
}
int lenght = 0;
for (long temp = num; temp != 0; temp /= 10) {
lenght++;
}
return lenght;
}
private int[] radixSort(int[] arr, int maxDigit) {
int mod = 10;
int dev = 1;
for (int i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
// 考虑负数的情况,这里扩展一倍队列数,其中 [0-9]对应负数,[10-19]对应正数 (bucket + 10)
int[][] counter = new int[mod * 2][0];
for (int j = 0; j < arr.length; j++) {
int bucket = ((arr[j] % mod) / dev) + mod;
counter[bucket] = arrayAppend(counter[bucket], arr[j]);
}
int pos = 0;
for (int[] bucket : counter) {
for (int value : bucket) {
arr[pos++] = value;
}
}
}
return arr;
}
private int[] arrayAppend(int[] arr, int value) {
arr = Arrays.copyOf(arr, arr.length + 1);
arr[arr.length - 1] = value;
return arr;
}
}
private int data;
private BinaryTreeNode left;
private BinaryTreeNode right;
}
if(null!=root){
System.out.print(root.getData()+"\t");
preOrder(root.getLeft());
preOrder(root.getRight());
}
}
Stack
while(true){
while(root!=null){
System.out.print(root.getData()+"\t");
stack.push(root);
root=root.getLeft();
}
if(stack.isEmpty()) break;
root=stack.pop();
root=root.getRight();
}
}
if(null!=root){
inOrder(root.getLeft());
System.out.print(root.getData()+"\t");
inOrder(root.getRight());
}
}
Stack
while(true){
while(root!=null){
stack.push(root);
root=root.getLeft();
}
if(stack.isEmpty())break;
root=stack.pop();
System.out.print(root.getData()+"\t");
root=root.getRight();
}
}
if(root!=null){
postOrder(root.getLeft());
postOrder(root.getRight());
System.out.print(root.getData()+"\t");
}
}
Stack
while(true){
if(root!=null){
stack.push(root);
root=root.getLeft();
}else{
if(stack.isEmpty()) return;
if(null==stack.lastElement().getRight()){
root=stack.pop();
System.out.print(root.getData()+"\t");
while(root==stack.lastElement().getRight()){
System.out.print(stack.lastElement().getData()+"\t");
root=stack.pop();
if(stack.isEmpty()){
break;
}
}
}
if(!stack.isEmpty())
root=stack.lastElement().getRight();
else
root=null;
}
}
}
BinaryTreeNode temp;
Queue
queue.offer(root);
while(!queue.isEmpty()){
temp=queue.poll();
System.out.print(temp.getData()+"\t");
if(null!=temp.getLeft())
queue.offer(temp.getLeft());
if(null!=temp.getRight()){
queue.offer(temp.getRight());
}
}
}
List<String> list = new ArrayList<>();
list.add("F1");
list.add("F2");
list.add("F3");
for(int i = 0; i < 2; i++) {
list.remove(i);
}
for(int i = 0; i < 2; i ++) {
if(list.get(i).equals("F2")) {
list.remove(i);
}
}
List<String> list = new ArrayList<>();
list.add("F1");
list.add("F2");
list.add("F3");
for(String tmp : list) {
list.remove(tmp);
}
a int ,
b int,
c int,
d int,
key index_abc(a,b,c)
)engine=InnoDB default charset=utf8;
DELIMITER $
CREATE PROCEDURE proc_initData()
BEGIN
DECLARE i INT DEFAULT 1;
WHILE i<=10000 DO
INSERT INTO test(a,b,c,d) VALUES(i,i,i,i);
SET i = i+1;
END WHILE;
END $
CALL proc_initData();
最后才生成真正的执行计划。
所以,当然是我们能尽量的利用到索引时的查询顺序效率最高咯,
所以mysql查询优化器会最终以这种顺序进行查询执行。
b+数是按照从左到右的顺序来建立搜索树的,
比如当(张三,20,F)这样的数据来检索的时候,
b+树会优先比较name来确定下一步的所搜方向,
如果name相同再依次比较age和sex,最后得到检索的数据;
但当(20,F)这样的没有name的数据来的时候,
b+树就不知道下一步该查哪个节点,
因为建立搜索树的时候name就是第一个比较因子,
必须要先根据name来搜索才能知道下一步去哪里查询。
比如当(张三,F)这样的数据来检索时,b+树可以用name来指定搜索方向,
但下一个字段age的缺失,
所以只能把名字等于张三的数据都找到,
然后再匹配性别是F的数据了,
这个是非常重要的性质,
即索引的最左匹配特性。
`userid` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(20) NOT NULL DEFAULT '',
`password` varchar(20) NOT NULL DEFAULT '',
`usertype` varchar(20) NOT NULL DEFAULT '',
PRIMARY KEY (`userid`),
KEY `a_b_c_index` (`username`,`password`,`usertype`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
下面开始验证最左匹配原则
mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,
比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,
如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整
mysql的查询优化器会帮你优化成索引可以识别的形式
2. 都是可重入锁,同一线程可以多次获得同一个锁
3. 都保证了可见性和互斥
2. ReentrantLock 可响应中断、可轮回,synchronized 是不可以响应中断的,为处理锁的 不可用性提供了更高的灵活性
3. ReentrantLock是API级别的,synchronized是JVM级别的
4. ReentrantLock可以实现公平锁
5. ReentrantLock通过 Condition可以绑定多个条件
6. 底层实现不一样, synchronized是同步阻塞,使用的是悲观并发策略,lock是同步非阻 塞,采用的是乐观并发策略
7. Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言 实现。
8. synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生; 而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象, 因此使用Lock时需要在finally块中释放锁。
9. Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时, 等待的线程会一直等待下去,不能够响应中断。
10. 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
11. Lock可以提高多个线程进行读操作的效率,既就是实现读写锁等。
CAS 算法的过程是这样:它包含 3 个参数 CAS(V,E,N)。
V 表示要更新的变量(内存值),E 表示预期值(旧的),N 表示新值。
当且仅当 V 值等于 E 值时,才会将 V 的值设为 N,
如果 V 值和 E 值不同,则说明已经有其他线程做了更新,
则当 前线程什么都不做。最后,CAS返回当前V的真实值。
CAS 操作是抱着乐观的态度进行的(乐观锁),
它总是认为自己可以成功完成操作。
当多个线程同时 使用 CAS 操作一个变量时,
只有一个会胜出,并成功更新,其余均会失败。
失败的线程不会被挂 起,仅是被告知失败,
并且允许再次尝试,当然也允许失败的线程放弃操作。
基于这样的原理, CAS操作即使没有锁,
也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
JDK1.5 的原子包:java.util.concurrent.atomic 这个包里面提供了一组原子类。
其基本的特性就 是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,
具有排他性,即当某个 线程进入方法,执行其中的指令时,
不会被其他线程打断,而别的线程就像自旋锁一样,
一直等 到该方法执行完成,才由 JVM 从等待队列中选择一个另一个线程进入,
这只是一种逻辑上的理解。
相对于对于 synchronized 这种阻塞算法,
CAS 是非阻塞算法的一种常见实现。
由于一般 CPU 切 换时间比CPU指令集操作更加长,
所以J.U.C在性能上有了很大的提升。
CAS 会导致“ABA 问题”。
CAS 算法实现一个重要前提需要取出内存中某时刻的数据,
而在下时 刻比较并替换,那么在这个时间差类会导致数据的变化。
比如说一个线程 one 从内存位置 V 中取出 A,
这时候另一个线程 two 也从内存中取出 A,
并且 two进行了一些操作变成了B,
然后two又将V位置的数据变成A,
这时候线程one进行CAS操 作发现内存中仍然是A,然后one操作成功。
尽管线程one的CAS操作成功,
但是不代表这个过 程就是没有问题的。
部分乐观锁的实现是通过版本号(version)的方式来解决ABA问题,
乐观锁每次在执行数据的修 改操作时,
都会带上一个版本号,
一旦版本号和数据的版本号一致就可以执行修改操作并对版本 号执行+1 操作,
否则就执行失败。
因为每次操作的版本号都会随之增加,
所以不会出现 ABA 问 题,
因为版本号只会增加不会减少。
CAS 算法的过程是这样:它包含 3 个参数 CAS(V,E,N)。
V 表示要更新的变量(内存值),E 表示预期值(旧的),N 表示新值。
当且仅当 V 值等于 E 值时,才会将 V 的值设为 N,
如果 V 值和 E 值不同,则说明已经有其他线程做了更新,
则当 前线程什么都不做。最后,CAS返回当前V的真实值。
CAS 操作是抱着乐观的态度进行的(乐观锁),
它总是认为自己可以成功完成操作。
当多个线程同时 使用 CAS 操作一个变量时,
只有一个会胜出,并成功更新,其余均会失败。
失败的线程不会被挂 起,仅是被告知失败,
并且允许再次尝试,当然也允许失败的线程放弃操作。
基于这样的原理, CAS操作即使没有锁,
也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
AQS定义了一套多线程访问 共享资源的同步器框架,
许多同步类实现都依赖于它,
如常用的 ReentrantLock/Semaphore/CountDownLatch。
独占资源 -ReentrantLock Exclusive(独占,只有一个线程能执行,如ReentrantLock)
共享资源 -Semaphore/CountDownLatch Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。
AQS这里只定义了一个 接口,
具体资源的获取交由自定义同步器去实现了(通过state的get/set/CAS)之所以没有定义成 abstract,
是因为独占模式下只用实现 tryAcquire-tryRelease,
而共享模式下只用实现 tryAcquireShared-tryReleaseShared。
如果都定义成abstract,
那么每个模式也要去实现另一模 式下的接口。
不同的自定义同步器争用共享资源的方式也不同。
自定义同步器在实现时只需要实 现共享资源 state 的获取与释放方式即可,
至于具体线程等待队列的维护(如获取资源失败入队/ 唤醒出队等),
AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
1. isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
2. tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
3. tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
4. tryAcquireShared(int):共享方式。尝试获取资源。
负数表示失败;
0 表示成功,但没有剩余 可用资源;
正数表示成功,且有剩余资源。
5. tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回 true,否则返回false。
state初始化为0,表示未锁定状态。
A线程 lock()时,会调用 tryAcquire()独占该锁并将 state+1。
此后,其他线程再 tryAcquire()时就会失 败,
直到A线程unlock()到state=0(即释放锁)为止,
其它线程才有机会获取该锁。
当然,释放 锁之前,A 线程自己是可以重复获取此锁的(state 会累加),
这就是可重入的概念。
但要注意, 获取多少次就要释放多么次,
这样才能保证state是能回到零态的。
任务分为N个子线程去执行,
state也初始化为N(注意N要与 线程个数一致)。
这 N 个子线程是并行执行的,
每个子线程执行完后 countDown()一次,state 会CAS减1。
等到所有子线程都执行完后(即state=0),
会unpark()主调用线程,
然后主调用线程 就会从await()函数返回,
继续后余动作。
他们也只需实现 tryAcquiretryRelease、
tryAcquireShared-tryReleaseShared 中的一种即可。
但 AQS 也支持自定义同步器 同时实现独占和共享两种方式,
如ReentrantReadWriteLock。
属性注入当某个属性对应的bean第一次被注入时才实例化。
是类型安全的,会对参数做限制;
无须装箱和拆箱,类在实例化时按照传入的数据类型生成本地代码;
无须类型转换。
/* W and H are already swapped */
int w = matrix.length;
int h = matrix[0].length;
int[][] ret = new int[h][w];
for (int i = 0; i < h; ++i) {
for (int j = 0; j < w; ++j) {
ret[i][j] = matrix[w - j - 1][i];
}
}
return ret;
}
/* W and H are already swapped */
int w = matrix.length;
int h = matrix[0].length;
int[][] ret = new int[h][w];
for (int i = 0; i < h; ++i) {
for (int j = 0; j < w; ++j) {
ret[i][j] = matrix[j][h - i - 1];
}
}
return ret;
}
https://www.cnblogs.com/afeng7882999/p/4318397.html
<error-code>404</error-code>
<location>/404</location>
</error-page>
<error-page>
<error-code>500</error-code>
<location>/500</location>
</error-page>
<!-- 未捕获的错误,同样可指定其它异常类,或自定义异常类 -->
<error-page>
<exception-type>java.lang.Exception</exception-type>
<location>/uncaughtException</location>
</error-page>
<mvc:view-controller path="/404" view-name="404"/>
<mvc:view-controller path="/500" view-name="500"/>
<mvc:view-controller path="/uncaughtException" view-name="uncaughtException"/>
public class MainController {
@ResponseBody
@RequestMapping("/")
public String main(){
throw new NullPointerException("NullPointerException Test!");
}
}
//可应用到所有@RequestMapping类或方法上的@ExceptionHandler、@InitBinder、@ModelAttribute,在这里是@ExceptionHandler
@ControllerAdvice
public class AControllerAdvice {
@ExceptionHandler(NullPointerException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public String handleIOException(NullPointerException ex) {
return ClassUtils.getShortName(ex.getClass()) + ex.getMessage();
}
}
可以这样编写全局异常处理类:
class GlobalDefaultExceptionHandler {
public static final String DEFAULT_ERROR_VIEW = "error";
@ExceptionHandler(value = Exception.class)
public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {
// If the exception is annotated with @ResponseStatus rethrow it and let
// the framework handle it - like the OrderNotFoundException example
// at the start of this post.
// AnnotationUtils is a Spring Framework utility class.
if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null)
throw e;
// Otherwise setup and send the user to a default error-view.
ModelAndView mav = new ModelAndView();
mav.addObject("exception", e);
mav.addObject("url", req.getRequestURL());
mav.setViewName(DEFAULT_ERROR_VIEW);
return mav;
}
}
public class GlobalExceptionHandler{
/**
* 基础异常
*/
@ExceptionHandler(BaseException.class)
public AjaxResult baseException(BaseException e){
return AjaxResult.error(e.getMessage());
}
}
<bean id="exceptionResolver" class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<prop key="java.lang.Exception">errors/500</prop>
<prop key="java.lang.Throwable">errors/500</prop>
</props>
</property>
<property name="statusCodes">
<props>
<prop key="errors/500">500</prop>
</props>
</property>
<!-- 设置日志输出级别,不定义则默认不输出警告等错误日志信息 -->
<property name="warnLogCategory" value="WARN"></property>
<!-- 默认错误页面,当找不到上面mappings中指定的异常对应视图时,使用本默认配置 -->
<property name="defaultErrorView" value="errors/500"></property>
<!-- 默认HTTP状态码 -->
<property name="defaultStatusCode" value="500"></property>
</bean>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>500 Error</title>
</head>
<body>
<% Exception ex = (Exception)request.getAttribute("exception"); %>
<H2>Exception: <%= ex.getMessage()%></H2>
<P/>
<% ex.printStackTrace(new java.io.PrintWriter(out)); %>
</body>
</html>
public CustomException(){
super();
}
public CustomException(String msg, Throwable cause){
super(msg, cause);
//Do something...
}
}
@RequestMapping("/ce")
public String ce(CustomException e){
throw new CustomException("msg",e);
}
@Override
public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) {
Map<String, Object> model = new HashMap<String, Object>();
model.put("e", e);
//这里可根据不同异常引起类做不同处理方式,本例做不同返回页面。
String viewName = ClassUtils.getShortName(e.getClass());
return new ModelAndView(viewName, model);
}
}
可以配置优先级。
DispatcherServlet初始化HandlerExceptionResolver的时候
会自动寻找容器中实现了HandlerExceptionResolver接口的类,
然后添加进来。配置Spring支持异常捕获
首先定义一个错误
public final String url;
public final String ex;
public ErrorInfo(String url, Exception ex) {
this.url = url;
this.ex = ex.getLocalizedMessage();
}
}
@ExceptionHandler(MyBadDataException.class)
@ResponseBody
ErrorInfo handleBadRequest(HttpServletRequest req, Exception ex) {
return new ErrorInfo(req.getRequestURL(), ex);
}
符合1NF的关系中的每个属性都不可再分。
2NF在1NF的基础之上,确保表中的每列都和主键相关
一是表必须有一个主键;
二是没有包含在主键中的列必须完全依赖于主键,而不能只依赖于主键的一部分。
而第三范式是分表之后,每张表中都只能含有另一张表的id,
不能包含另一张表的其他属性。
位图(例如用途、用户中的管理员和vip)
逻辑齿解决稀疏矩阵 行列值
不可能停滞在中间环节。
事务执行过程中出错,会回滚到事务开始前的状态,
所有的操作就像没有发生一样。
也就是说事务是一个不可分割的整体,
就像化学中学过的原子,
是物质构成的基本单位。
数据库的完整性约束没有被破坏 。
比如A向B转账,不可能A扣了钱,B却没收到。
不同的事务之间彼此没有任何干扰。
比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。
然后基于最初选定的值更新该行时,
由于每个事务都不知道其他事务的存在,
就会发生丢失更新问题
在没有事务隔离的情况下,
两个事务都同时更新一行数据,
但是第二个事务却中途失败退出,
导致对数据的两个修改都失效了。
2. 事务B获取工资为5000,汇入100,并提交数据库,工资变为5100;
3. 事务A发生异常,回滚了,恢复张三的工资为5000;
END: 导致事务B的更新丢失。
不可重复读的特例。
有两个并发事务同时读取同一行数据,
然后其中一个对它进行修改提交,
而另一个也进行了修改提交。
这就会造成第一次写操作失效。
2. 事务B,存储1000,把张三的存款改为6000,并提交了事务。
3. 在事务A中,存储500,把张三的存款改为5500,并提交了事务。
END:事务A的更新覆盖了事务B的更新。
事务A读取了事务B更新的数据,
然后B回滚操作,
那么A读取到的数据是脏数据
2. 事务B正在读取张三的工资,读取到张三的工资为8000。
3. 事务A发生异常,而回滚了事务。张三的工资又回滚为5000。
END: 事务B读取到的张三工资为8000的数据即为脏数据,事务B做了一次脏读。
事务 A 多次读取同一数据,
事务 B 在事务A多次读取的过程中,
对数据作了更新并提交,
导致事务A多次读取同一数据时,结果不一致。
2. 事务B把张三的工资改为8000,并提交了事务。
3. 事务A中,再次读取张三的工资,此时工资变为8000。
END:在一个事务中前后两次读取的结果并不致,导致了不可重复读。
当事务不是独立执行时发生的一种现象,
例如第一个事务对一个表中的数据进行了修改,
这种修改涉及到表中的全部数据行。
同时,第二个事务也修改这个表中的数据,
这种修改是向表中插入一行新数据。
那么,以后第一个事务的用户再次读取时会发现表中还有没有修改的数据行,
就好象发生了幻觉一样。
2. 事务B插入一条工资也为5000的记录。
END:事务A再次读取工资为5000的员工,记录为11人。此时产生了幻读。
不可重复读侧重于修改,幻读侧重于新增或删除。 解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表
写数据会锁住相应行
解决第一类丢失更新的问题,但仍存在脏读、不可重复读、第二类丢失更新的问题、幻读 。
解决第一类丢失更新和脏读的问题,但会出现不可重复读、第二类丢失更新的问题、幻读问题
解决第一类丢失更新,脏读、不可重复读、第二类丢失更新的问题,但会出幻读
事务隔离级别为可重复读时,如果有索引(包括主键索引)的时候,以索引列为条件更新数据,会存在间隙锁间隙锁、行锁、下一键锁的问题,从而锁住一些行;如果没有索引,更新数据时会锁住整张表
事务被处理为顺序执行。
每处理一个事务,其值自动+1
找之前版本的数据就是通过这个指针
当由innodb自动产生聚集索引时,
聚集索引包括这个DB_ROW_ID的值,
否则聚集索引中不包括这个值,
这个用于索引当中
这里的不是真正的删除数据,
而是标志出来的删除。
真正意义的删除是在commit的时候
写事务编号,
回滚指针指向undo log中的修改前的行
因为insert时,原始的数据并不存在,
所以回滚时把insert undo log丢弃即可,
而update undo log则必须遵守上述过程
删除时间未定义,
旧数据行“创建时间”不变,删除时间=该事务的DB_ROW_ID;
删除时间=该事务的DB_ROW_ID;
这确保当前事务 读取的行都是事务之前已经存在的,
或者是由当前事务创建或修改的行
确定了当前事务开始之前,行没有被删除
它也把系统版本号作为了删除行的版本。
当事务仅修改一行记录使用理想的MVCC模式是没有问题的,
可以通过比较版本号进行回滚;
但当事务影响到多行数据时,理想的MVCC据无能为力了。
此时需要回滚Row1,但因为Row1没有被锁定,
其数据可能又被Transaction2所修改,如果此时回滚Row1的内容,
则会破坏Transaction2的修改结果,导致Transaction2违反ACID。
与修改两个分布式系统中的数据并无区别,
而二提交是目前这种场景保证一致性的唯一手段。
按道理在一个事务B(在事务A后,但A还没commit)select的话 B.
DB_TRX_ID>A.DB_TRX_ID则应该能返回A事务对数据的操作以及修改。
那不是和前面矛盾?其实不然。
会将当前系统中的活跃事务列表(trx_sys->trx_list)创建一个副本(read view),
然后一致性读去比较记录的tx id的时候,
并不是根据当前事务的tx id,
而是根据read view最早一个事务的tx id(read view->up_limit_id)来做比较的,
这样就能确保在事务B之前没有提交的所有事务的变更,B事务都是看不到的。
阻止其他事务获得相同数据集的排他锁。
都能访问到数据,但是只能读不能修改。
阻止其他事务取得相同数据集的共享读锁和排他写锁。
其他事务就不能再获取该行的其他锁,
包括共享锁和排他锁,但是获取排他锁的事务是可以对数据就行读取和修改。
保证同一行记录修改与删除的串行性;
事务打算给数据行加行共享锁,
事务在给一个数据行加共享锁前必须先取得该表的IS锁。
事务打算给数据行加行排他锁,
事务在给一个数据行加排他锁前必须先取得该表的IX锁。
并请求共享或排他锁时,
InnoDB会给符合条件的已有数据记录的索引项加锁;
对于键值在条件范围内但并不存在的记录,
叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,
这种锁机制就是所谓 的间隙锁(Next-Key锁)。
如果使用相等条件请求给一个不存在的记录加锁,
InnoDB也会使用间隙锁!
对于上面的例子,要是不使用间隙锁,
如果其他事务插入了empid大于100的任何记录,
那么本事务如果再次执行上述语句,就会发生幻读;
封锁范围既包含索引记录又包含索引区间
如果一个会话占有了索引记录R的共享/排他锁,其他会话不能立刻在R之前的区间插入新的索引记录。
如果把事务的隔离级别降级为RC,临键锁则也会失效。
专门针对事务插入AUTO_INCREMENT类型的列。
事务可以通过以下语句显示给记录集加共享锁或排他锁。
所以虽然是访问不同行的记录,
但是如果是使用相同的索引键,是会出现锁冲突的。
另外,不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。
如: select id from t where num is null 可以在num上设置默认值0,确保表中num列没有null值,然后这样查询: select id from t where num=0
唯一性索引的值是唯一的,可以更快速的通过该索引来确定某条记录。
2. 为经常需要排序、分组和联合操作的字段建立索引 :
3 .为常作为查询条件的字段建立 索引 。
4 .限制索引的数目:
越多的索引,会使更新表变得很浪费时间。
尽量使用数据量少的索引
6. 如果索引的值很长,那么查询的速度会受到影响。
尽量使用前缀来索引
7. 如果索引字段的值很长,最好使用值的前缀来索引。
7 .删除不再使用或者很少使用的索引
8 . 最左前缀匹配原则,非常重要的原则。
10 . 尽量选择区分度高的列作为索引
区分度的公式是表示字段不重复的比例
11 . 索引列不能参与计算,保持列“干净”:带函数的查询不参与索引。
12 . 尽量的扩展索引,不要新建索引。
所以它的处理速度比VARCHAR快得多,
但是其缺点是浪费存储空间,
程序需要对行尾空格进行处理,
所以对于那些长度变化不大并且对查询速度有较高要求的数据可以考虑使用CHAR类型来存储。
建议使用VARCHAR类型。
对于InnoDB数据表,
内部的行存储格式没有区分固定长度和可变长度列
(所有数据行都使用指向数据列值的头指针),
因此在本质上,使用固定长度的CHAR列不一定比使用可变长度VARCHAR列性能要好。
因而,主要的性能因素是数据行使用的存储总量。
由于CHAR平均占用的空间多于VARCHAR,
因此使用VARCHAR来最小化需要处理的数据行的存储总量和磁盘I/O是比较好的。
索引列的顺序意味着索引首先按照最左列进行排序,
其次是第二列...
以满足精确符合列顺序的ORDER BY、GROUP BY和DISTINCT等子句的查询需求。
但要注意最左原则(参照字典查询单词的例子)
函数(function)
触发器(trigger)
时间调度器(event)
show create …
SQL命令传递到解析器的时候会被解析器验证和解析。
解析器是由Lex和YACC实现的,是一个很长的脚本。
并将这个结构传递到后续步骤,
以后SQL语句的传递和处理就是基于这个结构的。
那么就说明这个sql语句是不合理的。
他使用的是“选取-投影-联接”策略进行查询。
用一个例子就可以理解: select uid,name from user where gender = 1;
而不是先将表全部查询出来以后再进行gender过滤。
这个select查询先根据uid和name进行属性投影,
而不是将属性全部取出以后再进行过滤,
将这两个查询条件联接起来生成最终查询结果。
查询语句就可以直接去查询缓存中取数据。
这个缓存机制是由一系列小缓存组成的。
比如表缓存,记录缓存,key缓存,权限缓存等。
它根据MySql AB公司提供的文件访问层的一个抽象接口来定制一种文件访问机制(这种访问机制就叫存储引擎)。
现在有很多种存储引擎,各个存储引擎的优势各不一样,最常用的InnoDB,MyISAM。
位于同一磁盘块中的数据会被一次性读取出来,而不是按需读取。
页是其磁盘管理的最小单位,默认 page 大小是 16k。
因此 InnoDB 每次申请磁盘空间时都会是若干地址连续磁盘块来达到页的大小 16KB。
一个节点上有两个升序排序的关键字和三个指向子树根节点的指针,
指针存储的是子节点所在磁盘块的地址。
力求达到树的深度不超过 3,
也就是说 I/O 不需要超过 3 次。
和3次内存查找操作。
由于内存中的关键字是一个有序表结构,可以利用二分法查找提高效率。
而每一个页的存储空间是有限的,
如果 data 数据较大时将会导致每个节点(即一个页)能存储的 key 的数量很小,
会导致 B-Tree 的深度较大,增大查询时的磁盘 I/O 次数,进而影响查询效率。。
也是InnoDB磁盘管理的最小单位,
与数据库相关的所有内容都存储在这种Page结构里。
每个Page使用一个32位的int值来唯一标识,
这也正好对应InnoDB最大64TB的存储容量(16Kib * 2^32 = 64Tib)。
分别指向前一个Page和后一个Page,
Page链接起来就是一个双向链表的结构。
它们分别是1主键索引树非叶节点 2主键索引树叶子节点 3辅助键索引树非叶节点 4辅助键索引树叶子节点。
最初数据是按照插入的先后顺序排列的,
但是随着新数据的插入和旧数据的删除,
数据物理顺序会变得混乱,
但他们依然保持着逻辑上的先后顺序。
为了减小索引体积,提高索引的扫描速度,
就用索引的前部分字串索引,
这样索引占用的空间就会大大减少,
并且索引的选择性也不会降低很多。
而且是对BLOB和TEXT列进行索引,
或者非常长的VARCHAR列,
就必须使用前缀索引,
因为MySQL不允许索引它们的全部长度。
之前只有MyISAM引擎支持FULLTEXT索引。
对于FULLTEXT索引的内容可以使用MATCH()…AGAINST语法进行查询。
可以使用MATCH()...AGAINST语法进行查询
而对于中文来讲,显然用空格就不合适,需要针对中文语义进行分词。
外键的主要作用是保证记录的一致性和完整性。
(具体参照下图中的索引数据结构)
都包含了主键值、事务ID、用于事务和MVCC(多版本控制)的回滚指针以及所有的剩余列。
并且同时将索引列与相关数据行保存在一起。
这意味着,当你访问同一数据页不同行记录时,
已经把页加载到了Buffer中,再次访问的时候,
会在内存中完成访问,不必访问磁盘。
放在不同的物理文件中,索引文件是缓存在key_buffer中,
索引对应的是磁盘位置,不得不通过磁盘位置访问磁盘数据。
导致需要移动行的时候,可能面临“页分裂(page split)”的问题。
页分裂会导致表占用更多的磁盘空间。
选在负载较低的时间段,
通过OPTIMIZE TABLE优化表,
因为必须被移动的行数据可能造成碎片。
使用独享表空间可以弱化碎片
这就会出现聚簇索引有可能有比全表扫面更新
因为辅助索引的叶子存储的是主键值;
过长的主键值,会导致非叶子节点占用更多的物理空间
为了提高扫描速度,
把索引键值单独放在独立的数据的数据块里,
并且每个键值都有个指向原数据块的指针,
因为索引比较小,扫描索引的速度就比扫描全表快,
这种需要扫描所有键值的方式就称为紧凑索引扫描
发现只需要和每个数据块的第一行键值匹配,
就可以判断下一个数据块的位置或方向,
因此有效数据就是每个数据块的第一行数据,
如果把每个数据块的第一行数据创建索引,
这样在这个新创建的索引上折半查找,
数据定位速度将更快。这种索引扫描方式就称为松散索引扫描。
即利用索引返回select列表中的字段,
而不必根据索引再次读取数据文件
包含主键值、事务ID、回滚指针(rollback pointer用于事务和MVCC)和余下的列(如col2)。
存储引擎需要先查找二级索引的叶子节点来获得对应的主键值,
然后根据这个主键值到聚簇索引中查找对应的数据行。
叶子节点中保存的是数据的物理地址。
主键索引仅仅只是一个叫做PRIMARY的唯一、非空的索引,
且MYISAM引擎中可以不设主键。
MySQL的日志有很多种,
如二进制日志、错误日志、查询日志、慢查询日志等,
此外InnoDB存储引擎还提供了两种事务日志:
redo log(重做日志)和undo log(回滚日志)。
其中redo log用于保证事务持久性;
undo log则是事务原子性和隔离性实现的基础。
undo log
当事务对数据库进行修改时,InnoDB会生成对应的undo log;
如果事务执行失败或调用了rollback,导致事务需要回滚,
便可以利用undo log中的信息将数据回滚到修改之前的样子。
持久性是指事务一旦提交,
它对数据库的改变就应该是永久性的。
Buffer Pool的使用大大提高了读写数据的效率,但是也带了新的问题:如果MySQL宕机,而此时Buffer Pool中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证。
于是,redo log被引入来解决这个问题:当数据修改时,除了修改Buffer Pool中的数据,还会在redo log记录这次操作;当事务提交时,会调用fsync接口对redo log进行刷盘。如果MySQL宕机,重启时可以读取redo log中的数据,对数据库进行恢复。redo log采用的是WAL(Write-ahead logging,预写式日志),所有修改先写入日志,再更新到Buffer Pool,保证了数据不会因MySQL宕机而丢失,从而满足了持久性要求。
既然redo log也需要在事务提交时将日志写入磁盘,为什么它比直接将Buffer Pool中修改的数据写入磁盘(即刷脏)要快呢?主要有以下两方面的原因:
1、刷脏是随机IO,因为每次修改的数据位置随机,但写redo log是追加操作,属于顺序IO。
2、刷脏是以数据页(Page)为单位的,MySQL默认页大小是16KB,一个Page上一个小修改都要整页写入;而redo log中只包含真正需要写入的部分,无效IO大大减少。
我们知道,在MySQL中还存在binlog(二进制日志)也可以记录写操作并用于数据的恢复,但二者是有着根本的不同的:
1、作用不同:redo log是用于crash recovery的,保证MySQL宕机也不会影响持久性;
binlog是用于point-in-time recovery的,保证服务器可以基于时间点恢复数据,此外binlog还用于主从复制。
2、层次不同:redo log是InnoDB存储引擎实现的,
而binlog是MySQL的服务器层(可以参考文章前面对MySQL逻辑架构的介绍)实现的,同时支持InnoDB和其他存储引擎。
3、内容不同:redo log是物理日志,内容基于磁盘的Page;
binlog是逻辑日志,内容是一条条sql。
4、写入时机不同:binlog在事务提交时写入;
redo log的写入时机相对多元:
前面曾提到:当事务提交时会调用fsync对redo log进行刷盘;
这是默认情况下的策略,修改innodb_flush_log_at_trx_commit参数可以改变该策略,但事务的持久性将无法保证。
除了事务提交时,还有其他刷盘时机:如master thread每秒刷盘一次redo log等,这样的好处是不一定要等到commit时刷盘,commit速度大大加快。
隔离性研究的是不同事务之间的相互影响。
隔离性是指,事务内部的操作与其他事务是隔离的,
并发执行的各个事务之间不能互相干扰。
事务在修改数据之前,需要先获得相应的锁;
获得锁之后,事务便可以修改数据;
该事务操作期间,这部分数据是锁定的,
其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁。
锁可以分为表锁、行锁以及其他位于二者之间的锁。
表锁在操作数据时会锁定整张表,并发性能较差;
行锁则只锁定需要操作的数据,并发性能好。
但是由于加锁本身需要消耗资源(获得锁、检查锁、释放锁等都需要消耗资源),
因此在锁定数据较多情况下使用表锁可以节省大量资源。
MySQL中不同的存储引擎支持的锁是不一样的,例如MyIsam只支持表锁,而InnoDB同时支持表锁和行锁,且出于性能考虑,绝大多数情况下使用的都是行锁。
十万个为什么地址:
https://benjaminwhx.com/2018/04/25/
%E8%B0%88%E8%B0%88MySQL-InnoDB
%E5%AD%98%E5%82%A8%E5%BC%95%
E6%93%8E%E4%BA%8B%E5%8A%A1%E7
%9A%84ACID%E7%89%B9%E6%80%A7/
然后进行数据的修改,如果出现错误或者用户需要回滚的时候可以利用Undo log的备份数据恢复到事务开始之前的状态。
在最后COMMIT的时候,必须先将该事务的所有日志写入到redo log file进行持久化(这里的写入是顺序写的),
待事务的COMMIT操作完成才算完成。即使COMMIT后数据库有任何的问题,
在下次重启后依然能够通过redo log的checkpoint进行恢复。也就是crash recovery。
最常见的例子是转帐。例如从帐户A转一笔钱到帐户B上,如果帐户A上的钱减少了,
而帐户B上的钱却没有增加,那么我们认为此时数据处于不一致的状态。
在数据库实现的场景中,一致性可以分为数据库外部的一致性和数据库内部的一致性。
前者由外部应用的编码来保证,即某个应用在执行转帐的数据库操作时,
必须在同一个事务内部调用对帐户A和帐户B的操作。
如果在这个层次出现错误,这不是数据库本身能够解决的,也不属于我们需要讨论的范围。
后者由数据库来保证,即在同一个事务内部的一组操作必须全部执行成功(或者全部失败)。
这就是事务处理的原子性。(上面说过了是用Undo log来保证的)
但是,原子性并不能完全保证一致性。
在多个事务并行进行的情况下,
即使保证了每一个事务的原子性,
仍然可能导致数据不一致的结果,
比如丢失更新问题。
为了保证并发情况下的一致性,引入了隔离性,
即保证每一个事务能够看到的数据总是一致的,
就好象其它并发事务并不存在一样。
用术语来说,就是多个事务并发执行后的状态,
\和它们串行执行后的状态是等价的。
但是RC和RR能够通过MVCC来保证记录只有在最后COMMIT后才会让别的事务看到。
而在RR下,事务开始后第一个读操作创建ReadView,一直到事务结束关闭。
SELECT id FROM ( SELECT max(b.id) AS id FROM t_4g_phone b
GROUP BY b.SERIAL_NUMBER ) b
)
由于数据都存放于内存中,所以不占用磁盘空间,
比较重要的目录有/proc/cpuinfo、/proc/interrupts、/proc/dma、/proc/ioports、/proc/net/*等
insert,光标当前字符前插入
a
append, 光标当前字符后插入
o
下一行插入
s
不常用,删除当前字符并插入
:q
退出不保存
:q!
强制退出不保存
:wq
退出保存
:wq!
强制退出保存
ICMP协议:Internet控制报文协议
ARP协议:地址解析协议
RARP协议:逆地址解析协议
TCP协议:传输控制协议
FTP:定义了文件传输协议,使用21端口。
Telenet:远程登录协议,23和22端口
POP3:邮局协议,用于接收邮件。通常情况下,POP3协议所用的是110端口。
HTTP协议
SMTP:简单邮件传送协议,25号端口
DNS:用于域名解析服务,将域名地址转换为IP地址。DNS用的是53号端口。
SNMP:简单网络管理协议,使用161号端口,是用来管理网络设备的。由于网络设备很多,无连接的服务就体现出其优势。
TFTP(Trival File Transfer Protocal):简单文件传输协议,该协议在熟知端口69上使用UDP服务。
同时自己也发送一个SYN包(syn=y),即SYN+ACK包,此时服务器进入SYN_RECV状态;
客户端和服务器进入ESTABLISHED状态,完成三次握手。
用来关闭主动方到被动关闭方的数据传送,
也就是主动关闭方告诉被动关闭方:
我已经不会再给你发数据了
(当然,在fin包之前发送出去的数据,如果没有收到对应的ack确认报文,
主动关闭方依然会重发这些数据),
但是,此时主动关闭方还可以接受数据。
确认序号为收到序号+1。
用来关闭被动关闭方到主动关闭方的数据传送,
也就是告诉主动关闭方,我的数据也发送完了,不会再给你发数据了。
(与SYN相同,一个FIN占用一个序号)。
发送一个ACK给被动关闭方,
确认序号为收到序号+1,至此,完成四次挥手。
包含协议版本号、首部长度、总长度、唯一标识、TTL、首部检验和、源IP地址和目的IP地址。
B类地址:以10开头,第一个字节范围:128~191;
C类地址:以110开头,第一个字节范围:192~223;
D类地址:以1110开头,第一个字节范围为224~239;
这可以通过子网掩码来确定。
将网络号和子网号全设为1的IP地址为子网掩码。
它作为可以将域名和IP地址相互映射的一个分布式数据库,
能够使人更方便的访问互联网,
而不用去记住能够被机器直接读取的IP数串。
它会查询DNS服务器来解析该名称。
客户机发送的每条查询信息包括三条信息:
包括:
指定的DNS域名,
指定的查询类型,
DNS域名的指定类别。
该应用一般不直接为用户使用,
而是为其他应用服务,
如HTTP,SMTP等在其中需要完成主机名到IP地址的转换。
如果主机所询问的本地域名服务器不知道被查询的域名的IP地址,
那么本地域名服务器就以DNS客户的身份,
向其它根域名服务器继续发出查询请求报文(即替主机继续查询),
而不是让主机自己进行下一步查询。
当根域名服务器收到本地域名服务器发出的迭代查询请求报文时,
要么给出所要查询的IP地址,要么告诉本地服务器:“你下一步应当向哪一个域名服务器进行查询”。
然后让本地服务器进行后续的查询。
根域名服务器通常是把自己知道的顶级域名服务器的IP地址告诉本地域名服务器,
让本地域名服务器再向顶级域名服务器查询。
顶级域名服务器在收到本地域名服务器的查询请求后,
要么给出所要查询的IP地址,
要么告诉本地服务器下一步应当向哪一个权限域名服务器进行查询。
最后,知道了所要解析的IP地址或报错,然后把这个结果返回给发起查询的主机。
本地服务器采用迭代查询。它先向一个根域名服务器查询。
根域名服务器告诉本地服务器,下一次应查询的顶级域名服务器dns.com的IP地址。
本地域名服务器向顶级域名服务器dns.com进行查询。
顶级域名服务器dns.com告诉本地域名服务器,下一步应查询的权限服务器dns.abc.com的IP地址。
本地域名服务器向权限域名服务器dns.abc.com进行查询。
权限域名服务器dns.abc.com告诉本地域名服务器,所查询的主机的IP地址。
本地域名服务器最后把查询结果告诉m.xyz.com。
提供的是面向连接、可靠的字节流服务。
当客户和服务器批次交换数据前,
必须建立TCP连接之后才能传输数据。
TCP提供超时重传、丢弃重复数据、流量控制等功能,
保证数据能从一端传到另一端。
UDP不提供可靠性,不保证数据能够到达目的地。
由于UDP在传输数据前不用在客户和服务器之间建立连接,
且没有超时重传等机制,故而传输速度很快。
通过这个IP地址找到客户端到服务器的路径。
客户端浏览器发起一个HTTP会话到220.161.27.48,
然后通过TCP进行封装数据包,输入到网络层。
在客户端的传输层,把HTTP会话请求分成报文段,
添加源和目的端口,如服务器使用80端口监听客户端的请求,
客户端由系统随机选择一个端口如5000,与服务器进行交换,
服务器把相应的请求返回给客户端的5000端口。
然后使用IP层的IP地址查找目的端。
客户端的网络层不用关系应用层或者传输层的东西,
主要做的是通过查找路由表确定如何到达服务器,
期间可能经过多个路由器,这些都是由路由器来完成的工作,
通过查找路由表决定通过那个路径到达服务器。
客户端的链路层,包通过链路层发送到路由器,
通过邻居协议查找给定IP地址的MAC地址,
然后发送ARP请求查找目的地址,
如果得到回应后就可以使用ARP的请求应答交换的IP数据包现在就可以传输了,
然后发送IP数据包到达服务器的地址。
因为服务器和应用客户端之间存在着流量的瓶颈,
所以读取大容量数据时,使用缓存来直接为客户端服务,
可以减少客户端与服务器端的数据交互,从而大大提高程序的性能。
scard key
set key index value
sismember key member
smembers key
zcard key
zrange key start stop[withscores]
1、只支持key-value数据结构存储
2、缓存数据全部在内存中,不支持数据持久化
通过数据冗余实现高可用
通过负载实现高性能
扩展硬件
Hash
Key
List
Composite
写操作的锁开销变小
两阶段提交
三阶段提交
性能非常高, 缺点是如果时间回拨或者各个实例节点时间不一致, 容易出错
支持多种不同模式的生成策略
1. 号段模式
该模式需要建DB表, 需要有专门的服务来提供获取id的接口, 存在网络延迟
2. Snowflake模式
为了追求更高的性能,需要通过RPC Server来部署Leaf 服务,那仅需要引入leaf-core的包,
把生成ID的API封装到指定的RPC框架中即可
简单易用, 可以指定workerId或者不指定, 直接通过jar的方式引入即可
需要建DB表, 需要有专门的服务来提供获取id的接口, 存在网络延迟
物理数据库
物理表
分片(切分)
分片节点
分片键
分片算法
逻辑表
逻辑数据库
对于内存泄漏,需要通过内存监控软件(headdump)查找程序中的泄漏代码;而堆大小可通过设置JVM的参数-Xms、-Xmx来解决
此种情况可以通过更改方法区的大小来解决,设置-XX:PermSize=64m -XX:MaxPermSize=256m参数
另外,过多的常量尤其是字符串也会导致方法区溢出,因为常量池也位于方法区中
可靠、可扩展、可维护的应用系统
高速缓存 : 缓存那些复杂或操作代价昂贵的结果,以加快下一次访问。缓存失效/雪崩
索引 : 用户可以按关键字搜索数据井支持各种过掳。elastic search/solr
流式处理:持续发送消息至另一个进程,处理采用异步方式。stream(spark streaming/ kafka streams)
批处理 : 定期处理大量的累积数据。hadoop/hive/hbase
硬件冗余
人为失误
数据库中写入的比例,
聊天室的同时活动用户数量,
缓存命中率等。
除了处理请求时间(服务时间, service time)外,
还包括来回网络延迟和各种排队延迟。
延迟则是请求花费在处理上 的时间。
一半的用户请求的服务时间少 于中位数响应时间,
另一半则多于中位数的时间。
有状态服务从单个节点扩展 到分布式多机环境的复杂性会大大增加
至少对于某些应用类型来 讲,
上述通常做告或许会发生改变。
可以乐观设想 ,
即使应用可能并不会处理大量数 据或流量,
但未来分布式数据系统将成为标配。
首先查找所 有的关注对象,
列出这些人的所有tweet,
最后以时间为序来排序合井。
当用户推送新tweet肘,
查询其关注者,
将tweet插入到每个关注者的时间线 缓存中。
因为已经预先将结果取出,
之后访问时间线性能非常快。
方法2已经得到了稳定实现,Twitter正在转向结合两种方法。
大多数用户的tweet在发布时继续以一对多写入时间线,
但是少数具有超多关注者 (例如那些名人)的用户除外,
对这些用户采用类似方案1 ,
其推文被单独提取,在 读取时才和用户的时间线主表合并。
这种混合方法能够提供始终如一的良好表现
支持自动化, 与标准工具集成。
避免绑定特定的机器,这样在整个系统不间断运行的同时,允许机器停机维护。
提供良好的文档和易于理解的操作模式,诸如“如果我做了X,会发生Y”。
提供良好的默认配置,且允许管理员在需要时方便地修改默认值。
尝试自我修复,在需要时让管理员手动控制系统状态。
行为可预测,减少意外发生。
一个好的设计抽象可以隐藏大量的实现细节,
并对外提供干净、易懂的接口。
一个好的设计抽象可用于各种不同的应用程序。
这样,复用远比多次重复实现更有效率;
另一方面,也带来更高质量的软件,
而质量过硬的抽象组件所带来的好处,
可以使运行其上的所有应用轻松获益。
或为了更好地理解用户需求,
或商业环境发生变化时,
就需要不断地添加或修改功能。
构建可适应变化的系统
数据模型与查询语言
数据被组织成关系,在SQL中称为表,每个关系都是元组的无序集合(SQL中称为行)
那么应用层代码中的对象与表、行
和列的数据库模型之间需要一个笨拙的转换层(ORM)。
关系(表)只是元组(行) 的集合 ,仅此而已。
没有复杂的嵌套结构, 也没有复杂的访问路径。
可以读取表中的任何一行或者所有行,
支持任意条件查询。
可以指定某些列作为键并匹配这些列来读取特定行。
可以在任何表中插入新行,
而不必担心与其他表之间的外键关系
只需构建一次查询优化器,
然后使用该数据库的所有应用程序都可以从中受益。
即在其父记录中保存了嵌套记 录(一对多关系), 而不是存 储在单独的表中。
没有模式意味 着可以将任意的键值添加到文档中,
并且在读取时,客户端无陆保证文档可能包含哪些字段。
这些性能方面的不利因素大大限制了文档数据库的适用场景。
但是随着数据之间的关联越来越复杂,
将数据建模转化为图模型会更加自然。
顶点(也称为结点或实体)
和边(也称为关系或弧)。
很多数据 可以建模为图。
例如,汽车导航系统搜索道路网中任意两点 之间的最短路径,
PageRank可以计算Web图上网页的流行度,从而确定搜索排名
图更为强大的用途在于,
提供了单个数 据存储区中保存完全不同类型对象的一致性方式。
在这种情况下, 三元组的谓语和客体分别相当于主体(顶点) 属性中的键和值。
例如, (Lucy, αge, 33 ) 就好比是顶 点lucy, 具有属性{"age”: 33}。
此时,谓语是图中的边 ,主体是尾部顶点 ,而客体是头部顶点。
例如,在(Lucy , m.arriedTo, alain)中,主体Lucy和客体alain都是顶点 ,并且谓语marriedTo是连接二者的边的标签。
一种用于属性图的声明式查询语言,最早为Neo4j图形数据库而创建
(person) -[ : BORN_IN] - > ()来匹配这样的模式,
即所有顶点间带有标签BORN_IN 的边,
且尾部顶点对应于变量person ,而头部顶点则没有要求
找到满足以下两个条件的任何顶点(顶点代表人,称其为person)
1. person有一个到其他顶点的出边BORN_IN。
从该顶点开始,可以沿着一系列出边WITHIN,
直到最终到达类型为Location的顶点, name属性为”United States ” 。
2. 同一个person顶点也有一个出边LIVES_IN。
沿着这条边,然后是一系列出 边WITHIN ,
最终到达类型为Location的顶点, name属性为"Europe” 。
采用RDF数据模型的三元存储查询语言
它采用“谓语 (主体, 客体 )” 的表达方式而不是三元组 (主体,谓语 ,客体) 。
这允许将多值数据存储在单行内 ,并支持在这些文档中查询和索引。
其存储在数据库的文本列中,并由应用程序解释其结构和内容。
对于此方法,通常不能使用数据库查询该编码列中的值。
因此用 JSON表示非常合适, 参见示17tl2-l 。
与XML相比, JSON的吸引力在于它更简单。
面向文档的数据库(如MongoDB、RethinkDB、CouchDB和Espresso )都支持该数据模型。
因为它对人类没有任何直接意义,
所以永远不需要直接改变 : 即使 ID标识的信息发生了变化,
它也可以保持不变。
而在文档数据库中, 一对多的树状结构不需要 联结,支持联结通常也很弱
但随着应用支持越来越多 的功能,数据也变得更加互联一体化。
关系数据库和文档数据库并没有根本的不 同 :
在这两种情况下,相关项都由唯一的标识符引用,
该标识符在关系模型中被称为 外键,
在文档模型中被称为文档引用。
标识符可以查询时通过联结操作或相关后续查询来解析。
对于某些应用来说,它更接近于应用程序所使用的数据结构。
XPath表达式
而是介于两 者之间: 查询的逻辑用代码片段来表示, 这些代码片段可以被处理框架重复地调用 。
它主要基于许多函数式编程语言中的map和reduce函数
function map() { ②
var year = this. observationTimestamp.getFull Year();
var month = this .observationTimestamp.getMonth() + 1;
emit (year +-”+ month, this.numAnimals); ③
},
function reduce(key, values) { ④
return Array.sum(values); ⑤
},
{
query: { family:”Sharks" } , ①
out :”monthlySharkReport”⑥
}
);
② 对于每个匹配查询的文档,都会调用一次JavaScript的map函数,并将其设置为文 档对象。
③ map函数发射一个键-值对,其中键是由年份和月份组成的字符串,如"2013-12” 或"2014-1”;值代表观察的动物数量。
④ map函数发射的键-值对按键分组。对于相同键(即相同的月份和年份)的所有键-值对,调用reduce函数。
⑤ reduce函数将特定月份内所有观察到的动物数量相加。
⑥ 最终的输出写入到monthlySharkReport集合中。
它们必须是纯函数,这意味着只能使用传递进去的数据作为输入,
而不能执行额外的数据库查询, 也不能有任何副作用。
必须编写两个密切协调的JavaScript函数,
这通常 比编写单个查询更难。
此外, 声明式查询语言为查询优化器提供了更多提高查询性 能的机会。
由于这些原因, MongoDB 2.2增加了称为聚合管道的声明式查询语言的支 持。
数据存储与检索
B-tree
日志是一个仅支持追加式更新的 数据文件。
需要新的数据结构 : 索引
这些元数据作为路标,帮助定位想要的数据。
因此任何类型的 索引通常都会降低写的速度。
适当的索引可以加速读取查询,
但每个索引都会减慢写速度。
通常采用hash map (或 者hash table,哈希表)来实现
那么最简单的索引策略 就是:
保存内存中的hash map,
把每个键一一映射到数据文件中特定的字节偏移量,
这样就可以找到每个值的位置
而value数据量则可以超 过内存大小,
只需一次磁盘寻址,就可以将value从磁盘加载到内存。
如果那部分数据 文件已经在文件系统的缓存中,
则读取根本不需要任何的磁盘I/O。
一个好的解决方案 是将日志分解成一定大小的段,
当文件达到一定大小时就关闭它,井将后续写入到新 的段文件中。
当合并日志段时, 一旦发现墓碑标记,则会丢弃这个己删除键 的所有值。
可以更快地加载 到内存中,以此加快恢复速度。
可以被多个线程同时读取。
主要原因有以下几个 :
key-value对 按键排序
排序字符串表
当多个段包含相同的键时,可以保留最新段的值,并丢弃旧段中的值
不再需要在内存中保存所有键的索引。
可以考虑将这些记录保存到一个块中并在写磁盘之前将其压缩
这个内存中 的树有时被称为内存表。
然后是最新的磁盘段文件,
接下 来是次新的磁盘段文件,以此类推,直到找到目标(或为空)。
以合并多个段文件,并丢弃那些已被 覆盖或删除的值。
为了避免该问题,可以在磁盘上保留单独的日 志,每个写入都会立即追加到该日志。
旧数据被移动到单独的“层级”,
这样压缩可以逐步进行并节省磁盘空间。
日志结构的合并树
LSM-Tree算法可能很慢:在确定键不存在之前,
必须先检查内存表,然后将段一直回溯访问到最旧的段文件
为了优化这种访问,存储引擎通常使用额外的布隆过滤器
因此可以有效地执行区间查询(从最小值到最大值扫描所有的 键),
并且由于磁盘是顺序写入的,
所以LSM-tree可以支持非常高的写入吞吐量。
许多非关系型 数据库也经常使用
这样可以实现高效的key-value查 找和区间查询。
传统上大小为4 KB (有时更大),页是内部读/写的最小单元。
这种设计更接近底层 硬件,因为磁盘也是以固定大小的块排列。
这样可以让一个页面引用另一个页面,
类 似指针,不过是指向磁盘地址,而不是内存。
首先搜索包含该键的叶子页,更改该页的值,
并将页写回到磁盘(对该页的任何引用仍然有效)。
如果要添加新键,则需要找到其范围 包含新键的页,并将其添加到该页。
如果页中没有足够的可用空间来容纳新键,则将其分裂为两个半满的页,
并且父页也需要更新以包含分裂之后的新的键范围,
这是一个仅支持追加修改的文件,每个B-tree的修改必须先更新WAL然后再修改树本身的页。
当数据库在崩 愤后需要恢复时,该日志用于将B-tree恢复到最近一致的状态。
否则线程可能会看到树处于不一致的状态。
通常使用锁存器(轻量级的锁)保 护树的数据结构来完成。
B-tree被认为对于读取更快。
读取通常在LSM-tree上较慢,
因为它们必须在不同的压缩阶段检查多个不 同的数据结构和SSTable。
在这种情 况下, 写放大具有直接的性能成本 :
存储引擎写入磁盘的次数越多 ,
可用磁盘带宽中 每秒可以处理的写入越少。
部分是因为它们有时具有 较低的写放大,部分原因是它们以顺序 方式写入紧凑的SSTable文件, 而不必重写树中的多个页
由于碎片, B-tree存储引擎使某些磁盘空间无法使用:
当页被分裂或当一行的内容不能适合现 有页时,
页中的某些空间无能使用。
由于LSM tree不是面向页的,
并且定期重写 SSTables,消除碎片化,
所以它们具有较低的存储开销,特别是在使用分层压缩时
如果写入吞吐量很高并且压缩没有仔细配置,
那么就会发生压缩无法匹配新数据写入 速率的情况。
而日志结构的存储引 擎可能在不同的段中具有相同键的多个副本。
如果数据库希望提供强大的事务语义,
这方面B-tree显得更具有吸引力:
在许多关系数据库中, 事务隔离是通过键范围上的 锁来实现的,
并且在B-tree索引中,这些锁可以直接定义到树中
主键唯一标识关系表中的一行,或文档数据库中的一个文档,
或图形数据库中的 一个顶点。
数据库中的其他记录可以通过其主键(或ID )来引用该行/文档/顶点,
该 索引用于解析此类引用 。
主要区别在于它的键不是唯一的,
即可能有许多行(文档,顶点)具有相同键。
这可以通过两种方式解决:
使索引中的 每个值成为匹配行标识符的列表(像全文索引中的posting list),
或者追加一些行标 识符来使每个键变得唯一。
无论哪种方式, B-tree和日志结构索引都可以用作二级索引。
而值则可以是以下两类之一
这样当存在多个 二级索引时,
它可以避免复制数据,
即每个索引只引用堆文件中的位置信息,
实际数 据仍保存在一个位置。
堆文件方法会非常高效:
它可能需要移动数据以得到一 个足够大空间的新位置。
在这种情况下,所有索引都需要更新以指向记录的新的堆位 置 ,
或者在旧堆位置保留一个间接指针
表的主键始终是聚集索引,
二级索引引用主键(而不是堆文件位置)
和非聚集索引(仅存储索引中的数据的引用)
之间有一种折中设计称为覆盖索 引或包含列的索引,
它在索 引中保存一些表的列 值。
它可以支持只通过索引即可回答某些简单查询
(在这种情况下,称索引覆盖了查询)
但是它们需要额外 的存储,
并且会增加写入的开销。
此外,数据库还需要更多的工作来保证事务性,
这 样应用程序不会因为数据冗余而得到不一致的结果。
它通过将一列追加到另一列,
将几个字段简单 地组合成一个键(索引的定义指定宇段连接的顺序 )
这对地理空间数据尤为重要。
例如,餐馆 搜索网站可能有一个包含每个餐厅的纬度和经度的数据库。
它只能提供一个纬度范围内 (但在任何经度)的所有餐馆,
或者所有经度范围内的每厅 (在北极和南极之间的任 何地方),
但不能同时满足。
然后使用常规的B-tree 索引。
更常见的是使用专门的空间索引,如R树。
并忽略单词语法上的变体
Lucene能够在某个编辑距离内搜索 文本
此 结构需要一个小的内存索引来告诉查询,
为了找到一个键,需要排序文件中的哪个偏 移量。
如果有足够 的内存,即使是基于磁盘的存储引擎,
也可能永远不需要从磁盘读取,
因为操作系统 将最近使用的磁盘块缓存在内存中。
都提供了类 似数据库的访问接口。
由于所有的数据都保存在内存中,所以实现可以比较简单。
在线事务处理
在线分析处理
事实表位于中间,被一系列维度表包 围;
这些表的连接就像星星的光芒。
而是将每列中的所有 值存储在一起。
如果每个列存储在一个单独的文件中,
查询只需要读取和解析在该查 询中使用的那些列,
这可以节省大量的工作
如果主排序列上没有很多不同的值,
那么在排序之后,
它将出现一个非常长的序列,
其中相同的值在一行中重复多次
将其添加到已排序的结构中,
接着再准备写入磁盘
需要检查磁盘上的列数据和内存中最近的写入
它通常被定义为标准(虚 拟)视图:
一个类似表的对象,其内容是一些查询的结果
并被写到磁盘 ,
而虚拟视图只是用于编写查询的快捷方式。
物化视图也需要随之更新,
因为它是数据的非规范化副本。
数据编码与演化
在大多数情况下,
更改应用程序功能时,
也需要更改其存储的数据 :
可能需要捕获新 的字段或记录类型,
或者需要以新的方式呈现已有数据。
尽管该模式可以改变,
这样在任何一个给定时间点都只有一个有效的模式
代码更迭往往并非易事
新版本部署无需服务暂停,从而支持更频繁的版本发布和更好的演化
然而他们在一段时间内可能不会马上安 装更新。
以及新旧数据格式,
可能会同时在系统内共存。
为了使系 统继续顺利运行,
需要保持双向的兼容性
清楚旧代码所编写的数据格式,
因此可 以比较明确地处理这些旧数据
(如果需要,只需保留 旧的代码来读取旧的数据)。
这些 数据结构针对CPU的高效访问和操作进行了优化
从内存中的表示到字节序列的转化称 为编码(或序列化等),
相反的过程称为解码(或解析,反序列化)
例如,Java有java.io.Serializable, Ruby有Marshal, Python有pickle等。
此外,还有许多第 三方库,例如用于Java 的Kryo。
而用另一种语言访问数据就非常困难。
不能将系统与其他组织(可能使用不同的语言)的系统方便地集成在 一起。
解码过程需要能够实例化任意的类
导致一些安全问题:
如果攻击者可以让应用程序解码任意的字节序列,
那么它们可以实例化任意的类,这通常意味着,
它们可以做些可怕的事情,比如远程执行任意代码
例 如, Java的内置序列化由于其糟糙的性能和臃肿的编码而广为诟病。
以及相对于XML的简单性
但是它们不 支持二进制字符串(没有字符编码的字节序列)。
这些模式语言相当强大,因此学习和 实现起来也比较复杂。
并且可以在需要时指定长度 (包括字符串的长度、列表中的项数)。
与之前类 似,
数据中出现的字符串 ( “Martin”, “ daydreami ng”, “hacking”)
也被编码 为ASCII (或者更确切地说, UTF-8 )
相反, 编码数据包含数字类型的字段标签 ( 1、 2和3 ) 。
这些是模式定 义中出现的数字。
字段标签就像字段的别名,
用来指示当前的字段,但更为紧凑 ,可 以省去引用字段全名。
它通过将字段类型和标签号打包到单字节中,
并使用可 变长度整数来实现。
它的位打包方式略有不同 ,
但与Thrift的CompactProtocol非常相似。
Protocol Buffers只用33字节可以表示相同的记录。
每个字段被标i己为required (必须) 或optional (可选) ,
但这对宇段如何编码没有任何影响
(二进制数据中不会指示某 宇段是否必须)。
区别在于,如果字段设置了required ,但字段未填充,
则运行时检 查将出现失败, 这对于捕获错误非常有用。
每个字段 由其标签号 标识,
并使用数据类型(例如字符串或整数)进行注 释。
如果没有设置字段值,则将其从编码的记录中简单地忽略。
由此可以看出, 字段标签(field tag)对编码数据的含义至关重要。
可以轻松更改模式中字段的名称,
而编码永远不直接引用字段名称。
但不能随便更改宇段的标签,
它会导致所有现有编码 数据无效。
只要给每个字段一个新的标记号码。
如果旧的代码(不知道 添加的新标记号码)试图读取新代码写入的数据,
包括一个它不能识别的标记号码中新 的字段,
则它可以简单地忽略该字段。
实现时,通过数据类型的注释来通知解析器跳过 特定的字节数。
这样可以实现向前兼容性, 即旧代码可以读取由新代码编写的记录。
因此,为了 保持向后兼容性,
在模式的初始部署之后添加的每个字段都必须是可选的或具有默认 值。
它有两种模式语言 :
一种(Avro IDL)用于 人工编辑,
另一种(基于JSON)更易于机器读取。
才能正确解码二进制数据。
读和写的模式如果有任何不匹配 都将无法解码数据。
(例如将其写入文件或数据库,以及 通过网络发送) 时 ,
它使用所知道的模式的任何版本来编码数据,
例如,可以编译到 应用程序中的模式。
(例如从文件或数据库读取数据,或者从网络接收数据 等)时,
它期望数据符合某个模式。
例如 , union{ null, long, string}宇 段;
表示该宇段可以是数字、字符串或null。
只有当null是联合的分支之一时, 才可以 使用它作为默认值。
reader的模式可以包含字段名称的别名,
因此它可以将旧writer模式字段名称与别名进行匹配。
这意味着更改字段名称是向后兼容的,但不能向前兼容。
同样,向联合类型添加分支也是向后兼容的,但不能向前兼容。
Avro通过指定一个文件格式(对象容器文件)来做到这一点。
并在数据库中保留一个模式版本列表。
reader可以获取记 录,提取版本号,
然后从数据库中查询该版本号的writer模式。
使用该writer模 式,它可以解码记录的其余部分
他们可以在建立连接时协商模式版 本,
然后在连接的生命周期中使用该模式。
并使用该模式对数据库内容进行编码,
然后将其全部转储到Avro对象容器 文件中。
可以为每个数据库的表生成对应的记录模式,
而每个列成为该记录中的一 个字段。
数据库中的列名称映射到Avro中的字段名称。
则可以从 更新的数据库模式生成新的Avro模式,
并用新的Avro模式导出数据。数据导出过程不 需要关注模式的改变,
每次运行时都可以简单地进行模式转换。
任何读取新数据文件 的人都会看到记录的字段已经改变,
但是由于字段是通过名字来标识的,
所以更新的writer模式仍然可以与旧的reader模式匹配。
所以可以确定它是最 新的(而手动维护的文档可能很容易偏离现实)。
从模式生成代码的能力是有用的,
它能够在 编译时进行类型检查。
这些进程可能是几个不同 的应用程序或服务,
也可能只是同一服务的几个实例
然后由仍在运行的旧版本代码读 取。
因此,数据库通常也需要向前兼容。
通过使服务可独立部署和演化让应用程序更易于更改和维护
它强调简单的数据格 式,
使用URL来标识资源,
并使用HTTP功能进行缓存控制、身份验证和内容类型协商。
在跨组织服务集成的背景下, 经常与微服务相关联。
根据REST原则所设计的API称为RESTful
通常涉及较少的代码生成和自动化工具。
定义 格式如OpenAPI,也称为Swagger,
可用于描述RESTful API并帮助生成文挡。
用于发出网络API请求的。
虽然它最常用 于HTTP,但其目的是独立于HTTP ,
并避免使用大多数HTTP功能。
相反,它带有庞大而复杂的多种相关标准,和 新增的各种功能。
看起来与在同一进程中调用编程语言中的函数或方法相同
(这种抽象称为位置透明)
网络请求是 不可预测的
要么抛出一个异常,
或者永远不会返回
网络请求有另一个可能的结果 :
由于超时,它返回时 可能没有结果。
可能会发生请求实际上已经完成,
只是响应丢失的情 况。
在这种情况下,重试将导致该操作被执行多次,
除非在协议中建立重复数据 消除(幂等性)机制。
网络请求比函数调用要 慢得多,而且其延迟也有很大的变化
当发出 网络请求时,
所有这些参数都需要被编码成可以通过网络发送的字节序列。
如果 参数是像数字或字符串这样的基本类型,
这没关系, 但是对于较大的对象很快就 会出现问题。
所以RPC框架必须将数据类型从一 种语言转换成另一种语言
即允许客户端查询在哪个IP地址和端口号上获得特 定的服务
可以实现比诸如REST上的JSON之类的通用协议更好的性能。
其次是所有的客户端
而请求则采用 JSON或URI编码/表单编码的请求参数。
为了保持兼容性,
通常考虑的更改包括 添加可边的请求参数和在响应中添加新的字段。
服务的提供 者经常无法控制其客户,也不能强制他们升级
通信模式是异步的
并且代理确保消息、被传递给队列或主题的一个或 多个消费者或订阅者。
在同一主题上可以有许多生产者和许多消费者。
也可以发送到一个回复队 列,该队列由原始消息发送者来消费
每个Actor通常代表一个客户 端或实体,
它可能具有某些本地状态(不与其他任何Actor共享),
并且它通过发送 和接收异步消息与其他Actor通信。
不保证消息传送 : 在某些错误情况下,消息将丢 失。
由于每个Actor一次只处理一条消息,因此不需要担心线程,每个Actor都可以由框架独立调度。
这个编程模型被用来跨越多个节点来扩展应用程序。
都使用相同的消息传递机制。
如果它们位于不同的节点上,
则消息被透明地编码成字节序列,
通过网络发送,并在 另一端被解码。
因为Actor模型已经假定消息可能会丢 失,
即使在单个进程中也是如此。
尽管网络上的延迟可能比同一个进程中的延迟更 高 ,
但是在使用Actor模型时,
本地和远程通信之间根本上的不匹配所发生的概率更小
则仍需担心向前和向后兼容性问题
它不提供向前或向后兼容性。
但 是,可以用类似Protocol Buffers的东西替代它,
从而获得滚动升级的能力
要部署 新版本的应用程序,
需要建立一个新的集群,
将流量从旧集群导入到新集群,
然 后关闭旧集群。
像Akka一样, 也可以使用自定义序列化插件。
(尽管系统具有许多为高可用性而设 计的功能) 。
滚动升级在技术上是可能的,但要求仔细规划
数据复制
使数据在地理位置上更接近用户,从而降低访问延迟。
当部分组件出现位障,系统依然可以继续工作,从而提高可用性。
扩展至多台机器以同时提供数据访问服务,从而提高读吞吐量。
否则, 某些副本将出现不一致。
最常见的解决方案是基于主节点的复制
主副本把新数据写入本地存 储后,
然后将数据更改作为复制的日志或更改流发送给所有从副本。
每个从副本 获得更改日志之后将其应用到本地,
且严格保持与主副本相同的写入顺序。
可以在主副本或者从副本上执行查询。
再次强调, 只有主副本才可以接受写请求:
从客户端的角度来看,从副本都是只读的。
和SQL Server的AlwaysOn Availability Groups。
MongoDB、 RethinkDB和Espresso,Kafka和RabbitMQ
同步复制还是异步复制。
对于关系数据库系统,
同步 或异步通常是一个可配置的选项:
而其他系统则可能是硬性指定或者只能二选一。
从节点可以明确保证完成了与主节点的更新同 步,
数据已经处于最新版本。
万一主节点发生故障,总是可以在从节点继续访问最新 数据。
(例如由于从节点发生崩愤,或者 网络故障,或任何其他原因),
写入就不能视为成功。
主节点会阻塞其后所有的写操作,直到同步副本确认完成。
通常意味着其中某一个从节点是同步的,
而其他节点则是异步模式。
万一同步的从节 点变得不可用或性能下降,
则将另一个异步的从节点提升为同步模式。
这样可以保证 至少有两个节点
(即主节点和一个同步从节点)拥有最新的数据副本。
这种配置有时 也称为半同步
主节 点总是可以继续响应写请求,
系统的吞吐性能更好。
则所有尚未复制到从节点的写请求都会丢失。
这意味着即使向客户端确认了写操作,
却无 法保证数据的持久化。
听起来是一个非常不靠谱的折中设计,
但是异步复制还是 被广泛使用,
特别是那些从节点数量 巨大或者分布于广域地理环境。
替换失败的副本,增加新的从节点
这样避免长时间锁定整 个数据库。
目前大多数数据库都支持此功能,
快照也是系统备份所必需的。
而在 某些情况下,可能需要第三方工具,
如MySQL的innobackupex
因为在第一步创 建快照时,
快照与系统复制日志的某个确定位置相关联,
这个位置信息在不同的 系统有不同的称呼,
如PostgreSQL将其称为“log sequence number”(日志序列 号),
而MySQL将其称为“binlog coordinates” 。
这个过程称之为追 赶。
接下来,它可以继续处理主节点上新的数据变化。
并重复步骤1~步骤4。
在选举之后,原主节点很快又重新上线并加入到集群,
接下来的写操作会发生什 么?新的主节点很可能会收到冲突的写请求,
这是因为原主节点未意识的角色变 化,
还会尝试同步其他从节点,
但其中的一个现在已经接管成为现任主节点。
丢弃数据 的方案就特别危险
可能会发生两个节点同时-都自认为是主节 点。
这种情况被称为脑裂,
它非常危险:两个主节点都可能接受写请求,
并且没 有很好解决冲突的办法,最后数据可 能会丢失或者破坏。
主节点失效后,超时时间设置得越长 也意味着总体恢复时间就越长
可用性与延迟之 间各种细微的权衡,
实际上正是分布式系统核心的基本问题
而任何副本只能接受只读查询
也称为读写一致性
从主节点读取 ; 否则 ,在从节点读取。
则总是在主节点读取;并监控从节点的复制滞后程度 ,
避免从那些滞后时 间超过一分钟的从节点读取。
并附带在读请求中,据此信息,
系统可 以确保对该用户提供读服务时
都应该至少包含了该时间戳的更新。
(例如考虑与用户的地理接近,以及高可用性),
情 况会更复杂些。
必须先把请求路由到主节点所在的数据中心
(该数据中心可能离 用户很远)
无法保证来自不同设备的连接经过路由之后都到达
同一个数据中心
这是一个比强一致性弱,但比最终一致 性强的保证。
当读取数据时,
单调读保证,
如果某个用户依次进行多次读取,
不会看到回滚现象
(而不同的 用户可以从不同的副本读取)。
例如,基于用户ID的哈希的方法而不是随机选择副 本。
但如果该副本发生失效,则用户的查询必须重新路由到另一个副本。
那么读取这些内容时也会按照当时写入的顺序。
但该方案 真实实现效率会大打折扣。
现在有一些新的算法来显式地追踪事件因果关系,
“Happened-before关系与并发”会继续该问题的探讨。
在每个数据中心内,
采用常规的主从复制方案;
而在数据中心之间,
由各个数 据中心的主节点来
负责同其他数据中心的主节点进行数据的交换、更新。
与多主复制方案之 间的差异
这 会大大增加写入延迟,基本偏离了采用多数据中心的初衷
然后采用异步 复制方式将变化同步到其他数据中心
必须切换至另一个数据中 心,
将其中的一个从节点被提升为主节点。
每个数据中心则 可以独立于其他数据中心继续运行,
发生故障的数据中心在恢复之后更新到最新 状态。
对数据中心之间的网络性能和稳定 性等更加依赖。
可以更好地容忍此类问题,
例如临时网络闪断不会妨碍写请求最终成功。
(用来接受写请求),
然后 在所有设备之间采用异步方式同步这些多主节点上的副本,
同步滞后可能是几小时或 者数天,
具体时间取决于设备何时可以再次联网。
第二个写请求要么会被阻塞直到第一个写完成,
要么被中止 (用户必须重试) 。
然而在多主节点的复制模型下,
这两个写请求都是成功的,
并且 只能在稍后的时间点上才能异步检测到冲突,
那时再要求用户层来解决冲突为时已 晚。
即如果应用层可以保证对特定记录的写请求 总是通过同一个主节点,
这样就不会发生写冲突
数据更新符合顺序性原则,
即如果同一个字段有多个更新,
则最 后一个写操作将决定该字段的最终值。
例如, 一个时间戳, 一个足够长的随机数,一个 UUID
或者一个基于键-值的哈希,挑选最高ID的写入作为胜利者,并将其他写入丢弃。
例如序号高的副本写入始终优先 于序号低的副本。
这种方法也可能会导致数据丢失。
然后依靠应用层的逻 辑,事后解决冲突
所以大多数多主节点复制模型都有工具
来让用户编写应用代码来解决冲突
就会调用应用层的冲突处理程 序
所有冲突写入值都会暂时保存下来。
下一次读取数据时,
会将 数据的多个版本读返回给应用层。
应用层可能会提示用户或自 动解决冲突,
并将 最后的结果返回到数据库。
主要是存在某些网络链路比其他链 路更快的情况
(例如由于不同网络拥塞),
从而导致复制日志之间的覆盖
并将这些写入(加上自 己的写入)转发给后序节点。
星形拓扑还可以推广到树状结构。
即中间节点需 要转发从其他节点收到的数据变更。
为防止无限循环,每个节点需要赋予一个唯一的 标识符,
在复制 日志中的每个写请求都标记了已通过的节点标识符。
如果某个节点 收到了包含自身标识符的数据更改,
表明该请求已经被处理过,
因此会忽略此变更请 求,避免重复转发。
在修复之前,
会影响其他节 点之间复制日志的转发。
可以采用重新配置拓扑结构的方法暂时排除掉故障节点。
在 大多数部署中,这种重新配置必须手动完成。
而对于链接更密集的拓扑(如全部到全 部),
消息可以沿着不同的路径传播,避免了单点故障,
因而有更好的容错性。
客户端直接将其写请求发送到多副本,
而在其他一些实 现中,
由一个协调者节点代表客户端进行写入,
但与主节点的数据库不同,
协调者井 不负责写入顺序的维护。
有两个可用副本接受写请求,
而不可用的副本无 法处理该写请求。
如果假定三个副本中有两个成功确认写操作,
用户 1234收到两个 确认的回复之后,
即可认为写入成功。
客户完全可以忽略其中一个副本无法写入的情 况。
它不是向一个副本发送请 求,
而是并行地发送到多个副本。
客户端可能会得到不同节点的不同响应,
包括某些 节点的新值和某些节点的旧值。
可以采用版本号技术确定哪个值更新
例如,在图5-10中 , 用户2345从副本3获得的是版本6,
而从副本l和2得到的是版本7。
客户端可以判 断副本3一个过期值,
然后将新值写入到该副本。
这种方怯主要适合那些被频繁 读取的场景。
将任何缺少的数 据从一个副本复制到另一个副本。
与基于主节点复制的复制日志不同,
此反熵过 程并不保证以特定的顺序复制写入,
并且会引入明显的同步滞后。
写入需要w个节点确认,
读取必须至少 查询r个节点,
则只要 w+r>n,读取的节点中一定会包含最新值。
w=r = (n + 1) /2 (向上舍入)。
也可以根据自己的需求灵 活调整这些配置。
数据分区
即每一条数据(或者每条记录,每行或每个文档)
只属于某 个特定分区。
每个分区都可以视为一 个完整的小型数据库,
虽然数据库可能存在一些跨分区的操作。
即每个分区在多个节点都存有副本。
这意味着某条记录属 于特定的分区 ,
而同样的内容会保存在不同的节点上以提高系统的容错性。
分区的主要 目标是
将数据和查询负载
均匀分布在所有节点上。
就可以轻松确定哪个分区包含这些关键字。
如果还知道哪个分区分配在哪个节 点,
就可以直接向该节点发出请求
这主要是因为数据本身可能就不均匀。
或者由数据库自动选择
包括Bigtable, Bigtable的开源版本HBase ,
RethinkDB和2.4版本之前MongoDB
这 样可以轻松支持区间查询,
即将关键字作为一个拼接起来的索引项
从而一次查询得到 多个相关记录
如果关键字是时间 戳,则分区对应于一个时间范围,
例如每天一个分区。
然而,当测量数据从传感器写 入数据库时,
所有的写入操作都集中在同一个分区(即当天的分区),
这会导致该分 区在写入时负载过高,
而其他分区始终处于空闲状态
例如,可 以在时间戳前面加上传感器名称作为前缀,
这样首先由传感器名称,
然后按时间进行 分区。
假设同时有许多传感器处于活动状态,
则写入负载最终会比较均匀地分布在多 个节点上。
接下来,当需要获取一个时间范围内、多个传感器的数据时,
可以根据传 感器名称,各自执行区间查询。
许多分布式系统采用了基于关键字哈希函数的方式来 分区。
不适合分区
分区边界可以是均匀间隔,
也可以是伪随机选择(在这种情况下,该技术有时被称为一致性哈希)
我们丧失了良好的区间查询特性。
即使关键字相 邻,
但经过哈希之后会分散在不同的分区中,
区间查询就失去了原有的有序相邻的特 性。
Cassandra中的表可以声明为由 多个列组成的复合主键。
复合主键只有第一部分可用于哈希分区,
而其他列则用作组 合索引来对Cassandra SSTable中的数据进行排序
因此,它不支持在第一列上进行区 间查询,
但如果为第一列指定好了固定值,
可以对其他列执行高效的区间查询。
那么理论上10个节点应该
能够处理10倍的数据量
和10倍于单个节点的读写吞吐量
如果分区不均匀,
则会出现某些分区节点
比其他分区承担更多的
数据量或查询负 载,
称之为倾斜。
在极端情况下,
所有的负载可能会 集中在一个分区节点上,
这就意味着10个节点9个空闲,
系统的瓶颈在最繁忙的那个 节点上。
这种负载严重不成比例的分区即成为系统热点。
一个简单的技术就是在关键字的开 头或结尾处添加一个随机数。
只需一个两位数的十进制随机数
就可以将关键字的写操 作分布到100个不同的关键字上,
从而分配到不同的分区上。
必须从所有100个关键字中读取数据然后进行合并。
因此通常只对少量的热点关键字附加随机数才有意 义;
而对于写入吞吐量低的绝大多数关键字,
这些都意味着不必要的开销。
此外,还 需要额外的元数据来标记哪些关键字进行了特殊处理。
二级索引通常不能唯一标识一条记录,
而是用来加速特定值的查询
在文档数据库中应用也非常普遍。
但考虑到其 复杂性,许多键-值存储(HBase和Voldemort)并不支持二级索引;
但其他一些如 Riak则开始增加对二级索引的支持。
此外, 二级索引技术也是Solr和Elasticsearch等全 文索引服务器存在之根本。
各自维护自己的二级索引,
且只负责自己分 区内的文档而不关心其他分区中数据。
每当需要写数据库时,包括添加,删除或更新 文档等,
只需要处理包含目标文档ID的那一个分区。
因此文档分区索引也被称为本地 索引
然后合并所有 返回的结果。
查询分区数据库的方法有时也称为分散/聚集,
显然这种二级索引的查询代价高 昂。
即使采用了并行查询,也容易导致读延迟显著放大
而不是每个分区维护自己的本地 索引。
不能将全局索引存储在一个节点上,
否则就破坏了设计分区均衡的目标。
所以,全局索引也必须进行分区,
且可以与数据关键字采用不同 的分区策略。
或者对其取哈希值。
直接分区的好处是可以支持高效的区间查询
而采用哈希的方式则可以更均句的划分分区。
主要因为单个文档的更新时,
里面可能会涉及多个二级索引,
而二级索引的分区 又可能完全不同甚至在不同的节点上,
由此势必引人显著的写放大。
数据库可能总会出现某些变化:
查询压力增加,因此需要更多的CPU来处理负载。
数据规模增加,因此需要更多的磁盘和内存来存储数据。
节点可能出现故障,因此需要其他机器来接管失效的节点。
平衡之后,负载、数据存储、读写请求等应该在集群范围更均匀地分布。
再平衡执行过程中,数据库应该可以继续正常提供读写服务。
避免不必要的负载迁移,以加快动态再平衡,并尽量减少网络和磁盘I/O影响。
不直接使用mod
会导致很多关键字需要从现 有的节点迁移到另一个节点。
那么这个关键字应该放在节点6 ( 123456 mod 10 = 6);
当节点数增加到11时,它需要移动到节点3 ( 123456 mod 11 = 3) ;
当继续增长到 12个节点时,又需要移动到节 点0 ( 123456 mod 12 = 0)。
这种频繁的迁移操作大大增加了再平衡的成本。
然后 为每个节点分配多个分区。
该新节点可以从每个现有的节点上匀走几个 分区,
直到分区再次达到全局平衡
则采取相反的均衡措施。
但分区的总数量仍维持不变,
也不会改变关键字 到分区的映射关系。
这里唯一要调整的是分区与节点的对应关系。
考虑到节点间通过 网络传输数据总是需要些时间 ,
这样调整可以逐步完成,
在此期间,
旧的分区仍然可 以接收读写请求。
即性能更强大的节点将分 配更多的分区,从而分担更多的负载。
设置一个足够大的分 区数。
而每个分区也有些额外的管理开销,
选择过高的数字可能会有副作用。
它就拆分为两个分区,每 个承担一半的数据量。
相反,如果大量数据被删除,
并且分区缩小到某个阈值以下,
则将其与相邻分区进行合并。该过程类似于B树的分裂操作
而每个节点可以承载多个分区,
这点与固定数量的分 区一样。
当一个大的分区发生分裂之后,
可以将其中的一半转移到其他某节点以平衡 负载。
如果只有少量的数据,
少量 的分区就足够了,
这样系统开销很小;
如果有大量的数据,
每个分区的大小则被限制 在一个可配的最大值
因为没有任何先验知识可以帮助确定分 区的边界,
所以会从一个分区开始。
可能数据集很小,但直到达到第一个分裂点之 前,
所有的写入操作都必须由单个节点来处理,
而其他节点则处于空闲状态。
HBase和MongoDB允许在一个空的数据库上
配置一组初始分区(这 被称为预分裂)。
对于关键字区间分区,预分裂要求已经知道一些关键字的分布情况
也适用于基于哈希的分区策略。
MongoDB从 版本2.4开始,同时支持二者,并且都可以动态分裂分区。
因此这种方陆也使每个分区大小保持稳定。
它随机选择固定数量的现有分区进行分裂,
然后拿走这 些分区的一半数据量,
将另一半数据留在原节点。
但是当平均分区数量较大时 ,
新节点最终会从现有节点中拿走相当数量的负载。
一类典型的服务发现问题,
服务发现并不限于数据库,
任何通过网络访问 的系统都有这样的需求,
尤其是当服务目标支持高可用时
如果某节点恰 好拥有所请求的分区,
则直接处理该请求 :
否则,将请求转发到下一个合适的节 点,
接收答复,并将答复返回给客户端。
由后者负责将请求转发到对应的分区 节点上。
路由层本身不处理任何请求,
它仅充一个分区感知的负载均衡器。
此时,客户端可以直接连接到目标节点,而不 需要任何中介。
ZooKeeper维护了分区到节 点的最终映射关系。
其他参与者(如路由层或分区感知的客户端)可以向ZooKeeper 订阅此信息。
一旦分区发生了改变,或者添加、 删除节点,
ZooKeeper就会主动通知 路由层,这样使路由信息保持最新状态。
不同的分区可以放在一个无共享集群的不同节点上。
一个大数据集可以分散在更多的磁盘上,
查询负载也随之分布到更多的处理器上。