文章目录
volatile为什么不能保证原子性?
1.首先volatile可以保证变量每次读的都是最新值。但是在并发操作中,向i++,这种操作不是原子性,因为这里涉及到了多个操作,1.线程A 获取i的值100,这个时候获取的是最新值,此时由于线程阻塞等原因,线程B获取的最新值100,然后将i 自增到101,更新到内存。
此时线程A又来进行一次自增操作,又将101更新到内存。两个自增结果都是101,跟我们预期不一致,所以并不是原子操作。但是,每次获取值都是最新的,volatile保证了可见了,保证每次值的更新都更新到了主存,每次获取到的都是最新值
cas 原理?
cas原子性原理
将预期值和内存的值进行比较,如果两者相等就进行更新操作,更新操作是通过unsafe调用的操作系统底层的方法,大概是cpu会将缓存的值进行加一个总线锁,保证数据操作的原子性。
cas如何保证可见性?
我们通过cas处理的变量,可以设置为volatile,cpu底层会采用内存屏障保证数据可见性,cpu中有个高速缓存,每次更新值,会直接更新到主内存,刷新高速缓存。读取值的时候,会直接从主存读取数据
cas缺点
缺点有三。
1.cas是一种类似乐观锁的时间,如果资源的竞争比较激烈。那么每次都到内存做cas判断,会想好资源。这时候可能还没有悲观锁的好。
2.cas可能存在ABA的问题,第一:线程 1 和线程2都读取值 A,但是线程1执行很快,将值更新为B,一番操作后,线程2又更新为A,这时候线程1进行cas操作,发现内存中是A,然后继续操作。所以也是执行成功,但是整个过程有问题,因为数据做了变更,但是线程1却不知道。
解决办法: java中提供了一个AtomicStampedReference来处理ABA问题,相当于在对象中额外增加一个标记来标记对象是否有过变更。
3.cas只能保证单个标量的原子性,却不能同时保证多个变量的原子性,可以将把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij
在java的并发包中,通常用cas配合变量的volatile,保证数据操作的原子性可见性。
CAS包含哪些参数:
unsafe.compareAndSwapObject(this, headOffset, null, update)
三个运算符:
-
个是this,也就是unsafe这个对象
-
一个内存地址V
-
一个期望的值A
-
一个新值B
-
基本思路:
如果地址V上的值和期望的值A相等,就给地址V赋值新值B,如果不是,不做任何操作
循环CAS:
在一个(死)循环中里不断进行CAS操作,直到成功为止(自旋操作即死循环)
每次操作都取出最新值跟内存中的比较,cas比较失败,就从新拿最新值进行比较。比如在可重入锁的节点加入队列时候,用到自旋的cas。缺点是竞争比较高时候,无效的自旋操作浪费性能。
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
double check 单例模式中,变量为什么定义成volatile?
比如定义一个person: Person p = new Person().
这里大概涉及到三个操作。
第一:new person 在内存开辟空间。
第二:调用person构造方法
第三:将new person的引用复制给变量p.
由于jvm的内部优化,执行过程可能会指令重排。 也就是说,将变量的赋值操作和对象的构造方法操作,可能会重排序,那么double check在并发的时候,可能拿到的一个空的对象,也就是没有属性值的对象。
所以可以使用volatile,禁止指令重排序,相当于是加了一个内存屏障,数据每次读都是从主存中读,写会更新到主存,刷新缓存。
但是volatile不能保证原子,类似变量的自增操作这种,不是一个原子操作。只能保证每次读的都是最新数据,不会说存在线程A更新了,线程B却读的老的。