【多线程】浅探CAS实现原理

news/2024/5/17 19:34:16 标签: 多线程, CAS, synchronized

前言

CAS,全称是Compare And Swap,即比较并交换,是一种乐观锁的实现


悲观锁与乐观锁

悲观锁

总是假设最坏的情况,线程a每次去获取或更新数据的时候,都会觉得别的线程也正在修改这个数据,为了避免自己的更新操作丢失,线程a会尝试获取此数据的锁,线程a获取到之后,才能对此数据进行一些更新操作。在此期间,别的线程无法更新,只能等到线程a释放锁之后,才能进行更新

之所以叫做悲观锁,是因为这是一种对数据的修改抱有悲观态度的并发控制方式。我们一般认为数据被并发修改的概率比较大,所以需要在修改之前先加锁。

悲观并发控制,实际上是一种“先取锁再访问”的保守策略。

synchronized就是对悲观锁的一种实现

乐观锁

乐观锁假设数据一般不会造成冲突,所以在拿数据的时候不会去加锁,但是会在更新的时候判断此期间内有没有别的线程修改过数据

CAS机制就是对乐观锁的一种实现


乐观锁的实现——CAS

CAS操作一般包含3个参数,期望值、内存值、新值。如果期望值与内存值相等,则用新值去更新这个内存值。如果不相等,则可以再次进行比较,一直到成功为止。

CAS是一种非阻塞的算法,线程在更新失败时,不需要挂起,因此省去了大量线程上下文切换的开销。

java使用Unsafe类来支持CAS操作,对Unsafe类不了解的同学可以先参考我的另外一篇文章JUC基石——Unsafe类。

我们用java代码来简要模拟CAS的过程:

    /**
     * @param expect 期望值
     * @param update 新值
     * @return
     */
    public int cas(int expect, int update) {
        //更新失败就一直进行忙循环
        while (true) {
            //get方法从内存中获取最新的值
            int memory = get();
            if (memory == expect) {
                //set方法将内存中的值设置为新值
                set(update);
                return update;
            }
        }
    }

当然这只是一个模拟,实际cas操作将会用到底层的系统指令,这些指令将会保证整个cas操作具有原子性,关于这些指令,可能要另开篇幅讲解。


悲观锁的实现——synchronized

synchronized是悲观锁的典型实现,有关它的用法,可以参考我的这篇文章浅说Synchronized,早期的synchronized十分笨重,所幸在1.6之后进行了大量的优化,锁性能提升了很多,关于synchronized的优化,可以参考我的这篇文章Synchronized的优化。


CAS的缺陷——ABA问题

假设有这样的一种情况,x的内存值首先是A,线程1读取到了A,之后忙别的事情了,该值在之后被线程2改成了B,接着又被线程3改成了A,线程1此时进行CAS操作,发现内存值还是A,于是进行了更新操作。但是这个A已经不是原来的A了,或者说不是之前那个版本的A了。

解决这种缺陷,可以使用带版本号时间戳CAS,A值每次被更新后,版本号加1,或者更新时间戳。此时内存值与期望值相等,但却不是线程期望的版本号。

此时的A→B→A,就变成了A(version=1)→B(version=2)→A(version=3)。当使用带版本号的CAS后,就可以避免ABA问题。


CASsynchronized适用场景

线程冲突比较小时,CAS进行自旋操作,synchronized升级为轻量级锁,也是在自旋,两者的效率差不多。

线程冲突严重时,CAS绝大部分的自旋操作将大量浪费CPU的时间片,此时synchronized升级为重量级锁,但在这种情况下,synchronized的效率远高于CAS。(因为在线程冲突严重时,synchronized已经意识到轻量级锁的自旋操作效率低下,主动升级为重量级锁,所以这里的忙循环的开销远远大于线程切换的开销)。


JAVA中的CAS操作

AtomicInteger实现了CAS,可以原子性地更新一个int类型数据,其实底层也是调用Unsafe类。但时如果要一次原子性地更新多个变量,可以使用AtomicReference,当然这个存在上述的ABA问题,这时可以使用带版本号机制的CAS实现类——AtomicStampedReference,该类使用了一个stamp字段来表示版本号,代码如下图所示:

 


数据库中的CAS操作

数据库中的乐观锁机制不需要借助表锁、行锁等,以修改库存为例,乐观锁实现如下:

update goods set quantity=99 where id=1 and quantity = 100;

这个情景比较简单,暂不考虑ABA问题。

以上SQL其实还是有一定的问题的,就是一旦高并发的时候,就只有一个线程可以修改成功,那么就会存在大量的失败。所以,需要减小乐观锁的粒度

有一条比较好的建议,可以减小乐观锁力度,最大程度的提升吞吐率,提高并发能力!如下:

update goods set quantity=quantity - 1 where id = 1 and quantity - 1 > 0

将quantity=100转化成了quantity - 1 > 0,大大减少了乐观锁的力度,效率得到很大的提升。


JVM中的CAS操作

     Java调用new object()会创建一个对象,这个对象会被分配到JVM的堆中。那么这个对象到底是怎么在堆中保存的呢?

首先,new object()执行的时候,这个对象需要多大的空间,其实是已经确定的,因为java中的各种数据类型,占用多大的空间都是固定的。怎么去确定对象大小,可以参考我的这篇文章对象的内存布局,怎样确定对象的大小。那么接下来的工作就是在堆中找出那么一块空间用于存放这个对象。 

在单线程的情况下,一般有两种分配策略:

  • 指针碰撞:这种一般适用于内存是绝对规整的(内存是否规整取决于内存回收策略)。用过的内存放在一边,空闲的内内存放在另外一边,之间有一个分界指针,分配空间的工作只是将分界指针向空闲内存一侧移动对象大小的距离即可。

  • 空闲列表:这种适用于内存非规整的情况,这种情况下JVM会维护一个内存列表,记录哪些内存区域是空闲的,大小是多少。给对象分配空间的时候去空闲列表里查询到合适的区域然后进行分配即可。

然而,对象的创建工作是很频繁的,为了保证效率,JVM可以并发地给对象分配内存空间。由于分配内存的时候不是原子性的操作,至少需要以下几步:查找空闲列表、分配内存、修改空闲列表等等,这是不安全的。解决并发时的安全问题也有两种策略:

  • CAS:实际上虚拟机采用CAS配合上失败重试的方式保证更新操作的原子性,原理和上面讲的一样。

  • TLAB:如果使用CAS其实对性能还是会有影响的,所以JVM又提出了一种更高级的优化策略:每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲区(TLAB),线程内部需要分配内存时直接在TLAB上分配就行,避免了线程冲突。只有当缓冲区的内存用光需要重新分配内存的时候才会进行CAS操作分配更大的内存空间。 虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来进行配置(jdk5及以后的版本默认是启用TLAB的)。


最后放一个很有意思的东西,以和面试官聊天的形式讲讲cas,原文链接并发面试:又是CAS问题,10家面试9家问


http://www.niftyadmin.cn/n/1705213.html

相关文章

【多线程】CountDownLatch实现原理

前言 CountDownLatch是多线程中一个比较重要的概念,它可以使得一个或多个线程等待其他线程执行完毕之后再执行。它内部有一个计数器和一个阻塞队列,每当一个线程调用countDown()方法后,计数器的值减少1。当计数器的值不为0时,调用…

利用Ghost实现点对点克隆

要想利用点对点功能,需要将两台计算机通过打印口,USB接口或网卡连接起来,一定要使用正确的线缆,要使用网卡,还要在引导DOS系统时加载正确的网卡驱动,这个在制作引导映像时就要选择正确,我实验时…

面试官:请写一个你认为比较“完美”的单例

转自半路雨歌,原文地址面试官:请写一个你认为比较“完美”的单例 单例模式是保证一个类的实例有且只有一个,在需要控制资源(如数据库连接池),或资源共享(如有状态的工具类)的场景中…

【转】asp.net2.0中gridView的删除确认问题

asp.net2.0中gridView的删除确认问题用惯了datagrid,第一次用gridView,倒有点不习惯.写删除确认时还有点不习惯,经过一番折磨,gridView的删除确认可以这样写: if(e.Row.RowType DataControlRowType.DataRow) ...{ e.Row.Cells[0].Attributes.Add …

一步一步推导出 Mysql 索引的底层数据结构

Mysql 作为互联网中非常热门的数据库,其底层的存储引擎和数据检索引擎的设计非常重要,尤其是 Mysql 数据的存储形式以及索引的设计,决定了 Mysql 整体的数据检索性能。 我们知道,索引的作用是做数据的快速检索,而快速…

【多线程】CyclicBarrier实现原理

前言 CyclicBarrier,字面意思“循环屏障”,用于多个线程一起到达屏障点后,多个线程再一起接着运行的情况。例如,线程1和线程2一起运行,线程1运行到屏障点a时,将会被阻塞,等到线程2运行到屏障点…

JavaSE 初学进度条JProgressBar

预备知识 创建进度条类后将其直接加入JFrame看看效果 public class JProgressBarDemo2 {public static void main(String args[]) {JFrame jf new JFrame() ;JProgressBar jpb new JProgressBar() ;jpb.setPreferredSize(new Dimension(400,30));//设置好首选大小利于显示jpb…

Silverlight 数据绑定 (2):Source to Target

接着上一篇,在 Silverlight 中支持3种绑定:OneWay, TwoWay, OneTime. 默认是 OneWay. 其中 OneWay 表示仅仅从数据源绑定到目标(通常是 UI 对象),单向的; TwoWay 表示既可以从数据源绑定到目标,目标的更改也可以反馈给…