[译]Java并发之AtomicInteger

原文链接

AtomicInteger一个是专门被设计用来线程安全地更新Integer的类。为什么我们需要这个类呢?为什么不能仅仅就用一个volatile int ?我们能如何使用AtomicInteger?

为什么要用AtomicInteger?

下面展示了一个使用volatile int的非线程安全的counter例子:

public class CounterNotThreadSafe {
    private volatile int count = 0;
    public void increment() {
       count++;
    }
    public int getCount() {
        return count;
    }
}

你可以从GitHub下载示例源码
我们把计数存在第二行的这个volatile int中。我们需要volatile这个关键词来确保所有线程总是能获取到当前的实际值,正如更多细节中描述的。我们在第四行使用++操作来给counter增加计数。为了确认这个类是否是线程安全的,我们使用了如下的测试:

public class ConcurrencyTestCounter {
    private final CounterNotThreadSafe counter = new CounterNotThreadSafe();
    @Interleave
    private void increment() {
        counter.increment();
    }

    @Test
    public void testCounter() throws InterruptedException {
        Thread first = new Thread(() -> {increment();});
        Thread second = new Thread(() -> {increment();});
        first.start();
        second.start();
        first.join();
        second.join();
        assertEquals(2, counter.getCount());
}
}

我们需要两个线程来测试counter是否是线程安全的,分别在第9和第10行创建。
我们在11和12行启动这两个线程。然后,在13和14行使用thread.join()等到两个线程都结束。在两个线程都结束后,在15行检验是否counter的值是2。

为了使所有的线程并发地测试,在第三行使用了来自vmlens的InterLeave注解。
Interleave注解会告知vmlens测试被注解的方法时所有的线程交替执行。运行该测试,我们会看到这样的错误:

ConcurrencyTestCounter.testCounter:22 expected:<2> but was:<1>

出现这个错误是由于++操作并非是原子性的,这两个线程会互相覆盖对方的运算结果。从vmlens的报告中我们可以知道:


[译]Java并发之AtomicInteger_第1张图片
测试报告

在这种错误下,两个都线程首先都并行地读取变量值,然后都对变量进行了写,这就导致了错误值1。
要解决这个bug,可以使用AtomicInteger类:

public class CounterUsingIncrement {
    private final AtomicInteger count = new AtomicInteger();
    public void increment() {
        count.incrementAndGet();
    }
    public int getCount() {  
        return count.get();
    }
}

在第2行,我们将用AtomicInteger而非int来定义变量count。在第4行使用incrementAndGet代替++操作。
现在,由于方法inncrementAndGet()是原子性的,其他的线程总是在这个方法调用之前或之后获取到值,线程们不会覆盖其他线程的计算结果。所以,在多线程并行下,现在count的值一直都是2。

如何使用AtomicInteger

AtomicInteger有多种方法允许我们原子地更新AtomicInteger变量。例如,increment()方法原子地增加AtomicInteger变量,decrementAndGet()原子地减少AtomicInteger变量。
但是compareAndSet()方法比较特别,这个方法允许我们原子地实现任意计算。compareAndSet()方法有两个参数,一个是预期值,一个是更新值。这个方法原子地检查当前值是否等于预期值,如果是的话,这个方法会将值替换成更新值并返回true;如果不是的话,保留当前值不变并返回false。
使用这个方法的目的是让compareAndSet()检查是否当前值是否在我们计算新值的时候被其他线程修改了。如果没有的话我们可以安全地更新当前值。否则,我们需要用当前被修改过的值重新计算新的值。
下面的例子展示了如何使用compareAndSet()来实现我们的counter:

public void increment() {
    int current = count.get();
    int newValue = current + 1;
    while(!count.compareAndSet(current, newValue)) {
            current = count.get();
            newValue = current + 1;
    }
}

在第2行我们先读取当前的值,然后在第3行计算新的值。然后,在第4行我们使用compareAndSet()检查是否有另一个线程已经修改了当前值。如果当前值没有被修改过,compareAndSet()会更新当前值并返回true。否则返回false。由于这个测试可能会失败多次,我们需要使用一个while循环。如果这个值被其他线程改变了,我们需要获取获取被修改过的当前值(在第5行),然后计算出新的值(第6行)并尝试再次进行更新。

结论

AtomicInteger使我们可以以线程安全的方式更新integer变量。使用incrementAndGet()或者decrementAndGet()这些方法来做简单类型的计算。使用get()和compareAndSet用于所有其他类型的计算。

你可能感兴趣的:([译]Java并发之AtomicInteger)