最近都在看极客时间的《Java并发编程》这一课程,看了好一阵,有些明白,有些混沌。于是想着自己整理一版,根据自己的理解做一点笔记。
先罗列了一个笔记大纲,从整体去看,如何去学习。
大纲.png并发定义
并发经常和另一个概念被一同提起-并行。
简单点说,并发与并行的区别就是,并行是真正的同一时刻干了几件事,并发只是看上去一段时间内干了几件事。两者的时间维度不一样,并发是“一段时间”这一维度。
并发是通过线程切换可以在同一段时间内干几件事。而线程切换就会带来一些问题。
带来的问题
- 可见性问题
可见性问题:一个线程对一个变量的修改对于另一个线程是可见的。而并发可能导致一个线程对变量的修改对于另一个线程是不可见的。
具体见下面这个常见的例子-计数器:
public class Test {
private long count = 0;
private void add10K() {
int idx = 0;
while(idx++ < 10000) {
count += 1;
}
}
public static long calc() {
final Test test = new Test();
// 创建两个线程,执行 add() 操作
Thread th1 = new Thread(()->{
test.add10K();
});
Thread th2 = new Thread(()->{
test.add10K();
});
// 启动两个线程
th1.start();
th2.start();
// 等待两个线程执行结束
th1.join();
th2.join();
return count;
}
}
直觉count是20000,但是结果是10000~20000之间的一个数。
导致可见性问题的原因:
cpu内存与主内存图.png
关于可见性问题,今天刚刚细细看了一下,是由于cpu缓存与内存不一致导致的,那么单核应该是不会存在可见性问题,另外多个线程如果在同一个cpu上运行应该也是不会产生可见性问题,因为大家都是读取的同一个cpu缓存。
- 原子性问题
原子性问题:一个操作或多个操作在cpu执行过程中被执行不被中断
我们知道I/O的读取是很慢的,cpu相对来说快很多,于是我们在做一个I/O操作时可能会要等待很久,这段时间内cpu是空闲的,于是有人提出多线程分时复用这个概念,可以在等待I/O的同时,让出cpu执行别的操作,就好像早上热牛奶这一操作要等很久,于是同时你可以洗漱。你就是cpu,热牛奶就是读取I/O。于是呢,就会出现线程切换,这样就会导致操作被中断。
例如:count+=1这一操作
原子性问题例子.png
如上图如果有两个线程同时执行count+=1这一操作,有可能出现线程切换,那么会导致数据错乱。
- 有序性问题
例如程序中:“a=6;b=7;”编译器优化后可能变成“b=7;a=6" 。这个主要是编译器优化导致的,关于编译器优化这一点我还是不是很理解。
以上就是关于右边并发定义,问题及问题产生的原因这三部分的叙述。
解决方案
如上所知,可见性问题是缓存导致,原子性问题是线程切换导致,有序性问题是编译器优化导致。那么解决方案也是从这三点入手,按需禁用缓存/编译器优化。至于线程切换这一点,我们不能禁止,因为这个是解决慢的问题提出的方案呀。
我们先看按需禁用缓存/编译器优化问题的解决方案:
给出了volatile,final,synchronized,Happens-Before规则这几个概念。
- volatile
就是禁用缓存的意思,只能从内存读取。
那这样就可以解决可见性问题啦 - final
就不用说啦,不可修改,但是我有一个疑惑,对于基本类型是不可修改,但如果是一个实体,那么还是可以set去修改里面的值,那应该还是有可见性问题。 - Happens-Before原则
这个是重点,大概有6点规则是属于这里面的。
先明白这个单词的含义,他规则的核心意思是指:前面的操作对后续的操作是可见的,也就说后面的操作是以前面的操作为前提。这点很重要!!!
例子:
// 以下代码来源于【参考 1】
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
// 这里 x 会是多少呢?
}
}
}
答案是x=42,就是根据Happens-Before原则。
- 程序顺序性原则
- volatile 的写操作 Happens-Before后面的读操作
- 传递性
以上三条就可以的得出 x=42 - 管程中的锁
大意是说一个锁的解锁会Happens-Before后面对这个锁的加锁 - 线程start()
大意是说主线程里开启线程B,线程B能看到主线程在启动子线程B之前的操作 - 线程join()
大意是说主线程能看到子线程的操作
- 原子性问题的解决方案
有一个经典的例子,在32位cpu上对long写操作,long是64位,写操作会被分为两步,高32位和低32位。如果被两个线程去写,就会有问题啦。
所以呢,我们要保证同一时刻只有一个线程被执行,称之为互斥。这个是解决问题的思路~
写了好久,先去吃块饼干🍪,休息会~
网友评论