并发编程理论1.并发问题的由来
Posted shinyrou
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并发编程理论1.并发问题的由来相关的知识,希望对你有一定的参考价值。
并发编程中问题的由来:
CPU、内存、I/O设备的速度存在巨大差异,程序的整体性能取决于最慢的操作——读取I/O设备,为了合理利用CPU性能,平衡三者的速度差异,计算机体系结构、操作系统、编译程序做出了以下改进。
- CPU增加了缓存
- 操作系统增加进程、线程分时复用CPU,进而均衡CPU与I/O设备的速度差异
- 编译程序优化指令执行次序,使得缓存能够更加合理地利用
由此引发出了以下问题:
1.可见性——CPU缓存导致
早期单核CPU时,CPU缓存的数据与内存的数据是保持一致的,
线程A,线程B在同一个核心上切换运行,A对缓存中的操作对于B是立即可见的,所以不存在可见性问题
到了多核CPU时代,每个核心都有自己的CPU缓存(L1 cache,L2 cache).
线程A,线程B在执行不同的核心上执行时,先将数据从内存读取到各自的CPU缓存,此时线程A对于数据的操作,对于线程B是不可见的。
写操作对于读操作 是立即可见的
2.原子性——线程切换导致
高级程序语言中的一条语句往往对应这操作系统中的多条指令,
线程在CPU的执行又是时间片轮转的方式执行,和可能在线程A将内存中的变量值V = 0 读取到寄存器中时,切换到线程B执行将V的值进行了更新 V=V+1
此时V = 0 更新到内存,切换到A线程,对寄存器中 V=0 进行V++,在更新到内存 V=1。
就会出现两个线程进行 V = V+1操作,结果却还是2的情况。
一个或者多个操作在 CPU 执行的过程中不被中断的特性,称 为“原子性”
3.有序性——编译器优化导致
编译器为了优化性能,有时候会改变程序中语句的先后顺序,例如程序中:“a=6;b=7;”
编译器优化后可能变成“b=7;a=6;”,在这个例子中,编译器调整了语句的顺序,
但是不影响程序的最终结果。不过有时候编译器及解释器的优化可能导致意想不到的 Bug。
例如:双重校验单例模式
public class Singleton {
static Singleton instance;
static Singleton getInstance(){
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
理想的执行过程:
假设线程A、线程B同时调用getInstance()方法,
- 1.假设A先得到执行权对Singleton.class进行加锁
- 2.此时instance = null 创建Singleton对象
- 3.获取到单例对象,释放锁
- 4.线程B调用getInstance(),instance不为null,获取到线程A初始化的instance
经过编译优化后对象的创建过程
- 1.分配一块内存 M;
- 2.将 M 的地址赋值给 instance 变量;
- 3.最后在内存 M 上初始化 Singleton 对象。
当执行2后发生线程切换,线程B得到的instance应用地址并未初始化对象就会产生空指针。
Java内存模型如何解决并发问题
Java内存模型针对于
可见性问题 ——CPU缓存导致
有序性问题 ——编译优化导致
原子性问题 ——线程切换导致
提供的方案是按需禁用CPU缓存及编译优化
https://www.bilibili.com/video/av81008349
具体来说,这些方法包括 volatile、synchronized 和 final 三个关键字,以及Happens-Before 规则
volatile关键字:
被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。
虽然禁用了缓存,所有线程都要从内存中读取,但是不具备互斥性(即同一时间只有一个线程可以执行)
适用于一个线程写 另一个线程读 可以读到写入的最新值
synchronized关键字:
同步关键字,进行修饰的代码块或者方法,进行加锁,保证同时只有一个线程能够获取到锁。
Happens-Before 约束 了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守 HappensBefore 规则。
核心原则:前面一个操作的结果对后续操作是可见的。
六大规则:
- 1.程序的顺序性规则
指在一个线程中,按照程序顺序,前面的操作 Happens-Before 于后续的任意 操作。
即程序前面对某个变量的修改一定是对后续操作可见的。 - 2.volatile 变量规则
指对一个 volatile 变量的写操作, Happens-Before 于后续对这个 volatile 变量的读操作。 - 3.传递性
指如果 A Happens-Before B,且 B Happens-Before C,那么 A HappensBefore C。 - 4.管程中锁的规则
管程是一种通用的同步原语,在 Java 中指的就是 synchronized,synchronized 是 Java 里对管程的实现。
上一个加锁的线程中的操作释放锁后对下一个加锁线程可见。 - 5.线程 start() 规则
指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在 启动子线程 B 前的操作。 - 6.线程 join() 规则
指主线程 A 等待子线程 B 完成(主线程 A 通过调用子线程 B 的 join() 方法实现),当子线程 B 完成后(主线程 A 中 join() 方法返回),主线程能 够看到子线程的操作。当然所谓的“看到”,指的是对共享变量的操作。
/**
* 可见性问题
* 1.主线程中启动子线程 获取a的值 判断进行循环
* 2.因为循环中是空方法,a一直会读取CPU缓存中的值 一直为true
* 3.所以执行结果是 打印false后 子线程还在死循环执行
*/
public class Test {
static boolean a = true;
public static void main(String[] args) throws InterruptedException{
new Thread(()->{
while(a){
}
}).start();
Thread.sleep(1000);
a = false;
System.out.println(a);
}
}
/**
* 解决可见性问题
* volatile禁用CPU缓存
* 并且使得线程的写操作对 读操作可见
* 执行结果: 主线程打印false后 子线程读取到a的变更停止死循环
*/
public class Test {
static volatile boolean a = true;
public static void main(String[] args) throws InterruptedException{
new Thread(()->{
while(a){
}
}).start();
Thread.sleep(1000);
a = false;
System.out.println(a);
}
}
/**
* 原子性问题
* a++操作对应
* 1.读取a的值
* 2.a+1
* 3.赋值
* 下面两个线程可能出现 线程1 读取a = 0
* 切换到 线程2 读取到 a = 0
* 线程2 执行 a++ 更新 a的值 a=1
* 再次回到线程1 寄存器中 a = 0,执行a+1,更新a=1
* 两个线程的操作 却没有 a = 2
*
* 执行结果 <=20000
**/
public class Test {
static int a = 0;//此时使用volatile关键字修饰a 并没有保证原子性
public static void main(String[] args) throws InterruptedException{
for(int i=0;i<10000;i++){
new Thread(()->{
a++;
}).start();
new Thread(()->{
a++;
}).start();
}
Thread.sleep(2000);
System.out.println(a);
}
}
/**
* 原子性问题
* a++操作对应
* 1.读取a的值
* 2.a+1
* 3.赋值
*
* 解决原子性问题之使用原子类
* AtomicInteger 的 getAndAdd方法 在执行过程中 的读写是原子性的,不允许线程切换的
* AtomicInteger内部基于volatile关键字
*
* 执行结果:20000
**/
public class Test {
static AtomicInteger a = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException{
for(int i=0;i<10000;i++){
new Thread(()->{
a.getAndAdd(1);
}).start();
new Thread(()->{
a.getAndAdd(1);
}).start();
}
Thread.sleep(2000);
System.out.println(a);
}
}
/**
* 原子性问题
* a++操作对应
* 1.读取a的值
* 2.a+1
* 3.赋值
*
* 解决原子性问题之使用synchronized关键字
* 线程1、线程2 同一个时间只有一个线程能执行同步代码块,通过互斥性来保证原子性
* 最为重量级的实现
*
* 执行结果:20000
**/
public class Test {
static int a = 0;
public static void main(String[] args) throws InterruptedException{
for(int i=0;i<10000;i++){
new Thread(()->{
synchronized (Test.class){
a++;
}
}).start();
new Thread(()->{
synchronized (Test.class){
a++;
}
}).start();
}
Thread.sleep(2000);
System.out.println(a);
}
}
总结:
- volatile 保证可见性,一个线程的写对另一个线程的读可见 无锁
- AtomicInteger(原子类) 保证原子性,同时进行读写时的操作是原子性的. (基于CPU提供的CAS) 无锁
- Synchrnoized 保证互斥性 最为重量级 同步代码块是原子性的,同时只有一个线程可以执行 加锁
以上是关于并发编程理论1.并发问题的由来的主要内容,如果未能解决你的问题,请参考以下文章
全栈编程系列SpringBoot整合Shiro(含KickoutSessionControlFilter并发在线人数控制以及不生效问题配置启动异常No SecurityManager...)(代码片段