提交 7ae36af3 authored 作者: 黄夏豪's avatar 黄夏豪

新增了新的笔记

上级 cb73b3a0
......@@ -47,7 +47,7 @@ public class SnowFake {
* @Author : HuangXiahao
* @Date : 2020/5/22 15:31
*/
public static long nextId(){
public static synchronized long nextId(){
//获取当前时间戳
long l = System.currentTimeMillis();
if (lastTimestamp==l){
......@@ -99,4 +99,11 @@ public class SnowFake {
}
}
```
\ No newline at end of file
```
注意点:
1. 雪花算法需要保障生成的ID无重复,以及单调递增。在多线程的情况下需要通过加锁的方式保障雪花算法的这一特性
2. 雪花算法依赖服务器的时钟保证无重复性,因此假若服务器出现时钟回拨的情况会导致算法出现重复。在编写雪花算法的时候需要考虑到这一点
3. 时钟回拨可以通过在生成ID进行判断当前时间是否小于上一次生成的时间,并在出现这种情况时阻塞并等待时钟进入下一秒解决。
也可以通过环形数组解决问题。
4. 通过环形数组解决时钟回拨问题会造成生成出的ID时间位无实际意义的情况
\ No newline at end of file
## 记录一次Java多线程下while进入死循环的问题
##### 起因
在对雪花算法进行测试时使用了以下代码计数
```java
ExecutorService executorService = Executors.newFixedThreadPool(threadNum);
SnowFlake snowFake = new SnowFlake(sequenceLength);
long start = System.currentTimeMillis();
for (int i = 0; i < threadNum; i++) {
executorService.execute(new SnowFlakThread(snowFake, num / threadNum));
}
//暂定主线程直到num个雪花ID生成完毕
while (snowFake.getAtomicLong() != num) {
}
System.out.println(snowFake.getAtomicLong());
```
在上面的代码中,通过获取snowFake的AtomicLong值判断SnowFake生成Id的数量。当生成的ID数与目标数值相同时,代表线程已经运行结束。
在SnowFake对象中,AtomicLong这个变量使用了,AtomicLong 原子类实现。
由于使用原子类的开销大于基础类型,在后面的测试中尝试中将原子类修改为基础类型,则发现了while循环无法退出的情况。
#### 例子
```java
public class test {
static int num = 100_000_000;
static Object o = new Object();
static int flag = 0;
public static void main(String[] args) throws FileNotFoundException {
new Thread(){ //线程1
@Override
public void run() {
for (int i = 0; i <num ; i++) {
flag++;
}
System.out.println("flag已经和num相等了");
}
}.start();
int currentNum = flag;
while (currentNum!=num){
currentNum = flag;
}
System.out.println("线程运行结束了");
}
}
```
在这段代码中,即使线程1已经运行完了,但是主线程已然不会结束。
以下是运行结果
```
flag已经和num相等了
```
可以看到 while循环下面的那句打印并不会随着线程1的执行完毕而执行。
#### 解决方案
1. ```java
while (currentNum!=num){
Thread.sleep(1);
currentNum = flag;
}
```
2. ```java
while (currentNum!=num){
System.out.println(num);
currentNum = flag;
}
```
3. ```java
volatile static int flag = 0; //将flag该为volatile
```
#### 原因
先看下面的一个图
![Java内存模型图](./images/img14.png)
在Java中每一个线程在运行的时候都具有其独立的工作内存,线程在运行时并不是实时的从主内存中获取数据。而是先从主内存中将数据获取到工作内存中,之后从工作内存中使用该数据。
也就说各线程之间对变量的操作是互相不可见的。
当while循环去读取 num 的时候,这个num实际上是主线程工作内存中的num,并非是thread1向主存中写入的num ,也就造成了 num 数值不对的情况,导致循环不会退出。
解决方案1通过对线程的阻塞,使得线程有充足的时间去主内存中刷新工作内存的数据,从而使flag 变成正确的值,以让线程退出。
解决方案2,由于println()方法中带有 synchronized 代码块 ,从而是主线程和thread1的数据进行同步。
解决方案3,通过 volatile 保证了变量的可见性,使得主线程能够及时的看到 thread1 对 flag 变量做的操作
## 一段诡异的死循环
```java
public class Main {
static boolean run = true;
static volatile int other = 1;
public static void main(String[] args) {
new Thread(() -> { //Thread1
int loop = 0;
while (run) {
loop=1;
//如果注释掉这一行,会是死循环;如果加上,在循环就会正常退出
// other = 1; //①
}
System.out.println("loop=" + loop);
}).start();
new Thread(()->{ //Thread2
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
run = false;
System.out.println("set run = false");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end");
}).start();
}
```
在这段代码中,被作为循环条件的 run 并没有被 volatile 修饰,但如果 将 code① 取消注释后,这段死循环依然能够正常。
在解释这段代码之前先看下面这张图
![Git合并示例图](./images/img15.png)
#### Strore Buffer
Cpu每次写数据时,需要通知其他Cpu将该变量 标记为 Invalidate 并且在接收到其他的Cpu回复的 Invalidate Ack 后才能 将数据写入主存。但是这个等待时间可能会很长,所以这里的等待时间通过 Store Buffer 进行了优化。Cpu在写数据时将数据写入 StoreBuffer中,等StoreBuffer接收到其他线程的 Invalidate Ack 后 ,由StoreBuffer 将数据写入主存中。
#### Invalidate Queue
每个Cpu上的StoreBuffer都是有限的。当StoreBuffer被写满之后,后续写入就必须等StoreBuffer有位置后才能再写。就导致了性能问题。所以需要缩短写入请求在StoreBuffer的排队时间,之前提到StoreBuffer存在原因就是等待 Incalidate Ack 可能需要较长的时间,那么只要缩短Incalidate Ack 回复的时间就能够缩短 StoreBuffer的排队时间。
于是解决方法就是为每个Cpu再等价一个Incalidate Queue。收到Incalidate 请求后将请求放入 队列中,并立即回复Ack
## 内存屏障
### 内存屏障的类型
Jvm提供了四种类型的屏障
1)LoadLoad屏障:操作序列Load1,LoadLoad Load2,用于保证访问Load2的读取操作一定不能重拍到Load1之前。在执行完Load之后需要先处理Incalidate Queue 后再读Load2
(2)StoreStore屏障:操作序列 Store1,StoreStore,Store2,用于保证Store1及其之后写出数据一定先于Store2写出,即别的Cpu一定先看到Store1的数据,再看到Store2的数据。可能会有一次StoreBuffer的刷写,也有可能通过所有写操作都放入StoreBuffer排序来保证
(3)LoadStore屏障:操作序列Load1,LoadStore,Store2,用于保证Store2及其之后写出的数据被其它CPU看到之前,Load1读取的数据一定先读入缓存。
(4)StoreLoad屏障:操作序列Store1,StoreLoad,Load2,用于保证Store1写除的数据被其他CPU看到后才能读取Load2的数据到缓存中。
## 原因
到这里就能得出上面这段代码中,other能够使循环退出的原因了。
![Java内存模型图](./images/img16.png)
......@@ -41,10 +41,33 @@
| 2 | 100_000_000 | 多 | 10 | 14 | 8241000 | 12133 | 8241 | 57% |
![折线图](./images/snowFlak-Linechart1.png)
在不同的线程数量下,算法对CPU的使用率不同。
### 结论
1. 通过观察在相同位数下多线程和单线程的QPS能够发现,在多线程下QPS低于单线程。(12位序列号和13位序列号由于没有达到CPU计算瓶颈无法看出明显变化主要观察14位序列号的情况下QPS的变化)
2. 序列号位数越大则QPS更大,对CPU的占用率也越高
3. 对于第一点的解释,由于雪花算法存在计算量大,其中无IO,网络请求等需要对线程进行阻塞并等待的操作,因此单个线程对算法进行运行,节约了其中各个线程之间切换的时间。
# 如何决定线程池的大小
1. 线程池的大小能够影响程序的性能,线程池过大会导致程序在运行时CPU在各个线程之间频繁切换,导致浪费资源。线程池过小则会导致程序在等待时间内CPU处于空闲状态。
2. 线程池的设置不宜过大也不宜过小,但也不需要太过精确。
3. 线程池大小计算公式
**Ncpu *Ucpu(1+W/C)**
NCpu 计算机CPU核心数
Ucpu 期望对CPU的使用率 0 ≤ UCPU ≤ 1
W 等待时间
C 计算速率
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论