在多线程环境下,如果多个线程同时更新一个变量,可能并不会得到预期的结果。往往我们会使用加锁机制如 Synchrnized 等解决这个问题,但是仅仅对于更新基本变量来说,加锁的开销太大了,从 JDK 1.5 开始, java.util.concurrent.atomic
(简称 J.U.C 原子类)提供一种用法简单、性能高效、线程安全的更新一个变量的方式。因为变量的类型有很多,所以在 Atomic 包中一共提供了 13 个类,属于 4 种类型的原子更新方式,分别是原子更新基本类型、原子更新数组、原子更新引用和原子更新属性。Atomic 包基本都是使用的 Unsafe 实现的包装类。
使用原子方式更新基本类型,原子类包中提供了三个类:
上面三个类提供的方法基本一模一样,所以这里我们只以 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()); //返回新值 } }
测试结果如下:
那么 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 类主要是提供原子的方式更新数组中的整形,其常用方法如下:
以上几个类提供的方法几乎一样,我们只以 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]); } }
测试结果:
需要注意的是,AtomicIntegerArray 传入数组的构造方法仅仅是将原数组复制了一份,所以原数组中内容不会被影响。
原子更新基本类型 AtomicInteger 只能更新一个变量,如果要原子更新多个变量,就需要使用这个原子更新引用类型提供的类。Atomic包提供以下类:
此处仅以 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 个类进行原子字段更新:
要想原子的更新字段类需要两步,第一步因为原子更新字段类都是抽象类,每次使用的时候必须使用静态方法 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; } } }
输出结果如下: