一、简要介绍
- 今天在项目代码中,注意到有使用AtomicInteger类,这个类主要是在java.util.concurrent.atomic并发包下的。
- Java并发机制的三个特性,如下所示:
(1)原子性
(2)可见性
(3)有序性 - volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证可见性、有序性。但是无法保证原子性;今天我们所讲述的AtomicInteger的作用就是为了保证原子性。
代码如下所示:
package concurrent;
public class Concurrent {
private static volatile int a = 0;
public static void main(String[] args) {
Thread[] threads = new Thread[5];
//定义5个线程,每个线程加10
for (int i = 0; i < 5; i++) {
threads[i] = new Thread(() -> {
try {
for (int j = 0; j < 10; j++) {
System.out.println(a++);
Thread.sleep(1000);
}
} catch (Exception e) {
e.printStackTrace();
}
});
threads[i].start();
}
}
}
在上述示例代码中,我们定义了一个变量a。并且使用了5个线程分别去增加。为了保证可见性和有序性,我们定义了一个静态的volatile的关键字来对a进行修饰。在这里我们只测试原子性。如果我们第一次接触的话肯定会觉得5个线程,每个线程加10,最后结果一定是50呀。但是当对代码运行后,结果却跟我们想象的不太一样。
我们不难发现,运行的结果除了含有重复值外,最大的值也不是50,而仅仅只有39。这个现象的出现,让我不禁感叹:为什么会出现这个问题呢?这是因为变量a,虽然保证了可见性和有序性,但是却没有保证原子性。
分析如下所示:
其实代码中对于a++的操作,大致可以分为3个步骤:
(1)从主存中读取a的值
(2)对a进行加1操作
(3)把a重新刷新到主存
这三个步骤在单线程中一点问题都没有,但是到了多线程就出现了问题了。比如说有的线程已经把a进行了加1操作,但是还没来得及重新刷入到主存,其他的线程就重新读取了旧值。因为才造成了错误。如何去解决呢?方法当然很多,但是为了和我们今天的主题对应上,很自然的联想到使用AtomicInteger。
下面我们使用AtomicInteger重新来测试一遍:
package concurrent;
import java.util.concurrent.atomic.AtomicInteger;
public class Concurrent_1 {
//使用AtomicInteger定义a
static AtomicInteger atomicInteger = new AtomicInteger();
public static void main(String[] args) {
Thread[] threads = new Thread[5];
//定义5个线程,每个线程加10
for (int i = 0; i < 5; i++) {
threads[i] = new Thread(() -> {
try {
for (int j = 0; j < 10; j++) {
//incrementAndGet
System.out.println(atomicInteger.incrementAndGet());
Thread.sleep(500);
}
} catch (Exception e) {
e.printStackTrace();
}
});
threads[i].start();
}
}
}
结果如下所示:
当我们使用AtomicInteger类,无论你执行多少次,最后的结果一定是50。这是为什么呢?一切都跟AtomicInteger类的底层实现方法有关系,具体分析详见下日描述。
[补充] compareAndSwapInt又叫做CAS。
CAS 即比较并替换,实现并发算法时常用到的一种技术。CAS操作包含三个操作数——内存位置、预期原值及新值。执行CAS操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。