【十一】一文带你迅速掌握锁策略

文章详细介绍了各种锁的概念和特性,包括乐观锁和悲观锁的策略差异,轻量级锁与重量级锁在同步机制中的角色,自旋锁与挂起等待锁的实现方式,以及互斥锁和读写锁在多线程场景下的应用。此外,讨论了可重入锁避免死锁的情况,并指出synchronized作为非公平锁在并发控制中的作用。
摘要由CSDN通过智能技术生成

1. 乐观锁VS悲观锁

锁得实现者,预测接下来锁冲突的概率(同个线程针对同一个对象加锁,产生阻塞等待)大不大,根据这个冲突的概率,来决定接下来该怎么做~

  • 乐观锁:看名字就是比较乐观,预测冲突概率不大,所以做的工作会比较少,效率会高一点(并不绝对)
  • 悲观锁:就是预测冲突概率比较大,所做的工作比较多,效率会低一点(并不绝对)

乐观锁:假设数据一般情况下不会发生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则返回用户错误的信息,让用户决定如何去做
悲观锁:总是假设最坏情况,每次对数据都会进行修改操作,所以在每次拿数据的时候都会上锁,这样其他线程想操作这个数据就会阻塞等待

2. 轻量级锁VS重量级锁

在这里插入图片描述

synchronized 并不仅仅是对 mutex 进行封装,内部还做了很多其他的工作
synchronized 开始是一个轻量级锁,如果锁冲突比较严重,就会 变成重量级锁

  • 轻量级锁:加锁解锁,过程更快更高效,加锁机制尽可能不使用mutex,而是在用户态代码完成,实在搞不定,再使用mutex
    • 少量的内核用户态切换
    • 不太容易引发线程的调度
  • 重量级锁:加锁解锁,过程更慢更低效,加锁机制重度依赖OS提供的mutex
    • 大量的内核用户态切换
    • 很容易引发线程的调度

3. 自旋锁VS挂机等待锁

自旋锁(轻量级锁的一种典型实现)

  • 按照之前的方式,线程在抢锁失败后进入阻塞状态,放弃CPU,需要很久才能再次调度,然后实际上,在大多数情况下,虽然抢锁失败,但过不了多久,锁就会被释放。所以就没必要放弃CPU,这个时候就可以使用自旋锁来处理这种情况。
  • 优点:如果获取锁失败后,就会一直尝试获取,无限循环,一直等到获取锁位置!一旦锁被其他线程释放,就会第一时间获取到锁!
  • 缺点:会造成忙等,消耗CPU资源

挂起等待锁(重量级锁的一种典型实现)

  • 挂起等待锁,当抢锁失败后,就放弃了CPU,进入到等待队列,这个时候如果其他线程释放锁之后,就会被唤醒,但是不能第一时间拿到资源的,可能就会需要很久才能获取到锁!

举两个例子:
例子1(自旋锁):假设你向女神表白,但是你被发了好人卡~,但是你仍旧每天锲而不舍的,向女神发送早安晚安,一旦哪一天,女神和男朋友分手了,此时你就有很大机会,把锁给加上!
例子2(挂起等待所):表白失败后,你就不找女神了,或许在女神分手后某一天,想到了你,就会主动来找你,这个时候你的机会也来了,但是不一样你就是找的第一个~

针对上述三组策略:提出个疑问? synchronized 属于哪一种
synchronized 既是悲观锁,也是乐观锁;既是轻量级锁,也是重量级锁;轻量级锁基于自旋锁实现,重量级锁基于挂起等待锁实现~
具体synchronized 会根据当前锁竞争的激烈程度,自适应

4. 互斥锁VS读写锁

synchronized就是互斥锁,加锁就是单纯的加锁,没有更细化的区分。
但是读写锁,能够把 读 和 写 两种加锁区分开来
在多线程中,数据的读取不会产生线程安全的问题。但是数据的写入互相之间和读之间都需要进行互斥。 此时考虑到一个问题,如果两种情况下都使用同一个锁,就会产生极大的性能消耗,所以读写锁就产生了!

一个线程对于数据的访问,就要是两种:读操作和写操作

  • 两个线程都只是进行读操作,没有线程安全问题
  • 两个线程都要进行写操作,就会有线程安全问题
  • 一个线程读另一个线程写,就会有线程安全问题

所以针对上面读和写操作区别,Java标准库提供了 ReentrantReadWriteLock 类,实现了读写锁

  • ReentrantReadWriteLock.ReadLock 类 表示 一个读锁,这个对象提供了 lock/ unlock 进行加锁解锁
  • ReentrantReadWriteLock.WriteLock 类 表示 一个写锁,这个对象也提供了 lock/unlock 方法进行加锁解锁

其中:

  • 读加锁和读加锁之间,不互斥
  • 读加锁和写加锁之间,互斥
  • 写加锁和写加锁之间,互斥

读写锁特别适合 “频繁都,不频繁写” 的场景中

5. 可重入锁VS不可重入锁

可重入锁:就是允许同一个线程多次获取同一把锁
不可重入锁:一个线程,针对同一把锁,连续加锁两次,发生死锁

Object locker = new Object();
synchronized(locker){
	synchronized(locker){
		// ...	
	}
}

上述代码中,不会产生死锁,因为 synchronized 是可重入锁!在加锁的时候,判断当前申请锁的线程是不是已经是锁的拥有者,如果是,就直接放行了~


下面介绍一些死锁的情况:

  • 1.一个线程,一把锁,可重入锁没有事,不可重入锁就会出现死锁
Object locker = new Object();
synchronized(locker){
	synchronized(locker){
		// ...	
	}
}

  • 2.两个线程,两把锁,即使是可重入锁,也会死锁
Object locker1 = new Object();
Object locker2 = new Object();

// t1 线程
synchronized(locker1){
	synchronized(locker2){
		//...
	}
}

// t2 线程
synchronized(locker2){
	synchronized(locker1){
		//...
	}
}

上述代码,t1 线程获取 locker1 锁 ,t2 获取 locker2 锁,此时 t1要获取locker2锁时获取不到,要等t2释放以后,而t2要获取 locker1锁 之后,也必须等 t1 释放 locker1,此时就造成了死锁~~


  • 3.N个线程,M把锁
    这里就有个经典的哲学家,就餐问题
    在这里插入图片描述

说到这里,我们介绍下产生死锁的四个必要条件:

  • 互斥使用:一个线程拿到一把锁之后,另一个线程不能使用
  • 不可抢占:一个线程拿到锁,只能自己主动释放,不能被其他线程强行占用
  • 请求和保持:一个线程在拿到锁之后,不释放,同时又在申请获取其他的锁
  • 循环等待:上述例子,逻辑依赖循环的

如何避免死锁~只要破除其中的任意一个条件就行,这里介绍 破坏循环等待这个条件!

  • 就是针对锁进行编号,如果需要同时获取多把锁,约定加锁顺序,务必是先对小的编号进行加锁,后对大的编号加锁!

在这里插入图片描述

// 伪代码
Object locker1 = new Object();
Object locker2 = new Object();

// 线程A执行下面代码逻辑
synchronized(locker1){
	synchronized(locker2){
		//...
	}
}

// 线程B执行下面代码逻辑
synchronized(locker1){
	synchronized(locker2){
		//...
	}
}

此时就不会造成循环等待现象~

6. 公平锁VS非公平锁

假设有三个线程对一个数据进行操作,线程A先获得锁,线程B也在申请获取锁,线程C也在申请获取锁。线程B在线程C之前申请获取锁

  • 公平锁:就是当线程A释放锁之后,线程B先线程C之前申请获取锁,此时就是线程B先获得,然后等线程B释放以后,线程C再获得锁
  • 非公平锁:就是当线程A释放锁,线程B和线程C获取锁的先后 ,是不确定的,两个可能先于对方

7. synchronized

系统对于线程的调度是随机的,自带的synchronized这个锁是非公平的~
要想实现公平锁需要在 synchronized 的基础上,加上队列,来记录这些加锁线程的顺序

synchronized的特点:

  • 既是悲观锁,也是乐观锁
  • 既是轻量级锁,也是重量级锁
  • 轻量级锁基于自旋锁实现,重量级锁基于挂机等待锁
  • 不是读写锁
  • 是可重入锁
  • 是非公平锁
  • 是互斥锁
  • 7
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

一个想打拳的程序员

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

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

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

打赏作者

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

抵扣说明:

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

余额充值