大家都知道,线程会存在安全性问题,那接下来我们从原理层面去了解线程为什么会存在安全性问题,并且我们应该怎么去解决这类的问题。
其实线程安全问题可以总结为: 原子性
、可见性
、有序性
这几个问题,我们搞懂了这几个问题并且知道怎么解决,那么多线程安全性问题也就不是问题了。
常见的解决安全性问题方案:
原子性: Synchronized、AtomicXXX、Lock (CAS)
可见性: Synchroinzed、volatile
有序性: Synchroinzed、volatile => happens-before模型
线程是 CPU 调度的最小单元,线程涉及的目的最终仍然是更充分的利用计算机处理的效能,但是绝大部分的运算任务不能只依靠处理器“计算”就能完成,处理器还需要与内存交互,比如读取运算数据、存储运算结果,这个 I/O 操作是很难消除的。而由于计算机的存储设备与处理器的运算速度差距非常大,所以现代计算机系统都会增加一层读写速度尽可能接近处理器运算速度的高速缓存来作为内存和处理器之间的缓冲:将运算需要使用的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步到内存之中
。
高速缓存从下到上越接近 CPU 速度越快,同时容量也越小。现在大部分的处理 器都有二级或者三级缓存,从下到上依次为 L3 cache
, L2 cache
, L1 cache
缓
存又可以分为指令缓存和数据缓存,指令缓存用来缓存程序的代码,数据缓存
用来缓存程序的数据
L1 Cache
: 一级缓存,本地 core 的缓存,分成 32K 的数据缓存 L1d 和 32k 指 令缓存 L1i,访问 L1 需要 3cycles,耗时大约 1ns;
L2 Cache
:二级缓存,本地 core 的缓存,被设计为 L1 缓存与共享的 L3 缓存 之间的缓冲,大小为 256K,访问 L2 需要 12cycles,耗时大约 3ns;
L3 Cache
:三级缓存,在同插槽的所有 core 共享 L3 缓存,分为多个 2M 的 段,访问 L3 需要 38cycles,耗时大约 12ns;
CPU-0
读取主存的数据,缓存到 CPU-0 的高速缓存中,CPU-1
也做了同样的事 情,而 CPU-1 把 count 的值修改成了 2,并且同步到 CPU-1 的高速缓存,但
是这个修改以后的值并没有写入到主存中,CPU-0 访问该字节,由于缓存没有 更新,所以仍然是之前的值,就会导致数据不一致的问题
引发这个问题的原因是因为`多核心 CPU 情况下存在指令并行执行,而各个 CPU 核心之间的数据不共享从而导致缓存一致性问题`,为了解决这个问题, CPU 生产厂商提供了相应的解决方案
总线锁
当一个 CPU 对其缓存中的数据进行操作的时候,往总线中发送一个 Lock 信 号。其他处理器的请求将会被阻塞,那么该处理器可以独占共享内存
。总线锁 相当于把 CPU 和内存之间的通信锁住了,所以这种方式会导致 CPU 的性能下降,所以 P6 系列以后的处理器,出现了另外一种方式,就是缓存锁
。
缓存锁
如果缓存在处理器缓存行中的内存区域在 LOCK 操作期间被锁定,当它执行锁操作回写内存时,处理不在总线上声明 LOCK 信号,而是修改内部的缓存地址,然后通过缓存一致性机制
来保证操作的原子性,因为缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域的数据,当其他处理器回写已经被锁定的缓存行的数据时会导致该缓存行无效
。
所以如果声明了 CPU 的锁机制,会生成一个 LOCK 指令,会产生两个作用
MESI 协议
了,它的方法是在 CPU 缓存中保存一个标记位,这个标记为有四种状态
M(Modified)
修改缓存,当前 CPU 缓存已经被修改,表示已经和内存中的 数据不一致了I(Invalid)
失效缓存,说明 CPU 的缓存已经不能使用了E(Exclusive)
独占缓存,当前cpu的缓存和内存中数据保持一直,而且其他处理器没有缓存该数据 总线锁
S(Shared)
共享缓存,数据和内存中数据一致,并且该数据存在多个 cpu 缓存中每个 Core 的 Cache 控制器不仅知道自己的读写操作,也监听其它 Cache 的读 写操作,嗅探(snooping)协议
CPU 的优化执行
除了增加高速缓存以为,为了更充分利用处理器内内部的运算单元,处理器可能会对输入的代码进行乱序执行优化
,处理器会在计算之后将乱序执行的结果充足,保证该结果与顺序执行的结果一致,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致,这个是处理器的优化执行
;还有一个就是编程语言的编译器
也会有类似的优化,比如做指令重排
来提升性能。
前面说的和硬件有关的概念你可能听得有点蒙,还不知道他到底和软件有啥关系,其实原子性、可见性、有序性问题,是我们抽象出来的概念,他们的核心本质就是刚刚提到的`缓存一致性问题、处理器优化问题导致的指令重排序问题`。比如缓存一致性就导致可见性问题、处理器的乱序执行会导致原子性问题、指令重排会导致有序性问题。为了解决这些问题,所以在 JVM 中引入了JMM 的 概念
JMM
内存模型定义了共享内存系统中多线程程序读写操作行为的规范,来屏蔽各种 硬件和操作系统的内存访问差异,来实现 Java 程序在各个平台下都能达到一致 的内存访问效果。
Java 内存模型的主要目标是定义程序中各个变量的访问规则,也就是在虚拟机中将变量存储到内存以及从内存中取出变量(这里的变量,指的是共享变量,也就是实例对象、静态字段、数组对象等存储在堆内存中的变量。而对于局部变量这类的,属于线程私有,不会被共享)这类的底层细节
。
通过这些规则来规范对内存的读写操作
,从而保证指令执行的正确性。 它与处理器有关、与缓存有关、与并发有关、与编译器也有关。它解决了 CPU 多级缓存、处理器优化、指令重排等导致的内存访问问题,保证了并发场景下 的可见性、原子性和有序性,。
内存模型解决并发问题主要采用两种方式:`限制处理器优化`和`使用内存屏障`
Java 内存模型定义了线程和内存的交互方式,在 JMM 抽象模型中,分为主内存
、工作内存
。主内存是所有线程共享的,工作内存是每个线程独有的。线程对变量的所有操作(读取、赋值)都必须在工作内存
中进行,不能直接读写主内存
中的变量。并且不同的线程之间无法访问对方工作内存中的变量,线程间的变量值的传递都需要通过主内存来完成,他们三者的交互关系如下: 8个操作
:Lock、Read、Load、Use、Assign、Store、Write、Unlock
Use <--- Load <--- Read Lock
Java线程 --> 高速缓存(变量副本) --> (Save Load) --> 主内存变量
Java线程 --> 高速缓存(变量副本) --> (Save Load) --> 主内存变量
Java线程 --> 高速缓存(变量副本) --> (Save Load) --> 主内存变量
Java线程 --> 高速缓存(变量副本) --> (Save Load) --> 主内存变量
Assign ---> Store ---> Write Unlock
所以,总的来说,JMM 是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。目的是保证并发编程场景中的原子性、可见性和有序性。
大家都知道,线程会存在安全性问题,那接下来我们从原理层面去了解线程为什么会存在安全性问题,并且我们应该怎么去解决这类的问题。
其实线程安全问题可以总结为: 原子性
、可见性
、有序性
这几个问题,我们搞懂了这几个问题并且知道怎么解决,那么多线程安全性问题也就不是问题了。
常见的解决安全性问题方案:
原子性: Synchronized、AtomicXXX、Lock (CAS)
可见性: Synchroinzed、volatile
有序性: Synchroinzed、volatile => happens-before模型
Synchronized减重的过程,通常被称为锁膨胀或是锁升级的过程。 主要步骤是:
大家都知道,线程会存在安全性问题,那接下来我们从原理层面去了解线程为什么会存在安全性问题,并且我们应该怎么去解决这类的问题。
其实线程安全问题可以总结为: 原子性
、可见性
、有序性
这几个问题,我们搞懂了这几个问题并且知道怎么解决,那么多线程安全性问题也就不是问题了。
常见的解决安全性问题方案:
原子性: Synchronized、AtomicXXX、Lock (CAS)
可见性: Synchroinzed、volatile
有序性: Synchroinzed、volatile => happens-before模型
从JDK1.5 开始引入的。多线程环境下,一个操作对另一个操作可见,必然存在happens-before关系。(可见性与有序性)
as-if-serial:不管怎么重排序,单线程执行的结果都是不变的。
int a = 10; // A
int b = 18; // B
int c = a * b; //C
//不管 A、B怎么重排序,C是不允许重排序的,结果都是不变的。
//如同上面的例子:
// A happens-before B
// B happens-before C
//推导出: A happens-before C (其中A、B可以重排,不影响结果。)
对于volatile修饰的变量的写操作,一定happens-before后续对于volatile变量的读操作。底层通过内存屏障机制来防止指令重排。
public class VolatileTest{
int a = 1;
volatile boolean flag = false;
public void write()
{
a = 2; // 1
flag = true; // 2 后volatile写时,前面操作不能重排
}
public void read(){
if(flag) // 3 先volatile读时,后面操作不能重排
{
int m = a; //4
}
}
public static void main(String[] args) {
VolatileTest vt = new VolatileTest();
Thread t1 = new Thread(() -> {
vt.write();
});
Thread t2 = new Thread(() -> {
vt.read();
});
// t1.start();
// t2.start();
t2.start();
t1.start();
}
}
是否重排 第二个操作
第一个操作 | 普通读/写 | volatile 读 | volatile写 |
---|---|---|---|
普通读/写 | NO | ||
volatile读 | NO | NO | NO |
volatile写 | NO | NO |
synchronized: 一个线程对于一个锁的释放操作,一定happen-before与后续线程对这个锁的加锁操作。
public class SynchronizedTest{
int m =10;
public void test(){
synchronized(this) //此处自动加锁
{
// m 是共享变量,初始值为20
if(m < 18)
{
this.m = 18;
}
}//此处自动释放锁
}
}
假设m的初始值是10,线程A执行完代码块后,m的值会变成12,执行完成之后会释放锁。线程B进入代
码块时,能够看到线程A对m的写操作,也就是B线程能够看到m=12。
start:如果线程A执行操作ThreadB.start(),那么线程A的ThreadB.start()之前的操作happens-before线程B中的任意操作。
public class StartTest{
int x=0;
public static void main(String[] args) {
Thread t1=new Thread(()->{
//主线程调用t1.start()之前
//所有对共享变量的修改,此处皆可见 //此例中,x==20
System.out.println(x);
});
//修改共享变量的值
x = 20;
t1.start();
}
}
join规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功的返回。
public class JoinTest{
int x =0;
public static void main(String[] args) {
Thread t1=new Thread(()->{
//此处对共享变量x修改
x=100;
});
//例如此处对共享变量修改,
//则这个修改结果对线程t1可见
//主线程启动子线程
t1.start();
t1.join()
//子线程所有对共享变量的修改
//在主线程调用t1.join()之后皆可见//此例中,x==100
}
}
现在各个项目基本是与容器化部署,为了减少或避免各个服务启动时对外部服务的影响,所以服务平滑升级必不可少;
SpringCloud+Eureka +K8s + SpringBoot 平滑升级原理分析
主要基于三个动作:注册、续约、取消,下面基于k8s + eureka + springcloud + springboot 为例分析:
一句话总结:当新服务启动成功后,旧服务下线然后停止服务,流量转移到新服务。eureka:
environment: ${spring.profiles.active}
datacenter: "mzz"
# 客户端
client:
# 是否注册到注册中心
register-with-eureka: true
# 是否获取注册信息
fetch-registry: true
# 是否健康状态监测
healthcheck:
enabled: true
# 从Eureka服务器注册表中获取注册信息的时间间隔(s),默认为30秒
registry-fetch-interval-seconds: 5
# 复制实例变化信息到Eureka服务器所需要的时间间隔(s),默认为30秒
instance-info-replication-interval-seconds: 20
# 询问Eureka服务url信息变化的时间间隔(s),默认为300秒
eurekaServiceUrlPollIntervalSeconds: 10
instance:
# Eureka客户需要多长时间发送心跳给eureka服务器,表明它仍然活着,默认为30 秒
lease-renewal-interval-in-seconds: 5
# Eureka服务器在接收到实例的最后一次发出的心跳后,需要等待多久才可以将此实例删除,默认为90秒
lease-expiration-duration-in-seconds: 15
instance-id: ${spring.application.name}:${spring.cloud.client.ipAddress}:${server.port}
server:
enable-self-preservation: false
eviction-interval-timer-in-ms: 1500
dashboard:
enabled: true
# 是否需要安全配置(如果需要,则注册时也需要验证)
management:
security:
enabled: false