提交 c44be931 authored 作者: xuyang's avatar xuyang

threadQuestions

上级 7ae36af3
package api.thread.memory_barrier;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* 测试多线程数据不一致问题
*/
class ThreadDemo implements Runnable {
//Solution1: volatile
/*private volatile boolean flag = false;*/
private boolean flag = false; //flag初始化为false
//Solution6: CAS
/*private AtomicBoolean flag = new AtomicBoolean(false);*/
@Override
public void run() { //将flag设置为true
flag = true;
/*flag.getAndSet(true);*/
System.out.println("flag=" + flag);
}
public boolean isFlag() {
return flag;
}
/*public AtomicBoolean isFlag() {
return flag;
}*/
}
/**
* main
*/
class Main {
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
new Thread(threadDemo).start(); //启动线程threadDemo,线程中设置flag为true
//Solution3: lock
/*Lock lock = new ReentrantLock();
lock.lock();*/
while (true) {
//Solution4: sleep()
/*try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
//Solution5: sout
/*System.out.println("output here");*/
//Solution2: synchronized
/*synchronized (threadDemo) {*/
if (threadDemo.isFlag()) { //主线程中判断flag为true/false
/*if (threadDemo.isFlag().get()) {*/
System.out.println("----------------");
break;
}
/*}*/
}
/*lock.unlock();*/
}
}
\ No newline at end of file
### **1. 数据库自增长序列或字段**
最常见的方式。利用数据库,全数据库唯一。
最常见的方式。利用数据库,全数据库唯一。
优点:
**· 优点:**
1)简单,代码方便,性能可以接受
​ 1)使用INT自增,消耗空间小,性能好
2)数字ID天然排序,对分页或者需要排序的结果很有帮助
​ 2)数字ID天然排序,便于需要分页和排序的场合
缺点:
**· 缺点:**
1)不同数据库语法和实现不同,数据库迁移的时候或多数据库版本支持的时候需要处理。
1)不同数据库语法和实现不同,数据库迁移的时候或多数据库版本支持的时候需要处理。
2)在单个数据库或读写分离或一主多从的情况下,只有一个主库可以生成。有单点故障的风险。
2)在单个数据库或读写分离或一主多从的情况下,只有一个主库可以生成。有单点故障的风险。
3)在性能达不到要求的情况下,比较难于扩展。
3)在性能达不到要求的情况下,比较难于扩展。
4)如果遇见多个系统需要合并或者涉及到数据迁移会相当痛苦
​ 4)如果多个系统需要合并或者涉及到数据迁移,可能会主键重复
5)分表分库的时候会有麻烦
​ 5)分表分库的时候,也可能造成主键重复
优化方案:
**· 优化:**
1)针对主库单点,如果有多个Master库,则每个Master库设置的起始数字不一样,步长一样,可以是Master的个数。比如:Master1 生成的是 1,4,7,10,Master2生成的是2,5,8,11 Master3生成的是 3,6,9,12。这样就可以有效生成集群中的唯一ID,也可以大大降低ID生成数据库操作的负载。
​ 1)针对主库单点,如果有多个Master库,则每个Master库设置的起始数字不一样,步长一样,可以是 Master的个数。比如:Master1 生成的是 1,4,7,10,Master2生成的是2,5,8,11,Master3生成的是 3,6,9,12。这样就可以有效生成集群中的唯一ID,也可以大大降低ID生成数据库操作的负载。
### **2. UUID**
常见的方式。可以利用数据库也可以利用程序生成,一般来说全球唯一
​ 常见的方式。可以利用数据库也可以利用程序生成
优点:
**· 优点:**
1)简单,代码方便
​ 1)可以很简单地使用代码生成
2)生成ID性能非常好,基本不会有性能问题。
2)生成ID性能非常好,基本不会有性能问题。
3)全球唯一,在遇见数据迁移,系统数据合并,或者数据库变更等情况下,可以从容应对
​ 3)保证在全球范围的唯一性,适用于数据迁移,系统数据合并,或者数据库变更等情况下
缺点:
**· 缺点:**
1)没有排序,无法保证趋势递增。
1)没有排序,无法保证趋势递增。
2)UUID往往是使用字符串存储,查询的效率比较低。
2)UUID往往是使用字符串存储,查询的效率比较低。
3)存储空间比较大,如果是海量数据库,就需要考虑存储量的问题。
3)存储空间比较大,如果是海量数据库,就需要考虑存储量的问题。
4)传输数据量大
​ 4)传输数据量大。
5)不可读
​ 5)可读性很差
### **3.snowflake算法 **
snowflake是Twitter开源的分布式ID生成算法。
snowflake是Twitter开源的分布式ID生成算法。
**· 优点:**
1)不依赖于数据库,灵活方便,且性能优于数据库。
1)不依赖于数据库,灵活方便,且性能优于数据库。
2)ID按照时间在单机上是递增的。
2)ID按照时间在单机上是递增的。
**· 缺点:**
1)在单机上是递增的,但是由于涉及到分布式环境,每台机器上的时钟不可能完全同步,也许有时候也会出现不是全局递增的情况。
​ 1)在单机上是递增的,但是由于涉及到分布式环境,每台机器上的时钟不可能完全同步,也许有时候也会 出现不是全局递增的情况。
2)存在时间回退问题,时间回退后,生成的ID就有可能重复。
2)存在时间回退问题,时间回退后,生成的ID就有可能重复。
**· ID结构:**
结果是一个long型的ID,分成四个部分:
​ 结果是一个long型的ID,分成四个部分:
​ 0(符号位)— 000···000(41位表示毫秒数)— 000···000(10位表示机器的ID)— 000···000(12位 作为序列号)
**· 实现方法(生成一个ID的流程):**
​ 1)设置一个初始对照时间戳*lastTime*,默认为0
​ 2)获取当前时间戳*currentTime**lastTime*比较,假如在同一毫秒内,序列号+1;假如 *currentTime*>*lastTime*,重置序列号为0;假如*currentTime*<*lastTime*,说明时间倒退应该报错。
​ 3)若序列号达到最大值(12位全为1),则会触发等待直到下一毫秒,并将下一毫秒的时间戳作为 *currentTime*
​ 4)将*lastTime*更新为*currentTime*作为对照时间戳
· **优化(时间回溯):**
​ 1)可以使用RingBuffer环形数组,预先生成一批ID,放在数组中。当低于阈值时触发数据填充。填充的时 候时间戳为当前的时间戳。
​ 2)可以缩短ID中时间占用的位数,增加序列号或者机器号的位数。
​ 3)单线程情况下QPS较多线程更高。
### **4.关于多线程中出现的一些问题 **
```
//出现的问题
public class ThreadDemo implements Runnable {
private boolean flag = false; //flag初始化为false
@Override
public void run() { //将flag设置为true
flag = true;
System.out.println("flag=" + flag);
}
public boolean isFlag() {
return flag;
}
}
public class Main {
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
new Thread(threadDemo).start(); //启动线程threadDemo,线程中设置flag为true
while (true) {
if(threadDemo.isFlag()) { //主线程中判断flag为true/false
System.out.println("----------------");
break;
}
}
}
}
//启动main方法后,只有输出flag=true,但是程序并没有停止。
```
**· 多线程数据共享的流程图:**
![image-20200618141052118](upload\image-20200618141052118.png)
​ · JVM会为每个线程分配独立的工作内存(缓存)。
​ · 当一个线程线程需要读取数据时,会先去缓存中获取数据,若缓存中没有需要的数据,则会从主存中获取数据再加载到缓存中。
​ · 当一个线程有对数据进行修改后,会先刷新缓存中的数据,再经过缓存刷新主存中的数据。
​ · 当主存数据更新时,会及时刷新各个缓存中对应的值。
​ · 在案例中线程threadDemo修改flag为true,刷新了自身缓存与主存中的flag值,然而main线程在while循环下CPU占用过高,高频读取自身缓存中flag值,导致缓存中的flag值不能及时从主存中刷新。
**· 对于共享数据不一致的解决措施:**
​ 1)共享变量使用volatile关键字,利用其可见性和禁止重排的特性(CPU为了提高效率可能会重排)
​ 2)使用synchronized锁
​ 3)使用lock锁
​ 4)main方法中调用Thread.sleep()方法
​ 5)在while(true)中System.out.print输出,利用其内部Syncronized锁
0(符号位)— 000···000(41位表示毫秒数)— 000···000(10位表示机器的ID)— 000···000(12位作为序列号)
​ 6)CAS保证原子性
**· 原理:**
​ ……
​ ①设置一个初始对照时间戳*lastTime*,默认为0
**· 关于volatile的tips**
②获取当前时间戳*currentTime**lastTime*比较,假如在同一毫秒内,序列号+1;假如 *currentTime*>*lastTime*,重置序列号为0;假如*currentTime*<*lastTime*,说明时间倒退应该报错
1)针对含有volatile修饰的参数的指令,禁止该指令与之前和之后的读和写指令重排序
③若序列号达到最大值(12位全为1),则会触发等待直到下一毫秒,并将下一毫秒的时间戳作为*currentTime*
2)把写缓冲区中的所有数据刷新到内存中。进行写入操作时,会在写后面加上一条store指令,将本地内 存中的共享变量值立即刷新到主存。(即内存屏障)
④将*lastTime*更新为*currentTime*作为对照时间戳
3)使其他处理器里的缓存行无效。在进行读操作时,会在读前面加上一条load指令,从主存中读取变量。
· **关于如何解决时间回退问题:**
​ 4)无法保证原子性。例如volatile int a = 0; a++; 其中a++不是原子操作,仍可能会发生重排序。
可以使用RingBuffer环形数组,预先生成一批ID,放在数组中。当低于阈值时触发数据填充。填充的时候时间戳为当前的时间戳。
\ No newline at end of file
\ No newline at end of file
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论