一、从一个经典的面试题谈起
相信有过几年的开发经验的人或多或少对AtomicInteger类都会有些亲切吧。AtomicInteger在java.util.concurrent.atomic包下,被称为原子操作类。高并发的情况下,我们可以在不使用锁的前提下(如synchronized关键字,或者Lock类),可以做到保证数据的正确性。那他内部是如何实现的呢?
在回答这个问题之前,我们先看一个非常经典的面试题,问我们如何做到三个线程循环打印ABC10次?其实这个解决思路有很多,可以使用最常见的锁机制那一套(synchronized,notifyAll,wait),也可以使用Lock 和 Condition,还可以使用Semaphore类,今天我们来说一种不使用锁机制的思路。那就是下面要说的AtomicInteger类。先看下具体的代码实现,他是如何实现三个线程循环打印ABC 10次的。
public class AtomicIntegerExample {
private AtomicInteger sycValue = new AtomicInteger(0);
private static final int MAX_SYC_VALUE = 3 * 10;
public static void main(String[] args) {
AtomicIntegerExample example = new AtomicIntegerExample();
ExecutorService service = Executors.newFixedThreadPool(3);
service.execute(example.new RunnableA());
service.execute(example.new RunnableB());
service.execute(example.new RunnableC());
service.shutdown();
}
private class RunnableA implements Runnable {
public void run() {
while (sycValue.get() < MAX_SYC_VALUE) {
if (sycValue.get() % 3 == 0) {
System.out.println(String.format("第%d遍",
sycValue.get() / 3 + 1));
System.out.println("A");
sycValue.getAndIncrement();
}
}
}
}
private class RunnableB implements Runnable {
public void run() {
while (sycValue.get() < MAX_SYC_VALUE) {
if (sycValue.get() % 3 == 1) {
System.out.println("B");
sycValue.getAndIncrement();
}
}
}
}
private class RunnableC implements Runnable {
public void run() {
while (sycValue.get() < MAX_SYC_VALUE) {
if (sycValue.get() % 3 == 2) {
System.out.println("C");
System.out.println();
sycValue.getAndIncrement();
}
}
}
}
}
代码并不复杂,我们看一下其中的一句关键代码
sycValue.getAndIncrement();
上面的代码执行结果是可以正常依次循环打印ABC10次的。我们要分析他为什么可以实现上述需求。分析过程如下:
我们开启了三个线程,来分别打印ABC,我们暂且就叫ABC线程吧。当三个线程开始抢夺cpu执行权,我们假设C线程抢到了,在run方法中我们可以看到
...
while (sycValue.get() < MAX_SYC_VALUE) {
if (sycValue.get() % 3 == 2) {
System.out.println("C");
System.out.println();
sycValue.getAndIncrement();
}
}
...
我们来看下其中关键代码,getAndIncrement是AtomicInteger 类中的一个方法,我们进入源码中看看,这句代码到底执行了啥
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
这里有一个很意思的类Unsafe。要说到这个类就得牵扯出另外一个概念,也是今天要说的核心概念:CAS。
二、CAS
CAS全称是Compare And Swap翻译过来就是比较交换。我们可以抽象出一个方法cas(V,E,N),其包含3个参数
V表示要更新的变量
E表示预期值
N表示新值
当执行cas函数时,我们会比较V和E的值是否相等,如果不相等,表示预期值和要更新的值不一样说明该操作已经有其他线程完成了,当前线程无需做任何操作。如果相等,则表示预期值和当前要更新的值一样,则说明此时是可以进行cas操作的。
回到代码这里,
利用Unsafe类的JNI本地方法实现,使用CAS指令,来保证读-改-写是一个原子操作。compareAndSwapInt有4个参数,this - 当前AtomicInteger对象,valueOffset- value属性在内存中的位置(需要强调的不是value值在内存中的位置),expect - 预期值,update - 新值,根据上面的CAS操作过程,当内存中的value值等于expect值时,则将内存中的value值更新为update值,并返回true,否则返回false。在这里我们有必要对Unsafe有一个简单点的认识,从名字上来看,不安全,确实,这个类是用于执行低级别的、不安全操作的方法集合,这个类中的方法大部分是对内存的直接操作,所以不安全,但当我们使用反射、并发包时,都间接的用到了Unsafe。
...
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
...
简单点理解,CAS保证数据操作的原子性。我们知道要使线程数据安全还得同时保证数据的可见性,我们查看AtomicInteger源码,会发现其实内部是使用了volatile 关键字来保证数据的可见性。
...
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
...
三、AtomicInteger源码阅读总结
所以对AtomicInteger原子类操作为什么在高并发下能保证数据多而不乱,总结为:内部使用cas指令保证了原子性,使用volatile关键字 保证了数据在线程中的可见性。
四、数据库乐观锁
既然谈到了CAS操作方式,那我们就谈谈与之相关的数据库乐观锁。为啥呢,因为数据库乐观锁实现方式之一就是使用CAS算法。
在谈到乐观锁之前,我们先看下面一个案例。
现在有张账户表,有账号id和余额money两个字段。一般用户购买商品,系统要做的流程一般如下:
1、查询当前账号余额;
2、根据余额与商品价格判断用户是否可以进行下单购买
3、余额可以支持用户下单,下单之后对账户表中的余额值进行相应的扣除
假设有两个人A、B同时使用同一个账号id为123456的账号购买商品,已知该账号还剩1000元。当A和B都看中了一款价格为800元的商品,他们同时进行了下单。在第一步中,A和B同时下单,对于系统而言,执行了以下sql
select * from table where id=123456
AB此时都得知账户有1000元,于是进行第二步的时候判断都是可以下单的。往下走,进行第三步,更新账户表中的余额值。这下一个流程走下来,AB使用相同的账号购买了2件800的商品,结果账户余额还剩200.这样的结果老板肯定是会亏的裤衩都不剩的。那怎么解决这个问题呢?我们可以使用数据库锁机制来解决。
数据库锁分为悲观锁和乐观锁。
悲观锁,通俗的理解总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加锁(读锁、写锁、行锁等),当其他线程想要访问数据时,都需要阻塞挂起。可以依靠数据库实现,如行锁、读锁和写锁等,都是在操作之前加锁,在Java中,synchronized的思想也是悲观锁。
乐观锁,总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改。
乐观锁有两种方式可以实现,
一种是我们刚刚上面提到的使用CAS操作,当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作,即不断的重试。注意:这种方式会产生ABA的问题。
还有一种方式,是在数据表中加上一个数据版本号version字段,默认为0,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。如在该场景中,我们在账户表中添加一个version字段,在第一步和第二步中和前面结果是一样的,当进行第三步的时候,加入A先执行了更新账户表余额操作,那么此时该账户的version为1,当B执行的时候,本质是执行以下sql
//因为在第一步查询的时候,B得到的version值为0
update table set money =200 where id =123456 and version=0
很显然此时B无法更新该账户的数据,因为此时version已经在A更新的时候增加了1,此时version是1。这样我们就可以通过update的记录数得知其是否更新成功。
网友评论