线程并发--原子变量解决自增自减原子性问题

前言

    线程并发问题一直都是面试的时候经常问的问题,为什么那些面试官、老总喜欢问这些问题呢,因为多线程运行起来要比快呀?那多线程就真的要比单线程快?在我看来未必,因为多线程存在上下文切换[1]、线程死锁、以及一些受限于硬件的问题。所以今天我们就面试当中的一些问题,一起来学习并解决线程并发问题。

自增自减原子性问题

    曾经我遇到过这样的一个问题[在我刚刚毕业出来面试的时候,曾经遇到过这样的问题,你觉得在多线程的情况下i++数据安全么?我当时是这样想的,你问我肯定是需要问为什么的,如果我说安全那就没什么必要说为什么了,所以我当时的回答是不安全的,但是为什么我就回答出来了,所以回家后我就立志要这个问什么弄明白。]:你觉得在多线程的情况下i++数据安全么?
    答案是不安全的。
    现在我们一起来分析一下为什么?
    首先i++;它本身不仅仅是表面上看上去只有一个操作,实际上它是一个“读取 - 修改 - 写入”的操作,i++可以看做一下三步操作:

  • 先是将i的值从内存中拿出来;
  • 然后将i的值加1;
  • 最后将加1后的值存放到i的内存中;
    所以i++存在原子性问题[2]。

解决方案有两个:

  • 方案1:使用synchronized加锁,使i的数据同步,但是这个方式简单是简单,比较耗资源,在多线程竞争资源的时候,加锁、释放锁,线程之间不停的切换会引起性能问题。一个线程持有锁,其它线程需要等到的这把锁之后才能执行,期间线程是被挂起,容易导致死锁发生。
  • 方案2:使用原子变量[3]AtomicInteger定义i.
    在i++原子性问题中,方案2对比起方案1更加优。

原子变量AtomicInteger通过以下两个实现方式保证线程安全:

1.封装了volatile修饰的变量,使内存可见
2.方法都实现了CAS算法来实现非加锁的原子操作。

volatile轻量级锁的原理

    volatile关键字可以保证多线程之间的数据可见性。其实就是一个线程修改的结果,另一个线程马上就能看到。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。

没有volatile修饰的变量内存分析:


image.png

    线程从内存中读取没有volatile修饰的变量的数据,都需要经过CPU的缓存,先从主内存中读取到CPU缓存中,然后线程从CPU缓冲中读取数据
volatile修饰的变量内存分析:


image.png

    线程从内存中读取volatile修饰的变量的数据,直接从主内存中获取数据,不需要经过CPU缓存,这样使得多线程获取的数据都是一致的。

volatile不能够替代synchronized,原因有两点:

  • 对于多线程,不是一种互斥关系
  • 不能保证变量状态的“原子性操作”

所以为了保证AtomicInteger变量的安全,引入了CAS算法。

CAS算法

资料上面是这样描述CAS算法的:
    CAS (Compare-And-Swap) 是一种硬件对并发的支持,针对多处理器 操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问。

什么意思?我们来看看AtomicInteger类中的自增方法:

public final int getAndIncrement() {
       for (;;) {
           int current = get();
           int next = current + 1;
           if (compareAndSet(current, next))
               return current;
       }
 }

它做了三件事情:
1.使用get方法获取主内存中的当前value值,get方法的实现。

public final int get() {
     return value;
}

2.当前值current 加1;
3.比较当前值current 是否和加1之后的next值相等,如果不相等就继续下一次循环,直到current和 next相等。那么也就是主内存中的值+1成功之后结束CAS操作.compareAndSet方法如下:

public final boolean compareAndSet(int expect, int update) {
     return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

    其中,compareAndSwap方法中调用的compareAndSwapInt方法是JNI(Java Native Interface,JAVA本地调用)的代码,借助C来调用CPU底层指令实现的,目的为了比较内存中的值this和expect是否一致,一致则将this修改为update并返回true,通过这样的方式将内存中的值修改为update。
如果用java代码来模拟,表示如下:

if (this == expect) {
    this = update;
    return true;
} else {
    return false;
}

    这个CAS算法确实比synchronized修饰符更为高效的解决原子性问题,但是为什么synchronized没有被取代,也就说明CAS算法并不完美,它存在以下3点问题:
1)ABA问题:
    ABA这个名字听着有点玄乎,其实就是说内存修改了,线程都不知道。为什么出现这样的情况,我们一起来分析一下:

    场景:现在内存中的值value为10,有两个线程,线程1将value 的值修改为20,线程2将value的值修改为30。

    时间点1:线程1获取value=10
    时间点2:线程2获取value=10
    时间点3:线程2比较value和修改值是否一致 10!=30,value=30
    时间点4:线程1 比较value和修改值是否一致 30!=20,value=20

    上述的案例最终线程1将value修改为20,但是中间线程2将value修改为30,这个线程1是无法感知的。这就是我们说的ABA问题。我们可以通过AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

2)循环时间长开销大:
    getAndIncrement方法中我们可以看到它利用了死循环,如果判断一直不为true结束方法,代码就一直执行到判断为true为止,这样compareAndSwapInt一直被调用,增加了CUP 的使用率,资源开销大。

3)只能保证一个共享变量的原子操作
    如果多个变量就无法保证原子性了,但是这个问题后面出现了AtomicReference类保证多个变量的原子性,就是将多个变量封装到一个对象中,使用对象进行CAS算法操作。

【相关词汇】

[1]上下文切换:
单个CPU运行多个线程,由于时间片比较短,也就是代码运行时间,一般在几十毫秒,所以多个线程之间不停的切换,这个过程就叫做上下文切换。

[2]原子性问题:
原子是世界上的最小单位,具有不可分割性。比如 a=0;这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:a++;这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。

[3]原子变量:
java的concurrent包下提供了一些原子类,根据修改的数据类型,可以分为 4 类。
基本类型: AtomicInteger, AtomicLong, AtomicBoolean ;
数组类型: AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray ;
引用类型: AtomicReference, AtomicStampedRerence, AtomicMarkableReference ;
对象的属性修改类型: AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater 。

[4]JNI:
JNI是Java Native Interface的缩写,它提供了很多已经用C或者C++写好的接口给java调用,使得Java代码和其他语言写的代码进行交互。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,029评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,238评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,576评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,214评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,324评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,392评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,416评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,196评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,631评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,919评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,090评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,767评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,410评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,090评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,328评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,952评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,979评论 2 351

推荐阅读更多精彩内容