什么是原子操作呢?所谓原子操作,就是一个独立且不可分割的操作。
AtomicInteger 工具类提供了对整数操作的原子封装。为什么要对整数操作进行原子封装呢?
在 java 中,当我们在多线程情况下,对一个整型变量做加减操作时,如果不加任何的多线程并发控制,大概率会出现线程安全问题,也就是说当多线程同时操作一个整型变量的增减时,会出现运算结果错误的问题。AtomicInteger 工具类就是为了简化整型变量的同步处理而诞生的。
大家记住,在多线程并发下,所有不是原子性的操作但需要保证原子性时,都需要进行原子操作处理
,否则会出现线程安全问题。
概念已经了解了,那么 AtomicInteger 工具类怎么用呢?别急,最基本的用法请看下面的描述。
// 首先创建一个 AtomicInteger 对象
AtomicInteger atomicInteger = new AtomicInteger();
// 在操作之前先赋值,如果不显式赋值则值默认为 0 ,就像 int 型变量使用前做初始化赋值一样。
atomicInteger.set(1000);
// 之后可以调用各种方法进行增减操作
...
// 获取当前值
atomicInteger.get();
// 先获取当前值,之后再对原值加100
atomicInteger.getAndAdd(100)
// 先获取当前值,之后再对原值减1
atomicInteger.getAndDecrement()
...
是不是很简单,AtomicInteger 在我们日常实践中,到底应该应用在哪些场合比较合适呢?下面我们给出最常用的场景说明。
AtomicInteger 经常用于多线程操作同一个整型变量时,简化对此变量的线程安全控制的场合。当在研发过程中遇到这些场景时,就可以考虑直接使用 AtomicInteger 工具类辅助实现,完全可以放弃使用 synchronized 关键字做同步控制。
下面我们用 AtomicInteger 工具实现电影院某场次电影票销售的例子。
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 工具类的认识。
除了上面代码中使用的最基本的 AtomicInteger (int)、AtomicInteger ()、 set () 、get () 和 decrementAndGet () 方法之外,我们还需要掌握其他几组核心方法的使用。下面逐个介绍。
第 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
第 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
原值与 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());
① AtomicReference和AtomicInteger非常类似,不同之处就在于AtomicInteger是对整数的封装,而AtomicReference则对应普通的对象引用。也就是它可以保证你在修改对象引用时的线程安全性。
② AtomicReference是作用是对”对象”进行原子操作。 提供了一种读和写都是原子性的对象引用变量。
原子意味着多个线程试图改变同一个AtomicReference(例如比较和交换操作)将不会使得AtomicReference处于不一致的状态
。
本节介绍的 AtomicReference 工具类直译为 “原子引用”。原子操作的概念我们在之前的章节中已经介绍过了,那什么是引用呢?
引用就是为对象另起一个名字,引用对象本身指向被引用对象,对引用对象的操作都会反映到被引用对象上。在 Java 中,引用对象本身存储的是被引用对象的 “索引值”。如果对引用概念还是比较模糊,请查阅 Java 基础语法知识复习。
AtomicReference 工具类和 AtomicInteger 工具类很相似,只是 AtomicInteger 工具类是对基本类型的原子封装,而 AtomicReference 工具类是对引用类型的原子封装。我们用一张原理图展示其基本逻辑。
我们看下面 AtomicReference 工具类的基本用法。
先简单定义个 User 类
@Data
@AllArgsConstructor
public class User {
private String name;
private Integer age;
}
使用 AtomicReference 初始化,并赋值
public static void main( String[] args ) {
User user1 = new User("张三", 23);
User user2 = new User("李四", 25);
User user3 = new User("王五", 20);
//初始化为 user1
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(user1);
//把 user2 赋给 atomicReference
atomicReference.compareAndSet(user1, user2);
System.out.println(atomicReference.get());
//把 user3 赋给 atomicReference
atomicReference.compareAndSet(user1, user3);
System.out.println(atomicReference.get());
}
输出结果如下:
User(name=李四, age=25)
User(name=李四, age=25)
解释
compareAndSet(V expect, V update)
该方法作用是:如果atomicReference==expect,就把update赋给atomicReference,否则不做任何处理。
AtomicReference 和 AtomicInteger 非常类似,不同之处就在于 AtomicInteger 是对整数的封装,且每次只能对一个整数进行封装,而 AtomicReference 则是对普通的对象引用的封装,可将多个变量作为一个整体对象,操控多个属性的原子性的并发类。
下面我们用 AtomicReference 工具类实现生活中汽车牌照竞拍的例子:假设总共有 10 位客户参与竞拍,每位客户只有一次竞拍机会,竞拍是资格竞拍不以竞拍价格为目的。请看下面的代码。
import java.util.concurrent.atomic.AtomicReference;
public class AtomicReferenceTest {
// 代表待拍的车牌
private static CarLicenseTag carLicenseTag = new CarLicenseTag(80000);
// 创建一个 AtomicReference 对象,对车牌对象做原子引用封装
private static AtomicReference<CarLicenseTag> carLicenseTagAtomicReference = new AtomicReference<>(carLicenseTag);
public static void main(String[] args) {
// 定义5个客户进行竞拍
for(int i=1; i<=5; i++) {
AuctionCustomer carAuctionCustomer = new AuctionCustomer(carLicenseTagAtomicReference, carLicenseTag, i);
// 开始竞拍
new Thread(carAuctionCustomer).start();
}
}
}
/**
* 车牌
*/
public class CarLicenseTag {
// 每张车牌牌号事先是固定的
private String licenseTagNo = "沪X66666";
// 车牌的最新拍卖价格
private double price = 80000.00;
public CarLicenseTag(double price) {
this.price += price;
}
public String toString() {
return "CarLicenseTag{ licenseTagNo='" + licenseTagNo + ", price=" + price + '}';
}
}
每个客户是如何动作呢,看下面的代码。
import java.util.Random;
import java.util.concurrent.atomic.AtomicReference;
public class AuctionCustomer implements Runnable {
private AtomicReference<CarLicenseTag> carLicenseTagReference;
private CarLicenseTag carLicenseTag;
private String customerNo;
public AuctionCustomer(AtomicReference<CarLicenseTag> carLicenseTagReference, CarLicenseTag carLicenseTag, int customerNo) {
this.carLicenseTagReference = carLicenseTagReference;
this.carLicenseTag = carLicenseTag;
this.customerNo = customerNo+"";
}
public void run() {
// 客户竞拍行为 (模拟竞拍思考准备时间4秒钟)
try {
Thread.sleep(new Random().nextInt(4000));
} catch (Exception e) {}
// 举牌更新最新的竞拍价格
// 此处做原子引用更新
boolean bool = carLicenseTagReference.compareAndSet(carLicenseTag,
new CarLicenseTag(new Random().nextInt(1000)));
System.out.println("第" + customerNo + "位客户竞拍" + bool + " 当前的竞拍信息" + carLicenseTagReference.get().toString());
}
}
运行后运行结果如下。
第4位客户竞拍true 当前的竞拍信息CarLicenseTag{ licenseTagNo='沪X66666, price=80946.0}
第5位客户竞拍false 当前的竞拍信息CarLicenseTag{ licenseTagNo='沪X66666, price=80946.0}
第2位客户竞拍false 当前的竞拍信息CarLicenseTag{ licenseTagNo='沪X66666, price=80946.0}
第1位客户竞拍false 当前的竞拍信息CarLicenseTag{ licenseTagNo='沪X66666, price=80946.0}
第3位客户竞拍false 当前的竞拍信息CarLicenseTag{ licenseTagNo='沪X66666, price=80946.0}
至此,大家对 AtomicReference 已经有了初步的理解,接下来我们继续丰富对 AtomicReference 工具类的认识。
除过上面代码中使用的最基本的 AtomicReference (V)、compareAndSet (int, int)、get () 方法之外,我们还再介绍两个方法的使用。下面逐个介绍。
可以使用不带参数的构造方法构造好对象后,再使用 set () 方法设置待封装的对象。等价于使用 AtomicReference (V) 构造方法。
此方法以原子方式设置为给定值,并返回旧值。逻辑等同于先调用 get () 方法再调用 set () 方法。
adder 加法器
DoubleAdder 工具类采用了 “分头计算最后汇总” 的思路,避免每一次(细粒度)操作的并发控制,提高了并发的性能。什么是细粒度的同步控制呢?所谓细粒度的同步控制,指的是对待同步控制对象的每一次操作都需要加以控制,这样描述是不是有点抽象,别着急,看下面的图示。
我们看下面 DoubleAdder 工具类的基本用法。
// 首先创建一个 DoubleAdder 对象
DoubleAdder doubleAdder = new DoubleAdder();
...
// 调用累加方法
doubleAdder.add(50.5);
doubleAdder.add(49.5);
// 调用求和方法
double sum = doubleAdder.sum();
...
是不是很简单,那 DoubleAdder 在我们日常实践中,到底应该应用在哪些场合比较合适呢?下面我们给出最常用的场景说明。
DoubleAdder 经常用于多线程并发做收集统计数据的场合,而不是细粒度的同步控制。
下面我们用 DoubleAdder 工具类实现一个生活案例:某商场为了掌握客流特征,在商场所有出入口架设了人体特征识别设备,此类设备可以有效识别客人性别等信息。基于此,商场管理办公室计划制作一个客流性别流量图表,用于决策商场的服务内容。
import java.util.concurrent.atomic.DoubleAdder;
public class DoubleAdderTest {
// 首先创建三个 DoubleAdder 对象分别表示统计结果
// 代表当天所有进入商场的男性客户总数量
private static DoubleAdder maleCount = new DoubleAdder();
// 代表当天所有进入商场的女性客户总数量
private static DoubleAdder womenCount = new DoubleAdder();
// 代表当天所有进入商场的未能识别的客户总数量
private static DoubleAdder unknownGenderCount = new DoubleAdder();
public static void main(String[] args) {
// 定义30个商场入口检测设备
for (int i = 1; i <= 30; i++) {
MonitoringDevice monitoringDevice = new MonitoringDevice(maleCount, womenCount, unknownGenderCount, i);
// 开启检测设备进行检测
new Thread(monitoringDevice).start();
}
}
}
在上面的代码中,首先创建三个 DoubleAdder 对象分别表示统计结果,然后创建了 30 个商场入口检测设备模拟检测识别,接下来每个检测设备如何动作呢,看下面的代码。
import java.util.Random;
import java.util.concurrent.atomic.DoubleAdder;
public class MonitoringDevice implements Runnable {
private DoubleAdder maleCount;
private DoubleAdder womenCount;
private DoubleAdder unknownGenderCount;
private String monitoringDeviceNo;
public MonitoringDevice(DoubleAdder maleCount, DoubleAdder womenCount, DoubleAdder unknownGenderCount, int monitoringDeviceNo) {
this.maleCount = maleCount;
this.womenCount = womenCount;
this.unknownGenderCount = unknownGenderCount;
this.monitoringDeviceNo = "第" + monitoringDeviceNo + "监控采集处";
}
public void run() {
while (true) {
// 监测处理 (监测设备输出1代表男性,0代表女性,其他代表未能识别,此处随机产生监测结果)
try {
Thread.sleep(new Random().nextInt(3000));
} catch (Exception e) {}
int monitoringDeviceOutput = new Random().nextInt(3);
// 对监测结果进行统计
switch (monitoringDeviceOutput) {
case 0:
womenCount.add(1);
System.out.println(monitoringDeviceNo + "统计结果: womenCount=" + womenCount.sum());
break;
case 1:
maleCount.add(1);
System.out.println(monitoringDeviceNo + "统计结果: maleCount=" + maleCount.sum());
break;
default:
unknownGenderCount.add(1);
System.out.println(monitoringDeviceNo + "统计结果: unknownGenderCount=" + unknownGenderCount.sum());
break;
}
}
}
}
在 MonitoringDevice 类中,首先模拟监测设备输出,然后将输出结果使用 add () 进行统计累加,使用 sum () 输出累加结果。运行一段时间后运行结果如下。
...
第10监控采集处统计结果: maleCount=39.0
第17监控采集处统计结果: maleCount=40.0
第15监控采集处统计结果: unknownGenderCount=41.0
第25监控采集处统计结果: womenCount=47.0
...
上面的案例中,总共计算了三个统计值,每一个统计值都使用了多个线程同时进行统计计算。在统计过程中,每一个线程只需要累加自己的那份统计结果,所以不需要做同步控制,只要在最后进行汇总统计结果时做同步控制进行汇总即可。像这样的场景使用 DoubleAdder 工具类会非常方便简洁。
至此,大家对 DoubleAdder 已经有了初步的理解,接下来我们继续丰富对 DoubleAdder 工具类的认识。
除过上面代码中使用的最基本的 add (int)、sum () 方法之外,我们再介绍两个方法的使用。
将累加器值置为 0,即为后继使用重新归位。
此方法逻辑等同于先调用 sum () 方法再调用 reset () 方法,简化代码编写。
Accumulator [əˈkjuːmjəleɪtə(r)] 累加器
相比 LongAdder,LongAccumulator工具类提供了更灵活更强大的功能。不但可以指定计算结果的初始值,相比 LongAdder 只能对数值进行加减运算,LongAccumulator 还能自定义计算规则,比如做乘法运行,或其他任何你想要的计算规则。这样描述是不是有点抽象,别着急,看下面的图示。
我们看下面 LongAccumulator 工具类的基本用法。
// 首先创建一个双目运算器对象,这个对象实现了计算规则。
LongBinaryOperator longBinaryOperator = new LongBinaryOperator() {
@Override
public long applyAsLong(long left, long right) {
...
}
}
// 接着使用构造方法创建一个 LongAccumulator 对象,这个对象的第1个参数就是一个双目运算器对象,第二个参数是累加器的初始值。
LongAccumulator longAccumulator = new LongAccumulator(longBinaryOperator, 0);
...
// 调用累加方法
longAccumulator.accumulate(1000)
// 调用结果获取方法
long result = longAccumulator.get();
...
是不是简单又强大!LongAccumulator 在我们日常实践中,到底应该应用在哪些场合比较合适呢?下面我们给出最常用的场景说明。
LongAccumulator 经常用于自定义运算规则场景下的多线程并发场合。一些简单的累加计算可以直接使用我们之前课程中介绍的工具类,但是当运行规则比较复杂或者 JDK 没有提供对应的工具类时,可以考虑 LongAccumulator 辅助实现。当然所有可使用 LongAdder 的场合都可使用 LongAccumulator 代替,但是没有必要。
下面我们用 LongAccumulator 工具类实现上一节中的生活实例,为了简化叙述,本节我们只统计男性客户总数量。请看下面的代码。
import java.util.concurrent.atomic.LongAccumulator;
public class LongAccumulatorTest {
// 此处的运算规则是累加,所以创建一个加法双目运算器对象作为构造函数的第一个参数。
// 将第二个参数置为0,表示累加初始值。
// maleCount 对象代表当天所有进入商场的男性客户总数量。
private static LongAccumulator maleCount = new LongAccumulator(new LongBinaryOperator() {
// 此方法用于实现计算规则
@Override
public long applyAsLong(long left, long right) {
// 在本例中使用加法计算规则
return left + right;
}
}, 0);
public static void main(String[] args) {
// 定义30个商场入口检测设备
for (int i = 1; i <= 30; i++) {
MonitoringDevice monitoringDevice = new MonitoringDevice(maleCount, i);
// 开启检测设备进行检测
new Thread(monitoringDevice).start();
}
}
}
在上面的代码中,首先创建一个 LongAccumulator 对象表示统计结果,然后创建了 30 个商场入口检测设备模拟检测识别,接下来每个检测设备如何动作呢,看下面的代码。
import java.util.Random;
import java.util.concurrent.atomic.LongAccumulator;
/**
* 模拟设备
*/
public class MonitoringDevice implements Runnable {
private LongAccumulator maleCount;
private String monitoringDeviceNo;
public MonitoringDevice(LongAccumulator maleCount, int monitoringDeviceNo) {
this.maleCount = maleCount;
this.monitoringDeviceNo = "第" + monitoringDeviceNo + "监控采集处";
}
/**
* 设备运行的处理逻辑
*/
public void run() {
while (true) {
// 监测处理 (监测设备输出1代表男性,0代表女性,其他代表未能识别,此处随机产生监测结果)
try {
Thread.sleep(new Random().nextInt(3000));
} catch (Exception e) {}
int monitoringDeviceOutput = new Random().nextInt(3);
// 对监测结果进行统计
switch (monitoringDeviceOutput) {
case 1: maleCount.accumulate(1);
System.out.println("统计结果: maleCount=" + maleCount.get());
break;
default:
System.out.println("忽略统计");
break;
}
}
}
}
在 MonitoringDevice 类中,首先模拟监测设备输出,然后将输出结果使用 add () 进行统计累加,使用 sum () 输出累加结果。运行一段时间后运行结果如下。
...
忽略统计
统计结果: maleCount=50
...
上面的示例中,使用 LongAccumulator 实现了上一节中相同的需求。对比观察,能够体会到 LongAccumulator 工具类更灵活的地方,但同时也更复杂一些。
至此,大家对 LongAccumulator 已经有了初步的理解,接下来我们继续丰富对 LongAccumulator 工具类的认识。
除过上面代码中使用的最基本的 accumulate (int)、get () 方法之外,我们再介绍两个方法的使用。
将累加器值置为 0,即为后继使用重新归位。