前言
大家好, 这里是Yve菌. 今天给大家带来一篇并发编程的基础知识, 要想学好并发编程就必须要掌握好以下的知识点!
一、并发和并行
并发和并行时两种CPU执行的方式, 他们的目标都是最大化CPU的使用率.
并行(parallel): 在同一时刻, 多个CPU同时执行不同的线程, 他是真正意义上的同时执行.
并发(concurrency): 一个CPU不断交替轮流执行不同的线程, 在同一时刻只能有一个线程被执行. 在宏观上看具有多个线程一起执行的效果, 但是在微观上确不是同时执行的.
并行在多处理器系统中存在,而并发可以在单处理器和多处理器系统中都存在,并发能够在单处理器系统中存在是因为并发是并行的假象,并行要求程序能够同时执行多个操作,而并发只是要求程序假装同时执行多个操作(每个小时间片执行一个操作,多个操作快速切换执行)
二、并发三大特性
这时并发当中最最重要的概念, 所有并发相关的问题都是出在了这三种特性上面
1.原子性
在线程中的所有操作都应该为原子操作, 操作时一个或多个操作指令要么全部执行成功, 要么全部不执行.
例如: 用户A给用户B转账应该分为两步, 第一步用户A账户扣除金额, 第二步用户B账户增加金额. 这两步操作应该是一个原子操作, 即两个操作要么都成功执行, 这时转账成功, 要么两个操作同时失败, 这时转账失败, 不然就会出现严重的问题.
如何保证原子性:
- 使用synchronized关键字
- 使用Lock保证原子性
- 使用CAS保证原子性
注意: volatile关键字只能保成可见性和有序性, 不可以保证原子性.
2.可见性
可见性, 顾名思义就是当一个线程对共享变量进行操作时, 其他线程应当立即看到修改的值. Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方法来实现可见性的。
我们可以看看以下代码
private boolean flag = true;
public void refresh() {
flag = false;
System.out.println(Thread.currentThread().getName() + "修改flag");
}
public void load() {
System.out.println(Thread.currentThread().getName() + "开始执行.....");
int i = 0;
while (flag) {
i++;
}
System.out.println(Thread.currentThread().getName() + "跳出循环: i=" + i);
}
public static void main(String[] args) throws InterruptedException {
VisibilityTest test = new VisibilityTest();
// 线程threadA模拟数据加载场景
Thread threadA = new Thread(() -> test.load(), "threadA");
threadA.start();
// 让threadA执行一会儿
Thread.sleep(1000);
// 线程threadB通过flag控制threadA的执行时间
Thread threadB = new Thread(() -> test.refresh(), "threadB");
threadB.start();
}
我们启动线程A执行load()方法, 此时flag为true, 所以i会不断的增加; 我们通过线程B执行refresh()方法把flag转换为false, 此时线程A还会继续增加i吗?
答案是线程A中i还是会不断的增加. 这是因为线程A在执行时会把变量flag=true保存在自己的工作内存当中, 而这时即使线程B将flag修改成false, 线程A读取的依然是自己工作内存中的flag=true.
上述的例子就是一个典型的可见性问题, 一个线程改变了某个变量, 而另一个线程读取不到改变后的变量, 也就是可见性出了问题.
如何保证可见性:
- 通过 volatile 关键字保证可见性。
- 通过内存屏障保证可见性
- 通过 synchronized 关键字保证可见性。
- 通过 Lock保证可见性。
- 通过final关键字保证可见性
有序性
有序性就是程序执行的顺序按照代码的先后顺序执行。
JVM有时为了性能会对指令进行重新排序, 在单线程中这些重排序是不会对整体造成影响的, 但是在一些多线程环境下会影响并发执行的正确性
如何保证有序性:
- 通过 volatile 关键字保证可见性。
- 通过内存屏障保证可见性
- 通过 synchronized 关键字保证可见性。
- 通过 Lock保证有序性。
- 另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则
三、内存屏障
我们之前提到的如何保证可见性和有序性中都提到了一个内存屏障, 内存屏障大体来说分为两类:
1. JVM层面的内存屏障
在JSR规范中定义了4种内存屏障:
-
LoadLoad屏障:(指令Load1; LoadLoad; Load2),在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
-
LoadStore屏障:(指令Load1; LoadStore; Store2),在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
-
StoreStore屏障:(指令Store1; StoreStore; Store2),在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
-
StoreLoad屏障:(指令Store1; StoreLoad; Load2),在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能
由于x86只有store load可能会重排序,所以只有JSR的StoreLoad屏障对应它的mfence或lock前缀指令,其他屏障对应空操作
2. 硬件层内存屏障
硬件层提供了一系列的内存屏障 memory barrier / memory fence(Intel的提法)来提供一致性的能力。拿X86平台来说,有几种主要的内存屏障:
- lfence,是一种Load Barrier 读屏障
- sfence, 是一种Store Barrier 写屏障
- mfence, 是一种全能型的屏障,具备lfence和sfence的能力
- Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。它后面可以跟ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG等指令。
内存屏障的作用:
- 阻止屏障两边的指令重排序
- 刷新处理器缓存/冲刷处理器缓存
正是这两点作用才让内存屏障具有保证有序性和可见性的功能.
总结
以上就是个人对并发编程的一些理解, 如果本篇文章能够帮助到你, 麻烦点个赞呗, 感谢大家!