原子操作之 AtomicInteger
1. 前言
从本节开始,正式带领大家认识 Java 并发工具类,今天为大家介绍原子操作之 AtomicInteger。此工具位于 java.util.concurrent.atomic 包中。
本节先解释什么是原子操作,接着介绍 AtomicInteger 工具类的最基本用法,有了这些基本认识之后,给出 AtomicInteger 工具最常用的场合说明,然后通过简单的编码实现一个实际案例,让大家有一个理性的认识,最后带领大家熟悉 AtomicInteger 最常用的一些编程方法,进一步加深对 AtomicInteger 工具类的理解。
AtomicInteger 工具类本身使用很简单,重点是对常用编程方法的准确理解。
下面我们正式开始介绍吧。
2. 概念解释
什么是原子操作呢?所谓原子操作,就是一个独立且不可分割的操作。
AtomicInteger 工具类提供了对整数操作的原子封装。为什么要对整数操作进行原子封装呢?
在 java 中,当我们在多线程情况下,对一个整型变量做加减操作时,如果不加任何的多线程并发控制,大概率会出现线程安全问题,也就是说当多线程同时操作一个整型变量的增减时,会出现运算结果错误的问题。AtomicInteger 工具类就是为了简化整型变量的同步处理而诞生的。
大家记住,在多线程并发下,所有不是原子性的操作但需要保证原子性时,都需要进行原子操作处理,否则会出现线程安全问题。
概念已经了解了,那么 AtomicInteger 工具类怎么用呢?别急,最基本的用法请看下面的描述。
3. 基本用法
// 首先创建一个 AtomicInteger 对象
AtomicInteger atomicInteger = new AtomicInteger();
// 在操作之前先赋值,如果不显式赋值则值默认为 0 ,就像 int 型变量使用前做初始化赋值一样。
atomicInteger.set(1000);
// 之后可以调用各种方法进行增减操作
...
// 获取当前值
atomicInteger.get();
// 先获取当前值,之后再对原值加100
atomicInteger.getAndAdd(100)
// 先获取当前值,之后再对原值减1
atomicInteger.getAndDecrement()
...
是不是很简单,AtomicInteger 在我们日常实践中,到底应该应用在哪些场合比较合适呢?下面我们给出最常用的场景说明。
4. 常用场景
AtomicInteger 经常用于多线程操作同一个整型变量时,简化对此变量的线程安全控制的场合。当在研发过程中遇到这些场景时,就可以考虑直接使用 AtomicInteger 工具类辅助实现,完全可以放弃使用 synchronized 关键字做同步控制。
下面我们用 AtomicInteger 工具实现电影院某场次电影票销售的例子。
5. 场景案例
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIntegerTest {
// 首先创建一个 AtomicInteger 对象
// 代表《神龙晶晶兽》电影上午场次当前可售的总票数 10 张
private static AtomicInteger currentTicketCount = new AtomicInteger(10);
// 主程序
public static void main(String[] args) {
// 定义3个售票窗口
for(int i=1; i<=3; i++) {
TicketOffice ticketOffice = new TicketOffice(currentTicketCount, i);
// 每个售票窗口开始售票
new Thread(ticketOffice).start();
}
}
}
在上面的代码中,先创建了一个 AtomicInteger 对象,然后创建了 3 个售票窗口模拟售票动作 ,接下来每个售票窗口如何动作呢,看下面的代码。
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 模拟售票窗口
*/
public class TicketOffice implements Runnable {
// 当前可售的总票数
private AtomicInteger currentTicketCount;
// 窗口名称(编号)
private String ticketOfficeNo;
// 售票窗口构造函数
public TicketOffice(AtomicInteger currentTicketCount, int ticketOfficeNo) {
this.currentTicketCount = currentTicketCount;
this.ticketOfficeNo = "第" + ticketOfficeNo + "售票窗口";
}
// 模拟售票逻辑
public void run() {
// 模拟不间断的售票工作(生活中有工作时间段控制)
while (true) {
// 获取当前可售的总票数,如果没有余票就关闭当前售票窗口结束售票,否则继续售票
if (currentTicketCount.get() < 1) {
System.out.println("票已售完," + ticketOfficeNo + "结束售票");
return;
}
// 模拟售票用时
try {
Thread.sleep(new Random().nextInt(1000));
} catch (Exception e) {}
// 当总票数减1后不为负数时,出票成功
int ticketIndex = currentTicketCount.decrementAndGet();
if (ticketIndex >= 0) {
System.out.println(ticketOfficeNo + "已出票,还剩" + ticketIndex + "张票");
}
}
}
}
在 TicketOffice 类中,首先通过 get () 获取了当前可售的总票数,在有余票的情况下继续售票。然后随机休眠代替售票过程,最后使用 decrementAndGet () 尝试出票。我们观察一下运行结果。
第3售票窗口已出票,还剩9张票
第1售票窗口已出票,还剩8张票
第2售票窗口已出票,还剩7张票
第1售票窗口已出票,还剩6张票
第3售票窗口已出票,还剩5张票
第3售票窗口已出票,还剩4张票
第2售票窗口已出票,还剩3张票
第1售票窗口已出票,还剩2张票
第3售票窗口已出票,还剩1张票
第2售票窗口已出票,还剩0张票
票已售完,第2售票窗口结束售票
票已售完,第1售票窗口结束售票
票已售完,第3售票窗口结束售票
在这个案例中,因为存在多个售票窗口同时对一场电影进行售票,如果不对可售票数做并发售票控制,很可能会出现多卖出票的尴尬。例子中没有直接使用 synchronized 关键字做同步控制,而是使用 JDK 封装好的 AtomicInteger 原子工具类实现了并发控制整型变量的操作,是不是很方便呢。
至此,大家对 AtomicInteger 已经有了初步的理解,接下来我们继续丰富对 AtomicInteger 工具类的认识。
6. 核心方法介绍
除了上面代码中使用的最基本的 AtomicInteger (int)、AtomicInteger ()、 set () 、get () 和 decrementAndGet () 方法之外,我们还需要掌握其他几组核心方法的使用。下面逐个介绍。
- getAndAdd (int) 方法与 AddAndGet (int) 方法
第 1 个方法是先获取原值,之后再对原值做增加。注意获取的值是变更之前的值。而第 2 个方法正好相反,是先对原值做增加操作之后再获取更新过的值。
AtomicInteger atomicInteger = new AtomicInteger();
System.out.println(atomicInteger.get()); // 0
System.out.println(atomicInteger.getAndAdd(10)); // 0,获取当前值并加10
System.out.println(atomicInteger.get()); // 10
System.out.println(atomicInteger.addAndGet(20)); // 30,当前值先加20再获取
System.out.println(atomicInteger.get()); // 30
- getAndIncrement () 方法与 incrementAndGet () 方法
第 1 个方法是先获取值,之后再对原值做增 1 操作,注意获取的值是变更之前的值。而第 2 个方法正好相反,是先对原值做增 1 的操作之后再获取更新过的值。
AtomicInteger atomicInteger = new AtomicInteger();
System.out.println(atomicInteger.get()); // 0
System.out.println(atomicInteger.getAndIncrement()); // 0,获取当前值并自增1
System.out.println(atomicInteger.get()); // 1
System.out.println(atomicInteger.incrementAndGet()); // 2,当前值先自增1再获取
System.out.println(atomicInteger.get()); // 2
- compareAndSet(int expect, int update)
原值与 expect 相比较,如果不相等则返回 false 且原有值保持不变,否则返回 true 且原值更新为 update。
AtomicInteger atomicInteger = new AtomicInteger(10);
System.out.println(atomicInteger.get()); // 10
int expect = 12;
int update = 20;
Boolean b =atomicInteger.compareAndSet(expect, update);
System.out.println(b); // 10 不等于 12 不满足期望,所以返回false,且保持原值不变
System.out.println(atomicInteger.get());
7. 小结
本节通过一个简单的例子,介绍了 AtomicInteger 的基本用法,另外对一些核心方法做了简单介绍。在这个包下面,还有很多类似的工具类,也是对基本类型原子操作的封装,如 AtomicBoolean、AtomicLong,用法大同小异,希望大家在日常研发中多比较多总结,早日掌握之。