java.util.concurrent.atomic包中并发原子类AtomicInteger讲解
一、简要介绍
- 今天在项目代码中,注意到有使用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操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。