1 概述
原子变量最主要的一个特点就是所有的操作都是原子的,synchronized关键字也可以做到对变量的原子操作。只是synchronized的成本相对较高,需要获取锁对象,释放锁对象,如果不能获取到锁,还需要阻塞在阻塞队列上进行等待。而如果单单只是为了解决对变量的原子操作,建议使用原子变量。
关于原子变量的介绍,主要涉及以下内容:
- 原子变量的基本概念
- 通过AtomicInteger了解原子变量的基本使用
- 通过AtomicInteger了解原子变量的基本原理
- AtomicReference的基本使用
- 使用FieldUpdater操作非原子变量的字段属性
- 经典的ABA问题的解决
2 原子变量的基本概念
原子变量保证了该变量的所有操作都是原子的,不会因为多线程的同时访问而导致脏数据的读取问题。我们先看一段synchronized关键字保证变量原子性的代码:
public class Counter {
private int count;
public synchronized void addCount(){
this.count++;
}
}
简单的count++操作,线程对象首先需要获取到Counter类实例的对象锁,然后完成自增操作,最后释放对象锁。整个过程中,无论是获取锁还是释放锁都是相当消耗成本的,一旦不能获取到锁,还需要阻塞当前线程等等。对于这种情况,我们可以将count变量声明成原子变量,那么对于count的自增操作都可以以原子的方式进行,就不存在脏数据的读取了。Java给我们提供了以下几种原子类型:
- AtomicInteger和AtomicIntegerArray:基于Integer类型
- AtomicBoolean:基于Boolean类型
- AtomicLong和AtomicLongArray:基于Long类型
- AtomicReference和AtomicReferenceArray:基于引用类型
在本文的余下内容中,我们将主要介绍AtomicInteger和AtomicReference两种类型,AtomicBoolean和AtomicLong的使用和内部实现原理几乎和AtomicInteger一样。
3 AtomicInteger的基本使用
构造函数如下:
/**
* Creates a new AtomicInteger with the given initial value.
*
* @param initialValue the initial value
*/
public AtomicInteger(int initialValue) {
value = initialValue;
}
/**
* Creates a new AtomicInteger with initial value {@code 0}.
*/
public AtomicInteger() {
}
可以看到,我们在通过构造函数构造AtomicInteger原子变量的时候,如果指定一个int的参数,那么该原子变量的值就会被赋值,否则就是默认的数值0。也有获取和设置这个value值的方法:
/**
* Gets the current value.
*
* @return the current value
*/
public final int get() {
return value;
}
/**
* Sets to the given value.
*
* @param newValue the new value
*/
public final void set(int newValue) {
value = newValue;
}
我们可以看见上面两个方法并不是原子的,所以针对原子变量使用,我们很少使用这两个方法,而是使用下面的一些方法:
/**
* 获取旧值并设置新值
*
* @param newValue the new value
* @return the previous value
*/
public final int getAndSet(int newValue) {
return unsafe.getAndSetInt(this, valueOffset, newValue);
}
/**
* 当要设置的值和期望的值相同的时候就设置成新值
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
/**
* 获取当前的value值并自增一
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
/**
* 获取当前的value值并自减一
*
* @return the previous value
*/
public final int getAndDecrement() {
return unsafe.getAndAddInt(this, valueOffset, -1);
}
/**
* 获取当前的value值并增加delta
*
* @param delta the value to add
* @return the previous value
*/
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
使用原子变量实现计数器:
public class Test {
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[100];
for(int i = 0; i < 100;i++){
threads[i] = new MyThread();
threads[i].start();
}
for(int i = 0; i < 100;i++){
threads[i].join();
}
System.out.println(MyThread.value);
}
}
class MyThread extends Thread{
public static AtomicInteger value = new AtomicInteger();
@Override
public void run(){
try {
Thread.sleep(100);
//原子自增
value.incrementAndGet();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
我们可以看见这种实现计数器的操作比使用synchronized要简单很多,而且效率较高。
4 AtomicInteger的内部基本原理
通过查看源码我们可以看见value值前面的变量修饰如下:
private volatile int value;
我们可以看见通过volatile关键字进行了修饰,所以value具有可见性和禁止指令重排的特点。查看compareAndSet方法:
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
我们可以看见这个方法主要依靠了native方法来实现的,这个方法就相当于一个CAS操作,比较当前的值和期望值是否相同,如果相同就设置成功,并且返回true,否则直接放回false。所以我们可以看见其实内部的实现主要是依靠unsafe里面的方法。
5 AtomicReference的基本使用
/**
* Creates a new AtomicReference with the given initial value.
*
* @param initialValue the initial value
*/
public AtomicReference(V initialValue) {
value = initialValue;
}
/**
* Creates a new AtomicReference with null initial value.
*/
public AtomicReference() {
}
我们可以看见AtomicReference是基于泛型来实现的,依然可以看见里面的value被volitile修饰着。
private volatile V value;
关于它的一些方法我们可以查看源码。
6 使用FieldUpdater操作非原子变量的字段属性
FieldUpdater允许我们不必将字段设置为原子变量,利用反射直接以原子方式操作字段。例如:
public class Test {
public static void main(String[] args) throws InterruptedException {
Counter2 counter2 = new Counter2();
for(int i = 0; i < 100;i++){
counter2.addCount();
}
System.out.println(counter2.getCount());
}
}
class Counter2{
private volatile int count;
public int getCount(){
return count;
}
public void addCount(){
AtomicIntegerFieldUpdater<Counter2> updater = AtomicIntegerFieldUpdater.newUpdater(Counter2.class,"count");
updater.getAndIncrement(this);
}
}
后我们创建一百个线程随机调用同一个Counter对象的addCount方法,无论运行多少次,结果都是一百。这种方式实现的原子操作,对于被操作的变量不需要被包装成原子变量,但是却可以直接以原子方式操作它的数值。
7 经典的ABA问题
我们的原子变量都依赖一个核心的方法,那就是CAS。这个方法最核心的思想就是,更改变量值之前先获取该变量当前最新的值,然后在实际更改的时候再次获取该变量的值,如果没有被修改,那么进行更改,否则循环上述操作直至更改操作完成。假如一个线程想要对变量count进行修改,实际操作之前获取count的值为A,此时来了一个线程将count值修改为B,又来一个线程获取count的值为B并将count修改为A,此时第一个线程全然不知道count的值已经被修改两次了,虽然值还是A,但是实际上数据已经是脏的。这就是典型的ABA问题,一个解决办法是,对count的每次操作都记录下当前的一个时间戳,这样当我们原子操作count之前,不仅查看count的最新数值,还记录下该count的时间戳,在实际操作的时候,只有在count的数值和时间戳都没有被更改的情况之下才完成修改操作。
public class Test {
public static void main(String[] args) throws InterruptedException {
int count = 0;
int stamp = 1;
AtomicStampedReference reference = new AtomicStampedReference(count,stamp);
int next = 2;
System.out.println(reference.compareAndSet(count,next,stamp,stamp+1));
System.out.println(reference.getReference());
}
}
AtomicStampedReference的CAS方法要求传入四个参数,该方法的内部会同时比较count和stamp,只有这两个值都没有发生改变的前提下,CAS才会修改count的值。
上述我们介绍了有关原子变量的最基本内容,最后我们比较下原子变量和synchronized关键字的区别。
从思维模式上看,原子变量代表一种乐观的非阻塞式思维,它假定没有别人会和我同时操作某个变量,于是在实际修改变量的值的之前不会锁定该变量,但是修改变量的时候是使用CAS进行的,一旦发现冲突,继续尝试直到成功修改该变量。而synchronized关键字则是一种悲观的阻塞式思维,它认为所有人都会和我同时来操作某个变量,于是在将要操作该变量之前会加锁来锁定该变量,进而继续操作该变量。