【Java并发编程】—–深入分析CAS原子操作

本文主要从源码的角度分析JDK中的原子操作的实现原理,并且结合一些简单的例子来说明其使用的场景。主要内容包括一下方面:

  • CAS原理
  • 使用原子操作的好处
  • java.util.atomic包中几个重要类的源码分析

1.CAS原理

CAS的全称为Compare And Set,其作用是对某一个变量进行原子化的更新操作。该算法的思想是:cas(v,e,u);v表示要更新的变量,e表示变量的预期值,u表示变量的新值。当且仅当v的实际值等于e值时,才会将v的值设为u,如果v值和e值不同,则说明已经有其他线程做了更新,则当前线程什么都不做,即更新失败。
CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即时没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
注:CAS其底层是通过CPU的1条指令来完成3个步骤,因此其本身是一个原子性操作,不存在其执行某一个步骤的时候而被中断的可能。

2.使用原子操作的好处

在了解了CAS的原理操作之后,我们下面分析一下使用原子操作的好处,我主要从两个方面出发来考虑这个问题。

2.1 性能角度

当多个线程访问临界区(数据共享的区域)的数据时,如果使用锁来进行并发控制,当某一个线程(T1)抢占到锁之后,那么其他线程再尝试去抢占锁时就会被挂起,当T1释放锁之后,下一个线程(T2)再抢占到锁后并且重新恢复到原来的状态大约需要经过8W个时钟周期。而假设我们业务代码本身并不具备很复杂的操作,执行整个操作可能就花费3-10个时钟周期左右,那么当我们使用无锁操作时,线程T1和线程T2对共享变量进行并发的CAS操作,假设T1成功了,T2最多再执行一次,它执行多次的所消耗的时间远远小于由于线程所挂起到恢复所消耗的时间,它基本不可能运气差到要执行几千次才能完成操作,因此无锁的CAS操作在性能上要比同步锁高很多。

2.2 业务本身的需求

使用同步锁机制锁保证的"先行发生原则(happen before)"过于的粗力度,它虽然可以保证线程T1的操作如果早于线程T2获取锁,那么T1一定在T2之前完成操作;而CAS操作却不能保证这样的顺序的一致性,但是CAS操作保证了关键的修改一步具有先行发生原则。在我们实际的业务场景下,由锁机制保证的这种看似所谓的有序性其实没有太大的意义,因为我们只需保证最终结果的一致性就能满足业务的需要。我们以商品秒杀为例,当多个用户并发访问时,我们其实只需确保的就是其在抢占的那一刻是一个原子操作即可,当商品数目为0时提示操作失败,而无需保证先来的用户一定能够抢到商品。因此,在业务本身的需求上,无锁机制本身就可以满足我们绝不多数的需求,并且在性能上也可以大大的进行提升。
我们可以再举一个生活化的例子来理解无锁的原子化操作与锁的不同,我们使用的版本控制工具与之其实非常的相似,如果使用锁来同步,其实就意味着只能同时一个人对该文件进行修改,此时其他人就无法操作文件,如果生活中真正遇到这样的情况我们一定会觉得非常不方便,而现实中我们其实并不是这样,我们大家都可以修改这个文件,只是谁提交的早,那么他就把他的代码成功提交的版本控制服务器上,其实这一步就对应着一个原子操作,而后操作的人往往却因为冲突而导致提交失败,此时他必须重新更新代码进行再次修改,重新提交。

3.原子类使用及源码分析

接下来就让我们分析一下JDK中的原子类,这些原子操作类都位于java.util.concurrent.atomic包中。首先我们以AtomicInteger为来进行分析。

3.1 AtomicInteger分析

在分析该类之前,首先来看看为什么Java中的++运算符是非原子化操作,可以通过2个角度来证明该结论。

3.1.1 为什么Java中的自增运算符是非原子操作?

package concurrency.unlock;
/**
 * 非线程安全的++操作
 */
public class UnsafeIncr {
    static int count = 0;
    public static void main(String[] args) throws Exception{
        Thread[] threads = new Thread[100];
        for(int i = 0 ;i < threads.length;i++){
             threads[i] = new Thread(){
                public void run() {
                    for(int i = 0;i < 10000;i++){
                        count++;
                    }
                };
            };
            threads[i].start();
        }
        for (Thread thread : threads) {
            thread.join();
        }
        /*
         * 输出结果理论上是1000000,而实际结果总是小于该值
         * 造成的原因是:执行顺序不确定性以及中断的不可预知性产生的数据不一致
         */
        System.out.println(count);
    }
}

执行上面的代码,其结果可能总是小于实际值。这正是由于一个非原子操作在并发执行的情况下所导致。
上面我们通过代码证明了自增操作是非原子化的,我们接下来从字节码指令的角度来证明一下。

public class UnsafeThreadIncr{
    private int count;
    public void incr(){
        count++;
    }
}

将上面的代码进行编译,通过javap -verbose UnsafeThreadIncr.class会得到如下结果:

++操作对应的字节码执行.png

我们可以看到,该操作对应着4条字节码指令,而每条字节码执行>=1机器指令,因此从这里我们也可以很清楚地看到++操作的非原子性。

3.1.2 AtomicIntger源码分析

1-1.png

1-2.png

通过上面的截图,我们可以看到,AtomicInteger底层维护了一个value属性,并且该属性是使用volatile关键字进行修饰,其目的是为了确保内存的可见性,同时防止指令的重排序。通过Unsafe去获取到一 个属性在对象的基地址中的偏移量,之后通过对象的地址+偏移量,从而确定value在内存中的位置,然后完成更新操作。(注:Java之所以比起C而言要安全,其原因就在于其屏蔽了指针的操作,但是通过Unsafe类我们再次看到了指针的影子,因此该操作通过Unsafe这个名字就很好的表达了该操作是一个不安全的操作)
下面我们来看看AtomicInteger中的compareAndSet方法。

1-3.png

通过图1-3我们可以清楚看到,AtomicInteger底层的CAS操作其实是通过Unsafe类来完成的,Unsafe的compareAndSwapInt接受4个参数,第一个参数为要更新的对象,第二个参数为对象中的属性(实际更新的值),第三个参数为期望值,第四个参数为更新值。当且仅当期望值与实际值相等时才更新成功,返回true,否则即更新失败,返回false。
unsafe中的compareAndSwapInt方法是一个native方法,可以通过OpenJDK进行查看。

compareAndSwapInt.png

下面我们再来看看AtomicInteger中是如何如何保证线程安全的加法操作,我们以incrmentAndGet方法为例来说明。

incrementAndGet.png

在AtomicInteger还提供了如下的方法:

方法 说明
public final int get() 取得当前值
public final void set(int newValue) 设置当前值
public final int getAndSet(int newValue) 设置新值,并返回旧值
public final boolean compareAndSet(int expect, int u) 如果当前值为expect,则设置为u,否则则不进行修改
public final int getAndIncrement() 当前值加1,返回旧值
public final int getAndDecrement() 当前值减1,返回旧值
public final int getAndAdd(int delta) 当前值增加delta,返回旧值
public final int incrementAndGet() 当前值加1,返回新值
public final int decrementAndGet() 当前值减1,返回新值
public final int addAndGet(int delta) 当前值增加delta,返回新值

这些方法的具体实现与前面分析的incrementAndGet的实现基本一致,都是通过一个死循环进行反复的进行CAS操作,直到更新成功才返回。

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

推荐阅读更多精彩内容