notes

前言

这里记录一些笔记

正文

#1 等待通知机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// JVM 实现

public class Test2 {

private static final Object LOCK = new Object(); // 注意当作锁的对象一定要是final的,加入不是final的,一个线程用此对象加锁后,这个对象变了,另一个线程再用这个对象加锁就已经是另一把锁了,那么锁就失去语义了

public static void main(String[] args) throws InterruptedException {
Thread thread0 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (LOCK) {
try {
System.out.println("即将等待");
LOCK.wait(); // 释放锁并进入等待
System.out.println("等待结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});

Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (LOCK) { // 注意 synchronized 块,进入代表 lock 也就是 monitorenter
LOCK.notifyAll(); // 通知等待的其他人
} // 注意 synchronized 块,退出代表 unlock 也就是 monitorexit,退出了才释放锁,只有这里释放锁,上边等着的人才能获得锁,这不放,上边永远都进不到 synchronized 块里
}
});

// 注意顺序,等待的先启动,发通知的后启动,要发通知的先启动了,通知空放,等待的空等
thread0.start();
thread1.start();

thread0.join();
thread1.join();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// JDK 实现

public class Test5 {

private static Lock lock = new ReentrantLock();

private static Condition condition = lock.newCondition();

public static void main(String[] args) throws InterruptedException {
Thread thread0 = new Thread(new Runnable() {
@Override
public void run() {
try {
lock.lock(); // 先获得锁
condition.await(); // 进入阻塞等待
System.out.println("已被唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 释放锁
}
}
});

Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
lock.lock(); // 先获得锁
System.out.println("通知正在阻塞等待的其他人");
condition.signalAll(); // 通知正在阻塞等待的其他人
System.out.println("先不释放锁,停2秒再释放");
Thread.sleep(2000L);
System.out.println("停2秒已到时间了");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 释放锁
System.out.println("锁已经释放了");
}
}
});

thread0.start();
thread1.start();

TimeUnit.SECONDS.sleep(5);
}
}

synchronized + Object#wait() 内在的逻辑可以以一个通俗易懂的场景解释,想象一下这个场景:一个只有一个坑位的厕所,一次只能有一个人使用(synchronized (LOCK) ,获得锁的线程可以运行,未获得锁的线程,不能运行),这个人(某个线程)进去关上门插上门闩(也就是获得了锁)该干嘛干嘛,其他人需要在外面排队等待(也就是形成一个名为同步队列的队列),排队的规则是后来的人插入到队尾,然后里面的人解决完了,会打开门闩开开门(释放锁,也就是线程执行已经出了synchronized块)然后告诉队列排在最前面的那一个人(同步队列 头节点),让他进去;而对于 LOCK.wait()LOCK.notifyAll() ,里面涉及到 等待队列 的概念,等待队列的理解可以想象这个场景:还是刚才那个厕所排队,除了厕所里边正在工作的(获得锁的线程),以及门口等待进去的(同步队列中的线程),还有一批(或者多批)人(也就是执行了LOCK.wait()的线程)排成一队打盹,这些人是由原来在坑位门里的人(已经获得了锁的线程)因为某些原因,自己放弃了此次的“工作”,主动形成另一个队伍(等待队列)打盹,这些人不参与排队等着进门(也就是排队获得锁),只有被当前在坑位门里的人(已经获得锁的线程)唤醒时,他们才会重新排到等待进坑位门的队列里(也就是已获得锁的线程执行了 notifyAll() ,就会让等待队列里的线程从等待队列移到同步队列

Lock + Condition 实现等待通知机制,实际上和synchronized + Object#wait() 在逻辑上差不太多,都是有两个队列:synchronized + Object#wait()同步队列(入口集)和等待队列(等待集),而Lock + Condition则是AQS内部的等待队列和Condition自己的等待队列,套路还是一个,只是实现的层面不同了,synchronized + Object#wait()是JVM层面实现的,Lock + Condition是JDK库层面实现的

另外等待通知有个比较特殊的例子就是Threadjoin()join()内部仍是waitnotify,但是锁对象变成了Thread对象实例,当在上边的代码中执行

1
2
3
4
...
thread0.join();
thread1.join();
...

时,可以不严谨的理解为实际上执行者也就是main线程隐式调用了thread0.wait()thread1.wait() 这时main线程就等待了,然后在thread0线程执行结束后,会隐式调用thread0.notifyAll(),在thread1线程执行结束后,会隐式调用thread1.notifyAll(),这样在等待的main线程就被唤醒了。

一张及不严谨的图,大概描述个意思:

图1-1

#2 多态、虚方法表

刚刚看书发现了一个有意思的点,在Java里多态背后的实现是:调用方法的实例在本身存储了自身的类型信息的引用,通过在类型信息引用中查找想要调用方法如果找不到则会去找父类信息,看看那里面有没有定义想要调用的方法,这个过程会一直持续到找不到父类为止,如果父类链路比较深的话则开销会很大,Java的做法是用“虚方法表”这个虚方法表其实就是把一个子类能拥有的所有方法的定义信息(包括从父类继承的)全都事先拽过来存储到自己的类信息里,这样再查找就不要去父类找了(等于用冗余的手段把层次结构扁平花了),我感觉这个和咱们平常业务量做表的冗余字段加速查询是一个套路,套路还是那个套路,只是用的地方不一样了。

图2-1

图2-2

#3 类初始化锁

当多个线程需要初始化一个类,仅有一个线程会进⾏,其他线程需要等待。当活动的线程完成初始化之后, 它必须通知其他等待线程

#4 记录一次JVM old过高的排查

今天收到一条报警,有个实例JVM old过高,于是登录上去看了一下。

  1. 首先,jps -l 查到程序进程号为 1161

  2. 然后 jstat -gccause 1185 1000 5

1
2
3
4
5
6
7
8
9
10
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT    LGCC                 GCC                 
100.00 0.00 100.00 100.00 94.11 87.48 74 9.622 296 2855.880 2865.502 Allocation Failure Allocation Failure
100.00 0.00 100.00 100.00 94.11 87.48 74 9.622 296 2855.880 2865.502 Allocation Failure Allocation Failure
100.00 0.00 100.00 100.00 94.11 87.48 74 9.622 296 2855.880 2865.502 Allocation Failure Allocation Failure
100.00 0.00 100.00 100.00 94.11 87.48 74 9.622 296 2855.880 2865.502 Allocation Failure Allocation Failure
100.00 0.00 100.00 100.00 94.11 87.48 74 9.622 296 2855.880 2865.502 Allocation Failure Allocation Failure
100.00 0.00 100.00 100.00 94.11 87.48 74 9.622 296 2855.880 2865.502 Allocation Failure Allocation Failure
100.00 0.00 100.00 100.00 94.11 87.48 74 9.622 296 2855.880 2865.502 Allocation Failure Allocation Failure
100.00 0.00 100.00 100.00 94.11 87.48 74 9.622 296 2855.880 2865.502 Allocation Failure Allocation Failure
100.00 0.00 100.00 100.00 94.11 87.48 74 9.622 296 2855.880 2865.502 Allocation Failure Allocation Failure

What does “GC (Allocation Failure)” mean in my ElasticSearch 5.6 logs?

GC (Allocation Failure) is a JVM message (not an Elasticsearch-specific one) that can be a sign that there’s memory pressure, but it’s not catastropic to the JVM (that would cause a OutOfMemoryError log line). It can also be completely innocuous.
GC (Allocation Failure) means that the Java garbage collector tried to run, ran out of space in the heap, then tried to allocate more memory. It’s not a bad sign, necessarily. If you’re receiving OutOfMemoryError errors and the JVM is crashing, then you know you’re in trouble.
Side note / disclaimer / full disclosure / whatever the heck you want: I work for a DBaaS company that hosts Elasticsearch clusters.

Java GC (Allocation Failure)

“Allocation Failure” is a cause of GC cycle to kick in.
“Allocation Failure” means that no more space left in Eden to allocate object. So, it is normal cause of young GC.
Older JVM were not printing GC cause for minor GC cycles.
“Allocation Failure” is almost only possible cause for minor GC. Another reason for minor GC to kick could be CMS remark phase (if +XX:+ScavengeBeforeRemark is enabled).

  1. jinfo 1161 可以确认一下应用的JVM参数
1
2
3
...
Command line: -Djava.util.logging.config.file=/opt/web/xxx/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Xms4g -Xmx4g -Xmn1024m -Xss1024K -XX:PermSize=256m -XX:MaxPermSize=512m -XX:ParallelGCThreads=8 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:SurvivorRatio=4 -XX:MaxTenuringThreshold=10 -XX:CMSInitiatingOccupancyFraction=80 -Djava.security.egd=file:/dev/./urandom -DWF.uspcluster= -DWF.uspcluster=xxx -Djava.endorsed.dirs= -Dcatalina.base=/opt/web/xxx -Dcatalina.home=/opt/soft/tomcat8 -Djava.io.tmpdir=/opt/web/xxx/temp
...
  1. jmap -heap 1161 可以看一下堆的整体情况,可以看到,新生代、老年代都已满
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
Attaching to process ID 1161, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.65-b01

using parallel threads in the new generation.
using thread-local object allocation.
Concurrent Mark-Sweep GC

Heap Configuration:
MinHeapFreeRatio = 40
MaxHeapFreeRatio = 70
MaxHeapSize = 4294967296 (4096.0MB)
NewSize = 1073741824 (1024.0MB)
MaxNewSize = 1073741824 (1024.0MB)
OldSize = 3221225472 (3072.0MB)
NewRatio = 2
SurvivorRatio = 4
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)

Heap Usage:
New Generation (Eden + 1 Survivor Space):
capacity = 894828544 (853.375MB)
used = 894828536 (853.3749923706055MB)
free = 8 (7.62939453125E-6MB)
99.99999910597398% used
Eden Space:
capacity = 715915264 (682.75MB)
used = 715915264 (682.75MB)
free = 0 (0.0MB)
100.0% used
From Space:
capacity = 178913280 (170.625MB)
used = 178913272 (170.62499237060547MB)
free = 8 (7.62939453125E-6MB)
99.99999552855998% used
To Space:
capacity = 178913280 (170.625MB)
used = 0 (0.0MB)
free = 178913280 (170.625MB)
0.0% used
concurrent mark-sweep generation:
capacity = 3221225472 (3072.0MB)
used = 3221225472 (3072.0MB)
free = 0 (0.0MB)
100.0% used

62273 interned Strings occupying 7047888 bytes.
  1. 既然内存被占满了,那么我们需要看看是什么占了内存,使用 jmap -histo 1161
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

num #instances #bytes class name
----------------------------------------------
1: 82597943 3245691336 [B
2: 6768948 541514920 [[B
3: 6768814 162451536 com.mysql.jdbc.ByteArrayRow
4: 347621 42931968 [C
5: 79883 41219352 [Ljava.lang.Object;
6: 104039 9155432 java.lang.reflect.Method
7: 343375 8241000 java.lang.String
8: 220766 7064512 java.util.concurrent.ConcurrentHashMap$Node
9: 93615 3744600 java.util.LinkedHashMap$Entry
10: 56944 2733312 org.aspectj.weaver.reflect.ShadowMatchImpl

...省略

可以看到 com.mysql.jdbc.ByteArrayRow 很可疑,有6768814个实例占了154M多,在他前边的 [B 代表字节数组,有82597943个实例占了3.24G多,[[B代表二位字节数组,他有6768948个实例,占了541M多,基本上这三个货基本把整个堆(从前面jinfo打出的JVM参数可知最大4G)占满了,同时我们找一台正常的实例(与这个实例完全相同的工程、完全相同的配置)看一下他的jmap -histo,对比一下:

1
2
3
4
5
6
7
8
9
10
11
12
 num     #instances         #bytes  class name
----------------------------------------------
1: 190913 105464840 [B
2: 886653 84314088 [C
3: 270310 55553000 [I
4: 191697 16869336 java.lang.reflect.Method
5: 659489 15827736 java.lang.String
6: 460922 14749504 java.util.concurrent.locks.AbstractQueuedSynchronizer$Node
7: 220887 7068384 java.util.concurrent.ConcurrentHashMap$Node
8: 129368 6858776 [Ljava.lang.Object;
9: 151208 6048320 java.util.LinkedHashMap$Entry
10: 77036 5630744 [Ljava.util.HashMap$Node;

经过对比可知,前面排在第三名的 com.mysql.jdbc.ByteArrayRow 基本上可以确定为疑犯了,那么我们就大致确定了排查的方向。

  1. 来看 com.mysql.jdbc.ByteArrayRow
1
2
3
4
5
6
7
8
9
10
11
...

/**
* A RowHolder implementation that is for cached results (a-la mysql_store_result()).
*/
public class ByteArrayRow extends ResultSetRow {

byte[][] internalRowData;

}
...

可以看到,这个类里包含了一个二维字节数组,这基本上符合了 [B [[B com.mysql.jdbc.ByteArrayRow 这三个实例都很多的现象。再往下我们可以结合搜索引擎以及大胆的假设来继续排查下去了。

  1. 总结一下

通过这个实例我们可以得出 JVM问题排查的一般步骤:通过工具(调优三大件:jstat jstack jmap 以及其他工具:jps、jinfo、jcmd)观察现象,通过现象加以猜测和对比得出方向,通过方向进一步白盒排查外加猜测尝试得出结论

#5 实现一个小顶堆

再写数据结构题时,我们先要搞明白数据结构的定义,以及要写的数据结构有哪些特性,有什么样的行为,有了这些特性、行为的理解,我们才可能写出代码,空中楼阁是造不出来的

可以把数据结构的特性1,2,3,4的列出来再写,另外一开始可以先把次要的元素用简单的东西实现出来(比如我这个小顶堆一开始并没有“节点Node的概念”只是先用一个整形代替了,一开始先关注实现堆的行为,节点是不是对象并不影响堆的行为,可先忽略),实现完主流程然后再回过头来改成更复杂的实现,这样循序渐进,早期把关注点集中在主流程上更容易成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
package io.since1986.demo;

import java.util.Arrays;
import java.util.StringJoiner;

public class HeapTest {

public static void main(String[] args) {
/*
堆的性质
1. 父节点比左子节点和右子节点都大或比左子节点和右子节点都小
2. 节点和兄弟节点之间的大小无所谓(也就是左右子节点之间大小与位置无关)
3. 每个节点都有编号,根节点编号为1,自根节点向下,从左至右依次顺序编号(也就是编号+1)
4. 每个节点的编号有这样的规律:某个节点N的有左子节点L和右子节点R,L.number = N.number * 2, R.number = N.number * 2 + 1, L.number = R.number + 1


基于堆的上述性质,可以将堆在物理上使用数组存储(编号即是数组索引)
*/

SmallTopHeap smallTopHeap = new SmallTopHeap();
smallTopHeap.add(10);
smallTopHeap.add(7);
smallTopHeap.add(8);
smallTopHeap.add(5);
smallTopHeap.add(9);
smallTopHeap.add(6);
smallTopHeap.add(4);
smallTopHeap.add(3);
smallTopHeap.add(1);
smallTopHeap.add(2);
System.out.println(smallTopHeap);

System.out.println(smallTopHeap.delete());
System.out.println(smallTopHeap.delete());
System.out.println(smallTopHeap.delete());
System.out.println(smallTopHeap.delete());
System.out.println(smallTopHeap.delete());
System.out.println(smallTopHeap.delete());
System.out.println(smallTopHeap.delete());
System.out.println(smallTopHeap.delete());
System.out.println(smallTopHeap.delete());
System.out.println(smallTopHeap.delete());
}

// 小顶堆
static class SmallTopHeap {

// 根节点索引恒定为1
private static final int ROOT_NODE_INDEX = 1;

// 真正的堆的承载数组是需要扩容的,但是我这个只是演示用的堆,写的简单一点,不支持承载数组扩容
private Node[] nodes = new Node[11];

private int largestNodeIndex = ROOT_NODE_INDEX;

SmallTopHeap() {
// 注意数组的索引是从0开始的,但是我为了方便起见,承载数组索引0位置空着不用了,因为概念上堆的索引从要从1开始
nodes[0] = new Node(null);
}

void add(Integer nodeValue) {
if (largestNodeIndex > 10) {
throw new UnsupportedOperationException("这个只是演示用的堆,不支持承载数组扩容");
}

Node currentNode = new Node(nodeValue);

// 插入时总是将要插入的节点放在整个堆的最后
int currentIndex = largestNodeIndex;
nodes[currentIndex] = currentNode;

// 插入节点以后需要更新最大节点的索引+1(加了一个元素因此最大的索引要 + 1)
if (largestNodeIndex < 10) {
largestNodeIndex++;
}

// 如果当前承载数组中还没有存储根节点,那么当前插入的节点就是根节点,插入完成直接返回
if (currentIndex == 1) {
return;
}

// 如果当前承载数组中已经存储了根节点,那么插入完成后就需要进行校验,检查当前堆是否还符合堆的特性
// 也就是从新插入节点的父节点开始一直循环比较大小是关系否合规,直到合规或者已经到根节点为止(根节点是编号最小的节点)

// 找到刚刚插入的节点的父节点 parentElementIndex = currentIndex / 2 这里不用考虑当前是右节点还是左节点,不管是哪个节点,得到父节点编号都是简单的除以2即可,我们可以想一下:
// 假如左子节点索引为 6 那么他的兄弟节点(也就是右节点)索引就是 6 + 1 = 7 父节点索引为 6 / 2 = 3 ,用右节点 7 来推算父节点索引: 7 / 2 = 3,这里 7 除以 2 等于 3 是因为整形除法会丢弃小数点,索引必然都是整形,因此不管是用左子节点还是右子节点来计算父节点的索引,公式都是除以2就可以了
while (true) {
int parentNodeIndex = currentIndex / 2;
Node parentNode = nodes[parentNodeIndex];

// 我们这里是小顶堆(也就是父节点的值小于子节点的值)
if (parentNode.value.compareTo(currentNode.value) < 0) {
// 如果父节点的值小于当前节点的值,是符合小顶堆的特性的,那么循环就可以结束了
break;
}
// 要是父节点的值大于当前节点的值,那么就交换父节点与当前节点
nodes[parentNodeIndex] = currentNode;
nodes[currentIndex] = parentNode;

// 将当前索引设置为父节点索引,并且将当前节点设置为父节点这样就可以进行下次循环了
currentIndex = parentNodeIndex;
currentNode = nodes[currentIndex];

// 循环到了根节点(也就是索引为1的节点)就退出
if (currentIndex == ROOT_NODE_INDEX) {
break;
}
}
}

Integer delete() {
if (largestNodeIndex < 1) {
return -1;
}

// 如果只剩一个了,直接置空并返回
if (largestNodeIndex == 1) {
Node firstNode = nodes[1];
nodes[1] = null;
return firstNode.value;
}

// 堆删除节点一般是删除承载数组第一个元素,然后将承载数组最后一个元素填到第一个元素的位置上,然后将最后一个元素置为null,将最后一个元素放到第一个后会破坏原来堆的性质,需要重新调整让整个承载数组重新符合堆的性质
Node nodeThatBeenDeleted = nodes[1];
Integer result = nodeThatBeenDeleted.value;

Node largestIndexNode = nodes[largestNodeIndex];
nodes[1] = largestIndexNode;
nodes[largestNodeIndex] = null;
largestNodeIndex--;

// 重新调整结构,让承载数组重新符合堆的特性
// 找到左子节点和右子节点,比较一下它们的值大小,将当前节点的值大小与他们两个之中小的那个进行比较,如果值大于就置换(置换后能保证上边的比下边的小,举个例子 假设当前节点值为 10 它的左子节点值为 5,右子节点值为 6,置换后5变为当前节点,比10和6都小,这样是符合小顶堆的性质的),如果小于就返回
int currentIndex = 1;
Node currentNode = nodes[1];

while (true) {
int leftChildNodeIndex = currentIndex * 2;
int rightChildNodeIndex = currentIndex * 2 + 1;

// 特殊流程1:如果左子节点的索引已经大于最大索引了,那直接跳出循环
if (leftChildNodeIndex > largestNodeIndex) {
break;
}

// 特殊流程2:如果左子节点已经是最后一个了,那么直接比较交换并结束循环
if (leftChildNodeIndex == largestNodeIndex) {
Node leftChildNode = nodes[leftChildNodeIndex];
if (leftChildNode.value.compareTo(currentNode.value) > 0) {
break;
}

nodes[currentIndex] = leftChildNode;
nodes[leftChildNodeIndex] = currentNode;
break;
} else {
// 普通流程:左子节点没有到最大索引(此时右子节点有可能是最大索引也有可能不是,倒无所谓,不影响流程,走普通流程就好)
Node leftChildNode = nodes[leftChildNodeIndex];
Node rightChildNode = nodes[rightChildNodeIndex];

// 找到左子节点右子节点中小的那个
Node smallerChild = leftChildNode.value.compareTo(rightChildNode.value) < 0 ? leftChildNode : rightChildNode;
int smallerChildIndex = leftChildNode.value.compareTo(rightChildNode.value) < 0 ? leftChildNodeIndex : rightChildNodeIndex;

// 并于当前节点比较,如果小于就返回(父节点比两个子节点小是符合小顶堆性质的,就不用再置换处理了)
if (smallerChild.value.compareTo(currentNode.value) > 0) {
break;
}

// 当前节点的值大小与他们两个之中小的那个进行比较,如果值大于就置换处理
nodes[currentIndex] = smallerChild;
nodes[smallerChildIndex] = currentNode;

// 并将当前索引置为较小子节点的索引,将当前节点置为较小节点,以便进行下一轮循环
currentIndex = smallerChildIndex;
currentNode = nodes[currentIndex];

// 如果已经到了最大索引,就退出循环
if (currentIndex == largestNodeIndex) {
break;
}
}
}

return result;
}

@Override
public String toString() {
return new StringJoiner(",", "{", "}")
.add("\"nodes\":" + Arrays.toString(nodes))
.add("\"largestNodeIndex\":" + largestNodeIndex)
.toString();
}

static class Node {

private Integer value;

Node(Integer value) {
this.value = value;
}

@Override
public String toString() {
return new StringJoiner(":", "{", "}")
.add("\"value\"")
.add(String.valueOf(value))
.toString();
}
}
}
}

输出:

1
2
3
4
5
6
7
8
9
10
11
{"nodes":[{"value":null}, {"value":1}, {"value":2}, {"value":5}, {"value":4}, {"value":3}, {"value":8}, {"value":6}, {"value":10}, {"value":7}, {"value":9}],"largestNodeIndex":10}
1
2
3
4
5
6
7
8
9
10

#6 JMM对64位数据类型有特殊规范

第一点:JMM 规范定义了8种操作(逻辑上的定义)lock、unlock、read、load、use、assign、store、write ,并且有规定:“不允许read和load、store和write操作单一出现”,也就是 read必然伴随load 、store必然伴随write,也就是主内存中的变量同步至工作内存(read和load)这个操作是原子的,同时工作内存中的值同步到主内存这个操作(store和write)也是原子的 我不知道你问题里“原子的”是不是指的这个范畴

第二:以上那两个操作,JMM的设计是:对于64为数据类型,不要求完全遵守,但是强烈建议遵守,商业虚拟机在实现上也一般听从了JMM的建议,将64位类型的那两种操作也实现成了原子的

#7 new 都做了什么操作

一个 new 语句分为4步 第一步NEW是创建并默认初始化 然后放到栈顶 然后复制一份栈顶再放到栈顶 然后用栈顶传入 最后再哪栈顶放到局部变量

参考 关于JVM字节码中dup指令的问题?

图7-1

#8 随机数

Java的随机数生成的大致逻辑是:用户提供(或Random类内部自行生成)一个初始的数,然后通过一个公式(名字叫线性同余公式)把这个初始数作为输入给你算出另一个数,用这个过程模拟随机性,构造器里有值就是使用者给初识数,没值就是Random类自己给生成一个初识数(初始数叫做种子)

#9 @annotation 类型的切入点只在有接口方法声明的前提下生效,为什么

昨天那个问题我基本上已经串起来了,我灵机一动看了一下 idea 的调用栈视图(这个方法在整个流程的什么位置已经直观的在调用栈里显示出来了,很清晰,想知道流程就沿着调用栈倒推即可)

图9-1

大致的是:

  1. 在Spring创建我那个被切入的Service的Bean的时候,也就是 doCreateBean 的时候会执行一个初始化流程,也就是执行这个方法: org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#initializeBean(java.lang.String, java.lang.Object, org.springframework.beans.factory.support.RootBeanDefinition)

  2. 在上面这个方法里有一个应用后处理器(BeanPostProcessor)的调用 ,也就是 执行applyBeanPostProcessorsAfterInitialization

  3. 在上面那个方法里会调用 BeanPostProcessor的postProcessAfterInitialization,而执行的对象是AbstractAutoProxyCreator,也就是执行这个方法: org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#postProcessAfterInitialization

  4. 上边那个方法会调用 wrapIfNecessary (Wrap the given bean if necessary, i.e. if it is eligible for being proxied.)

  5. 上边那个方法会调用 getAdvicesAndAdvisorsForBean ( Return whether the given bean is to be proxied, what additional advices (e.g. AOP Alliance interceptors) and advisors to apply.)

  6. 上边方法会调用 findEligibleAdvisors (Find all eligible Advisors for auto-proxying this class.)也就是找到所有合格的 Advisor (Advisor 是增强器,对应了我们自己写的标注了@Aspect的类中具体对原需要增强方法的增强操作)

  7. 然后上边方法调用 findAdvisorsThatCanApply (Search the given candidate Advisors to find all Advisors that)

  8. 然后上边调用 findAdvisorsThatCanApply (Determine the sublist of the {@code candidateAdvisors} list that is applicable to the given class.)

  9. 然后调用 canApply (Can the given advisor apply at all on the given class?)

  10. 然后调用 org.springframework.aop.support.AopUtils#canApply(org.springframework.aop.Pointcut, java.lang.Class, boolean) 这个也就是昨天我加断点的方法,在这个方法里通过切入点定义 Pointcut 以及通过 ClassUtils.getAllInterfacesForClassAsSet(targetClass) 找到的切入目标类的接口(重点是*接口*)然后循环此接口的所有方法来判断是不是能被切入(具体调用 org.springframework.aop.aspectj.AspectJExpressionPointcut#matches(java.lang.reflect.Method, java.lang.Class, boolean) 来判断) 关键就在于是找的是接口而不是实现

切面(Aspect) = 切入点(Pointcut) + 通知(Advice)

spring 中 AOP 是通过 BeanPostProcessor 插入到 IoC 流程里的

框架源码系列十:Spring AOP(AOP的核心概念回顾、Spring中AOP的用法、Spring AOP 源码学习)

#10 责任链模式尝试

图10-1

#11 spring配置方式的变更与DRY

Spring的配置信息由外部配置xml的方式改为内部注解配置的方式正是DRY原则的一种体现,原有的xml配置方式你需要将类的完全限定名等元信息重复的在XML里再写一份,而注解则不需要,因为类自己就是对象的元信息,自描述了,当然不需要外部描述了,这正是DRY的一种体现。

图11-1

#12 容器管理实例的模式

Spring对于Java中标准的new逻辑的增强,与hibernate中的实体的概念有着异曲同工之妙,都是是用外部容器管理实例,这样可以给整个实例的生命周期中插入很多扩展点,通过扩展点就能玩出很多new玩不出来的新花样

图12-1

#13 编程本质

编程本质的一个方面:将人类的逻辑转化为机器逻辑。

人类逻辑 -> 自然语言
自然语言 -> 高级编程语言
高级编程语言 -> 底层编程语言
底层编程语言 -> 机器语言
机器语言 -> 机器逻辑

编程的一方面本质:恰恰是在造轮子,某个业务工程的设计思想完全可能来自于某个中间件的设计思想,比如我将netty中责任链的设计思路和Spring的MethodHandleAdptor中的SPI思路移植到支付系统的架构思路中,这本质上是将表象的代码逻辑实现回归拆解提炼为最基本的逻辑内核,然后再反相用这个逻辑内核结合业务需求提炼出的细节来重塑一个完整体系的过程

刚才思考了一个问题和大家分享一下:
前一阵我在我的一个支付业务工程里应用了类似netty中的责任链的设计方式 我觉得这个过程背后有一些通用的东西

编程的一方面本质:构建逻辑内核,用逻辑内核制造上层建筑。

某个业务工程的设计思想完全可能来自于某个中间件的设计思想,比如将netty中责任链的设计思路和Spring的MethodHandleAdptor中的SPI思路移植到支付系统的架构思路中

这本质上是将表象的代码逻辑实现回归拆解提炼为最基本的逻辑内核,然后再反相用这个逻辑内核结合业务需求提炼出的细节来重塑一个完整体系的过程

不过这个理解感觉还不是特别深 毕竟 netty的这个设计思路我不是自己看懂的 而是看来别人写的分析 算是间接的 但是感觉整体的方向应该差不多

自顶向下然后又自底而上

归根结底是逻辑

共性的东西

其实也就是把轮子拆解一下,或者把造轮子的过程拆解一下,提取出构造的通用办法,然后配合手头的工具箱和原材料再去造自己的轮子或者造别的什么东西的一个过程

简单点说就是不断琢磨比人的轮子然后造自己的轮子

#14 分析Java线程CPU占用率高的步骤

  1. jps -l 找到Java进程pid
  2. echo "obase=16;<pid>" | bc 得到pid的十六进制值(也可使用在线转换工具)
  3. jstack <pid> | grep <pid_hex> 按上一步得到的十六进制pid搜素jstack的输出结果,从而定位到具体的栈信息
  4. 根据得到的栈信息进行推测

#15 对于Spring IoC的理解

对于Spring IoC的理解 :自动化车间。给定原材料(原始类定义)和规格说明书(元数据,包括注解和xml等),就会生产出产品(Bean),在这个期间不需过多干预,但是提供了扩展点可以进行必要的干预(BeanFactoryPostProcessor 和 BeanPostProcessor)

#16 Spring的设计哲学

核心功能 + 扩展点

spring的轻量是轻量在设计上 他的微内核设计思想 核心功能BeanFactory很小 很多功能比如自动装配Autoware均是通过扩展点PostProcessor实现的

比如各种Awre如ApplicationContextAware也是通过PostProcessor实现的?
AOP也是通过BeanPostProcessor这个扩展点插入的IoC这个主逻辑中的

#17 对象的本质是 状态 + 行为

A Class is a software bundle of related states(properties, or variables) and behavior(methods)

#18 Spring 兼容循环依赖的做法

图18-1

图18-2

图18-3

图18-4

这样加一下断点调一下试试,所有断点都是条件断点 然后两个Bean的无参构造器上加方法断点

先通过 getBean -> createBean 流程创建Bean3实例,然后通过 InstantiationAwareBeanPostProcessorBean3 在其中解析依赖,解析依赖又会触发创建流程创建,创建Bean4实例,然后又会通过 InstantiationAwareBeanPostProcessorBean4 在其中进行Bean4解析依赖拿出来Bean3设置到自己的属性,然后将已经设置了Bean3 的 Bean4 设置到一开始的 Bean3上,这样Bean3和Bean4就实现了循环依赖

这个过程大概通俗一点就是 (A -> (B -> BA)) -> ABA

整个流程应该就是先各自生成一个没依赖的光棍对象Bean,然后把前一个光棍设置后一个光棍当依赖,然后再把已经有依赖的后一个光棍(其实现在已经不是光棍了)设置给前一个光棍当依赖,这样就成了一个环状依赖了

#19 Spring Bean Scope

Spring bean的两种scope的区别在于对待状态的态度 一个是共享状态 一个是状态独立

#20 RootBeanDefinition

RootBeanDefinition 是对于容器内实例的定义

图20-1

#21 IoC 的两种实现方式 DF 和 DI

通过容器提供的回调Api而非Java语言级别的Api(setter 、构造器)完成控制,这种方式叫做DF也就是依赖查找 是IoC的另一种实现方式

#22 对于 IoC 的理解

对于IoC的理解:怎样对待依赖:是自己创建依赖,也就是new还是从外部获取依赖,从外部获取依赖分为DF和DI也就是依赖查找和依赖注入

#23 ApplicationContext

ApplicationContext 比 BeanFactory 多了 事件监听机制、MessageSource和统一的资源加载Api(ResourceLoader),另外比普通BeanFactory 多了 BeanFactoryPostProcessor 可以在ApplicationContext启动时修改Bean声明,比如内置的后处理器PropertyOverrideConfigure就可以用于覆盖Bean中的属性

#24 ProxyFactoryBean

ProxyFactoryBean 也是一个重要的扩展点,事物AOP通过这个实现,FactoryBean是个重要的扩展点,

#25 Spring事务处理的顶层设计

Spring事务处理的顶层设计:面对事物资源(数据库)用PlatformTransactionManager去对接,而面向客户端代码(业务逻辑)使用AOP来对接(细粒度的实现:ProxyFactoryBean 和 Interceptor 或 粗粒度的实现;TransactionProxyFactoryBean)

事物定义:TransactionDefinition 这个可类比 BeanDefinition

事物属性需TransactionAttribute要事物定义

事物管理器(面向事物资源的顶层接口)需要事物定义 然后返回事物状态,事物状态是面向客户端的接口

Spring允许客户端通过两种方式使用事物支持:编程式(直接使用TransactionManager或用TransactionTemplate)和声明式(用AOP来接入TransactionManager)

Spring通过TransactionProxyFactoryBean,也就是FactoryBean这个IoC扩展点实现了代理对象的生成,然后通过TransactionIterseptor完成对代理方法的拦截将事务处理功能编织到拦截方法里(PlatformTransactionManager实现了TransactionInterception

spring—transaction(1)—源代码分析(事务的拦截器TransactionInterceptor)

#26 持续重构

持续重构 说白了其实就是当看到自己之前写的代码有可以改进的地方,比如可以提出方法的逻辑块,就着手改进,而不是放任不管,追求卓越是重构的精髓

#27 SpringBoot

SpringBoot 不再像传统 Spring 需要从外部容器的回掉中启动 应用上下文,而是反过来将容器纳入了应用上下文

#28 spring-mvc请求处理过程

在某一 Controller 方法上加断点,走到断点处,在调试视图中通过调用栈视图可看到 spring-mvc 的入口点为 FrameworkServlet 的 doService,我们可在此加断点,从新来一遍进行分析

图28-1

整体概念

DispatcherServlet 做请求分发

分发是根据 HandlerMapping 获得 Handler 然后封装成 HandlerExecutionChain

HandlerExecutionChain中包含两部分,一部分是标识执行主体的HandlerMethod,另一部分是其拦截器集合,拦截器分别在其执行handle前、后执行。

HandlerMethod 会再封装为 HandlerAdapter 在其中用反射调用真正的 Controller中的方法逻辑

大流程

  • 入口 FrameworkServlet 模板方法 doService

  • DispatcherServlet 实现 doService

org.springframework.web.servlet.DispatcherServlet#doService

  • doDispatch 做请求分发

org.springframework.web.servlet.DispatcherServlet#doDispatch

  • getHandler 获得 HandlerExecutionChain,HandlerExecutionChain是Handler(也就是Controller)和一组HandlerInterceptor的聚合 org.springframework.web.servlet.DispatcherServlet#getHandler 其内部处理流程:
    • 循环 this.handlerMappings 调用每一个 mapping 的 getHandler :org.springframework.web.servlet.HandlerMapping#getHandler 其内部处理流程:
      • org.springframework.web.servlet.handler.AbstractHandlerMapping#getHandlerInternal 获得HandlerMethod(实际上也就是我们写的Controller中各种标记了@RequestMapping的方法包装后的结果)
      • 如果上一步获得的是null那么就获得默认的handler org.springframework.web.servlet.handler.AbstractHandlerMapping#getDefaultHandler
      • 如果上一步默认handler也为null那么直接返回null退出方法
      • 如果 handler instanceof String 那么证明handler是需要通过Bean名字从ApplicationContext里拿,如果是那么就去拿出来
      • org.springframework.web.servlet.handler.AbstractHandlerMapping#getHandlerExecutionChain 获得 HandlerExecutionChain 并返回,其内部处理流程:
        • 如果handler不是HandlerExecutionChain类型的,那么将handler套入new HandlerExecutionChain() 创建成 HandlerExecutionChain
        • 获得Handler的lookupPath,也就是我们自己写的Controller的RequestMapping中的URI
        • 循环所有MappedInterceptor看起是否match到lookupPath,如果match到则此拦截器放到第一步生成的HandlerExecutionChain中然后返回
    • 遇到第一个不为null的HandlerExecutionChain 立刻(跳出循环)返回
  • 将上一步产生的HandlerExecutionChain中的HandlerMethod拿出来当作参数传入getHandlerAdapter获得HandlerAdapter ,HandlerAdapter 是一个SPI,用于外部扩展干预spring-mvc内部流程用的,其内部流程:

    • 循环handlerAdapters找到第一个supports的就返回
  • Process last-modified header 处理 last-modified

  • org.springframework.web.servlet.HandlerExecutionChain#applyPreHandle 进行handle前处理

  • org.springframework.web.servlet.HandlerAdapter#handle 进行真正的Handler的handle(也就是 Controller 中对应的我们自己实现的逻辑)并返回 ModelAndView 其内部处理流程:

    • 调用 checkRequest 校验是否支持当前请求方法类别
    • 将HandlerMethod 封装为 InvocableHandlerMethod 并执行其 invokeAndHandle,在其内部流程中最终会通过反射( java.lang.reflect.Method#invoke)执行HandlerMethod中对应的我们自己定义的Controller中的方法
    • 调用 getModelAndView 完成并返回
  • org.springframework.web.servlet.DispatcherServlet#applyDefaultViewName 为上一步返回的 ModelAndView 应用DefaultViewName 其内部流程:

  • org.springframework.web.servlet.HandlerExecutionChain#applyPostHandle 进行handle后处理

  • org.springframework.web.servlet.DispatcherServlet#processDispatchResult 完成处理

#29 @Bean

@Bean 在 @Componet 和 @Configuration 中语义不同,在 @Configuration 中会被 cglib 增强而在 @Componet 中则不会

图29-1

#30 spring-boot 自动配置实现的重要的点

META-INF/spring.factories

#31 @Controller 注解

主要用于标识当前Bean是否是spring-mvc中的 “Handler” 概念

1
2
3
4
5
6
7
8
9
10
/**
* {@inheritDoc}
* <p>Expects a handler to have either a type-level @{@link Controller}
* annotation or a type-level @{@link RequestMapping} annotation.
*/
@Override
protected boolean isHandler(Class<?> beanType) {
return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}

#32 AbstractHandlerMethodMapping

spring-mvc 注册 Controller与请求映射路径的对应关系的入口在 org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#initHandlerMethods

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Scan beans in the ApplicationContext, detect and register handler methods.
* @see #getCandidateBeanNames()
* @see #processCandidateBean
* @see #handlerMethodsInitialized
*/
protected void initHandlerMethods() {
for (String beanName : getCandidateBeanNames()) {
if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
processCandidateBean(beanName);
}
}
handlerMethodsInitialized(getHandlerMethods());
}

#33 RequestMappingHandlerMapping

RequestMappingHandlerMapping 这个类型的 Bean 是通过 @EnableWebMvc 中的 @Import(DelegatingWebMvcConfiguration.class) 进入容器的

具体的通过:

org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#requestMappingHandlerMapping
注册到容器的

#34 ExecutionChain

Spring mvc 的 Handler (也就是Controller)会被链接上若干拦截器然后包装成 ExecutionChain

#35 原型方式的Bean是否支持依赖注入?

Spring 本身能够支持 prototype 类型 Bean 的依赖注入,无需特殊处理

#36 @Lookup

@Lookup 用于单例引用另一个单例而另一单例引用原型的场景,如不做处理,这种场景每回得到的原型Bean都是同一实例,因为单例引用单例时不会触发另一个单例中原型的重新创建实例操作,@Lookup 内部的解决方法是将被引用的单例也就是@Lookup所在的单例用cglib每次都生成一个子类实例,每次实例不同这样就可触发引用的原型的实例化了

(如果不用Lookup则那个原型不回生成新实例 那么原型的语义就丢失了)

#37 生命周期

关于生命周期:没有容器管理 生命周期只有 new 和 被垃圾回收,有容器管理 容器会给它安排一系列回调比如 PostConstruct

所谓生命周期是容器内部的状态转换,但是一般对外提供回调从而可以进行干预

#38 ServletContextListener

ServletContextListener 是Servlet容器提供的扩展点,spring的ContextLoaderListener实现了这个扩展点,是传统 spring-mvc 的入口点

#39 @Configuration

@Configuration 是通过 ConfigurationClassPostProcessor 这个 BeanFactoryPostProcessor 接入的

图39-1

#40 什么是语法?

在编程中,语法意味着一个调用命令,输入参数去让应用执行程序的文法结构。这些语法被规则或明或暗的约束。程序员遵循语法规范以和计算机交互。如果一段程序语法不正确,计算机将无法识别。这些语法可以自我释义,支持注释。

#41 GC日志释意

图41-1

#42

把自己对于一个话题的扩展理解说出来 而不是局限于具有话题本身

#43

ServletContainerInitializer 是一种 SPI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/**
* Interface which allows a library/runtime to be notified of a web
* application's startup phase and perform any required programmatic
* registration of servlets, filters, and listeners in response to it.
*
* <p>Implementations of this interface may be annotated with
* {@link javax.servlet.annotation.HandlesTypes HandlesTypes}, in order to
* receive (at their {@link #onStartup} method) the Set of application
* classes that implement, extend, or have been annotated with the class
* types specified by the annotation.
*
* <p>If an implementation of this interface does not use <tt>HandlesTypes</tt>
* annotation, or none of the application classes match the ones specified
* by the annotation, the container must pass a <tt>null</tt> Set of classes
* to {@link #onStartup}.
*
* <p>When examining the classes of an application to see if they match
* any of the criteria specified by the <tt>HandlesTypes</tt> annotation
* of a <tt>ServletContainerInitializer</tt>, the container may run into
* classloading problems if any of the application's optional JAR
* files are missing. Because the container is not in a position to decide
* whether these types of classloading failures will prevent
* the application from working correctly, it must ignore them,
* while at the same time providing a configuration option that would
* log them.
*
* <p>Implementations of this interface must be declared by a JAR file
* resource located inside the <tt>META-INF/services</tt> directory and
* named for the fully qualified class name of this interface, and will be
* discovered using the runtime's service provider lookup mechanism
* or a container specific mechanism that is semantically equivalent to
* it. In either case, <tt>ServletContainerInitializer</tt> services from web
* fragment JAR files excluded from an absolute ordering must be ignored,
* and the order in which these services are discovered must follow the
* application's classloading delegation model.
*
* @see javax.servlet.annotation.HandlesTypes
*
* @since Servlet 3.0
*/
public interface ServletContainerInitializer {

/**
* Notifies this <tt>ServletContainerInitializer</tt> of the startup
* of the application represented by the given <tt>ServletContext</tt>.
*
* <p>If this <tt>ServletContainerInitializer</tt> is bundled in a JAR
* file inside the <tt>WEB-INF/lib</tt> directory of an application,
* its <tt>onStartup</tt> method will be invoked only once during the
* startup of the bundling application. If this
* <tt>ServletContainerInitializer</tt> is bundled inside a JAR file
* outside of any <tt>WEB-INF/lib</tt> directory, but still
* discoverable as described above, its <tt>onStartup</tt> method
* will be invoked every time an application is started.
*
* @param c the Set of application classes that extend, implement, or
* have been annotated with the class types specified by the
* {@link javax.servlet.annotation.HandlesTypes HandlesTypes} annotation,
* or <tt>null</tt> if there are no matches, or this
* <tt>ServletContainerInitializer</tt> has not been annotated with
* <tt>HandlesTypes</tt>
*
* @param ctx the <tt>ServletContext</tt> of the web application that
* is being started and in which the classes contained in <tt>c</tt>
* were found
*
* @throws ServletException if an error has occurred
*/
public void onStartup(Set<Class<?>> c, ServletContext ctx)
throws ServletException;
}

#44 JVM内存划分

JVM 分为 堆 Java栈 native栈 方法区 PC

Java栈 中是 帧

桢中包含 本地变量表 异常处理表 操作数栈 常量池指针

调用方法时帧入栈 return或exception时帧出栈

#45 栈上分配

栈上分配:经逃逸分析后发现未逃逸(也就是没有其他引用到的地方)那么会分配到栈上,标量替换 将对象的属性拆出来各自分配到栈 http://blueskykong.com/2019/07/13/kong1/

#46 HashMap

HashMap 获得节点的逻辑 : 入参为 key 的 hash 值和 key 的值
先用 key的hash值 & (桶数组长度 - 1)也就是对于桶长度取模,也就是掩码操作得出key在桶中index由此index拿出元素,若此元素和key的hash值相同并且此元素的key和给定的key相同 那么直接返回此元素

如果不满足上边条件,同时此元素有后继,则一路看其后继的 hash、key是否与传入值相等(这里分为两种情况,如果当前元素为树节点,则走树检索,若不是树节点则走普通的链表检索)

#47 HashMap 是扩容成2倍

HashMap 是扩容成2倍

1
2
3
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold

左移1位等效于 *2

HashMap 扩容时 如果节点是普通的节点 则用新的容量作为掩码从新计算原来元素的的索引
newTab[e.hash & (newCap - 1)] = e;

注意:是不需要计算索引下标,节点的Hash值是不会发生变化的!!!
&运算的定义:两位同时为”1”,结果才为”1”,否则为0
首先,我们先根据下标计算公式得出扩容前后索引的变化

图47-1

根据图片可知,扩容后的21的索引下标比扩容前的索引下标多了一个1,且这个1位于newCap-1的掩码最高位
结论:元素在重新计算hash后,因为n变为2倍,那么n-1的mask范围在高位多1bit,即多了个原容量的距离
优化:无需重新计算Hash,节省了时间,新索引=原索引+原容量

集合番@HashMap一文通(1.8版)

(e.hash & bit) 实际上是为了达到一个布尔值的效果 (e.hash & bit) == 0 相当于 true 否则为 false

图47-2

#48 自学英语

自学英语阶段记录二

#49 ConcurrentHashMap

ConcurrentHashMap 的 update操作 happen-before retrieval操作

More formally, an update operation for a given key bears a
happens-before relation with any (non-null) retrieval for
that key reporting the updated value.

ConcurrentHashMap 的 update操作 happen-before retrieval操作

ConcurrentHashMap 的 size 当没有并非更新时才有用 它返回的是个近似值 或者说是个不能保证完全准确的值

ConcurrentHashMap 不能允许 null 键 HashMap可有有null键

不能为null的一点原因:

Conversely, because keys
and values in the map are never null, null serves as a reliable
atomic indicator of the current lack of any result.

ConcurrentHashMap取消了segment分段锁,而采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。
synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。
JDK1.8的ConcurrentHashMap的结构图如下:

图49-1

 JDK8中的实现也是锁分离的思想,它把锁分的比segment(JDK1.5)更细一些,只要hash不冲突,就不会出现并发获得锁的情况。它首先使用无锁操作CAS插入头结点,如果插入失败,说明已经有别的线程插入头结点了,再次循环进行操作。如果头结点已经存在,则通过synchronized获得头结点锁,进行后续的操作。性能比segment分段锁又再次提升。

也就是乐观的思想:大部分插入由于hash值并不冲突(hash冲突毕竟是少数)因此并不需要加锁,少部分有hash冲突的插入

#50 CAS

Java CAS 通过 unsafe调用底层 cmpxchgl 指令的支持实现 用记数量比对来避免ABA

#51 hash索引

hash索引存储上无序无法应对区间查询 只能用于等值查询 B+树索引存储有序 hash索引不支持复合索引左匹配 B+树索引叶节点可能存储整行数据或主键值

#52 覆盖索引

覆盖索引就是从索引就能直接拿到想要的值不用回表

#53 mysql复合索引

mysql复合索引 前边的字段是定值那就只需要考虑后边的字段顺序就可以了 因为复合索引是将 所有字段 concat 以后排序的 前边是定值就不需要考虑他的顺序了

左匹配原则是 查询条件中包含的字段能从复合索引最左侧开始的字段匹配上 就一直能用到这个索引 直到中间断档了或者是不等值查询了 后边字段就用不到索引了

从某个字段断档或者不是等值查询就意味着无法在树节点上确定前部分的顺序 前部分无序则后边是有序还是无序整体上都是无序的 而无序就无法使用二分查找 因此也就是无法使用索引带来的查询时间复杂度降低了

最左匹配:查询条件中必须存在复合索引中最左边那个字段

全值匹配我最爱,最左前缀要遵守;

带头大哥不能死,中间兄弟不能断;

索引列上少计算,范围之后全失效;

LIKE百分写最右,覆盖索引不写星;

不等空值还有or,索引失效要少用。

mysql索引优化详解

带头大哥不能死并且中间兄弟不能断

abc这个索引 where a= and c= 这种中间少了b 也就是中间兄弟断了不能用索引

where b= and c= 这种没有开头的a 带头大哥死了也不能用索引

#54 UUID

uuid作为主键的话由于uuid任意两个实体间本身无序 在构建索引时回造成页频繁分裂和调整 从而导致插入效率变低

而顺序主键则依次插入 到插入不下时才会页分裂 而由于是顺序插入因此其插入不下的情况较少 因此页分裂不频繁 插入效率高

#55 redolog、undolog

redolog永远灾后重新提交 undolog用于回滚

redolog记录物理页修改 undolog记录回滚逻辑

#56 通过间隙锁解决幻读

行级锁分为 记录锁 间隙锁 临间锁

用间隙锁防止幻读(MVCC也可防止幻读):间隙锁锁定记录范围,这样其他事物无法在当前事务结束前在指定范围内插入新值(因为这个范围被锁定了)

临间锁(next-key lock)= 普通记录锁 + 间隙锁 临间锁即锁定记录前的间隙 又锁定记录后的区间 这样其他事务及不能再当前事务完成前在前之前插新值同时也不能在其后插新值,也就是两边都锁定了

select for update 会在行上加排他锁

#57 ObjectId

时间戳 + 主机名hash + pid + 自增

#58 Redis删除策略

redis使用定期删除+惰性删除(也就是触发式删除)

#59 Redis高性能

redis通过io多路复用实现高性能

#60 io多路复用

io多路复用模块监听多个FD当事件来临回调FD绑定处理器处理

epoll 比 select的优势在于不用遍历fd数组,。

在 epoll_wait 函数返回时会提供一个 epoll_event 数组:其中保存了事件以及发生事件的fd

#61 Redis哨兵

redis通过哨兵模式实现高可用

Redis的哨兵是一种高可用方案,若干互备的哨兵节点负责接收客户端请求,将请求路由到Master,并且在出现Master挂了的情况下进行重新选主操作

图61-1

#62 Redis 分布式锁 setNX

setNX指令可实现分布式锁

Redis分布式锁可能遇到的三个问题:

1.锁超时时间设置的原子性,使用setnx和expire连用来解决

2.如何防止锁被其他进程释放:在setnx时给value设置一个id值,下一次设置时先比对id是否相等,相等才获得锁成功(比对然后设置这是一个典型的check-then-act竞态条件,需要用lua来保证两个操作的原子性和隔离性)

3.如何保证可重入:使用加锁前先判断是否已setnx过,set过就直接获取的办法(具体点用ThreadLocal来存储计数量来实现)

Redis实现分布式锁的可重入可以 ThreadLocal记数的办法,在真正操作setnx前看看ThreadLocal里有没有记数值,有就说明之前已经操作过setnx里就不用在操作了直接返回可获得锁就好了(再操作Redis会报错,因为已经有止里,而setnx是没有值才能set成功)

RedLock算法实现分布式锁:对多个节点setnx 过半成功则为成功

#63 synchronized 锁升级

synchronized 偏向锁 在对象头中用CAS记录线程ID 若有竞争升级为轻量锁 轻量锁自旋获得锁 自旋次数到阈值或又多了一个线程竞争那么升级为重量锁

#64 Kafka

Kafka单分区有序

Kafka将主题分为若干分区,在每个分区内,按时间排序(offset)并保证客户端只有一个线程消费一个分区,这样多个线程消费多个分区,保证消息有序同时还实现了负载均衡

Kafka是用的拉(Pull)模型,由消费者自行记录消费状态 由watermark控制能消费的偏移量的阈值

#65 Xmn

Xmn一般设置为整个堆空间的1/3或1/4

#66 模拟内存分配

可用新建byte数组的方式模拟内存分配

#67 CardTable

CardTable用来记录老年代某个区域是否持有新生代引用,这样新生代在GC时不必遍历老年代查看是否持有引用而只需先看卡表中是否为1,是才需要扫描对应的老年代

卡表里实际上就是存的一堆flag

#68 水平分表

水平分表用字段作为依据分 如取模

#69 spring Bean 生命周期

图69-1

#70 zset

zset计算排行排

#71 mvcc

mvcc 读不加锁 读写不冲突

rc隔离和rr隔离级别通过mvcc来实现写-读或读-写操作之间不用加锁,rc隔离级别会在每次select前生成rearview,rr级别会在第一次select前生成rearview

readview包含当前所有事务id所形成的数组,当聚簇索引行当中trx_id小于这个数组中最小值的时候,说明此条记录已提交,那么对于readview可见,大于则不可见,若大小在这个数组上下界之间并且not in这个数组,则也可见

mvcc会在每次update操做时增加一个undo日志,每个undo日志都有一个roll_pointer指向上一次的undo日志,这样每次update操作的undo日志会串成一个链,称为版本链,ReadView会沿着版本链一种回溯,每个链节上都执行tr_id比对从而确定此链节更新对于ReadView中的事物是否可见

MySQL事务隔离级别和MVCC

#72 MySQL事务隔离级别

脏读:一个事务读到了另一个事务还未提交的数据 见于read uncommited

不可重复读:一个事务中两次相同的查询得到不同的结果(另一个事务在两次读之间提交事务修改了数据) 见于 read commited

幻读:见于 用读来确认是否已存在,不存在才能插入然后再执行插入 的情况 事务1查询是否能插入后事务2完成了插入然后事务1插入时发现已存在了就不能插入了,但实际上事务上一步时得到的结果是能插入的 这样就造成了事物第一步得到的结论是幻像的结果 也就是所谓幻读

幻读:在事务未完成前另一个事务又新增了一些记录

理论上 repeatable read 不能防止幻读 但是?

#73

https://juejin.im/post/5d905039e51d45782d053ca3 极为靠后的分页 大量数据后的分页 可用 定位偏移位置的 id,然后往后查询的办法解决

1
select id from orders_history where type=8 limit 100000,1; select * from orders_history where type=8 and id>=(select id from orders_history where type=8 limit 100000,1) limit 100;

或者从业务上优化 避免掉这种情况

#74 mvcc原理

mvcc :聚簇索引包含两个隐藏列 trx_id 每次对聚簇索引修改都会将事务id赋给trx_id,roll_pointer 每次聚簇索引改动后都会将 undo日志中对应内容的指针赋给roll_pointer

ReadView 包含了当前所有事务的id的列表m_ids 然后如果trx_id 小于m_ids中最小值则说明对应事务在生成ReadView之前已经提交 那么该版本就可以被当前的m_ids对应对事物们可见

如果trx_id大于m_ids最大值 则说明 trx_id对应的事物在 ReadView创建之后 那么m_ids对应的事物们不能访问此版本

如果trx_id大小介于m_ids的最小值和最大值之间同时不与m_ids中任何一个数相等则说明生成ReadView时事物已提交则可以访问

如果大小介于之间并且存在等值则说明此事物还活跃则不可访问

某个trx_id对m_ids不可访问就依次沿版本链找下一版本的trx_id重复以上过程直到找不到trx_id为止

MySQL事务隔离级别和MVCC

#75 CountDownLatch

new CountDownLatch(10) 设置 AQS state 为 10

count() :

  1. 用 CAS 给 state进行 state-1 操作,减完后判断如果为 0 返回true
  2. 如果上一步返回了 true 那么就调用 AQS 的doReleaseShared()流程 也就是唤醒等待队列头节点的流程

在需要等待的线程中await() 这个会调用 AQS 的 doAcquireSharedInterruptibly()
AQS 的 doAcquireSharedInterruptibly() 会判断tryAcquireShared(1) 的返回值,如果大于0就让等待的出队 如果小于0则让其入等待队列队

而 CountDownLatch 的tryAcquireShared(1)返回的是 -1 是小于0的,也就是上一步中会进入等待的的分支,因此当前调用的线程会等待

#76 TCP三次握手

三次握手:
1 客户端 sync
2 服务端 ack + sync
3 客户端 ack

#77 TCP四次挥手

四次挥手:客户端和服务端均可为主动方

1 主动方 fin + ack
2 被动方 ack
3 被动方 fin
4 主动方 ack

主动方最后收到被动方的fin后需要time_wait一下(一般4分种)然后才close

#78 滑动窗口

滑动窗口:
要是一个包一个包的发送-确认 发送-确认 效率太低,因此需要一次性发多个包

缓存大小就是窗口 一次性发出去多个包 收到第一个 ack 就将缓存第一个值赶出去,同时最后一个值后面的值进来一个 一点一点往后滑动着重复这个过程:走一个来一个 走一个来一个。。。 就像是拉链一样 拉链能容纳的拉齿数量就是窗口大小 拉链拉过去就是出一个进一个

如果拉链头那个没收到ack(也就是超时)那么就需要重传,只有他收到ack才能继续往后拉

也就近似类似于拉拉链卡在某个齿了那么就再试一下(超时重传)

一篇带你读懂TCP之“滑动窗口”协议

#79 netty线程模型

netty线程模型:同时支持 Reactor单线程 Reactor多线程 主从Reactor多线程

图79-1

client -> Reactor Thread(内部含有Dispatcher Acceptor接收TCP连接请求 链路建成后通过Dispatcher把ByteBuffer派发到Handler

#80 Reactor 模型

Acceptor相当于店小二招呼客人建立好菜单 然后发给厨师让他们处理菜单上的请求

Reactor单线程:一个店小二一个厨师

Reactor多线程:一个店小二多个厨师

Reactor主从多线程:一个总店小二发派多个店小二,然后店小二们发派多个厨师

#81 netty handler无锁设计

netty handler处理时不切换线程 这样不需要加锁 不存在切换成本 类似于一道菜从头到尾只有一个厨师负责做

#82 粘包拆包

TCP粘包:收到多个包黏在一起

TCP拆包:一个包被分为两部分 一次收到一部分

解决方案:在上层协议栈解决

1 消息定长

2 包尾分隔符

3 分为消息头和消息体 在消息头中包含消息总长度

netty使用编/解码器解决拆包黏包

#83 Linux io模型

Linux io模型:

阻塞io: 进程调用recvfrom后一直等待数据包到达应用进程缓冲区或返回错误后再执行

非阻塞io:recvfrom没数据返回的话之间返回错误让进程继续 然后轮询看看是否有数据到来

io多路复用:select或epoll 进程将fd传给select或poll系统调用阻塞在select/poll上select/poll替进程(也就是不用像普通非阻塞io那样进程自己轮询了 而是让帮手轮询)轮询看看是否有数据来

epoll:比select队列基于事件的处理 不必每次完整轮询一遍 而是有就绪的就会入一个数组 然后回调告知有数据

类比:

阻塞io:客人自己点完菜只能傻等着菜来别的什么也不能做

非阻塞io:客人点完菜需要不断询问厨师做好了没有直到菜来

select:客人点完菜自己干点别的事,店小二不断问厨师菜好了没有,好了就端过来给客人

epoll:客人点完菜自己干点别的事,厨师做好菜了会放到一个固定的取菜窗口并按铃告诉店小二过来拿菜,店小二拿了菜给客人

epoll通过mmap加速了内存从内核空间到用户空间的拷贝

epoll不受fd数量限制

epoll不会因为fd数量增加而效率线性下降

#84 netty零拷贝

netty零拷贝:通过CompositeByteBuf等机制实现数据不需频繁的复制粘贴

#85 一致性Hash

负载均衡时用普通的 hash 算法比如 对集群节点数取模 当集群大小发生变化时,hash需要重新计算,否则原hash可能无法命中

一致性 hash 做法是将 hash 范围定位一个有上限的范围区间

然后将节点进行 hash 到这个区间内

数据的位置也是做 hash 然后看看数据的hash离哪个节点的hash近,就归属于哪个节点(小蝌蚪找妈妈)当某个节点掉线从范围内摘除,其影响到的是hash值比起小的最接近的又比另一个比其小节点大的这一批数据,而不是全部数据,这样就降低了rehash的成本

一致性hash如果节点少的可能造成数据倾斜 解决办法是一个节点生成多个hash 也就是一个节点当作n个节点

#86 mvcc ReadView

RR下,事务在第一个Read操作时,会建立Read View
RC下,事务在每次Read操作时,都会建立Read View

InnoDB,快照读,在RR和RC下有何差异?

可重复读(Repeated Read, RR)
这是InnoDB默认的隔离级别,在RR下:

普通的select使用快照读(snapshot read),这是一种不加锁的一致性读(Consistent Nonlocking Read),底层使用MVCC来实现

4种事务的隔离级别,InnoDB如何巧妙实现?

数据库索引,到底是什么做的?

#87 旁路缓存解决数据库和缓存双写一致性

旁路缓存

Cache Aside Pattern

对于读请求

  • 先读cache,再读db
  • 如果,cache hit,则直接返回数据
  • 如果,cache miss,则访问db,并将数据set回缓存

对于写请求

  • 淘汰缓存,而不是更新缓存
  • 先操作数据库,再淘汰缓存

Cache Aside Pattern为什么建议淘汰缓存,而不是更新缓存?

答:如果更新缓存,在并发写时,可能出现数据不一致。所以,Cache Aside Pattern建议,delete缓存,而不是set缓存。

#88 主线程

是主线程执行main,而不是main创建主线程

#89 线程状态转换

线程状态转换:创建好线程实例还未调用start:NEW

调用start后:Runnable,这里面分为两种情况,被调度器调度到(不可控,但可建议):Running,调用yield:Ready

执行阻塞操作,也就是进入同步队列后:Blocked,出了同步队列并争抢到了锁后:Runnable

调用Object.wait后,也就是释放锁进入等待队列后:Waiting,notify或notifyall,也就是出了等待队列进入同步队列,或被中断后:Runnable

调用wait(long)后,也就是有限期在等待队列中等待:Timed_waiting,等待到了时间(超时)后:Runnable

run正常返回或抛异常后:Terminated

注意:ready和running在Thread.getState的返回中并不存在

图89-1

#90 Thread.sleep

不释放锁

Java中Thread.sleep源码分析

#91

C线程同步

Thread 设计模式1
在程序中有不同的方法使用线程,这里讨论 3 种线程设计模式,没有哪一种模式最好,每种模式都有相应适合的应用场合.

Boss/worker(Thread pool)

图91-1

如上图,一个 Boss 线程创建其他 Worker 线程,并给它们分配任务,必要的话,并等待其他线程运行结束.通常 Boss 线程会在初始建立 Thread Pool 来为之后分配.尽管线程是轻量级的,但是创建它们仍是有开销的.

Peer(Workcrew)

图91-2

Peer 模式又叫做 workcrew 模式,一个 thread 创建其他 peer threads 当程序开始,但是如上图,与 Boss/worker 模式不同,这个 thread 之后也变成 peer thread 去处理自己的任务.

Pipeline

图91-3

Pipeline 模式假定:

一串连续长输入.
每个输入经过一连串的子操作(熟知为 stages 或 fliers).
每个处理 stage 能一次处理个不同的输入.
如上图, Pipeline 就像流水线一般,每个 thread 是一个长链中的一部分.每个 thread 处理由之前 thread 过的数据.

#92 线程同步原语

线程同步原语
如上线程中的定义,线程们共享进程中的全局变量或资源,它们可以并行同时对这些数据和资源操作,如果没有一定的机制协调它们,那么数据或资源将处于一个不安全状态,引起诸如如下的一些问题:

Race condition发生于不能决定行为的结果因为线程们操作共享数据或资源没有遵循一定的同步规则.
ABA problem发生于一个地方被读取两次,都读到相同的值,’值是相同的’被用来说明’没有东西被改变’.但是,另外一个线程能在这两次读取中间执行操作并修改这个位置的值,然后做一些其他操作,最后把这个值改回去,以致愚弄第一个线程让它认为’没有东西被改变’,即使第二个线程的操作已经破坏了这个假设.
所以我们需要如下的一些线程同步原语满足不同的线程间同步需求.

Mutex
Mutex 又被称为 Lock,所以它就像一把 Lock,一个线程 Lock 住一段资源,那么其他线程就不能去访问那段资源,只有等到第一个线程 Unlock 那么资源,它才能访问.

在 Lock 和 Unlock 之间的代码,一般被称为 critical section.

Mutex 也包含一些复杂的类型,如下:

Recursive: 允许占有锁的那一个线程再次获取同样的锁,对递归算法是必要的.
Queuing: 使得 公平 的获取锁,通过 FIFO 排序锁的请求.
Reader/Writer(rwlock): 允许多个 reader 同时获取锁,如果有 reader 占用锁,writer 只有等到 reader 释放锁.
Scoped: RAII 类型定义的锁获取和解锁.
但 Mutex 也会引入其他一些问题,如deadlock 和 priority inversion.

在 Blog 中之前浅谈 Mutex (Lock)中可以看到更多有关 Mutex 的性能和开销分析,并如何实现一个轻量级的 Mutex.

Join
线程 join 机制能让一个线程 join 到另外一个线程中.比如一个子线程 join 回主线程,那么主线程就会等待子线程运行结束.从而达到线程间等待的同步机制.

Condition Variable
Condition variable 允许线程同步到某个共享资源的某个值.

比如,程序有一个计数器,当计数器达到某一个值时去激活某个线程运行.把计数器当成一个 Condition variable.这个线程可以等待这个 Condition variable,其他 active 线程操作完这个 Condition variable,可以通过 signal/broadcast 去唤醒那些等待这个 Condition variable 睡眠的线程.

Barrier
Barrier 是一种能让一系列线程在某个点得到同步的方法,通过让参与 barrier 的线程等待直到所有参与线程都调用了这个 barrier 函数.本质上就是,阻塞所有参与 barrier 的线程直到最慢的那个参与线程调用 barrier.

Spinlock
Spinlock 与 mutex 类似,是种锁,但当获取锁失败时,spinlock 不会让线程进入睡眠,而是不断 poll 去获取这个锁直到获取成功.更多Mutex 与 Spinlock 的区别.

Semaphore
当某些资源具有多个时,简单的 Mutex 不能满足,引入 Semphore,Semphore 可以根据资源个数初始化为任意值.当线程们占有所有资源,使得 Semphore 为 0,那么其他线程再获取资源只有等待.当 Semphore 值只能是 1 或 0 时,它相当于简单的 Mutex.

#93 ArrayList的remove

ArrayList在自身的remove中会检测modCount和expectdModCount是否相等,不想等会抛异常,自身的remove并未进行调整使其相等,因此会抛出异常,而iterator的remove则会进行调整使两个值相等,因此不会异常

#94 ConcurrentHashMap

ConcurrentHashMap:在内部维持一个 segments 每个 segment 中是 HashEntry 形成的数组,也就是table 在加锁时,各自segment持有自己各自的锁,在操作同一segment时才会出现同步,操作不同segment由于锁不同,因此不存在同步,当segment分的足够细的时候,同步就很少了

1.8的ConcurrentHashMap放弃了segments的设计,使用CAS和synchronized来实现并发更新的控制,结构与HashMap相同,都是table里每个元素为链表或树。

在初始化table时通过循环CAS设置一个状态量sizeCtl是否成功,来分成两个执行路径,成功设置了sizeCtl的线程有权初始化table,而未成功的则Thread.yield来让出被调度到的权利,从而保证保证只有一个线程在进行初始化table的操作

当进行更新操作(put)时:当插入元素时发现table对应元素为null时,也就是没产生hash冲突,那么就使用循环CAS尝试在此位置插入,如果发现已有元素了,也就是已经hash冲突了,那么就用synchronized对插入进行同步

由于ConcurrentHashMap中value不允许null值存在,因此在get中利用了这一点,在获得value时首先尝试不加锁读,如果发现获得的值为null那么就说明发生了冲排序,则就升级为加锁读

ConcurrentHashMap的每一个segment中有一个记录此segment的table里包含多少HashEntry的属性count,而不是在整个ConcurrentHashMap中记录了count,这样避免了对这个频繁更新值进行全局加锁

探索 ConcurrentHashMap 高并发性的实现机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
V get(Object key, int hash) { 
if(count != 0) { // 首先读 count 变量
HashEntry<K,V> e = getFirst(hash);
while(e != null) {
if(e.hash == hash && key.equals(e.key)) {
V v = e.value;
if(v != null)
return v;
// 如果读到 value 域为 null,说明发生了重排序,加锁后重新读取
return readValueUnderLock(e);
}
e = e.next;
}
}
return null;
}

#95 MySQL主从同步

mysql主从同步:主端生成binlog 从端io线程将binlog复制为relaylog然后另一个线程读出relaylog并执行

解决主从同步延迟:开启并行复制;分库;延迟查(业务上给写完和可读直接留一些时间);直接强制从主库查

#96 线程池

线程池:当提交任务时如果当前任务数小于corPoolSize那么不管核心线程们是否空闲,一定会创建线程并执行任务,如果当前线程数大于corePoolSize了,那么再来任务就尝试入队,如果队列满了或因为其他原因入队失败并且当前线程数小于maxPoolSize则创建非核心线程,如果已经大于maxPoolSize了则走拒绝策略,拒绝策略包括直接抛异常、放弃当前入队任务、放弃等待最久任务、调用者线程执行任务

#97 spring-boot自动配置实现

spring-boot通过@EnableAutoConfiguration实现自动配置:具体的,在@EnableAutoConfiguration中@Import了EnableAutoConfigurationImportSelector然后其使用SpringFactoryLoader根据spring.properties中的配置反射实例化指定的标注了@Configuration的配置类,然后加载到IoC容器

#98 G1 收集可能有4个阶段

G1 收集可能有4个阶段:

  • 新生代收集
  • 并发标记周期
  • 混合收集
  • 如果需要进行FullGC

G1的并发标记周期:

  • 初始标记,初始标记必然伴随一次新生代收集 STW
  • 根区域扫描,根区域扫描会由survivor区可达的老年代区 并记录这些可达对象
  • 并发标记 标记堆中所有存活对象
  • 重新标记 STW
  • 独占清理:计算各区 存活对象和GC回收比 然后进行排序 同时还会更新记忆集 Remembered Set(记忆集)

以上整个并发标记周期后 会标示出垃圾比例高的区,这些区会被放入 Collection Sets

混合回收阶段:

并发标记周期中标记出来的垃圾比例高的区会被回收,混合回收会阶段也会进行新生代收集,注意 被清理的区中的存活对象会被复制到其他区以减少内存碎片,

混合GC 并发标记周期 年轻代GC 会循环进行

图98-1

图98-2

如果混合GC发生时空间不足或者新生代GC时surviver区和老年代区无法容纳幸存对象 都会导致FullGC

#99 CMS的步骤

CMS的步骤:

  • 初始标记(标记根对象)STW
  • 并发标记(标记所有对象)
  • 预清理(可降低停顿时间)
  • 重新标记(修正并发标记阶段的标记)STW
  • 并发清理
  • 并发重置

为了保证能和工作线程并发,CMS不会等到快要耗尽才触发 而是留较大剩余空间时就触发(G1也是同理),默认68%时触发 可调,CMS可配置为若干次回收后进行一次内存压缩以解决内存碎片

#100 内在逻辑共性

了解原理可用帮助理解技术的内在逻辑共性:比如CopyOnWriteArrayList 内部实现机制类比与MySQL的快照度

Redis的zset和Jdk的ConcurrentSkipListMap 内部都利用了 SkipList 来实现

SkipList使用在数据上(链表实现)添加若干层索引的方式来达到排序二叉树的同等效果

还可继续发散 将特定技术领域的内在逻辑与其他领域的内在逻辑建立共性,比如生活常识:

厕所排队和同步队列

餐馆点菜和io多路复用

#101 什么是线程安全

什么是线程安全:可以先考虑什么是线程不安全,它的反面就是线程安全

某逻辑在竞态条件下由于线程调度的不确定性导致了结果的不可预测以及不确定性就是线程不安全

#102 竞态条件

竞态条件:

  • check-then-act
  • 读-改-写

可以用 加锁、循环CAS或写时复制 来确保可预测

#103 任务的抽象

Runnable 只是任务的抽象,而线程则是执行的抽象,是两个不同层面的概念 不可混为一谈

#104 netty的线程模型

netty的线程模型:事件循环

一个事件循环(EventLoop)将永远只有某一个线程驱动 不会发生线程切换

一个事件循环可能服务多个Channel(Channel是io部分的抽象)

也就是说一个事件循环可以服务多个io 但是就某一个io来说其只会被一个事件循环服务

类似于办事窗口:某个窗口只有一个工作人员,但是有多个顾客 一般一个事分为多分步骤 办事员每次处理一个人的一个步骤,当这个步骤需要顾客填写资料时这个顾客去一边填写,此时办事员给下一个顾客办理,原来那个去填写资料的顾客自东排到这个办事队列的尾部 对于某个顾客,从其开始办事到结束只允许对应一个办事员,而在办事员看来,他是依次循环着服务多个客户

同时,一个办事大厅里有多个办事员,也有多个办事顾客队列

netty分为io处理部分和执行处理部分 也就是io模型(具体的是io多路复用)和线程模型(事件循环)或者说是事件循环组

netty线程模型之所以设计成某个客户自始至终对应一个办事员这样是为了消除上下文切换的成本

类似于你办一个事,一个办事员办了A步骤,假如你去另一个办事员那,那个办事员就需要你告诉他现在办倒那个步骤了,然后需要提交一堆材料,你每次换人都需要记住自己办到哪个步骤了,还得把材料拿着到处跑,假如自始至终是一个人,那么你只需第一次把材料给他,然后他会记住你办到哪个步骤了,不需要额外记忆,也不需要拿着材料来回跑

#105 netty的组件

netty的组件:io处理组件和执行组件

io处理组件:Channel 代表某一次io,类似于jdbc中的Connection的概念;在Channel的生命周期中会有很多事件产生(事件是NIO 中Selector逻辑的高层次封装)这些事件交由ChannelHandler处理,ChannelHandler是编程人员用来干预Channel的入口

执行组件:事件循环,一个Channel整个生命周期一直由同一个事件循环来执行

EventLoop是对于Thread的高级抽象,一个EventLoop整个生命周期只对应一个线程 一对一

EventLoopGroup对应于线程池

编解码器:用于数据的格式处理,比如转换,界定长度等(Socket的数据皆是二进制数据)

Bootstrap:整个服务器抽象或客户端抽象的入口

ByteBuf是对于数据自身的封装

Handler:链上对应的任务

HandlerContext:链的某一环

HandlerPipeline:用于将链穿起来的工厂

FileRegion 实现零拷贝

#106 Selector

elector是NIO中最关键的API 他是事件模型的 用事件API来通知那些套接字可以已就绪可进行操作

Selector的使用方式:

  1. 打开一个Selector
  2. 将Channel设置为非阻塞模式
  3. 将Channel注册到Selector
  4. while(true)轮询select
    4.1 如果select()返回0则进行下一轮
    4.2 如果返回不是0则从Selector获得SelectorKey集合然后迭代这个集合,判断(四种)感兴趣的事件是否存在,并坐业务处理;处理完成后需要将处理过的当前SelectorKey从迭代器中移除,然后进入下轮迭代

Channel对应于Socket

#107 netty使用引用计数

netty使用引用计数来处理资源释放

#108 布隆过滤器

布隆过滤器大致原理:每一个key计算多个hash 再由每个计算出来的hash确定一个在桶中的index然后在相应index位置标记为1

在判断给定key是否存在于其中时,重复上面计算index的过程看看得到的index位是否都是1只有有一个不是1就不存在

实际上,即便都是1也有可能不存在,因为有可能恰好有另一个完全不同的key算出来的index和给定的key算出来的index完全重合

就像是两组完全不同的双色球组合却都中了一等奖一样,这种可能是存在的但是概率较低,因为只一个index碰上了比较容易,但是同时多个一块碰上就不容了

#109 HyperLogLog

HyperLogLog用于粗略记数

#110 Paxos协议

Paxos协议简单理解:
两种角色:提案评审者 提案发布者

过程:分为两个阶段

第一阶段:提案预提交。多个提案发布者共同发布提案给多个评审者组成的评审委员会,评审委员会根据某个规则给这些提案进行编号,然后共同常试告知编号最大提案对应的提案发布者可以正式提交提案了

第二阶段:正式提交提案。提出最大编号的那个提案提交者收到半数以上的评审委员会委员返回“你可以正式提交提案了”的批复,则正式提交自己的提案

注意,在第一阶段和第二阶段进行的过程中,不不阻碍新的一轮第一阶段的发生

因此,接着说第二阶段:评审委员会接到提案者的正式提案后,先要看一下,现在有没有新的编号更大的提案,如果没有,则通过此正式提案,如果有,则不通过

zookeeper 入门系列 : paxos 协议

#111 ZooKeeper所有数据都在内存中

ZooKeeper所有数据都在内存中,结构为树状结构名为ZNode Tree,每个斜杠分割中的都对应一个ZNode

ZNode分为两种:一种是持久节点(创建后除非主动删除否则一直存在)另一种是临时节点,客户端会话结束会销毁

#112 ZAB协议

ZooKeeper使用ZAB协议

ZAB协议:ZooKeeper原子广播协议

可以大致分为两种情况的处理:正常情况和异常情况

两个角色:主(Leader)和从(Follower)

Leader只能存在一个,Follower可以多个,正常情况下只能由Leader接收事务请求(如果Follower接收了也需要转交给Leader)

Leader接收了事务请求(写请求)后,类似于Paxos协议,也需要经过两个阶段:第一阶段预提议:Leader将事务提交封装为提案交由各个Follower 等有过半的Follower返回通过预提案了就进入第二阶段:Leader告诉所有Follower真正提交事务

异常情况:当Leader挂掉或不存在半数Follower能与Leader正常通信就进入异常情况的处理。

异常情况处理过程:首先ZAB协议需要保证异常情况出现之前那些在正常情况下进入第二阶段的事务能被所有Follower提交,或者,如果第二阶段明确指明需要忽略这些事务的提交的话则所有Fellower都要忽略事务的提交

为了满足上述要求,ZAB协议会选出持有最大事务编号(在正常情况中Leader会为每一个事务编一个事务编号,也就是提案编号,所有提案按编号排序进行提案)的Follower为新的Leader

然后新的Leader会让所有Follower完成之前未完成的事务的提交或者回退事务。当提交或回退完成后整个系统又会回归到正常状态

#113 ZooKeeper实现分布式锁

ZooKeeper实现分布式锁:在某一节点下常识创建(create)一个临时节点,在创建时,多个客户端同时创建只能有一个成功(这就模拟了共同争抢锁只能有一个成功的场景)

其他未成功的,在锁对应节点的父节点上注册对于锁节点的Watcher,用来监听锁对应节点的变化(比如节点销毁),锁的释放:正常释放 锁使用者使用完后主动删除锁节点,这个逻辑可类比于synchronized内部的同步队列机制和显式锁Lock内部AQS的同步队列机制,逻辑是相通的,可互相借鉴

锁的释放:

  • 正常释放 锁使用者使用完后主动删除锁节点
  • 异常释放:获得了锁的客户端出了异常,那么会导致和ZooKeeper会话的结束,然后临时节点会自行销毁

#114 零拷贝

零拷贝:免除了 内核读缓冲到用户缓冲、用户缓冲到socket缓冲、socket缓冲到网卡的拷贝过程

直接从磁盘中读到内核读缓冲,然后再由内核读缓冲到网卡

#115 StringBuilder 为什么线程不安全

简单的说就是 append 中存在 读-改-写 竞态条件

StringBuilder 为什么线程不安全?

#116 CAP

CAP:当网络发生分区时,有可能需要牺牲可用性来保全一致性,比如全体服务不可写;或者牺牲一致性(强一致)来保全可用性,比如Redis的主从同步不保证强一致同步,当某一节点挂掉然后重启后,并不会阻断其他同步的进行,此节点再恢复后数据已经不一致了,但是节点会尝试重新让数据恢复一致(也就是做最终一致)

#117 Redis内存回收

Redis内存回收:近似LRU,给每个key加一个最后一次访问时间戳,当内存超过阈值时,随机选取5个key,删掉时间戳最靠前的(最旧的),如果内存还超阈值,则重复一次上述过程

#118 Radix Sort

【图解数据结构】 一组动画彻底理解基数排序

#119 happens-before

  1. 同一个线程中的每个Action都happens-before于出现在其后的任何一
    个Action。
  2. 对一个监视器的解锁happens-before于每一个后续对同一个监视器的
    加锁。
  3. 对volatile字段的写入操作happens-before于每一个后续的同一个字段 的读操作。
  4. Thread.start()的调用会happens-before于启动线程里面的动作。
  5. Thread中的所有动作都happens-before于其他线程检查到此线程结束
    戒者Thread.join()中返回戒者Thread.isAlive()==false。
  6. 一个线程A调用另一个另一个线程B的interrupt()都happens-before 于线程A发现B被A中断(B抛出异常戒者A检测到B的isInterrupted() 戒者interrupted())。
    那么A动作happens-before于C动作。 http://www.blogjava.net/xylz/archive/2010/07/03/325168.html
  7. 一个对象构造函数的结束happens-before与该对象的finalizer的开始
  8. 如果A动作happens-before于B动作,而B动作happens-before与C动作,

#120 ConcurrentHashMap在1.7和1.8的不同实现

谈谈ConcurrentHashMap在1.7和1.8的不同实现

#121 到底什么是线程

到底什么是线程?又是怎么从start到run?

#122 限流算法

漏桶算法:可以想象为买煎饼果子,煎饼果子摊处理能力有限,人多了直接拍队,老板基本上保持恒定的速度出煎饼,出一个走一个

令牌桶算法:(guava的RateLimiter就是一种实现)可以想象成看电影买票,有发票窗口专门安排出票,出票速率可调,排队不直接拍在影厅,而是拍在售票处,然后影厅门口会有检票员,拿到票的才能进

普通的计数法限流无法限制峰值聚集的情况,也就是说如果设定10秒内不能超过10个,那恶意者完全可以利用这样的漏洞:两个10秒,在第一个10秒的最后一秒一次性走10个,然后立刻在第二个10秒的第一秒一次性走10个,这样峰值仍然可能压垮系统

时间窗口算法:将时间段分为更小的切片,然后没过切片时间整个窗口就滑动一个切片的距离,这样假如遇到两个峰值聚集在一起时,也就是聚集在两个临近的分片中时,在整个窗口看来已经超过限定了,这样就起到了限流的目的

Spring Boot的接口限流应用

#123 线程池

小问题大智慧 :线程池是怎样工作的

#124 循环提升

将多次循环中的操作提取到循环外部的一次性操作

图124-1

图124-2

循环提升前提是假定了无竞争,可能影响到多线程执行的语义

volatile可阻止JIT进行循环提升

#125 volatile可见性保证

volatile可见性保证读取到当前相对新值 而非最新值,因为volatile并不阻塞其他线程的写操作 有可能读到后其他线程就该了该值 因此依旧会得不到预期结果

原子性保证能够产生相对新值,而不保证能读到,因此 原子性+可见性才能保证并发语义

volatile不保证互斥 读改写场景中 读后不保证其他线程不能改,也就是做不到隔离 或者说完全的原子性

图125-1

volatile 不仅仅对被修饰的变量起作用,还会对被修饰变量之前的(源代码顺序)的变量产生作用

volatile写操作前的任何读 写操作都会确保被先提交

图125-2

图125-3

如果volatile修饰数组变量,那么它只对数组变量本身起作用,对数组内元素并不起作用,如果volatile修饰引用类型的变量,那么它只对变量本身起作用,对变量对应的引用类型实例内部的属性并不起作用

#126 join() 可见性保证

join()之前的更改对于join()之后的操作可见

#127 貌似串行语义

貌似串行语义 as-if-serial 只保证单线程程序冲排序后结果和未超排序一致而不保证多线程下的

#128 线程安全

多线程保证线程安全需要满足的:原子性(完全的原子性还要包含互斥,volatile的原子性不包含互斥并不完整) 可见性 有序性

原子性 保证不产生中间状态同时不交错

#129 start() 可见性保证

start()之前的写操作对之后启动的线程可见

#130 临界区

临界区:加锁和释放锁之间的执行区

#131 ConcurrentHashMap 锁的粒度细

1.7的ConcurrentHashMap 相当于快递柜 各锁各的

HashTable 相当于库房的锁,一把锁锁所有

#132 锁句柄要用final修饰

锁句柄要用final修饰,因为如果是可变的就会造成锁在了不同的锁上导致语义丢失

#133 入口集

被从入口集(也就是同步队列)中唤醒的线程不一定就必然能获得到锁,因为他有可能要与没在入口集中的线程争抢锁

#134 用于线程操作的一些工具方法

Thread#holdsLock(Object) 可检测当前线程是否持有指定内部锁

ReentrantLock#isLocked() Queries if this lock is held by any thread.

ReentrantLock#getQueueLength() 可以检查等待锁的线程数量(同步队列长度?)

#135 读写锁

读写锁是有单独接口的

读写锁:写读、读写、写写 互斥而 读读共享

读写锁支持获得了写锁后在释放写锁前获得读锁或者说是将锁转换为读锁,也就是锁降级

读写锁内并不是有两个锁 而是一个锁有读锁和写锁两种角色 并可支持写锁转换为读锁角色的操作

图135-1

#136 synchronized

synchronized在虚拟机层面实现是插入了MonitorEnterMonitorExit 这两个字节码指令

而到了机器码层面后会在MonitorEnter对应的机器码指令前插入LoadBarrier(加载屏障 作用是将内存中数据同步到缓存,也就是刷新缓存)而在MonitorExit对应的机器指令后边会插入StoreBarrier(存储屏障 作用是将缓存中数据同步到主存也就是flush

#137 类初始化是惰性的

类初始化是懒惰型的,类加载后静态变量全是默认值,只有被第一个线程访问静态变量时才会执行 static 关键字保证线程能够看到 后的静态变量值 这一点不需加锁即可保证

#138 final 可见性保证

final关键字保证其修饰的变量能够完成初始化,也就是线程能够看到最终的值

如果其修饰的是引用类型那么能保证该引用类型完全初始化完毕 也就是线程看到的引用类型是完全初始化完毕的

图138-1

图138-2

#139 CAS 与可见性

CAS不保证可见性

#140 CAS 怎么实现

CAS通过CPU指令实现

#141 两类内存屏障

加载屏障和存储屏障用于保证可见性也就是内存和缓存的同步

获取屏障和释放屏障用于保证有序性 也就是禁止重排序

加载屏障的作用是在屏障处强行将内存值同步到缓存而存储屏障则是在其位置强制触发缓存值同步到内存

获取屏障需要插在某个读操作后,获取屏障用于禁止此处的读操作和其后的任意写操作冲排序而释放屏障需要插在某个写操作前,释放屏障用于禁止当前位置写操作和其之前的任何读操作进行冲排序

#142 透彻理解Java并发编程

透彻理解Java并发编程

#143 B+树多个字段复合索引

复合索引用 concat 确定顺序

An in-depth look at Database Indexing

#144 jvisualvm

jvisualvm可做线程运行时间等待时间的采样分析

图144-1

#145

wait和notify内部怎样实现?在JVM内部有入口集和等待集 想当于JDK中Lock的同步队列和等待队列

#146

wait方法在线程真正等待后并不会返回,直到线程被唤醒后获得到了锁从等待集中删除掉后才会返回也就说wait会跨越一个很长的周期

图146-1

图146-2

#147 CyclicBarrier 和 CountdownLatch 对比

CyclicBarrier 多个线程都准备好了再一块做某事

除了最后一个到达的线程外 其他线程调用CyclicBarrier#await都会变为WAITING 最后一个调用await的会先执行barieAction然后调用signalAll来唤醒之前所有线程开始干活

CountdownLatch 是一来了就干活 然后有一个监工等着这些人都干完活了验收,CyclicBarrier是r一来了,如果最后一个人没到就先等着,最后一个人到了,他负责通知大家开始干活,并且在通知之前可选择自己先干些额外的事

#148 如何结束线程

waiting的线程可被中断唤醒,需要终止线程时,可以用一个boolean属性作为标识,用于退出run方法,同时还需要配合Thread.interrupt 保证已经等待的线程能被唤醒(若不唤醒的话,run里即便有控制退出run的逻辑也会因为线程处于等待状态而无法运行到)

#149 同步、异步,阻塞、非阻塞

同步和异步是从返回结果这个视角来看的,同步意味着调用后收到的结果就是想要的结果而异步是调用后不会收到想要的结果而是结果由通知机制另行通知

阻塞和非阻塞是从执行的视角来看的,阻塞意味着调用者发起调用后线程需要一直等待,不能执行别的任务,直到返回结果。或者发起调用后先收到一个确认(而非结果)然后在被通知真正结果之前要线程一直等待,不能执行别的任务

#150 用枚举做单例好吗?

不好,干扰语义,应极力避免。

推荐 InstanceHolder 机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Singleton {

private static class InstanceHolder {

/**
* <cinit> 由 JVM 内部的锁保证线程安全,并且是懒加载,只有第一个线程用到时才会触发执行
*/
private static Singleton instance = new Singleton();
}

private Singleton() {
}

public static Singleton getInstance() {
return InstanceHolder.instance;
}
}

#151 ThreadLocalMap 的 key 是 WeakReference

ThreadLocalMap 的 Entry 中 key是WeakReference value是普通引用(强引用)弱引用在没有普通引用引用的情况下会被下次垃圾回收清除也就是key置为null,而value不会被立刻清除,他会在下次执行put时被清除(懒清除)如果线程长期不处于运行状态,一直不调用put,那么一直触发不了 value的清除就可能会造成内存泄漏

tomcat 每个应用都有独立的WebAppLoader加载,而各个应用的执行是共享了一套线程池的,也就意味着ThreadLocal也是在多个应用间共享的,当一个应用停止后,其WebAppLoader加载的类(以及静态字段等)元信息并不会卸载(当然tomcat内部的泄漏检测时可以处理这个情况的)也就是没有把ThreadLocal对应的静态变量卸妆(一般ThreadLocal都会定义为静态变量,要是定义为实例变量的画会导致ThreadLocal引用反复创建,语义是不对的),那么就意味着有强引用引用了TreadLocalMap的key 那么就意味着不会被回收,这样就有可能内存泄漏

#152 装饰器模式下生成的同步集合的迭代器需要额外加锁才能保证线程安全

装饰器模式下生成的同步集合(Collections.synchronizedMap等)的迭代器(iterator)需要额外加锁才能保证线程安全,而且这个锁需要选取同步集合实例作为锁对象(目的是保证和内部的写操作同步在同一个锁上)

这个实际上违背了最少知识原则

#153 双重检查锁定

双重检查锁定如果实例不加volatile可能存在 一个线程将对象实例赋值到引用先于 new 对象实例发生(也就说编译器汇编指令冲排序)导致另一个线程判断对象不为null成立直接返回了未填充值的全是初始值的对象

#154 Java内存模式是硬件内存模型的高层抽象

图154-1

图154-2

#155 flip()

图155-1

ByteBuffer 有两种模式:写模式和读模式。在写模式下调用 flip() 之后,ByteBuffer 从写模式变成读模式。

那么 limit 就设置成了 position 当前的值(即当前写了多少数据),position 会被置为 0,以表示读操作从缓存的头开始读,mark 置为-1。

也就是说调用 flip() 之后,读/写指针 position 指到缓冲区头部,并且设置了最多只能读出之前写入的数据长度(而不是整个缓存的容量大小)。

#156 TCP 长连接

TCP 长连接简单的讲就是应用层实现一个心跳机制来维持 TCP 连接,不能指望 TCP 内置的 keep alive

手把手教你写 Socket 长连接

#157 happens-before

happens-before:有两组操作 每组操作包含若干字操作,每个字操作会产生子结果。A{a1,a2,a3} B{b1,b2,b3} 如果 A happens-before B 那么,从可见性来看,A的所有字操作产生的字结果对于B都是可见的,从有序性来看,从B的角度来观察A中所有操作都是按程序顺序执行的,尽管实际上A的各个字操作可能存在冲排序,但是对于B来说它认定A内部就像是没有冲排序一样

happens-before规则又可分为若干细则:

  • 程序顺序规则
  • 内部锁规则
  • volatile变量规则
  • 线程启动规则
  • 线程终止规则

图157-1

happens-before 是一套标准,它只提出目标而不关心怎么实现,具体实现是由 JVM(包含JIT)来操作的,JVM会通过一系列手段(比如在合适的位置插入内存屏障)来确保自己能够达到目标

DLC单例中给实例变量加volatile后就利用了happens-before规则中的volatile变量细则

图157-2

instance = new Instance() 这个volatile变量写操作 happens-before 了 if(null == instance) 这个volatile变量的读操作 因此JVM保证(通过禁止冲排序)在后面的读操作读到的一定是写操作执行完的(也就是new完了然后再赋值完的)这样就不会出现读到一个没有完成构造的里面都是初始值的对象这样的情况了

#158 Java内存布局

Java内存布局中存在8字节对齐规则,对其意味着有填充,而填充是在浪费空间的,为了减少填充发生的频率,JVM不会按照程序顺序存储对象中的共享变量,而是按照能够优化存储空间的顺序来存放

这就像小学教室里排座位,老师不会因为某两个学生关系好而把他们排成连坐,而是按照个体大小排因为他的诉求是更高效的利用空间

也就是一个Context有一个Context的规则

#159 伪共享

伪共享 不同的实例的共享变量被安排在了同一个缓存行中,当一个变量应用保证线程安全的手段导致其所在的缓存行失效会牵连到同一缓存行中的其他人

解决办法,使用@Contented等填充手段在某个共享变量后边填充上数据从而让另一个共享变量无法再放入这个缓存行

也就是一个变量独占缓存行

#160 NIO

NIO中的主要概念:Channel和Buffer,Channel是和操作系统层面打交道的 类似于厨房的水池和水管,水管和底层公共下水系统(操作系统)相连接,而Buffer则连在Channel上层 或者类似于抽水机水管和蓄水池,抽水机水管和底层地下室系统打交道,而蓄水池则连接在抽水机水管上层

Buffer是与上层系统(应用代码)进行交换的门径

FileChannel可以从其他Channel转移数据到自己

Selector和FileChannel不能联合用,因为FileChannel不支持非阻塞模式

图160-1

#161 一次CPU 100%

观察 GC 情况,1小时发生了 40 次 FullGC ,并且看GC曲线,每次GC后内存并未下降,同时 jstack 可看到 CPU 全跑在 VM THREAD 这个线程上,因此可大致认定,CPU 100% 是频繁而无效得 FullGC 导致的

图161-1

上图中呈上升趋势的是有异常的docker实例,呈下降趋势的是正常的docker实例

#162 操作系统内置的计算器可轻松处理程序上的计算

图162-1

#163 Servlet

servlet也是一种扩展点

图163-1

图163-2

#164

处理连接的部分 处理调用的部分 处理执行的部分 分为三大模块 大模块底下再分小模块 模块和模块之间进行交互

借鉴 tomcat 的设计风格

transfer组件和invoke组件

transfer组件下包含序列化组件 invoke组件下包含执行器组件

图164-1

#165 innodb 缓冲池

innodb 通过缓冲池(内存中)来弥补磁盘的速度不足,在从缓冲池向磁盘刷入时,并不是每一次页变更都刷盘,而是到了check point才进行,类似游戏中存档点

缓冲池占很大的内存区一般以G计,其页大小为16k

缓冲池内存管理使用带有可配置中间点(midpoint)的改良型LRU算法

中间点之前的是冷数据,之后的是热数据,插入时插在中间点,中间点在整个内存区中的位置是可调的,也就是可事先预估一下冷热数据的比例,依据此比例调整中间点

之所以要在中间点插入而非头插入,是为了防止将热数据挤掉

innodb_old_blocks_pct 参数用于调整中间点位置,或者说是调整冷热数据比例,例如设置为30意思就是预估冷数据占大约30%热数据大约70%

#166 redo log

redo log buffer会每秒(可配置)刷到redo log 因此此buffer的大小配置只要保证大于每秒的所有事物产生的redo log大小的总和就可

重做日志使用专门的内存缓冲区,与缓冲池没在一起

日志先行 缓冲池不是每次有脏页就会刷盘的,因此如果先进行改页的话有可能存在数据不一致的风险,所有为了防止,innodb先会写redo log然后再改缓冲池中的页

redo log buffer比缓冲池窑小很多 以M计,默认8M

redo log是innodb存储引擎专属的文件

redo log记录的是页的更改属于物理型日志

redo log先入redo log buffer 然后按照一定顺序写入redo log文件(ib_logfile1、ib_logfile2)redo log 文件会滚动使用

redo log从redo log buffer写入文件时一次写一个磁盘扇区的大小(512字节)因此可以保证一定能写成功,因此不用double write

#167 insert buffer

innodb 中有 insert buffer:这个 buffer和缓冲池并不是一回事,这个buffer是一个存在于共享表空间中的B+树,是一个磁盘结构而不是内存结构,它的作用是用于将小的多次DML操作合并为一次大的DML操作(相当于一个草稿箱)
在 insert buffer 中的数据会在进行select 二级索引时或其他一些条件下合并到真正的二级索引的B+树里去

#168 double write

innodb 使用 double write 机制来防止partial page write的情况发生(partial page write会导致无法通过redo log正确恢复)

再次写的大致流程:对脏页进行刷新时,不直接写盘,而是通过 memcpy 将脏页复制到doublewrite buffer(内存中)然后分两次,每次1M顺序写到共享表空间的磁盘中,然后立刻fsync同步刷盘

图168-1

这个其实就是同时向共享表空间磁盘和自身表空间的磁盘中刷了盘 写了两次 所以叫双写

图168-2

#169 自适应 hash 索引

innodb 存储引擎支持给热点数据建立hash索引(自适应hash索引)

#170 binlog

binlog 只记录数据修改的执行(注意,如果执行并为导致数据真正改了,也是回记的)

binlog可以选择格式,有 statement、row、mixed三种,默认是statement,statement记录的是语句级的逻辑日志,row格式记录的是每个行的更改是物理型的记录

binlog的文件形式不是文本形式的必须用mysqlbinlog这个工具才能查看到,直接cat是看不了的

binlog并不是存储引擎层面上的

binlog在事务提交前提交(写盘),一次事务之对应一条binlog,而一个事务中会多次提交redo log

#171 innodb 逻辑存储结构

innodb逻辑存储结构:表空间 段 区 页 行

图171-1

#172 innodb 页目录

innodb的剧集索引的叶子节点中 行数据也是形成双向链表的,在磁盘中并非顺序排放的

图172-1

图172-2

#173 PrintTenuringDistribution

-XX:+PrintTenuringDistribution 可用于观察在每次新生代GC后幸存区中年龄分布

#174 GC吞吐量和暂停时间

GC吞吐量和暂停时间:吞吐量指的是用户线程工作时间占总工作时间(用户线程工作时间 + GC线程工作时间)的比例

要想增加吞吐量就需要减少GC触发的次数,然而GC次数少了就意味着每一次GC的时间需要延长才能完成GC目标,也就是说暂停时间会变长

也就是说吞吐量指标和暂停时间指标是互斥的

图174-1

#175 Using index

Using index 代表索引覆盖

Using index condition 代表使用了 Index Condition Pushdown 优化

图175-1

图175-2

图175-3

#176 容器启动和Bean实例化

Spring两个主要流程:容器启动和Bean实例化

容器启动会通过收集分析外部配置(xml或注解)构造Bean的元信息(BeanDefinition)

注册到元信息注册表(BeanDefinitionRegistry)

#177 innodb 意向锁

innodb意向锁:innodb支持多粒度锁定,在一个事务相对细粒度对象(行)加锁时需要先在此对象上级的粗粒度对象(表)上成功加上意向锁后才能进行,此时如果上级粗粒度对象上已经有了粗粒度的锁,并且和该意向锁不兼容,则意向锁加锁失败,从而连带细粒度加锁也失败,通过这种机制就支持了粗粒度锁(表锁)和细粒度锁(行锁)的共同兼容性判断

#178 快照读

repitable read 级别下 非锁定一致性读是读取事务开始时对应的那个快照,而 read commited 级别下是读取事务结束前最后一个快照

#179 innodb 锁

innodb的锁(lock)是针对事务的一种结构,在事务结束后就释放了,不针对线程,innodb里针对线程的结构叫latch

#180 一致性锁定读

一致性锁定读:select for update 和 select lock in share mode

一致性锁定读加锁并不影响一致性非锁定读(也就是普通select)因为一致性非锁定读本来就是在事务加锁情况下进行并发读取的机制

#181 innodb会给外键自动加索引

在有外键的情况下,在子表中插入值需要用一致性锁定读的方式来查父表,以确保数据一致

也就是说父表如果正在执行带有X锁的事物时,想往子表里插入数据的事物会被父表的事物的锁阻塞,这样就避免了可能的数据不一致

图181-1

图181-2

#182 幻读

幻读是在一个事务中进行了两次(或以上)查询得到的结果不一样的情况

当一个事务进行两次查询的空档之中,另一个事务进行了DML,如果在事务的第二次查询执行时,另一个事务没有提交,当前事务就能读到DML修改后的结果,则称之为脏读。如果在事务的第二次查询执行时,另一个事务已经提交,当前事务能读到DML修改后的结果,则称之为幻读(或不可重复读)

#183 死锁发生时,会立刻回滚

当检测到死锁发生时,会立刻回滚,不需程序员干预,也就是当看到 1213 错误时,意味着已经回滚了

图183-1

#184 事务

事务是从一个一致状态转换为另一个一致状态的过程

#185 事务的提交会触发 redo log 的刷盘

事务的提交会触发 redo log 的刷盘,也就是执行 fsync,而fsync是一个高开销动作,因此在执行大批量DML时,最好最后一起commit 而不是每一次都commit

图185-1

图185-2

#186 undo log 没有单独的文件

undo log 没有单独的文件,而是存在于共享表空间中的一个段中,叫 undo 段

由于undo log中内容是undo逻辑,也就是undo sql,这些 undo 逻辑同样也需要持久性保证,因此写入undo log的同时,同样伴随着 redo log 的产生

#187 show full processlist

show full processlist 显示所有线程信息

#188 快照备份

mysql自身不支持快照备份,但是可通过文件系统的快照功能来变相实现 比如 Linux中的LVM

图188-1

图188-2

mysql的replication并不能用于备份需要,因为DDL误操作同样会复制到从库

想要借由复制实现备份可以在从库所在的文件系统上做快照

图188-3

#189 性能观测工具

性能调优观测工具:
top
vmstat
iostat
pidstat

#190 jcmd

jcmd是一个综合工具,可替代 jmap jinfo jstack jstat

#191 直接内存

直接内存的上限由 -XX:MaxDirectMemorySize来指定,若未指定则与-Xmx设置的值相同

直接内存到达MaxDirectMemorySize后会进行回收,或者用显式的full gc System.gc()也可触发回收

#192 MAT 深堆

MAT中深堆指的是某对象的浅堆 + 次对象所有专属关联对象浅堆大小

图192-1

图192-2

#193 concurrent mode failure

CMS的concurrent mode failure 意思是年轻代晋升入老年代过快,已经快于老年代回收的速度,从而导致老年代没有足够空间容纳晋升的对象,从而触发老年代碎片整理(Full GC)

图193-1

#194 System.gc()

图194-1

System.gc() 在gc日志中带有与普通full gc不同的标示

#195 kafka

kafka 的 broker server 组成集群,消息用 topic 区分,每个 topic 进入集群后会 分区记录partition

消息有 key value 和 ts

消息写成某个分区后会附加 offset 各个分区的 offset 相互独立

图195-1

Kafka 消费时由消费者自行记录消费状态,也就是使用了 拉 模型,消费者能拉取的最大限度由 watermark 控制,记录消费状态也就是记录 offset,Kafka不会在消费过后就删除,而是定期删除

消息有 key 时 相同key的消息会被发达同一分区 没有 key 则会轮训

分区日志会分布式存储在集群中,并且每个分区日志会有一主多从 只有主分区日志才能写 从只能读,主挂了要从新选主

Kafka的某节点能与zk保持会话,并且与主节点复制的进度相差不多则此节点则为 in-sync

每个主节点会跟踪所有 in-sync 的从节点集合(也叫 ISR)如果某节点的复制落后太多则会被移除出ISR 如果重新赶上进度则会加回ISR

只有已提交的消息才能被客户端发现并消费,这样就保证了数据一致

#196 年终总结

1.体现工作量:属于没有功劳有苦劳类型,一年到头确实做了很多事情,把主要工作罗列出来(不要太细)。比如天天疯狂加班的,年终奖就该体现体现
2.体现技术能力:5年内的,以技术硬实力为主。通过一些CASE体现自己的能力,例如:钻研新技术并应用、性能优化、技术设计等等,能力可以体现在工资涨幅上
3.体现综合能力:管理能力、沟通能力、成本管控、客户关系等等。如果是基层管理岗,可以体现自己带的团队的业绩,团队人员的成长情况;体现自己管理的项目的各项指标等。这些有助于职场通道上升
4.体现学习能力与技术兴趣爱好:通过自己业务的一些CASE来展现自己
5.体现自己对于所属部门业务的思考,建言献策
6.有自己明确的职业规划,展示出自己的成长性

公司为员工的过去及未来付费

#197 文档的写法

文档类别:原理介绍;操作手册

操作手册主要给出操作步骤 1234,类似于大疆机器人的组装手册,告诉你一个基本的标准组装步骤,按照这个步骤就能组装好

#198 redis 持久化

redis 持久化有两种方式:rdb(全量)和 aof(增量)

rdb是将整个内存做快照写盘,容易丢失大量命令的执行结果

aof(append only file)只将新执行的命令结果进行刷盘(也是定期刷,默认周期1s)aof默认配置下最多只丢1s的命令结果

rdb恢复快,aof恢复慢,4.0之后可使用rdb + aof 混合持久化,这样既保证数据全而且保证恢复快

redis与mysql的数据一致方案:删缓存+删数据+延迟二次删缓存+新写入的key设置自动过期

es的索引文档以段的形式存储在磁盘,段是不可变的

段还没写到磁盘前(内存中)只有写权限没有读权限(也就是不进入检索结果),而在写到磁盘完成后会加上一个提交点,此时段就只有读权限没有写权限了

由于段是不可变的,因此对于端的修改操作会变成删除和新增的复合操作

段不可变设计的优势:缓存友好、可压缩、无锁化

es并不会实时fsync而是先写入文件系统缓存然后定期以分片为单位进行刷新(默认1s)

在这期间有可能操作系统宕机,为了防止数据不一致,还会写到Translog一份,用于重启时的恢复

elastic search 中文档相当于 mysql的行的概念

elastic search搜索结果默认按 _score 降序排序

脚本字段中若用 _source 则内存消耗低但速度慢 若用 doc 则内存消耗高但速度快

es的查询分为两个阶段:发散和收集

发散:接到搜索请求的节点将请求发散到所有分片,然后等待所有分片完成搜索并返回数据后,接受节点再将这些结果收集并排序

收集:再发一次请求,这次只会发到持有所需文档的分片中,然后收集并建立响应然后返回客户端

#200 tenuring threshold 会动态改变

tenuring threshold 会动态改变:如果当次gc后survivor区已经不足,则jvm会降低tenuring threshold,这样下次会有一部分对象晋升至老年代从而释放幸存区空间

#201 新生代一般需要设置的比老年代小

新生代一般需要设置的比老年代小,老年代一般设置为占整个堆的3/4 新生代占1/4

交互式应用适合用低暂停时间gc

#202 cms

cms的6个阶段中有2个阶段会导致stop the world

cms要做到和业务线程并发就不能等到堆完全满了以后再启动回收,堆满了应用线程是无法运作的,cms会在运行时收集统计信息,自行决定堆占用到多少百分比后会开启cms过程

首次cms需要手工指定堆占用百分比触发cms条件 也就是 -XX:CMSInitiatingOcupancyFraction ,注意这个参数只影响第一次CMS过程,之后的CMS过程会使用基于统计信息的占比触发条件;如果想每次cms都基于手工指定的占比触发则可启用-XX:+UseCMSInitiatingOccupancyOnly这个参数(不建议这样做)

#203 类加载是一个递归的过程

一个类被加载时,起引用的类也会被加载,这是一个递归的过程

#204 SHOW EXTENDED

SHOW EXTENDED 可以查看mysql在后台自动创建的隐式索引

当我们创建一个次级索引的时候,mysql 会自动在后台自动创建一个此次级索引的包含的所有列以及主键咧组成的复合索引

比如 创建了 index on name 则会后台创建index on (name,id)