锁策略,cas和synchronized的优化过程

常见的锁策略

1.悲观锁 和 乐观锁 (概念)

悲观锁: 预期锁冲突的概率很高

乐观锁 : 预期锁冲突的概率很低

悲观锁 做的工作更多, 付出成本更多,更低效

悲观锁 做的工作更少, 付出成本更低,更高效

2. 读写锁 和 普通互斥锁

对于普通锁 只有两个操作

加锁 和 解锁

只要两个线程针对同一个对象加锁 , 就会产生互斥

对于读写锁来说, 分成了三个操作

加读锁 - > 如果代码只是进行了读操作, 就加读锁

加写锁 -> 如果代码中进行了修改操作 就得加写锁

解锁

好处就是针对读锁和读锁之间 (多线程之间读取同一个变量不会出现线程安全问题)是不存在互斥关系的

读锁和写锁, 写锁和写锁 之间才需要互斥.

读写锁 一般出现在 " 频繁读, 但不频繁写" 的场景中

3. 重量级锁 和 轻量级锁 (结果)

重量级锁: 做的事情更多 开销更大 加锁解锁 过程更慢,更低效

轻量级锁: 做的事情更少, 开销更少, 加锁解锁,过程更快更高效

可以认为 在一般情况下:

悲观锁 一般都是重量级锁

乐观锁 一般都是轻量级锁

在使用的锁中,如果锁基于内核的一些功能来实现的(比如调用了操做系统提供的 mutex 接口) , 此时一般认为这是重量级锁(操作系统会在内核中的很多的事情,比如让线程阻塞等待)

如果锁纯用户态实现, 此时一般认为是轻量级锁(用户态代码 可控, 也高效)

4. 挂起等待锁 和 自旋锁 (实现)

挂起等待锁 一般就是通过内核的一些机制来实现了,往往较重; [重量级锁的典型实现]

自旋锁, 一般都是通过用户态代码来实现的,往往较轻 [轻量级锁的典型实现]

  • 自旋锁 相当于 一旦锁被释放, 就能第一时间拿到锁,速度会更快,(忙等, 消耗cpu资源)
  • 挂起等待锁, 如果锁被释放,不能第一时间拿到所, 可能需要过很久才能拿到锁.但这个时间空出来了, 可以实现别的任务…

5.公平锁 和 非公平锁

公平锁: 多个线程等待一把锁的时候, 谁先来的 谁就能获得这个锁(遵守先来后到)

非公平锁: 多个线程等待一把锁的时候,不遵守先来后到(每个等待的线程获取到锁的概率都是均等的)

对于操做系统来说,本身线程之间的调度就是随机的(机会均等的) 操做系统的mutex这个锁 就是属于非公平锁.

要想实现公平锁 反而要实现更多的代价 (整个队列,来把这些参与竞争的线程给排一排先来后到);

6. 可重入锁 和 不可重入锁 (Reetrant)

可重入锁 : 一个线程 针对一把锁,上锁两次,不会出现死锁

不可重入锁 : 一个线程 针对一把锁,上锁两次,会出现死锁

CAS (compare and swap)

要做的事情就是 拿着寄存器/ 某个内存中的值, 和另外一个内存的值 进行比较 如果值相同的, 就把另一个寄存器/ 内存的值和当前的这个内存交换.

伪代码如下:

boolean CAS(address, expectValue, swapValue) {
    if (&address == expectedValue) {
        &address = swapValue;
        return true;
    }
    return false;
}
//此处的"交换" 可以理解成赋值 
//返回true 代表成功,返回false 代表失败.

此时CAS指的是 CPU提供了一个单独的CAS指令,通过这条指令 就完成了上述伪代码的描述的过程 ,

一条CPU指令也就是保证了原子性 中途不会被其他线程干扰(对于CPU来说 指令就是最小的不可分割的最小单位.)

CAS最大的意义就是让我们写多线程安全的代码, 提供了一个新的思路和方向(这就和锁不太一样)用硬件实现通过一条指令 , 直接让程序员使用 .

CAS的作用:

  1. 基于CAS能够实现"原子类"

    (Java标准库里提供了一些原子类, 针对所常用多一些的int , long , int arry … 进行封装 基于CAS的修改方式, 并且线程安全)

public class Demo19 {
    public static void main(String[] args) throws InterruptedException {
        //原子类 (整型)
        AtomicInteger num = new AtomicInteger(0);
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5_0000; i++) {
                //相当于 i ++;
                num.getAndIncrement();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5_0000; i++) {
                //相当于 i ++;
                num.getAndIncrement();
                // ++i
//                num.incrementAndGet();


            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(num.get());
    }
}

在这里插入图片描述

这个代码中不存在线程安全问题

基于CAS 实现的++ 操作

这里面就可以保证既能够线程安全, 由比 synchronized 更高效

synchronized 就会涉及到锁的竞争, 两个线程需要相互等待

CAS 不会涉及到线程的阻塞等待问题

伪代码实现

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

上述代码 while循环的CAS是核心

进行判定,当前内存的值 是不是和刚才寄存器里取到的值是不是一致,

如果判定成功,就把Value 设为oldValue + 1 返回 true ; 循环结束

如果判定不成功 就啥也不做 返回false 继续下次循环

// 站在单线程的角度 , 如果代码操作value 那么读到的value 值 是一样的 那么一定会true

// 但是如果站在多线程的角度 , 可能会存在 多个线程都在 load 和 cas 这个值的情况 ,就拿上述原子类代码来说.

在这里插入图片描述

对于线程操作来说如果t1 和 t 2都是先读取的数据
在这里插入图片描述

那么两者内部都是 0 此时 t1 先进行cas 判断 因为 内存和寄存器的值是相同的 因此 自增1 然后交换

在这里插入图片描述

此时就成为了这个样子. t1线程自增一次结束

此时 t2 进行 cas 判断 因为不相等 所以判定为 cas () != true 判定成功就 进入循环

在这里插入图片描述

循环内部的值是继续加载一次 内存中的值给寄存器
在这里插入图片描述

也就成为了这样 此时 寄存器内部的值和内存相等 因此 内存器中的值进行 + 1 后交换 循环结束
在这里插入图片描述

因此不管有多少个线程,自增多少次,内存中的值就会准确无误的自增多少次.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1lz0NLJY-1681630820986)(assets/image-20230313150532432.png)]

  1. CAS 能够实现" 自旋锁 "

请添加图片描述

CAS 的 ABA 问题

如果当前内存的值就是旧值, 那么就认为当前的内存没有发生改变(也就是 没有别的进行进行修改,) 于是就会继续进行执行交换的操作 但是这样的设定并不一定就准确 可能会出现一些极端情况

例如: 本来旧值是A,被其他线程改成了B ,又改回成了A

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
这样就导致B线程内这样的误操作成功了, 这就是典型的ABA问题.

解决思路:

引入一个"版本号" 只能变大, 不能变小, 修改变量的时候,比较的就不是变量本身了 , 而是比较版本号

(相当于就是引入一个变量 只能往一个方向进行 然后比较这个变量是否相等 在进行 操作 …)\

就相当于 两个线程进行 100 - 50 操作

但是这次只需要减少1次50 即可 本来余额会变成 50 但是此时 t3 这里 + 50 就会变成 100 此时 t2 判定成功 就会误操作 再减 50 如果引入版本号 ( 只能加 不能 减 那么 判断版本号就不会出现这样的问题.)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-arb9CjeM-1681630820988)(assets/image-20230313154430284.png)]

如果只拿变量本身进行判定 , 因为变量的值有加有减 就容易出现ABA问题

但是现在拿版本号来进行判定, 要求版本号只能增加(固定方向) , 这个时候就不会有ABA问题.


基于版本号的方式来进行多线程数据的控制,也是一种乐观锁的典型体现.

  1. 数据库里
  2. 版本管理工具(SVN) 通过版本号来进行多人开发协同.

synchronized 中的锁优化机制

  1. 锁膨胀/ 锁升级

    体现了synchronized 能够"自适应" 能力

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nihcSF8o-1681630820988)(assets/2-1681627487510-1.png)]

  2. 锁粗化

    锁的粗细就指的是"锁的粒度"(加锁代码涉及范围,加锁代码的范围越大,锁的粒度越粗 ,加锁代码范围越小,锁的粒度越细)

    也就是锁粒度越细 优点: 说明多个线程的并发性就更高,

    如果锁粒度比较粗 优点: 就说明加锁解锁的开销就更小,

    因此编译器就会有一个优化 自动判定 如果某个代码锁的粒度太细, 就会进行粗化 ( 扩大加锁代码的范围 )

    如果两次加锁之间的间隔较大,(中间隔的代码多) ,一般不会优化,

    如果两次加锁之间的间隔较小(中间隔的代码少) , 就很有可能出发这个优化.

  3. 锁消除

    有些代码,明明不用加锁 , 但是你还是加锁了,编译器就会发现加锁好像没有必要, 此时就直接把锁给去掉了.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值