题记:总是被绕在一个无法挣脱的并发深渊里,眼睛一闭一挣都是线程的世界。
这边最近是这样的情况:
基于JVM的内存模型,我们知道了多个线程并发访问主存里面的共享数据。这时候在每一个线程中会有一个工作内存的逻辑概念。线程和主存之间的工作方式将会遵循:lock unlock read load use assign restore write的执行方式来完成线程和主存之间的数据交换处理操作。
并发的三个概念:原子性 可见性 有序性。
对于原子性:我这边的理解是 不可分割的操作,这个定义有点狭隘。JAVA语言默认对于基本类型变量的读写都是原子操作(不可分割的操作,对应于JAVA内存模型就是read load restore write操作),比如int a=3这种,就是一个原子操作,对于double long型占用64位的变量,虚拟机规范提出可以采用两个32位的方式来读写。这就不具有原子性,但是大多数的编程语言都会将64位的数据的读写操作默认实现为原子性。还有一点需要注意的是对于基本类型变量的自加 自减等操作,本质上是不具有原子性的,例如:a=a+3;JAVA中采用
AtomicInteger这种方式来替换Integer完成自加自减等操作,使其具有原子性。也就没有线程安全问题。
问题: 对于volatile 和 synchronized修饰的变量和同步的代码,和原子性之间的关系,怎么去理解?
volatile会保证在一个线程中对一个共享变量的修改会立即同步至其他线程,也就是对于其他线程会立即可见。使用volatile还是会存在线程的安全问题,例如,两个线程同时将主存中int i=2 load进自己的工作内存中,都准备执行i+1操作,正常逻辑下输出应该是4,
但是 线程1将i=2已经放在栈帧的顶部进行+1之前,线程2已经做完了+1操作,这时候线程1会立即可见线程2的操作,线程1中的i被至于3,但是执行+1的还是原来的2,导致最终主存中的i=3;
使用synchronized修饰的代码段都具有原子性,这边我的理解是在一个线程中执行该代码段的时候,即使CPU切换到其他线程,但是由于共享变量已经被锁住,也无法去执行该代码段,所以对于程序员来说,被synchronized修饰的代码片段是具有原子性,会执行完整而不会在执行过程中被其他线程任意修改变量状态。
对于可见性:
指的是线程1对于共享变量的修改可以对于其他线程可见。怎么去理解呢?JAVA中实现可见性可以使用volatile和synchronized两个操作。使用volatile可以保证一个线程的修改对于其他线程立即可见(前面已经讲过),对于synchronized操作,每个线程获取锁之后,都会首先去主存中load数据,然后操作完成之后,在unlock之前,会将修改完成的数据restore进主存,完成了各个线程之间数据的可见性。
对于有序性
我这边的理解是java中的代码执行在并发的情况下应该按照某种规则顺序的执行,不然会产生结果的不可预测性,原因是指令重排。
对于单CPU单线程来说:
字节码在编译执行的时候会采用指令重排来提高执行效率。为了不影响最后执行结果的正确性,编译和CPU的指令重排必须遵循as-if-serial原则,该原则说的是,无论最后怎么指令重排,都不能改变执行结果的正确性。编译和CPU为了达到该准则,使用依赖原则来保证,
也就说是 在单线程中,两个对同一个变量的读写的操作具有依赖性,对于有依赖性的多个操作,编译和CPU不会进行指令重排,也就保证了结果的正确性。
在多线程中,为了保证最后执行结果的正确性,提出了先行发生原则,如果两个操作不满足先行发生原则,CPU就可能会对其指令重排。
具体的先行发生原则是如下:
1、程序次序规则。在一个线程内,书写在前面的代码先行发生于后面的。确切地说应该是,按照程序的控制流顺序,因为存在一些分支结构。
2、Volatile变量规则。对一个volatile修饰的变量,对他的写操作先行发生于读操作。
3、线程启动规则。Thread对象的start()方法先行发生于此线程的每一个动作。
4、线程终止规则。线程的所有操作都先行发生于对此线程的终止检测。
5、线程中断规则。对线程interrupt()方法的调用先行发生于被中断线程的代码所检测到的中断事件。
6、对象终止规则。一个对象的初始化完成(构造函数之行结束)先行发生于发的finilize()方法的开始。
7、传递性。A先行发生B,B先行发生C,那么,A先行发生C。
8、管程锁定规则。一个unlock操作先行发生于后面对同一个锁的lock操作。
我们可以通过Volatile 和synchronized 对于一些共享的变量或者代码片段进行保护,从而避免了指令重排,保证结果的正确执行。
问题:这里有一个线程start书写的顺序,其实可以认为就是在符合先行发生准则的时候,代码执行的顺序。
线程被CPU随机切换,这是无法预知的,在不符合先行发生准则下,也就是重排序。。。。。
使用Volatile synchronized 处理之后,会符合先行发生的原则,对于符合先行发生的原则的两个操作来说,严格按照先行发生原则的顺序来顺序执行,也就保证了有序性。