提交 390bca00 authored 作者: 杨雪's avatar 杨雪

上传新文件

上级 5fe29cdb
 随着分布式系统的发展,常见的通过主键自增等生成id的方式已经不能满足需求,成为被时代抛弃的弃儿。
为了解决在分布式系统中产生全局唯一、有序的主键号这一难题,Twitter想到了Snowflake算法。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200524231749760.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dpc2VfeWFuZw==,size_16,color_FFFFFF,t_70)
- 1bit 不可用 因为它是符号位,ID一定为正数,所以它的值为0;
- 41bit 时间戳,可以精确到毫秒级,41bit能使用69年。它存储的不是当前时间的时间戳,而是**时间戳差值**=(当前时间截 - 开始时间截)。因为它的存在,实现了**ID有序性。**
**系统的时钟错误也是雪花算法的一个致命问题,所以要一定要保证服务器的系统时间正确。**
- 10bit 机器标识,这个可以细分:前5bit属于 数据中心ID;后5bit属于机器ID。
- 12bit 自增序列号 前面的时间和机器数字如果都一样的话,用序列的区别来生成不同的id。2^12=4095,意味着每个节点每毫秒(同一机器,同一时间截)产生(从0——4095)这4096个ID序号。
1+41+10+12=64,刚好构成一个64bit,Long类型。
```
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
```
这里的方法表达式是移位算法,可以帮助我们很快计算出几位二进制所对应的最大十进制数。
```
import java.util.HashSet;
import java.util.Set;
public class SnowFlow {
/**
* 开始时间截(这个可以自己使用已经流逝的时间,我用的是自己写代码时的实际时间
*/
private final long startStamp = 1590368867576L;
/**
* 机器id占5bit
*/
private final long workerIdBits = 5L;
/**
* 数据标识id占5bit
*/
private final long datacenterIdBits = 5L;
/**
* 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
*/
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
/**
* 支持的最大数据标识id,结果是31
*/
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
/**
* 序列在id中占的位数
*/
private final long sequenceBits = 12L;
/**
* 机器ID向左移12位
*/
private final long workerIdLeft = sequenceBits;
/**
* 数据标识id向左移17位(12+5)
*/
private final long datacenterIdLeft = sequenceBits + workerIdBits;
/**
* 时间截向左移22位(5+5+12)
*/
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
/**
* 生成序列(0-4095),总共4096个
*/
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
/**
* 工作机器ID(0-31)
*/
private long workerId;
/**
* 数据中心ID(0-31)
*/
private long datacenterId;
/**
* 毫秒内序列(0-4095)
*/
private long sequence = 0L;
/**
* 上次生成ID的时间截
*/
private long lastTimestamp = 0L;
/*
* 构造函数(工作机器id,数据中心id)
* */
public SnowFlow(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
/*
* 得到新的id
* */
public synchronized long newId() {
long timestamp = System.currentTimeMillis();
//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (timestamp < lastTimestamp) {
throw new RuntimeException(
String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
//如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
//毫秒内序列溢出
if (sequence == 0) {
//阻塞到下一个毫秒,获得新的时间戳
timestamp = NextMillis(lastTimestamp);
}
}
//时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
//上次生成ID的时间截
lastTimestamp = timestamp;
//移位并通过或运算拼到一起组成64位的ID
return ((timestamp - startStamp) << timestampLeftShift) //时间戳部分
| (datacenterId << datacenterIdLeft) //数据中心部分
| (workerId << workerIdLeft) //机器标识部分
| sequence; //序列号部分
}
/*
* 如果阻塞到下一毫秒,获取新的时间戳
* */
protected long NextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
public static void main(String[] args) {
SnowFlow snowFlow = new SnowFlow(0, 0);
Set ids = new HashSet();
long start = System.currentTimeMillis();
int i = 0;
while (i <= 1000000) {
i++;
ids.add(snowFlow.newId());
System.out.println(snowFlow.newId());
}
long end = System.currentTimeMillis();
System.out.println("总共生成"+ids.size()+"个ID,花费时间为"+(end - start)+"毫秒");
}
}
```
异或运算
相同取0,不同取1
> 1 ^ 1 = 0
> 0 ^ 0 = 0
>
> 1 ^ 0 = 1
> 0 ^ 1 = 1
另外雪花算法,也有很多种的升级方式:改变序列号的位数、多线程等方式。
单线程:
序号位数为12位时:31%
序号位数为13位时:31%
序号位数为14位时:31%
序号位数为15位时:85%
序号位数为16位时:98%
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200619090729850.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dpc2VfeWFuZw==,size_16,color_FFFFFF,t_70)
多线程:
5个线程:
序号位数为12位时:75%
序号位数为16位时:98%
4个线程
序号位数为12位时:78%
序号位数为16位时:95%
3个线程
序号位数为12位时:74%
序号位数为16位时:95%
2个线程
序号位数为12位时:67%
序号位数为16位时:88%
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200619090821421.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dpc2VfeWFuZw==,size_16,color_FFFFFF,t_70)
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200619090842690.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dpc2VfeWFuZw==,size_16,color_FFFFFF,t_70)
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200619090912239.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dpc2VfeWFuZw==,size_16,color_FFFFFF,t_70)
# 内存模型
## 1.为什么会有内存模型?
再介绍Java内存模型之前,先介绍一下计算机内存模型。
计算机执行程序的过程,指令是在CPU中进行的,而CPU执行指令的过程中肯定会涉及到数据的操作,数据就保存在主存中,即计算机的物理内存。
随着CPU技术的发展,它的处理速度越来越快,但是内存发展相对迟缓,从内存中读取和写入数据的速度相对较慢,CPU每次操作内存都要浪费很多等待时间。师傅,这该怎么办呢?
你看,总不能说CPU发展得太快了,不让CPU发展了吧。也不能让内存读写速度慢拉后腿吧。
> 所以,聪明的人就想到一个聪明的方法,就是在**内存和CPU之间增加一个高速缓存**。缓存具有速度快、内存小的特点,程序将需要的数据从主存中拷贝一份到高速缓存中,运行过程中直接从缓存中读写数据,当运行结束的时候再将缓冲中的数据刷新到主存中。
因为CPU的发展特别迅猛,一层缓存逐渐地不能满足需求,所以就出现了多级缓存。
根据数据读取顺序和与CPU结合的紧密程度,缓存就划分为一级缓存(L1),二级缓存(L2),甚至有些更厉害的CPU会有三级缓存(L3)。此时,如果CPU需要操作某个数据时,首先会去一级缓存中查找,如果没有找到就会去二级缓存中查找,以此类推,优先去缓存中查找,如果缓存中都没有找到最后就去内存中查找。
> 单核CPU只有一套多级缓存
> 多核CPU:
> 对只有两级缓存来说,每个核心都有一套一级缓存,共享二级缓存。
> 对含有三级缓存来说,每个核心都有一套一级和二级缓存,共享三级缓存。
随着缓存个数的增加,如果线程个数也随之增加,会出现什么情况呢?
- **单线程**:这个线程会独占缓存,比较安全。
- **多线程单核CPU**:因为单核CPU只有一套多级缓存,单核情况下只能执行一个线程,线程操作完数据会保存在缓冲中,当其他线程执行时,会从同一个缓冲中操作这个数据,因此不会出现访问冲突。
- **多线程多核CPU**:因为多核CPU每个核心都有一套多级缓存,如果多线程是在不同核心中执行的话,多核是可以并行。当不同核心的线程们同时对某个共享数据进行读写操作,然后将操作后的结果保存在各自的缓冲中,这样就有可能出现这个共享数据在不同缓冲中的值是不同的。
*综上可见,在CPU和内存之间增加缓冲,会导致在多线程的情况下可能出现缓存一致性的问题。即在多核CPU中,每个核心都有自己的缓冲,会造成同一个数据在不同的缓冲中值不一样的情况。*
## 2.处理器优化和指令重排
为了使处理器中的运算单元被充分运用,处理器会进行优化处理,将我们写的代码进行乱序处理。
```java
int i =1; //1
int j = 2; //2
i = i + j; //3
i = i * j; //4
```
在这个代码块中,1和2 的顺序没有要求,谁先执行都可以。但是3和4的执行顺序必须是先执行完3再执行4。因为3和4中具有数据依赖性。当然,处理器在对代码进行重排序的时候也会考虑到这一点。
除了一些主流的处理器会将我们的代码进行优化乱序处理以外,还有很多编程语言的编译器也会有类似的优化,比如:Java虚拟机的即时编译器(JIT)也会做指令重排。
```bash
发现问题:
缓存一致性问题就是可见性问题
处理器优化问题导致原子性问题
指令重排序问题导致有序性问题
```
### 2.1、可见性
在多线程中,普通的共享变量,不能保证可见性。因为当这个变量被修改时,不能及时在主存更新,也不知道什么时间能被写入主存。此时如果其他线程去读取这个变量,很可能在内存读取到的还是之前的值,无法保证可见性。 volatile修饰一个共享变量时,它能保证当这个变量值发生改变后的新值会被立即更新到主存中,当其他线程需要读取这个变量时,它会去内存中读取新值。 synchronized和Lock锁机制也具有可见性,因为它们的作用就是在同一个时刻 有且只有一个线程能获取锁,然后去执行同步代码块,将变量值刷新到主存后,才会释放锁。 System.out.println()语句也能保证可见性,因为在它的源码中 ,也出现了锁。
```java
> public void println(long x) {
synchronized (this) {
print(x);
newLine();
}
}
```
println有个加锁的过程,即操作如下:
1.获取同步锁。
2.清空自己工作内存上的变量。
3.从主内存获取最新值,并加载到工作内存中。
4.打印并输出。
### 2.2、原子性
原子是最小单位,具有不可分割性。原子性是指操作不可以被中途其他CPU暂停然后调度, 即不能被中断, 要不就执行完, 要不就不执行.。如果一个操作是原子性的, 那么在多线程环境下, 就不会出现变量被修改等奇怪的问题。
比如:
i = 9;
j=0;
i++;
j=x;
这些表达式中只有前两个是原子性的。
### 2.3、有序性
有序性是指程序执行的顺序就是根据你代码编写的先后顺序。但是实际执行代码的时候,为了提供程序运行效率,可能会对代码进行优化,处理器会对指令进行重新排序。这样就无法保证程序执行的顺序是代码编写的先后顺序,但是它能保证最终结果和优化前的结果一致。
eg:
int i =1; //1
int j = 2; //2
i = i + j; //3
i = i * j; //4
在这个代码块中,1和2 的顺序没有要求,谁先执行都可以。但是3和4的执行顺序必须是先执行完3再执行4。因为3和4中具有数据依赖性。当然,处理器在对指令进行重排序的时候也会考虑到这一i点。
虽然在单线程中,指令重排序不会对结果造成影响。但是在多线程中,却存在很大的隐患。
```java
class OrderExample {
int a = 0;
boolean flag = false;
public void writer() {
a = 1;
flag = true;
}
public void reader() {
if (flag) {
a++;
System.out.println(a);
}
}
}
```
这里最终结果a的值可能不是2。比如:当线程1进入 writer()时,可能先执行flag = true; 这个时候可能线程2去执行 reader()方法,最终结果为a=1。
## 3.什么是Java内存模型
因为Java程序是在Java虚拟机中运行的。
> **Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。**
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200618082203999.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dpc2VfeWFuZw==,size_16,color_FFFFFF,t_70)
当执行写操作的时候,会检查这个变量是否为共享变量(即一个变量在多个CPU中都有缓存)。如果是共享数变量,当有线程对这个数据进行写操作的时候,就会发出信号通知其他CPU将该变量的缓存行置为无效状态。因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。 这样就能很好的解决缓存不一致的问题。
因为JMM只是一种规范,它是为了解决多线程中缓存不一致,处理器对代码优化乱序执行、编译器对代码指令重排序引发的问题,保证并发使线程的可见性、原子性、有序性。
它的实现依赖很多关键字。
比如Atomic、字节码指令monitorenter和monitorexit保证原子性,因为这两个字节码在Java中对应的关键字就是synchronized。
volatile、synchronized和final都可以保证可见性。
synchronized和volatile可以保证多线程操作的有序性。volatile关键字会禁止指令重排。synchronized关键字保证同一时刻只允许一条线程操作。
综上所述,synchronized是万能的,可以同时保证这三个问题。但不可忽视的是,它的使用会对项目的性能有较大的负面影响。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论