生活中我们看待一个事物总有不同的态度,比如半瓶水,悲观的人会觉得只有半瓶水了,而乐观的人则会认为还有半瓶水呢。很多技术思想往往源于生活,因此在多个线程并发访问数据的时候,有了悲观锁和乐观锁。
悲观锁和乐观锁其实本质都是一种思想,在JAVA中对于悲观锁的实现大家可能都很了解,可以通过synchronized
、ReentrantLock
加锁实现,本文不展开讲解了。那么乐观锁在JAVA中是如何实现的呢?底层的实现机制又是什么呢?
我们用一个账户取钱的例子来说明乐观锁和悲观锁的问题。
public class AccountUnsafe {// 余额private Integer balance;public AccountUnsafe(Integer balance) {this.balance = balance;}@Overridepublic Integer getBalance() {return balance;}@Overridepublic void withdraw(Integer amount) {balance -= amount;}
}
复制代码
withdraw()
方法是取钱方法。public static void main(String[] args) {// 账户10000元AccountUnsafe account = new AccountUnsafe(10000);List ts = new ArrayList<>();long start = System.nanoTime();// 1000个线程,每次取10元for (int i = 0; i < 1000; i++) {ts.add(new Thread(() -> {account.withdraw(10);}));}ts.forEach(Thread::start);ts.forEach(t -> {try {t.join();} catch (InterruptedException e) {e.printStackTrace();}});long end = System.nanoTime();// 打印账户余额和花费时间log.info("账户余额:{}, 花费时间: {}", account.getBalance(), (end-start)/1000_000 + " ms");}
复制代码
运行结果:
原因分析:
原因也很简单,取钱方法withdraw()
的操作balance -= amount;
看着就一行代码,实际上会生成多条指令,如下图所示:
多个线程运行的时候会进行线程切换,导致这个操作不是原子性,所以不是线程安全的。
最简单的方法,我想大家都能想到吧,给withdraw()
方法加锁,保证同一时刻只有一个线程能够执行这个方法,保证了原子性。
synchronized
关键字加锁。运行结果:
关键来了,如果用乐观锁的思想在JAVA中该如何实现呢?
大致思路就是我默认不加任何锁,我先把余额减掉10元,最后更新余额的时候,发现余额和我一开始不一样了,我就丢弃当前更新操作,重新读取余额的值,直到更新成功。
找啊找,最终发现JDK中的Unsafe
方法提供了这样的方法compareAndSwapInt
。
oldBalance
,计算出新的余额newBalance
unsafe.compareAndSwapInt()
方法,如果内存中余额属性的偏移量BALANCE_OFFSET
对应的值等于老的余额,说明的确没有被其他线程访问修改过,我就大胆的更新为newBalance
,退出方法那么是如何获取unsafe
呢?
Unsafe
类太底层了,它一般不建议程序员直接使用。这个Unsafe类的名称并不是说线程不安全的意思,只是这个类太底层了,不要乱用,对程序员来说不大安全。
最后别忘了余额balance
要加volatile修饰。
运行结果:
完成代码:
@Slf4j(topic = "a.AccountCAS")
public class AccountCAS {// 余额private volatile int balance;// Unsafe对象static final Unsafe unsafe;// balance 字段的偏移量static final long BALANCE_OFFSET;static {try {Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");theUnsafe.setAccessible(true);unsafe = (Unsafe) theUnsafe.get(null);// balance 属性在 AccountCAS 对象中的偏移量,用于 Unsafe 直接访问该属性BALANCE_OFFSET = unsafe.objectFieldOffset(AccountCAS.class.getDeclaredField("balance"));} catch (NoSuchFieldException | IllegalAccessException e) {throw new Error(e);}}public AccountCAS(Integer balance) {this.balance = balance;}public int getBalance() {return balance;}public void withdraw(Integer amount) {// 自旋while (true) {// 获取老的余额int oldBalance = balance;// 获取新的余额int newBalance = oldBalance - amount;// 更新余额,BALANCE_OFFSET表示balance属性的偏移量, 返回true表示更新成功, false更新失败,继续更新if(unsafe.compareAndSwapInt(this, BALANCE_OFFSET, oldBalance, newBalance)) {return;}}}public static void main(String[] args) {// 账户10000元AccountCAS account = new AccountCAS(10000);List ts = new ArrayList<>();long start = System.nanoTime();// 1000个线程,每次取10元for (int i = 0; i < 1000; i++) {ts.add(new Thread(() -> {account.withdraw(10);}));}ts.forEach(Thread::start);ts.forEach(t -> {try {t.join();} catch (InterruptedException e) {e.printStackTrace();}});long end = System.nanoTime();// 打印账户余额和花费时间log.info("账户余额:{}, 花费时间: {}", account.getBalance(), (end-start)/1000_000 + " ms");}
}
复制代码
好麻烦呀,我们自己调用原生的UnSafe
类实现乐观锁,有什么更好的方式吗?
当然有,其实JDK给我们封装了很多基于UnSafe
乐观锁实现的原子类,比如AtomicInteger
、AtomicReference
等等。我们用AtomicInteger
改写下上面的实现。
AtomicInteger
作为余额的类型addAndGet
方法运行结果:
原理:
查看源码最终也是调用的Unsafe
方法。
前面的一个取钱的例子,大家是不是对乐观锁的思想以及在JAVA中的实现更深入的认识。
在JAVA中对这种实现起了一个名字,叫做CAS, 全称Compare And Swap
,是不是很形象,先比较,然后再替换。
那CAS的本质是什么?
CAS先比较然后再替换,感觉是有2步,比较和替换,不像是原子性操作,如果不是原子性操作问题就可大了。实际上,CAS本质对应的是一条指令,是原子操作。
CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的原子性。
强调一点,CAS 必须借助 volatile
才能读取到共享变量的最新值来实现【比较并交换】的效果,因为volatile
会保证变量的可见性。
结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景或者读多写少的场景。
synchronized
是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。synchronized
,所以线程不会陷入阻塞,这是效率提升的因素之一如果本文对你有帮助的话,请留下一个赞吧