前言
原子是世界上的最小单位,具有不可分割性。比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以它不是一个原子操作(线程执行a=0这个语句时直接将数据写入内存中;而执行a++时,会先获取a的值,再去执行加操作,最后再将数据写入内存)。只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。Java从JDK1.5开始提供了java.util.concurrent.atomic包,方便程序员在多线程环境下,无锁的进行原子操作。原子变量的底层使用了处理器提供的原子指令,但是不同的CPU架构可能提供的原子指令不一样,也有可能需要某种形式的内部锁,所以该方法不能绝对保证线程不被阻塞。
Atomic包介绍
在Atomic包里一共有12个类,四种原子更新方式,分别是原子更新基本类型,原子更新数组,原子更新引用和原子更新字段。Atomic包里的类基本都是使用Unsafe实现的包装类。
原子更新基本类型
用于通过原子的方式更新基本类型,Atomic包提供了以下三个类:
- AtomicBoolean:原子更新布尔类型。
- AtomicInteger:原子更新整型。
- AtomicLong:原子更新长整型。
AtomicInteger的常用方法如下:
- AtomicInteger(int initialValue):创建一个AtomicInteger实例,初始值由参数指定。不带参的构造方法初始值为0。
- int addAndGet(int delta) :以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果,与getAndAdd(int delta)相区分,从字面意思即可区分,前者返回相加后结果,后者先返回再相加。
- boolean compareAndSet(int expect, int update) :如果当前值等于预期值,则以原子方式将该值设置为输入的值。
- int getAndIncrement():以原子方式将当前值加1,注意:这里返回的是自增前的值。
- void lazySet(int newValue):最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。关于该方法的更多信息可以参考并发网翻译的一篇文章《AtomicLong.lazySet是如何工作的?》
- int getAndSet(int newValue):以原子方式设置为newValue的值,并返回旧值。
AtomicInteger例子代码如下:
1 import java.util.concurrent.atomic.AtomicInteger; 2 3 public class Demo 4 { 5 // 初始值设为1 6 static AtomicInteger atom = new AtomicInteger(1); 7 8 public static void main(String[] args) 9 { 10 System.out.println("初始值 = " + atom); 11 12 // 以原子的方式加1,注意是先返回原数值再加1 13 System.out.println("调用getAndIncrement()返回值 = " + atom.getAndIncrement()); 14 System.out.println("调用getAndIncrement()后初始值变为 = " + atom); 15 16 // 以原子的方式与指定数值相加,注意是先加再返回相加后的值 17 System.out.println("调用addAndGet()返回值 = " + atom.addAndGet(10)); 18 System.out.println("调用addAndGet()后初始值变为 = " + atom); 19 20 // 以原子的方式将当前值设为指定数值,注意是先返回原值再设为指定值 21 System.out.println("调用getAndSet()返回值 = " + atom.getAndSet(-5)); 22 System.out.println("调用getAndSet()后初始值变为 = " + atom); 23 24 // 如果当前值等于预期值(第一个参数),则以原子的方式将当前值设置为指定值,并返回true,否则返回false 25 System.out.println("调用compareAndSet()返回值 = " + atom.compareAndSet(-5, 100)); 26 System.out.println("调用compareAndSet()后初始值变为 = " + atom); 27 } 28 }
结果如下:
初始值 = 1 调用getAndIncrement()返回值 = 1 调用getAndIncrement()后初始值变为 = 2 调用addAndGet()返回值 = 12 调用addAndGet()后初始值变为 = 12 调用getAndSet()返回值 = 12 调用getAndSet()后初始值变为 = -5 调用compareAndSet()返回值 = true 调用compareAndSet()后初始值变为 = 100
那 AtomicInteger 怎么实现院子操作的呢,以 getAndIncrement() 为例,我们看看源码是怎样实现这个方法的
public final int getAndIncrement() { for (;;) { // 获取AtomicInteger当前对应的long值 int current = get(); // 将current加1 int next = current + 1; // 通过CAS函数,更新current的值 if (compareAndSet(current, next)) return current; } }
getAndIncrement()首先会根据get()获取AtomicInteger对应的int值。该值是volatile类型的变量,当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时发现此时值改变了,它会去内存中读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
get()的源码如下:
// value是AtomicInteger对应的int值 private volatile int value; // 返回AtomicInteger对应的int值 public final int get() { return value; }
getAndIncrement()接着将current加1,然后通过CAS函数(乐观锁),将新的值赋值给value。compareAndSet()的源码如下:
1 public final boolean compareAndSet(intexpect, int update) { 2 return unsafe.compareAndSwapLong(this, valueOffset, expect, update); 3 }
调用 Unsafe 来实现
private static final Unsafe unsafe = Unsafe.getUnsafe();
这段代码写得很巧妙:
1. compareAndSet 方法首先判断当前内存值this是否等于预期值current;
2. 如果当前值 = current,说明 AtomicInteger 类的值没有被其他线程修改,则将内存值更新为next
3. 如果当前值 != current,说明 AtomicInteger 类的值已经被其他类修改了,这时会再次进入循环重新获取更新后值并比较
valueOffset表示的是变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的原值的。
Atomic包提供了三种基本类型的原子更新,但是Java的基本类型里还有char,float和double等。那么问题来了,如何原子的更新其他的基本类型呢?Atomic包里的类基本都是使用Unsafe实现的,它提供了硬件级别的原子操作。让我们一起看下Unsafe的源码,发现Unsafe只提供了三种CAS方法,compareAndSwapObject,compareAndSwapInt和compareAndSwapLong,再看AtomicBoolean源码,发现其是先把Boolean转换成整型,再使用compareAndSwapInt进行CAS,所以原子更新double也可以用类似的思路来实现。
参考资料: