首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

多线程常见锁策略+CAS

一、乐观锁 & 悲观锁

1.1 乐观锁的定义

乐观锁,顾名思义,他比较乐观,他认为一般情况下不会出现冲突,所以只会在更新数据的时候才会对冲突进行检测。如果没有发生冲突直接进行修改,如果发生了冲突则不进行任何修改,然后把结果返回给用户,让用户自行处理。

1.1.1乐观锁的实现-CAS

乐观锁的实现并不是给数据加锁 ,而是通过CAS(Compare And Swap)比较并替换,来实现乐观锁的效果。

CAS比较并替换的流程是这样子的:CAS中包含了三个操作,单位:V(内存值)、A(预期的旧址)、B(新值),比较V值和A值是否相等,,如果相等的话则将V的值更换成B,否则就提示用户修改失败,从而实现了CAS机制。

这只是定义的流程,但是在实际执行过程中,并不会当V值和A值不相等时,就立即把结果返回给用户,而是将A(预期的旧值)改为内存中最新的值,然后再进行比较,直到V值也A值相等,修改内存中的值为B结束。

可能你还是觉得有些晦涩,那我们举个栗子:

看完这个图相信你一定能理解了CAS的执行流程了。

1.1.2 CAS的应用

CAS的底层实现是靠Unsafe类实现的,Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(Native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定的内存数据。Unsafe类存在包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中的CAS操作的执行依赖于Unsafe类的方法。

注意Unsafe类的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应的任务。因此不推荐使用Unsafe类,如果用不好会对底层资源造成影响。

为什么Atomic修饰的包装类,能够保证原子性,依靠的就是底层的unsafe类,我们来看看的源码:

在方法中还调用了unsafe的方法,因此这也就是为什么它能够保证原子性的原因。

因此我们可以利用Atomic+包装类实现线程安全的问题。

1.1.3 CAS存在的问题

循环时间长,开销大

只能保证一个共享变量的原子性操作(可以通过循环CAS的方式实现)

存在ABA问题

1.1.4 ABA问题

什么时ABA问题呢?

比如说两个线程t1和t2,t1的执行时间为10s,t2的执行时间为2s,刚开始都从主内存中获取到A值,t2先开始执行,他执行的比较快,于是他将A的值先改为B,再改为A,这时t1执行,判断内存中的值为A,与自己预期的值一样,以为这个值没有修改过,于是将内存中的值修改为B,但是实际上中间可能已经经历了许多:A->B->A。

所以ABA问题就是,在我们进行CAS中的比较时,预期的值与内存中的值一样,并不能说明这个值没有被改过,而是可能已经被修改了,但是又被改回了预期的值。

这个例子演示了ABA问题,A有100元,A向B转钱,第一次转了50元,但是点完转账按钮没有反应,于是又点击了一次。第一次转账成功后A还剩50元,而这时C给A转了50元,A的余额变为100元,第二次的CAS判断(100,100,50),A的余额与预期的值一样,于是将A的余额修改为50元。

1.1.5 ABA问题的解决方案

由于CAS是只管头和尾是否相等,若相等,就认为这个过程没问题,因此我们就引出了,时间戳原子引用,在这里应用于版本号的更新。也就是我们新增了一种机制,在每次更新的时候,需要比较当前值和期望值以及当前版本号和期望版本号,若值或版本号有一个不相同,这个过程都是有问题的。

我们来看上面的例子怎么用解决呢?

解决了ABA问题,在每次更新值之前,比较值和版本号。

1.2 悲观锁

什么是悲观锁?

悲观锁就是比较悲观,总是假设最坏的情况,每次去拿数据的时候都会认为别人会修改,所以在每次拿数据的时候都会上锁,这样别人想拿数据就会阻塞直到它拿到锁。

比如我们之前提到的synchronized和Lock都是悲观锁。

二、公平锁和非公平锁

公平锁:按照线程来的先后顺序获取锁,当一个线程释放锁之后,那么就唤醒阻塞队列中第一个线程获取锁。

非公平锁:不是按照线程来的先后顺序唤醒锁,而是当有一个线程释放锁之后,唤醒阻塞队列中的所有线程,随机获取锁。

之前在讲synchronized和Lock这两个锁解决线程安全问题线程安全问题的解决的时候,我们提过:

synchronized的锁只能是非公平锁;

Lock的锁默认情况下是非公平锁,而挡在构造 函数中传入参数时,则是公平锁;

公平锁:

非公平锁:

由于公平锁只能按照线程来的线程顺序获取锁,因此性能较低,推荐使用非公平锁。

三、读写锁

3.1 读写锁

读写锁顾名思义是一把锁分为两部分:读锁和写锁。

读写锁的规则是:允许多个线程获取读锁,而写锁是互斥锁,不允许多个线程同时获得,并且读操作和写操作也是 互斥的,总的来说就是读读不互斥,读写互斥,写写互斥。

为什么要这样设置呢?

让整个读写的操作到设置为互斥不是更方便吗?

其实只要涉及到“互斥”,就会产生线程挂起等待,一旦挂起等待,,再次被唤醒就不知道什么时候了,因此尽可能的减少“互斥"的机会,就是提高效率的重要途径。

Java标准库提供了类实现了读写锁。

类表示一个读锁,提供了lock和unlock进行加锁和解锁。

类表示一个写锁,提供了lock和unlock进行加锁和解锁。

下面我们来看下读写锁的使用演示~

根据运行结果我们看到,读锁操作是一起执行的,而写锁操作是互斥执行的。

3.2 独占锁

独占锁就是指任何时候只能有一个线程能执行资源操作,是互斥的。

比如写锁,就是一个独占锁,任何时候只能有一个线程执行写操作,synchronized、Lock都是独占锁。

3.3 共享锁

共享锁是指可以同时被多个线程获取,但是只能被一个线程修改。读写锁就是一个典型的共享锁,它允许多个线程进行读操作 ,但是只允许一个线程进行写操作。

四、可重入锁 & 自旋锁

4.1 可重入锁

可重入锁指的是该线程获取了该锁之后,可以无限次的进入该锁。

因为在对象头存储了拥有当前锁的id,进入锁之前验证对象头的id是否与当前线程id一致,若一致就可进入,因此实现可重入锁 。

4.2 自旋锁

自旋锁是指尝试获取锁的线程不会立即阻塞,而是采取循环的方式尝试获取锁,这样的好处是减少线程上下文切换的消耗。线程上下文切换就是从用户态—>内核态。

synchronized就是一种自适应自旋锁(自旋的次数不固定),hotSpot虚拟机的自旋机制是这一次的自旋次数由上一次自旋获取锁的次数来决定,如果上次自旋了很多次才获取到锁,那么这次自旋的次数就会降低,因为虚拟机认为这一次大概率还是要自旋很多次才能获取到锁,比较浪费系统资源。

这就是常见的一些锁策略,如果觉得对你有用的话就点个赞吧~~

热爱技术才能学好技术

每天进步一点点

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20230305A05I0200?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券