Java多线程并发的前世今生
背景:
单处理机情况下:
每个处理机都有自己的cache缓存块,我们读数据的时候会进行地址转换,如果发现在cache里面而且有效的话可以直接读到寄存器。我们写数据有两种策略,第一写直通式,同时修改cache块和内存块,耗时比较长,第二种是这块cache块被替换回内存的时候,如果cache块的修改标志是true的话写回内存。
多处理机情况下:
多处理器环境下,每个处理机的都有cache缓存,从而导致数据不一致,于是Intel出了一个MESI,缓存一致性协议,问题解决,这个协议大概原理是写数据的时候,发现这个数据是共享数据,第一时间更新到内存,同时把其它缓存的该数据标志无效,别的处理器访问数据时候,因为cache失效会到内存重新取。不过这个协议有个缺点,处理机1和处理机2都同时把数据读到寄存器,处理机1更新完数据到cache时即使处理机2的cache失效,数据还是脏的了,因为之前的数据已经在处理机2的寄存器中了。
虚拟机:
volatile
我们的虚拟机也是一台机器,JVM每个线程(相当于一个处理机)也会把主存中的变量先搞到自己的缓存(抽象的缓存,又被博主们称为工作内存,注意它是JVM抽象了物理机器中寄存器,Cache和编译器优化的东西,不要对应到实际物理内存去)里面,就会导致数据不一致的问题,虚拟机设计者的我们需要像Intel一样解决问题,于是我们推出了volatile关键字,保证所有缓存和主存里面的数据是一致的。
有这个volatile关键字并不能保证并发下的变量线程安全,
I 初始值为0
线程1:
Load I from storage to Thread1
OP I 3
Write I
线程2:
Load I from storage to Thread:2
OP I 3
Write I
violate这个关键字的作用是这样的,当执行读取violate修饰的数据的时候,缓存即使有这个数据,也从内存中重新读,执行写这个数据的时候,会直写到内存。保证了我们读数据的时候读的是内存里面的最新数据,其实跟上面cache是一样的道理。那为什么没有原子性呢?跟上面cache里面的一样,有一个可能已经计算完放在临时寄存器了,写回数据就变脏了。
Synchronized
这里说一下,我们总是听到各种锁,以为有多高级,其实内容是很简单的,有计算机基础的不要被这些名词搞混了。synchronized是Java提供的一个关键字,
synchronized(虎符){
虎符.调兵;
}
只有一个虎符,多个线程一起执行到synchronized()这句话时,只有一个线程能抢到虎符开始调兵,其余线程会被挂到虎符的等待队列上面。等第一个执行完后,其余线程继续抢。
这样写有点不方便,所以语言设计者允许你把synchronized关键字写在调兵方法的前面,效果是一样的。顺便解释一下什么是线程安全的类,就是他的方法都有synchronized修饰。
Lock对象
Lock对象跟synchronized效果差不多,多了一个方法,它允许你设置一个值,如果在这个时间值内抢不到虎符,就不抢了。
CAS算法
听起来好像很牛逼,其实就是CompareAndSwap的意思,对应底层就一条指令而已。咱们线程取值到缓存,设置一个三元组(V,E,N)V:value对应的内存中数据值,E:expect期望值,N :new新值。第一步V=E,表示我期望我写回的时候值还是我取的那个值,如果真的你还是那个你,那我就写回了,如果你不是我期望的那个,你跟之前不一样了,N就是错误的,回到第一步,重新取V,E;重新取V,E再来一遍,像不像旋转,所以又是自旋锁的一种。比较期望值和内存中的值如果可能的话写回内存都是一气呵成的,不会被中断,因为是一条指令,只有在指令周期的尾部才会判断有没有中断来进行中断。
CAS算法在Java8中进行了改进,第一个改进就是解决了ABA问题,这个问题大概是这样的:虽然内存中的值跟我的期望值是一样的,可是有可能是别人用过了再补回来的。好像没什么问题啊?对那些“值相同”情况下不会引发业务逻辑错误的确实是这样的。可是我也想不出来这样有什么错误,知乎大佬说在链栈场景下会出现致命性错误。所以为了解决这个问题,引入了versionControl变量,相当于期望值多了一个版本号E-->(Evalue,Everion)两个都相同才能进行写入内存。
第二个改进是,如果太多线程操作这个变量,而且很多线程都修改了值,不是单纯的读,会导致一直写不进去,重写,旋转,性能大大降低。这时候我们设置了一个cell数组,比如有一百个线程,cell数组大小为10,我们把线程分为十组,每组操作一个元素。最后把这十个值合起来就是我们的结果了。
synchronized和cas的比较
synchronized是悲观的,总认为大部分线程会把数据改了,不如独占。cas很乐观,认为很多线程只是读数据不会修改他,让线程在自旋的开销会比把线程阻塞再投入运行的开销小。当然了场景不同,我们应该选用不同的机制。
AQS
Abstract queue synchronizer ,就是抽象队列同步器,听我解析。
AQS是一个抽象类,我们可以用这个类来实现我们自己的同步设施,听说过PV操作吗?信号量初值设置的不同可以让这个信号量作为互斥锁也可以是有好几个资源被人申请的。AQS里面有个state变量,是用volatile修饰的,这个变量就是信号量的意思。AQS里面还有一个CLH队列,名字是发这篇论文的三个人的名字,其实他就是一个FIFO队列。用AQS我们实现自己的类,然后我们实现自己的锁把这个子类聚合起来。AQS提供了几个方法来操纵state变量,这些方法里面都是用CAS算法的。这部分可以看我另外一篇博文,Java并发包。反正就是so easy,大概也没有什么难的。用AQS实现的锁跟synchronized比起来用什么不同吗?后面那个关键字是系统级别加锁开销很大,我们用AQS实现 判断state用cas算法也不用进入内核态性能是非常小的。不过Java的设计者后面对synchronized进行了优化,可能差别也不会太大。
可重入锁:
是使用AQS进行实现的一个锁,英语名字叫做Re entrant Lock 。可以设置听多属性的,我们常用concurrentHashMap里面实现
分段锁技术的Segment锁是他的一个子类。