反骨之Java是如何解决并发中的原子性问题

1,128 阅读5分钟

前言

前段时间笔者写过一篇关于,《反骨之硬件&软件为Java并发编程中挖的坑(可见性&原子性&有序性)》的博文。

那么,这篇博文笔者想讲讲:Java是如何解决并发中的原子性问题的。

在开始正文之前,读者们需要知道,什么是互斥锁以及CAS的相关概念。

因为,Java并发编程正是依靠互斥锁CAS来解决—原子性问题的。

正文

关于互斥锁和CAS深度知识点,读者们需要自行去了解。

接下来,将对互斥锁CAS的基本概念进行简单概括:
  • 什么是互斥锁

    简单地说,互斥锁是一般是用于多线程编程中,为了防止多个线程(两个以上)同时对同一公共资源进行读写操作。

  • 什么是CAS(Compare And Sweep/Set)?

    CAS是其实是一种机制,是为了解决多线程环境下,使用互斥锁造成性能损耗的一种备用机制。

    众所周知,互斥锁是重量级锁,加锁和解锁都非常费时间。

    所以,当我们无法忍受互斥锁带来的性能损耗,却又想体验飞一般的感觉。

    那么,使用CAS来保证操作的原子性,在某种条件(数据安全性较低等...)下也是一种不错的选择。


笔者在之前的博文中提过,java并发包**(JUC**)包都是围绕着解决原子、有序、可见性而设计的。

那么,Java并发包中是如何利用互斥锁解决原子性问题的?

其实,在JUC并发包中,互斥锁的实现无外乎两个:

  • synchronized
  • Lock

说到synchronized关键字,笔者第一反应便是:慢!

不过,据说自从Java6之后,对synchronized进行了各种优化,执行速度和性能都与Lock实现的锁相差不大...

既然性能&速度两者相差不大,那为什么Java还会设计Lock相关互斥锁的实现呢?

在解答问题之前,读者们先看看Lock的代码实现:

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

从Lock实现中,我们可以看出相比于**“头铁”的synchronized,Lock支持“有个性”**的互斥锁实现,比如:

  • 支持头铁锁(synchronizes):lock/unlock
  • 支持锁中断:lockInterruptibly
  • 非阻塞获取锁:tryLock
  • 非阻塞超时等待获取锁:tryLock
  • 支持条件变量:newCondition

所以,Lock更适合复杂的加锁环境,而synchronized则相对比较简单粗暴。

那么,我们回到问题:为什么Java还会设计Lock相关互斥锁的实现呢?

因为,Java需要针对不用的场景,设计出不同的解决方案。

在此,笔者感慨,可能这就是大师和普通人的区别吧。


那么,CAS又是如何解决Java并发编程中的原子性问题的?

从前文的CAS的英文全称中, 笔者至可以猜出CAS至少包含:比较+(交换or赋值)两个操作(手动狗头)。

  • CAS操作流程组成元素:V-A-B
    • V:当前内存中的值
    • A:旧的预期值
    • B:即将更新的值
  • CAS判断条件:
    • 当且仅当,A等于V时,将V更新为B。

从CAS判断条件可以看出,CAS保证操作的原子性,并不是利用互斥锁,而是利用比较&交换操作。

那么,这种CAS操作方式,我们也可以称为—乐观锁。

当然,乐观锁有很多实现方式,在这里主要讲解利用版本号实现的乐观锁。其实用版本号实现乐观锁,在数据库中倒是经常使用。比如,要更新一条数据之前,则必须查询该数据且在查询的时候必须把当前版本号一起查询出来。

select age = 20, version from table1 where name = '张飞';

在这里查出的版本号version假设为:1。那么,在更新的时候需要将版本号作为更新条件:

update table1 set version+=1, age=23 where name = "张飞" and version = 1

其中,updata更新语句的执行状态无非为两个:

  • 成功:表示旧的预期值(A)等于当前内存中的值(V), 更新A = B(赋值)
    • 旧的预期值可以理解成查询出来的version,而**当前内存中的值(V)**可以理解成数据库表中存放的version。
    • 操作成功,也表示这条数据从执行查询操作,到更新操作期间,没有其他线程修改过这条数据。因为,一旦有人修改数据那么,version版本一定会改变。
  • 失败:表示有其他线程操作该条记录,当前内存中的值(V)已经改变,乐观锁失败。
    • 当前内存中的值(V)不等于旧的预期值(A),说明在执行查询操作,到更新操作期间,存在其他线程修改过该数据。

以上的的叙述,最终都想说明一个重点:

Java并发编程利用CAS(比较交换)操作,解决原子性问题。


总结

  • 互斥锁在JUC中的实现:synchronized和Lock

  • CAS在JUC中的实现:大部分前缀为Automic*的工具类,比如:

    AtomicInteger
    AtomicStampedReference
    等.....
    
  • CAS操作不由软件完成,而是由处理器CPU提供的指令完成。而CAS作为C PU指其本身是能够保证原子性的。

  • 所以,一般我们可以戏说互斥锁为“软件锁”,而CAS为”硬件锁“。