2018年10月21日星期日

Java中的CAS与ABA问题

今天面试别人的时候,提到了CAS,本来想要引导他说出CAS潜在的ABA问题,发现自己也没发简单的向他解释清楚,需要好好梳理下。

什么是 CAS

维基百科的解释是:
In computer science, compare-and-swap (CAS) is an atomic instruction used in multithreading to achieve synchronization. It compares the contents of a memory location with a given value and, only if they are the same, modifies the contents of that memory location to a new given value. This is done as a single atomic operation. The atomicity guarantees that the new value is calculated based on up-to-date information; if the value had been updated by another thread in the meantime, the write would fail. The result of the operation must indicate whether it performed the substitution; this can be done either with a simple boolean response (this variant is often called compare-and-set), or by returning the value read from the memory location (not the value written to it).
CAS(compare and swap),简单地说就是一种在多线程的情况下,让每个线程修改某个数据是一种原子操作。要实现CAS,有几个关键的值:
  1. 要修改的变量内存中的值V
  2. 更新变量前事先记录的期望值E,取值来自V
  3. 将要更新的值A
一个典型的CAS更新操作如下:
  1. 读取内存中的值V,赋值给E
  2. 更新变量前,比较内存值V与E
  3. 如果V==E,将V更新成A
  4. 如果V!=E,重复步骤1
如此循环,直到步骤3更新操作完成,写成伪代码就是:
do{
    int e=v.getVal();
    synchronized(this){
        if(e==getVal()){
            v.setVal(a);
            break;
        }
    }

}while(true)
CAS在JDK中被广泛应用,比如java.util.concurrent包下面的Lock、AtomicInteger相关的类都有用到。
例如a++;这种操作在Java里面并不是原子操作(包含了值的累加和赋值两个动作),所以并发情况下竞争操作某一个变量,需要用AtomicXXX几个类。

举个例子

AtomicInteger 实现类似a++操作,使用的是它的incrementAndGet 方法,源码很简单:
    /**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        for (;;) {
            int current = get();
            int next = current + 1;
            // 此处的 compareAndSet 由usafe对象提供硬件级别的原子操作
            if (compareAndSet(current, next))
                return next;
        }
    }
Lock中对于竞争变量的CAS,也是类似的操作。

什么是ABA问题

简单地说就是,多个线程同时使用CAS更新数据时:
  1. 线程1要将数据从A变成B时(此时线程1的期待值E=='A')
  2. 其他线程已经抢先更新了变量,把变量从A变成其他值,再变回A(如A->C->A)。
  3. 当线程1用CAS机制准备更新变量时,发现E==A,所以继续更新变量。

这样有什么问题?

虽然变量最终结果是对的,但是线程1更新变量前,变量已经经历了一系列变化才回到原值。对于某些场景,忽略变化会继续进行更新操作,会带来错误的结果。
比如:

银行账户扣费问题

某个银行账户扣款操作,由于系统故障,产生了2个线程(T1,T2)对同一账户进行扣款,正常防重逻辑应该是一个执行成功,另一个失败,但是如果使用上面的CAS操作,就是:
  1. 账户里有100元,需要扣款50元
  2. T1先完成操作,扣款50元,将账户值V改为50
  3. T2准备扣款,当时期待值E==100
  4. 此时有其他转账操作先于T2对V进行累加,比如转入50元,此时V又变成100
  5. T2进行CAS的更新操作,发现E==V==100,执行更新操作,又扣款50

堆栈操作问题

如果CAS中的操作,变量值V是栈顶指针,也会有同样的问题:
  1. 某个堆栈内容是:A-B-C,栈顶为A
  2. 线程1更新前,得到期望值E==A
  3. 其他线程对栈进行进行pop,push操作,pop A B,push D A,此时栈的内容为 A-D-C
  4. 此时栈顶还是A,但是内容已经改变,线程1要更新的堆栈,已经不是第2步拿到期望值E时,自己要操作的那个堆栈了

如何规避

思路也很简单,就是对得到的期望值E和变量值V,增加一个版本号(比如时间戳),对于不同时期操作产生的同一个值的V,版本号是不同的,比较E与V时,需要同时比较版本号。比如juc包的AtomicStampedReference实现:
public class AtomicStampedReference<V> {
    private static class Pair<T> {
        final T reference;  //维护对象引用
        final int stamp;  //用于标志版本
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }
    private volatile Pair<V> pair;
    ....
    /**
      * expectedReference :更新之前的原始值
      * newReference : 将要更新的新值
      * expectedStamp : 期待更新的标志版本
      * newStamp : 将要更新的标志版本
      */
    public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair; //获取当前pair
        return
            expectedReference == current.reference && //原始值等于当前pair的值引用,说明值未变化
            expectedStamp == current.stamp && // 原始标记版本等于当前pair的标记版本,说明标记未变化
            ((newReference == current.reference &&
              newStamp == current.stamp) || // 将要更新的值和标记都没有变化
             casPair(current, Pair.of(newReference, newStamp))); // cas 更新pair
    }
}    

总结

  1. CAS 是一种自旋锁,由一个死循环+compareAndSet 实现。
  2. CAS 存在ABA隐患,对于需要关注竞争变量变化过程(不仅仅是变量的值)的场景,ABA问题必须关注。
  3. 解决ABA问题,需要在CAS的compare过程中,增加对期望值E和当前值V版本号的判断。

2018年10月7日星期日

随笔

国庆假期的最后2天,我基本上是11点前入睡,6点起床,起床后背背单词,做几一些简单的锻炼,这种感觉非常好。 目前来说,自己还做不到严格的自律,每天脑子里还是会时不时地蹦出一些非计划外的想法。但是我发现,如果保持一个良好的作息和精神状态,我是可以一点一点跟自己沟通,说服自己集中精力,抛弃不必要的突发念头。每天给自己设立了一些小目标,每天尽早地完成它,过段时间再review一下看看。 明天就要上班,月底就要离职了,得慢慢地把自己的状态调整回来。

2018年10月5日星期五

黄金周西安游

9月底突发奇想地想国庆和老婆来西安玩一次,于是就匆匆忙忙订了机票和住宿,提前请了2天假29号飞到了西安。28晚上自己刚从北京出差回来,简直马不停蹄。从29号启程到4号回家,我们看了长恨歌演出,逛了西安城区、华山、陕西历史博物馆、华清池、兵马俑、西安城墙。一路上吃吃喝喝,除了在兵马俑见识了千分之一的中国人口以外,其他时候体验不算太差。整座西安城给人一种浓厚的历史氛围和民族融合带来的活力,让沿海长大的我倍感新奇。

9月底10月初的西安,让我一个南方人感受到了秋天。一天气温十几度到二十几度,早晚偏冷,白天晒着太阳也不会满头大汗,真正的秋高气爽。所以每天出门我总是穿着一件薄长袖和夹克外套,热了就把外套脱下别在腰上。值得一提的是,西安城内经常能见到穿着汉服的姑娘小伙,也许是这座城市的道路和建筑本身古色古香,不想在其他地方,走在街上看着他们,竟然不觉得突兀,反而别有一番风味,有种“这果然是长安啊”的感叹。

西安的吃,只要吃得惯的话,简直不要太棒。
6天时间内,我们逛遍了回民街永兴坊,这里的吃的价格也不算便宜,但是分量够大,随便一份面食都是一大碗,我跟老婆为了多尝几口,总是要两人吃一份。第一天老婆同学请吃羊肉泡馍,见识了如何自助徒手撕膜,撕了半个小时手都麻了,煮上羊肉粉丝汤,满满的一大海碗,吃完都快扶着墙出。路边烤羊肉串一串10块,羊肉又大又肥。面馆里的水盆羊肉、biangbiang面、油泼面……每一个都在让我跟米饭说再见。肉夹馍每家卖的都不大一样,什么都能夹,简直西安汉堡。还有永兴坊的子长煎饼,长得跟广东肠粉一样,吃一份得排半小时队。路边随处可见的石榴汁,小镜糕,大甄糕,酸梅汤,花椒酸奶(真加花椒。。),油茶(这个怂了喝不惯),凉粉,酱牛肉,卤羊蹄,羊杂汤……几天下来,自己的味蕾仿佛打开了一个新世界。

由于我们住的酒店离西安城中心挺近的,我们一有空就往钟楼附近跑。钟楼算是西安城区的中心,再加上周围14公里的西安城墙,整个城内古色古香,建筑风格很有特色,满眼望去都是中国传统的中间高两边低的那种屋顶,这种我以前只在电视剧和故宫里看到。路也很宽,在市内骑自行车很舒服,道路横平竖直,房子坐落得也很规整,整个城市颜色跟中国大多数内陆城市一样,整体偏灰色调。虽然国庆路上人多,地上也不见什么垃圾。不知道是不是来的这几天天气好,空气虽然干燥,但是空气还算清新,蓝天白云的天空配合飒爽的秋意给人感觉很好。

这几天出门一般都是打的外加摩拜,出租车司机师傅跟大多数北方司机一样,喜欢唠嗑,平易近人。西安城区的街道方方正正,看好地图的话,骑车走路都不容易迷路(当然回民街还是要多走几次才懂)。比较痛苦的是去周边景区的大巴车,去华山、兵马俑、华清池我们都报的是一日团,一大早就要坐车出发,路上快则两个小时,堵车就要三四个小时,体验不是很好,特别是从兵马俑回来的时候,我们晚上6点返程,回来已经9点半了。

这次行程也算比较紧,第一晚我们去华清池看了长恨歌演出,华山、陕西历史博物馆、兵马俑华清池我们各花了一天时间,最后一天逛了西安城墙,然后在几个晚上陆续逛了钟楼附近和回民街永兴坊,大雁塔之类的景点就没再去了。
长恨歌演出门票260多块钱,需要坐车到华清池景区看(后来想想应该在参观兵马俑华清宫的那天晚上来看比较节省时间)。第一次看这种现场的真人歌舞剧表演,比较新鲜,有种春晚现场的感觉,大体上就是用现代声光电技术演了一出杨贵妃唐玄宗的爱情故事,后面还有鹊桥相会啥的(能把一个公公儿媳扒灰的故事写的如此美好也是囧。。)。可惜我艺术功底不够深刻,只能跟周围的大叔大妈看个新鲜。
第二天一大早坐大巴来登华山,两人比较怂,选择了缆车路线,西峰上北峰下,5个小时爬了华山西峰南峰。一整块花岗岩形成的华山,挺拔陡峭,地势险峻,山上很多台阶不到半个脚宽。站在华山上,俯瞰山下,真的会让人感叹祖国大好河山,雄伟瑰丽。可惜最近上班忽略了身体锻炼,西峰爬了不到一半就气喘吁吁,追不上老婆的步伐。看来回去还是要好好锻炼,有生之年要从山底下上一次,来东峰看日出。
第三天鉴于前一天爬华山太累,就选择了市内的陕西历史博物馆,还好提前订了30块钱的大唐珍宝馆门票,免去了排长队进馆的痛苦。陕博不愧为中国博物馆的Top,文物之多,时间跨度之长令人叹为观止。
第四天我们又跟了一日团参观了华清池和兵马俑,这一天算是体验最差的一天。华清池参观什么呢,就是看看以前杨贵妃杨玄宗洗澡的澡堂子,还有西安事变老蒋被抓之前的故居,都是历史遗迹,想要知道以前的情景,只能对着景观配合导游解说发挥想象力了。下午的兵马俑参观让我体验了什么是“世界第一大坑”。据说当天下午兵马俑的客流量达到了15万,从进博物馆大门到走到展馆门口,浩浩荡荡都是人头,排队的时候前胸贴后背,整个人被后面推着走。要进一号坑之前更是痛苦,博物馆方管理太混乱,三队人马三个方向进一个门,其中不乏插队的低素质游客,大家互不相让,差点爆发肢体冲突。进坑之后人山人海,根本看不到前面,人在队伍中,不由自主地被后面人推着走,还没仔细看完兵马俑的细节,就被推着到出口了。
第五天西安城墙游算是最满意的一天,跟老婆吃完一顿早午饭,慢慢悠悠地买上一张上城墙的票,租上一辆双人自行车,开始休闲游。中午到下午这段时间城墙上人不多,西安城墙虽然不是水泥路,但是一路上没有什么坡度,也足够宽敞,3个小时车程下来,轻轻松松绕城墙骑行了一圈半,两边都是充满古城特色的房子和风景,伴着凉爽的秋意,一扫前几天旅行的疲惫。

写在旅行结束后

最后坐飞机回来前,老婆问我,西安比起以前去过的重庆和成都怎么样?我觉得除了兵马俑那天的大坑以外,西安还是让我非常喜欢的。较慢的生活节奏,好吃的各种美食,浓厚的历史底蕴,人们美好的精神面貌。甚至我都觉得,下半辈子我们应该好好锻炼身体,攒钱来西安置业养老(笑)。人生不应该只有生活工作的那三点一线,应该多一点不一样的经历。相信有生之年,我还会再来几次。












Effective Java 3rd edition 读书笔记

最近把去年出版的 Effective Java 3rd Edition 的新章节读完了,把笔记统一整理一下。 Lambdas and Streams Item 42: Prefer lambdas to anonymous class Java ...