多线程之Volatile与CAS原理
1、Volatile
vilatile有两个作用:
- 保证线程可见性
- 禁止指令重排序
可见性:
会将堆内存中的值copy一份到自己的工作线程中,如果要改,那么先改工作内存中的值,再改堆内存中的值,但是另外一个线程读这个变量的时候,读的是没有更改的时候的值,什么时候读新的值,不好控制。加了voliate,一个线程堆变量的改变,另外一个线程可以看到。
可见性的本质:
是使用了CPU的缓存一致性协议MESI。由于不同的线程运行到不同CPU上,所以不同的CPU也需要缓存,用的是CPU的缓存一致性协才能保证这个值的可见。
有序性:
和CPU有关系,将指令并发的执行,这种架构需要编译器将指令重新排序。 有个例子,DCL单例模式。
有序性本质:
底层原理是加了读屏障与写屏障loadfence,storefance。
单例模式可以分为三种形式,饿汉式、懒汉式以及DCL双重检查。
首先是饿汉式的:
懒汉式的:
但是懒汉式的线程不安全,两个线程会发现同时进入了if后面,同时new了新的instance,可以加锁解决,但是要确保锁的粒度小一些,这就是DCL。
需要加个volatile,不加会出现指令重排序。因为new的时候,指令分为3步,第一步为申请内存(值为默认值),第二步给成员变量初始化,第三步将内存的内容赋值给instance。
如果指令重排序,那么会instance指向了第一步的默认值。(超高级并发才能出现这种情况)
2、CAS
cas又称无锁优化、自旋锁。
最经典的Atomicinteger,使用CAS表示线程安全的类。
CAS原理: 内部调用unsafe类的compareandset方法。
CAS有三个参数,1.要改的值V 2.期望当前的值E 3.设定的新值 CAS是CPU原语的支持,是指令级别,不可被打断。所以在判断V == E时,不可能又另外的线程去改。
如果往hotspot的C源码看下,会发现其实CAS底层原语仍旧是用lock cxg命令的,其实也是从原语层面上了锁。
CAS有个问题:
ABA问题,1变成2,又变成了1,对外不可见。怎么解决?加个版本号,修改时版本号+1,加version,所以还需要检查version。在java中式用AtomicStampedRefence实现的。如果引用那么会有问题,是int的话没有问题。
Unsafe就像C 或者C++的指针,能释放内存也可以分配内存。(可以直接操作内存)
自增的时候,有三种方法保证一致性,一种synchronized,一种是atomicinteger,还有一种是longadder。
LongAdder用分段锁概念,atomicAdd用的无锁,sync最慢。一般longadder最快。
1000个线程,250个线程锁再第一块,另外250个锁再第二块。整体相加就得出(但是蛇和线程特别多的情况,如果少了则没有优势)