并发编程
最近在学习即刻时间王宝令老师的并发编程专栏,有一种醍醐灌顶的感觉。故对前四篇文章做个总结。
如何设计一个并发程序?
把并发编程抽象一层,你会发现当你写一个并发程序的时候,干的就是三件事:
分工:考虑的事把一个任务拆解成多少个子任务分别分配给多少个线程做呢,比如说采用并发的方式实现wordcount,那么我可能能把这个任务拆解成 1.对文件进行切割 2.对这些不同的切割分别计算它们的所包含的词的数量 3.把最后的结果输出出来。
而这三类任务,1和3在主线程中做,2就由多个线程进行并行执行。
协作: 考虑的是任务启动的时机,最主要的就是任务之间的依赖关系,如同一个DAG一般,一个线程执行完了一个任务,要通知哪些后续的任务的线程开工。而对于上面的例子而言,主线程一边做着1任务,切割了一个就启动一个线程干2任务,然后等所有的2任务干完了,主线程就可以干三任务。
线程安全(互斥):分工同步考虑了程序的性能,但并发程序中准确性问题同样至关重要。从任务具体执行的角度上看,任务之间同样可能不是说能够独立运行,因为他们可能共享着变量,可能会有变量的可见性,原子性,顺序性的问题。接着上面的例子,我们在任务2中用了一个共享变量count计算词的总数,由于多个线程对count进行操作,所以可能就需要对这个count采用原子的方式进行修改,比如用锁,用CAS等等。
(我其实觉得协作互斥的名字起得不是很好,但我也没有想到能够简单表达意思的好名字,😔。)
并发编程的问题根源是啥子?
计算机在飞速发展,但核心矛盾仍然是CPU,内存,磁盘的性能差距,形象点说,CPU 是天上一天,内存是地上一年;而如果内存是天上一天,I/O 设备是地上十年。于实乎就有什么L1 L2等告诉缓存去均衡CPU与内存的速度差距;就有对进程,线程对CPU进行分时复用,均衡CPU与I/O设备的差距;编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。而这三方面的改进,同时也引入了三方面的问题,缓存的存在,数据的修改如何让其他线程看见,可见性问题;比如一个I++的操作,多个线程运行,线程切换的时候,就可能导致这个i++操作停i变量的加载过程中,从而导致原子性问题。指令顺序的重排,导致其他线程不能以该线程的代码顺序做假定编写代码。比如下面事例,线程二就不能因为线程一中i=10 是在isOutPut = true之前的,所以就认为输出的i一定是10。
//线程一
i = 10
isOutput = true;
//线程二
if(isOutput){
System.out.println(i);
}
java内存模型,能不能讲的对程序员友好点?
什么java内存模型,尤其是happens-before规则,一直以来我就感觉要么是废话,要么不知道肿么用。王宝令老师给了一个特别对广大程序员友好的解释。因为缓存导致了可见性问题,因为指令重排导致了顺序性问题,那么我们是不是可以通过按需禁止缓存,按需禁止指令重排解决进行解决呢。而java内存模型正是规范了按需禁止缓存指令重排的方法。这些方法包括 volatile、synchronized 和final 以及happenbefore的六项规则。
happenbefore 影响的是顺序性,和可见性语义
- 在同一个线程中,前面的操作happens-before于后面操作。这个规则其实很显然,但需要强调的是这并不是说禁止指令重排。在i=10的后面,你都可以利用i=10这个信息,但如果你没用,是不是说我放到j=10后面执行也ok呢。
i=10;
j=10;
- volatile 变量规则 , 对volatile变量的写操作 happens-before 对volatile变量的读操作。这条规则好像和禁用缓存的效果是一致的, 所以对这条规则我一直有个问题,就是首先这条规则无疑是针对一个线程写,另外的线程读的场景的,那么我在写线程二的程序的时候,同样还是有顺序性问题呀?
//线程一
i = 10 //操作1
volatile isOutput = true;//操作2
//线程二
if(isOutput){//操作3
System.out.println(i);//操作4
}
然而我忽略了
- 传递性规则如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C。
有什么用呢? 如果线程二if isOutput 为true了,那由规则2 操作2 happens-before 操作3,由规则一 操作3 happens-before 操作4,所以操作2 happens-before 操作4,同理可证 操作1happens-before 操作4。 简直棒呆。所以1,2,3规则结合起来用,也就说明了volatile修饰变量的禁用缓存,禁用指令重排的语义。
管程中的锁规则
对一个锁的解锁 Happens-Before 于后续对这个锁的加锁操作。线程start() 规则
它是指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。线程join()规则
主线程A等待子线程B运行完后,主线程A能够看到子线程B的操作。