当前位置: 首页 > article >正文

【多线程】多线程(10):常见锁策略,锁原理,CAS

【常见的锁策略】

【乐观锁vs悲观锁】

乐观锁:加锁时假设出现锁冲突的概率不大——接下来围绕加锁要做的工作,就会更少

悲观锁:加锁时假设出现锁冲突的概率很大——接下来围绕加锁要做的工作,就会更多

【轻量级锁vs重量级锁】

轻量级锁:加锁的开销比较小,要做的工作相对更少

重量级锁:加锁的开销比较大,要做更多的工作

//通常悲观锁做的重,乐观锁做的轻

【自选锁vs挂起等待锁】

自选锁:悲观锁/重量级锁的一种典型实现

挂起等待锁:乐观锁/轻量级锁的一种典型实现

【公平锁vs非公平锁】

公平锁:加锁优先级为来后到

非公平锁:加锁优先级为概率均等

【可重入锁vs不可重入锁】

一个线程针对一把锁连续加锁2次,可能出现死锁,但若把锁设定为「可重入」就可以避免死锁

【读写锁】

若多个线程同时读一个变量,无线程安全问题

但一个线程读/一个线程写/两个线程都写,则会产生线程安全问题

因此产生「读写锁」,把读操作和写操作区分开来进行加锁

1.读加锁 2.写加锁

读写锁提供了2种加锁的API

加读锁:

加写锁:

(解锁的API是一样的)

如果两个线程都是按照读方式加锁,此时不会产生锁冲突

如果两个线程都是按照写方式加锁,此时会产生锁冲突

如果一个线程读锁,一个线程写锁,会产生锁冲突

即:读操作加锁时,读锁和读锁之间不发生互斥;写操作加锁时,读锁和写锁之间,写锁和写锁之间会互斥

该锁适合于频繁读,不频繁写的场景

【synchronized的锁原理】

【synchronized锁策略】

对于乐观/悲观,它是自适应的

对于重量/轻量,它是自适应的

对于自旋/挂起等待,它是自适应的

它是非公平锁

它是可重入锁

【synchronized加锁过程】

刚开始使用synchronized加锁,首先锁会处于“偏向锁”状态,遇到线程之间的锁竞争,升级到“轻量级锁”

进一步统计竞争出现的频次,达到一定的激烈程度之后,升级到“重量级锁”

synchronized加锁时,会经历无锁——偏向锁——轻量级锁——重量级锁的过程

【偏向锁和锁升级】

“偏向锁”不是真正的加锁(真的加锁开销比较大),偏向锁只是做个标记(标记的过程轻量而高效)

当出现锁竞争时,偏向锁会立即升级为轻量级锁(进行实质性加锁)

锁竞争激烈时,轻量级锁会立即升级为重量级锁

锁升级的过程中「不可逆」

【总结】

上述锁升级的过程,是为了能让synchronized这个锁很好地适应不同的场景,可以降低程序员的使用负担

【锁消除(编译器的优化策略)】

编译器会对写的synchronized代码作出判定,判定这个地方是否确实需要加锁,若没必要,则会自动把synchronized干掉

【锁粗化(编译器的优化策略)】

每次加锁可能会涉及到阻塞等待

锁粗化,就是把多个「细粒度」的锁,合并成一个「粗粒度」的锁,最大程度上减少阻塞等待带来的效率降低

【锁的粒度】

加锁的代码块中,代码越多,粒度越粗;代码越少,粒度越细

//算粒度看的是「实际执行的代码数量」,因此某些时候虽然代码块中只有几行代码,但调用的方法中代码很多,因此粒度粗

【CAS】

全称“compare and swap”,比较和交换

比较内存和cpu寄存器中的内容,如果发现相同,就进行交换(交换的是内存和另一个寄存器的内容)

【具体解释】

1.“一个内存的数据”和“两个寄存器中的数据”进行操作(寄存器1和寄存器2)

2.比较内存和寄存器1中的值,是否相等,若不相等,则无事发生,若相等,就交换内存和寄存器2的值

//此处一般只关心内存交换后的内容

//虽然叫做“交换”,实际上达成的效果可以看作是“赋值”

CAS的关键不在于其内部进行了什么操作,而是在于通过「一个cpu指令」完成了上述的一系列操作

【用途:基于CAS实现“原子类”】

int/long在进行++或--时,都不是原子的

基于CAS实现的原子类,对int/long等这些类型进行了封装,从而可以原子的完成++或--等操作

java标准库中提供了原子类的实现

private static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() ->{
            for(int i = 0;i < 50000;i++){
                   count.getAndIncrement(); //count++;
 //                count.incrementAndGet(); //++count;
 //                count.getAndDecrement(); //count--;
 //                count.decrementAndGet(); //--count;
 //                count.getAndAdd(x);       //count+=10;
            }
        });

        Thread t2 = new Thread(() ->{
            for(int i = 0;i < 50000;i++){
                count.getAndIncrement();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }

【如何通过CAS实现原子类?】

可看伪代码

value是内存数据,oldvalue是寄存器中的数据

1.把内存中的数据读取到寄存器中

2.while循环,在CAS中执行比较和交换的操作,比较value和oldvalue是否相同

若相同,意味着没有其他代码穿插到这两个代码之间执行,此时可以安全地修改变量中的内容:将寄存器2的值(oldvalue+1)交换(赋值)到value中

若不相同,意味着有其他代码穿插到这两个代码之间执行,此时就需要重新加载内存中的值到寄存器中(执行oldvalue=value)


http://www.kler.cn/news/341089.html

相关文章:

  • 欧姆龙(Omron)协议解析
  • uniapp 设置 tabbar 的 midButton 按钮
  • 软考新教程10月出版?11月软考会用到新内容吗?
  • excel表格转换为在线成绩查询怎么制作?
  • 2024年区块链钱包现状与未来趋势分析
  • 微信小程序处理交易投诉管理,支持多小程序,一键授权模式
  • LangChain入门
  • 串接模式对网络性能的影响
  • [实时计算flink]双流JOIN语句
  • [每日一练]利用子查询配合union all进行全连接后丢失数据的查询
  • Redis:分布式 - 主从复制
  • 通过阿里云Milvus和LangChain快速构建LLM问答系统
  • 阿里云NAS之间迁移实践
  • Python酷库之旅-第三方库Pandas(141)
  • 自动化测试中如何高效进行元素定位!
  • springboot接口如何支持400并发量
  • postman变量,断言,参数化
  • C++ day05(模版与容器)
  • MySQL高阶2010-职员招聘人数2
  • 前端Javascript常见算法题(一)【待学】