并发之atomic与CAS自旋锁

通过前几章的讲解我们知道i++这种类似操作是不安全的。针对这种情况,我们可能会想到利用synchronize关键字实现线程同步,保证++操作的原子性,的确这是一种有效的方法,但我们还有一种选择--AtomicInteger。

AtomicInteger解析

源码

成员解析


private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset; static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
} private volatile int value;
  1. Unsafe 类是用来在任意内存地址位置处读写数据,可见,对于普通用户来说,使用起来还是比较危险的。
  2. valueOffset 表示该变量在内存中的地址。
  3. value;当前类中存储的值

方法

两个构造方法,赋初始值,不传参数则默认为0
public AtomicInteger(int initialValue) {
value = initialValue;
}
public AtomicInteger() {
}
取值和设置值

这个值是volatile的所以是能保证可见性的

public final int get() {
return value;
} public final void set(int newValue) {
value = newValue;
}
特殊取值和赋值。

该方法时用常规方式赋值,不能保证变量可见性。

public final void lazySet(int newValue) {
unsafe.putOrderedInt(this, valueOffset, newValue);
}

既然用volatile修饰了那么常规赋值方式到底有什么用呢?

首先volatile关键字这一章中将到

  1. volatile变量的读操作,会强制使CPU缓存失效,强制从内存读取变量。
  2. 写volatile变量,会强制刷新CPU写缓冲区,把缓存数据写到主内存。

    而这些都是通过内存屏障实现的。如果该AtomicInteger在锁中,而锁住的代码块并不会产生可见性问题,那么volatile的操作会让系统添加多个屏障。而使用lazySet()就是在不需要让共享变量的修改立刻让其他线程可见的时候,以设置普通变量的方式来修改共享状态,可以减少不必要的内存屏障,从而提高程序执行的效率

该方法将新值设置进去的同时将旧值返回回来

public final int getAndSet(int newValue) {
return unsafe.getAndSetInt(this, valueOffset, newValue);
}

比较期望值并且更新该值 原子操作

public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
//示例
@Test
public void compareAndSetTest(){
AtomicInteger a = new AtomicInteger(10);
a.compareAndSet(10,15);
log.info("{}",a.get());
a.weakCompareAndSet(10,12);
log.info("{}",a.get());
}

结果:

13:42:36.391 [main] INFO atomic.AtomicIntegerApiExample - 15

13:42:36.404 [main] INFO atomic.AtomicIntegerApiExample - 15

可以看到第二个语句没有替换成功

比较并且更新,和lazySet一样用于锁中
public final boolean weakCompareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
原子减操作
//先获取后再加1
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
//先加后获取
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
} //减1操作
public final int getAndDecrement() {
return unsafe.getAndAddInt(this, valueOffset, -1);
}
//先减1然后获取
public final int decrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
}

对数据执行方法更新。

传入一个lambda算法,然后按照该算法更新数据。将当前数据更新为

public final int updateAndGet(IntUnaryOperator updateFunction) {
int prev, next;
do {
//取值
prev = get();
//做操作
next = updateFunction.applyAsInt(prev);
//原子替换
} while (!compareAndSet(prev, next));
return next;
}
//演示
//读取atomicInteger里面的值做lambda表达式的操作。
//
public void updateAndGetTest(){
AtomicInteger atomicInteger = new AtomicInteger(10);
IntUnaryOperator updateFunction = (a)->a*5+3;
atomicInteger.updateAndGet(updateFunction);
assert Objects.equals(atomicInteger.get(),53);
}
两个参数负责运算。
public final int getAndAccumulate(int x,
IntBinaryOperator accumulatorFunction) {
int prev, next;
do {
prev = get();
next = accumulatorFunction.applyAsInt(prev, x);
} while (!compareAndSet(prev, next));
return prev;
}
public final int accumulateAndGet(int x,
IntBinaryOperator accumulatorFunction) {
int prev, next;
do {
prev = get();
next = accumulatorFunction.applyAsInt(prev, x);
} while (!compareAndSet(prev, next));
return next;
} //示例 @Test
public void getAndAccumulateTest(){
AtomicInteger atomicInteger = new AtomicInteger(10);
IntBinaryOperator intBinaryOperator = (a,b)->(b-a)*2;
atomicInteger.accumulateAndGet(2,intBinaryOperator);
System.out.println(atomicInteger.get());
}
字符串方法 取值方法
public String toString() {
return Integer.toString(get());
}
public int intValue() {
return get();
}
public long longValue() {
return (long)get();
}
public float floatValue() {
return (float)get();
}
public double doubleValue() {
return (double)get();
}

CAS机制

定义

比如自增1算法中

public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}

AndAddInt的底层逻辑是什么

public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5;
}

我们看到了一个方法的调用compareAndSwap,取这三个单词的首字母即为CAS。

他具体是怎么操作的呢。

var1 AtomicInteger对象本身。

var2 该对象值得引用地址。

var4 需要变动的数量。

var5 是用过var1 var2 找出的主内存中真实的值。

用该对象当前的值与var5比较,如果相同 更新var5+var4并且返回true,如果不相同继续取值然后在比较,直到更新完成。

简单的来说:

CAS (CompareAndSwap) 比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存中的值一致为止.

这个CAS方法是原子性的在加上该变量由于使用了volatile修饰所以也可以保证可见性和有序性,可以模拟一个锁。通过这种比较的方式可以有效的减少锁带来的性能降低的问题。

问题

这么做并不是十全十美的,这里有2个比较大的问题。

并发竞争

在并发竞争比较严重的场合会严重的增加cpu的运算量。

在并发竞争不严重的场合中CAS的并发处理速度是远远高于锁的,但是在竞争比较严重的场合中,因为每个线程都在执行循环判断,所以会消耗大量cpu的运算能力。在这种情况下synchronized反而是一个更好的选择。

ABA

可能出现了一个线程修改了该数据另外一个线程又修改了回来,第三个线程并没有发现该变量变化过了。即ABA问题。

我们先来看一个例子:

private static AtomicInteger atomicInt = new AtomicInteger(100);

public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(()->{
//进行一次ABA操作
atomicInt.compareAndSet(100, 101);
atomicInt.compareAndSet(101, 100);
});
executorService.execute(()->{
try {
//等待ABA执行完毕
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//判断ABA向他婆娘个则替换 返回替换结果
boolean c3 = atomicInt.compareAndSet(100, 101);
//true
System.out.println(c3);
});
executorService.shutdown();
}

从上述例子中可以看出atomicInt并没有意识到自己的100与原始的100有什么差别,并且被替换成功。

举个现实中的栗子:你非常渴的情况下你发现一个盛满水的杯子,你一饮而尽。之后再给杯子里重新倒满水然后下无色无味的毒。然后你离开,当杯子的真正主人回来时看到杯子还是盛满水,他当然不知道是否被人喝完重新倒满。他只知道杯子和水还是原来的样子。这是他喝下了水,中毒了。

这里就引申出了一个问题:ABA到底会发生在什么样的数据结构中。会产生错误呢呢?

@Test
public void testABA(){
AtomicInteger a = new AtomicInteger(10);
int x = a.hashCode();
log.info("a= {} code={}",a.get(),x);
AtomicInteger b = a;
a.set(20);
int y = a.hashCode();
log.info("a= {} code={}",a.get(),y);
log.info("res={}",b.equals(a));
}

11:53:24.257 [main] INFO atomic.AtomicIntegerApiExample - a= 10 code=2007331442

11:53:24.267 [main] INFO atomic.AtomicIntegerApiExample - a= 20 code=2007331442

11:53:24.267 [main] INFO atomic.AtomicIntegerApiExample - res=true

通过上述的例子可以看到a引用并没有变化,但是引用的对象其中的值发生了变化那么实际上他a的应用还是a的引用然而引用的对象却不是原始对象了。这就好比被"调包“了。

当然ABA 问题,表面上上不会影响你的业务逻辑,但是在有些情况下,发生这种中途 “调包” 的事情,还是会有影响。

如何避免ABA问题:

通常的做法是在CAS的每一次操作上添加一个版本号。每次不仅比较引用而且需要对比版本号。当两者都相同时才能确认A就是A,而不是ABA。

幸好JDK中提供了AtomicStampedReference用于解决ABA的问题。

//值以及初级版本号
private static AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<Integer>(100, 0);
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(()->{
atomicStampedRef.compareAndSet(100, 101,
atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
atomicStampedRef.compareAndSet(101, 100,
atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
});
executorService.execute(()->{
int stamp = atomicStampedRef.getStamp();
// stamp = 0
System.out.println("before sleep : stamp = " + stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("after sleep : stamp = " + atomicStampedRef.getStamp());//stamp = 1
boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp+1);
System.out.println(c3); //false });
executorService.shutdown();
}

结果

before sleep : stamp = 0

after sleep : stamp = 2

false

我们可以看到版本号由0变成了2

结果也是不相同。可以说缓解了ABA的问题。

总结

首先将了AtomicInteger的API解释了一遍 然后通过了解该类时如何保证并发的,引出了CAS。然后解释了cas原理用法以及CAS的问题并演示了demo.

这里的CAS机制是一种自旋锁。通过不停的自旋获取更新锁。

并发之atomicInteger与CAS机制的更多相关文章

  1. 高并发之CAS机制和ABA问题

    什么是CAS机制 CAS是英文单词Compare and Swap的缩写,翻译过来就是比较并替换 CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B. 看如下几个例子: pac ...

  2. 深入浅出Java并发包—CAS机制

    在JDK1.5之前.Java主要靠synchronized这个关键字保证同步,已解决多线程下的线程不安全问题,但是这会导致锁的发生,会引发一些个性能问题. 锁主要存在一下问题 (1)在多线程竞争下,加 ...

  3. CAS机制与自旋锁

    CAS(Compare-and-Swap),即比较并替换,java并发包中许多Atomic的类的底层原理都是CAS. 它的功能是判断内存中某个地址的值是否为预期值,如果是就改变成新值,整个过程具有原子 ...

  4. 什么是CAS机制?(转)

    围绕下面四个点展开叙述: 一:什么是CAS机制? 二:Java当中CAS的底层实现 三:CAS的ABA问题和解决方法 四:java8对CAS的优化 一:什么是CAS机制? 我们先看一段代码: 启动两个 ...

  5. Java CAS同步机制 原理详解(为什么并发环境下的COUNT自增操作不安全): Atomic原子类底层用的不是传统意义的锁机制,而是无锁化的CAS机制,通过CAS机制保证多线程修改一个数值的安全性。

    精彩理解:  https://www.jianshu.com/p/21be831e851e ;  https://blog.csdn.net/heyutao007/article/details/19 ...

  6. (白话理解)CAS机制

    (白话理解)CAS机制 通过一段对话我们来了解cas用意 示例程序:启动两个线程,每个线程中让静态变量count循环累加100次. 最终输出的count结果是什么呢?一定会是200吗? 加了同步锁之后 ...

  7. 对CAS机制的理解(一)

    先看一段代码:启动两个线程,每个线程中让静态变量count循环累加100次. public class CountTest { public static int count = 0; public ...

  8. 对CAS机制的理解(二)

    一.Java当中CAS的底层实现首先看看AtomicInteger的源码,AtomicInteger中常用的自增方法 incrementAndGet: public final int increme ...

  9. 并发之AtomicInteger

    并发之AtomicInteger 1 java.util.concurrent.atomic概要     在java.util.concurrent.atomic包下存在着18个类,其中Integer ...

随机推荐

  1. django分页功能

    采用django自带的Paginator功能 from django.core.paginator import Paginator food = foodInfo.objects.filter(fo ...

  2. 我的 $OI$, 退役前写点东西

    离 \(NOIp2018\) 还有五天, 总想写点什么 马上退役了啊 是什么时候喜欢上信息技术的呢 记不清了, 很小的时候就喜欢捣鼓关于电脑的东西 当时也不知道有算法这种东西 只是知道有黑客 巨 j8 ...

  3. Jenkins自动发布代码实战篇

    Jenkins自动发布代码实战篇 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任.  一.Jenkins服务器配置秘钥对并上传到Gitlab中 1>.在Jenkins后端生成秘钥 ...

  4. JAVA记录-java代码优化策略

    java代码优化策略 1.生成对象时,合理分配空间和大小:new ArrayList(100); 2.优化for循环: Vector vect = new Vector(1000); For(int ...

  5. CM记录-操作系统调优

    1.避免使用swap分区---将hadoop守护进程的数据交换到磁盘的行为可能会导致操作超时:物理内存(交换)--Swap分区 2.调整内存分配策略---操作系统内核根据vm.overcommit_m ...

  6. Centos7更改yum镜像源

    1. 备份本地yum源 mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo_bak 2.获取阿里yum源配置文 ...

  7. 如何创建带有大纲和书签的交互式web报表

    交互式报表允许用户与之交互.例如,报表可以包含超链接.书签和大纲.通过点击大纲部分的标题,你可以将书签导航到报表中的所需位置.这样的报表经常用在产品目录中.(查看更多web报表教程) 让我们为Web创 ...

  8. utf8_bin跟utf8_general_ci的区别

    ci是 case insensitive, 即 "大小写不敏感", a 和 A 会在字符判断中会被当做一样的; bin 是二进制, a 和 A 会别区别对待. 例如你运行: SEL ...

  9. Tomcat中配置URIEncoding="UTF-8"来处理中文的方法

    http://www.cnblogs.com/seabird1979/p/4837237.htmlTomcat中配置URIEncoding="UTF-8"来处理中文的处理打开 se ...

  10. VS2015 与 Git 的简单使用

    前言 在白忙之中抽了点时间,记录了下 VS 与 Git 的简单使用. 在之前使用命令行的时候,提交或拉取代码时,总报错:(提取时遇到错误: Unsupported URL protocol),后来在网 ...