[Java]无锁操作与CAS

By | 2019年8月26日

最近在看并发编程的相关内容,在分析ConcurrentHashMap(JDK 1.8版本)源码时,发现其效率高的很重要的原因之一,就是使用了CAS无锁操作——Compare and swap操作。之前不是很了解CAS操作,于是今天详细研究了一下CAS操作。

在介绍CAS之前,先来看一个多线程的例子。

class Increment implements Runnable{
    private static volatile int value = 0;

    public static int getValue() {
        return value;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10000; ++i) {
            value++;
        }
    }
}
class AtomicIntegerStudy {
    private static final int THREADS_COUNT = 20;

    public static void test01() {
        Thread[] threads = new Thread[THREADS_COUNT];
        for (int i = 0; i < THREADS_COUNT; ++i) {
            threads[i] = new Thread(new Increment());
            threads[i].start();
        }

        for (int i = 0; i < THREADS_COUNT; ++i) {
            try {
                threads[i].join();
            } catch (Exception e) {

            }
        }

        System.out.println("result: " + Increment.getValue());
        /* 命令行输出
        result: 76777

        注:无论如何是一个小于200,000的数字
         */
    }
}

有过多线程基础的同学都能看出,AtomicIntegerStudy.test01()最终的结果一定不是200,000。多个线程修改Increment类中的value变量时,一定会有相互覆盖的情况,所以最后的结果一定是小于200,000的。如何能够得到正确的结果呢?首先想到的肯定是加锁。不过加锁肯定会使得程序的执行效率变低。但是如果将成员变量改成AtomicInteger类进行自增运算,就可以得到正确的结果了,而且还没有加锁。看看下面的例子。

class AtomicIntegerIncrement implements Runnable {
    private static AtomicInteger value = new AtomicInteger(0);

    public static int getValue() {
        return value.get();
    }

    @Override
    public void run() {
        for (int i = 0; i < 10000; ++i) {
            value.getAndIncrement();
        }
    }
}

class AtomicIntegerStudy {
    private static final int THREADS_COUNT = 20;

    public static void test02() {
        Thread[] threads = new Thread[THREADS_COUNT];
        for (int i = 0; i < THREADS_COUNT; ++i) {
            threads[i] = new Thread(new AtomicIntegerIncrement());
            threads[i].start();
        }

        for (int i = 0; i < THREADS_COUNT; ++i) {
            try {
                threads[i].join();
            } catch (Exception e) {

            }
        }

        System.out.println("result: " + AtomicIntegerIncrement.getValue());
        /* 命令行输出
        result: 200000

        注:一定是200,000
         */
    }
}

AtomicInteger类在此就不详述了。但是此类能够在并发的条件下,得到正确的结果,就是因为其getAndIncrement方法最终调用了compareAndSwapInt方法更新其中的值。

好了,下面我们就来介绍一下CAS操作。

CAS操作都在sun.misc.Unsafe类中,以compareAndSwap开头。其所有方法都是native,并且都是原子操作。这也为并发条件下保证每一次更新不会相互覆盖带来了可能。

那么CAS到底原理是什么呢?我们以compareAndSwapInt方法为例,分析一下。

首先来看下这个函数的函数签名:

public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);

其中,offset是目标成员变量在对象o中的偏移,expected是此成员变量期望的值(只有当成员变量等于expected值时,才会更新成员变量),x是变更后的值。更新成功返回true,失败返回false。

还是以一个例子来看一下compareAndSwapInt的实际用法。

class CompareAndSwapIntStudy {
    static class Target {
        public volatile int value = 1;
    }

    private static Unsafe getUnsafe() throws IllegalAccessException, NoSuchFieldException {
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        return (Unsafe) field.get(null);
    }

    public static void compareAndSwapIntTest() {
        Target t = new Target();
        System.out.println("原始value值:" + t.value);
        try {
            /*
            注:此处Unsafe不能直接通过new运算符以及Unsafe.getUnsafe()获得实例。
            1)Unsafe的构造方法是private的,无法通过new得到(实际是个单例模式)
            2)Unsafe.getUnsafe()被设计成只能从引导类加载器(bootstrap class loader)加载
             */
            Unsafe unsafe = getUnsafe();
            long valueOffset = unsafe.objectFieldOffset(
                    Target.class.getDeclaredField("value"));
            System.out.println("first time swap(1, 2) return: " +
                    unsafe.compareAndSwapInt(t, valueOffset, 1, 2));
            System.out.println("after first time swap(1, 2) value: " +
                    t.value);
            System.out.println("second time swap(1, 2) return: " +
                    unsafe.compareAndSwapInt(t, valueOffset, 1, 2));
            System.out.println("after second time swap(1, 2) value: " +
                    t.value);
        } catch (Exception e) {
            e.printStackTrace();
        }
        /* 命令行输出
            原始value值:1
            first time swap(1, 2) return: true
            after first time swap(1, 2) value: 2
            second time swap(1, 2) return: false
            after second time swap(1, 2) value: 2
         */
    }
}

CAS的用法就介绍到这儿。

引申阅读:

为什么当我们在学习时会有挫败感?

By | 2019年8月2日

是学校让我们变得在失败面前感到沮丧。

失败是非常不错的经历。失败是最好的,或者说可能是最好的学习机制。但是许多学校并不将失败视为很好的课程将其充分利用,反而是将其视为一件非常糟糕的事情。当人们毕业的时候,他们在学校将失败视为罪过的固化思想就会使他们非常讨厌失败。

在许多学校中,衡量学生最主要的方法就是排名。我们对排名都太过看重了。当排名存在的时候,我们就非常重视它。A、B、C、D、F,通过/未通过。在最悲剧的情况时,你没有通过而且还要重修一年,这都会影响你未来的前途。

我有许多关于老师把问题搞复杂的回忆。他们不会说:“这是多么的有趣,你得到的是F,让我们来检测一下你最近的情况并且找出为什么你会得到一个F”。取而代之的是,他们会给你很严厉的批评。当我们得到了F,老师(和家长们)会对我们非常失望。

(我从来没有听老师说过:“我的天哪,你已经连续获得4个A了。我不能教授你什么了。让我们看看,如果将你送入挫折中,你能否克服它。私人教练懂得这件事的重要性。他们不会让你一直保持举着杠铃的姿势,那样不能锻炼你的肌肉。许多老师们并不了解这些或者本身就不接受这个观点。)

他们不会告诉我们,挫折是学习进程当中非常自然的一个阶段。他们告诉我们,遭受挫折会令他们和我们自己失望。我们一直被告知,如果我们得到了F,那就是因为我们的懒惰或者是愚蠢。懒惰是因为我们态度不端正,愚蠢是天生的缺陷。学校告诉我们,失败意味着我们态度或者身体上是不健全的。

可以理解,人们非常痛恨这些,所以人们尽可能得使自己保持在不会再次失败的位置,或者失败可能性很小的境地。他们找的工作都不会拥有很大的挑战,他们的目标,不管他们意识到与否,都可能是沿着其他人已有的步伐过完余生。

这些都使成年人获得太少的失败的经历了。许多人在学校设置的一些课程中尝到了失败的滋味(可能不是让他们得到了F,而是对某些学科很吃力),现在他们会经常的认为:“我不是一个_____样的人”或者“我不可能成为_______样的人”,例如:“我不是一个适合学数学的人”或者“我不可能成为莎士比亚”。这些都使他们不想再去尝试,这些都使他们会一直处于失败之中。

这些都与我们学习的开始过程相背离。如果婴儿们决定,在尝试了许多次的失败之后,就认为:“我不是一个能独立行走的人”或者“我不可能学会说话了”,那么我们都会陷入麻烦。幸运的是,这些技能是我们在进入学校之前就学会了的。

update:google也赞成的我观点。看这里:
Why Google doesn’t care about hiring top college graduates:

Megan McArdle最近提出,作家们拖延的原因是他们在英文课程中得到了太多的A。成功的年轻毕业生一直被教授要依赖他们的天赋,这样会使他们不能优雅的失败。

谷歌寻找有这样能力的人才——他们能够退一步来接受他人更好的想法。“这是一种谦虚的智慧,如果没有谦逊这种精神,你就不能学习到新的东西”,Laszlo Bock(谷歌的人力运营部的领导)说道。“成功的聪明人很少能体验到失败,所以他们不知道怎样从失败中学习到东西。”

“……我们看到的是,在我们这儿成功的人们,那些我们想要雇佣的人们,都拥有很高的职位。他们都非常爱争论,他们对自己的观点都确信无疑。不过如果你说:“这里有一些新的实事”,他们看了之后就会说:“嗯,还是你说的对,你点观点能把事情解决”。

同样值得阅读的还有:
Why Writers Are the Worst Procrastinators:

[ Carol ] Dweck 花费他的职业生涯在研究失败以及人们对失败的反应。正如你们所预想的,失败不总是一件受人欢迎的事情。然而,她从研究中发现,不是所有的人都是躲着自己不擅长的事情的。当她研究的许多的人都在讨厌着自己不擅长的任务的时候,还是有一些人在挑战中成功了。他们都在快乐的享受着他们并不擅长的事情,这其中有一个原因应该是确定的,那就是当他们失败的时候,他们学到了新的东西。

Dweck对此感到疑问——是什么让这些人与其他的同龄人如此不同呢?当有一天她正坐在她的办公室,与她的一个研究生思考着最近一个实验结果的时候,突然想到:那些不喜欢挑战的人们认为,才能是天生的,要么有要么没有。那些享受着挑战带来快乐的人们认为,这些才能是可以通过尝试自己不擅长的事情而慢慢培养的。

……“那些没有受到过多的监督,超过别人的孩子们被夸耀非常聪明时,”Dweck说。“那么他们学到了什么呢?他们学到的是,聪明才智不是从克服困难而得到的,而是找寻那些简单的事情。当他们上了大学或者研究生之后,他们面对的事情越来越困难,他们就不是特别清楚如何去应对这些事情了。


识别下图中的二维码即可关注我的微信公众号

我给自己题个序——《奔向三十》

By | 2019年6月4日

「三十而立」,古人如是说。

「人们真的要在同样的年龄段去做同样的事情么?」我对此还有些疑问。

不过,随着年岁的增长,阅历的增多,会发现同样的年龄段确实会有相近的疑惑与焦虑。三十岁的年纪,你的经历是怎样的呢?虽然每个人所处的状态都是不同的,但是我相信三十岁的人们都经历过这些事情了:同年龄段的好友有人结婚、生子,成家、立业了;即使没遇到过伴侣,也开始对自己向往的爱情有较为成熟的认识了;对自己的未来有些许担心,但更多的还是乐观、积极地去面对挑战与困难。

我的三十岁是怎样呢?

我憧憬,我期待,我惶恐,我淡然。

「奔向三十」三部曲中的文章,均写于2016年,时值我刚从「弹药工程与爆炸技术」转行到了「计算机科学与技术」。其中夹杂着我刚刚挣脱原先专业束缚的欣喜与从头再来所面临挑战与困难的沮丧。

第一篇名为《借寒冬之名》,自然是取自诗人雪莱的「冬天到了,春天还会远吗?」。当年开春,南京鸡鸣寺花开得比往年稍早,于是便有了《鸡鸣春早》。初中的语文课上,给我印象最深的一句话便是「『死亡』与『爱情』是文学的永恒的主题」。当我试着去体会这句话的含义的时候,当我开始思考自己的生命终点应该是什么样的时候,当我憧憬我的爱情应该是什么样的时候,我写下了《死亡与文学》。

那一年我25岁,虽然在20到30岁的行程仅仅过半,但是我真的感觉到是在「奔向三十」。

人生是一场有趣的旅程。