在实际的业务场景中为了保证数据的安全性,原子操作是必不可少的,比如一些秒杀场景、银行存取款、火车票抢购等业务。为了模拟一下原子操作的必要性,我们实现一个不安全的抢票代码:
public class Main {
public static int ticket = 5;
public static void main(String[] args) throws InterruptedException {
for(int i = 0; i < 20; i ++) {
new Thread(() -> {
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (ticket > 0) {
System.out.println("用户" + Thread.currentThread().getName() +"抢购成功!余票:" + -- ticket);
}
}).start();
}
TimeUnit.MILLISECONDS.sleep(500);
}
}
用户Thread-12抢购成功!余票:2
用户Thread-6抢购成功!余票:-1
用户Thread-10抢购成功!余票:2
用户Thread-13抢购成功!余票:4
用户Thread-7抢购成功!余票:1
用户Thread-19抢购成功!余票:0
用户Thread-2抢购成功!余票:3
用户Thread-5抢购成功!余票:-2
用户Thread-3抢购成功!余票:4
解释一下为什么会出现这样的情况?
因为在本代码中if判断以及 --ticket
不是一个原子的操作,这个操作可以分解成:
- 判断余票
- 读取ticket
- ticket - 1
- 存到ticket中
所以一些线程在读取ticket之后发现还有余票,那么即使这时候因为时间片轮转到其他线程抢票成功导致无票了,这个线程也会抢票成功。很明显,这样非常的不安全,他抢到票了到时候没地方坐,把列车员踢下去吗?
那么如何实现线程安全?就要使用synchronized实现整个不安全的代码为原子操作(即不可拆分的操作)。再详细的解释一下什么叫原子操作:
在世界上我们可以认为原子是最小的结构。(说夸克的给我爬,夸克不能被直接观测到,你要是杠那你说的对)一个原子操作是不可再分解的,如果这个原子操作没执行完,其他的线程必须要等待这个原子操作结束后才可以被轮到执行。因此,在上述代码中只要把抢票的代码封装一下就可以实现抢票安全(注意这里说的不是线程安全,抛出一个小水包等后续博客再解释)了。
优化:实现抢票安全的代码
public class Main {
public static int ticket = 5;
public static void main(String[] args) throws InterruptedException {
for(int i = 0; i < 20; i ++) {
new Thread(() -> {
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
getTicket();
}).start();
}
TimeUnit.MILLISECONDS.sleep(500);
}
public static synchronized void getTicket() {
if (ticket > 0) {
System.out.println("用户" + Thread.currentThread().getName() +"抢购成功!余票:" + -- ticket);
}
}
}
用户Thread-2抢购成功!余票:4
用户Thread-8抢购成功!余票:3
用户Thread-14抢购成功!余票:2
用户Thread-15抢购成功!余票:1
用户Thread-7抢购成功!余票:0
可以看到每个线程抢到票的几率是相等的。
使用JUC提供的原子操作优化抢票方案
首先我们要明白为什么这样做?
上述使用的是synchronized关键字修饰的方法解决了方案,但我们都知道synchronized不仅效率低下而且会经常出现死锁的问题。在JUC里提供的方案是不急于synchronized实现的,而是基于原生底层实现的。
优化:使用JUC实现抢票安全(我这个例子不是太好因为不使用synchronized还是无法保证原子性,但你们懂的就行)
public class Main {
public static AtomicInteger ticket = new AtomicInteger(5);
public static void main(String[] args) throws InterruptedException {
for(int i = 0; i < 20; i ++) {
new Thread(() -> {
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
getTicket();
}).start();
}
TimeUnit.MILLISECONDS.sleep(500);
}
public static synchronized void getTicket() {
if (ticket.get() > 0) {
System.out.println("用户" + Thread.currentThread().getName() +"抢购成功!余票:" + ticket.decrementAndGet());
}
}
}
用户Thread-6抢购成功!余票:4
用户Thread-19抢购成功!余票:3
用户Thread-18抢购成功!余票:2
用户Thread-15抢购成功!余票:1
用户Thread-17抢购成功!余票:0
···
原子操作类并没有使用到传统的同步机制,而是通过了一种 CAS 的机制来完成的,那么CAS 是什么?后面会进行详细的描述,整个的原子类都采用了类似的实现机制。
由于在实际的项目开发中会牵扯到多种数据类型的使用,所以在java.util.concurrent.atomic
包中提供了多种原子性的操作类支持,这些操作类可以分为四类:
- 基本类型:Atomicinteger、Atomiclong.AtomicBoolean ;
- 数组类型:AtomicintegerArray、AtomicLongArray、AtomicReferenceArray;
- 引用类型:AtomicReference、AtomicStampedReference、AtomicMarkableReference;
- 对象的属性修改类型:AtomiclntegerFieldUpdater、AtomicLongFieldUpdater、 AtomicReferenceFieldUpdater。