多线程锁相关

日升时奋斗,日落时自省 

目录

1、常见的锁策略

1.1、悲观锁vs乐观锁

1.2、轻量级锁vs重量级锁

1.3、自旋锁vs挂起等待锁

1.4、互斥锁vs读写锁

1.5、公平锁vs非公平锁

1.6可重入锁vs不可重入锁

2、CAS

2.1、CAS解析

 2.2、CAS的应用场景

2.2.1、实现原子类

2.2.2、实现自旋锁

2.2.3、CAS相关ABA问题

  3、Synchronized原理

3.1、锁升级/锁膨胀

3.2、锁消除

3.3、锁粗化

1、常见的锁策略

谈到锁不分语言,因为多线程操作中,锁是一种很通用的方法保证线程安全,锁也不局限于Java,C++,Python,数据库,操作系统等,如果涉及到锁,基本都是可以使用下列策略锁的

1.1、悲观锁vs乐观锁

这里不是具体的锁,应该叫做“两类锁” (两种锁的类型)

乐观锁:预测锁竞争不是很激烈(所以做一些相对更少的工作)

如:数据改变的时候 一般不会发生冲突,所以提交更新数据后才会检测是否发生冲突,如果冲突交给用户决定如何做(也就是后知后觉)

悲观锁:预测锁竞争是很激烈的(工作可能会比较多)

如:悲观锁相反而已,你要操作数据,就先加锁,谁也拿不走别人修改不了,保证安全

总结:两类锁的背后工作是截然不同的,这里也不是绝对的,判定依据,主要就是看预测锁竞争激烈程度

1.2、轻量级锁vs重量级锁

轻量级锁:加锁解锁开销都比较小,效率更高(多数情况下,乐观锁,也是一个轻量级锁,但是不完全保证) 

重量级锁:加锁解锁开销都比较大,效率更低(多数情况下,悲观锁,也是一个重量级锁,但是不完全保证)

1.3、自旋锁vs挂起等待锁

自旋锁:是一种典型的轻量级锁

事例解释:一个庙里的主持,,有多个小徒弟,有一个小徒弟每天都问老主持,我能不能担任咱们庙里的主持(老和尚就对当前主持这个位置加锁),(这个小和尚就是自旋锁,老和尚辞去主持位置也就是解锁,自己就可以上位了)

优点:没有放弃CPU,不涉及线程阻塞和调度,一旦锁被释放,就能第一时间获取到锁

缺点:如果锁被其他线程持有的时间比较久,那么就会持续的消耗CPU资源(此时挂起等待就不会消耗CPU的资源)

挂起等待锁:是一种典型的重量级锁

事例解释:还是小和尚们和老和尚主持,但是老和尚虽然对主持加锁了,但是没有小和尚主动去争取主持位置,想等老和尚主持主动辞职,让这个小和尚担任主持(小和尚就是挂起等待),或许老和尚主持辞去让别的小和尚担任主持,他就只是等待自动轮到他

总述:以上三种锁任何一个需要锁的场景,其实都涉及到这样的一些类似的策略情况

1.4、互斥锁vs读写锁

互斥锁:就是咱们前面用过的像synchronized这样的锁,提供加锁 和 解锁 两个操作,如果一个线程加锁了,另一个线程也尝试加锁,就会阻塞等待

读写锁:提供了三种操作 针对读加锁、针对写加锁、解锁(适合频繁读,不频繁写的场景)

多线程并发读是不存在线程安全问题的,也不需要加锁控制

(1)读锁和读锁之间,没有互斥

(2)写锁和读锁之间,存在互斥

(3)写锁和写锁之间,存在互斥 

只有一组操作有读也有写,才会产生竞争(解释为多线程操作情况下)

注:只要涉及到“互斥”,就会产生线程的挂起等待,一旦线程挂起,再次被唤醒就不知道隔了多久了。

因此尽可能减少“互斥”的机会,就是提高效率的重要途径

在我们所做的开发中一般情况下,读操作比写操作更加高频

1.5、公平锁vs非公平锁

公平锁:就是所有的线程都在等待,但是等待时间肯定是有差异的,公平起见就让等的时间最长的那个来执行吧。(遵循先来后到的规则)

非公平锁:还是那线程来说,所有线程都在等,有的线程等的时间久,有的只等了一会,但是现在选择线程不会有任何条件,随机来选,选到谁来执行就谁来,对于等的时间长的线程来说就不公平了。(不遵循先来后到的规则)

操作系统和java synchronized原生都是“非公平锁”

操作系统这里的针对加锁的控制,本身就是依赖于线程调度顺序,这个调度顺序是随机的,不会考虑线程锁等了多久,如果不做任何额外的限制,锁就是非公平锁;如果要是实现公平锁,就需要依赖额外的数据结构,来记录线程们的先后顺序

该两种锁没有好坏之分,看程序的不同情况

1.6可重入锁vs不可重入锁

可重入锁:一个线程对一把锁,连续加锁多次都不会出现死锁(这里多次加的是同一把锁,有点像递归一样锁里套锁,可重入锁也可以叫做“递归锁”)

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

针对java:Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的

针对java提供的锁synchronized判别

(1)synchronized 即是一个悲观锁,也是乐观锁

注:synchronized默认是乐观锁,竞争变激烈后就会变成悲观锁

(2)synchronized即是轻量级锁,也是一个重量级锁

注:synchronized默认是轻量级锁,如果当前锁竞争比较激烈,就会转换成重量级锁

(3)synchronized这里轻量级锁,是基于自旋锁的方式实现的。

synchronized这里的重量级锁,是基于挂起等待锁的方式实现的

(4)synchronized不是读写锁

(5)synchronized不是公平锁

(6)synchronized是可重入锁

2、CAS

2.1、CAS解析

CAS:全称Compare and swap ,字面意思:比较并且交换

翻译有点直白,不过确实是这样的

解释CAS:原数据为 V   预期值A    需要修改值B

(1)比较A 与 V 是否相等 (比较)

(2)如果比较相等 ,将B 写入 V(交换),如果不等的话就是直接到(3)

(3)返回操作成功 

这里文字解释还是有点模糊不清

来写我们这里拿图来解释  寄存器 与 内存的关系

 此处最特别的地方,上述这个CAS的过程,并非是通过一段代码实现的,而是通过一条CPU指令实现的,也就是说该操作是一个“原子”(CAS是原子)

原子就可以在一定程度上回避线程安全问题,咱们解决线程安全问题除了加锁之外又有了一个新的路子

总述:CAS可以理解为CPU给我们提供了一个特殊指令,通过这个指令,就可以一定程度的解决线程安全问题

(这里写一个伪代码来解释CAS)

伪代码:就是假的,不能运行编译的,用来帮助我们来断思路的

 2.2、CAS的应用场景

2.2.1、实现原子类

java标准库里提供的类AtomicInteger

AtomicInteger count=new AtomicInteger(0);

不要感觉陌生,这里就和创建一个包装类一样,直接赋值,后面的传参就是赋值

说了CAS是一个“原子” 那如何证明呢,之前的博客中有提及到锁synchronized可以解决多线程安全问题,这里也用代码尝试一下

public class Test {
    public static void main(String[] args) throws InterruptedException {
        //这些原子类 就是基于 CAS实现了 自增 自减等操作, 此时进行这类操作,也是线程安全的
        AtomicInteger count=new AtomicInteger(0);
        //使用原子类 来解决线程安全问题
        Thread t1=new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                //java中不支持运算重载 ,所以只能使用普通的方法表示自增自减
                count.getAndIncrement();   //解释 count++
               /* count.incrementAndGet();   //解释 ++count
                count.getAndDecrement();  //解释 count--
                count.decrementAndGet();   //解释 --count*/
            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                count.getAndIncrement();
            }
        });
        t1.start();
        t2.start();
        t1.join();  //等待
        t2.join();
        System.out.println(count);  //直接解决线程安全问题 不会影响多线程加的结果
    }
}

我们知道如果两个线程都执行++操作,在线程不安全的情况下不会得到一个准确的数值,当前可以。

之所以有这个代码出现,不仅仅是证明CAS可以保证线程安全,还有就是java中不支持运算重载,所以中用普通的方法来表示加加或者减减(前或者后)

伪代码解释一下CAS的精妙之处

 上图的(3)没有截上,这里补充一下

 到这里是不是友友想问既然一条CPU指令就实现了,精妙还安全为啥还要锁来维护线程安全

CAS属于“特殊方法” ->只能特定场景使用,没有那么通用(共享访问一个量可以,例如解决++在线程的安全问题)。

synchronized 属于“通用方法” ->各种场景,都能使用。

2.2.2、实现自旋锁

伪代码解释:

 2.2.3、CAS相关ABA问题

CAS在运行中的核心,检查value 和 oldValue 是否一致,如果一致就任务value没有被修改过,进行交换操作是没有问题的。(这个真的,下面解释的问题和这个有点点冲突)

要知道线程操作是很快的

value值 一致 有两种可能

value=A; 

一种是真没有修改过  value =A;

 第二种就是改了但是又改回来了。 value=A ->  value=B -> value=A

ABA问题 家人看见的你出远门的时候好好的 ,中途可能生病了,但是回家的时候好了但是在你不说的情况下,家人认为你一直是好的。(基本就是这样一个情况)

但是ABA仍然算是个缺陷,极端情况下,也可能会造成影响(实际中概率很低)

以取钱为例,去取款机上取钱这种

许某账户余额 有1000块,准备取500元

当按下取款机一瞬间,机器卡了许某就想着多按几下,能不能快点出来,这时候就会产bug重复扣款,考虑使用CAS方式扣款(图解)

 当然能看出来,上面这种情况概率还是很低的。

主要是要满足两个条件(1)恰好许某按了很多下,产生扣款操作(2)刚刚好非常极限的时间有人转账一样的金额

概率很低,但是不代表不会有,还是要考虑的,因为此时只是一种现实的情况,但是此情况一旦出现,不好解决吧,提前准备是最好的。

解决当前问题的就是 加一个版本号(就是标记),每次修改时,版本号都会更新+1,然后CAS不是以金额为基准,以版本号为基准,版本号就是无限加(版本号没有改变就代表什么都没有发生)

  3、Synchronized原理

使用原理前面提及过 : 保证线程执行安全,如果两个线程加锁同一个对象,就会产生阻塞。

synchronized不仅仅是我们看到的,内部还有很多操作。

3.1、锁升级/锁膨胀

锁要经历的4个过程 无锁、偏向锁、轻量级锁、重量级锁

(1)无锁

(2)偏向锁

加锁的时候,首先会进入到偏向锁状态,偏向锁,并不是真正的加锁,而只是占个位置,有需要了在真加锁,没有需要就算了。(其实看的出还是很优质的,毕竟加锁是要有开销的)

举个例子解释偏向锁:该两个小孩买了一个夜光球,小孩A先看见就在旁边看(尚未加锁),过了一会小孩B来了,孩子都喜欢独一份的,小孩A立马将夜光球抱到怀里(加锁),这就是偏向锁(有偏向性,只是没有遇到竞争者,一旦遇到立刻加锁)

偏向锁又是怎么做到:为了降低冲突,减少开销。

这里synchronized等待也不是凭空等待,有偏向性,做个标记 (这个过程很轻量)

<1> 如果整个使用锁的过程中没有出现锁竞争synchronized执行完后,取消偏向锁即可(清楚标记)

<2>   如果另一个线程也尝试加锁,在它加锁之前,迅速就标记的偏向锁升级为真正的加锁状态

(3)轻量级锁

synchronized发生锁竞争的时候,会从偏向锁,升级到轻量锁,此时,synchronized通过自选的方式来加锁,也是自旋锁(与刚才CAS伪代码一样思路)

(4)重量级锁

自旋锁不能一直自旋不是嘛,CPU是资源开销的,自旋次数多了就不划算了,自旋到一定程度会自己停的,再次升级为重量级锁(挂起等待锁)

重量级锁就不像前面的锁一样了,很保险,是基于操作系统的原生API来进行加锁,linux原生提供了mutex一组API,操作系统内核提供加锁功能,这个锁会影响到线程的调度,如果此时线程进行到了重量级锁,发生锁竞争就只能等了,该线程也就被放阻塞队列中,直到锁被释放,线程才有机会被调度,并且有机会获取锁,(注:是有机会,不是直接就获取)线程一旦被切出CPU就会变的低效

关于锁升级就是以上内容,有升级是不是就降级,当前还没有,到了重量级锁就在这个位置了,JVM只有锁升级;要想降级也只有等该线程执行完,锁释放了,从新有一个对象获取锁,开始重复刚刚加锁的过程(偏向锁,轻量级锁,重量级锁)

3.2、锁消除

编译器智能判定,看当前的代码是否是真的要加锁

判定当前场景不需要加锁,程序员也加了,就自动把锁给卸掉

在学习StringBuffer的时候说个这个类是标准库提供的,具有线程安全的涉及线程安全会在标准库里面已经加了synchronized,如果我们针对StringBuffer就锁的话,就会被编译器直接卸掉,判定安全不需要加锁

3.3、锁粗化

锁的粒度:synchronized包含的代码越多,粒度就越粗,包含代码越少,粒度就越细,很符合生活中的场景,留个印象就行

通常认为锁的粒度细一点比较好,为什么这么理解?我们知道多线程随机调度并发执行的,但是加锁部分的代码,是不能进行并发执行的,会降低多线程的执行速度,锁的粒度越细,synchronized内代码越少,有更多的代码能够进行并发执行。

也不是说粒度越细就一定越好,如果加锁比较频繁的情况下,大部分还是都加锁,这时候就不划算了,加锁也是有开销的,此时锁与锁之间的间隙就很小,不如一次给整个阶段都加上锁,减少加锁的开销,直接只加一次。(用图举个例子)

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值