Java中的原子操作类

Java中的原子操作类

请注意,本文编写于  921  天前,最后修改于  921  天前,其中某些信息可能已经过时。

概述

在多线程环境下,如果多个线程同时更新一个变量,可能并不会得到预期的结果。往往我们会使用加锁机制如 Synchrnized 等解决这个问题,但是仅仅对于更新基本变量来说,加锁的开销太大了,从 JDK 1.5 开始,java.util.concurrent.atomic(简称 J.U.C 原子类)提供一种用法简单、性能高效、线程安全的更新一个变量的方式。因为变量的类型有很多,所以在 Atomic 包中一共提供了 13 个类,属于 4 种类型的原子更新方式,分别是原子更新基本类型、原子更新数组、原子更新引用和原子更新属性。Atomic 包基本都是使用的 Unsafe 实现的包装类。

原子更新基本类型

使用原子方式更新基本类型,原子类包中提供了三个类:

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

上面三个类提供的方法基本一模一样,所以这里我们只以 AtomicInteger 作为代表进行讨论。

AtomicInteger 的常用方法如下:

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

使用方法如下:

import java.util.concurent.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

那么 AtomicInteger 底层是如何实现的呢?我们扒一下 JDK 1.8 中的源码:

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

其中 getAndAddInt 的源码如下:

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

这里面的 this 指示的是 unsafe 对象,可以看到最终都是调用 unsafe.compareAndSwapInt 方法,也就是 CAS 方法。

底层调用的都是 Unsafe 类的方式,类权限定名为sun.misc.Unsafe,由于这个包是 JDK 的 native 方法,底层使用 C++ 实现,用户不能直接通过 new Unsafe() 或者 Unsafe.getUnsafe() 等方法创建对象。

所以换种方法,我们通过查看 openjdk-8 的源码来查看底层操作,目录是openjdk-8-src-b132-03_mar_2014\openjdk\jdk\src\share\classes\sun\misc。此类包含了低级(native 硬件级别的原子操作)、不安全的操作集合。

获取 Unsafe 实例静态方法:

private Unsafe() {}

private static final Unsafe theUnsafe = new Unsafe();

@CallerSensitive
public static Unsafe getUnsafe() {
    Class<?> caller = Reflection.getCallerClass();
    if (!VM.isSystemDomainLoader(caller.getClassLoader()))
        throw new SecurityException("Unsafe");
    return theUnsafe;
}

我们再看一下其中的 public native 方法,扩展的 C++ 就不看了:

//扩充内存  
public native long reallocateMemory(long address, long bytes);  

//分配内存  
public native long allocateMemory(long bytes);  

//释放内存  
public native void freeMemory(long address);  

//在给定的内存块中设置值  
public native void setMemory(Object o, long offset, long bytes, byte value);  

//从一个内存块拷贝到另一个内存块  
public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes);  

//获取值,不管java的访问限制,其他有类似的getInt,getDouble,getLong,getChar等等  
public native Object getObject(Object o, long offset);  

//设置值,不管java的访问限制,其他有类似的putInt,putDouble,putLong,putChar等等  
public native void putObject(Object o, long offset);  

//从一个给定的内存地址获取本地指针,如果不是allocateMemory方法的,结果将不确定  
public native long getAddress(long address);  

//存储一个本地指针到一个给定的内存地址,如果地址不是allocateMemory方法的,结果将不确定  
public native void putAddress(long address, long x);  

//该方法返回给定field的内存地址偏移量,这个值对于给定的filed是唯一的且是固定不变的  
public native long staticFieldOffset(Field f);  

//报告一个给定的字段的位置,不管这个字段是private,public还是保护类型,和staticFieldBase结合使用  
public native long objectFieldOffset(Field f);  

//获取一个给定字段的位置  
public native Object staticFieldBase(Field f);  

//确保给定class被初始化,这往往需要结合基类的静态域(field)  
public native void ensureClassInitialized(Class c);  

//可以获取数组第一个元素的偏移地址  
public native int arrayBaseOffset(Class arrayClass);  

//可以获取数组的转换因子,也就是数组中元素的增量地址。将arrayBaseOffset与arrayIndexScale配合使用, 可以定位数组中每个元素在内存中的位置  
public native int arrayIndexScale(Class arrayClass);  

//获取本机内存的页数,这个值永远都是2的幂次方  
public native int pageSize();  

//告诉虚拟机定义了一个没有安全检查的类,默认情况下这个类加载器和保护域来着调用者类  
public native Class defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain);  

//定义一个类,但是不让它知道类加载器和系统字典  
public native Class defineAnonymousClass(Class hostClass, byte[] data, Object[] cpPatches);  

//锁定对象,必须是没有被锁的
public native void monitorEnter(Object o);  

//解锁对象  
public native void monitorExit(Object o);  

//试图锁定对象,返回true或false是否锁定成功,如果锁定,必须用monitorExit解锁  
public native boolean tryMonitorEnter(Object o);  

//引发异常,没有通知  
public native void throwException(Throwable ee);  

//CAS,如果对象偏移量上的值=期待值,更新为x,返回true.否则false.类似的有compareAndSwapInt,compareAndSwapLong,compareAndSwapBoolean,compareAndSwapChar等等。  
public final native boolean compareAndSwapObject(Object o, long offset,  Object expected, Object x);  

// 该方法获取对象中offset偏移地址对应的整型field的值,支持volatile load语义。类似的方法有getIntVolatile,getBooleanVolatile等等  
public native Object getObjectVolatile(Object o, long offset);   

//线程调用该方法,线程将一直阻塞直到超时,或者是中断条件出现。  
public native void park(boolean isAbsolute, long time);  

//终止挂起的线程,恢复正常.java.util.concurrent包中挂起操作都是在LockSupport类实现的,也正是使用这两个方法
public native void unpark(Object thread);  

//获取系统在不同时间系统的负载情况  
public native int getLoadAverage(double[] loadavg, int nelems);  

//创建一个类的实例,不需要调用它的构造函数、初使化代码、各种JVM安全检查以及其它的一些底层的东西。即使构造函数是私有,我们也可以通过这个方法创建它的实例,对于单例模式,简直是噩梦,哈哈  
public native Object allocateInstance(Class cls) throws InstantiationException;

上面这些有兴趣可以仔细看看,我们主要看 compareAndSwapObject 的使用方法:

/**
     * Atomically exchanges the given reference value with the current
     * reference value of a field or array element within the given
     * object <code>o</code> at the given <code>offset</code>.
     *
     * @param o object/array to update the field/element in
     * @param offset field/element offset
     * @param newValue new value
     * @return the previous value
     * @since 1.8
     */
public final Object getAndSetObject(Object o, long offset, Object newValue) {
    Object v;
    do {
        v = getObjectVolatile(o, offset); //获取对象内存地址偏移量上的数值v
    } while (!compareAndSwapObject(o, offset, v, newValue)); //如果现在还是v,设置为newValue,否则返回false,!false=true,一直循环直到等于v退出循环返回v.
    return v;
}

到这里我们就可以明白了,更新数值都是基于 CAS 实现的,

CAS 指的是现代 CPU 广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。这个指令会对内存中的共享数据做原子的读写操作。简单介绍一下这个指令的操作过程:首先,CPU 会将内存中将要被更改的数据与期望的值做比较。然后,当这两个值相等时,CPU 才会将内存中的数值替换为新的值。否则便不做操作。最后,CPU 会将旧的数值返回。这一系列的操作是原子的。它们虽然看似复杂,但却是 Java 5 并发机制优于原有锁机制的根本。简单来说,CAS 的含义是“我认为原有的值应该是什么,如果是,则将原有的值更新为新值,否则不做修改,并告诉我原来的值是多少”。(这段描述引自《Java并发编程实践》)

简单的来说,CAS有 3 个操作数,内存值 V,旧的预期值 A,要修改的新值 B。当且仅当预期值 A 和内存值 V 相同时,将内存值 V 修改为 B,否则返回 V。这是一种乐观锁的思路,它相信在它修改之前,没有其它线程去修改它;而 Synchronized 是一种悲观锁,它认为在它修改之前,一定会有其它线程去修改它,悲观锁效率很低。

原子更新数组

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

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

其中 AtomicIntegerArray 类主要是提供原子的方式更新数组中的整形,其常用方法如下:

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

以上几个类提供的方法几乎一样,我们只以 AtomicIntegerArray 讲解:

public class AtomicIntegerArraytest{
    static int[] value = {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 传入数组的构造方法仅仅是将原数组复制了一份,所以原数组中内容不会被影响。

原子更新引用类型

原子更新基本类型 AtomicInteger 只能更新一个变量,如果要原子更新多个变量,就需要使用这个原子更新引用类型提供的类。Atomic包提供以下类:

  • AtomicReference:原子更新引用类型
  • AtomicReferenceFieldUpdater:原子更新引用类型里的字段
  • AtomicMarkableReference:原子更新带有引用标记的引用类型,可以原子更新一个布尔类型的标记位和引用类型。构造方法是 AtomicMarkableReference(V initialRef,boolean initialMark)

此处仅以 AtomicReference 为例进行讲解:

public class AtomicReferenceTest{
    public static AtomicReference<user> atomicUserRef = new AtomicReference<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,String old){
            this.name = name;
            this.old = old;
        }
        
        public String getName(){
            return name;
        }
        
        public int getOld(){
            return old;
        }
    }
}

输出结果如下:

Shinichi
17

原子更新字段类

如果需要原子更新某个类里的某个字段时,就需要使用原子更新字段类,Atomic 包提供了以下 3 个类进行原子字段更新:

  • AtomicintegerFieldUpdater:原子更新整型的字段的更新器
  • AtomicLongFieldUpdater:原子更新长整型字段的更新器
  • AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新出现的 “ABA” 问题。

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

此处仅以 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

参考文章

  1. 方腾飞 等著 《Java 并发编程的艺术》
  2. 在openjdk8下看Unsafe源码
  3. Java线程(十):CAS

本文由 Sanarous 创作,如果您觉得本文不错,请随意赞赏
采用 知识共享署名4.0 国际许可协议进行许可,转载前请务必署名
本文链接:https://bestzuo.cn/posts/2201553789.html
最后更新于:2019-07-22 10:11:56

切换主题 | SCHEME TOOL