简单说说可见性和volatile

如下由写在书上的笔记整理出来的,前一篇文章就再也不更新了(懒)java

以可见性的讨论开始

可见性和硬件的关联

计算机为了高速访问资源,对内存进行了必定的缓存,但缓存不必定能在各线程(处理器)之间相互通讯,所以在多线程上须要额外注意硬件带来的可见性问题(可能会读到脏数据),注意这里只讨论共享变量下的状况缓存

可能致使的问题

处理器不直接与主内存执行读写操做(慢),而是经过寄存器/写缓冲器/高速缓存/无效化队列等部件执行,解决一个问题的同时会产生更多的问题,所以多线程下会致使如下问题安全

1.不可访问:线程所共享的变量分配到寄存器中数据结构

2.不可同步:县城所共享变量只更新到写缓冲器中,未到达高速缓冲多线程

3.可同步,但需经过缓存一致性协议:总算写入到高速缓存中,但其余处理器把该更新通知的内容存入无效化队列(x86并无该部件)并发

缓存一致性协议

先说疗效:对于某个处理器,经过该协议,该协议可读取其余处理器的高速缓存工具

咱们称一个处理器从自身缓存之外的其它存储部件读取的数据并更新到该处理器的高速缓存成为缓存同步性能

所以缓存同步能使一个线程(处理器)读取到其它线程(处理器)的共享变量,也就保障了可见性测试

缓存一致性协议就是一种缓存同步的方法优化

缓存一致性协议作到的事情:

1.冲刷缓存:处理器的更新最终写入到(该处理器)的高速缓存或主内存

2.刷新缓存:处理器读取时必须从其余高速缓存/主内存对应的变量进行缓存同步

更为具体的内部实现Chapter.11有提到

Java上的体现

volatile的做用之一即是写进行冲刷缓存,读进行冲刷缓存,以达到保证线程可见性的目的

(另外的做用是提示JIT不要乱优化,这是有序性上的问题,通常指令重排序由JIT引发)

轮到volatile

可见性的程度

前面提到volatile是调用缓存一致性在Java中的体现,保障了可见性,但可见到什么程度?咱们须要注意这个问题

给出答案:咱们能保障的可见性仅是读取到共享变量的相对新值,而并不是最新值

相对新值:线程更新值后,其余线程能读取到更新后的值

最新值:读取变量的线程在读取时其余线程没法对该值更新

举例

我对书上P53的例子作出必定修改来讲明上面的问题

假设a为volatile int型共享变量,初值为0,先开启两个线程

处理器0 处理器1
时刻0 null(a此时为0) null(a此时为0)
时刻1 a=1 无关操做
时刻2 无关操做 b=a+1

从时刻2来看,处理器1能看到此时a必然为1,由于时刻1以后其余处理器均能看到更新后的值,所以b=2

处理器0 处理器1
时刻0 null(a此时为0) null(a此时为0)
时刻1 a=1 无关操做
时刻2 a=2 b=a+1

但若是是时刻2中处理器1在读取a的同时,处理器0也在更新,那么此时a便没法由可见性确认是1仍是2,所以b没法肯定

题外话:Java还规定了子线程对建立前的父线程更新的可见性,所以时刻1的读操做前不管是否有volatile均可得知a=0

题外话2:若是a仅为int型,咱们只能确保其中的原子性,在表1的时刻2的处理器1看来a多是0或者1

题外话3:若是a为long型,咱们甚至没法确保原子性,在大数值时可能会产生一个不存在的数(区分高低32位)

解决重排序

volatile解决重排序除了软件顶层提醒JIT的优化之外,还会对读写操做设置不一样的内存屏障禁止存储子系统重排序,有待更新

volatile的性能

从性能层面来讲,volatile暴打内部锁是没问题的,缘由以下

1.没有上下文切换的开销

2.没有锁的申请

但和普通变量相比,它依然有所不足:

1.读写会冲刷/刷新缓存

2.变量确定不会暂存于寄存器,最多也就在L1

所以对于极为频繁的读操做,仍是要打折扣的

(至于量化测试,待我学有所成再说吧)

做用总结

1.可见性,我已经(尽我所能)说明了

2.有序性,因为Java中其它单独控制有序性的工具没有别的(final我会后续补充)

3.极为有限的原子性,volatile在规定上保证longdouble的读写原子性,以及任意操做只与自身相关的原子性

volatile的读和写

读:做为读的使用,咱们是能够放心的,由于它注定只涉及自身相关,前面提到了,原子性也是能够保证的

写:写仅当不涉及共享变量时才确保原子性。具体以volatile a为例:

1.首先多个线程写入不共享也会保证原子性,好比a=3,由于最后一步必成功(必保证单一写的原子性,而3可认为是immutable)

2.即便只涉及自身的运算也不必定线程安全,由于a自身即是共享变量,volatile并不保证赋值(涉及到读共享变量和写共享变量)必定具备原子性,好比a++即是线程不安全的

针对写的不足,能够采用以下方法

1.部分加锁,可利用对读直接返回,对写加锁进行处理,好比读写锁的实现、单例模式的实现

2.CAS解决a++问题,而这即是Atomic类的实现思路

volatile的使用场合

1.做为某个通知变量,只读并输出

2.部分代替锁,对于建立新的对象,如volatile Map map = new HashMap(),该操做会分为3个步骤,分配HashMap.class所需的空间、初始化引用对象、将对象引用写入变量map。注意到前面两个步骤依然是只涉及局部变量,而最后的写操做也必然保证原子性,所以该赋值是原子性的。假若设计一个类封装许多变量,读是并发的,但写仿照该方式来赋值,那也无需任何加锁

3.做为锁的部分优化,好比前面提到的读写锁,对读并发,对写独占,虽然不足是有的,但仍是比锁厉害

final的多线程用法

在语义上,volatilefinal是不可共存的,所以final在设计上也须要线程安全的某种保障,使人惊异的是它具备有序性却没有可见性和原子性,这种设计和安全发布有必定关联

安全发布

以前曾经遇到private的逸出操做,深感本身菜到不行,由于这是常见的getter暴露对象的作法,但本篇重点不在这里,相关的内容放到之后总结

这里要提的是初始化安全问题,new的操做涉及三个步骤,1分配空间2初始化3引用写入,但重排序会致使2和3的步骤不一致,可能发布后对象内某个变量依然没有初始化完毕(或者不可见)

final就保证了引用写入前变量必然初始化完毕,这里须要注意的是,final不保证可见性但必须保证有序性,我的认为缘由以下:

1.对于单一线程来讲,有序性是无需讨论的,而多线程意义上,判断一个对象是否初始化想必是使用obj == null来判断,即便它不可见,但也不影响当其它线程可见时该final对象必然初始化完毕这个结论,更况且final的意义就在于发布,所以是否可见已经无所谓了

2.若是不保证有序性,其它线程无从判断是否彻底初始化完毕,好比上面的例子,虽然只是部分初始化,但它确实不是null,线程安全没法保证

3.那能不能只保证可见但不保证有序,我认为不能,起码Java没这操做

题外话,static能保证读线程必然读到初始值,也算是一种有限的可见性,该初始化可能致使上下文切换

CAS

CAS可认为是硬件锁,能实现C和S的原子性(由硬件大哥保证),通常搭配volatile使用

我的感悟是其特色是不阻塞,一般用于单一可变变量的判断,好比如何在多线程下仅多开启一个线程?你能够用AtomicBooleantruefalse的CAS来轻松实现(原子性保证只有一个线程成功,其他线程均告失败但毫不阻塞),注意这个操做仅靠bool是没法完成的,由于原子(判)+原子(写)≠原子

喜闻乐见的ABA可用相似时间戳的方法解决(其中ABA在数据结构上引起的问题挺有意思的,随便搜搜就有),MySQL也有相似操做(乐观锁),不写了bye

相关文章
相关标签/搜索