原子操作类(一)原子操作类详细介绍

引言

??Java从JDK1.5开始提供了java.util.concurrent.atomic包,方便程序员在多线程环境下,无锁的进行原子操作。原子变量的底层使用了处理器提供的原子指令,但是不同的CPU架构可能提供的原子指令不一样,也有可能需要某种形式的内部锁,所以该方法不能绝对保证线程不被阻塞。

??因为变量的类型有很多种,所以在Atomic包里一共提供了13个类,可分为4种类型的原子更新方式,分别是

  • 原子更新基本类型
  • 原子更新数组
  • 原子更新引用
  • 原子更新属性(字段)

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

一、基本类型的原子操作类

Atomic 包提供了以下3个基本类型的原子操作类:

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

以上3个类提供的方法几乎一模一样,所以本文仅以AtomicInteger为例,常用方法如下:

  • int addAndGet(int delta): 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果
  • boolean compareAndSet(int expect,int update): 如果输入的数值等于预期值,则以原子方式将该值设置为输入的值
  • int getAndIncrement(): 以原子方式将当前值加1,注意,这里返回的是自增前的值,相当于 (i++) 的形式。
  • void lazySet(int newValue): JDK6所增加,最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。关于该方法的更多信息可以参考并发编程网翻译的一篇文章《AtomicLong.lazySet是如何工作的?》,文章地址是“http://ifeve.com/howdoes-atomiclong-lazyset-work/
  • int getAndSet(int newValue): 以原子方式设置为newValue的值,并返回旧值。

@ Example1 ?AtomicInteger 使用例子

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerTest {

    static AtomicInteger ai = new AtomicInteger(1);

    public static void main(String[] args) {
        System.out.println(ai.getAndIncrement());
        System.out.println(ai.get());
    }
}

运行结果:

1

2

其余的基本类的原子操作类的解决方案

??java 的基本类型一共有8个,然而JDK却只提供了三个,如何原子的更新其他的基本类型呢?特别是常用的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方法,那么 AtomicBoolean 是如何实现的呢,我们再来看一下 AtomicBoolean 的源码。只给出关键部分

public class AtomicBoolean implements java.io.Serializable {
//存储值:1,0;1代表 true,0 代表 false
private volatile int value;
//Unsafe 对象
 private static final Unsafe unsafe = Unsafe.getUnsafe();

public final boolean compareAndSet(boolean expect, boolean update) {
        //boolean 类型 转换成 整形,true 对应 1,false 对应 0;
        int e = expect ? 1 : 0;
        int u = update ? 1 : 0;
        return unsafe.compareAndSwapInt(this, valueOffset, e, u);
    }

public final void set(boolean newValue) {
        value = newValue ? 1 : 0
    }

public final boolean get() {
        return value != 0;
    }

//.......
}

??从AtomicBoolean 的源码可以看出,它是先把 boolean 的值转换成整型来存储,再使用compareAndSwapInt进行CAS。所以其他基本类型的原子类型也可参照这个思路来实现。

??思路有了,接下来考虑怎么实现。bytecharint 的转换很容易,他们的原子操作类就很容易实现了。那floatdouble 要怎样才能以整形或者长整形来存储值呢?

??Float 类中 提供了static两个方法:通过 Static int floatToIntBits(float value) (根据 IEEE 754 浮点“单一格式”位布局,返回指定浮点值的表示形式)方法,便可以将float 的值以整形的方式存储下来。如果要取值,则通过 static float intBitsToFloat(int bits)方法,将存储下来的整形的浮点值转换回原来的float值。

??同理,Double 类也提供了类似的两个方法:static long doubleToLongBits(double value) 产生long类型的浮点值。static double longBitsToDouble(long bits)long的浮点值转换回double值。

下面是 AtomicDouble 的实现例子:

@ Example2 ?AtomicDouble 的实现例子

import java.util.concurrent.atomic.AtomicLong;

public class AtomicDouble extends Number {

    private AtomicLong bits;

    public AtomicDouble() {
        this(0d);
    }

    public AtomicDouble(Double value){
        bits = new AtomicLong(Double.doubleToLongBits(value));
    }

    public final double get() {
        return Double.longBitsToDouble(bits.get());
    }

    public final void set(double value) {
        bits.set(Double.doubleToLongBits(value));
    }

    public final double addAndGet(double delta) {
        long newBits = bits.addAndGet(Double.doubleToLongBits(delta));
        return Double.longBitsToDouble(newBits);
    }

    public final double getAndAdd(double delta) {
        long oldBits = bits.getAndAdd(Double.doubleToLongBits(delta));
        return Double.longBitsToDouble(oldBits);
    }

    public final double getAndSet(double newValue) {
        long oldBits = bits.getAndSet(Double.doubleToLongBits(newValue));
        return Double.longBitsToDouble(oldBits);
    }

    public final boolean compareAndSet(double expect, double update) {
        return bits.compareAndSet(Double.doubleToLongBits(expect), Double.doubleToLongBits(update));
    }

    public final boolean weakCompareAndSet(double expect, double update) {
        return bits.weakCompareAndSet(Double.doubleToLongBits(expect), Double.doubleToLongBits(update));
    }

    public final void lazySet(double newValue) {
         bits.lazySet(Double.doubleToLongBits(newValue));
    }

    @Override
    public int intValue() { return (int) this.get(); }
    @Override
    public long longValue() { return (long) this.get(); }
    @Override
    public float floatValue() { return (float) this.get();}
    @Override
    public double doubleValue() { return this.get(); }
}

关于原子操作类实现其余猜想:

??除了上面的那种实现方案外,在 stackOverflow 上也发现两个原子操作类的实现方案:

  1. 利用 AtomicReference 类;如 Float 的原子操作类可以是 AtomicReference<Float>。不可,经过测试,这种方法是不行的,因为像compareAndSet这类方法比较的是对象的内存地址,而不会使用equal()方法进行比较。
  2. 使用JDK1.8新增的方法:DoubleAdderDoubleAccumulator。没去深入了解,粗略看了下,这两个类都是适合于“多写少读”的情况。

二、数组的原子操作类

通过原子的方式更新数组里的某个元素,Atomic包提供了以下3个类:

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

这几个类的方法几乎一样,以AtomicIntegerArray为例,AtomicIntegerArray类主要是提供原子的方式更新数组里的整型;

构造方法:

AtomicIntegerArray(int length): 创建给定长度的新 AtomicIntegerArray。

AtomicIntegerArray(int[] array): 创建与给定数组具有相同长度的新 AtomicIntegerArray,并从给定数组复制其所有元素。

其常用方法:

int addAndGet(int i, int delta): 以原子方式将输入值与数组中索引i的元素相加。

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

@ Example3 ?AtomicIntegerArray 的使用例子

public class AtomicIntegerArrayTest {

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

    static AtomicIntegerArray ai = new AtomicIntegerArray(value);

    public static void main(String[] args) {
        ai.getAndSet(0, 3);
        System.out.println(ai.get(0));
                System.out.println(value[0]);
    }
}

运行结果:

3

1

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


三、引用类型的原子操作类

?基本类型的原子操作类只能更新基本类型的值,不能更新引用类型的对象引用。Atomic包提供了以下三个类,可以原子方式更新的对象引用。

  • AtomicReference: 原子更新引用类型。
  • AtomicStampedReference: 原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更数据和数据的版本号,可以解决使用CAS进行原子更新时,可能出现的ABA问题。
  • AtomicMarkableReference: 原子更新带有标记位的引用类型。可以原子的更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef, boolean initialMark)

1. AtomicReference 介绍

构造方法

AtomicReference(): 使用 null 初始值创建新的 AtomicReference

AtomicReference(V initialValue): 使用给定的初始值创建新的 AtomicReference

常用的方法: 与前面介绍的类差不多,get()set(V newValue)compareAndSet(V expect, V update)getAndSet(V newValue);

@ Example4 ?AtomicReference 的使用例子

public class AtomicReferenceTest {

    public static AtomicReference<user> atomicUserRef = new AtomicReference</user><user>();

    public static void main(String[] args) {
        User user = new User("conan", 15);
        atomicUserRef.set(user);
        User updateUser = new User("Shinichi", 17);
        atomicUserRef.compareAndSet(user, updateUser);
        System.out.println(atomicUserRef.get().getName());
        System.out.println(atomicUserRef.get().getOld());
    }

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

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

        public String getName() {
            return name;
        }

        public int getOld() {
            return old;
        }
    }
}

运行结果:

Shinichi

17

2. AtomicMarkableReference 与 AtomicStampedReference 介绍

??这两个类的作用与 AtomicReference 的作用非常相似,都是原子更新引用类型。但他们还有一个作用:能解决CAS过程中的ABA问题

什么是ABA问题?

??ABA的问题就是:在多线程的环境下,线程1将A的值赋给了B,然后B的值又重新赋值了A。在这个过程中,A已经被修改了一次了,但是线程2不知道,在进行CAS时,认为A的值没有被修改过,所以就进行修改。当然,如果只对结果敏感,而对修改的次数不敏感,那么这个问题就无所谓了。

AtomicStampedReference 和 AtomicMarkableReference 是怎么解决ABA问题的?

??这两个类的解决方案也很简答,就是多维护了一个标志位记录修改的状态 或者 维护一个版本号记录修改的次数,然后进行CAS时,也会比较标志位或者版本号。简单看一下源码吧。

AtomicMarkableReference 用的是标志位mark(布尔类型)来标志修改的状态,下面是关键部分源码:

//构造方法,initialRef 是 初始的引用类型,initialMark 是初始的标志位
public AtomicMarkableReference(V initialRef,  boolean initialMark);

//CAS 不仅要比较引用类型,还要比较标志位
public boolean compareAndSet(V expectedReference, V newReference, boolean expectedMark, boolean newMark) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedMark == current.mark &&
            ((newReference == current.reference &&
              newMark == current.mark) ||
             casPair(current, Pair.of(newReference, newMark)));
 }

AtomicStampedReference 用的则是版本号stamp(整形int) 来记录修改的次数,下面是关键部分的源代码:

//构造方法,初始引用类型 和 初始版本号
 public AtomicStampedReference(V initialRef, int initialStamp);

//CAS 不仅要比较引用类型,还要比较版本号
  public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }

@ Example5 ?AtomicStampedReference 的解决ABA问题的例子

看一个AtomicStampedReference的例子。当需要执行一次更新当前用户的操作,这里故意将更新的用户就是当前用户,也就是更新后对象引用是没有变化的。线程A、线程B 都接到这个任务,但只能执行一次,意味着得有一个线程更新失败。User类的代码请查看上一个例子。

public class AtomicStampedReferenceTest {

    public static void main(String[] args) throws InterruptedException {
        User user = new User("小赫",25);
        //初始版本为1
        int stamp = 1;

        //设置当前用户是  小赫
        AtomicStampedReference<User> currentAtomicUser = new AtomicStampedReference<User>(user, stamp);

        //更新当前用户,更新的目标用户就是当前用户
        UpdateUserTask task = new UpdateUserTask(stamp, currentAtomicUser, user, user);
        //线程A,线程B执行相同的更新任务,但更新操作只需要执行一次。
        Thread threadA = new Thread(task,"ThreadA");
        Thread threadB = new Thread(task,"ThreadB");
        threadA.start();
        threadB.start();
    }
}

class UpdateUserTask implements Runnable{

    private int stamp;
    private AtomicStampedReference<User> currentAtomicUser;
    private User newUser;
    private User oldUser;

    public UpdateUserTask(int stamp, AtomicStampedReference<User> currentAtomicUser,User newUser,User oldUser) {
        this.stamp = stamp;
        this.currentAtomicUser = currentAtomicUser;
        this.newUser = newUser;
        this.oldUser = oldUser;
    }

    @Override
    public void run() {
        //更新当前用户,版本号加一
        boolean b = currentAtomicUser.compareAndSet(oldUser, newUser, stamp, stamp+1);
        if(b){//更新执行成功
            System.out.println("线程"+Thread.currentThread().getName()+": 成功执行更新操作");
        }else{//更新失败
            System.out.println("线程"+Thread.currentThread().getName()+": 当前用户是 "+ currentAtomicUser.getReference().getName()+" ,版本号已经过期,更新操作已经被其他线程完成");
        }
    }

 }

运行结果:

线程ThreadB: 当前用户是 小赫 ,版本号已经过期,更新操作已经被其他线程完成

线程ThreadA: 成功执行更新操作

??从结果可以看出,当线程B执行任务时,尽管当前用户的对象引用没有改变,但版本号却已经改变了,线程B从而知道了更新操作已经被执行了,于是便不再执行更新。


四、 更新字段的原子操作类

如果我们只需要某个类里的某个字段,那么就需要使用原子更新字段类,Atomic包提供了以下三个类:

  • AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。通过静态工厂方法创建newUpdater(Class<U> tclass, String fieldName)
  • AtomicLongFieldUpdater: 原子更新长整型字段的更新器。通过静态工厂方法创建newUpdater(Class<U> tclass, String fieldName)
  • AtomicReferenceFieldUpdater 原子更新引用类型里的字段。比前两个方法的范围要广,可以更新任意类型的字段,通过静态的工厂方法创建 newUpdater(Class<U> tclass, Class<W> vclass, String fieldName)

原子更新字段类都是抽象类,每次使用都时候必须使用静态方法newUpdater创建一个更新器。原子更新类的字段的必须使用public volatile修饰符。以上3个类提供的方法差不多,下面给出AtomicIntegerFieldUpdater 的例子:

@ Example6 ?AtomicIntegerFieldUpdater 例子

public class AtomicIntegerFieldUpdaterTest {

    private static AtomicIntegerFieldUpdater<User> a = AtomicIntegerFieldUpdater
            .newUpdater(User.class, "old");

    public static void main(String[] args) {
        User conan = new User("conan", 10);
        System.out.println(a.getAndIncrement(conan));
        System.out.println(a.get(conan));
    }

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

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

        public String getName() {
            return name;
        }

        public int getOld() {
            return old;
        }
    }
}

运行结果:

10

11

原文地址:https://www.cnblogs.com/jinggod/p/8495395.html

时间: 2024-10-29 19:07:06

原子操作类(一)原子操作类详细介绍的相关文章

Hibernate核心类和接口详细介绍

一.hiobernate核心类和接口预览图 二.hibernate.properties 这个文件是以前老版本使用的 类似于hibernate.cfg.xml文件:作用和hibernate.cfg.xml一致. 三.hibernate.cfg.xml (1)详细介绍 ①该文件主要用于指定各个参数,是hibernate核心文件 ②默认放在src目录下,也可以放在别的目录下. ③指定连接数据库的驱动.用户名.密码.url.连接池.. ④指定对象关系映射文件的位置. ⑤也可使用hibernate.pr

类的详细介绍

类       这个概念,学过java的人们肯定不陌生.因为java本来就是用类的思想实现的.           我在一开始学类和对象的时候,总是问别人:为什么要有类这个语法?           而他们总是回答我: 因为类这个东西,是人们写着写着代码而逐渐产生的一种东西.我也不知道这样说对不对,反正他们说c++难就难在这块上面,就像继承与派生,都是人们通过生活演化而来的. 1.类和对象的定义 类是现实世界或思维世界中的实体在计算机中的反映,它将数据以及这些数据上的操作封装在一起.      

View类和surfaceView详细介绍

View类和surfaceView详细介绍: view类: view类是Android的一个超类,这个类几乎包含了所有的屏幕类型,每一个view都有一个用于绘画 的画布,这个画布可以进行任意的扩展.布局视图可以通过Android的XML来进行.在游戏开发中当然也可以自定义视图(view),让这个画布的功能更能满足我们在游戏开发是的需要,在Android中,任何一个view类只需要重写onDraw方法来实现界面显示,自定义视图可以是复杂的3D实现,也可以是简单地文本形式等. 游戏中最重要的就是与玩

几类常见的试验箱详细介绍!

为了方便检验商品的质量和特点,是必须检测其在不一样自然环境下的主要表现,为了方便对产品品质开展操纵,各式各样的试验箱请变成许多制造业企业不可或缺的关键机器设备. 试验箱的类型有许多,下边欧可仪器设备将几类常见的试验箱开展详细介绍: 高温试验箱: 高温试验箱关键是适用工业品高溫.超低温的可靠性测试,可以依照国家行业标准或顾客规定,仿真模拟各种各样的超低温或高溫自然环境,用以检测商品的工艺性能和高溫.超低温自然环境下其各类性能参数,来分辨商品的特性是不是可以合乎预订规定,便于对设计产品.改善.评定及

MFC常用的类详细介绍

常用的MFC类 CRuntimeClass结构 在CRuntimeClass结构中定义了类名.对象所占存储空间的大小.类的版本号等成员变量及动态创建对象.派生关系判断等成员函数.每一个从CObject类派生的类都有一个CRuntimeClass结构同它关联,以便完成在运行时得到对象的信息或基类的信息. 要使用CRuntimeClass结构,必须结合使用RUNTIME_CLASS()宏和其他有关运行时类型识别的MFC宏. CObject类 MFC的CObject类为程序员提供了对象诊断.运行时类型

java常用类详细介绍及总结:字符串相关类、日期时间API、比较器接口、System、Math、BigInteger与BigDecimal

一.字符串相关的类 1.String及常用方法 1.1 String的特性 String:字符串,使用一对""引起来表示. String声明为final的,不可被继承 String实现了Serializable接口:表示字符串是支持序列化的. 实现了Comparable接口:表示String可以比较大小 String内部定义了final char[] value用于存储字符串数据 String:代表不可变的字符序列.简称:不可变性. 体现: 当对字符串重新赋值时,需要重写指定内存区域赋

oc语言学习之基础知识点介绍(二):类和对象的进一步介绍

一.类.对象在内存中的存储 /* 内存分区: 栈:局部变量 堆:程序员自己写代码申请开辟的 程序员自己维护,编译器现在帮我们自动优化了,它在合适的给我们加上了释放空间的语句,所以我们现在写的对象不会造成内存泄露 全局区:所有的全局变量和静态变量 常量区:所有的常量 代码区:程序编译后的指令集 类是模板,肯定需要存在内存里面,因为实例化对象的时候需要根据这个模板来创建,那么存在内存里面,存在哪呢?? 类模板存在:全局区! 存的是:类的描述,还有所有的方法实现 每个对象都会有一个系统给我们的isa指

Delphi 绘图TCanvas类[3] TPen类参数及介绍

Delphi 绘图TCanvas类[3]  TPen类参数及介绍TPen 主要属性: Color.Width.Style.Mode //Style: 样式 TPenStyle,:psSolid = 0; {实线}psDash = 1; {段线; 要求笔宽<=1}psDot = 2;    {点线; 要求笔宽<=1}psDashDot = 3; {线.点; 要求笔宽<=1}psDashDotDot = 4; {线.点.点; 要求笔宽<=1}psClear = 5; {不可见}psIn

python------面向对象介绍之经典类与新式类的继承顺序

一. 经典类与新式类的继承顺序 1 class A: 2 def __init__(self): 3 print("A") 4 5 class B(A): 6 def __init__(self): 7 print("B") 8 9 class C(A): 10 def __init__(self): 11 print("C") 12 13 class D(B,C): 14 pass 15 16 obj = D() 注:python2.x 经典类

转:详细介绍了如何学习Java

以下详细介绍了如何学习Java,如果楼主有耐心的话,建议仔细看一下~ 学习一门新的知识,不可能指望只看一本,或者两本书就能够完全掌握.需要有一个循序渐进的阅读过程.我推荐Oreilly出版的Java系列书籍. 在这里我只想补充一点看法,很多人学习Java是从<Thinking in Java>这本书入手的,但是我认为这本书是不适合初学者的.我认为正确的使用这本书的方法应该是作为辅助的读物.<Thinking in Java>并不是在完整的介绍Java的整个体系,而是一种跳跃式的写作