常见锁策略_CAS(Compare And Swap)_synchronized优化

目录

1.常见锁策略

1.1乐观锁vs悲观锁

1.2轻量级锁vs重量级锁

1.3自旋锁vs挂起等待锁

自旋锁

挂起等待锁

1.4互斥锁vs读写锁

1.5公平锁vs非公平锁

公平锁

非公平锁

1.6可重入锁vs不可重入锁

1.7使用锁策略描述synchronized

2.CAS(Compare And Swap)

2.1CAS应用场景

实现原子类

实现自旋锁

2.2CAS的ABA问题

3.synchronized原理

3.1锁升级/锁膨胀

无锁

偏向锁

轻量级锁

重量级锁

3.2锁消除

3.3锁粗化


1.常见锁策略

锁策略不仅仅局限于java,任何与"锁"相关的话题(操作系统,数据库...),都会涉及到锁策略,这些策略是给锁的实现者用来参考的

1.1乐观锁vs悲观锁

这个不是两把具体的锁.而是两类锁,是在锁冲突的概率上进行区分的

乐观锁指的是预测锁竞争不是很激烈(做的工作相对少一些),悲观锁预测锁竞争会很激烈(这里做的工作会多一些).

1.2轻量级锁vs重量级锁

是从锁开销的角度区分的

轻量级锁加锁解锁开销比较小,效率更高.重量级锁加锁解锁开销比较大,效率更低.

多数情况下,乐观锁也是一个轻量级锁,悲观锁也是一个重量级锁

1.3自旋锁vs挂起等待锁

自旋锁是典型的轻量级锁

挂起等待锁是典型的重量级锁]

自旋锁

自旋锁伪代码:

while (抢锁(lock) ==失败) {}

自旋锁如果获取锁失败,立即再尝试获取锁,无限循环..一旦锁被其他线程释放,就能第一时间获取到锁

自旋锁的优点:
没有放弃cpu,不涉及线程阻塞和调度,一旦锁被释放,就饿能第一时间获取到锁
缺点:
如果锁被其它线程持有的时间较长,那么就会持续的消耗cpu资源(挂起等待是不需要消耗资源的)

挂起等待锁

挂起等待锁:如果一个锁被另外的线程持有,挂起等待锁会一直等待,不会主动去获取锁

这种做法不会消耗大量cpu资源,就可以做别的工作了.

1.4互斥锁vs读写锁

互斥锁

提供加锁和解锁操作,就像我们使用过的synchronized这样的锁.如果一个线程加锁了,另一个线程也尝试获取锁,就会阻塞等待

读写锁

提供了三种操作

1.针对读加锁
2.针对写加锁
多线程针对同一个变量并发读是没有线程安全问题的.也不需要加锁.
读锁和读锁之间没有互斥
写锁和写锁之间是互斥的
写锁和读锁之间存在互斥
假设一组线程并发读同一个变量,这时线程之间是没有锁竞争的,也没有线程安全问题!假设一组线程有读又有写,才会产生锁竞争..实际开发中,读操作非常高频

3.解锁

1.5公平锁vs非公平锁

公平锁

把公平锁定义为"先来后到"

B比C先来获取锁然后阻塞等待的,当A释放锁之后,B就能先于C获取到锁

非公平锁

不遵守"先来后到"

不管BC谁先来的,当A释放锁之后,BC都有可能获取到锁,synchronized就是非公平锁!

操作系统内部的线程调度就是随机的,如果不做额外的限制,锁就是非公平锁,如果要实现公平锁,就需要额外的数据结构来保存先后顺序
公平锁和非公平锁没有优劣,要看适用的场景

1.6可重入锁vs不可重入锁

不可重入锁:一个线程针对同一把锁,连续加锁两次,出现死锁

可重入锁:一个线程针对同一把锁,连续加锁多次都不会出现死锁

1.7使用锁策略描述synchronized

上述种锁策略,就像是锁的形容词.任何一个锁,都能用上述锁策略来描述,形容,我们看synchronized是怎样的

1.synchronized既是一个悲观锁,又是个乐观锁
synchronized默认是乐观锁,但是如果发现锁竞争比较激烈,就会变成悲观锁!!
2.synchronized既是轻量级锁,又是一个重量级锁
synchronized默认是轻量级锁,当锁冲突剧烈后,就变成重量级锁!
3.synchronized这里的轻量级锁是基于自旋锁的方式实现的
synchronized这里的重量级锁是基于挂起等待锁的方式实现的
4.synchronized不是读写锁
5.synchronized是非公平锁
6.synchronized是可重入锁

2.CAS(Compare And Swap)

一个CAS涉及到以下操作:

我们假设内存中的原数据V,旧的预期值A,需要修改的新值B
1.比较A与V是否相等
2.如果相等,将B写入V
3.返回操作是否成功

上述交换过程中,大多数不关心B后续的情况了,更关心的是V这个变量的情况.近似可以理解成赋值了

如果AV不同,则没有其他操作

我们看一下CAS的伪代码:

boolean CAS(V,A,B){
    if(A == V){
        V = B;
        return true;
    }
    return false;
}
但是CAS的过程并非是通过代码实现的!!而是通过一条CPU指令完成的!CAS操作是原子的,因此它是线程安全的.那么解决线程安全问题除了加锁,就又有个新的思路了.
CAS是CPU提供的一个特殊指令,通过这个指令,就可以一定程度的处理线程安全问题!

2.1CAS应用场景

实现原子类

Java标准库中提供的有原子类,之前我们学习线程安全时,写过一个问题,两个线程对同一个变量进行自增操作后,这个变量没有达到预期的结果,我们是通过加锁解决线程安全问题的.这里我们直接使用原子类,就不会出现线程安全问题

        AtomicInteger count = new AtomicInteger();

AtomicInteger是原子类,基于CAS实现了自增,自减等操作,此时进行自增等操作不需要加锁,也线程安全的

public class Test {
    public static void main(String[] args) throws InterruptedException {
        //使用原子类解决线程安全问题
        AtomicInteger count = new AtomicInteger();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count.getAndIncrement();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count.getAndIncrement();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

结果:

我们看一下伪代码实现的原子类

class AtomicInteger {
    private int value;
    public int getAndIncrement() {
        int oldValue = value;
        while ( CAS(value, oldValue, oldValue+1) != true) {
            oldValue = value;
       }
        return oldValue;
   }
}

这里的oldValue可以理解为是寄存器中的值,相当于先把内存中的值读到寄存器里

正常情况下,oldValue应该是和value的值是相同的,然后这里发生CAS,把old Value+1写到value中

但是也可能会有:执行完读取value到寄存器中后,线程切换了,另外一个线程也修改了内存中value的值,此时这个线程如果继续执行进行CAS判定,就会认为value和oldValue不相等了

value和oldValue不相等,然后重新读取oldValue

我们画图解释一下这个过程:

按照这个时间执行两个线程

t1,t2都进行加载

然后t2开始CAS

比较oldValue和value的值,发现相等,oldValue+1赋给value

t2线程执行完毕,切换回t1线程,t1线程开始CAS,发现oldValue和value的值不相等,返回false,不进行任何交换...然后进入循环,循环内部重新读取value的值到oldValue 中,此时再次比较,发现相等了,进行CAS操作,并返回true,循环结束

原子类这里的实现,每次修改之前都会再确认一下这个值是否符合要求

CAS是属于特殊方法,特定场景能使用,加锁操作是通用方式,各种场景都能使用,打击面很广!

实现自旋锁

我们看一下自旋锁的伪代码

public class SpinLock {
    private Thread owner = null;
    public void lock(){
        // 通过 CAS 看当前锁是否被某个线程持有.
        // 如果这个锁已经被别的线程持有, 那么就自旋等待.
        // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
        while(!CAS(this.owner, null, Thread.currentThread())){
       }
   }
    public void unlock (){
        this.owner = null;
   }
}
Thread owner是记录当前锁是谁加的
this.owner是检测当前的owner是否是null,如果是null的,就进行交换,也就是把当前的线程的引用赋值给owner.如果赋值成功,此时循环结束,加锁完成!
如果当前锁已经被别的线程占用了,那么owner就不是null的,那么CAS就不会产生赋值,同时返回false,循环继续执行,进行下次判断,这就完成了自旋过程!!

在Java中,并不是直接提供了一个方法CAS.此处伪代码是便于理解

2.2CAS的ABA问题

CAS在运行中的核心是检查oldValue和value是否一致,如果一致,就认为value中途没有被修改过.所以进行下一步操作是没问题的

但是还有可能是中途被修改过,然后又还原回来了.把value值设为A,CAS判定value为A,此时value确实可能始终是A,也有可能本来是A,然后被修改为B,最后又还原成了A!这就是ABA问题

ABA情况大部分是不会对代码/逻辑产生太大影响的,当然也有极端情况,我们看下面这个情景:

如果ATM取钱使用的是CAS来扣款,假设A的账户余额1000,要取500.当按下取款按键时,机器卡顿了,A没忍住多按了几下,此时就会产生bug,可能出现重复扣款的现象

正常情况下,机器卡顿多按两次,t1线程的CAS发现余额是1000,然后就交换成500.扣款成功,然后t2线程加载时余额也是1000,CAS发现余额不是1000,就不扣款.正确的逻辑

下面这种情况,当t2执行CAS的时候,正好有人给A转入了500.那么余额就变成1000了, 执行CAS操作,又扣了500,出现了bug!!

当然这种情况出现的概率是很低的,但是还是可能出现,针对这种情况,采取的解决方案就是加入一个版本号,初始版本号是1,每次修改版本号都加1,然后进行CAS的时候,不是以金额多少为准了,是以版本号为准,此时如果版本号没变,就一定没有发生改变

3.synchronized原理

两个线程针对同一个变量加锁,就会阻塞等待.除了上述基本原理,synchronized还有一些内部的优化机制,存在的目的就是为了让锁更高效,好用.

3.1锁升级/锁膨胀

当执行到加锁的代码块儿时,加锁过程就可能经历下面几个升级阶段

无锁

无锁状态,还没开始加锁

偏向锁

进行加锁的时候,首先会进入偏向锁状态

偏向锁,并不是真正的加锁,而只是先占个位置,如果有需要就加锁,没需要就不加锁了

相当于"懒汉模式"提到的懒加载一样,非必要,不加锁

synchronized加锁的时候,并不是真正的加锁,而是先进入偏向锁状态,就相当于做一个标记,如果一直没有别的线程来获取这个锁,那么就不会升级,仅仅只做个标记,因为这个变量本来就只有这个线程要使用,过程也没有出现锁竞争,执行完synchronized{}代码块后,再取消掉标记(偏向锁)即可
但是如果出现了锁竞争,再另一个线程加锁之前,偏向锁会迅速升级为真正的加锁状态!!另一个线程阻塞等待...

轻量级锁

当synchronized发生锁竞争的时候,就会从偏向锁升级为轻量级锁(自旋锁)

此时,synchronized是通过自旋的方式来进行加锁的(就和刚刚伪代码一样的逻辑)

但是,如果很快就释放锁了,自旋是值得的,可以立即获取被释放的锁,反之,迟迟不被释放,那么久迟迟拿不到锁,自旋就不划算了..这时候就需要再次升级了!

重量级锁

一直自旋但是又拿不到锁,synchronized也不会无止境的自旋,此时升级为重量级锁(挂起等待锁)

重量级锁(挂起等待锁)则是基于操作系统原生的API来进行加锁了

linux原生提供了mutex一组API,操作系统北河提供的加锁功能,这个锁是会影响到线程的调度的

此时,如果线程进行了重量级锁的加锁,并且发生了锁竞争,此时线程就会被放入阻塞队列中,暂时不参加CPU的调度了,直到锁被释放了,这个线程才有机会被调度到并有机会获取到锁

锁升级了就不能降级了

3.2锁消除

这是编译器的智能判定,看当前代码是否真的需要加锁,如果这个场景不用加锁,就会自动把加的锁销毁

就像StringBuffer中的关键的方法都是带有synchronized修饰的,就不需要程序员再加锁,加了编译器也会自动销毁!

3.3锁粗化

锁的粒度:synchronized包含的代码越多,粒度就越粗.包含的代码越少,粒度就越细.

通常情况下,粒度细一点比较好,加锁的代码是不能并发执行的,锁的粒度越细,能并发的代码就越多,粒度越粗,能并发的越少.

有些情况,粒度粗反而更好

这种情况下,两次加锁解锁之间的间隙非常小,反反复复加锁解锁效率低开销大,可以直接加一个大锁,将间隙也包括,效率反而高些,毕竟间隙很小,这块儿代码能不能并发执行影响不大!

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

YoLo♪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值