AtomicInteger、AtomicBoolean、AtomicLong等原子类的使用

/ 多线程 / 没有评论 / 373浏览

我们知道在多线程中如果操作的是实例变量那么就有可能出线线程安全问题。代码如下:

/**
* 管理用户请求
*
* @author Sama
* @author admin@jilinwula.com
* @date 2017-03-15 10:44
* @since 1.0.0
*/
public class RequestAdmin implements Runnable {

private int count;

@Override
public void run() {
System.out.println(String.format("count: %s\tthread: %s", count++, Thread.currentThread().getName()));
}
}
/**
* @author Sama
* @author admin@jilinwula.com
* @date 2017-03-20 13:35
* @since 1.0.0
*/
public class Test {
public static void main(String[] args) throws InterruptedException {
RequestAdmin requestAdmin = new RequestAdmin();
Thread thread1 = new Thread(requestAdmin);
Thread thread2 = new Thread(requestAdmin);
thread1.start();
thread2.start();
}
}
count: 0	thread: Thread-1
count: 0	thread: Thread-0

我们看两个线程输出的count值都是0这显然是不正确的,原因就是因为++这个操作符不是一个原子操作。我们可以把这个操作符拆分开来看一下它的实现逻辑。

count++ // 相当于count = count + 1;

按照上面表达式count++要经历3个必要的步骤:

  1. 首先要获取count的值,
  2. 计算count+1的值
  3. 在然后将计算后的值赋值给count。

并且我们还知道count++操作是后置运算符,也就是说JVM在处理这种操作的时候,是先输出值然后在将计算后的值赋值给count的。也就是说如果我们没有用多线程只有单线程执行的话,那么程序的运行结果一直都是0,原因就是因为count++这个++符号在后面,所以是先输出在赋值的,也就是在输出完才将count的值加1。

/**
* @author Sama
* @author admin@jilinwula.com
* @date 2017-03-20 13:35
* @since 1.0.0
*/
public class Test {
public static void main(String[] args) throws InterruptedException {
RequestAdmin requestAdmin = new RequestAdmin();
Thread thread1 = new Thread(requestAdmin);
// Thread thread2 = new Thread(requestAdmin);
thread1.start();
// thread2.start();
}
}
count: 0	thread: Thread-0

我们看如果只有一个线程的话,count++输出的值一定是0,并不是因为没有执行++操作,只是这个++操作是在输出之后才赋值的,所以如果想看++操作执行后的值,那么我们在输出一次count的值就可以了。

/**
* 管理用户请求
*
* @author Sama
* @author admin@jilinwula.com
* @date 2017-03-15 10:44
* @since 1.0.0
*/
public class RequestAdmin implements Runnable {

private int count;

@Override
public void run() {
System.out.println(String.format("count++: %s\tthread: %s", count++, Thread.currentThread().getName()));
System.out.println(String.format("count: %s\tthread: %s", count, Thread.currentThread().getName()));
}
}
count++: 0	thread: Thread-0
count: 1	thread: Thread-0

在回到我们刚刚说的多线程的问题上,如果同时有两个以上的线程都执行count++操作,它们的步骤就有可能是(多线程的执行结果是不固定的)线程一先获取到count值因为它是实例变量,所以JVM会初始化默认的值也就是0。那么此时线程一获取到的count值就是0,又因为++操作是后置运算符,所以JVM会先输出在执行运算赋值。所以线程一就会先输出0,但这时有可能线程二也执行了count++。也就是说线程一还没有输出呢,线程二就执行了,它执行的过程和上面的一样,先获取count然后输出。有可能线程二执行的比较快,比线程一先执行完,所以输出结果就有可能先输出线程二的后输出线程一,但它们的结果都可能是0,这也是count++不是原子操作的体现。下面我们看一下具有原子操作的类AtomicInteger、AtomicBoolean、AtomicLong。这三个类的的方法使用都是一样的,我们重点看一下AtomicInteger类的使用。它为我们提供了很多原子操作的方法。具体如下:

int addAndGet(int delta) // 以原子的方式将参数的值和AtomicInteger对象里的值相加然后返回相加后的结果
boolean compareAndSet(int expect, int update) // 如果AtomicInteger对象里的值等于expect值,则以原子的方式将AtomicInteger对象里的值设置为update值
int getAndIncrement() // 以原子的方式将当前值加1并返回加1之前的值

这里面有很多支持原子操作的方法,这里就不一一介绍了,具体使用的时候可以查相关的API文档。下面我们将详细介绍一下上面的3个方法。上面遇到的线程安全问题是因为执行count++操作的时候因为不是原子操作,多个线程可以同时执行,才导致的线程安全问题。现在我们将代码修改为支持原子操作的getAndIncrement()方法在执行一下上面的代码看一下结果会怎么样。

/**
* 管理用户请求
*
* @author Sama
* @author admin@jilinwula.com
* @date 2017-03-15 10:44
* @since 1.0.0
*/
public class RequestAdmin implements Runnable {

private AtomicInteger count = new AtomicInteger();

@Override
public void run() {
System.out.println(String.format("count++: %s\tthread: %s", count.getAndIncrement(), Thread.currentThread().getName()));
}
}
count++: 0	thread: Thread-0
count++: 1	thread: Thread-1

我们看这回没有线程安全的问题了,原因是getAndIncrement()方法是一个原子操作的方法,线程在执行时是不能中断的。那getAndIncrement()方法是怎么实现原子操作的呢,我们下面来分析一下getAndIncrement()方法的源码。

public final int getAndIncrement() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

我们看到源码中第一条语句就是一个死循环,也就是说如果赋值操作失败,它会一直循环执行。get()的作用是获取当前AtomicInteger类中的值。然后将获取的AtomicInteger类中的值执行加1运算。最重要的就是这个compareAndSet方法,我们知道这个方法的作用是将AtomicInteger类中的值与预期值做比较,如果相等,那就把AtomicInteger类中的值设置为方法参数的值,并且这个方法也是原子操作,不会有线程的安全问题。如果compareAndSet方法返回false。说明get()方法获取的值不是最新的了,也就是说有其它线程已经对AtomicInteger类中的值与做了修改了,那么方法就会一直执行循环,然后继续判断,一直到compareAndSet方法返回ture时为止,这也就是getAndIncrement()方法实现原子操作的逻辑。我们继续看一下addAndGet()方法的源码。

public final int addAndGet(int delta) {
for (;;) {
int current = get();
int next = current + delta;
if (compareAndSet(current, next))
return next;
}
}

我们发现addAndGet()方法和getAndIncrement()方法的大体逻辑一样,只是一个是执行加1运算一个是执行加参数运算,具体的原理就向上面分析的一样,这里就不在详细介绍了。总之,如果在多线程中如果有数据运算等操作,那么最好是用Java为我们提供的原子操作类,它可以帮我们解决很多线程安全的问题。