7.Java中的12个原子操作类

Java并发编程的艺术笔记


简介

官方介绍

当程序更新一个变量时,如果多线程同时更新这个变量,可能得到期望之外的值。
比如变量 i = 1A 线程更新 i+1B 线程也更新i+1,经过两个线程操作之后可能 i 不等于 3,而是等于 2
因为 AB 线程在更新变量 i 的时候拿到的 i 都是 1,这就是 线程不安全的更新操作,通常我们会使用 synchronized 来解决这个问题,synchronized 会保证多线程不会同时更新变量 i

而 Java 从 JDK 1.5 开始提供了 java.util.concurrent.atomic 包(以下简称Atomic包),这个包中的 原子操作类 提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。

因为变量的类型有很多种,所以在 Atomic 包里一共提供了 12个 类,属于以下 4 种类型的原子更新方式:

  • 原子更新基本类型
    • AtomicBoolean:原子更新布尔类型。
    • AtomicInteger:原子更新整型。
    • AtomicLong:原子更新长整型。
  • 原子更新数组
    • AtomicIntegerArray:原子更新整型数组里的元素。
    • AtomicLongArray:原子更新长整型数组里的元素。
    • AtomicReferenceArray:原子更新引用类型数组里的元素。
  • 原子更新引用
    • AtomicReference:原子更新对象引用。
    • AtomicMarkableReference:原子更新带有标记位的对象引用。
    • AtomicStampedReference:原子更新带有版本号的对象引用。
  • 原子更新属性(字段)
    • AtomicIntegerFieldUpdater:原子更新volatile修饰的整型的字段的更新器。
    • AtomicLongFieldUpdater:原子更新volatile修饰的长整型字段的更新器。
    • AtomicReferenceFieldUpdater:原子更新volatile修饰的引用类型里的字段的更新器。

Atomic 包里的类基本都是使用 Unsafe 实现的包装类。


原子更新基本类型

  • AtomicBoolean:原子更新布尔类型。
  • AtomicInteger:原子更新整型。
  • AtomicLong:原子更新长整型。

以上3个类提供的方法几乎一模一样,所以本节仅以 AtomicInteger 为例进行讲解。

AtomicInteger 的常用方法如下

  • int addAndGet(int delta):以原子方式将输入的数值与实例中的值(AtomicInteger 里的 value)相加,并返回结果。
  • boolean compareAndSet(int expect,int update):如果输入的数值等于预期值,则以原子方式将该值设置为输入的值。
  • int getAndIncrement():以原子方式将当前值加1,注意,这里返回的是自增前的值。
  • void lazySet(int newValue):最终会设置成 newValue,使用 lazySet 设置值后,可导致其他线程在之后的一小段时间内还是可以读到旧的值。
  • int getAndSet(int newValue):以原子方式设置为 newValue 的值,并返回旧值。

示例代码:

public static void main(String[] args) {
    AtomicInteger ai = new AtomicInteger(1);

    System.out.println("ai.get() = " + ai.get());

    System.out.println("ai.addAndGet(5) = " + ai.addAndGet(5));
    System.out.println("ai.get() = " + ai.get());

    System.out.println("ai.compareAndSet(ai.get(), 10) = " + ai.compareAndSet(ai.get(), 10));
    System.out.println("ai.get() = " + ai.get());

    System.out.println("ai.getAndIncrement() = " + ai.getAndIncrement());
    System.out.println("ai.get() = " + ai.get());

    ai.lazySet(8);
    System.out.println("ai.lazySet(8)");
    System.out.println("ai.get() = " + ai.get());

    System.out.println("ai.getAndSet(5) = " + ai.getAndSet(5));
    System.out.println("ai.get() = " + ai.get());
}

输出:

ai.get() = 1
ai.addAndGet(5) = 6
ai.get() = 6
ai.compareAndSet(ai.get(), 10) = true
ai.get() = 10
ai.getAndIncrement() = 10
ai.get() = 11
ai.lazySet(8)
ai.get() = 8
ai.getAndSet(5) = 8
ai.get() = 5

AtomicIntegergetAndIncrement()方法:

public final int getAndIncrement() {
    for (; ; ) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next)) {
            return current;
        }
    }
}
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

  • for 循环体的先取得 AtomicInteger 里存储的数值
  • AtomicInteger 的当前数值进行 +1 操作,
  • 关键是调用 compareAndSet 方法来进行原子更新操作,该方法先检查 当前数值是否等于current
    • 等于意味着 AtomicInteger 的值没有被其他线程修改过,则将 AtomicInteger 的当前数值更新成 next的值。
    • 如果不等 compareAndSet 方法会返回 false,程序会进入 for 循环重新进行 compareAndSet 操作。

Atomic 包提供了 3 种基本类型的原子更新,但是 Java 的基本类型里还有 charfloatdouble 等。
那么问题来了,如何原子的更新其他的基本类型呢?
Atomic包里的类基本都是使用 Unsafe 实现的,让我们一起看一下Unsafe的源码:

/**
 * 如果当前数值是expected,则原子的将Java变量更新成x
 *
 * @return 如果更新成功则返回true
 */
public final native boolean compareAndSwapObject(Object o, long offset,
                                                 Object expected, Object x);

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

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

通过以上代码,我们发现 Unsafe 只提供了 3 种 CAS 方法:compareAndSwapObjectcompareAndSwapIntcompareAndSwapLong,再看 AtomicBoolean 源码,发现它是先把 Boolean 转换成 整型,再使用 compareAndSwapInt 进行 CAS,所以原子更新 charfloatdouble 变量也可以用类似的思路来实现。


原子更新数组

  • AtomicIntegerArray:原子更新整型数组里的元素。
  • AtomicLongArray:原子更新长整型数组里的元素。
  • AtomicReferenceArray:原子更新引用类型数组里的元素。

以上几个类提供的方法几乎一样,所以仅以 AtomicIntegerArray 为例进行介绍:
AtomicIntegerArray 类主要是提供原子的方式更新数组里的整型。

常用方法如下:

  • int addAndGet(int i,int delta):以原子方式将输入值与数组中索引i的元素相加。
  • boolean compareAndSet(int i,int expect,int update):如果当前值等于预期值,则以原子方式将数组位置i的元素设置成update值。

示例代码:

public static void main(String[] args) {

    int[] value = new int[]{1, 2};

    AtomicIntegerArray ai = new AtomicIntegerArray(value);

    System.out.println("ai.getAndSet(0, 3)");
    ai.getAndSet(0, 3);
    System.out.println("ai.get(0) = " + ai.get(0));
    System.out.println("value[0] = " + value[0]);

    ai.compareAndSet(1, 2, 5);
    System.out.println("ai.compareAndSet(1, 2, 5)");
    System.out.println("ai.get(1) = " + ai.get(1));
}

输出结果:

ai.getAndSet(0, 3)
ai.get(0) = 3
value[0] = 1
ai.compareAndSet(1,2,5)
ai.get(1) = 5

需要注意的是,数组value通过构造方法传递进去,然后AtomicIntegerArray会将当前数组复制一份,所以当AtomicIntegerArray对内部的数组元素进行 修改 时,不会影响传入的数组


原子更新引用

  • AtomicReference:原子更新对象引用。
  • AtomicMarkableReference:原子更新带有标记位的对象引用。
  • AtomicStampedReference:原子更新带有版本号的对象引用。该类将整数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA问题

以上几个类提供的方法几乎一样,所以仅以AtomicReference为例进行介绍。

示例代码:

public class AtomicReferenceTest {
    public static AtomicReference<User> atomicUserRef = new
            AtomicReference<User>();

    public static void main(String[] args) {
        User user = new User("103style", 20);
        atomicUserRef.set(user);
        System.out.println("atomicUserRef.get() = " + atomicUserRef.get().toString());

        User updateUser = new User("xiaoke", 22);
        atomicUserRef.compareAndSet(user, updateUser);

        System.out.println("atomicUserRef.compareAndSet(user, updateUser);");

        System.out.println("atomicUserRef.get() = " + atomicUserRef.get().toString());
    }

    static class User {
        private String name;
        private int age;

        public User(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public int getAge() {
            return age;
        }

        @Override
        public String toString() {
            return "name='" + name + ", age=" + age;
        }
    }
}

输出结果:

atomicUserRef.get() = name='103style, age=20
atomicUserRef.compareAndSet(user, updateUser);
atomicUserRef.get() = name='xiaoke, age=22

原子更新属性(字段)

  • AtomicIntegerFieldUpdater:原子更新volatile修饰的整型的字段的更新器。
  • AtomicLongFieldUpdater:原子更新volatile修饰的长整型字段的更新器。
  • AtomicReferenceFieldUpdater:原子更新volatile修饰的引用类型里的字段的更新器。

要想原子地更新字段类需要两步

  • 因为原子更新字段类都是抽象类,每次使用的时候必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。
  • 更新类的字段(属性)必须使用public volatile修饰符。

以上3个类提供的方法几乎一样,所以仅以 AstomicIntegerFieldUpdater 为例进行讲解。

示例代码:

public class AtomicIntegerFieldUpdaterTest {

    public static void main(String[] args) {
        // 创建原子更新器,并设置需要更新的对象类和对象的属性
        AtomicIntegerFieldUpdater<User> a = AtomicIntegerFieldUpdater.
                newUpdater(User.class, "age");
        // 设置柯南的年龄是10岁
        User conan = new User("conan", 10);
        // 柯南长了一岁,但是仍然会输出旧的年龄
        System.out.println(a.getAndIncrement(conan));
        // 输出柯南现在的年龄
        System.out.println(a.get(conan));
    }

    public static class User {
        public volatile int age;
        private String name;

        public User(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public int getAge() {
            return age;
        }
    }
}

输出结果:

10
11

以上

</article>

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

推荐阅读更多精彩内容