【多线程与并发】Java中的12个原子操作类

从JDK1.5开始,Java提供了java.util.concurrent.atomic包,该包中的原子操作类提供了一种使用简单、性能高效(使用CAS操作,无需加锁)、线程安全地更新一个变量的方式。

`java.util.concurrent.atomic`包中的类.png

根据变量类型的不同,Atomic包中的这12个原子操作类可以分为4种类型:
①原子更新基本类型AtomicBoolean、AtomicInteger、AtomicLong
②原子更新数组AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
③原子更新引用AtomicReference、AtomicReferenceFiledUpdater、AtomicMarkableReference
④原子更新字段(属性)AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicStampedReference
它们都是使用Unsafe实现的包装类。


原子更新基本类型类

使用原子的方式更新基本类型,Atomic包提供了以下3个类:
AtomicBoolean
AtomicInteger
AtomicLong
这三个类提供的方式几乎是一模一样,下面以AtomicInteger为例进行讲解,AtomicInteger的常用的方法如下:

int addAndGet(int delta)
以原子方式将AtomicInteger的value设置为:delta + 原value,返回更新后的值(即delta + 原value)

boolean compareAndSet(int expect, int update)
以原子的方式,如果AtomicInteger的当前值是expect,则将AtomicInteger的值设置为update

int getAndIncrement()
以原子的方式将AtomicInteger的当前值加1,注意:返回的是加1前的值

void lazySet(int newValue)
最终将AtomicInteger设置为newValue(使用lazySet设置值后,其他线程可能在之后的一段时间内还是可以读到旧的值)

int getAndSet(int newValue)
以原子的方式将AtomicInteger设置为newValue

基本使用方式示例:

public void test(){
    Executor executor = Executors.newFixedThreadPool(3);
    AtomicInteger atomicInteger = new AtomicInteger(0);

    for(int i = 0; i < 10; i++){
        executor.execute(()->{
            System.out.println("atomicInteger的当前值:" + atomicInteger.addAndGet(1));
        });
    }
}

//输出如下(输出顺序可能不同,但结果一定是正确的)
atomicInteger的当前值:1
atomicInteger的当前值:2
atomicInteger的当前值:4
atomicInteger的当前值:5
atomicInteger的当前值:3
atomicInteger的当前值:7
atomicInteger的当前值:6
atomicInteger的当前值:9
atomicInteger的当前值:8
atomicInteger的当前值:10

其内部实现原子操作的原理是通过UnSafe类的CAS操作。//TODO 具体实现
其他Java的基本类型均可以使用类似的思路实现。


原子更新数组

通过原子的方式更新数组里的某个元素,Atomic包提供了以下3个类:
AtomicIntegerArray:原子更新整型数组里的元素
AtomicLongArray:原子更新长整型数组里的元素
AtomicReferenceArray:原子更新引用类型数组里的元素

以AtomicIntegerArray为例,主要是提供以原子的方式更新数组里的整型元素,其主要方法如下:

int addAndGet(int i, int delta)
以原子的方式将数组中i位置处的元素值加上delta,返回:i位置处的元素的旧值+ delta

boolean compareAndSet(int i, int expect, int update)
如果当前值等于预期值(数组i位置处的元素),则以原子的方式将数组i位置处的元素值设置为update

使用示例:

public void testAtomicIntegerArray() {

    int[] originArray = new int[]{1, 2, 3};

    AtomicIntegerArray array = new AtomicIntegerArray(originArray);

    array.getAndSet(0, 8);
    System.out.println(array.get(0));
    System.out.println(originArray[0]);
}

//输出结果:
8
1   ----注意这里,构造方法中是将原数组复制了一份,所以对AtomicIntegerArray的操作,不会影响原数组

原子更新引用类型

如果要原子更新多个变量,就需要使用原子更新引用类型,Atomic提供了3个类:
AtomicReference:原子更新引用类型
AtomicReferenceFiledUpdater:原子更新引用类型里的字段
AtomicMarkableReference:原子更新带有标记位的引用类型

AtomicReference为例,演示代码如下:

class AtomicReferenceExample{

    private  AtomicReference<User> userAtomicReference = new AtomicReference<>();

    @Test
    public void test(){
        User originUser = new User(18, "小岳");
        userAtomicReference.set(originUser);
        User updateUser = new User(28, "老岳");
        userAtomicReference.compareAndSet(originUser, updateUser);
        System.out.println(userAtomicReference.get().getName() + ":" + userAtomicReference.get().getAge());
    }

    class User{
        private String name;
        private int age;

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

        public int getAge() {
            return age;
        }

        public String getName() {
            return name;
        }
    }
}

//输出结果如下
老岳:28

原子更新字段类

如果需要原子地更新某个类里的某个字段,就需要使用原子更新字段类,Atomic包提供了一下3个类进行原子字段更新。
AtomicIntegerFieldUpdater:原子更新整型的字段
AtomicLongFieldUpdater:原子更新长整型的字段
AtomicStampedReference:原子更新带有版本号的引用类型(可以解决CAS操作的ABA问题

使用更新字段类必须使用静态方法newUpdater(Class<U> tclass, String fieldName)创建一个更新器(同时指定要更新的类和该类中的要更新的字段名),并且该字段必须用public volatile修饰

AtomicIntegerFieldUpdater为例,演示代码如下:

class AtomicIntegerFieldUpdaterExample{

    private AtomicIntegerFieldUpdater<User> fieldUpdater = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");

    @Test
    public void test(){
        User user = new User(18, "小岳");
        fieldUpdater.addAndGet(user, 10);
        System.out.println("user现在的年龄:" + fieldUpdater.get(user));
    }

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

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

        public int getAge() {
            return age;
        }

        public String getName() {
            return name;
        }
    }
}

//输出结果如下
user现在的年龄:28

理解循环CAS

Java中可以通过两种方式来实现原子操作:加锁和循环CAS。
Java中使用循环CAS实现原子操作的实例:

//原子操作类AtomicInteger的incrementAndGet实现(最新源码已非如此,但思想是一致的)
public final int incrementAndGet() {
    for (;;) {//一直循环
        int current = get();//取出AtomicInteger当前的内存值
        int next = current + 1;//要设置的新值
        if (compareAndSet(current, next)){//用CAS操作更新AtomicInteger
            return next;
        }
    }
}

boolean compareAndSet(int current, int newValue)方法是一个原子方法,该方法首先会先检查AtomicInteger的当前数值(从内存中读取,即CAS操作中内存值)是否等于current(即CAS操作中预期值),如果等于,则说明AtomicInteger的值没有被其他线程修改过,则会将AtomicInteger的值设置为newValue(即CAS操作中要修改的新值),并返回true;如果不等于,说明AtomicInteger的值被其他线程修改过,则返回fasle

CAS实现原子操作的三个问题

①ABA问题
因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

②循环时间长开销大
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。

③只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。


参考

内容全部来自《Java并发编程的艺术》以及上述书籍作者的聊聊并发(五)——原子操作的实现原理

作者:maxwellyue
链接:https://www.jianshu.com/p/712681f5aecd
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

原文地址:https://www.cnblogs.com/xiaoshen666/p/11258541.html

时间: 2024-08-27 18:30:52

【多线程与并发】Java中的12个原子操作类的相关文章

第七章 Java中的13个原子操作类

当程序更新一个变量时,如果多线程同时更新这个变量,可能得到期望之外的值,比如变量i = 1:A线程更新i + 1,B线程也更新i + 1,经过两个线程操作之后可能i不等于3,而是等于2,.因为A和B线程在更新变量i的时候拿到的i都是1,这就是线程不安全的更新操作,通常我们会使用synchronized来解决这个问题,synchronized会保证多线程不会同时更新变量i. 而Java从JDK1.5开始提供了java.util.concurrent.atomic(以下简称Atomic包),这个包中

重温java中的String,StringBuffer,StringBuilder类

任何一个系统在开发的过程中, 相信都不会缺少对字符串的处理. 在 java 语言中, 用来处理字符串的的类常用的有 3 个: String.StringBuffer.StringBuilder. 它们的异同点: 1) 都是 final 类, 都不允许被继承; 2) String 长度是不可变的, StringBuffer.StringBuilder 长度是可变的; 3) StringBuffer 是线程安全的, StringBuilder 不是线程安全的. String 类已在上一篇随笔 小瓜牛

java中常用的包、类、以及包中常用的类、方法、属性-----io包

由于最近有需要,所以下面是我整理的在开发中常用的包.类.以及包中常用的类.方法.属性:有需要的看看 java中常用的包.类.以及包中常用的类.方法.属性 常用的包 java.io.*; java.util.*; java.lang.*; java.math.*; java.sql.*; java.text.*; java.awt.*; javax.swing.*;   包名 接口 类 方法 属性 java.io.*; java.io.Serializable实现序列化 java.io.Buffe

java中必须知道的常用类

1.Java的包装类 基本数据类型我们都很熟悉,例如:int.float.double.boolean.char等,基本数据类型不具备对象的特征,不能调用方法,一般能实现的功能比较简单,为了让基本数据类型也具备对象的特性,Java为每个数据类型都提供了一个包装类,这样我们就可以像操作对象一样,操作这些基本数据类型了 常见的包装类和基本类型对应关系如下: 包装类主要提供了两类方法: 1.进行多个类型之间的转换 2.将字符串和本类型及包装类相互转换 比如下面代码: 1 int i = 2; 2 In

Java中的集合和常用类

Java中的常用类: ? Object类 ? Math类 ? String类和StringBuffer类(字符串) ? 8种基本类型所对应的包装类 ? java.util包中的类——Date类 Object类: Object类是Java语言程序中所有类的父类,即承自Object类.Object类中包含了Java语言类中的所有的公共属性. ? toString()方法 ? equals()方法 ? getClass()方法 ? clone()方法 ? finalize()方法 枚举类: 用于储存变

java中的URLEncoder和URLDecoder类的联系与区别

今天碰到了这个问题,就查找了些资料总结下:java中的URLEncoder和URLDecoder类的联系与区别. 首先说这两者的联系与区别: URLEncoder是编码,URLDecoder是解码.两者的转换过程刚好是相反的.URLEncoder该类包含了将 String 转换为 application/x-www-form-urlencoded MIME 格式的静态方法:URLDecoder该类包含了将 String 从 application/x-www-form-urlencoded MI

java中IO写文件工具类

下面是一些根据常用java类进行组装的对文件进行操作的类,平时,我更喜欢使用Jodd.io中提供的一些对文件的操作类,里面的方法写的简单易懂. 其中jodd中提供的JavaUtil类中提供的方法足够我们使用,里面的方法写的非常简练,例如append,read等方法,封装更好,更符合面向对象, 这里面我写的一些方法可多都是模仿jodd,从里面进行抽取出来的. /** * 获取路径文件夹下的所有文件 * @param path * @return */ public static File[] ge

【转 Java 中的内部类和匿名类

 Java 中的内部类和匿名类 2008-10-16 13:47:41 标签:Java 内部类 匿名类 休闲 职场 Java 中的内部类和匿名类* 为什么需要内部类? Java 内部类有什么好处?为什么需要内部类? 首先举一个简单的例子,如果你想实现一个接口,但是这个接口中的一个方法和你构想的这个类中的一个方法的名称,参数相同,你应该怎么办?这时候,你可以建一个内部类实现这个接口.由于内部类对外部类的所有内容都是可访问的,所以这样做可以完成所有你直接实现这个接口的功能. 不过你可能要质疑,更改一

讲说问题:|和||的区别以及&amp;和&amp;&amp;的区别。2、Java中的数据类型分几类?基本数据类型有哪些?

|和||的区别以及&和&&的区别. |或 为或运算 判断为逻辑或 || 为短路或 只有逻辑判断 当左侧为真不再继续判断 &与 为与运算 判断为逻辑与 &&为短路与 当左侧为假不再继续判断 java中的数据类型分几类? 分为基本数据类型和引用数据类型 基本数据类型有8种 引用数据类型 是指如数组 字符串 集合等 也可以是用户自定义的类型.如Person Phone等 原文地址:https://www.cnblogs.com/qingyundian/p/8416